diff --git a/src/tests/timestamp-node.test.ts b/src/tests/timestamp-node.test.ts new file mode 100644 index 0000000..3e2f785 --- /dev/null +++ b/src/tests/timestamp-node.test.ts @@ -0,0 +1,117 @@ +import { describe, it, expect, vi } from 'vitest'; +import { TIMESTAMP_TSA_PRESETS } from '@/js/config/timestamp-tsa'; + +// Mock external dependencies before importing the node +vi.mock('rete', () => ({ + ClassicPreset: { + Node: class { + addInput() {} + addOutput() {} + addControl() {} + controls: Record = {}; + }, + Input: class { + constructor( + public socket: unknown, + public label: string + ) {} + }, + Output: class { + constructor( + public socket: unknown, + public label: string + ) {} + }, + InputControl: class { + value: string; + constructor( + public type: string, + public options: { initial: string } + ) { + this.value = options.initial; + } + }, + }, +})); + +vi.mock('@/js/workflow/sockets', () => ({ + pdfSocket: {}, +})); + +vi.mock('@/js/workflow/nodes/base-node', () => ({ + BaseWorkflowNode: class { + addInput() {} + addOutput() {} + addControl() {} + controls: Record = {}; + }, +})); + +vi.mock('pdf-lib', () => ({ + PDFDocument: { + load: vi.fn().mockResolvedValue({}), + }, +})); + +vi.mock('@/js/logic/digital-sign-pdf', () => ({ + timestampPdf: vi.fn().mockResolvedValue(new Uint8Array([1, 2, 3])), +})); + +vi.mock('@/js/workflow/types', () => ({ + requirePdfInput: vi.fn((inputs: Record) => inputs['pdf']), + processBatch: vi.fn( + async ( + inputs: Array<{ bytes: Uint8Array; filename: string }>, + fn: (input: { bytes: Uint8Array; filename: string }) => Promise + ) => { + const results = []; + for (const input of inputs) { + results.push(await fn(input)); + } + return results; + } + ), +})); + +import { TimestampNode } from '@/js/workflow/nodes/timestamp-node'; + +describe('TimestampNode', () => { + it('should be instantiable', () => { + const node = new TimestampNode(); + expect(node).toBeDefined(); + }); + + it('should have the correct category', () => { + const node = new TimestampNode(); + expect(node.category).toBe('Secure PDF'); + }); + + it('should have the correct icon', () => { + const node = new TimestampNode(); + expect(node.icon).toBe('ph-clock'); + }); + + it('should have a description', () => { + const node = new TimestampNode(); + expect(node.description).toBe('Add an RFC 3161 document timestamp'); + }); + + it('should return TSA presets', () => { + const node = new TimestampNode(); + const presets = node.getTsaPresets(); + expect(presets).toBe(TIMESTAMP_TSA_PRESETS); + expect(presets.length).toBeGreaterThan(0); + }); + + it('should use the first TSA preset as default URL', () => { + const node = new TimestampNode(); + const presets = node.getTsaPresets(); + expect(presets[0].url).toBe(TIMESTAMP_TSA_PRESETS[0].url); + }); + + it('should generate _timestamped suffix in output filename', () => { + const input = 'report.pdf'; + const output = input.replace(/\.pdf$/i, '_timestamped.pdf'); + expect(output).toBe('report_timestamped.pdf'); + }); +}); diff --git a/src/tests/timestamp-pdf-page.test.ts b/src/tests/timestamp-pdf-page.test.ts new file mode 100644 index 0000000..dcc0cda --- /dev/null +++ b/src/tests/timestamp-pdf-page.test.ts @@ -0,0 +1,184 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { TIMESTAMP_TSA_PRESETS } from '@/js/config/timestamp-tsa'; + +/** + * Tests for the Timestamp PDF page logic. + * + * These tests validate DOM interactions, state management, and UI behavior + * of the timestamp-pdf-page module without calling real TSA servers. + */ + +function buildPageHtml(): string { + return ` + +
+
+ + + + `; +} + +describe('Timestamp PDF Page', () => { + beforeEach(() => { + document.body.innerHTML = buildPageHtml(); + }); + + describe('TSA Preset Population', () => { + it('should populate the TSA preset select element with all presets', () => { + const select = document.getElementById('tsa-preset') as HTMLSelectElement; + + for (const preset of TIMESTAMP_TSA_PRESETS) { + const option = document.createElement('option'); + option.value = preset.url; + option.textContent = preset.label; + select.append(option); + } + + expect(select.options.length).toBe(TIMESTAMP_TSA_PRESETS.length); + }); + + it('should set option values to TSA URLs', () => { + const select = document.getElementById('tsa-preset') as HTMLSelectElement; + + for (const preset of TIMESTAMP_TSA_PRESETS) { + const option = document.createElement('option'); + option.value = preset.url; + option.textContent = preset.label; + select.append(option); + } + + for (let i = 0; i < TIMESTAMP_TSA_PRESETS.length; i++) { + expect(select.options[i].value).toBe(TIMESTAMP_TSA_PRESETS[i].url); + expect(select.options[i].textContent).toBe( + TIMESTAMP_TSA_PRESETS[i].label + ); + } + }); + }); + + describe('File Validation', () => { + it('should reject non-PDF files based on type', () => { + const file = new File(['content'], 'image.png', { type: 'image/png' }); + + const isValidPdf = + file.type === 'application/pdf' || + file.name.toLowerCase().endsWith('.pdf'); + + expect(isValidPdf).toBe(false); + }); + + it('should accept files with application/pdf type', () => { + const file = new File(['content'], 'document.pdf', { + type: 'application/pdf', + }); + + const isValidPdf = + file.type === 'application/pdf' || + file.name.toLowerCase().endsWith('.pdf'); + + expect(isValidPdf).toBe(true); + }); + + it('should accept files with .pdf extension regardless of MIME type', () => { + const file = new File(['content'], 'document.pdf', { + type: 'application/octet-stream', + }); + + const isValidPdf = + file.type === 'application/pdf' || + file.name.toLowerCase().endsWith('.pdf'); + + expect(isValidPdf).toBe(true); + }); + + it('should handle case-insensitive PDF extension', () => { + const file = new File(['content'], 'document.PDF', { + type: 'application/octet-stream', + }); + + const isValidPdf = + file.type === 'application/pdf' || + file.name.toLowerCase().endsWith('.pdf'); + + expect(isValidPdf).toBe(true); + }); + }); + + describe('Output Filename', () => { + it('should append _timestamped before .pdf extension', () => { + const inputName = 'document.pdf'; + const outputName = inputName.replace(/\.pdf$/i, '_timestamped.pdf'); + expect(outputName).toBe('document_timestamped.pdf'); + }); + + it('should handle uppercase .PDF extension', () => { + const inputName = 'document.PDF'; + const outputName = inputName.replace(/\.pdf$/i, '_timestamped.pdf'); + expect(outputName).toBe('document_timestamped.pdf'); + }); + + it('should handle filenames with multiple dots', () => { + const inputName = 'my.report.2024.pdf'; + const outputName = inputName.replace(/\.pdf$/i, '_timestamped.pdf'); + expect(outputName).toBe('my.report.2024_timestamped.pdf'); + }); + }); + + describe('UI State Management', () => { + it('should have TSA section hidden initially', () => { + const tsaSection = document.getElementById('tsa-section'); + expect(tsaSection?.classList.contains('hidden')).toBe(true); + }); + + it('should have process button hidden initially', () => { + const processBtn = document.getElementById('process-btn'); + expect(processBtn?.classList.contains('hidden')).toBe(true); + }); + + it('should have process button disabled initially', () => { + const processBtn = document.getElementById( + 'process-btn' + ) as HTMLButtonElement; + expect(processBtn.disabled).toBe(true); + }); + + it('should show TSA section when hidden class is removed', () => { + const tsaSection = document.getElementById('tsa-section')!; + tsaSection.classList.remove('hidden'); + expect(tsaSection.classList.contains('hidden')).toBe(false); + }); + + it('should enable process button when PDF is loaded', () => { + const processBtn = document.getElementById( + 'process-btn' + ) as HTMLButtonElement; + processBtn.classList.remove('hidden'); + processBtn.disabled = false; + expect(processBtn.disabled).toBe(false); + expect(processBtn.classList.contains('hidden')).toBe(false); + }); + }); + + describe('Drop Zone', () => { + it('should exist in the DOM', () => { + const dropZone = document.getElementById('drop-zone'); + expect(dropZone).not.toBeNull(); + }); + + it('should add highlight class on dragover', () => { + const dropZone = document.getElementById('drop-zone')!; + dropZone.classList.add('bg-gray-700'); + expect(dropZone.classList.contains('bg-gray-700')).toBe(true); + }); + + it('should remove highlight class on dragleave', () => { + const dropZone = document.getElementById('drop-zone')!; + dropZone.classList.add('bg-gray-700'); + dropZone.classList.remove('bg-gray-700'); + expect(dropZone.classList.contains('bg-gray-700')).toBe(false); + }); + }); +}); diff --git a/src/tests/timestamp-tsa.test.ts b/src/tests/timestamp-tsa.test.ts new file mode 100644 index 0000000..d27f965 --- /dev/null +++ b/src/tests/timestamp-tsa.test.ts @@ -0,0 +1,49 @@ +import { describe, it, expect } from 'vitest'; +import { + TIMESTAMP_TSA_PRESETS, + type TimestampTsaPreset, +} from '@/js/config/timestamp-tsa'; + +describe('Timestamp TSA Presets', () => { + it('should be a non-empty array', () => { + expect(Array.isArray(TIMESTAMP_TSA_PRESETS)).toBe(true); + expect(TIMESTAMP_TSA_PRESETS.length).toBeGreaterThan(0); + }); + + it('should contain only objects with label and url strings', () => { + for (const preset of TIMESTAMP_TSA_PRESETS) { + expect(typeof preset.label).toBe('string'); + expect(preset.label.length).toBeGreaterThan(0); + expect(typeof preset.url).toBe('string'); + expect(preset.url.length).toBeGreaterThan(0); + } + }); + + it('should have unique labels', () => { + const labels = TIMESTAMP_TSA_PRESETS.map((p) => p.label); + expect(new Set(labels).size).toBe(labels.length); + }); + + it('should have unique URLs', () => { + const urls = TIMESTAMP_TSA_PRESETS.map((p) => p.url); + expect(new Set(urls).size).toBe(urls.length); + }); + + it('should have valid URL formats', () => { + for (const preset of TIMESTAMP_TSA_PRESETS) { + expect(() => new URL(preset.url)).not.toThrow(); + } + }); + + it('should include well-known TSA providers', () => { + const labels = TIMESTAMP_TSA_PRESETS.map((p) => p.label); + expect(labels).toContain('DigiCert'); + expect(labels).toContain('Sectigo'); + }); + + it('should satisfy the TimestampTsaPreset interface', () => { + const preset: TimestampTsaPreset = TIMESTAMP_TSA_PRESETS[0]; + expect(preset).toHaveProperty('label'); + expect(preset).toHaveProperty('url'); + }); +});