378 lines
13 KiB
JavaScript
378 lines
13 KiB
JavaScript
/**
|
|
* 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('<?xml version="1.0"'),
|
|
'Should start with XML declaration'
|
|
);
|
|
|
|
// Then: Should contain urlset element with correct namespace
|
|
assert.ok(
|
|
response.body.includes('<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">'),
|
|
'Should contain urlset with sitemap namespace'
|
|
);
|
|
|
|
// Then: Should contain closing urlset tag
|
|
assert.ok(
|
|
response.body.includes('</urlset>'),
|
|
'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('<url>') && response.body.includes('</url>'),
|
|
'Should contain url entries when documents exist'
|
|
);
|
|
assert.ok(
|
|
response.body.includes('<loc>') && response.body.includes('</loc>'),
|
|
'URL entries should contain loc elements'
|
|
);
|
|
assert.ok(
|
|
response.body.includes('<lastmod>') && response.body.includes('</lastmod>'),
|
|
'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'
|
|
);
|
|
});
|
|
});
|