Initial Version of sitemap.xml spec

This commit is contained in:
2026-03-06 23:34:00 -06:00
parent fec5bfa5c7
commit e9495f65b5
41 changed files with 10665 additions and 35 deletions

View File

@@ -0,0 +1,366 @@
/**
* 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 &amp;', () => {
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&amp;test', 'Should escape & as &amp;');
// assert.ok(!escaped.includes('&test'), 'Should not contain unescaped &');
});
it('should escape less than (<) as &lt;', () => {
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&lt;123', 'Should escape < as &lt;');
});
it('should escape greater than (>) as &gt;', () => {
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&gt;456', 'Should escape > as &gt;');
});
it('should escape double quote (") as &quot;', () => {
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&quot;test', 'Should escape " as &quot;');
});
it('should escape single quote (\') as &apos;', () => {
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&apos;xyz", "Should escape ' as &apos;");
});
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&amp;b&lt;c&gt;d&quot;e&apos;f',
// 'Should escape all special characters'
// );
});
it('should not double-escape already escaped characters', () => {
const url = 'http://localhost:3000/documents/doc&amp;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;amp;'), 'Should not double-escape &amp;');
});
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');
});
});