72 lines
3.1 KiB
TypeScript
72 lines
3.1 KiB
TypeScript
|
|
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);
|
||
|
|
});
|
||
|
|
});
|