generated from coulomb/repo-seed
Add Binect SDK implementation, Explorer, and test suite
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>
This commit is contained in:
59
tests/client.test.ts
Normal file
59
tests/client.test.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { BinectClient } from '../src/client.js';
|
||||
import { DocumentsClient } from '../src/clients/documents.js';
|
||||
import { AttachmentsClient } from '../src/clients/attachments.js';
|
||||
import { SendingsClient } from '../src/clients/sendings.js';
|
||||
import { AccountsClient } from '../src/clients/accounts.js';
|
||||
import { InvoicesClient } from '../src/clients/invoices.js';
|
||||
|
||||
describe('BinectClient', () => {
|
||||
it('creates client with required config', () => {
|
||||
const client = new BinectClient({
|
||||
username: 'test@example.com',
|
||||
password: 'password123',
|
||||
});
|
||||
|
||||
expect(client).toBeInstanceOf(BinectClient);
|
||||
});
|
||||
|
||||
it('creates client with custom baseUrl', () => {
|
||||
const client = new BinectClient({
|
||||
username: 'test@example.com',
|
||||
password: 'password123',
|
||||
baseUrl: 'https://custom.api.com/v1',
|
||||
});
|
||||
|
||||
expect(client).toBeInstanceOf(BinectClient);
|
||||
});
|
||||
|
||||
describe('sub-clients', () => {
|
||||
let client: BinectClient;
|
||||
|
||||
beforeAll(() => {
|
||||
client = new BinectClient({
|
||||
username: 'test@example.com',
|
||||
password: 'password123',
|
||||
});
|
||||
});
|
||||
|
||||
it('has documents client', () => {
|
||||
expect(client.documents).toBeInstanceOf(DocumentsClient);
|
||||
});
|
||||
|
||||
it('has attachments client', () => {
|
||||
expect(client.attachments).toBeInstanceOf(AttachmentsClient);
|
||||
});
|
||||
|
||||
it('has sendings client', () => {
|
||||
expect(client.sendings).toBeInstanceOf(SendingsClient);
|
||||
});
|
||||
|
||||
it('has accounts client', () => {
|
||||
expect(client.accounts).toBeInstanceOf(AccountsClient);
|
||||
});
|
||||
|
||||
it('has invoices client', () => {
|
||||
expect(client.invoices).toBeInstanceOf(InvoicesClient);
|
||||
});
|
||||
});
|
||||
});
|
||||
89
tests/errors.test.ts
Normal file
89
tests/errors.test.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { BinectApiError, BinectAuthError } from '../src/errors.js';
|
||||
|
||||
describe('BinectApiError', () => {
|
||||
it('creates error with all properties', () => {
|
||||
const response = { error: 'Not found', message: 'Document not found' };
|
||||
const error = new BinectApiError('Not found', 404, '/documents/123', 'GET', response);
|
||||
|
||||
expect(error.name).toBe('BinectApiError');
|
||||
expect(error.message).toBe('Not found');
|
||||
expect(error.status).toBe(404);
|
||||
expect(error.endpoint).toBe('/documents/123');
|
||||
expect(error.method).toBe('GET');
|
||||
expect(error.response).toEqual(response);
|
||||
});
|
||||
|
||||
it('creates error without response', () => {
|
||||
const error = new BinectApiError('Server error', 500, '/documents', 'POST');
|
||||
|
||||
expect(error.status).toBe(500);
|
||||
expect(error.response).toBeNull();
|
||||
});
|
||||
|
||||
it('has correct inheritance', () => {
|
||||
const error = new BinectApiError('Test', 400, '/test', 'GET');
|
||||
|
||||
expect(error instanceof Error).toBe(true);
|
||||
expect(error instanceof BinectApiError).toBe(true);
|
||||
});
|
||||
|
||||
describe('toDetailedString', () => {
|
||||
it('includes all error details', () => {
|
||||
const response = {
|
||||
error: 'Validation error',
|
||||
message: 'Invalid document format',
|
||||
details: ['Page size incorrect', 'Missing address'],
|
||||
};
|
||||
const error = new BinectApiError('Validation failed', 400, '/documents', 'POST', response);
|
||||
|
||||
const detailed = error.toDetailedString();
|
||||
|
||||
expect(detailed).toContain('BinectApiError: Validation failed');
|
||||
expect(detailed).toContain('Status: 400');
|
||||
expect(detailed).toContain('Endpoint: POST /documents');
|
||||
expect(detailed).toContain('Error: Validation error');
|
||||
expect(detailed).toContain('Message: Invalid document format');
|
||||
expect(detailed).toContain('Details: Page size incorrect, Missing address');
|
||||
});
|
||||
|
||||
it('handles minimal error response', () => {
|
||||
const error = new BinectApiError('Server error', 500, '/test', 'GET');
|
||||
|
||||
const detailed = error.toDetailedString();
|
||||
|
||||
expect(detailed).toContain('BinectApiError: Server error');
|
||||
expect(detailed).toContain('Status: 500');
|
||||
// Should not contain "Error:" as a separate field (only in class name)
|
||||
expect(detailed).not.toContain(' Error:');
|
||||
expect(detailed).not.toContain(' Message:');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('BinectAuthError', () => {
|
||||
it('creates authentication error', () => {
|
||||
const error = new BinectAuthError('/accounts', 'GET');
|
||||
|
||||
expect(error.name).toBe('BinectAuthError');
|
||||
expect(error.message).toBe('Authentication failed: Invalid credentials');
|
||||
expect(error.status).toBe(401);
|
||||
expect(error.endpoint).toBe('/accounts');
|
||||
expect(error.method).toBe('GET');
|
||||
});
|
||||
|
||||
it('includes response when provided', () => {
|
||||
const response = { error: 'Unauthorized', message: 'Invalid credentials' };
|
||||
const error = new BinectAuthError('/accounts', 'GET', response);
|
||||
|
||||
expect(error.response).toEqual(response);
|
||||
});
|
||||
|
||||
it('inherits from BinectApiError', () => {
|
||||
const error = new BinectAuthError('/test', 'GET');
|
||||
|
||||
expect(error instanceof Error).toBe(true);
|
||||
expect(error instanceof BinectApiError).toBe(true);
|
||||
expect(error instanceof BinectAuthError).toBe(true);
|
||||
});
|
||||
});
|
||||
281
tests/helpers.test.ts
Normal file
281
tests/helpers.test.ts
Normal file
@@ -0,0 +1,281 @@
|
||||
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');
|
||||
});
|
||||
});
|
||||
});
|
||||
221
tests/http.test.ts
Normal file
221
tests/http.test.ts
Normal file
@@ -0,0 +1,221 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { HttpClient, encodeBasicAuth, DEFAULT_BASE_URL } from '../src/http.js';
|
||||
import { BinectApiError, BinectAuthError } from '../src/errors.js';
|
||||
|
||||
describe('encodeBasicAuth', () => {
|
||||
it('encodes credentials correctly', () => {
|
||||
const encoded = encodeBasicAuth('user@example.com', 'password123');
|
||||
// "user@example.com:password123" in base64
|
||||
expect(encoded).toBe('dXNlckBleGFtcGxlLmNvbTpwYXNzd29yZDEyMw==');
|
||||
});
|
||||
|
||||
it('handles special characters', () => {
|
||||
const encoded = encodeBasicAuth('user@example.com', 'p@ss:word!');
|
||||
const decoded = Buffer.from(encoded, 'base64').toString('utf-8');
|
||||
expect(decoded).toBe('user@example.com:p@ss:word!');
|
||||
});
|
||||
});
|
||||
|
||||
describe('DEFAULT_BASE_URL', () => {
|
||||
it('points to Binect API', () => {
|
||||
expect(DEFAULT_BASE_URL).toBe('https://app.binect.de/binectapi/v1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('HttpClient', () => {
|
||||
let client: HttpClient;
|
||||
let mockFetch: ReturnType<typeof vi.fn>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockFetch = vi.fn();
|
||||
global.fetch = mockFetch;
|
||||
|
||||
client = new HttpClient({
|
||||
baseUrl: 'https://api.example.com',
|
||||
username: 'testuser',
|
||||
password: 'testpass',
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('request', () => {
|
||||
it('makes GET request with auth header', async () => {
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
headers: new Headers({ 'content-type': 'application/json' }),
|
||||
text: async () => JSON.stringify({ id: '123' }),
|
||||
});
|
||||
|
||||
const result = await client.request<{ id: string }>({
|
||||
method: 'GET',
|
||||
path: '/test',
|
||||
});
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledWith(
|
||||
'https://api.example.com/test',
|
||||
expect.objectContaining({
|
||||
method: 'GET',
|
||||
headers: expect.objectContaining({
|
||||
Authorization: expect.stringMatching(/^Basic /),
|
||||
Accept: 'application/json',
|
||||
}),
|
||||
})
|
||||
);
|
||||
expect(result.id).toBe('123');
|
||||
});
|
||||
|
||||
it('makes POST request with JSON body', async () => {
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
headers: new Headers({ 'content-type': 'application/json' }),
|
||||
text: async () => JSON.stringify({ success: true }),
|
||||
});
|
||||
|
||||
await client.request({
|
||||
method: 'POST',
|
||||
path: '/test',
|
||||
body: { data: 'test' },
|
||||
});
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledWith(
|
||||
'https://api.example.com/test',
|
||||
expect.objectContaining({
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ data: 'test' }),
|
||||
headers: expect.objectContaining({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('adds query parameters to URL', async () => {
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
headers: new Headers({ 'content-type': 'application/json' }),
|
||||
text: async () => '{}',
|
||||
});
|
||||
|
||||
await client.request({
|
||||
method: 'GET',
|
||||
path: '/test',
|
||||
query: { limit: 10, offset: 20 },
|
||||
});
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledWith(
|
||||
'https://api.example.com/test?limit=10&offset=20',
|
||||
expect.any(Object)
|
||||
);
|
||||
});
|
||||
|
||||
it('skips undefined query parameters', async () => {
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
headers: new Headers({ 'content-type': 'application/json' }),
|
||||
text: async () => '{}',
|
||||
});
|
||||
|
||||
await client.request({
|
||||
method: 'GET',
|
||||
path: '/test',
|
||||
query: { limit: 10, offset: undefined },
|
||||
});
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledWith(
|
||||
'https://api.example.com/test?limit=10',
|
||||
expect.any(Object)
|
||||
);
|
||||
});
|
||||
|
||||
it('handles empty response', async () => {
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
headers: new Headers({ 'content-type': 'text/plain' }),
|
||||
text: async () => '',
|
||||
});
|
||||
|
||||
const result = await client.request({ method: 'DELETE', path: '/test' });
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it('throws BinectAuthError on 401', async () => {
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: false,
|
||||
status: 401,
|
||||
text: async () => JSON.stringify({ error: 'Unauthorized' }),
|
||||
});
|
||||
|
||||
await expect(
|
||||
client.request({ method: 'GET', path: '/test' })
|
||||
).rejects.toThrow(BinectAuthError);
|
||||
});
|
||||
|
||||
it('throws BinectApiError on other errors', async () => {
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: false,
|
||||
status: 404,
|
||||
text: async () => JSON.stringify({ message: 'Not found' }),
|
||||
});
|
||||
|
||||
await expect(
|
||||
client.request({ method: 'GET', path: '/test' })
|
||||
).rejects.toThrow(BinectApiError);
|
||||
|
||||
try {
|
||||
await client.request({ method: 'GET', path: '/test' });
|
||||
} catch (e) {
|
||||
if (e instanceof BinectApiError) {
|
||||
expect(e.status).toBe(404);
|
||||
expect(e.message).toBe('Not found');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('handles non-JSON error response', async () => {
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: false,
|
||||
status: 500,
|
||||
text: async () => 'Internal Server Error',
|
||||
});
|
||||
|
||||
// Now captures the raw text response as the error message
|
||||
await expect(
|
||||
client.request({ method: 'GET', path: '/test' })
|
||||
).rejects.toThrow('Internal Server Error');
|
||||
});
|
||||
});
|
||||
|
||||
describe('requestRaw', () => {
|
||||
it('returns raw response for binary data', async () => {
|
||||
const mockResponse = {
|
||||
ok: true,
|
||||
headers: new Headers({ 'content-type': 'application/pdf' }),
|
||||
blob: async () => new Blob(['pdf content']),
|
||||
};
|
||||
mockFetch.mockResolvedValueOnce(mockResponse);
|
||||
|
||||
const response = await client.requestRaw({
|
||||
method: 'GET',
|
||||
path: '/documents/123/pdf',
|
||||
});
|
||||
|
||||
expect(response).toBe(mockResponse);
|
||||
});
|
||||
|
||||
it('throws on error response', async () => {
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: false,
|
||||
status: 404,
|
||||
text: async () => JSON.stringify({ message: 'Document not found' }),
|
||||
});
|
||||
|
||||
await expect(
|
||||
client.requestRaw({ method: 'GET', path: '/documents/123/pdf' })
|
||||
).rejects.toThrow(BinectApiError);
|
||||
});
|
||||
});
|
||||
});
|
||||
52
tests/types.test.ts
Normal file
52
tests/types.test.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
DocumentStatus,
|
||||
EnvelopeType,
|
||||
FrankingType,
|
||||
ProductionCountry,
|
||||
ResponseFormat,
|
||||
} from '../src/types.js';
|
||||
|
||||
describe('Enums', () => {
|
||||
describe('DocumentStatus', () => {
|
||||
it('has correct values', () => {
|
||||
expect(DocumentStatus.IN_PREPARATION).toBe(1);
|
||||
expect(DocumentStatus.SHIPPABLE).toBe(2);
|
||||
expect(DocumentStatus.PRODUCTION_QUEUE).toBe(3);
|
||||
expect(DocumentStatus.PRINTING).toBe(4);
|
||||
expect(DocumentStatus.SENT).toBe(5);
|
||||
expect(DocumentStatus.CANCELED).toBe(6);
|
||||
expect(DocumentStatus.ERRONEOUS).toBe(7);
|
||||
});
|
||||
});
|
||||
|
||||
describe('EnvelopeType', () => {
|
||||
it('has correct values', () => {
|
||||
expect(EnvelopeType.DINLANG).toBe('DINLANG');
|
||||
expect(EnvelopeType.C4).toBe('C4');
|
||||
});
|
||||
});
|
||||
|
||||
describe('FrankingType', () => {
|
||||
it('has correct values', () => {
|
||||
expect(FrankingType.UNSPECIFIED).toBe('UNSPECIFIED');
|
||||
expect(FrankingType.STANDARD_FRANKING).toBe('STANDARD_FRANKING');
|
||||
expect(FrankingType.DV_FRANKING).toBe('DV_FRANKING');
|
||||
});
|
||||
});
|
||||
|
||||
describe('ProductionCountry', () => {
|
||||
it('has correct values', () => {
|
||||
expect(ProductionCountry.UNSPECIFIED).toBe('UNSPECIFIED');
|
||||
expect(ProductionCountry.DE).toBe('DE');
|
||||
expect(ProductionCountry.AT).toBe('AT');
|
||||
});
|
||||
});
|
||||
|
||||
describe('ResponseFormat', () => {
|
||||
it('has correct values', () => {
|
||||
expect(ResponseFormat.FULL).toBe('FULL');
|
||||
expect(ResponseFormat.SHORT).toBe('SHORT');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user