generated from coulomb/repo-seed
SDK (@binect/js): - BinectClient with domain sub-clients (documents, sendings, accounts, attachments, invoices) - HTTP Basic Auth, native fetch only (no runtime dependencies) - TypeScript types matching Binect API vocabulary - Status predicates and polling helpers in helpers.ts - Structured error handling (BinectApiError, BinectAuthError) Explorer: - Standalone browser-based API explorer (explorer/index.html) - Interactive testing without code Tests: - Unit tests for client, types, errors, helpers, http - E2E tests for upload/delete and send/cancel workflows Also includes: - Architecture Decision Records (ADRs) - Example DIN 5008 letter PDFs for testing - API specification research notes Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
282 lines
8.9 KiB
TypeScript
282 lines
8.9 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import {
|
|
isShippable,
|
|
isErroneous,
|
|
isInPreparation,
|
|
isInProductionQueue,
|
|
isPrinting,
|
|
isSent,
|
|
isCanceled,
|
|
isTerminal,
|
|
isCancelable,
|
|
getErrors,
|
|
getWarnings,
|
|
getInfoMessages,
|
|
hasErrors,
|
|
hasWarnings,
|
|
getStatusDescription,
|
|
pollUntil,
|
|
} from '../src/helpers.js';
|
|
import { DocumentStatus } from '../src/types.js';
|
|
import type { Document, ValidationMessage } from '../src/types.js';
|
|
|
|
// Helper to create mock documents with specific status
|
|
function createMockDocument(status: DocumentStatus, validationMessages?: ValidationMessage[]): Document {
|
|
return {
|
|
id: 12345,
|
|
filename: 'test-doc.pdf',
|
|
numberOfPages: 1,
|
|
status: {
|
|
code: status,
|
|
text: 'Test status',
|
|
},
|
|
documentType: 'LETTER',
|
|
letter: {
|
|
letterType: 'LETTERDATA',
|
|
letterData: {
|
|
options: {
|
|
simplex: false,
|
|
color: false,
|
|
franking: 'STANDARD_FRANKING',
|
|
productionCountry: 'DE',
|
|
envelope: 'DINLANG',
|
|
},
|
|
},
|
|
errors: validationMessages ?? [],
|
|
},
|
|
};
|
|
}
|
|
|
|
describe('Status Predicates', () => {
|
|
describe('isShippable', () => {
|
|
it('returns true for status SHIPPABLE', () => {
|
|
const doc = createMockDocument(DocumentStatus.SHIPPABLE);
|
|
expect(isShippable(doc)).toBe(true);
|
|
});
|
|
|
|
it('returns false for other statuses', () => {
|
|
expect(isShippable(createMockDocument(DocumentStatus.IN_PREPARATION))).toBe(false);
|
|
expect(isShippable(createMockDocument(DocumentStatus.ERRONEOUS))).toBe(false);
|
|
expect(isShippable(createMockDocument(DocumentStatus.SENT))).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('isErroneous', () => {
|
|
it('returns true for status ERRONEOUS', () => {
|
|
const doc = createMockDocument(DocumentStatus.ERRONEOUS);
|
|
expect(isErroneous(doc)).toBe(true);
|
|
});
|
|
|
|
it('returns false for other statuses', () => {
|
|
expect(isErroneous(createMockDocument(DocumentStatus.SHIPPABLE))).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('isInPreparation', () => {
|
|
it('returns true for status IN_PREPARATION', () => {
|
|
const doc = createMockDocument(DocumentStatus.IN_PREPARATION);
|
|
expect(isInPreparation(doc)).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('isInProductionQueue', () => {
|
|
it('returns true for status PRODUCTION_QUEUE', () => {
|
|
const doc = createMockDocument(DocumentStatus.PRODUCTION_QUEUE);
|
|
expect(isInProductionQueue(doc)).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('isPrinting', () => {
|
|
it('returns true for status PRINTING', () => {
|
|
const doc = createMockDocument(DocumentStatus.PRINTING);
|
|
expect(isPrinting(doc)).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('isSent', () => {
|
|
it('returns true for status SENT', () => {
|
|
const doc = createMockDocument(DocumentStatus.SENT);
|
|
expect(isSent(doc)).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('isCanceled', () => {
|
|
it('returns true for status CANCELED', () => {
|
|
const doc = createMockDocument(DocumentStatus.CANCELED);
|
|
expect(isCanceled(doc)).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('isTerminal', () => {
|
|
it('returns true for SENT, CANCELED, and ERRONEOUS', () => {
|
|
expect(isTerminal(createMockDocument(DocumentStatus.SENT))).toBe(true);
|
|
expect(isTerminal(createMockDocument(DocumentStatus.CANCELED))).toBe(true);
|
|
expect(isTerminal(createMockDocument(DocumentStatus.ERRONEOUS))).toBe(true);
|
|
});
|
|
|
|
it('returns false for non-terminal statuses', () => {
|
|
expect(isTerminal(createMockDocument(DocumentStatus.IN_PREPARATION))).toBe(false);
|
|
expect(isTerminal(createMockDocument(DocumentStatus.SHIPPABLE))).toBe(false);
|
|
expect(isTerminal(createMockDocument(DocumentStatus.PRODUCTION_QUEUE))).toBe(false);
|
|
expect(isTerminal(createMockDocument(DocumentStatus.PRINTING))).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('isCancelable', () => {
|
|
it('returns true for PRODUCTION_QUEUE and PRINTING', () => {
|
|
expect(isCancelable(createMockDocument(DocumentStatus.PRODUCTION_QUEUE))).toBe(true);
|
|
expect(isCancelable(createMockDocument(DocumentStatus.PRINTING))).toBe(true);
|
|
});
|
|
|
|
it('returns false for non-cancelable statuses', () => {
|
|
expect(isCancelable(createMockDocument(DocumentStatus.IN_PREPARATION))).toBe(false);
|
|
expect(isCancelable(createMockDocument(DocumentStatus.SENT))).toBe(false);
|
|
expect(isCancelable(createMockDocument(DocumentStatus.CANCELED))).toBe(false);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Validation Helpers', () => {
|
|
const mockMessages: ValidationMessage[] = [
|
|
{ type: 'ERROR', code: 'ERR001', message: 'Error 1' },
|
|
{ type: 'ERROR', code: 'ERR002', message: 'Error 2' },
|
|
{ type: 'WARNING', code: 'WARN001', message: 'Warning 1' },
|
|
{ type: 'INFO', code: 'INFO001', message: 'Info 1' },
|
|
];
|
|
|
|
describe('getErrors', () => {
|
|
it('returns only error messages', () => {
|
|
const doc = createMockDocument(DocumentStatus.ERRONEOUS, mockMessages);
|
|
const errors = getErrors(doc);
|
|
expect(errors).toHaveLength(2);
|
|
expect(errors.every(m => m.type === 'ERROR')).toBe(true);
|
|
});
|
|
|
|
it('returns empty array when no validation messages', () => {
|
|
const doc = createMockDocument(DocumentStatus.SHIPPABLE);
|
|
expect(getErrors(doc)).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe('getWarnings', () => {
|
|
it('returns only warning messages', () => {
|
|
const doc = createMockDocument(DocumentStatus.SHIPPABLE, mockMessages);
|
|
const warnings = getWarnings(doc);
|
|
expect(warnings).toHaveLength(1);
|
|
expect(warnings[0]?.type).toBe('WARNING');
|
|
});
|
|
});
|
|
|
|
describe('getInfoMessages', () => {
|
|
it('returns only info messages', () => {
|
|
const doc = createMockDocument(DocumentStatus.SHIPPABLE, mockMessages);
|
|
const info = getInfoMessages(doc);
|
|
expect(info).toHaveLength(1);
|
|
expect(info[0]?.type).toBe('INFO');
|
|
});
|
|
});
|
|
|
|
describe('hasErrors', () => {
|
|
it('returns true when document has errors', () => {
|
|
const doc = createMockDocument(DocumentStatus.ERRONEOUS, mockMessages);
|
|
expect(hasErrors(doc)).toBe(true);
|
|
});
|
|
|
|
it('returns false when document has no errors', () => {
|
|
const doc = createMockDocument(DocumentStatus.SHIPPABLE, [
|
|
{ type: 'WARNING', code: 'WARN001', message: 'Warning' },
|
|
]);
|
|
expect(hasErrors(doc)).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('hasWarnings', () => {
|
|
it('returns true when document has warnings', () => {
|
|
const doc = createMockDocument(DocumentStatus.SHIPPABLE, mockMessages);
|
|
expect(hasWarnings(doc)).toBe(true);
|
|
});
|
|
|
|
it('returns false when document has no warnings', () => {
|
|
const doc = createMockDocument(DocumentStatus.SHIPPABLE, [
|
|
{ type: 'INFO', code: 'INFO001', message: 'Info' },
|
|
]);
|
|
expect(hasWarnings(doc)).toBe(false);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('getStatusDescription', () => {
|
|
it('returns correct descriptions for all statuses', () => {
|
|
expect(getStatusDescription(DocumentStatus.IN_PREPARATION)).toBe('In preparation');
|
|
expect(getStatusDescription(DocumentStatus.SHIPPABLE)).toBe('Ready to ship');
|
|
expect(getStatusDescription(DocumentStatus.PRODUCTION_QUEUE)).toBe('In production queue');
|
|
expect(getStatusDescription(DocumentStatus.PRINTING)).toBe('Printing');
|
|
expect(getStatusDescription(DocumentStatus.SENT)).toBe('Sent');
|
|
expect(getStatusDescription(DocumentStatus.CANCELED)).toBe('Canceled');
|
|
expect(getStatusDescription(DocumentStatus.ERRONEOUS)).toBe('Has errors');
|
|
});
|
|
|
|
it('returns "Unknown status" for invalid status', () => {
|
|
expect(getStatusDescription(999 as DocumentStatus)).toBe('Unknown status');
|
|
});
|
|
});
|
|
|
|
describe('Polling Utilities', () => {
|
|
describe('pollUntil', () => {
|
|
it('returns immediately when condition is met', async () => {
|
|
let callCount = 0;
|
|
const result = await pollUntil(
|
|
async () => {
|
|
callCount++;
|
|
return { value: 42 };
|
|
},
|
|
(r) => r.value === 42,
|
|
{ intervalMs: 10 }
|
|
);
|
|
|
|
expect(result.value).toBe(42);
|
|
expect(callCount).toBe(1);
|
|
});
|
|
|
|
it('polls until condition is met', async () => {
|
|
let callCount = 0;
|
|
const result = await pollUntil(
|
|
async () => {
|
|
callCount++;
|
|
return { value: callCount };
|
|
},
|
|
(r) => r.value >= 3,
|
|
{ intervalMs: 10 }
|
|
);
|
|
|
|
expect(result.value).toBe(3);
|
|
expect(callCount).toBe(3);
|
|
});
|
|
|
|
it('throws when max attempts exceeded', async () => {
|
|
await expect(
|
|
pollUntil(
|
|
async () => ({ value: 1 }),
|
|
(r) => r.value > 100,
|
|
{ intervalMs: 10, maxAttempts: 3 }
|
|
)
|
|
).rejects.toThrow('Polling exceeded maximum attempts (3)');
|
|
});
|
|
|
|
it('respects abort signal', async () => {
|
|
const controller = new AbortController();
|
|
|
|
// Abort immediately
|
|
controller.abort();
|
|
|
|
await expect(
|
|
pollUntil(
|
|
async () => ({ value: 1 }),
|
|
() => false,
|
|
{ intervalMs: 10, signal: controller.signal }
|
|
)
|
|
).rejects.toThrow('Polling aborted');
|
|
});
|
|
});
|
|
});
|