Changes
10 changed files (+363/-2)
-
-
@@ -0,0 +1,35 @@// SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only import { describe, expect, test } from "bun:test"; import { isSameBytes } from "./helpers"; describe("isSameBytes", () => { test("Should return true for same references", () => { const arr = new Uint8Array([0, 1, 2, 3, 4, 5]); expect(isSameBytes(arr, arr)).toBe(true); }); test("Should return true for same bytes", () => { const foo = new Uint8Array([0, 1, 2, 3, 4, 5]); const bar = new Uint8Array([0, 1, 2, 3, 4, 5]); expect(isSameBytes(foo, bar)).toBe(true); }); test("Should return false for different bytes", () => { const foo = new Uint8Array([0, 1, 2, 3, 4, 5]); const bar = new Uint8Array([0, 1, 2, 0, 4, 5]); expect(isSameBytes(foo, bar)).toBe(false); }); test("Should return false for different length bytes", () => { const foo = new Uint8Array([0, 1, 2]); const bar = new Uint8Array([0, 1, 2, 3, 4, 5]); expect(isSameBytes(foo, bar)).toBe(false); }); });
-
-
-
@@ -0,0 +1,16 @@// SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only export function isSameBytes(a: Uint8Array, b: Uint8Array): boolean { if (a.byteLength !== b.byteLength) { return false; } for (let i = a.length; i >= 0; i--) { if (a[i] !== b[i]) { return false; } } return true; }
-
-
-
@@ -21,8 +21,8 @@ }export async function idbBackend(): Promise<IDBBackend> { return new IDBBackend({ db: await openDB("yamori", 1, { upgrade(db, oldVersion, _newVersion, _transaction, _event) { db: await openDB("yamori", 2, { async upgrade(db, oldVersion, _newVersion, transaction, _event) { if (oldVersion < 1) { const workspaces = db.createObjectStore("workspaces", { keyPath: "id",
-
@@ -31,6 +31,30 @@workspaces.createIndex("updatedAt", "updatedAt", { unique: false, }); } if (oldVersion < 2) { const store = transaction.objectStore("workspaces"); for await (const cursor of store.iterate()) { const deletionKey = new Uint8Array(16); self.crypto.getRandomValues(deletionKey); const updateKey = new Uint8Array(16); self.crypto.getRandomValues(updateKey); const workerAddKey = new Uint8Array(16); self.crypto.getRandomValues(workerAddKey); await cursor.update({ ...cursor.value, capabilities: { deletionKey, updateKey, workerAddKey, }, }); } } }, }),
-
-
-
@@ -107,6 +107,7 @@ }expect(resp.result.value.workspace?.displayName).toBe("Foo"); expect(resp.result.value.workspace?.id?.value).toStartWith("ws-"); expect(resp.result.value.workspace?.updateKey?.key.length).toBeGreaterThan(0); const listResp = fromBinary( ListResponseSchema,
-
@@ -120,6 +121,7 @@expect(listResp.result.value.workspaces).toHaveLength(1); expect(listResp.result.value.workspaces[0]?.displayName).toBe("Foo"); expect(listResp.result.value.workspaces[0]?.id?.value).toStartWith("ws-"); expect(listResp.result.value.workspaces[0]?.updateKey?.key.length).toBeGreaterThan(0); }); test("Should allow inserting duplicating displayName", async () => {
-
-
-
@@ -55,12 +55,26 @@ // TODO: Handle idempotency_key (how?)const id = "ws-" + crypto.randomUUID(); const deletionKey = new Uint8Array(16); self.crypto.getRandomValues(deletionKey); const updateKey = new Uint8Array(16); self.crypto.getRandomValues(updateKey); const workerAddKey = new Uint8Array(16); self.crypto.getRandomValues(workerAddKey); let addedKey: IDBValidKey; try { addedKey = await db.add("workspaces", { id, displayName: req.displayName, capabilities: { deletionKey, updateKey, workerAddKey, }, updatedAt: Date.now(), }); } catch (error) {
-
@@ -89,6 +103,15 @@added = createMessage(WorkspaceSchema, { id: { value: addedEntry.id }, displayName: addedEntry.displayName, deletionKey: { key: addedEntry.capabilities.deletionKey, }, updateKey: { key: addedEntry.capabilities.updateKey, }, workerAddKey: { key: addedEntry.capabilities.workerAddKey, }, }); } catch (error) { return toBinary(
-
-
-
@@ -59,6 +59,9 @@ {id: { value: "foo-bar", }, deletionKey: { key: new Uint8Array([]), }, }, { db: await openDB("test"),
-
@@ -86,6 +89,9 @@ expect.unreachable();} if (!created.result.value.workspace?.id?.value) { expect.unreachable("Create handler assigned an empty ID"); } if (!created.result.value.workspace.deletionKey?.key) { expect.unreachable("Create handler did not set deleteion_key"); } const deleted = await deleteWorkspace(
-
@@ -93,6 +99,7 @@ {id: { value: created.result.value.workspace.id.value + "-1111", }, deletionKey: created.result.value.workspace.deletionKey, }, ctx, );
-
@@ -124,10 +131,14 @@ }if (!created.result.value.workspace?.id?.value) { expect.unreachable("Create handler assigned an empty ID"); } if (!created.result.value.workspace.deletionKey?.key) { expect.unreachable("Create handler did not set deleteion_key"); } const deleted = await deleteWorkspace( { id: {}, deletionKey: created.result.value.workspace.deletionKey, }, ctx, );
-
@@ -147,6 +158,78 @@ expect(workspaces.result.value.workspaces).toHaveLength(1);expect(workspaces.result.value.workspaces[0]?.displayName).toBe("Foo"); }); test("Should return an error for insufficient capability", async () => { const { [CONTEXT]: ctx } = await idbBackend(); const foo = await createWorkspace( { displayName: "Foo", }, ctx, ); if (foo.result.case !== "ok") { expect.unreachable(); } if (!foo.result.value.workspace?.id?.value) { expect.unreachable("Create handler assigned an empty ID"); } if (!foo.result.value.workspace.deletionKey?.key) { expect.unreachable("Create handler did not set deleteion_key"); } const bar = await createWorkspace( { displayName: "Bar", }, ctx, ); if (bar.result.case !== "ok") { expect.unreachable(); } if (!bar.result.value.workspace?.id?.value) { expect.unreachable("Create handler assigned an empty ID"); } if (!bar.result.value.workspace.deletionKey?.key) { expect.unreachable("Create handler did not set deleteion_key"); } // キーなし const missingKey = await deleteWorkspace( { id: foo.result.value.workspace.id, }, ctx, ); if (missingKey.result.case !== "capabilityError") { expect.unreachable(`Expected "capabilityError", found "${missingKey.result.case}"`); } expect(missingKey.result.value.path).toBe("deletionKey"); // 別のワークスペースのキー const invalidKey = await deleteWorkspace( { id: foo.result.value.workspace.id, deletionKey: bar.result.value.workspace.deletionKey, }, ctx, ); if (invalidKey.result.case !== "capabilityError") { expect.unreachable(`Expected "capabilityError", found "${invalidKey.result.case}"`); } expect(invalidKey.result.value.path).toBe("deletionKey"); const workspaces = await listWorkspaces({}, ctx); if (workspaces.result.case !== "ok") { expect.unreachable(); } expect(workspaces.result.value.workspaces).toHaveLength(2); }); test("Should delete a workspace", async () => { const { [CONTEXT]: ctx } = await idbBackend();
-
@@ -162,6 +245,9 @@ }if (!foo.result.value.workspace?.id?.value) { expect.unreachable("Create handler assigned an empty ID"); } if (!foo.result.value.workspace.deletionKey?.key) { expect.unreachable("Create handler did not set deleteion_key"); } const bar = await createWorkspace( {
-
@@ -175,10 +261,14 @@ }if (!bar.result.value.workspace?.id?.value) { expect.unreachable("Create handler assigned an empty ID"); } if (!bar.result.value.workspace.deletionKey?.key) { expect.unreachable("Create handler did not set deleteion_key"); } const deleted = await deleteWorkspace( { id: foo.result.value.workspace.id, deletionKey: foo.result.value.workspace.deletionKey, }, ctx, );
-
-
-
@@ -8,6 +8,7 @@ DeleteRequestSchema,} from "@yamori/proto/yamori/workspace/v1/delete_request_pb.js"; import { DeleteResponseSchema } from "@yamori/proto/yamori/workspace/v1/delete_response_pb.js"; import { isSameBytes } from "../../../../helpers"; import { type Context } from "../../../../types"; function respond(payload: MessageInitShape<typeof DeleteResponseSchema>): Uint8Array {
-
@@ -50,6 +51,17 @@ },}); } if (!req.deletionKey?.key) { return respond({ result: { case: "capabilityError", value: { path: "deletionKey", }, }, }); } try { const tx = db.transaction("workspaces", "readwrite"); const store = tx.objectStore("workspaces");
-
@@ -62,6 +74,20 @@ result: {case: "notFound", value: { typeName: "yamori.workspace.v1.Workspace", }, }, }); } if ( !(found.capabilities.deletionKey instanceof Uint8Array) || !isSameBytes(req.deletionKey.key, found.capabilities.deletionKey) ) { return respond({ result: { case: "capabilityError", value: { path: "deletionKey", }, }, });
-
-
-
@@ -31,6 +31,15 @@ id: {value: workspace.id, }, displayName: workspace.displayName, deletionKey: { key: workspace.capabilities.deletionKey, }, updateKey: { key: workspace.capabilities.updateKey, }, workerAddKey: { key: workspace.capabilities.workerAddKey, }, }); }), },
-
-
-
@@ -59,6 +59,9 @@ {id: { value: "foo-bar", }, updateKey: { key: new Uint8Array([]), }, displayName: "Foo", fieldMask: { paths: ["displayName"],
-
@@ -91,12 +94,16 @@ }if (!created.result.value.workspace?.id?.value) { expect.unreachable("Create handler assigned an empty ID"); } if (!created.result.value.workspace.updateKey?.key) { expect.unreachable("Create handler did not set update_key"); } const updated = await updateWorkspace( { id: { value: created.result.value.workspace.id.value + "-1111", }, updateKey: created.result.value.workspace.updateKey, displayName: "Bar", fieldMask: { paths: ["displayName"],
-
@@ -133,10 +140,14 @@ }if (!created.result.value.workspace?.id?.value) { expect.unreachable("Create handler assigned an empty ID"); } if (!created.result.value.workspace.updateKey?.key) { expect.unreachable("Create handler did not set update_key"); } const updated = await updateWorkspace( { displayName: "Bar", updateKey: created.result.value.workspace.updateKey, fieldMask: { paths: ["displayName"], },
-
@@ -173,6 +184,9 @@ expect.unreachable();} if (!created.result.value.workspace?.id?.value) { expect.unreachable("Create handler assigned an empty ID"); } if (!created.result.value.workspace.updateKey?.key) { expect.unreachable("Create handler did not set update_key"); } const updated = await updateWorkspace(
-
@@ -180,6 +194,7 @@ {id: { value: created.result.value.workspace.id.value, }, updateKey: created.result.value.workspace.updateKey, fieldMask: { paths: ["display_name"], },
-
@@ -217,12 +232,16 @@ }if (!created.result.value.workspace?.id?.value) { expect.unreachable("Create handler assigned an empty ID"); } if (!created.result.value.workspace.updateKey?.key) { expect.unreachable("Create handler did not set update_key"); } const updated = await updateWorkspace( { id: { value: created.result.value.workspace.id.value, }, updateKey: created.result.value.workspace.updateKey, displayName: "Bar", }, ctx,
-
@@ -235,6 +254,90 @@expect(updated.result.value.workspace?.displayName).toBe("Foo"); }); test("Should return an error for insufficient capability", async () => { const { [CONTEXT]: ctx } = await idbBackend(); const foo = await createWorkspace( { displayName: "Foo", }, ctx, ); if (foo.result.case !== "ok") { expect.unreachable(); } if (!foo.result.value.workspace?.id?.value) { expect.unreachable("Create handler assigned an empty ID"); } if (!foo.result.value.workspace.updateKey?.key) { expect.unreachable("Create handler did not set update_key"); } const bar = await createWorkspace( { displayName: "Bar", }, ctx, ); if (bar.result.case !== "ok") { expect.unreachable(); } if (!bar.result.value.workspace?.id?.value) { expect.unreachable("Create handler assigned an empty ID"); } if (!bar.result.value.workspace.updateKey?.key) { expect.unreachable("Create handler did not set update_key"); } // キーなし const missingKey = await updateWorkspace( { id: foo.result.value.workspace.id, displayName: "Baz", fieldMask: { paths: ["display_name"], }, }, ctx, ); if (missingKey.result.case !== "capabilityError") { expect.unreachable(`Expected "capabilityError", found "${missingKey.result.case}"`); } expect(missingKey.result.value.path).toBe("updateKey"); // 別のワークスペースのキー const invalidKey = await updateWorkspace( { id: foo.result.value.workspace.id, updateKey: bar.result.value.workspace.updateKey, displayName: "Baz", fieldMask: { paths: ["display_name"], }, }, ctx, ); if (invalidKey.result.case !== "capabilityError") { expect.unreachable(`Expected "capabilityError", found "${invalidKey.result.case}"`); } expect(invalidKey.result.value.path).toBe("updateKey"); const workspaces = await listWorkspaces({}, ctx); if (workspaces.result.case !== "ok") { expect.unreachable(); } expect( workspaces.result.value.workspaces.find( (workspace) => workspace.displayName === "Baz", ), ).toBeEmpty(); }); function sleep() { return new Promise<void>((resolve) => { setTimeout(() => void resolve(), 1);
-
@@ -256,6 +359,9 @@ }if (!foo.result.value.workspace?.id?.value) { expect.unreachable("Create handler assigned an empty ID"); } if (!foo.result.value.workspace.updateKey?.key) { expect.unreachable("Create handler did not set update_key"); } // アサーションを簡単にするため、タイムスタンプをずらして順序を付けている await sleep();
-
@@ -272,6 +378,9 @@ }if (!baz.result.value.workspace?.id?.value) { expect.unreachable("Create handler assigned an empty ID"); } if (!baz.result.value.workspace.updateKey?.key) { expect.unreachable("Create handler did not set update_key"); } // 更新によってタイムスタンプが変わることを確認するため、スリープの必要がある await sleep();
-
@@ -281,6 +390,7 @@ {id: { value: foo.result.value.workspace.id.value, }, updateKey: foo.result.value.workspace.updateKey, displayName: "Bar", fieldMask: { paths: ["display_name"],
-
-
-
@@ -8,6 +8,7 @@ UpdateRequestSchema,} from "@yamori/proto/yamori/workspace/v1/update_request_pb.js"; import { UpdateResponseSchema } from "@yamori/proto/yamori/workspace/v1/update_response_pb.js"; import { isSameBytes } from "../../../../helpers"; import { type Context } from "../../../../types"; function respond(payload: MessageInitShape<typeof UpdateResponseSchema>): Uint8Array {
-
@@ -39,6 +40,17 @@ result: {case: "missingField", value: { path: req.id ? "id.value" : "id", }, }, }); } if (!req.updateKey?.key) { return respond({ result: { case: "capabilityError", value: { path: "updateKey", }, }, });
-
@@ -92,6 +104,20 @@ message:import.meta.env.NODE_ENV === "production" ? "Failed to read current workspace data" : `Exception thrown while reading current workspace: ${error}`, }, }, }); } if ( !(found.capabilities.updateKey instanceof Uint8Array) || !isSameBytes(req.updateKey.key, found.capabilities.updateKey) ) { return respond({ result: { case: "capabilityError", value: { path: "updateKey", }, }, });
-