96 lines
3.2 KiB
TypeScript
96 lines
3.2 KiB
TypeScript
|
|
import { describe, it, expect } from 'vitest';
|
||
|
|
|
||
|
|
import {
|
||
|
|
DOC_TYPE_LABEL,
|
||
|
|
extractSigningToken,
|
||
|
|
nextPendingSigner,
|
||
|
|
} from '@/lib/services/documenso-signers';
|
||
|
|
|
||
|
|
describe('extractSigningToken', () => {
|
||
|
|
it('pulls the last path segment when it looks token-shaped', () => {
|
||
|
|
expect(extractSigningToken('https://sig.example.com/sign/vbT8hi3jKQmrFP_LN1WcS')).toBe(
|
||
|
|
'vbT8hi3jKQmrFP_LN1WcS',
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('handles trailing slash gracefully', () => {
|
||
|
|
expect(extractSigningToken('https://sig.example.com/sign/HkrptwS42ZBXdRKj1TyUo/')).toBe(
|
||
|
|
'HkrptwS42ZBXdRKj1TyUo',
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('returns null for non-URL input', () => {
|
||
|
|
expect(extractSigningToken('not a url at all')).toBeNull();
|
||
|
|
expect(extractSigningToken('')).toBeNull();
|
||
|
|
expect(extractSigningToken(null)).toBeNull();
|
||
|
|
expect(extractSigningToken(undefined)).toBeNull();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('rejects too-short tails (defends against generic "sign" / "embed" terminators)', () => {
|
||
|
|
expect(extractSigningToken('https://example.com/sign')).toBeNull();
|
||
|
|
expect(extractSigningToken('https://example.com/abc')).toBeNull();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('rejects path tails with disallowed characters', () => {
|
||
|
|
// Real tokens are URL-safe base64 — no spaces, no punctuation
|
||
|
|
expect(extractSigningToken('https://example.com/sign/has%20space')).toBeNull();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('handles real v1.32 + v2 token shapes', () => {
|
||
|
|
// Documenso 1.32 token (21-char URL-safe alphabet from real webhook docs)
|
||
|
|
expect(extractSigningToken('https://sig.documenso.com/sign/vbT8hi3jKQmrFP_LN1WcS')).toBe(
|
||
|
|
'vbT8hi3jKQmrFP_LN1WcS',
|
||
|
|
);
|
||
|
|
// Mixed-case + underscores + dashes
|
||
|
|
expect(extractSigningToken('https://app.example.com/sign/Aa_-Zz09Aa_-Zz09')).toBe(
|
||
|
|
'Aa_-Zz09Aa_-Zz09',
|
||
|
|
);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('DOC_TYPE_LABEL', () => {
|
||
|
|
it('maps the three known document types to customer-facing labels', () => {
|
||
|
|
expect(DOC_TYPE_LABEL.eoi).toBe('Expression of Interest');
|
||
|
|
expect(DOC_TYPE_LABEL.contract).toBe('Sales Contract');
|
||
|
|
expect(DOC_TYPE_LABEL.reservation_agreement).toBe('Reservation Agreement');
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('nextPendingSigner', () => {
|
||
|
|
it('picks the lowest signingOrder pending signer', () => {
|
||
|
|
const signers = [
|
||
|
|
{ status: 'signed', signingOrder: 1 },
|
||
|
|
{ status: 'pending', signingOrder: 2 },
|
||
|
|
{ status: 'pending', signingOrder: 3 },
|
||
|
|
];
|
||
|
|
expect(nextPendingSigner(signers)).toEqual({ status: 'pending', signingOrder: 2 });
|
||
|
|
});
|
||
|
|
|
||
|
|
it('returns null when every signer has signed', () => {
|
||
|
|
expect(
|
||
|
|
nextPendingSigner([
|
||
|
|
{ status: 'signed', signingOrder: 1 },
|
||
|
|
{ status: 'signed', signingOrder: 2 },
|
||
|
|
]),
|
||
|
|
).toBeNull();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('treats declined the same as no longer pending', () => {
|
||
|
|
expect(
|
||
|
|
nextPendingSigner([
|
||
|
|
{ status: 'declined', signingOrder: 1 },
|
||
|
|
{ status: 'signed', signingOrder: 2 },
|
||
|
|
]),
|
||
|
|
).toBeNull();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('handles unordered input by signingOrder', () => {
|
||
|
|
const signers = [
|
||
|
|
{ status: 'pending', signingOrder: 5 },
|
||
|
|
{ status: 'signed', signingOrder: 1 },
|
||
|
|
{ status: 'pending', signingOrder: 2 },
|
||
|
|
];
|
||
|
|
expect(nextPendingSigner(signers)).toEqual({ status: 'pending', signingOrder: 2 });
|
||
|
|
});
|
||
|
|
});
|