Added new feature for document export, including API contracts, data model, implementation plan, and tests. Updated related configurations and instructions.

This commit is contained in:
2026-03-10 16:25:09 -05:00
parent 2acb04ad76
commit bf6f2eebd6
22 changed files with 2856 additions and 64 deletions

View File

@@ -0,0 +1,45 @@
/**
* Contract Tests: Document Export Endpoint
*
* Tests the HTTP contract for /documents/:documentId endpoint
* Verifies request/response behavior against the API contract specification
*/
import { describe, it } from 'node:test';
import assert from 'node:assert';
describe('Document Export Endpoint Contract', () => {
describe('Valid Google Workspace Document', () => {
it('should return 200 with correct Content-Type header', async () => {
// Test: Valid document returns 200 OK with appropriate Content-Type
// TODO: Implement once /documents/:documentId route exists in proxy.js
assert.ok(true, 'Test not yet implemented - awaiting route implementation');
});
it('should return Content-Disposition header with inline and correct filename', async () => {
// Test: Response includes Content-Disposition: inline; filename="..."
// TODO: Implement once headers are set in proxy.js
assert.ok(true, 'Test not yet implemented - awaiting implementation');
});
});
describe('Format-Specific Responses', () => {
it('should return text/x-markdown Content-Type for Markdown export', async () => {
// Test: Document with Markdown export returns text/x-markdown
// TODO: Implement once format selection is in proxy.js
assert.ok(true, 'Test not yet implemented - awaiting implementation');
});
it('should return text/html Content-Type when Markdown unavailable', async () => {
// Test: Falls back to HTML when Markdown not available
// TODO: Implement once format selection is in proxy.js
assert.ok(true, 'Test not yet implemented - awaiting implementation');
});
it('should return application/pdf Content-Type for PDF-only export', async () => {
// Test: Falls back to PDF when Markdown and HTML unavailable
// TODO: Implement once format selection is in proxy.js
assert.ok(true, 'Test not yet implemented - awaiting implementation');
});
});
});

View File

@@ -0,0 +1,39 @@
/**
* Integration Tests: Google Drive Export
*
* Tests integration with Google Drive API for document export
* Uses mocks to avoid real API calls during testing
*/
import { describe, it, mock, beforeEach } from 'node:test';
import assert from 'node:assert';
describe('Google Drive Export Integration', () => {
describe('Metadata Fetch', () => {
it('should fetch metadata from Google Drive API with correct fields', async () => {
// This test will verify that we request the correct fields from Google Drive API
// Fields: id, name, mimeType, exportLinks
// TODO: Implement once proxy.js has the document export route
assert.ok(true, 'Test not yet implemented - awaiting route implementation');
});
});
describe('Format Selection', () => {
it('should select first available export format from priority list', async () => {
// This test will verify that format selection respects priority: Markdown > HTML > PDF
// TODO: Implement once proxy.js has format selection logic
assert.ok(true, 'Test not yet implemented - awaiting implementation');
});
});
describe('Content Streaming', () => {
it('should stream content from Google Drive export link to response', async () => {
// This test will verify that content is streamed (not buffered)
// TODO: Implement once proxy.js has streaming logic
assert.ok(true, 'Test not yet implemented - awaiting implementation');
});
});
});

View File

