generated from coulomb/repo-seed
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>
235 lines
8.8 KiB
TypeScript
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);
|
|
});
|
|
});
|