import { describe, test, expect } from 'bun:test'; import { SubtleCryptoProvider } from '@shade/crypto-web'; import { StreamSender, StreamReceiver, StreamReplayError, StreamOutOfOrderError, generateStreamId, generateStreamSecret, } from '../src/index.js'; const crypto = new SubtleCryptoProvider(); async function pair() { const streamId = generateStreamId(crypto); const streamSecret = generateStreamSecret(crypto); const sender = await StreamSender.create({ crypto, streamId, streamSecret, laneId: 0 }); const receiver = await StreamReceiver.create({ crypto, streamId, streamSecret, laneId: 0 }); return { sender, receiver }; } describe('Replay and out-of-order detection', () => { test('replaying the same chunk twice → StreamReplayError', async () => { const { sender, receiver } = await pair(); const { bytes } = await sender.encryptChunk(new TextEncoder().encode('first'), false); await receiver.decryptChunk(bytes); await expect(receiver.decryptChunk(bytes)).rejects.toThrow(StreamReplayError); }); test('out-of-order chunk (skipping seq) → StreamOutOfOrderError', async () => { const { sender, receiver } = await pair(); await sender.encryptChunk(new TextEncoder().encode('a'), false); // seq 0 const { bytes: c1 } = await sender.encryptChunk(new TextEncoder().encode('b'), false); // seq 1 // Skip seq 0; send seq 1 first await expect(receiver.decryptChunk(c1)).rejects.toThrow(StreamOutOfOrderError); }); test('error contains expected and received seq', async () => { const { sender, receiver } = await pair(); await sender.encryptChunk(new TextEncoder().encode('skip'), false); // seq 0 produced const { bytes: c1 } = await sender.encryptChunk(new TextEncoder().encode('next'), false); try { await receiver.decryptChunk(c1); throw new Error('expected throw'); } catch (err) { expect((err as Error).message).toContain('expected seq=0'); expect((err as Error).message).toContain('got 1'); } }); test('out-of-order then in-order: in-order chunk (after error) still works', async () => { const { sender, receiver } = await pair(); const { bytes: c0 } = await sender.encryptChunk(new TextEncoder().encode('a'), false); const { bytes: c1 } = await sender.encryptChunk(new TextEncoder().encode('b'), true); await expect(receiver.decryptChunk(c1)).rejects.toThrow(StreamOutOfOrderError); // Receiver state still expects seq 0 (the error did not advance it) const dec0 = await receiver.decryptChunk(c0); expect(dec0.seq).toBe(0); const dec1 = await receiver.decryptChunk(c1); expect(dec1.seq).toBe(1); }); test('replay after a different in-order chunk advanced seq → StreamReplayError', async () => { const { sender, receiver } = await pair(); const { bytes: c0 } = await sender.encryptChunk(new TextEncoder().encode('a'), false); const { bytes: c1 } = await sender.encryptChunk(new TextEncoder().encode('b'), false); await receiver.decryptChunk(c0); await receiver.decryptChunk(c1); await expect(receiver.decryptChunk(c0)).rejects.toThrow(StreamReplayError); }); });