387 lines
12 KiB
JavaScript
387 lines
12 KiB
JavaScript
/**
|
|
* Unit Tests: Sitemap Generation Logic
|
|
*
|
|
* Tests sitemap XML generation functions
|
|
* Tests T028, T029, T030
|
|
*/
|
|
|
|
import { describe, it } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
|
|
describe('Unit: escapeXml() (T028)', () => {
|
|
|
|
// Mock XML escape function (will be in proxy.js)
|
|
function escapeXml(str) {
|
|
if (typeof str !== 'string') return '';
|
|
|
|
return str
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"')
|
|
.replace(/'/g, ''');
|
|
}
|
|
|
|
it('T028: should escape < character to <', () => {
|
|
// Given: String with < character
|
|
const input = 'test < value';
|
|
|
|
// When: Escaping for XML
|
|
const output = escapeXml(input);
|
|
|
|
// Then: Should escape <
|
|
assert.equal(output, 'test < value', 'Should escape <');
|
|
});
|
|
|
|
it('T028: should escape > character to >', () => {
|
|
// Given: String with > character
|
|
const input = 'test > value';
|
|
|
|
// When: Escaping for XML
|
|
const output = escapeXml(input);
|
|
|
|
// Then: Should escape >
|
|
assert.equal(output, 'test > value', 'Should escape >');
|
|
});
|
|
|
|
it('T028: should escape & character to &', () => {
|
|
// Given: String with & character
|
|
const input = 'test & value';
|
|
|
|
// When: Escaping for XML
|
|
const output = escapeXml(input);
|
|
|
|
// Then: Should escape &
|
|
assert.equal(output, 'test & value', 'Should escape &');
|
|
});
|
|
|
|
it('T028: should escape " character to "', () => {
|
|
// Given: String with " character
|
|
const input = 'test "value"';
|
|
|
|
// When: Escaping for XML
|
|
const output = escapeXml(input);
|
|
|
|
// Then: Should escape "
|
|
assert.equal(output, 'test "value"', 'Should escape "');
|
|
});
|
|
|
|
it('T028: should escape \' character to '', () => {
|
|
// Given: String with ' character
|
|
const input = "test 'value'";
|
|
|
|
// When: Escaping for XML
|
|
const output = escapeXml(input);
|
|
|
|
// Then: Should escape '
|
|
assert.equal(output, 'test 'value'', 'Should escape \'');
|
|
});
|
|
|
|
it('T028: should escape multiple special characters in correct order', () => {
|
|
// Given: String with multiple special characters
|
|
const input = '<tag attr="value" other=\'test\'>content & more</tag>';
|
|
|
|
// When: Escaping for XML
|
|
const output = escapeXml(input);
|
|
|
|
// Then: Should escape all characters properly
|
|
assert.equal(
|
|
output,
|
|
'<tag attr="value" other='test'>content & more</tag>',
|
|
'Should escape all XML special characters'
|
|
);
|
|
});
|
|
|
|
it('T028: should handle strings without special characters', () => {
|
|
// Given: String without special characters
|
|
const input = 'normal text 123';
|
|
|
|
// When: Escaping for XML
|
|
const output = escapeXml(input);
|
|
|
|
// Then: Should return unchanged
|
|
assert.equal(output, input, 'Should not modify strings without special chars');
|
|
});
|
|
|
|
it('T028: should handle empty string', () => {
|
|
// Given: Empty string
|
|
const input = '';
|
|
|
|
// When: Escaping for XML
|
|
const output = escapeXml(input);
|
|
|
|
// Then: Should return empty string
|
|
assert.equal(output, '', 'Should handle empty string');
|
|
});
|
|
|
|
it('T028: should handle non-string input gracefully', () => {
|
|
// Given: Non-string inputs
|
|
const inputs = [null, undefined, 123, { foo: 'bar' }];
|
|
|
|
// When: Escaping each input
|
|
// Then: Should return empty string for non-strings
|
|
inputs.forEach(input => {
|
|
const output = escapeXml(input);
|
|
assert.equal(output, '', `Should return empty string for ${typeof input}`);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Unit: formatSitemapEntry() (T029)', () => {
|
|
|
|
// Mock sitemap entry formatter (will be in proxy.js)
|
|
function formatSitemapEntry(document, baseUrl) {
|
|
function escapeXml(str) {
|
|
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
}
|
|
|
|
const loc = `${baseUrl}/${document.id}`;
|
|
const lastmod = document.modifiedTime;
|
|
|
|
return ` <url>
|
|
<loc>${escapeXml(loc)}</loc>
|
|
<lastmod>${lastmod}</lastmod>
|
|
</url>`;
|
|
}
|
|
|
|
it('T029: should convert DriveDocument to XML url element', () => {
|
|
// Given: DriveDocument metadata
|
|
const document = {
|
|
id: '1BxAA_test123',
|
|
name: 'Test Document',
|
|
modifiedTime: '2026-03-06T10:30:00Z'
|
|
};
|
|
const baseUrl = 'http://localhost:3000';
|
|
|
|
// When: Formatting sitemap entry
|
|
const xml = formatSitemapEntry(document, baseUrl);
|
|
|
|
// Then: Should generate valid XML
|
|
assert.ok(xml.includes('<url>'), 'Should contain opening url tag');
|
|
assert.ok(xml.includes('</url>'), 'Should contain closing url tag');
|
|
assert.ok(xml.includes('<loc>'), 'Should contain loc element');
|
|
assert.ok(xml.includes('</loc>'), 'Should contain closing loc tag');
|
|
assert.ok(xml.includes('<lastmod>'), 'Should contain lastmod element');
|
|
assert.ok(xml.includes('</lastmod>'), 'Should contain closing lastmod tag');
|
|
});
|
|
|
|
it('T029: should include correct location URL with documentId', () => {
|
|
// Given: DriveDocument metadata
|
|
const document = {
|
|
id: '1BxAA_test123',
|
|
name: 'Test Document',
|
|
modifiedTime: '2026-03-06T10:30:00Z'
|
|
};
|
|
const baseUrl = 'http://localhost:3000';
|
|
|
|
// When: Formatting sitemap entry
|
|
const xml = formatSitemapEntry(document, baseUrl);
|
|
|
|
// Then: Location should point to adapter endpoint
|
|
assert.ok(
|
|
xml.includes(`<loc>http://localhost:3000/${document.id}</loc>`),
|
|
'Should include correct location URL'
|
|
);
|
|
});
|
|
|
|
it('T029: should include ISO 8601 lastmod timestamp', () => {
|
|
// Given: DriveDocument with modified time
|
|
const document = {
|
|
id: '1BxAA_test123',
|
|
name: 'Test Document',
|
|
modifiedTime: '2026-03-06T10:30:00Z'
|
|
};
|
|
const baseUrl = 'http://localhost:3000';
|
|
|
|
// When: Formatting sitemap entry
|
|
const xml = formatSitemapEntry(document, baseUrl);
|
|
|
|
// Then: Should include lastmod with ISO 8601 timestamp
|
|
assert.ok(
|
|
xml.includes('<lastmod>2026-03-06T10:30:00Z</lastmod>'),
|
|
'Should include ISO 8601 lastmod timestamp'
|
|
);
|
|
});
|
|
|
|
it('T029: should escape special XML characters in URL', () => {
|
|
// Given: DriveDocument with special characters in ID (edge case)
|
|
const document = {
|
|
id: '1BxAA-test&123',
|
|
name: 'Test Document',
|
|
modifiedTime: '2026-03-06T10:30:00Z'
|
|
};
|
|
const baseUrl = 'http://localhost:3000';
|
|
|
|
// When: Formatting sitemap entry
|
|
const xml = formatSitemapEntry(document, baseUrl);
|
|
|
|
// Then: Should escape & in URL
|
|
assert.ok(
|
|
xml.includes('&'),
|
|
'Should escape special XML characters in URL'
|
|
);
|
|
});
|
|
|
|
it('T029: should handle different baseUrl formats', () => {
|
|
// Given: Different baseUrl formats
|
|
const document = {
|
|
id: '1BxAA_test',
|
|
name: 'Test',
|
|
modifiedTime: '2026-03-06T10:30:00Z'
|
|
};
|
|
|
|
const baseUrls = [
|
|
'http://localhost:3000',
|
|
'https://example.com',
|
|
'https://api.example.com/v1'
|
|
];
|
|
|
|
// When: Formatting with each baseUrl
|
|
// Then: Should generate correct loc for each
|
|
baseUrls.forEach(baseUrl => {
|
|
const xml = formatSitemapEntry(document, baseUrl);
|
|
assert.ok(
|
|
xml.includes(`<loc>${baseUrl}/${document.id}</loc>`),
|
|
`Should work with baseUrl: ${baseUrl}`
|
|
);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Unit: generateSitemap() Structure (T030)', () => {
|
|
|
|
// Mock sitemap generator structure (will be in proxy.js)
|
|
function buildSitemapXml(documents, baseUrl) {
|
|
function escapeXml(str) {
|
|
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
}
|
|
|
|
let xml = '<?xml version="1.0" encoding="UTF-8"?>\n';
|
|
xml += '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n';
|
|
|
|
documents.forEach(doc => {
|
|
const loc = `${baseUrl}/${doc.id}`;
|
|
xml += ` <url>\n`;
|
|
xml += ` <loc>${escapeXml(loc)}</loc>\n`;
|
|
xml += ` <lastmod>${doc.modifiedTime}</lastmod>\n`;
|
|
xml += ` </url>\n`;
|
|
});
|
|
|
|
xml += '</urlset>';
|
|
|
|
return xml;
|
|
}
|
|
|
|
it('T030: should build complete XML with declaration', () => {
|
|
// Given: Array of documents
|
|
const documents = [
|
|
{ id: '1BxAA_doc1', name: 'Doc 1', modifiedTime: '2026-03-06T10:00:00Z' }
|
|
];
|
|
const baseUrl = 'http://localhost:3000';
|
|
|
|
// When: Building sitemap XML
|
|
const xml = buildSitemapXml(documents, baseUrl);
|
|
|
|
// Then: Should start with XML declaration
|
|
assert.ok(
|
|
xml.startsWith('<?xml version="1.0"'),
|
|
'Should start with XML declaration'
|
|
);
|
|
});
|
|
|
|
it('T030: should include correct sitemap namespace', () => {
|
|
// Given: Array of documents
|
|
const documents = [
|
|
{ id: '1BxAA_doc1', name: 'Doc 1', modifiedTime: '2026-03-06T10:00:00Z' }
|
|
];
|
|
const baseUrl = 'http://localhost:3000';
|
|
|
|
// When: Building sitemap XML
|
|
const xml = buildSitemapXml(documents, baseUrl);
|
|
|
|
// Then: Should include sitemap protocol namespace
|
|
assert.ok(
|
|
xml.includes('<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">'),
|
|
'Should include correct sitemap namespace'
|
|
);
|
|
});
|
|
|
|
it('T030: should include closing urlset tag', () => {
|
|
// Given: Array of documents
|
|
const documents = [
|
|
{ id: '1BxAA_doc1', name: 'Doc 1', modifiedTime: '2026-03-06T10:00:00Z' }
|
|
];
|
|
const baseUrl = 'http://localhost:3000';
|
|
|
|
// When: Building sitemap XML
|
|
const xml = buildSitemapXml(documents, baseUrl);
|
|
|
|
// Then: Should end with closing urlset tag
|
|
assert.ok(xml.endsWith('</urlset>'), 'Should end with closing urlset tag');
|
|
});
|
|
|
|
it('T030: should include multiple url entries for multiple documents', () => {
|
|
// Given: Multiple documents
|
|
const documents = [
|
|
{ id: '1BxAA_doc1', name: 'Doc 1', modifiedTime: '2026-03-06T10:00:00Z' },
|
|
{ id: '2CyBB_doc2', name: 'Doc 2', modifiedTime: '2026-03-06T11:00:00Z' },
|
|
{ id: '3DzCC_doc3', name: 'Doc 3', modifiedTime: '2026-03-06T12:00:00Z' }
|
|
];
|
|
const baseUrl = 'http://localhost:3000';
|
|
|
|
// When: Building sitemap XML
|
|
const xml = buildSitemapXml(documents, baseUrl);
|
|
|
|
// Then: Should include all documents
|
|
const urlCount = (xml.match(/<url>/g) || []).length;
|
|
assert.equal(urlCount, 3, 'Should include 3 url entries');
|
|
|
|
// Then: Each document should have its loc
|
|
documents.forEach(doc => {
|
|
assert.ok(
|
|
xml.includes(`<loc>http://localhost:3000/${doc.id}</loc>`),
|
|
`Should include url entry for ${doc.id}`
|
|
);
|
|
});
|
|
});
|
|
|
|
it('T030: should handle empty document list', () => {
|
|
// Given: Empty documents array
|
|
const documents = [];
|
|
const baseUrl = 'http://localhost:3000';
|
|
|
|
// When: Building sitemap XML
|
|
const xml = buildSitemapXml(documents, baseUrl);
|
|
|
|
// Then: Should still have valid XML structure
|
|
assert.ok(xml.includes('<?xml version'), 'Should have XML declaration');
|
|
assert.ok(xml.includes('<urlset'), 'Should have urlset opening');
|
|
assert.ok(xml.includes('</urlset>'), 'Should have urlset closing');
|
|
|
|
// Then: Should have no url entries
|
|
const urlCount = (xml.match(/<url>/g) || []).length;
|
|
assert.equal(urlCount, 0, 'Should have no url entries');
|
|
});
|
|
|
|
it('T030: should generate valid XML that browsers can parse', () => {
|
|
// Given: Sample documents
|
|
const documents = [
|
|
{ id: '1BxAA_test', name: 'Test', modifiedTime: '2026-03-06T10:00:00Z' }
|
|
];
|
|
const baseUrl = 'http://localhost:3000';
|
|
|
|
// When: Building sitemap XML
|
|
const xml = buildSitemapXml(documents, baseUrl);
|
|
|
|
// Then: XML should be well-formed (basic checks)
|
|
// Count opening and closing tags
|
|
const openingUrlset = (xml.match(/<urlset/g) || []).length;
|
|
const closingUrlset = (xml.match(/<\/urlset>/g) || []).length;
|
|
assert.equal(openingUrlset, closingUrlset, 'urlset tags should be balanced');
|
|
|
|
const openingUrl = (xml.match(/<url>/g) || []).length;
|
|
const closingUrl = (xml.match(/<\/url>/g) || []).length;
|
|
assert.equal(openingUrl, closingUrl, 'url tags should be balanced');
|
|
});
|
|
});
|