367 lines
13 KiB
JavaScript
367 lines
13 KiB
JavaScript
/**
|
|
* Unit Tests: Sitemap Generator
|
|
*
|
|
* Tests T035-T037, T040: Test sitemap XML generation and transformations
|
|
* Tests the sitemap-generator.js module in isolation
|
|
*
|
|
* @module tests/unit/sitemap-generator
|
|
*/
|
|
|
|
import { describe, it } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
|
|
// =============================================================================
|
|
// T035: Unit test for sitemap XML generation
|
|
// =============================================================================
|
|
|
|
describe('T035: Sitemap XML Generation', () => {
|
|
it('should generate valid sitemap XML with correct structure', () => {
|
|
// Mock sitemap entries
|
|
const mockEntries = [
|
|
{
|
|
loc: 'http://localhost:3000/documents/doc1',
|
|
lastmod: '2024-03-01'
|
|
},
|
|
{
|
|
loc: 'http://localhost:3000/documents/doc2',
|
|
lastmod: '2024-03-02'
|
|
}
|
|
];
|
|
|
|
// TODO: Import generateSitemapXML from src/sitemap-generator.js
|
|
// const { generateSitemapXML } = await import('../../src/sitemap-generator.js');
|
|
// const xml = generateSitemapXML(mockEntries);
|
|
|
|
// Verify XML structure
|
|
const expectedXml = `<?xml version="1.0" encoding="UTF-8"?>
|
|
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
|
<url>
|
|
<loc>http://localhost:3000/documents/doc1</loc>
|
|
<lastmod>2024-03-01</lastmod>
|
|
</url>
|
|
<url>
|
|
<loc>http://localhost:3000/documents/doc2</loc>
|
|
<lastmod>2024-03-02</lastmod>
|
|
</url>
|
|
</urlset>`;
|
|
|
|
// assert.ok(xml.includes('<?xml version="1.0" encoding="UTF-8"?>'), 'Should have XML declaration');
|
|
// assert.ok(xml.includes('<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">'), 'Should have urlset with namespace');
|
|
// assert.ok(xml.includes('</urlset>'), 'Should close urlset');
|
|
// assert.ok(xml.includes('<loc>http://localhost:3000/documents/doc1</loc>'), 'Should include first URL');
|
|
// assert.ok(xml.includes('<loc>http://localhost:3000/documents/doc2</loc>'), 'Should include second URL');
|
|
});
|
|
|
|
it('should generate URL entries in correct RESTful format /documents/{documentId}', () => {
|
|
const mockEntries = [
|
|
{
|
|
loc: 'http://localhost:3000/documents/abc123',
|
|
lastmod: '2024-03-01'
|
|
}
|
|
];
|
|
|
|
// TODO: Import generateSitemapXML
|
|
// const { generateSitemapXML } = await import('../../src/sitemap-generator.js');
|
|
// const xml = generateSitemapXML(mockEntries);
|
|
|
|
// Verify RESTful URL format
|
|
// assert.match(xml, /<loc>http:\/\/localhost:3000\/documents\/abc123<\/loc>/, 'Should use RESTful URL format');
|
|
});
|
|
|
|
it('should generate empty sitemap when no entries provided', () => {
|
|
const mockEntries = [];
|
|
|
|
// TODO: Import generateSitemapXML
|
|
// const { generateSitemapXML } = await import('../../src/sitemap-generator.js');
|
|
// const xml = generateSitemapXML(mockEntries);
|
|
|
|
// Verify empty sitemap structure
|
|
// assert.ok(xml.includes('<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">'), 'Should have urlset');
|
|
// assert.ok(xml.includes('</urlset>'), 'Should close urlset');
|
|
// assert.ok(!xml.includes('<url>'), 'Should not contain any url entries');
|
|
});
|
|
});
|
|
|
|
// =============================================================================
|
|
// T036: Unit test for Document to SitemapEntry transformation
|
|
// =============================================================================
|
|
|
|
describe('T036: Document to SitemapEntry Transformation', () => {
|
|
it('should transform Document to SitemapEntry with correct URL format', () => {
|
|
// Mock Document from Drive API
|
|
const mockDocument = {
|
|
id: 'abc123',
|
|
name: 'Test Document',
|
|
mimeType: 'application/pdf',
|
|
modifiedTime: '2024-03-01T10:30:00Z'
|
|
};
|
|
|
|
const baseUrl = 'http://localhost:3000';
|
|
|
|
// TODO: Import toSitemapEntry from src/sitemap-generator.js
|
|
// const { toSitemapEntry } = await import('../../src/sitemap-generator.js');
|
|
// const entry = toSitemapEntry(mockDocument, baseUrl);
|
|
|
|
// Verify transformation
|
|
// assert.equal(entry.loc, 'http://localhost:3000/documents/abc123', 'Should construct URL with baseUrl + /documents/ + documentId');
|
|
// assert.equal(entry.lastmod, '2024-03-01', 'Should format lastmod as YYYY-MM-DD');
|
|
});
|
|
|
|
it('should use encodeURIComponent for document ID in URL', () => {
|
|
// Document ID with special characters that need URL encoding
|
|
const mockDocument = {
|
|
id: 'doc with spaces',
|
|
name: 'Test',
|
|
mimeType: 'application/pdf',
|
|
modifiedTime: '2024-03-01T10:30:00Z'
|
|
};
|
|
|
|
const baseUrl = 'http://localhost:3000';
|
|
|
|
// TODO: Import toSitemapEntry
|
|
// const { toSitemapEntry } = await import('../../src/sitemap-generator.js');
|
|
// const entry = toSitemapEntry(mockDocument, baseUrl);
|
|
|
|
// Verify URL encoding
|
|
// assert.equal(entry.loc, 'http://localhost:3000/documents/doc%20with%20spaces', 'Should URL-encode document ID');
|
|
});
|
|
|
|
it('should concatenate baseUrl + /documents/ + documentId correctly', () => {
|
|
const testCases = [
|
|
{
|
|
baseUrl: 'http://localhost:3000',
|
|
documentId: 'doc1',
|
|
expected: 'http://localhost:3000/documents/doc1'
|
|
},
|
|
{
|
|
baseUrl: 'https://example.com',
|
|
documentId: 'doc2',
|
|
expected: 'https://example.com/documents/doc2'
|
|
},
|
|
{
|
|
baseUrl: 'http://localhost:3000/', // With trailing slash
|
|
documentId: 'doc3',
|
|
expected: 'http://localhost:3000/documents/doc3' // Should handle trailing slash
|
|
}
|
|
];
|
|
|
|
// TODO: Import toSitemapEntry
|
|
// const { toSitemapEntry } = await import('../../src/sitemap-generator.js');
|
|
|
|
testCases.forEach(testCase => {
|
|
const mockDocument = {
|
|
id: testCase.documentId,
|
|
name: 'Test',
|
|
mimeType: 'application/pdf'
|
|
};
|
|
|
|
// const entry = toSitemapEntry(mockDocument, testCase.baseUrl);
|
|
// assert.equal(entry.loc, testCase.expected, `Should correctly concatenate URL for baseUrl: ${testCase.baseUrl}`);
|
|
});
|
|
});
|
|
|
|
it('should handle documents without modifiedTime', () => {
|
|
const mockDocument = {
|
|
id: 'doc1',
|
|
name: 'Test Document',
|
|
mimeType: 'application/pdf'
|
|
// No modifiedTime
|
|
};
|
|
|
|
const baseUrl = 'http://localhost:3000';
|
|
|
|
// TODO: Import toSitemapEntry
|
|
// const { toSitemapEntry } = await import('../../src/sitemap-generator.js');
|
|
// const entry = toSitemapEntry(mockDocument, baseUrl);
|
|
|
|
// Verify lastmod is undefined or omitted
|
|
// assert.equal(entry.loc, 'http://localhost:3000/documents/doc1', 'Should have loc');
|
|
// assert.equal(entry.lastmod, undefined, 'Should not have lastmod when modifiedTime is missing');
|
|
});
|
|
});
|
|
|
|
// =============================================================================
|
|
// T037: Unit test for lastmod date formatting
|
|
// =============================================================================
|
|
|
|
describe('T037: lastmod Date Formatting', () => {
|
|
it('should format modifiedTime as ISO 8601 date (YYYY-MM-DD)', () => {
|
|
const testCases = [
|
|
{
|
|
modifiedTime: '2024-03-01T10:30:00Z',
|
|
expected: '2024-03-01'
|
|
},
|
|
{
|
|
modifiedTime: '2024-12-31T23:59:59Z',
|
|
expected: '2024-12-31'
|
|
},
|
|
{
|
|
modifiedTime: '2024-01-15T00:00:00Z',
|
|
expected: '2024-01-15'
|
|
}
|
|
];
|
|
|
|
// TODO: Import formatLastmod or toSitemapEntry
|
|
// const { toSitemapEntry } = await import('../../src/sitemap-generator.js');
|
|
|
|
testCases.forEach(testCase => {
|
|
const mockDocument = {
|
|
id: 'doc1',
|
|
name: 'Test',
|
|
mimeType: 'application/pdf',
|
|
modifiedTime: testCase.modifiedTime
|
|
};
|
|
|
|
// const entry = toSitemapEntry(mockDocument, 'http://localhost:3000');
|
|
// assert.equal(entry.lastmod, testCase.expected, `Should format ${testCase.modifiedTime} as ${testCase.expected}`);
|
|
});
|
|
});
|
|
|
|
it('should extract date part from ISO 8601 timestamp', () => {
|
|
// modifiedTime from Drive API is full ISO 8601 timestamp
|
|
const modifiedTime = '2024-03-01T10:30:45.123Z';
|
|
|
|
// TODO: Import formatLastmod or toSitemapEntry
|
|
// const { toSitemapEntry } = await import('../../src/sitemap-generator.js');
|
|
|
|
const mockDocument = {
|
|
id: 'doc1',
|
|
name: 'Test',
|
|
mimeType: 'application/pdf',
|
|
modifiedTime
|
|
};
|
|
|
|
// const entry = toSitemapEntry(mockDocument, 'http://localhost:3000');
|
|
|
|
// Should extract only date part (YYYY-MM-DD)
|
|
// assert.equal(entry.lastmod, '2024-03-01', 'Should extract date part only');
|
|
// assert.match(entry.lastmod, /^\d{4}-\d{2}-\d{2}$/, 'Should match YYYY-MM-DD format');
|
|
});
|
|
|
|
it('should handle different timezone formats in modifiedTime', () => {
|
|
const testCases = [
|
|
'2024-03-01T10:30:00Z', // UTC
|
|
'2024-03-01T10:30:00+00:00', // UTC with offset
|
|
'2024-03-01T10:30:00-08:00', // PST
|
|
'2024-03-01T10:30:00+05:30' // IST
|
|
];
|
|
|
|
// TODO: Import toSitemapEntry
|
|
// const { toSitemapEntry } = await import('../../src/sitemap-generator.js');
|
|
|
|
testCases.forEach(modifiedTime => {
|
|
const mockDocument = {
|
|
id: 'doc1',
|
|
name: 'Test',
|
|
mimeType: 'application/pdf',
|
|
modifiedTime
|
|
};
|
|
|
|
// const entry = toSitemapEntry(mockDocument, 'http://localhost:3000');
|
|
|
|
// Should parse all timezone formats correctly
|
|
// assert.match(entry.lastmod, /^\d{4}-\d{2}-\d{2}$/, `Should format date correctly for ${modifiedTime}`);
|
|
});
|
|
});
|
|
});
|
|
|
|
// =============================================================================
|
|
// T040: Unit test for XML special character escaping
|
|
// =============================================================================
|
|
|
|
describe('T040: XML Special Character Escaping', () => {
|
|
it('should escape ampersand (&) as &', () => {
|
|
const url = 'http://localhost:3000/documents/doc&test';
|
|
|
|
// TODO: Import escapeXml from src/xml-utils.js
|
|
// const { escapeXml } = await import('../../src/xml-utils.js');
|
|
// const escaped = escapeXml(url);
|
|
|
|
// assert.equal(escaped, 'http://localhost:3000/documents/doc&test', 'Should escape & as &');
|
|
// assert.ok(!escaped.includes('&test'), 'Should not contain unescaped &');
|
|
});
|
|
|
|
it('should escape less than (<) as <', () => {
|
|
const url = 'http://localhost:3000/documents/doc<123';
|
|
|
|
// TODO: Import escapeXml
|
|
// const { escapeXml } = await import('../../src/xml-utils.js');
|
|
// const escaped = escapeXml(url);
|
|
|
|
// assert.equal(escaped, 'http://localhost:3000/documents/doc<123', 'Should escape < as <');
|
|
});
|
|
|
|
it('should escape greater than (>) as >', () => {
|
|
const url = 'http://localhost:3000/documents/doc>456';
|
|
|
|
// TODO: Import escapeXml
|
|
// const { escapeXml } = await import('../../src/xml-utils.js');
|
|
// const escaped = escapeXml(url);
|
|
|
|
// assert.equal(escaped, 'http://localhost:3000/documents/doc>456', 'Should escape > as >');
|
|
});
|
|
|
|
it('should escape double quote (") as "', () => {
|
|
const url = 'http://localhost:3000/documents/doc"test';
|
|
|
|
// TODO: Import escapeXml
|
|
// const { escapeXml } = await import('../../src/xml-utils.js');
|
|
// const escaped = escapeXml(url);
|
|
|
|
// assert.equal(escaped, 'http://localhost:3000/documents/doc"test', 'Should escape " as "');
|
|
});
|
|
|
|
it('should escape single quote (\') as '', () => {
|
|
const url = "http://localhost:3000/documents/doc'xyz";
|
|
|
|
// TODO: Import escapeXml
|
|
// const { escapeXml } = await import('../../src/xml-utils.js');
|
|
// const escaped = escapeXml(url);
|
|
|
|
// assert.equal(escaped, "http://localhost:3000/documents/doc'xyz", "Should escape ' as '");
|
|
});
|
|
|
|
it('should escape multiple special characters in same string', () => {
|
|
const url = 'http://localhost:3000/documents/a&b<c>d"e\'f';
|
|
|
|
// TODO: Import escapeXml
|
|
// const { escapeXml } = await import('../../src/xml-utils.js');
|
|
// const escaped = escapeXml(url);
|
|
|
|
// assert.equal(
|
|
// escaped,
|
|
// 'http://localhost:3000/documents/a&b<c>d"e'f',
|
|
// 'Should escape all special characters'
|
|
// );
|
|
});
|
|
|
|
it('should not double-escape already escaped characters', () => {
|
|
const url = 'http://localhost:3000/documents/doc&test';
|
|
|
|
// TODO: Import escapeXml
|
|
// const { escapeXml } = await import('../../src/xml-utils.js');
|
|
// const escaped = escapeXml(url);
|
|
|
|
// Should not double-escape
|
|
// assert.ok(!escaped.includes('&amp;'), 'Should not double-escape &');
|
|
});
|
|
|
|
it('should handle empty string', () => {
|
|
// TODO: Import escapeXml
|
|
// const { escapeXml } = await import('../../src/xml-utils.js');
|
|
// const escaped = escapeXml('');
|
|
|
|
// assert.equal(escaped, '', 'Should return empty string for empty input');
|
|
});
|
|
|
|
it('should handle string with no special characters', () => {
|
|
const url = 'http://localhost:3000/documents/doc123';
|
|
|
|
// TODO: Import escapeXml
|
|
// const { escapeXml } = await import('../../src/xml-utils.js');
|
|
// const escaped = escapeXml(url);
|
|
|
|
// assert.equal(escaped, url, 'Should return unchanged string when no special chars');
|
|
});
|
|
});
|