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); }); });