From 20462b48b32836a984c3b31d7a00e537f061c953 Mon Sep 17 00:00:00 2001 From: tegwick Date: Wed, 14 Jan 2026 23:08:25 +0100 Subject: [PATCH] Add e2e test for document send and cancel workflow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- tests/e2e.test.ts | 234 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 234 insertions(+) create mode 100644 tests/e2e.test.ts diff --git a/tests/e2e.test.ts b/tests/e2e.test.ts new file mode 100644 index 0000000..2c03e52 --- /dev/null +++ b/tests/e2e.test.ts @@ -0,0 +1,234 @@ +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); + }); +});