/** * Contract Tests: Document API * * Tests API contract compliance per OpenAPI specification * Tests T009, T010, T026, T037, T038, T039 */ import { describe, it, before, after } from 'node:test'; import assert from 'node:assert/strict'; import http from 'node:http'; import fs from 'node:fs'; import path from 'node:path'; import { handleRequest } from '../../src/proxy.js'; // Test configuration const TEST_PORT = 3001; const BASE_URL = `http://localhost:${TEST_PORT}`; // Server state let server; let serverReady = false; // Setup global config for tests const configPath = path.join(process.cwd(), 'config', 'default.json'); const configContent = fs.readFileSync(configPath, 'utf8'); global.config = JSON.parse(configContent); global.config.server.port = TEST_PORT; // Start server before all tests before(async () => { return new Promise((resolve) => { server = http.createServer(handleRequest); server.listen(TEST_PORT, () => { serverReady = true; resolve(); }); }); }); // Stop server after all tests after(async () => { return new Promise((resolve) => { if (server) { server.close(() => resolve()); } else { resolve(); } }); }); /** * Make HTTP request and return response details */ async function makeRequest(path, method = 'GET') { return new Promise((resolve, reject) => { const req = http.request(`${BASE_URL}${path}`, { method }, (res) => { let data = ''; res.on('data', chunk => data += chunk); res.on('end', () => { resolve({ statusCode: res.statusCode, headers: res.headers, body: data }); }); }); req.on('error', reject); req.end(); }); } describe('Contract: GET /:documentId (T009, T010)', () => { it('T009: should return 200 with Content-Type text/markdown for valid document ID', async () => { // Given: A valid Google Drive document ID const documentId = '1BxAA_validDocumentId123'; // When: Making GET request to /:documentId const response = await makeRequest(`/${documentId}`); // Then: Response should be 200 OK assert.equal(response.statusCode, 200, 'Status code should be 200 OK'); // Then: Content-Type should indicate Markdown assert.ok( response.headers['content-type']?.includes('text/markdown'), 'Content-Type should be text/markdown' ); // Then: X-Request-Id header should be present for tracing assert.ok( response.headers['x-request-id'], 'X-Request-Id header should be present' ); assert.match( response.headers['x-request-id'], /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i, 'X-Request-Id should be valid UUID v4' ); // Then: Body should contain Markdown content (non-empty) assert.ok(response.body.length > 0, 'Response body should not be empty'); }); it('T009: should include X-Document-Title header in successful response', async () => { // Given: A valid Google Drive document ID const documentId = '1BxAA_validDocumentId123'; // When: Making GET request to /:documentId const response = await makeRequest(`/${documentId}`); // Then: X-Document-Title header should be present assert.ok( response.headers['x-document-title'], 'X-Document-Title header should be present' ); }); it('T009: should include X-Document-Modified header with ISO 8601 timestamp', async () => { // Given: A valid Google Drive document ID const documentId = '1BxAA_validDocumentId123'; // When: Making GET request to /:documentId const response = await makeRequest(`/${documentId}`); // Then: X-Document-Modified header should be present assert.ok( response.headers['x-document-modified'], 'X-Document-Modified header should be present' ); // Then: Should be valid ISO 8601 timestamp const timestamp = response.headers['x-document-modified']; assert.ok( !isNaN(Date.parse(timestamp)), 'X-Document-Modified should be valid ISO 8601 date' ); }); it('T010: should return 404 with no body for invalid document ID', async () => { // Given: An invalid document ID (doesn't exist in Drive) const documentId = 'invalid-nonexistent-id'; // When: Making GET request to /:documentId const response = await makeRequest(`/${documentId}`); // Then: Response should be 404 Not Found assert.equal(response.statusCode, 404, 'Status code should be 404 Not Found'); // Then: Response body should be empty (status-only error response) assert.equal(response.body, '', 'Response body should be empty per spec'); }); it('T010: should return 403 with no body for document without permission', async () => { // Given: A document ID that user lacks permission to access const documentId = '1CyBB_forbiddenDocument456'; // When: Making GET request to /:documentId const response = await makeRequest(`/${documentId}`); // Then: Response should be 403 Forbidden assert.equal(response.statusCode, 403, 'Status code should be 403 Forbidden'); // Then: Response body should be empty (status-only error response) assert.equal(response.body, '', 'Response body should be empty per spec'); }); it('T010: should return 400 with no body for malformed document ID', async () => { // Given: A malformed document ID (too short, invalid characters) const documentId = 'bad@id!'; // When: Making GET request to /:documentId const response = await makeRequest(`/${documentId}`); // Then: Response should be 400 Bad Request assert.equal(response.statusCode, 400, 'Status code should be 400 Bad Request'); // Then: Response body should be empty (status-only error response) assert.equal(response.body, '', 'Response body should be empty per spec'); }); it('T010: should return 413 with no body for document exceeding 20MB limit', async () => { // Given: A document ID for file >20MB const documentId = '1DzCC_largeDocument25MB'; // When: Making GET request to /:documentId const response = await makeRequest(`/${documentId}`); // Then: Response should be 413 Payload Too Large assert.equal(response.statusCode, 413, 'Status code should be 413 Payload Too Large'); // Then: Response body should be empty (status-only error response) assert.equal(response.body, '', 'Response body should be empty per spec'); }); }); describe('Contract: GET /health', () => { it('should return 200 with health status object', async () => { // When: Making GET request to /health const response = await makeRequest('/health'); // Then: Response should be 200 OK assert.equal(response.statusCode, 200, 'Status code should be 200 OK'); // Then: Content-Type should be application/json assert.ok( response.headers['content-type']?.includes('application/json'), 'Content-Type should be application/json' ); // Then: Body should contain status field const health = JSON.parse(response.body); assert.equal(health.status, 'ok', 'Health status should be "ok"'); assert.ok(health.version, 'Health response should include version'); assert.ok(typeof health.uptime === 'number', 'Health response should include uptime in seconds'); }); }); describe('Contract: GET /sitemap.xml (T026)', () => { it('T026: should return 200 with Content-Type application/xml', async () => { // When: Making GET request to /sitemap.xml const response = await makeRequest('/sitemap.xml'); // Then: Response should be 200 OK assert.equal(response.statusCode, 200, 'Status code should be 200 OK'); // Then: Content-Type should be application/xml assert.ok( response.headers['content-type']?.includes('application/xml'), 'Content-Type should be application/xml' ); // Then: X-Document-Count header should be present assert.ok( response.headers['x-document-count'], 'X-Document-Count header should be present' ); // Then: Document count should be numeric const docCount = parseInt(response.headers['x-document-count'], 10); assert.ok(!isNaN(docCount), 'X-Document-Count should be numeric'); assert.ok(docCount >= 0, 'X-Document-Count should be non-negative'); }); it('T026: should return valid XML sitemap structure per sitemap protocol', async () => { // When: Making GET request to /sitemap.xml const response = await makeRequest('/sitemap.xml'); // Then: Should start with XML declaration assert.ok( response.body.startsWith(''), 'Should contain urlset with sitemap namespace' ); // Then: Should contain closing urlset tag assert.ok( response.body.includes(''), 'Should contain closing urlset tag' ); // Then: Should contain at least one url entry (if documents exist) const docCount = parseInt(response.headers['x-document-count'], 10); if (docCount > 0) { assert.ok( response.body.includes('') && response.body.includes(''), 'Should contain url entries when documents exist' ); assert.ok( response.body.includes('') && response.body.includes(''), 'URL entries should contain loc elements' ); assert.ok( response.body.includes('') && response.body.includes(''), 'URL entries should contain lastmod elements' ); } }); }); describe('Contract: GET /:documentId?format=html (T037)', () => { it('T037: should return 200 with Content-Type text/html when format=html', async () => { // Given: A valid document ID and format=html parameter const documentId = '1BxAA_validDocumentId123'; // When: Making GET request with format parameter const response = await makeRequest(`/${documentId}?format=html`); // Then: Response should be 200 OK assert.equal(response.statusCode, 200, 'Status code should be 200 OK'); // Then: Content-Type should be text/html assert.ok( response.headers['content-type']?.includes('text/html'), 'Content-Type should be text/html' ); // Then: Body should contain HTML content assert.ok(response.body.length > 0, 'Response body should not be empty'); }); }); describe('Contract: GET /:documentId?format=pdf (T038)', () => { it('T038: should return 200 with Content-Type application/pdf when format=pdf', async () => { // Given: A valid document ID and format=pdf parameter const documentId = '1BxAA_validDocumentId123'; // When: Making GET request with format parameter const response = await makeRequest(`/${documentId}?format=pdf`); // Then: Response should be 200 OK assert.equal(response.statusCode, 200, 'Status code should be 200 OK'); // Then: Content-Type should be application/pdf assert.ok( response.headers['content-type']?.includes('application/pdf'), 'Content-Type should be application/pdf' ); // Then: Body should contain binary PDF content assert.ok(response.body.length > 0, 'Response body should not be empty'); }); }); describe('Contract: Format parameter validation (T039)', () => { it('T039: should return 400 with no body for invalid format parameter', async () => { // Given: A valid document ID but invalid format const documentId = '1BxAA_validDocumentId123'; // When: Making GET request with invalid format const response = await makeRequest(`/${documentId}?format=invalid`); // Then: Response should be 400 Bad Request assert.equal(response.statusCode, 400, 'Status code should be 400 Bad Request'); // Then: Response body should be empty (status-only error response) assert.equal(response.body, '', 'Response body should be empty per spec'); }); it('T039: should default to markdown when format parameter is missing', async () => { // Given: A valid document ID without format parameter const documentId = '1BxAA_validDocumentId123'; // When: Making GET request without format parameter const response = await makeRequest(`/${documentId}`); // Then: Should return Markdown (default format) assert.ok( response.headers['content-type']?.includes('text/markdown'), 'Should default to text/markdown when format not specified' ); }); it('T039: should handle format parameter case-insensitively', async () => { // Given: A valid document ID with uppercase format parameter const documentId = '1BxAA_validDocumentId123'; // When: Making GET request with uppercase format const response = await makeRequest(`/${documentId}?format=HTML`); // Then: Should accept case-insensitive format assert.ok( response.statusCode === 200 || response.statusCode === 415, 'Should handle uppercase format parameter' ); }); });