296 lines
9.6 KiB
TypeScript
296 lines
9.6 KiB
TypeScript
import { describe, expect, it } from "@suaveplan/testing/runner";
|
|
import { VAULT_CODES } from "../errors/codes.js";
|
|
import { VaultError } from "../errors/vault-error.js";
|
|
import type {
|
|
CreatedShareLink,
|
|
CreateShareLinkInput,
|
|
DriveSharingLike,
|
|
} from "../types/adapters.js";
|
|
import type { ShareLink } from "../types/types.js";
|
|
import { VaultShares } from "./shares.js";
|
|
|
|
function vaultKey(): Uint8Array {
|
|
const key = new Uint8Array(32);
|
|
crypto.getRandomValues(key);
|
|
return key;
|
|
}
|
|
|
|
class StubDriveSharing implements DriveSharingLike {
|
|
public received: CreateShareLinkInput[] = [];
|
|
public revoked: string[] = [];
|
|
public failOn: "create" | "revoke" | undefined;
|
|
public counter = 0;
|
|
public async createLink(
|
|
input: CreateShareLinkInput
|
|
): Promise<CreatedShareLink> {
|
|
if (this.failOn === "create") throw new Error("offline");
|
|
this.received.push(input);
|
|
this.counter += 1;
|
|
const id = `share-${this.counter}`;
|
|
return { id, url: `https://share.example/${id}` };
|
|
}
|
|
public async revokeLink(id: string): Promise<void> {
|
|
if (this.failOn === "revoke") throw new Error("offline");
|
|
this.revoked.push(id);
|
|
}
|
|
public async isActive(id: string): Promise<boolean> {
|
|
return !this.revoked.includes(id);
|
|
}
|
|
}
|
|
|
|
function makeShares(driveSharing?: DriveSharingLike): {
|
|
shares: VaultShares;
|
|
driveSharing: StubDriveSharing;
|
|
metadata: Map<string, ShareLink>;
|
|
} {
|
|
const stub =
|
|
(driveSharing as StubDriveSharing | undefined) ??
|
|
new StubDriveSharing();
|
|
const metadata = new Map<string, ShareLink>();
|
|
const shares = new VaultShares({
|
|
driveSharing: stub,
|
|
partitionId: "part-1",
|
|
vaultKey: vaultKey(),
|
|
clock: () => 1,
|
|
rng: undefined,
|
|
metadata,
|
|
onMutation: () => {},
|
|
});
|
|
return { shares, driveSharing: stub, metadata };
|
|
}
|
|
|
|
describe("VaultShares", () => {
|
|
it("createLink wraps the plaintext and returns metadata", {
|
|
timeout: 5000,
|
|
}, async () => {
|
|
const { shares, driveSharing } = makeShares();
|
|
const link = await shares.createLink({
|
|
itemId: "i1",
|
|
itemPlaintext: new TextEncoder().encode("payload"),
|
|
expiresAt: 99,
|
|
maxViews: 5,
|
|
});
|
|
expect(link.id).toBe("share-1");
|
|
expect(link.url).toContain("share-1");
|
|
expect(driveSharing.received[0]?.partitionId).toBe("part-1");
|
|
expect(driveSharing.received[0]?.wrappedKey.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it("createLink wraps drive failures as DRIVE.FAILURE", {
|
|
timeout: 5000,
|
|
}, async () => {
|
|
const stub = new StubDriveSharing();
|
|
stub.failOn = "create";
|
|
const { shares } = makeShares(stub);
|
|
try {
|
|
await shares.createLink({
|
|
itemId: "i",
|
|
itemPlaintext: new Uint8Array(1),
|
|
expiresAt: null,
|
|
maxViews: null,
|
|
});
|
|
throw new Error("should have thrown");
|
|
} catch (err) {
|
|
expect(VaultError.is(err)).toBe(true);
|
|
expect((err as VaultError).code).toBe(VAULT_CODES["DRIVE.FAILURE"]);
|
|
}
|
|
});
|
|
|
|
it("revokeLink removes local metadata", { timeout: 5000 }, async () => {
|
|
const { shares, driveSharing, metadata } = makeShares();
|
|
const link = await shares.createLink({
|
|
itemId: "i",
|
|
itemPlaintext: new Uint8Array(1),
|
|
expiresAt: null,
|
|
maxViews: null,
|
|
});
|
|
await shares.revokeLink(link.id);
|
|
expect(metadata.has(link.id)).toBe(false);
|
|
expect(driveSharing.revoked).toEqual([link.id]);
|
|
});
|
|
|
|
it("revokeLink throws SHARE.NOT_FOUND on unknown ids", {
|
|
timeout: 5000,
|
|
}, async () => {
|
|
const { shares } = makeShares();
|
|
try {
|
|
await shares.revokeLink("missing");
|
|
throw new Error("should have thrown");
|
|
} catch (err) {
|
|
expect(VaultError.is(err)).toBe(true);
|
|
expect((err as VaultError).code).toBe(
|
|
VAULT_CODES["SHARE.NOT_FOUND"]
|
|
);
|
|
}
|
|
});
|
|
|
|
it("revokeLink wraps drive failures as DRIVE.FAILURE", {
|
|
timeout: 5000,
|
|
}, async () => {
|
|
const stub = new StubDriveSharing();
|
|
const { shares } = makeShares(stub);
|
|
const link = await shares.createLink({
|
|
itemId: "i",
|
|
itemPlaintext: new Uint8Array(1),
|
|
expiresAt: null,
|
|
maxViews: null,
|
|
});
|
|
stub.failOn = "revoke";
|
|
try {
|
|
await shares.revokeLink(link.id);
|
|
throw new Error("should have thrown");
|
|
} catch (err) {
|
|
expect(VaultError.is(err)).toBe(true);
|
|
expect((err as VaultError).code).toBe(VAULT_CODES["DRIVE.FAILURE"]);
|
|
}
|
|
});
|
|
|
|
it("isActive short-circuits when local metadata is missing", {
|
|
timeout: 5000,
|
|
}, async () => {
|
|
const { shares } = makeShares();
|
|
expect(await shares.isActive("missing")).toBe(false);
|
|
});
|
|
|
|
it("isActive returns the canonical answer from drive-sharing", {
|
|
timeout: 5000,
|
|
}, async () => {
|
|
const { shares } = makeShares();
|
|
const link = await shares.createLink({
|
|
itemId: "i",
|
|
itemPlaintext: new Uint8Array(1),
|
|
expiresAt: null,
|
|
maxViews: null,
|
|
});
|
|
expect(await shares.isActive(link.id)).toBe(true);
|
|
await shares.revokeLink(link.id);
|
|
// After revoke, metadata is removed locally; isActive returns false.
|
|
expect(await shares.isActive(link.id)).toBe(false);
|
|
});
|
|
|
|
it("list returns all known share metadata", { timeout: 5000 }, async () => {
|
|
const { shares } = makeShares();
|
|
await shares.createLink({
|
|
itemId: "a",
|
|
itemPlaintext: new Uint8Array(1),
|
|
expiresAt: null,
|
|
maxViews: null,
|
|
});
|
|
await shares.createLink({
|
|
itemId: "b",
|
|
itemPlaintext: new Uint8Array(1),
|
|
expiresAt: null,
|
|
maxViews: null,
|
|
});
|
|
expect(shares.list().length).toBe(2);
|
|
});
|
|
|
|
it("createLink with an injected RNG produces deterministic wrappedKey nonces", {
|
|
timeout: 5000,
|
|
}, async () => {
|
|
const stub = new StubDriveSharing();
|
|
const metadata = new Map<string, ShareLink>();
|
|
const rng = {
|
|
nextUint32: () => 1,
|
|
nextFloat: () => 0,
|
|
nextBytes: (len: number) => new Uint8Array(len).fill(2),
|
|
};
|
|
const shares = new VaultShares({
|
|
driveSharing: stub,
|
|
partitionId: "p",
|
|
vaultKey: new Uint8Array(32),
|
|
clock: () => 0,
|
|
rng,
|
|
metadata,
|
|
onMutation: () => {},
|
|
});
|
|
await shares.createLink({
|
|
itemId: "i",
|
|
itemPlaintext: new Uint8Array(1),
|
|
expiresAt: null,
|
|
maxViews: null,
|
|
});
|
|
const wrapped = stub.received[0]?.wrappedKey as Uint8Array;
|
|
expect(wrapped.slice(0, 24)).toEqual(new Uint8Array(24).fill(2));
|
|
});
|
|
|
|
it("non-Error createLink failures still surface as DRIVE.FAILURE", {
|
|
timeout: 5000,
|
|
}, async () => {
|
|
const sentinel: unknown = "non-error";
|
|
class BadStub extends StubDriveSharing {
|
|
public override async createLink(): Promise<CreatedShareLink> {
|
|
throw sentinel;
|
|
}
|
|
}
|
|
const stub = new BadStub();
|
|
const { shares } = makeShares(stub);
|
|
try {
|
|
await shares.createLink({
|
|
itemId: "i",
|
|
itemPlaintext: new Uint8Array(1),
|
|
expiresAt: null,
|
|
maxViews: null,
|
|
});
|
|
throw new Error("should have thrown");
|
|
} catch (err) {
|
|
expect(VaultError.is(err)).toBe(true);
|
|
expect((err as VaultError).code).toBe(VAULT_CODES["DRIVE.FAILURE"]);
|
|
}
|
|
});
|
|
|
|
it("non-Error revokeLink failures still surface as DRIVE.FAILURE", {
|
|
timeout: 5000,
|
|
}, async () => {
|
|
const sentinel: unknown = "non-error";
|
|
class BadStub extends StubDriveSharing {
|
|
public override async revokeLink(): Promise<void> {
|
|
throw sentinel;
|
|
}
|
|
}
|
|
const stub = new BadStub();
|
|
const { shares } = makeShares(stub);
|
|
const link = await shares.createLink({
|
|
itemId: "i",
|
|
itemPlaintext: new Uint8Array(1),
|
|
expiresAt: null,
|
|
maxViews: null,
|
|
});
|
|
try {
|
|
await shares.revokeLink(link.id);
|
|
throw new Error("should have thrown");
|
|
} catch (err) {
|
|
expect(VaultError.is(err)).toBe(true);
|
|
expect((err as VaultError).code).toBe(VAULT_CODES["DRIVE.FAILURE"]);
|
|
}
|
|
});
|
|
|
|
it("createLink validates the produced ShareLink", {
|
|
timeout: 5000,
|
|
}, async () => {
|
|
class BadStub extends StubDriveSharing {
|
|
public override async createLink(
|
|
_input: CreateShareLinkInput
|
|
): Promise<CreatedShareLink> {
|
|
return { id: "x", url: "not-a-url" };
|
|
}
|
|
}
|
|
const stub = new BadStub();
|
|
const { shares } = makeShares(stub);
|
|
try {
|
|
await shares.createLink({
|
|
itemId: "i",
|
|
itemPlaintext: new Uint8Array(1),
|
|
expiresAt: null,
|
|
maxViews: null,
|
|
});
|
|
throw new Error("should have thrown");
|
|
} catch (err) {
|
|
expect(VaultError.is(err)).toBe(true);
|
|
expect((err as VaultError).code).toBe(
|
|
VAULT_CODES["ITEM.VALIDATION_FAILED"]
|
|
);
|
|
}
|
|
});
|
|
});
|