@@ -0,0 +1,176 @@
/**
* Unit Tests: Export Headers Generation
*
* Tests Content-Disposition header generation logic
* Verifies filename sanitization and extension handling
*/
import { describe, it } from 'node:test';
import assert from 'node:assert';
// Load helper functions using vm.Script pattern
import { readFileSync } from 'fs';
import { Script } from 'vm';
// Create VM context with required globals
const vmContext = {
crypto: globalThis.crypto,
console: console
};
// Load googleDriveAdapterHelper.js
const helperCode = readFileSync('./src/globalVariables/googleDriveAdapterHelper.js', 'utf8');
const wrappedCode = `(function() {\n${helperCode}\n})()`;
const script = new Script(wrappedCode);
const helpers = script.runInNewContext(vmContext);
describe('Export Headers', () => {
describe('sanitizeFilename', () => {
it('should preserve valid alphanumeric filenames', () => {
const result = helpers.sanitizeFilename('MyDocument123');
assert.strictEqual(result, 'MyDocument123');
});
it('should replace spaces with underscores', () => {
const result = helpers.sanitizeFilename('My Important Document');
assert.strictEqual(result, 'My_Important_Document');
});
it('should preserve hyphens and dots', () => {
const result = helpers.sanitizeFilename('my-document.v2.final');
assert.strictEqual(result, 'my-document.v2.final');
});
it('should replace special characters with underscores', () => {
const result = helpers.sanitizeFilename('My<>Document:with*chars?');
assert.strictEqual(result, 'My__Document_with_chars_');
});
it('should remove leading dots', () => {
const result = helpers.sanitizeFilename('.hidden-file');
assert.strictEqual(result, 'hidden-file');
});
it('should remove trailing dots', () => {
const result = helpers.sanitizeFilename('document...');
assert.strictEqual(result, 'document');
});
it('should collapse multiple dots', () => {
const result = helpers.sanitizeFilename('my....document');
assert.strictEqual(result, 'my.document');
});
it('should handle null input', () => {
const result = helpers.sanitizeFilename(null);
assert.strictEqual(result, 'document');
});
it('should handle undefined input', () => {
const result = helpers.sanitizeFilename(undefined);
assert.strictEqual(result, 'document');
});
it('should handle empty string', () => {
const result = helpers.sanitizeFilename('');
assert.strictEqual(result, 'document');
});
it('should truncate very long filenames to 255 characters', () => {
const longName = 'a'.repeat(300);
const result = helpers.sanitizeFilename(longName);
assert.strictEqual(result.length, 255);
});
it('should handle unicode characters', () => {
const result = helpers.sanitizeFilename('Документ 文档');
// Non-ASCII chars should be replaced
assert.ok(result.includes('_'));
});
});
describe('getFileExtension', () => {
it('should return "md" for text/x-markdown', () => {
const result = helpers.getFileExtension('text/x-markdown');
assert.strictEqual(result, 'md');
});
it('should return "html" for text/html', () => {
const result = helpers.getFileExtension('text/html');
assert.strictEqual(result, 'html');
});
it('should return "pdf" for application/pdf', () => {
const result = helpers.getFileExtension('application/pdf');
assert.strictEqual(result, 'pdf');
});
it('should return "txt" for text/plain', () => {
const result = helpers.getFileExtension('text/plain');
assert.strictEqual(result, 'txt');
});
it('should return "json" for application/json', () => {
const result = helpers.getFileExtension('application/json');
assert.strictEqual(result, 'json');
});
it('should return "bin" for unknown mime types', () => {
const result = helpers.getFileExtension('application/octet-stream');
assert.strictEqual(result, 'bin');
});
it('should return "bin" for null input', () => {
const result = helpers.getFileExtension(null);
assert.strictEqual(result, 'bin');
});
it('should return "bin" for undefined input', () => {
const result = helpers.getFileExtension(undefined);
assert.strictEqual(result, 'bin');
});
});
describe('Content-Disposition header integration', () => {
it('should generate correct header for markdown file', () => {
const filename = 'Meeting Notes Q1 2026';
const sanitized = helpers.sanitizeFilename(filename);
const extension = helpers.getFileExtension('text/x-markdown');
const header = `inline; filename="${sanitized}.${extension}"`;
assert.strictEqual(header, 'inline; filename="Meeting_Notes_Q1_2026.md"');
});
it('should generate correct header for html file', () => {
const filename = 'Project<Plan>';
const sanitized = helpers.sanitizeFilename(filename);
const extension = helpers.getFileExtension('text/html');
const header = `inline; filename="${sanitized}.${extension}"`;
assert.strictEqual(header, 'inline; filename="Project_Plan_.html"');
});
it('should generate correct header for pdf file', () => {
const filename = 'Annual Report 2026';
const sanitized = helpers.sanitizeFilename(filename);
const extension = helpers.getFileExtension('application/pdf');
const header = `inline; filename="${sanitized}.${extension}"`;
assert.strictEqual(header, 'inline; filename="Annual_Report_2026.pdf"');
});
it('should handle filename with existing extension', () => {
const filename = 'document.old.txt';
const sanitized = helpers.sanitizeFilename(filename);
const extension = helpers.getFileExtension('text/x-markdown');
const header = `inline; filename="${sanitized}.${extension}"`;
// Should preserve the dots in filename and add new extension
assert.strictEqual(header, 'inline; filename="document.old.txt.md"');
});
});
});

