Files
storage-v1/tests/event-db.test.ts

72 lines
2.9 KiB
TypeScript

import { describe, expect, it } from "vitest";
import {
CanonicalJsonEventCodec,
EventDB,
EventHashMismatchError,
MemoryBlobStorage,
composeStorageTransforms,
createAesGcmTransform,
createEventIdentity,
createMsgPackTransform,
type BaseEventData,
type BlobStorageAddedEvent,
} from "../src/index.js";
interface TestEvent extends BaseEventData {
readonly type: "test";
readonly payload: unknown;
}
const codec = new CanonicalJsonEventCodec<TestEvent>();
const identify = createEventIdentity<TestEvent>((event) => codec.encodeCanonical(event));
describe("EventDB", () => {
it("loads historical events, deduplicates observations, and sorts deterministically", async () => {
const storage = new ObservableMemoryStorage<TestEvent>();
const first: TestEvent = { version: 1, timestamp: 2, nonce: "a", type: "test", payload: 1 };
const firstId = await identify(first);
await storage.set(firstId, first);
const database = await EventDB.open(storage, { identify });
const observed: string[] = [];
database.on("event", (event) => observed.push(event.id));
expect((await database.append(first)).id).toBe(firstId);
storage.notify({ id: firstId, data: first, source: "remote" });
const earlier = await database.append({ ...first, timestamp: 1, nonce: "b" });
const tied = await database.append({ ...first, nonce: "c" });
const events = await database.events();
expect(events[0]?.id).toBe(earlier.id);
expect(events.slice(1).map((event) => event.id)).toEqual([firstId, tied.id].sort());
expect(observed).toEqual([earlier.id, tied.id]);
});
it("rejects a logical value stored under the wrong ID", async () => {
const storage = new MemoryBlobStorage<TestEvent>();
await storage.set("sha256:wrong", {
version: 1, timestamp: 1, nonce: "a", type: "test", payload: null,
});
await expect(EventDB.open(storage, { identify })).rejects.toBeInstanceOf(EventHashMismatchError);
});
it("keeps event identity stable when storage encryption changes physical bytes", async () => {
const key = await crypto.subtle.generateKey({ name: "AES-GCM", length: 256 }, true, ["encrypt", "decrypt"]);
const transform = composeStorageTransforms(
createMsgPackTransform<TestEvent>(),
createAesGcmTransform(key),
);
const plainStorage = new MemoryBlobStorage<TestEvent>();
const encryptedStorage = new MemoryBlobStorage<TestEvent>({ transform });
const plain = await EventDB.open(plainStorage, { identify });
const encrypted = await EventDB.open(encryptedStorage, { identify });
const event: TestEvent = { version: 1, timestamp: 1, nonce: "same", type: "test", payload: "value" };
expect((await plain.append(event)).id).toBe((await encrypted.append(event)).id);
});
});
class ObservableMemoryStorage<T> extends MemoryBlobStorage<T> {
notify(event: BlobStorageAddedEvent<T>): void {
this.emitAdded(event);
}
}