This repository has been archived on 2026-03-27. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
drive/tests/server/keto_test.ts
Sienna Meridian Satterwhite 58237d9e44 Initial commit — Drive, an S3 file browser with WOPI editing
Lightweight replacement for the upstream La Suite Numérique drive
(Django/Celery/Next.js) built as a single Deno binary.

Server (Deno + Hono):
- S3 file operations via AWS SigV4 (no SDK) with pre-signed URLs
- WOPI host for Collabora Online (CheckFileInfo, GetFile, PutFile, locks)
- Ory Kratos session auth + CSRF protection
- Ory Keto permission model (OPL namespaces, not yet wired to routes)
- PostgreSQL metadata with recursive folder sizes
- S3 backfill API for registering files uploaded outside the UI
- OpenTelemetry tracing + metrics (opt-in via OTEL_ENABLED)

Frontend (React 19 + Cunningham v4 + react-aria):
- File browser with GridList, keyboard nav, multi-select
- Collabora editor iframe (full-screen, form POST, postMessage)
- Profile menu, waffle menu, drag-drop upload, asset type badges
- La Suite integration service theming (runtime CSS)

Testing (549 tests):
- 235 server unit tests (Deno) — 90%+ coverage
- 278 UI unit tests (Vitest) — 90%+ coverage
- 11 E2E tests (Playwright)
- 12 integration service tests (Playwright)
- 13 WOPI integration tests (Playwright + Docker Compose + Collabora)

MIT licensed.
2026-03-25 18:28:37 +00:00

422 lines
12 KiB
TypeScript

