/** * 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'); }); });