test: add unit tests for timestamp PDF feature
Add 31 tests across 3 test files covering: - TSA preset configuration (uniqueness, valid URLs, known providers) - Timestamp PDF page logic (file validation, output naming, UI state, drop zone) - TimestampNode workflow node (category, icon, description, presets)
This commit is contained in:
117
src/tests/timestamp-node.test.ts
Normal file
117
src/tests/timestamp-node.test.ts
Normal file
@@ -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<string, unknown> = {};
|
||||
},
|
||||
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<string, unknown> = {};
|
||||
},
|
||||
}));
|
||||
|
||||
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<string, unknown[]>) => inputs['pdf']),
|
||||
processBatch: vi.fn(
|
||||
async (
|
||||
inputs: Array<{ bytes: Uint8Array; filename: string }>,
|
||||
fn: (input: { bytes: Uint8Array; filename: string }) => Promise<unknown>
|
||||
) => {
|
||||
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');
|
||||
});
|
||||
});
|
||||
184
src/tests/timestamp-pdf-page.test.ts
Normal file
184
src/tests/timestamp-pdf-page.test.ts
Normal file
@@ -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 `
|
||||
<input type="file" id="file-input" accept=".pdf" />
|
||||
<div id="drop-zone"></div>
|
||||
<div id="file-display-area"></div>
|
||||
<div id="tsa-section" class="hidden">
|
||||
<select id="tsa-preset"></select>
|
||||
</div>
|
||||
<button id="process-btn" class="hidden" disabled></button>
|
||||
<button id="back-to-tools"></button>
|
||||
`;
|
||||
}
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
49
src/tests/timestamp-tsa.test.ts
Normal file
49
src/tests/timestamp-tsa.test.ts
Normal file
@@ -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');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user