import {
assertEquals,
assertRejects,
} from "https://deno.land/std@0.224.0/assert/mod.ts";
// ── Fetch mock infrastructure ────────────────────────────────────────────────
const originalFetch = globalThis.fetch;
function mockFetch(
handler: (url: string, init?: RequestInit) => Promise<Response> | Response,
) {
globalThis.fetch = ((input: string | URL | Request, init?: RequestInit) => {
const url = typeof input === "string"
? input
: input instanceof URL
? input.toString()
: input.url;
return Promise.resolve(handler(url, init));
}) as typeof globalThis.fetch;
}
function restoreFetch() {
globalThis.fetch = originalFetch;
}
// We need to set env vars before importing the module
Deno.env.set("KETO_READ_URL", "http://keto-read:4466");
Deno.env.set("KETO_WRITE_URL", "http://keto-write:4467");
// Dynamic import so env vars are picked up (module-level const reads at import time).
async function importKeto() {
const mod = await import(`../../server/keto.ts?v=${Date.now()}`);
return mod;
}
// Since Deno caches module imports, we import once and re-use.
const keto = await importKeto();
// ── checkPermission tests ───────────────────────────────────────────────────
Deno.test("checkPermission — returns true when Keto says allowed", async () => {
mockFetch((url, init) => {
assertEquals(url, "http://keto-read:4466/relation-tuples/check/openapi");
assertEquals(init?.method, "POST");
const body = JSON.parse(init?.body as string);
assertEquals(body.namespace, "files");
assertEquals(body.object, "file-123");
assertEquals(body.relation, "read");
assertEquals(body.subject_id, "user-abc");
return new Response(JSON.stringify({ allowed: true }), { status: 200 });
});
try {
const result = await keto.checkPermission("files", "file-123", "read", "user-abc");
assertEquals(result, true);
} finally {
restoreFetch();
}
});
Deno.test("checkPermission — returns false when Keto denies", async () => {
mockFetch(() => new Response(JSON.stringify({ allowed: false }), { status: 200 }));
try {
const result = await keto.checkPermission("files", "file-123", "write", "user-abc");
assertEquals(result, false);
} finally {
restoreFetch();
}
});
Deno.test("checkPermission — returns false on network error", async () => {
mockFetch(() => {
throw new Error("network down");
});
try {
const result = await keto.checkPermission("files", "file-123", "read", "user-abc");
assertEquals(result, false);
} finally {
restoreFetch();
}
});
Deno.test("checkPermission — returns false on non-200 response", async () => {
mockFetch(() => new Response("error", { status: 500 }));
try {
const result = await keto.checkPermission("files", "file-123", "read", "user-abc");
assertEquals(result, false);
} finally {
restoreFetch();
}
});
// ── createRelationship tests ────────────────────────────────────────────────
Deno.test("createRelationship — sends correct PUT request", async () => {
let capturedUrl = "";
let capturedBody: Record<string, unknown> = {};
let capturedMethod = "";
mockFetch((url, init) => {
capturedUrl = url;
capturedMethod = init?.method ?? "";
capturedBody = JSON.parse(init?.body as string);
return new Response(JSON.stringify({}), { status: 201 });
});
try {
await keto.createRelationship("files", "file-456", "owners", "user-xyz");
assertEquals(capturedUrl, "http://keto-write:4467/admin/relation-tuples");
assertEquals(capturedMethod, "PUT");
assertEquals(capturedBody.namespace, "files");
assertEquals(capturedBody.object, "file-456");
assertEquals(capturedBody.relation, "owners");
assertEquals(capturedBody.subject_id, "user-xyz");
} finally {
restoreFetch();
}
});
Deno.test("createRelationship — throws on failure", async () => {
mockFetch(() => new Response("bad", { status: 500 }));
try {
await assertRejects(
() => keto.createRelationship("files", "file-456", "owners", "user-xyz"),
Error,
"Keto createRelationship failed",
);
} finally {
restoreFetch();
}
});
// ── createRelationshipWithSubjectSet tests ──────────────────────────────────
Deno.test("createRelationshipWithSubjectSet — sends subject_set payload", async () => {
let capturedBody: Record<string, unknown> = {};
mockFetch((_url, init) => {
capturedBody = JSON.parse(init?.body as string);
return new Response("{}", { status: 201 });
});
try {
await keto.createRelationshipWithSubjectSet(
"files", "file-1", "parents",
"folders", "folder-2", "",
);
assertEquals(capturedBody.subject_set, {
namespace: "folders",
object: "folder-2",
relation: "",
});
} finally {
restoreFetch();
}
});
Deno.test("createRelationshipWithSubjectSet — throws on failure", async () => {
mockFetch(() => new Response("error", { status: 403 }));
try {
await assertRejects(
() => keto.createRelationshipWithSubjectSet("files", "f1", "parents", "folders", "f2", ""),
Error,
"Keto createRelationshipWithSubjectSet failed",
);
} finally {
restoreFetch();
}
});
// ── deleteRelationship tests ────────────────────────────────────────────────
Deno.test("deleteRelationship — sends DELETE with query params", async () => {
let capturedUrl = "";
let capturedMethod = "";
mockFetch((url, init) => {
capturedUrl = url;
capturedMethod = init?.method ?? "";
return new Response(null, { status: 204 });
});
try {
await keto.deleteRelationship("files", "file-1", "owners", "user-1");
assertEquals(capturedMethod, "DELETE");
const u = new URL(capturedUrl);
assertEquals(u.searchParams.get("namespace"), "files");
assertEquals(u.searchParams.get("object"), "file-1");
assertEquals(u.searchParams.get("relation"), "owners");
assertEquals(u.searchParams.get("subject_id"), "user-1");
} finally {
restoreFetch();
}
});
Deno.test("deleteRelationship — throws on failure", async () => {
mockFetch(() => new Response("error", { status: 500 }));
try {
await assertRejects(
() => keto.deleteRelationship("files", "file-1", "owners", "user-1"),
Error,
"Keto deleteRelationship failed",
);
} finally {
restoreFetch();
}
});
// ── batchWriteRelationships tests ───────────────────────────────────────────
Deno.test("batchWriteRelationships — formats patches correctly", async () => {
let capturedBody: unknown[] = [];
let capturedMethod = "";
mockFetch((_url, init) => {
capturedMethod = init?.method ?? "";
capturedBody = JSON.parse(init?.body as string);
return new Response(null, { status: 204 });
});
try {
await keto.batchWriteRelationships([
{
action: "insert",
relation_tuple: {
namespace: "files",
object: "file-1",
relation: "owners",
subject_id: "user-1",
},
},
{
action: "delete",
relation_tuple: {
namespace: "files",
object: "file-1",
relation: "viewers",
subject_id: "user-2",
},
},
]);
assertEquals(capturedMethod, "PATCH");
assertEquals(capturedBody.length, 2);
assertEquals((capturedBody[0] as Record<string, unknown>).action, "insert");
assertEquals((capturedBody[1] as Record<string, unknown>).action, "delete");
} finally {
restoreFetch();
}
});
Deno.test("batchWriteRelationships — throws on failure", async () => {
mockFetch(() => new Response("error", { status: 500 }));
try {
await assertRejects(
() =>
keto.batchWriteRelationships([
{
action: "insert",
relation_tuple: {
namespace: "files",
object: "file-1",
relation: "owners",
subject_id: "user-1",
},
},
]),
Error,
"Keto batchWriteRelationships failed",
);
} finally {
restoreFetch();
}
});
// ── listRelationships tests ─────────────────────────────────────────────────
Deno.test("listRelationships — sends GET with query params", async () => {
let capturedUrl = "";
mockFetch((url) => {
capturedUrl = url;
return new Response(
JSON.stringify({
relation_tuples: [
{ namespace: "files", object: "f1", relation: "owners", subject_id: "u1" },
],
}),
{ status: 200 },
);
});
try {
const tuples = await keto.listRelationships("files", "f1", "owners");
assertEquals(tuples.length, 1);
assertEquals(tuples[0].subject_id, "u1");
const u = new URL(capturedUrl);
assertEquals(u.searchParams.get("namespace"), "files");
assertEquals(u.searchParams.get("object"), "f1");
assertEquals(u.searchParams.get("relation"), "owners");
} finally {
restoreFetch();
}
});
Deno.test("listRelationships — with only namespace param", async () => {
let capturedUrl = "";
mockFetch((url) => {
capturedUrl = url;
return new Response(
JSON.stringify({ relation_tuples: [] }),
{ status: 200 },
);
});
try {
const tuples = await keto.listRelationships("files");
assertEquals(tuples.length, 0);
const u = new URL(capturedUrl);
assertEquals(u.searchParams.get("namespace"), "files");
assertEquals(u.searchParams.has("object"), false);
} finally {
restoreFetch();
}
});
Deno.test("listRelationships — with subject_id param", async () => {
let capturedUrl = "";
mockFetch((url) => {
capturedUrl = url;
return new Response(
JSON.stringify({ relation_tuples: [] }),
{ status: 200 },
);
});
try {
await keto.listRelationships("files", undefined, undefined, "user-1");
const u = new URL(capturedUrl);
assertEquals(u.searchParams.get("subject_id"), "user-1");
} finally {
restoreFetch();
}
});
Deno.test("listRelationships — throws on failure", async () => {
mockFetch(() => new Response("error", { status: 500 }));
try {
await assertRejects(
() => keto.listRelationships("files"),
Error,
"Keto listRelationships failed",
);
} finally {
restoreFetch();
}
});
Deno.test("listRelationships — handles empty relation_tuples", async () => {
mockFetch(() =>
new Response(JSON.stringify({}), { status: 200 })
);
try {
const tuples = await keto.listRelationships("files");
assertEquals(tuples.length, 0);
} finally {
restoreFetch();
}
});
// ── expandPermission tests ──────────────────────────────────────────────────
Deno.test("expandPermission — sends POST with correct body", async () => {
let capturedBody: Record<string, unknown> = {};
mockFetch((_url, init) => {
capturedBody = JSON.parse(init?.body as string);
return new Response(JSON.stringify({ type: "union", children: [] }), { status: 200 });
});
try {
const result = await keto.expandPermission("files", "file-1", "read", 5);
assertEquals(capturedBody.namespace, "files");
assertEquals(capturedBody.max_depth, 5);
assertEquals((result as Record<string, unknown>).type, "union");
} finally {
restoreFetch();
}
});
Deno.test("expandPermission — uses default max_depth of 3", async () => {
let capturedBody: Record<string, unknown> = {};
mockFetch((_url, init) => {
capturedBody = JSON.parse(init?.body as string);
return new Response(JSON.stringify({ type: "leaf" }), { status: 200 });
});
try {
await keto.expandPermission("files", "file-1", "read");
assertEquals(capturedBody.max_depth, 3);
} finally {
restoreFetch();
}
});
Deno.test("expandPermission — throws on failure", async () => {
mockFetch(() => new Response("error", { status: 500 }));
try {
await assertRejects(
() => keto.expandPermission("files", "file-1", "read"),
Error,
"Keto expandPermission failed",
);
} finally {
restoreFetch();
}
});