/**
* Contract Tests: /sitemap.xml XML Schema Validation
*
* Tests T020-T023: Verify API contract compliance for sitemap endpoint
* Reference: specs/001-drive-proxy-adapter/contracts/sitemap-xml-schema.md
*
* @module tests/contract/sitemap-schema
*/
import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
// =============================================================================
// T020: Contract test for /sitemap.xml success response (200 OK)
// =============================================================================
describe('T020: /sitemap.xml Success Response Contract', () => {
it('should return 200 OK with valid XML structure', async () => {
// Mock response from sitemap endpoint
const mockResponse = {
statusCode: 200,
headers: {
'content-type': 'application/xml; charset=utf-8'
},
body: `
http://localhost:3000/documents/abc123
2024-03-01
`
};
// Verify status code
assert.equal(mockResponse.statusCode, 200, 'Status code must be 200');
// Verify Content-Type header
assert.equal(
mockResponse.headers['content-type'],
'application/xml; charset=utf-8',
'Content-Type must be application/xml; charset=utf-8'
);
// Verify XML structure
assert.match(mockResponse.body, /^<\?xml version="1\.0" encoding="UTF-8"\?>/, 'Must have XML declaration');
assert.match(mockResponse.body, //, 'Must have urlset with correct namespace');
assert.match(mockResponse.body, /<\/urlset>$/, 'Must close urlset tag');
// Verify URL entry structure
assert.match(mockResponse.body, //, 'Must contain url entries');
assert.match(mockResponse.body, /.*<\/loc>/, 'Each url must have loc element');
assert.match(mockResponse.body, /.*<\/lastmod>/, 'Each url should have lastmod element');
});
it('should return valid XML with RESTful URL format', async () => {
const mockResponse = {
statusCode: 200,
body: `
http://localhost:3000/documents/abc123
`
};
// Verify RESTful URL pattern: /documents/{documentId}
assert.match(
mockResponse.body,
/http:\/\/[^<]+\/documents\/[^<]+<\/loc>/,
'URLs must follow RESTful format /documents/{documentId}'
);
});
});
// =============================================================================
// T021: Contract test for /sitemap.xml with empty Drive (0 documents)
// =============================================================================
describe('T021: /sitemap.xml Empty Drive Response Contract', () => {
it('should return valid XML with empty urlset when no documents exist', async () => {
const mockResponse = {
statusCode: 200,
headers: {
'content-type': 'application/xml; charset=utf-8'
},
body: `
`
};
// Verify status code
assert.equal(mockResponse.statusCode, 200, 'Status code must be 200 even for empty Drive');
// Verify empty urlset is valid XML
assert.match(mockResponse.body, //, 'Must have urlset with namespace');
assert.match(mockResponse.body, /<\/urlset>/, 'Must close urlset tag');
// Verify no url entries
assert.doesNotMatch(mockResponse.body, //, 'Should not contain any url entries');
});
});
// =============================================================================
// T022: Contract test for XML special character escaping
// =============================================================================
describe('T022: XML Special Character Escaping Contract', () => {
it('should properly escape XML special characters in URLs', async () => {
// Document IDs can contain special characters that need escaping in XML
const mockResponse = {
statusCode: 200,
body: `
http://localhost:3000/documents/test&doc
http://localhost:3000/documents/doc<123
http://localhost:3000/documents/doc>456
http://localhost:3000/documents/doc"test
http://localhost:3000/documents/doc'xyz
`
};
// Verify special characters are escaped
assert.match(mockResponse.body, /&/, 'Ampersand (&) must be escaped as &');
assert.match(mockResponse.body, /</, 'Less than (<) must be escaped as <');
assert.match(mockResponse.body, />/, 'Greater than (>) must be escaped as >');
assert.match(mockResponse.body, /"/, 'Double quote (") must be escaped as "');
assert.match(mockResponse.body, /'/, 'Single quote (\') must be escaped as '');
// Verify unescaped special characters are NOT present in content
const locContent = mockResponse.body.match(/(.*?)<\/loc>/g);
assert.ok(locContent, 'Must have loc elements');
locContent.forEach(loc => {
const content = loc.replace(/<\/?loc>/g, '');
const afterProtocol = content.split('://')[1] || '';
// Only check the path/query part, not the protocol separator
if (afterProtocol.includes('/')) {
const pathPart = afterProtocol.substring(afterProtocol.indexOf('/'));
assert.doesNotMatch(pathPart, /[&<>"'](?!amp;|lt;|gt;|quot;|apos;)/, 'Unescaped special chars must not appear in XML content');
}
});
});
});
// =============================================================================
// T023: Contract test for lastmod date format validation
// =============================================================================
describe('T023: lastmod Date Format Contract', () => {
it('should format lastmod as ISO 8601 date (YYYY-MM-DD)', async () => {
const mockResponse = {
statusCode: 200,
body: `
http://localhost:3000/documents/doc1
2024-03-01
http://localhost:3000/documents/doc2
2024-12-31
`
};
// Extract lastmod values
const lastmodMatches = mockResponse.body.match(/(.*?)<\/lastmod>/g);
assert.ok(lastmodMatches, 'Must have lastmod elements');
assert.ok(lastmodMatches.length > 0, 'Must have at least one lastmod element');
// Verify each lastmod follows ISO 8601 date format (YYYY-MM-DD)
lastmodMatches.forEach(lastmodTag => {
const dateValue = lastmodTag.match(/(.*?)<\/lastmod>/)[1];
// Check format: YYYY-MM-DD
assert.match(dateValue, /^\d{4}-\d{2}-\d{2}$/, 'lastmod must be in YYYY-MM-DD format');
// Verify it's a valid date
const date = new Date(dateValue);
assert.ok(!isNaN(date.getTime()), 'lastmod must be a valid date');
// Verify date components
const [year, month, day] = dateValue.split('-').map(Number);
assert.ok(year >= 1000 && year <= 9999, 'Year must be 4 digits');
assert.ok(month >= 1 && month <= 12, 'Month must be 01-12');
assert.ok(day >= 1 && day <= 31, 'Day must be 01-31');
});
});
it('should accept full ISO 8601 datetime format if provided', async () => {
// Sitemap protocol also accepts full datetime with timezone
const mockResponse = {
statusCode: 200,
body: `
http://localhost:3000/documents/doc1
2024-03-01T10:30:00+00:00
`
};
const lastmodMatch = mockResponse.body.match(/(.*?)<\/lastmod>/);
assert.ok(lastmodMatch, 'Must have lastmod element');
const dateValue = lastmodMatch[1];
// Accept either YYYY-MM-DD or full ISO 8601 with timezone
const isValidFormat = /^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2}[+-]\d{2}:\d{2})?$/.test(dateValue);
assert.ok(isValidFormat, 'lastmod must be valid ISO 8601 format');
// Verify it's a valid date
const date = new Date(dateValue);
assert.ok(!isNaN(date.getTime()), 'lastmod must be a valid datetime');
});
});