View File

@@ -0,0 +1,127 @@
/**
* Unit Tests: Format Selection Logic
*
* Tests the selectExportFormat helper function
* Verifies priority ordering: Markdown > HTML > PDF
*/
import { describe, it } from 'node:test';
import assert from 'node:assert';
// Load helper functions using vm.Script pattern (similar to proxy.js)
import { readFileSync } from 'fs';
import { Script } from 'vm';
// Create VM context with required globals
const vmContext = {
crypto: globalThis.crypto,
console: console
};
// Load googleDriveAdapterHelper.js
const helperCode = readFileSync('./src/globalVariables/googleDriveAdapterHelper.js', 'utf8');
const wrappedCode = `(function() {\n${helperCode}\n})()`;
const script = new Script(wrappedCode);
const helpers = script.runInNewContext(vmContext);
describe('Format Selection', () => {
describe('selectExportFormat', () => {
it('should prioritize text/x-markdown over other formats', () => {
const exportLinks = {
'text/x-markdown': 'https://example.com/export?format=md',
'text/html': 'https://example.com/export?format=html',
'application/pdf': 'https://example.com/export?format=pdf'
};
const result = helpers.selectExportFormat(exportLinks);
assert.strictEqual(result.contentType, 'text/x-markdown');
assert.strictEqual(result.extension, 'md');
assert.strictEqual(result.url, 'https://example.com/export?format=md');
});
it('should select text/html when markdown is unavailable', () => {
const exportLinks = {
'text/html': 'https://example.com/export?format=html',
'application/pdf': 'https://example.com/export?format=pdf'
};
const result = helpers.selectExportFormat(exportLinks);
assert.strictEqual(result.contentType, 'text/html');
assert.strictEqual(result.extension, 'html');
assert.strictEqual(result.url, 'https://example.com/export?format=html');
});
it('should select application/pdf when markdown and html are unavailable', () => {
const exportLinks = {
'application/pdf': 'https://example.com/export?format=pdf'
};
const result = helpers.selectExportFormat(exportLinks);
assert.strictEqual(result.contentType, 'application/pdf');
assert.strictEqual(result.extension, 'pdf');
assert.strictEqual(result.url, 'https://example.com/export?format=pdf');
});
it('should return null when no supported formats are available', () => {
const exportLinks = {
'text/plain': 'https://example.com/export?format=txt',
'application/json': 'https://example.com/export?format=json'
};
const result = helpers.selectExportFormat(exportLinks);
assert.strictEqual(result, null);
});
it('should return null when exportLinks is null', () => {
const result = helpers.selectExportFormat(null);
assert.strictEqual(result, null);
});
it('should return null when exportLinks is undefined', () => {
const result = helpers.selectExportFormat(undefined);
assert.strictEqual(result, null);
});
it('should return null when exportLinks is empty object', () => {
const result = helpers.selectExportFormat({});
assert.strictEqual(result, null);
});
it('should respect priority order even when formats appear in different order', () => {
const exportLinks = {
'application/pdf': 'https://example.com/export?format=pdf',
'text/x-markdown': 'https://example.com/export?format=md',
'text/html': 'https://example.com/export?format=html'
};
const result = helpers.selectExportFormat(exportLinks);
// Should still select Markdown despite PDF being first in object
assert.strictEqual(result.contentType, 'text/x-markdown');
});
});
describe('EXPORT_FORMATS constant', () => {
it('should define correct priority order', () => {
assert.ok(Array.isArray(helpers.EXPORT_FORMATS));
assert.strictEqual(helpers.EXPORT_FORMATS.length, 3);
// Verify order: Markdown > HTML > PDF
assert.strictEqual(helpers.EXPORT_FORMATS[0].mimeType, 'text/x-markdown');
assert.strictEqual(helpers.EXPORT_FORMATS[0].extension, 'md');
assert.strictEqual(helpers.EXPORT_FORMATS[1].mimeType, 'text/html');
assert.strictEqual(helpers.EXPORT_FORMATS[1].extension, 'html');
assert.strictEqual(helpers.EXPORT_FORMATS[2].mimeType, 'application/pdf');
assert.strictEqual(helpers.EXPORT_FORMATS[2].extension, 'pdf');
});
});
});