import { describe, test, expect, beforeEach, afterEach } from 'bun:test'; import { tmpdir } from 'os'; import { join } from 'path'; import { unlinkSync } from 'fs'; import { SqliteInboxStore } from '../src/sqlite-inbox-store.js'; function randBytes(n: number): Uint8Array { const buf = new Uint8Array(n); globalThis.crypto.getRandomValues(buf); return buf; } function tempDbPath(): string { return join( tmpdir(), `shade-inbox-test-${Date.now()}-${Math.random().toString(36).slice(2)}.db`, ); } describe('SqliteInboxStore', () => { let dbPath: string; let store: SqliteInboxStore; beforeEach(() => { dbPath = tempDbPath(); store = new SqliteInboxStore(dbPath); }); afterEach(() => { store.close(); try { unlinkSync(dbPath); } catch {} try { unlinkSync(dbPath + '-wal'); } catch {} try { unlinkSync(dbPath + '-shm'); } catch {} }); test('owner save + get', async () => { const key = randBytes(32); await store.saveAddressOwner('bob', key); const got = await store.getAddressOwner('bob'); expect(got).not.toBeNull(); expect(got!).toEqual(key); }); test('putBlob is idempotent on (address, msgId)', async () => { const ct = randBytes(64); const a = await store.putBlob({ address: 'bob', msgId: 'a'.repeat(64), ciphertext: ct, expiresAt: Date.now() + 60_000, }); expect(a.created).toBe(true); const b = await store.putBlob({ address: 'bob', msgId: 'a'.repeat(64), ciphertext: ct, expiresAt: Date.now() + 60_000, }); expect(b.created).toBe(false); expect(b.receivedAt).toBe(a.receivedAt); }); test('fetchBlobs respects sinceCursor and expires_at', async () => { const now = Date.now(); await store.putBlob({ address: 'bob', msgId: '1'.repeat(64), ciphertext: randBytes(8), expiresAt: now + 60_000, }); await store.putBlob({ address: 'bob', msgId: '2'.repeat(64), ciphertext: randBytes(8), expiresAt: now - 1000, // already expired }); await store.putBlob({ address: 'bob', msgId: '3'.repeat(64), ciphertext: randBytes(8), expiresAt: now + 60_000, }); const all = await store.fetchBlobs({ address: 'bob', sinceCursor: 0, now, limit: 100, }); // Expired one filtered out. expect(all.length).toBe(2); expect(all.map((r) => r.msgId).sort()).toEqual(['1'.repeat(64), '3'.repeat(64)]); // Cursor advances strictly. const half = await store.fetchBlobs({ address: 'bob', sinceCursor: all[0]!.receivedAt, now, limit: 100, }); expect(half.length).toBe(1); expect(half[0]!.msgId).toBe(all[1]!.msgId); }); test('purgeExpired removes only expired rows', async () => { const now = Date.now(); await store.putBlob({ address: 'bob', msgId: '1'.repeat(64), ciphertext: randBytes(8), expiresAt: now - 1, }); await store.putBlob({ address: 'bob', msgId: '2'.repeat(64), ciphertext: randBytes(8), expiresAt: now + 60_000, }); const removed = await store.purgeExpired(now); expect(removed).toBe(1); const remaining = await store.fetchBlobs({ address: 'bob', sinceCursor: 0, now, limit: 10, }); expect(remaining.length).toBe(1); expect(remaining[0]!.msgId).toBe('2'.repeat(64)); }); test('persists across reopen', async () => { const key = randBytes(32); await store.saveAddressOwner('bob', key); const ct = randBytes(64); await store.putBlob({ address: 'bob', msgId: '5'.repeat(64), ciphertext: ct, expiresAt: Date.now() + 60_000, }); store.close(); store = new SqliteInboxStore(dbPath); const got = await store.getAddressOwner('bob'); expect(got).toEqual(key); const blobs = await store.fetchBlobs({ address: 'bob', sinceCursor: 0, now: Date.now(), limit: 10, }); expect(blobs.length).toBe(1); expect(blobs[0]!.ciphertext).toEqual(ct); }); test('deleteAddress drops owner + blobs', async () => { const key = randBytes(32); await store.saveAddressOwner('bob', key); await store.putBlob({ address: 'bob', msgId: '7'.repeat(64), ciphertext: randBytes(8), expiresAt: Date.now() + 60_000, }); await store.deleteAddress('bob'); expect(await store.getAddressOwner('bob')).toBeNull(); const blobs = await store.fetchBlobs({ address: 'bob', sinceCursor: 0, now: Date.now(), limit: 10, }); expect(blobs.length).toBe(0); }); test('countBlobs ignores expired entries', async () => { const now = Date.now(); await store.putBlob({ address: 'bob', msgId: '1'.repeat(64), ciphertext: randBytes(8), expiresAt: now - 1, }); await store.putBlob({ address: 'bob', msgId: '2'.repeat(64), ciphertext: randBytes(8), expiresAt: now + 60_000, }); expect(await store.countBlobs('bob', now)).toBe(1); }); });