From c2ecc727756b307ed530cef14798ac00a488b12c Mon Sep 17 00:00:00 2001 From: "Peter.Morton" Date: Sat, 7 Mar 2026 01:36:41 -0600 Subject: [PATCH] removed .old files --- package.json | 2 +- tests/contract/document-api.test.js.old | 377 --------------- .../integration/drive-integration.test.js.old | 395 ---------------- tests/unit/proxy-export.test.js.old | 438 ------------------ tests/unit/proxy-routing.test.js.old | 377 --------------- tests/unit/proxy-sitemap.test.js.old | 386 --------------- 6 files changed, 1 insertion(+), 1974 deletions(-) delete mode 100644 tests/contract/document-api.test.js.old delete mode 100644 tests/integration/drive-integration.test.js.old delete mode 100644 tests/unit/proxy-export.test.js.old delete mode 100644 tests/unit/proxy-routing.test.js.old delete mode 100644 tests/unit/proxy-sitemap.test.js.old diff --git a/package.json b/package.json index 915dc92..ddf3720 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "test:unit": "node --test tests/unit/**/*.test.js", "test:integration": "node --test tests/integration/**/*.test.js", "test:contract": "node --test tests/contract/**/*.test.js", - "clean": "rm -rvf dist/* & rm -rvf **/*.{backup,backup-new,backup-old,backup-regenerated} & rm -rvf **/*-old.js" + "clean": "rm -rvf dist/* & rm -rvf **/*.old & rm -rvf **/*.{backup,backup-new,backup-old,backup-regenerated} & rm -rvf **/*-old.js" }, "keywords": [ "google-drive", diff --git a/tests/contract/document-api.test.js.old b/tests/contract/document-api.test.js.old deleted file mode 100644 index b315267..0000000 --- a/tests/contract/document-api.test.js.old +++ /dev/null @@ -1,377 +0,0 @@ -/** - * 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' - ); - }); -}); diff --git a/tests/integration/drive-integration.test.js.old b/tests/integration/drive-integration.test.js.old deleted file mode 100644 index aefaa52..0000000 --- a/tests/integration/drive-integration.test.js.old +++ /dev/null @@ -1,395 +0,0 @@ -/** - * Integration Tests: Google Drive API Integration - * - * Tests OAuth 2.0 and Drive API integration - * Tests T011, T027, T057 - */ - -import { describe, it, before, after } from 'node:test'; -import assert from 'node:assert/strict'; -import { google } from 'googleapis'; - -describe('Integration: OAuth2 Client Initialization (T011)', () => { - - let oauth2Client; - - before(() => { - // Mock global.config for testing - global.config = { - google: { - clientId: 'test-client-id.apps.googleusercontent.com', - clientSecret: 'test-client-secret', - redirectUri: 'http://localhost:3000/oauth/callback', - scopes: [ - 'https://www.googleapis.com/auth/drive.readonly', - 'https://www.googleapis.com/auth/drive.metadata.readonly' - ] - } - }; - }); - - it('T011: should initialize OAuth2 client from global.config', () => { - // Given: global.config contains OAuth credentials - const { clientId, clientSecret, redirectUri } = global.config.google; - - // When: Creating OAuth2 client - oauth2Client = new google.auth.OAuth2( - clientId, - clientSecret, - redirectUri - ); - - // Then: Client should be initialized - assert.ok(oauth2Client, 'OAuth2 client should be initialized'); - assert.equal(oauth2Client._clientId, clientId, 'Client ID should match config'); - assert.equal(oauth2Client._clientSecret, clientSecret, 'Client secret should match config'); - }); - - it('T011: should set credentials with access and refresh tokens', () => { - // Given: OAuth2 client is initialized - const credentials = { - access_token: 'ya29.test_access_token', - refresh_token: '1//test_refresh_token', - token_type: 'Bearer', - expiry_date: Date.now() + 3600000 // 1 hour from now - }; - - // When: Setting credentials - oauth2Client.setCredentials(credentials); - - // Then: Credentials should be set - const creds = oauth2Client.credentials; - assert.equal(creds.access_token, credentials.access_token, 'Access token should be set'); - assert.equal(creds.refresh_token, credentials.refresh_token, 'Refresh token should be set'); - }); - - it('T011: should listen for token refresh events', (t, done) => { - // Given: OAuth2 client with credentials - let tokenRefreshed = false; - - // When: Listening for tokens event - oauth2Client.on('tokens', (tokens) => { - tokenRefreshed = true; - assert.ok(tokens, 'Tokens should be emitted on refresh'); - done(); - }); - - // Then: Event listener should be registered - assert.ok(oauth2Client.listenerCount('tokens') > 0, 'Should have tokens event listener'); - - // Manually emit to test listener (in real scenario, googleapis emits this) - oauth2Client.emit('tokens', { access_token: 'new_token' }); - }); -}); - -describe('Integration: Drive API files.get() (T011)', () => { - - let drive; - - before(() => { - // Initialize Drive API client (will use mocked auth in tests) - const auth = new google.auth.OAuth2( - global.config.google.clientId, - global.config.google.clientSecret, - global.config.google.redirectUri - ); - - auth.setCredentials({ - access_token: 'test_token', - refresh_token: 'test_refresh' - }); - - drive = google.drive({ version: 'v3', auth }); - }); - - it('T011: should call files.get() with exportLinks field parameter', async () => { - // Given: A document ID - const fileId = '1BxAA_testDocumentId'; - - // When: Calling files.get() with fields parameter - // Note: This will fail in tests without real Drive API access (expected in TDD red phase) - try { - const response = await drive.files.get({ - fileId, - fields: 'id,name,mimeType,modifiedTime,size,exportLinks,webViewLink' - }); - - // Then: Response should contain expected fields - assert.ok(response.data, 'Response should contain data'); - assert.ok(response.data.id, 'Response should contain id field'); - assert.ok(response.data.name, 'Response should contain name field'); - - } catch (error) { - // Expected to fail without real credentials - this is TDD red phase - assert.ok( - error.message.includes('invalid') || error.message.includes('auth') || error.message.includes('credentials'), - 'Should fail with auth-related error in test environment' - ); - } - }); - - it('T011: should handle token expiry and refresh', async () => { - // Given: OAuth2 client with expired token - const auth = new google.auth.OAuth2( - global.config.google.clientId, - global.config.google.clientSecret, - global.config.google.redirectUri - ); - - // Set expired token - auth.setCredentials({ - access_token: 'expired_token', - refresh_token: 'valid_refresh_token', - expiry_date: Date.now() - 1000 // Expired 1 second ago - }); - - // When: Making API call with expired token - // Then: googleapis should automatically refresh (or fail trying) - const drive = google.drive({ version: 'v3', auth }); - - try { - await drive.files.get({ fileId: 'test', fields: 'id' }); - } catch (error) { - // Expected to fail in test environment - validates refresh attempt - assert.ok(error, 'Should attempt token refresh and fail without real refresh token'); - } - }); -}); - -describe('Integration: Drive API files.list() with Pagination (T027)', () => { - - let drive; - - before(() => { - const auth = new google.auth.OAuth2( - global.config.google.clientId, - global.config.google.clientSecret, - global.config.google.redirectUri - ); - - auth.setCredentials({ - access_token: 'test_token', - refresh_token: 'test_refresh' - }); - - drive = google.drive({ version: 'v3', auth }); - }); - - it('T027: should retrieve paginated list of documents', async () => { - // Given: Drive API client - let allFiles = []; - let pageToken = null; - - // When: Retrieving files with pagination - try { - do { - const response = await drive.files.list({ - pageSize: 100, - pageToken, - fields: 'nextPageToken,files(id,name,mimeType,modifiedTime)', - q: "mimeType='application/vnd.google-apps.document'" - }); - - // Then: Response should contain files array - assert.ok(Array.isArray(response.data.files), 'Response should contain files array'); - allFiles = allFiles.concat(response.data.files); - - // Update pageToken for next iteration - pageToken = response.data.nextPageToken; - - } while (pageToken); - - // Then: Should have retrieved all files - assert.ok(allFiles.length >= 0, 'Should retrieve files (may be 0 in test)'); - - } catch (error) { - // Expected to fail without real credentials - assert.ok( - error.message.includes('invalid') || error.message.includes('auth'), - 'Should fail with auth error in test environment' - ); - } - }); - - it('T027: should handle large result sets (>1000 documents)', async () => { - // Given: Query that might return many documents - let pageCount = 0; - let pageToken = null; - const maxPages = 15; // Test pagination up to 1500 docs (100 per page) - - // When: Paginating through results - try { - do { - const response = await drive.files.list({ - pageSize: 100, - pageToken, - fields: 'nextPageToken,files(id,name)', - q: "trashed=false" - }); - - pageCount++; - pageToken = response.data.nextPageToken; - - // Then: Should handle pagination correctly - assert.ok(pageCount <= maxPages, 'Should not infinite loop'); - - if (!pageToken) break; // No more pages - - } while (pageCount < maxPages); - - assert.ok(pageCount > 0, 'Should process at least one page'); - - } catch (error) { - // Expected to fail without real credentials - assert.ok(error, 'Should handle auth error gracefully'); - } - }); -}); - -describe('Integration: Large Document Streaming (T057)', () => { - - it('T057: should stream 5MB document without excessive memory usage', async () => { - // Given: A large document (5MB) - const initialMemory = process.memoryUsage().heapUsed; - - // When: Streaming large document - // (This would be a real streaming operation in implementation) - const mockStreamSize = 5 * 1024 * 1024; // 5MB - const chunks = []; - const chunkSize = 64 * 1024; // 64KB chunks - - // Simulate streaming by processing chunks - for (let i = 0; i < mockStreamSize; i += chunkSize) { - const chunk = Buffer.alloc(Math.min(chunkSize, mockStreamSize - i)); - chunks.push(chunk); - } - - // Then: Memory increase should be reasonable (<100MB) - const finalMemory = process.memoryUsage().heapUsed; - const memoryIncrease = (finalMemory - initialMemory) / (1024 * 1024); // MB - - assert.ok( - memoryIncrease < 100, - `Memory increase should be <100MB for 5MB document, was ${memoryIncrease.toFixed(2)}MB` - ); - }); - - it('T057: should handle streaming with backpressure', async () => { - // Given: A mock readable stream - const { Readable } = await import('node:stream'); - - let chunksRead = 0; - const totalChunks = 100; - - const mockStream = new Readable({ - read() { - if (chunksRead < totalChunks) { - this.push(Buffer.alloc(1024)); // 1KB chunk - chunksRead++; - } else { - this.push(null); // EOF - } - } - }); - - // When: Consuming stream with backpressure handling - const chunks = []; - for await (const chunk of mockStream) { - chunks.push(chunk); - } - - // Then: All chunks should be received - assert.equal(chunks.length, totalChunks, 'Should receive all chunks'); - assert.equal(chunksRead, totalChunks, 'Should read all chunks'); - }); -}); - -describe('Integration: Drive API Error Mapping', () => { - - it('should map Drive API 404 error to HTTP 404', () => { - // Given: Drive API 404 error - const driveError = { - code: 404, - message: 'File not found' - }; - - // When: Mapping to HTTP status - const httpStatus = driveError.code; - - // Then: Should map to 404 - assert.equal(httpStatus, 404, 'Drive 404 should map to HTTP 404'); - }); - - it('should map Drive API 403 error to HTTP 403', () => { - // Given: Drive API 403 error - const driveError = { - code: 403, - message: 'The user does not have permission' - }; - - // When: Mapping to HTTP status - const httpStatus = driveError.code; - - // Then: Should map to 403 - assert.equal(httpStatus, 403, 'Drive 403 should map to HTTP 403'); - }); - - it('should map Drive API 401 error to HTTP 401', () => { - // Given: Drive API 401 error - const driveError = { - code: 401, - message: 'Invalid credentials' - }; - - // When: Mapping to HTTP status - const httpStatus = driveError.code; - - // Then: Should map to 401 - assert.equal(httpStatus, 401, 'Drive 401 should map to HTTP 401'); - }); - - it('should map Drive API 429 error to HTTP 429 with Retry-After', () => { - // Given: Drive API rate limit error - const driveError = { - code: 429, - message: 'Rate limit exceeded', - errors: [{ reason: 'rateLimitExceeded' }] - }; - - // When: Mapping to HTTP status and calculating retry delay - const httpStatus = driveError.code; - const retryAfter = 60; // Default 60 seconds - - // Then: Should map to 429 with Retry-After header - assert.equal(httpStatus, 429, 'Drive 429 should map to HTTP 429'); - assert.equal(retryAfter, 60, 'Should include Retry-After of 60 seconds'); - }); - - it('should map Drive API 500 error to HTTP 500', () => { - // Given: Drive API internal error - const driveError = { - code: 500, - message: 'Internal server error' - }; - - // When: Mapping to HTTP status - const httpStatus = driveError.code; - - // Then: Should map to 500 - assert.equal(httpStatus, 500, 'Drive 500 should map to HTTP 500'); - }); - - it('should map Drive API 503 error to HTTP 503', () => { - // Given: Drive API service unavailable - const driveError = { - code: 503, - message: 'Service unavailable' - }; - - // When: Mapping to HTTP status - const httpStatus = driveError.code; - - // Then: Should map to 503 - assert.equal(httpStatus, 503, 'Drive 503 should map to HTTP 503'); - }); -}); diff --git a/tests/unit/proxy-export.test.js.old b/tests/unit/proxy-export.test.js.old deleted file mode 100644 index e5bebc3..0000000 --- a/tests/unit/proxy-export.test.js.old +++ /dev/null @@ -1,438 +0,0 @@ -/** - * Unit Tests: Document Export Logic - * - * Tests document export functions in proxy.js - * Tests T012, T013, T014, T040, T041 - */ - -import { describe, it } from 'node:test'; -import assert from 'node:assert/strict'; - -describe('Unit: validateDocumentId() (T012)', () => { - - // Mock function to test (will be in proxy.js) - function validateDocumentId(id) { - const pattern = /^[a-zA-Z0-9_-]{8,128}$/; - return pattern.test(id); - } - - it('T012: should accept valid 8-character alphanumeric ID', () => { - // Given: Valid 8-character document ID - const validId = '1BxAA789'; - - // When: Validating document ID - const isValid = validateDocumentId(validId); - - // Then: Should return true - assert.equal(isValid, true, 'Should accept 8-character alphanumeric ID'); - }); - - it('T012: should accept valid 128-character alphanumeric ID', () => { - // Given: Valid 128-character document ID - const validId = 'a'.repeat(128); - - // When: Validating document ID - const isValid = validateDocumentId(validId); - - // Then: Should return true - assert.equal(isValid, true, 'Should accept 128-character alphanumeric ID'); - }); - - it('T012: should accept IDs with hyphens and underscores', () => { - // Given: Valid IDs with hyphens and underscores - const idWithHyphen = '1BxAA-test-123'; - const idWithUnderscore = '1BxAA_test_123'; - const idWithBoth = '1BxAA-test_123'; - - // When: Validating document IDs - const isValidHyphen = validateDocumentId(idWithHyphen); - const isValidUnderscore = validateDocumentId(idWithUnderscore); - const isValidBoth = validateDocumentId(idWithBoth); - - // Then: Should return true for all - assert.equal(isValidHyphen, true, 'Should accept IDs with hyphens'); - assert.equal(isValidUnderscore, true, 'Should accept IDs with underscores'); - assert.equal(isValidBoth, true, 'Should accept IDs with both hyphens and underscores'); - }); - - it('T012: should reject IDs shorter than 8 characters', () => { - // Given: Invalid short ID - const shortId = '1BxAA78'; - - // When: Validating document ID - const isValid = validateDocumentId(shortId); - - // Then: Should return false - assert.equal(isValid, false, 'Should reject IDs shorter than 8 characters'); - }); - - it('T012: should reject IDs longer than 128 characters', () => { - // Given: Invalid long ID - const longId = 'a'.repeat(129); - - // When: Validating document ID - const isValid = validateDocumentId(longId); - - // Then: Should return false - assert.equal(isValid, false, 'Should reject IDs longer than 128 characters'); - }); - - it('T012: should reject IDs with invalid characters', () => { - // Given: IDs with invalid characters - const invalidChars = [ - '1BxAA@test', // @ symbol - '1BxAA test', // space - '1BxAA!test', // exclamation - '1BxAA#test', // hash - '1BxAA.test', // period - ]; - - // When: Validating each ID - // Then: All should return false - invalidChars.forEach(id => { - const isValid = validateDocumentId(id); - assert.equal(isValid, false, `Should reject ID with invalid character: ${id}`); - }); - }); - - it('T012: should reject empty string', () => { - // Given: Empty string - const emptyId = ''; - - // When: Validating document ID - const isValid = validateDocumentId(emptyId); - - // Then: Should return false - assert.equal(isValid, false, 'Should reject empty string'); - }); -}); - -describe('Unit: findExportLink() (T013, T041)', () => { - - // Mock function to test (will be in proxy.js) - function findExportLink(exportLinks, format = 'markdown') { - if (!exportLinks) return null; - - const formatMap = { - 'markdown': ['text/x-markdown', 'text/markdown', 'text/html'], - 'html': ['text/html'], - 'pdf': ['application/pdf'] - }; - - const mimeTypes = formatMap[format.toLowerCase()] || []; - - for (const mimeType of mimeTypes) { - if (exportLinks[mimeType]) { - return exportLinks[mimeType]; - } - } - - return null; - } - - it('T013: should select text/x-markdown from exportLinks when available', () => { - // Given: exportLinks with text/x-markdown - const exportLinks = { - 'text/x-markdown': 'https://docs.google.com/export?format=markdown', - 'text/html': 'https://docs.google.com/export?format=html', - 'application/pdf': 'https://docs.google.com/export?format=pdf' - }; - - // When: Finding export link for markdown format - const link = findExportLink(exportLinks, 'markdown'); - - // Then: Should select text/x-markdown - assert.equal(link, exportLinks['text/x-markdown'], 'Should select text/x-markdown'); - }); - - it('T013: should fall back to text/html when text/x-markdown unavailable', () => { - // Given: exportLinks without text/x-markdown or text/markdown - const exportLinks = { - 'text/html': 'https://docs.google.com/export?format=html', - 'application/pdf': 'https://docs.google.com/export?format=pdf' - }; - - // When: Finding export link for markdown format - const link = findExportLink(exportLinks, 'markdown'); - - // Then: Should fall back to text/html - assert.equal(link, exportLinks['text/html'], 'Should fall back to text/html'); - }); - - it('T013: should prefer text/markdown over text/html when available', () => { - // Given: exportLinks with text/markdown - const exportLinks = { - 'text/markdown': 'https://docs.google.com/export?format=markdown', - 'text/html': 'https://docs.google.com/export?format=html' - }; - - // When: Finding export link for markdown format - const link = findExportLink(exportLinks, 'markdown'); - - // Then: Should select text/markdown - assert.equal(link, exportLinks['text/markdown'], 'Should prefer text/markdown'); - }); - - it('T041: should select text/html MIME type for html format', () => { - // Given: exportLinks with multiple formats - const exportLinks = { - 'text/html': 'https://docs.google.com/export?format=html', - 'text/x-markdown': 'https://docs.google.com/export?format=markdown', - 'application/pdf': 'https://docs.google.com/export?format=pdf' - }; - - // When: Finding export link for html format - const link = findExportLink(exportLinks, 'html'); - - // Then: Should select text/html - assert.equal(link, exportLinks['text/html'], 'Should select text/html for html format'); - }); - - it('T041: should select application/pdf MIME type for pdf format', () => { - // Given: exportLinks with multiple formats - const exportLinks = { - 'text/html': 'https://docs.google.com/export?format=html', - 'application/pdf': 'https://docs.google.com/export?format=pdf' - }; - - // When: Finding export link for pdf format - const link = findExportLink(exportLinks, 'pdf'); - - // Then: Should select application/pdf - assert.equal(link, exportLinks['application/pdf'], 'Should select application/pdf for pdf format'); - }); - - it('T041: should return null when requested format unavailable', () => { - // Given: exportLinks without PDF - const exportLinks = { - 'text/html': 'https://docs.google.com/export?format=html' - }; - - // When: Finding export link for pdf format - const link = findExportLink(exportLinks, 'pdf'); - - // Then: Should return null - assert.equal(link, null, 'Should return null when format unavailable'); - }); - - it('should return null when exportLinks is null or undefined', () => { - // Given: Null or undefined exportLinks - const linkFromNull = findExportLink(null, 'markdown'); - const linkFromUndefined = findExportLink(undefined, 'markdown'); - - // Then: Should return null - assert.equal(linkFromNull, null, 'Should return null for null exportLinks'); - assert.equal(linkFromUndefined, null, 'Should return null for undefined exportLinks'); - }); -}); - -describe('Unit: validateDocumentSize() (T014)', () => { - - // Mock function to test (will be in proxy.js) - function validateDocumentSize(metadata) { - const maxSize = 20 * 1024 * 1024; // 20MB - - // Native Drive files (Docs, Sheets, Slides) don't have size property - if (!metadata.size) { - return { valid: true }; - } - - const size = parseInt(metadata.size, 10); - - if (size > maxSize) { - return { - valid: false, - error: 'Document exceeds 20MB size limit', - statusCode: 413 - }; - } - - return { valid: true, size }; - } - - it('T014: should accept documents under 20MB', () => { - // Given: Document metadata with size < 20MB - const metadata = { - id: '1BxAA_test', - name: 'test.pdf', - size: '10485760' // 10MB - }; - - // When: Validating document size - const result = validateDocumentSize(metadata); - - // Then: Should be valid - assert.equal(result.valid, true, 'Should accept document < 20MB'); - assert.equal(result.size, 10485760, 'Should return parsed size'); - }); - - it('T014: should accept documents exactly at 20MB', () => { - // Given: Document metadata with size exactly 20MB - const metadata = { - id: '1BxAA_test', - name: 'test.pdf', - size: '20971520' // Exactly 20MB - }; - - // When: Validating document size - const result = validateDocumentSize(metadata); - - // Then: Should be valid - assert.equal(result.valid, true, 'Should accept document exactly at 20MB'); - }); - - it('T014: should reject documents over 20MB', () => { - // Given: Document metadata with size > 20MB - const metadata = { - id: '1BxAA_test', - name: 'large.pdf', - size: '20971521' // 20MB + 1 byte - }; - - // When: Validating document size - const result = validateDocumentSize(metadata); - - // Then: Should be invalid - assert.equal(result.valid, false, 'Should reject document > 20MB'); - assert.equal(result.statusCode, 413, 'Should return 413 status code'); - assert.ok(result.error, 'Should include error message'); - }); - - it('T014: should accept native Google Drive documents without size', () => { - // Given: Google Doc metadata (no size property) - const metadata = { - id: '1BxAA_test', - name: 'My Document', - mimeType: 'application/vnd.google-apps.document' - // Note: No size property for native Drive files - }; - - // When: Validating document size - const result = validateDocumentSize(metadata); - - // Then: Should be valid (native files exported on-the-fly) - assert.equal(result.valid, true, 'Should accept native Drive documents without size'); - }); - - it('T014: should handle size as number string', () => { - // Given: Document metadata with size as string (Drive API returns strings) - const metadata = { - id: '1BxAA_test', - name: 'test.pdf', - size: '5242880' // 5MB as string - }; - - // When: Validating document size - const result = validateDocumentSize(metadata); - - // Then: Should parse and validate correctly - assert.equal(result.valid, true, 'Should handle size as string'); - assert.equal(result.size, 5242880, 'Should parse size to number'); - }); -}); - -describe('Unit: parseFormatParam() (T040)', () => { - - // Mock function to test (will be in proxy.js) - function parseFormatParam(url) { - const urlObj = new URL(url, 'http://localhost'); - const format = urlObj.searchParams.get('format'); - - if (!format) { - return { valid: true, format: 'markdown' }; // Default - } - - const normalized = format.toLowerCase(); - const validFormats = ['markdown', 'html', 'pdf']; - - if (!validFormats.includes(normalized)) { - return { - valid: false, - error: 'Invalid format parameter', - statusCode: 400 - }; - } - - return { valid: true, format: normalized }; - } - - it('T040: should extract format parameter from query string', () => { - // Given: URL with format parameter - const url = '/1BxAA_test?format=html'; - - // When: Parsing format parameter - const result = parseFormatParam(url); - - // Then: Should extract format - assert.equal(result.valid, true, 'Should be valid'); - assert.equal(result.format, 'html', 'Should extract html format'); - }); - - it('T040: should validate against allowed values (markdown|html|pdf)', () => { - // Given: URLs with valid formats - const urls = [ - '/doc?format=markdown', - '/doc?format=html', - '/doc?format=pdf' - ]; - - // When: Parsing each URL - // Then: All should be valid - urls.forEach(url => { - const result = parseFormatParam(url); - assert.equal(result.valid, true, `Should accept format in ${url}`); - }); - }); - - it('T040: should return default markdown when format parameter missing', () => { - // Given: URL without format parameter - const url = '/1BxAA_test'; - - // When: Parsing format parameter - const result = parseFormatParam(url); - - // Then: Should default to markdown - assert.equal(result.valid, true, 'Should be valid'); - assert.equal(result.format, 'markdown', 'Should default to markdown'); - }); - - it('T040: should normalize format to lowercase', () => { - // Given: URL with uppercase format - const urls = [ - '/doc?format=HTML', - '/doc?format=Markdown', - '/doc?format=PDF' - ]; - - // When: Parsing each URL - // Then: Should normalize to lowercase - assert.equal(parseFormatParam(urls[0]).format, 'html', 'Should normalize HTML to html'); - assert.equal(parseFormatParam(urls[1]).format, 'markdown', 'Should normalize Markdown to markdown'); - assert.equal(parseFormatParam(urls[2]).format, 'pdf', 'Should normalize PDF to pdf'); - }); - - it('T040: should return 400 status for invalid format values', () => { - // Given: URL with invalid format - const url = '/1BxAA_test?format=invalid'; - - // When: Parsing format parameter - const result = parseFormatParam(url); - - // Then: Should be invalid - assert.equal(result.valid, false, 'Should be invalid'); - assert.equal(result.statusCode, 400, 'Should return 400 status'); - assert.ok(result.error, 'Should include error message'); - }); - - it('T040: should handle multiple query parameters', () => { - // Given: URL with multiple query parameters - const url = '/1BxAA_test?format=pdf&other=value&another=param'; - - // When: Parsing format parameter - const result = parseFormatParam(url); - - // Then: Should extract format correctly - assert.equal(result.valid, true, 'Should be valid'); - assert.equal(result.format, 'pdf', 'Should extract format from multi-param URL'); - }); -}); diff --git a/tests/unit/proxy-routing.test.js.old b/tests/unit/proxy-routing.test.js.old deleted file mode 100644 index d8149a6..0000000 --- a/tests/unit/proxy-routing.test.js.old +++ /dev/null @@ -1,377 +0,0 @@ -/** - * Unit Tests: Request Routing Logic - * - * Tests request routing and error mapping in proxy.js - * Tests T015, T016, T050 - */ - -import { describe, it } from 'node:test'; -import assert from 'node:assert/strict'; - -describe('Unit: handleRequest() Routing (T015)', () => { - - // Mock routing function (will be in proxy.js) - function parseRoute(method, url) { - if (method !== 'GET') { - return { route: null, error: 'Method not allowed', statusCode: 405 }; - } - - const urlObj = new URL(url, 'http://localhost'); - const path = urlObj.pathname; - - if (path === '/health') { - return { route: 'health' }; - } - - if (path === '/sitemap.xml') { - return { route: 'sitemap' }; - } - - // Document route: /:documentId - const docMatch = path.match(/^\/([a-zA-Z0-9_-]+)$/); - if (docMatch) { - return { route: 'document', documentId: docMatch[1] }; - } - - return { route: null, error: 'Not found', statusCode: 404 }; - } - - it('T015: should route /health to health check handler', () => { - // Given: GET request to /health - const method = 'GET'; - const url = '/health'; - - // When: Parsing route - const result = parseRoute(method, url); - - // Then: Should route to health - assert.equal(result.route, 'health', 'Should route to health handler'); - }); - - it('T015: should route /:documentId to document export handler', () => { - // Given: GET request to /:documentId - const method = 'GET'; - const url = '/1BxAA_testDocument123'; - - // When: Parsing route - const result = parseRoute(method, url); - - // Then: Should route to document handler - assert.equal(result.route, 'document', 'Should route to document handler'); - assert.equal(result.documentId, '1BxAA_testDocument123', 'Should extract document ID'); - }); - - it('T015: should route /sitemap.xml to sitemap handler', () => { - // Given: GET request to /sitemap.xml - const method = 'GET'; - const url = '/sitemap.xml'; - - // When: Parsing route - const result = parseRoute(method, url); - - // Then: Should route to sitemap - assert.equal(result.route, 'sitemap', 'Should route to sitemap handler'); - }); - - it('T015: should return 404 for unknown routes', () => { - // Given: GET request to unknown path - const method = 'GET'; - const url = '/unknown/path'; - - // When: Parsing route - const result = parseRoute(method, url); - - // Then: Should return 404 - assert.equal(result.route, null, 'Should not match any route'); - assert.equal(result.statusCode, 404, 'Should return 404 status'); - }); - - it('T015: should return 405 for non-GET methods', () => { - // Given: POST request - const method = 'POST'; - const url = '/1BxAA_test'; - - // When: Parsing route - const result = parseRoute(method, url); - - // Then: Should return 405 Method Not Allowed - assert.equal(result.route, null, 'Should not match any route'); - assert.equal(result.statusCode, 405, 'Should return 405 status'); - }); - - it('T015: should extract documentId with hyphens and underscores', () => { - // Given: Document ID with special allowed characters - const urls = [ - '/1BxAA-test-123', - '/1BxAA_test_123', - '/1BxAA-test_123' - ]; - - // When: Parsing each route - // Then: Should extract document IDs correctly - urls.forEach(url => { - const result = parseRoute('GET', url); - assert.equal(result.route, 'document', `Should route ${url} to document handler`); - assert.ok(result.documentId, `Should extract document ID from ${url}`); - }); - }); -}); - -describe('Unit: mapDriveError() (T016)', () => { - - // Mock error mapping function (will be in proxy.js) - function mapDriveError(error) { - // Handle GaxiosError from googleapis - const statusCode = error.code || error.response?.status || 500; - - const mapping = { - 404: { status: 404, message: 'Not Found' }, - 403: { status: 403, message: 'Forbidden' }, - 401: { status: 401, message: 'Unauthorized' }, - 429: { status: 429, message: 'Too Many Requests', retryAfter: 60 }, - 500: { status: 500, message: 'Internal Server Error' }, - 503: { status: 503, message: 'Service Unavailable' } - }; - - return mapping[statusCode] || { status: 500, message: 'Internal Server Error' }; - } - - it('T016: should convert Drive API 404 to HTTP 404', () => { - // Given: Drive API 404 error - const driveError = { code: 404, message: 'File not found' }; - - // When: Mapping error - const result = mapDriveError(driveError); - - // Then: Should map to HTTP 404 - assert.equal(result.status, 404, 'Should map to 404 status'); - }); - - it('T016: should convert Drive API 403 to HTTP 403', () => { - // Given: Drive API 403 error - const driveError = { code: 403, message: 'Permission denied' }; - - // When: Mapping error - const result = mapDriveError(driveError); - - // Then: Should map to HTTP 403 - assert.equal(result.status, 403, 'Should map to 403 status'); - }); - - it('T016: should convert Drive API 401 to HTTP 401', () => { - // Given: Drive API 401 error - const driveError = { code: 401, message: 'Invalid credentials' }; - - // When: Mapping error - const result = mapDriveError(driveError); - - // Then: Should map to HTTP 401 - assert.equal(result.status, 401, 'Should map to 401 status'); - }); - - it('T016: should convert Drive API 429 to HTTP 429 with Retry-After', () => { - // Given: Drive API rate limit error - const driveError = { code: 429, message: 'Rate limit exceeded' }; - - // When: Mapping error - const result = mapDriveError(driveError); - - // Then: Should map to HTTP 429 with Retry-After - assert.equal(result.status, 429, 'Should map to 429 status'); - assert.equal(result.retryAfter, 60, 'Should include Retry-After of 60 seconds'); - }); - - it('T016: should convert Drive API 500 to HTTP 500', () => { - // Given: Drive API internal error - const driveError = { code: 500, message: 'Internal error' }; - - // When: Mapping error - const result = mapDriveError(driveError); - - // Then: Should map to HTTP 500 - assert.equal(result.status, 500, 'Should map to 500 status'); - }); - - it('T016: should convert Drive API 503 to HTTP 503', () => { - // Given: Drive API service unavailable - const driveError = { code: 503, message: 'Service unavailable' }; - - // When: Mapping error - const result = mapDriveError(driveError); - - // Then: Should map to HTTP 503 - assert.equal(result.status, 503, 'Should map to 503 status'); - }); - - it('should handle errors without code by checking response.status', () => { - // Given: Error with response.status instead of code - const driveError = { - response: { status: 404, statusText: 'Not Found' }, - message: 'Request failed' - }; - - // When: Mapping error - const result = mapDriveError(driveError); - - // Then: Should map using response.status - assert.equal(result.status, 404, 'Should map using response.status'); - }); - - it('should default to 500 for unknown error codes', () => { - // Given: Error with unknown status code - const driveError = { code: 999, message: 'Unknown error' }; - - // When: Mapping error - const result = mapDriveError(driveError); - - // Then: Should default to 500 - assert.equal(result.status, 500, 'Should default to 500 for unknown codes'); - }); -}); - -describe('Unit: Rate Limiting (T050)', () => { - - // Mock rate limiter (will be in proxy.js) - class RateLimiter { - constructor(maxRequests = 100, windowMs = 60000) { - this.maxRequests = maxRequests; - this.windowMs = windowMs; - this.requests = new Map(); // ip -> [timestamps] - } - - checkLimit(ip) { - const now = Date.now(); - const windowStart = now - this.windowMs; - - // Get existing requests for this IP - let timestamps = this.requests.get(ip) || []; - - // Remove old timestamps outside window - timestamps = timestamps.filter(ts => ts > windowStart); - - // Check if limit exceeded - if (timestamps.length >= this.maxRequests) { - const oldestRequest = timestamps[0]; - const retryAfter = Math.ceil((oldestRequest + this.windowMs - now) / 1000); - - return { - allowed: false, - statusCode: 429, - retryAfter - }; - } - - // Add current request - timestamps.push(now); - this.requests.set(ip, timestamps); - - return { allowed: true }; - } - - cleanup() { - const now = Date.now(); - const windowStart = now - this.windowMs; - - for (const [ip, timestamps] of this.requests.entries()) { - const filtered = timestamps.filter(ts => ts > windowStart); - if (filtered.length === 0) { - this.requests.delete(ip); - } else { - this.requests.set(ip, filtered); - } - } - } - } - - it('T050: should allow 100 requests from same IP within window', () => { - // Given: Rate limiter with 100 req/min limit - const limiter = new RateLimiter(100, 60000); - const testIp = '192.168.1.1'; - - // When: Making 100 requests - let allowedCount = 0; - for (let i = 0; i < 100; i++) { - const result = limiter.checkLimit(testIp); - if (result.allowed) allowedCount++; - } - - // Then: All 100 requests should be allowed - assert.equal(allowedCount, 100, 'Should allow 100 requests'); - }); - - it('T050: should return 429 with Retry-After header on 101st request', () => { - // Given: Rate limiter with 100 req/min limit - const limiter = new RateLimiter(100, 60000); - const testIp = '192.168.1.1'; - - // When: Making 101 requests - for (let i = 0; i < 100; i++) { - limiter.checkLimit(testIp); - } - - const result = limiter.checkLimit(testIp); - - // Then: 101st request should be rate limited - assert.equal(result.allowed, false, 'Should not allow 101st request'); - assert.equal(result.statusCode, 429, 'Should return 429 status'); - assert.ok(result.retryAfter > 0, 'Should include Retry-After in seconds'); - assert.ok(result.retryAfter <= 60, 'Retry-After should be <= 60 seconds'); - }); - - it('T050: should track requests per IP independently', () => { - // Given: Rate limiter and multiple IPs - const limiter = new RateLimiter(100, 60000); - const ip1 = '192.168.1.1'; - const ip2 = '192.168.1.2'; - - // When: Making 100 requests from each IP - for (let i = 0; i < 100; i++) { - limiter.checkLimit(ip1); - limiter.checkLimit(ip2); - } - - // Then: Both IPs should still be allowed (independent limits) - const result1 = limiter.checkLimit(ip1); - const result2 = limiter.checkLimit(ip2); - - assert.equal(result1.allowed, false, 'IP1 should be rate limited'); - assert.equal(result2.allowed, false, 'IP2 should be rate limited'); - }); - - it('T050: should cleanup old entries outside time window', () => { - // Given: Rate limiter with short window - const limiter = new RateLimiter(10, 1000); // 10 req/sec for testing - const testIp = '192.168.1.1'; - - // When: Making requests then cleaning up - for (let i = 0; i < 10; i++) { - limiter.checkLimit(testIp); - } - - // Wait for window to pass (simulate with manual cleanup) - limiter.cleanup(); - - // Then: Should have entries in map - assert.ok(limiter.requests.has(testIp), 'Should have IP in requests map'); - }); - - it('T050: should reset limit after time window expires', () => { - // Given: Rate limiter with very short window - const limiter = new RateLimiter(5, 100); // 5 req / 100ms - const testIp = '192.168.1.1'; - - // When: Filling up limit - for (let i = 0; i < 5; i++) { - limiter.checkLimit(testIp); - } - - // Simulate time passing by manipulating timestamps - const oldTimestamps = limiter.requests.get(testIp); - const expiredTimestamps = oldTimestamps.map(ts => ts - 200); // Make them 200ms old - limiter.requests.set(testIp, expiredTimestamps); - - // Then: New request should be allowed after window - const result = limiter.checkLimit(testIp); - assert.equal(result.allowed, true, 'Should allow request after window expires'); - }); -}); diff --git a/tests/unit/proxy-sitemap.test.js.old b/tests/unit/proxy-sitemap.test.js.old deleted file mode 100644 index 6aec707..0000000 --- a/tests/unit/proxy-sitemap.test.js.old +++ /dev/null @@ -1,386 +0,0 @@ -/** - * Unit Tests: Sitemap Generation Logic - * - * Tests sitemap XML generation functions - * Tests T028, T029, T030 - */ - -import { describe, it } from 'node:test'; -import assert from 'node:assert/strict'; - -describe('Unit: escapeXml() (T028)', () => { - - // Mock XML escape function (will be in proxy.js) - function escapeXml(str) { - if (typeof str !== 'string') return ''; - - return str - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"') - .replace(/'/g, '''); - } - - it('T028: should escape < character to <', () => { - // Given: String with < character - const input = 'test < value'; - - // When: Escaping for XML - const output = escapeXml(input); - - // Then: Should escape < - assert.equal(output, 'test < value', 'Should escape <'); - }); - - it('T028: should escape > character to >', () => { - // Given: String with > character - const input = 'test > value'; - - // When: Escaping for XML - const output = escapeXml(input); - - // Then: Should escape > - assert.equal(output, 'test > value', 'Should escape >'); - }); - - it('T028: should escape & character to &', () => { - // Given: String with & character - const input = 'test & value'; - - // When: Escaping for XML - const output = escapeXml(input); - - // Then: Should escape & - assert.equal(output, 'test & value', 'Should escape &'); - }); - - it('T028: should escape " character to "', () => { - // Given: String with " character - const input = 'test "value"'; - - // When: Escaping for XML - const output = escapeXml(input); - - // Then: Should escape " - assert.equal(output, 'test "value"', 'Should escape "'); - }); - - it('T028: should escape \' character to '', () => { - // Given: String with ' character - const input = "test 'value'"; - - // When: Escaping for XML - const output = escapeXml(input); - - // Then: Should escape ' - assert.equal(output, 'test 'value'', 'Should escape \''); - }); - - it('T028: should escape multiple special characters in correct order', () => { - // Given: String with multiple special characters - const input = 'content & more'; - - // When: Escaping for XML - const output = escapeXml(input); - - // Then: Should escape all characters properly - assert.equal( - output, - '<tag attr="value" other='test'>content & more</tag>', - 'Should escape all XML special characters' - ); - }); - - it('T028: should handle strings without special characters', () => { - // Given: String without special characters - const input = 'normal text 123'; - - // When: Escaping for XML - const output = escapeXml(input); - - // Then: Should return unchanged - assert.equal(output, input, 'Should not modify strings without special chars'); - }); - - it('T028: should handle empty string', () => { - // Given: Empty string - const input = ''; - - // When: Escaping for XML - const output = escapeXml(input); - - // Then: Should return empty string - assert.equal(output, '', 'Should handle empty string'); - }); - - it('T028: should handle non-string input gracefully', () => { - // Given: Non-string inputs - const inputs = [null, undefined, 123, { foo: 'bar' }]; - - // When: Escaping each input - // Then: Should return empty string for non-strings - inputs.forEach(input => { - const output = escapeXml(input); - assert.equal(output, '', `Should return empty string for ${typeof input}`); - }); - }); -}); - -describe('Unit: formatSitemapEntry() (T029)', () => { - - // Mock sitemap entry formatter (will be in proxy.js) - function formatSitemapEntry(document, baseUrl) { - function escapeXml(str) { - return str.replace(/&/g, '&').replace(//g, '>'); - } - - const loc = `${baseUrl}/${document.id}`; - const lastmod = document.modifiedTime; - - return ` - ${escapeXml(loc)} - ${lastmod} - `; - } - - it('T029: should convert DriveDocument to XML url element', () => { - // Given: DriveDocument metadata - const document = { - id: '1BxAA_test123', - name: 'Test Document', - modifiedTime: '2026-03-06T10:30:00Z' - }; - const baseUrl = 'http://localhost:3000'; - - // When: Formatting sitemap entry - const xml = formatSitemapEntry(document, baseUrl); - - // Then: Should generate valid XML - assert.ok(xml.includes(''), 'Should contain opening url tag'); - assert.ok(xml.includes(''), 'Should contain closing url tag'); - assert.ok(xml.includes(''), 'Should contain loc element'); - assert.ok(xml.includes(''), 'Should contain closing loc tag'); - assert.ok(xml.includes(''), 'Should contain lastmod element'); - assert.ok(xml.includes(''), 'Should contain closing lastmod tag'); - }); - - it('T029: should include correct location URL with documentId', () => { - // Given: DriveDocument metadata - const document = { - id: '1BxAA_test123', - name: 'Test Document', - modifiedTime: '2026-03-06T10:30:00Z' - }; - const baseUrl = 'http://localhost:3000'; - - // When: Formatting sitemap entry - const xml = formatSitemapEntry(document, baseUrl); - - // Then: Location should point to adapter endpoint - assert.ok( - xml.includes(`http://localhost:3000/${document.id}`), - 'Should include correct location URL' - ); - }); - - it('T029: should include ISO 8601 lastmod timestamp', () => { - // Given: DriveDocument with modified time - const document = { - id: '1BxAA_test123', - name: 'Test Document', - modifiedTime: '2026-03-06T10:30:00Z' - }; - const baseUrl = 'http://localhost:3000'; - - // When: Formatting sitemap entry - const xml = formatSitemapEntry(document, baseUrl); - - // Then: Should include lastmod with ISO 8601 timestamp - assert.ok( - xml.includes('2026-03-06T10:30:00Z'), - 'Should include ISO 8601 lastmod timestamp' - ); - }); - - it('T029: should escape special XML characters in URL', () => { - // Given: DriveDocument with special characters in ID (edge case) - const document = { - id: '1BxAA-test&123', - name: 'Test Document', - modifiedTime: '2026-03-06T10:30:00Z' - }; - const baseUrl = 'http://localhost:3000'; - - // When: Formatting sitemap entry - const xml = formatSitemapEntry(document, baseUrl); - - // Then: Should escape & in URL - assert.ok( - xml.includes('&'), - 'Should escape special XML characters in URL' - ); - }); - - it('T029: should handle different baseUrl formats', () => { - // Given: Different baseUrl formats - const document = { - id: '1BxAA_test', - name: 'Test', - modifiedTime: '2026-03-06T10:30:00Z' - }; - - const baseUrls = [ - 'http://localhost:3000', - 'https://example.com', - 'https://api.example.com/v1' - ]; - - // When: Formatting with each baseUrl - // Then: Should generate correct loc for each - baseUrls.forEach(baseUrl => { - const xml = formatSitemapEntry(document, baseUrl); - assert.ok( - xml.includes(`${baseUrl}/${document.id}`), - `Should work with baseUrl: ${baseUrl}` - ); - }); - }); -}); - -describe('Unit: generateSitemap() Structure (T030)', () => { - - // Mock sitemap generator structure (will be in proxy.js) - function buildSitemapXml(documents, baseUrl) { - function escapeXml(str) { - return str.replace(/&/g, '&').replace(//g, '>'); - } - - let xml = '\n'; - xml += '\n'; - - documents.forEach(doc => { - const loc = `${baseUrl}/${doc.id}`; - xml += ` \n`; - xml += ` ${escapeXml(loc)}\n`; - xml += ` ${doc.modifiedTime}\n`; - xml += ` \n`; - }); - - xml += ''; - - return xml; - } - - it('T030: should build complete XML with declaration', () => { - // Given: Array of documents - const documents = [ - { id: '1BxAA_doc1', name: 'Doc 1', modifiedTime: '2026-03-06T10:00:00Z' } - ]; - const baseUrl = 'http://localhost:3000'; - - // When: Building sitemap XML - const xml = buildSitemapXml(documents, baseUrl); - - // Then: Should start with XML declaration - assert.ok( - xml.startsWith(' { - // Given: Array of documents - const documents = [ - { id: '1BxAA_doc1', name: 'Doc 1', modifiedTime: '2026-03-06T10:00:00Z' } - ]; - const baseUrl = 'http://localhost:3000'; - - // When: Building sitemap XML - const xml = buildSitemapXml(documents, baseUrl); - - // Then: Should include sitemap protocol namespace - assert.ok( - xml.includes(''), - 'Should include correct sitemap namespace' - ); - }); - - it('T030: should include closing urlset tag', () => { - // Given: Array of documents - const documents = [ - { id: '1BxAA_doc1', name: 'Doc 1', modifiedTime: '2026-03-06T10:00:00Z' } - ]; - const baseUrl = 'http://localhost:3000'; - - // When: Building sitemap XML - const xml = buildSitemapXml(documents, baseUrl); - - // Then: Should end with closing urlset tag - assert.ok(xml.endsWith(''), 'Should end with closing urlset tag'); - }); - - it('T030: should include multiple url entries for multiple documents', () => { - // Given: Multiple documents - const documents = [ - { id: '1BxAA_doc1', name: 'Doc 1', modifiedTime: '2026-03-06T10:00:00Z' }, - { id: '2CyBB_doc2', name: 'Doc 2', modifiedTime: '2026-03-06T11:00:00Z' }, - { id: '3DzCC_doc3', name: 'Doc 3', modifiedTime: '2026-03-06T12:00:00Z' } - ]; - const baseUrl = 'http://localhost:3000'; - - // When: Building sitemap XML - const xml = buildSitemapXml(documents, baseUrl); - - // Then: Should include all documents - const urlCount = (xml.match(//g) || []).length; - assert.equal(urlCount, 3, 'Should include 3 url entries'); - - // Then: Each document should have its loc - documents.forEach(doc => { - assert.ok( - xml.includes(`http://localhost:3000/${doc.id}`), - `Should include url entry for ${doc.id}` - ); - }); - }); - - it('T030: should handle empty document list', () => { - // Given: Empty documents array - const documents = []; - const baseUrl = 'http://localhost:3000'; - - // When: Building sitemap XML - const xml = buildSitemapXml(documents, baseUrl); - - // Then: Should still have valid XML structure - assert.ok(xml.includes(''), 'Should have urlset closing'); - - // Then: Should have no url entries - const urlCount = (xml.match(//g) || []).length; - assert.equal(urlCount, 0, 'Should have no url entries'); - }); - - it('T030: should generate valid XML that browsers can parse', () => { - // Given: Sample documents - const documents = [ - { id: '1BxAA_test', name: 'Test', modifiedTime: '2026-03-06T10:00:00Z' } - ]; - const baseUrl = 'http://localhost:3000'; - - // When: Building sitemap XML - const xml = buildSitemapXml(documents, baseUrl); - - // Then: XML should be well-formed (basic checks) - // Count opening and closing tags - const openingUrlset = (xml.match(//g) || []).length; - assert.equal(openingUrlset, closingUrlset, 'urlset tags should be balanced'); - - const openingUrl = (xml.match(//g) || []).length; - const closingUrl = (xml.match(/<\/url>/g) || []).length; - assert.equal(openingUrl, closingUrl, 'url tags should be balanced'); - }); -});