Initial Version of sitemap.xml spec
This commit is contained in:
227
tests/contract/sitemap-schema.test.js
Normal file
227
tests/contract/sitemap-schema.test.js
Normal file
@@ -0,0 +1,227 @@
|
||||
/**
|
||||
* 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: `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
<url>
|
||||
<loc>http://localhost:3000/documents/abc123</loc>
|
||||
<lastmod>2024-03-01</lastmod>
|
||||
</url>
|
||||
</urlset>`
|
||||
};
|
||||
|
||||
// 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, /<urlset xmlns="http:\/\/www\.sitemaps\.org\/schemas\/sitemap\/0\.9">/, 'Must have urlset with correct namespace');
|
||||
assert.match(mockResponse.body, /<\/urlset>$/, 'Must close urlset tag');
|
||||
|
||||
// Verify URL entry structure
|
||||
assert.match(mockResponse.body, /<url>/, 'Must contain url entries');
|
||||
assert.match(mockResponse.body, /<loc>.*<\/loc>/, 'Each url must have loc element');
|
||||
assert.match(mockResponse.body, /<lastmod>.*<\/lastmod>/, 'Each url should have lastmod element');
|
||||
});
|
||||
|
||||
it('should return valid XML with RESTful URL format', async () => {
|
||||
const mockResponse = {
|
||||
statusCode: 200,
|
||||
body: `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
<url>
|
||||
<loc>http://localhost:3000/documents/abc123</loc>
|
||||
</url>
|
||||
</urlset>`
|
||||
};
|
||||
|
||||
// Verify RESTful URL pattern: /documents/{documentId}
|
||||
assert.match(
|
||||
mockResponse.body,
|
||||
/<loc>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: `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
</urlset>`
|
||||
};
|
||||
|
||||
// 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, /<urlset xmlns="http:\/\/www\.sitemaps\.org\/schemas\/sitemap\/0\.9">/, 'Must have urlset with namespace');
|
||||
assert.match(mockResponse.body, /<\/urlset>/, 'Must close urlset tag');
|
||||
|
||||
// Verify no url entries
|
||||
assert.doesNotMatch(mockResponse.body, /<url>/, '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: `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
<url>
|
||||
<loc>http://localhost:3000/documents/test&doc</loc>
|
||||
</url>
|
||||
<url>
|
||||
<loc>http://localhost:3000/documents/doc<123</loc>
|
||||
</url>
|
||||
<url>
|
||||
<loc>http://localhost:3000/documents/doc>456</loc>
|
||||
</url>
|
||||
<url>
|
||||
<loc>http://localhost:3000/documents/doc"test</loc>
|
||||
</url>
|
||||
<url>
|
||||
<loc>http://localhost:3000/documents/doc'xyz</loc>
|
||||
</url>
|
||||
</urlset>`
|
||||
};
|
||||
|
||||
// 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>(.*?)<\/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: `<?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-12-31</lastmod>
|
||||
</url>
|
||||
</urlset>`
|
||||
};
|
||||
|
||||
// Extract lastmod values
|
||||
const lastmodMatches = mockResponse.body.match(/<lastmod>(.*?)<\/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>(.*?)<\/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: `<?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-01T10:30:00+00:00</lastmod>
|
||||
</url>
|
||||
</urlset>`
|
||||
};
|
||||
|
||||
const lastmodMatch = mockResponse.body.match(/<lastmod>(.*?)<\/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');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user