Files
binect-js/tests/e2e.test.ts
tegwick 20462b48b3 Add e2e test for document send and cancel workflow
Tests the full letter lifecycle: upload → send → poll for status
transition → cancel. Includes polling logic to wait for document
status to transition from IN_PREPARATION to PRODUCTION_QUEUE before
attempting cancel. Gracefully handles insufficient balance errors.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-14 23:08:25 +01:00

235 lines
8.8 KiB
TypeScript

import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { readFileSync } from 'fs';
import { join } from 'path';
import { BinectClient } from '../src/client.js';
import { DocumentStatus } from '../src/types.js';
/**
* End-to-end test for Binect API integration.
*
* Requires environment variables:
* BINECT_USERNAME - Binect account email
* BINECT_PASSWORD - Binect account password
*
* Run with: BINECT_USERNAME=user@example.com BINECT_PASSWORD=pass npm test
*/
describe('E2E: Document Upload and Delete', () => {
const username = process.env['BINECT_USERNAME'];
const password = process.env['BINECT_PASSWORD'];
// Skip tests if credentials not provided
const runTests = username && password;
let client: BinectClient;
let uploadedDocumentId: string | null = null;
beforeAll(() => {
if (runTests) {
client = new BinectClient({ username, password });
}
});
afterAll(async () => {
// Cleanup: ensure document is deleted even if test fails
if (runTests && uploadedDocumentId && client) {
try {
await client.documents.delete(uploadedDocumentId);
} catch {
// Ignore cleanup errors
}
}
});
it.skipIf(!runTests)('should upload a PDF document', async () => {
// Read the test PDF file
const pdfPath = join(process.cwd(), 'examples/din5008/260114-brief-testbriefBinectJs.pdf');
const pdfBuffer = readFileSync(pdfPath);
const pdfContent = pdfBuffer.toString('base64');
// Upload the document
const document = await client.documents.upload({
content: pdfContent,
filename: '260114-brief-testbriefBinectJs.pdf',
color: false,
simplex: false, // false = duplex (double-sided)
envelope: 'DINLANG',
franking: 'STANDARD_FRANKING',
});
// Debug: log the full response
console.log('Upload response:', JSON.stringify(document, null, 2));
// Store document ID for cleanup (API returns numeric id)
uploadedDocumentId = String(document.id);
// Verify upload succeeded
expect(document.id).toBeTruthy();
expect(typeof document.id).toBe('number');
expect(document.numberOfPages).toBeGreaterThan(0);
// Status should be either IN_PREPARATION (1) or SHIPPABLE (2) or ERRONEOUS (7)
expect([
DocumentStatus.IN_PREPARATION,
DocumentStatus.SHIPPABLE,
DocumentStatus.ERRONEOUS,
]).toContain(document.status.code);
console.log(`Uploaded document: ${document.id}, status: ${document.status.code} (${document.status.text}), pages: ${document.numberOfPages}`);
});
it.skipIf(!runTests)('should retrieve the uploaded document', async () => {
expect(uploadedDocumentId).toBeTruthy();
const document = await client.documents.get(uploadedDocumentId!);
expect(String(document.id)).toBe(uploadedDocumentId);
expect(document.numberOfPages).toBeGreaterThan(0);
console.log(`Retrieved document: ${document.id}, status: ${document.status.code} (${document.status.text})`);
});
it.skipIf(!runTests)('should delete the uploaded document', async () => {
expect(uploadedDocumentId).toBeTruthy();
// Delete the document
await client.documents.delete(uploadedDocumentId!);
console.log(`Deleted document: ${uploadedDocumentId}`);
// Mark as deleted so afterAll doesn't try to delete again
uploadedDocumentId = null;
// Verify deletion by trying to get the document (should fail)
try {
await client.documents.get(uploadedDocumentId!);
// If we get here, the document wasn't deleted
expect.fail('Document should have been deleted');
} catch (error) {
// Expected: document not found
expect(error).toBeDefined();
}
});
it.skipIf(runTests)('skipped: no credentials provided', () => {
console.log('E2E tests skipped: Set BINECT_USERNAME and BINECT_PASSWORD environment variables to run');
expect(true).toBe(true);
});
});
describe('E2E: Document Send and Cancel', () => {
const username = process.env['BINECT_USERNAME'];
const password = process.env['BINECT_PASSWORD'];
const runTests = username && password;
let client: BinectClient;
let uploadedDocumentId: string | null = null;
beforeAll(() => {
if (runTests) {
client = new BinectClient({ username, password });
}
});
afterAll(async () => {
// Cleanup: try to delete the document if it still exists
if (runTests && uploadedDocumentId && client) {
try {
await client.documents.delete(uploadedDocumentId);
console.log(`Cleanup: Deleted document ${uploadedDocumentId}`);
} catch {
// Document may already be deleted or in a state that can't be deleted
console.log(`Cleanup: Could not delete document ${uploadedDocumentId} (may already be processed)`);
}
}
});
it.skipIf(!runTests)('should upload and attempt to send a document', async () => {
// Step 1: Upload the document
const pdfPath = join(process.cwd(), 'examples/din5008/260114-brief-testbriefBinectJs.pdf');
const pdfBuffer = readFileSync(pdfPath);
const pdfContent = pdfBuffer.toString('base64');
const document = await client.documents.upload({
content: pdfContent,
filename: 'send-cancel-test.pdf',
color: false,
simplex: false,
envelope: 'DINLANG',
franking: 'STANDARD_FRANKING',
});
uploadedDocumentId = String(document.id);
console.log(`Uploaded document: ${document.id}, status: ${document.status.code} (${document.status.text})`);
expect(document.id).toBeTruthy();
expect(document.status.code).toBe(DocumentStatus.SHIPPABLE);
// Step 2: Try to send the document (announce for delivery)
// Note: This may fail with "insufficient funds" if the test account has no balance
try {
const sending = await client.sendings.send(uploadedDocumentId);
console.log(`Sent document: ${uploadedDocumentId}, response:`, JSON.stringify(sending, null, 2));
// After sending, status may be IN_PREPARATION (1) initially, then transitions to PRODUCTION_QUEUE (3)
// Poll until document reaches a processable state
let sentDocument = await client.documents.get(uploadedDocumentId);
console.log(`After send - status: ${sentDocument.status.code} (${sentDocument.status.text})`);
// Wait for status to transition from IN_PREPARATION to PRODUCTION_QUEUE
const maxAttempts = 10;
const pollIntervalMs = 1000;
for (let attempt = 0; attempt < maxAttempts; attempt++) {
if (sentDocument.status.code !== DocumentStatus.IN_PREPARATION) {
break;
}
console.log(`Waiting for document to be processed (attempt ${attempt + 1}/${maxAttempts})...`);
await new Promise(resolve => setTimeout(resolve, pollIntervalMs));
sentDocument = await client.documents.get(uploadedDocumentId);
console.log(`Status: ${sentDocument.status.code} (${sentDocument.status.text})`);
}
// Status should now be in production queue or beyond
expect([
DocumentStatus.PRODUCTION_QUEUE,
DocumentStatus.PRINTING,
DocumentStatus.SENT,
]).toContain(sentDocument.status.code);
// Step 3: Cancel the sending (if still possible)
// Cancel only works for PRODUCTION_QUEUE (3) or PRINTING (4)
if (sentDocument.status.code === DocumentStatus.PRODUCTION_QUEUE ||
sentDocument.status.code === DocumentStatus.PRINTING) {
const cancelResult = await client.sendings.cancel(uploadedDocumentId);
console.log(`Canceled document: ${uploadedDocumentId}, response:`, JSON.stringify(cancelResult, null, 2));
// Verify the document is now canceled
const canceledDocument = await client.documents.get(uploadedDocumentId);
console.log(`After cancel - status: ${canceledDocument.status.code} (${canceledDocument.status.text})`);
expect(canceledDocument.status.code).toBe(DocumentStatus.CANCELED);
} else {
console.log(`Document already processed (status ${sentDocument.status.code}), cannot cancel`);
}
} catch (error) {
// The API may reject sending if account has insufficient balance
// This is a valid business error, not a code bug
const errorMessage = error instanceof Error ? error.message : String(error);
if (errorMessage.includes('Guthaben') || errorMessage.includes('balance') || errorMessage.includes('2330')) {
console.log('Send rejected due to insufficient account balance (expected for test accounts)');
console.log('Error:', errorMessage);
// This is acceptable - the SDK correctly communicated with the API
expect(true).toBe(true);
} else {
// Re-throw unexpected errors
throw error;
}
}
});
it.skipIf(runTests)('skipped: no credentials provided', () => {
console.log('E2E tests skipped: Set BINECT_USERNAME and BINECT_PASSWORD environment variables to run');
expect(true).toBe(true);
});
});