generated from coulomb/repo-seed
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>
This commit is contained in:
234
tests/e2e.test.ts
Normal file
234
tests/e2e.test.ts
Normal file
@@ -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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user