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.
This commit is contained in:
286
tests/server/wopi_discovery_test.ts
Normal file
286
tests/server/wopi_discovery_test.ts
Normal file
@@ -0,0 +1,286 @@
|
||||
/**
|
||||
* Tests for WOPI discovery XML parsing and cache behavior.
|
||||
*/
|
||||
|
||||
import {
|
||||
assertEquals,
|
||||
assertNotEquals,
|
||||
assertRejects,
|
||||
} from "https://deno.land/std@0.224.0/assert/mod.ts";
|
||||
import {
|
||||
parseDiscoveryXml,
|
||||
getCollaboraActionUrl,
|
||||
clearDiscoveryCache,
|
||||
} from "../../server/wopi/discovery.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;
|
||||
}
|
||||
|
||||
// ── parseDiscoveryXml tests ─────────────────────────────────────────────────
|
||||
|
||||
Deno.test("parseDiscoveryXml — parses single app with single action", () => {
|
||||
const xml = `
|
||||
<wopi-discovery>
|
||||
<net-zone name="external-http">
|
||||
<app name="application/vnd.oasis.opendocument.text" favIconUrl="http://example.com/icon.png">
|
||||
<action name="edit" ext="odt" urlsrc="http://collabora:9980/loleaflet/dist/loleaflet.html?" />
|
||||
</app>
|
||||
</net-zone>
|
||||
</wopi-discovery>
|
||||
`;
|
||||
|
||||
const result = parseDiscoveryXml(xml);
|
||||
assertEquals(result.size, 1);
|
||||
|
||||
const actions = result.get("application/vnd.oasis.opendocument.text");
|
||||
assertNotEquals(actions, undefined);
|
||||
assertEquals(actions!.length, 1);
|
||||
assertEquals(actions![0].name, "edit");
|
||||
assertEquals(actions![0].ext, "odt");
|
||||
assertEquals(actions![0].urlsrc, "http://collabora:9980/loleaflet/dist/loleaflet.html?");
|
||||
});
|
||||
|
||||
Deno.test("parseDiscoveryXml — parses multiple apps", () => {
|
||||
const xml = `
|
||||
<wopi-discovery>
|
||||
<net-zone name="external-http">
|
||||
<app name="application/vnd.oasis.opendocument.text">
|
||||
<action name="edit" ext="odt" urlsrc="http://collabora:9980/edit/odt?" />
|
||||
<action name="view" ext="odt" urlsrc="http://collabora:9980/view/odt?" />
|
||||
</app>
|
||||
<app name="application/pdf">
|
||||
<action name="view" ext="pdf" urlsrc="http://collabora:9980/view/pdf?" />
|
||||
</app>
|
||||
</net-zone>
|
||||
</wopi-discovery>
|
||||
`;
|
||||
|
||||
const result = parseDiscoveryXml(xml);
|
||||
assertEquals(result.size, 2);
|
||||
|
||||
const odtActions = result.get("application/vnd.oasis.opendocument.text");
|
||||
assertEquals(odtActions!.length, 2);
|
||||
assertEquals(odtActions![0].name, "edit");
|
||||
assertEquals(odtActions![1].name, "view");
|
||||
|
||||
const pdfActions = result.get("application/pdf");
|
||||
assertEquals(pdfActions!.length, 1);
|
||||
assertEquals(pdfActions![0].name, "view");
|
||||
});
|
||||
|
||||
Deno.test("parseDiscoveryXml — returns empty map for empty XML", () => {
|
||||
const result = parseDiscoveryXml("<wopi-discovery></wopi-discovery>");
|
||||
assertEquals(result.size, 0);
|
||||
});
|
||||
|
||||
Deno.test("parseDiscoveryXml — skips actions without name or urlsrc", () => {
|
||||
const xml = `
|
||||
<wopi-discovery>
|
||||
<net-zone>
|
||||
<app name="text/plain">
|
||||
<action name="" ext="txt" urlsrc="http://example.com/view?" />
|
||||
<action name="edit" ext="txt" urlsrc="" />
|
||||
<action name="view" ext="txt" urlsrc="http://example.com/view?" />
|
||||
</app>
|
||||
</net-zone>
|
||||
</wopi-discovery>
|
||||
`;
|
||||
|
||||
const result = parseDiscoveryXml(xml);
|
||||
const actions = result.get("text/plain");
|
||||
assertEquals(actions!.length, 1);
|
||||
assertEquals(actions![0].name, "view");
|
||||
});
|
||||
|
||||
Deno.test("parseDiscoveryXml — handles realistic Collabora discovery XML", () => {
|
||||
const xml = `<?xml version="1.0" encoding="utf-8"?>
|
||||
<wopi-discovery>
|
||||
<net-zone name="external-http">
|
||||
<app name="application/vnd.openxmlformats-officedocument.wordprocessingml.document" favIconUrl="http://collabora:9980/favicon.ico">
|
||||
<action name="edit" ext="docx" urlsrc="http://collabora:9980/browser/dist/cool.html?" />
|
||||
<action name="view" ext="docx" urlsrc="http://collabora:9980/browser/dist/cool.html?" />
|
||||
</app>
|
||||
<app name="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" favIconUrl="http://collabora:9980/favicon.ico">
|
||||
<action name="edit" ext="xlsx" urlsrc="http://collabora:9980/browser/dist/cool.html?" />
|
||||
</app>
|
||||
<app name="application/vnd.oasis.opendocument.text" favIconUrl="http://collabora:9980/favicon.ico">
|
||||
<action name="edit" ext="odt" urlsrc="http://collabora:9980/browser/dist/cool.html?" />
|
||||
</app>
|
||||
</net-zone>
|
||||
</wopi-discovery>`;
|
||||
|
||||
const result = parseDiscoveryXml(xml);
|
||||
assertEquals(result.size, 3);
|
||||
assertNotEquals(
|
||||
result.get("application/vnd.openxmlformats-officedocument.wordprocessingml.document"),
|
||||
undefined,
|
||||
);
|
||||
assertNotEquals(
|
||||
result.get("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"),
|
||||
undefined,
|
||||
);
|
||||
assertNotEquals(
|
||||
result.get("application/vnd.oasis.opendocument.text"),
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
// ── getCollaboraActionUrl tests ─────────────────────────────────────────────
|
||||
|
||||
Deno.test("getCollaboraActionUrl — returns urlsrc for matching mimetype + action", async () => {
|
||||
clearDiscoveryCache();
|
||||
mockFetch(() =>
|
||||
new Response(
|
||||
`<wopi-discovery>
|
||||
<net-zone>
|
||||
<app name="text/plain">
|
||||
<action name="edit" ext="txt" urlsrc="http://collabora:9980/edit/txt?" />
|
||||
<action name="view" ext="txt" urlsrc="http://collabora:9980/view/txt?" />
|
||||
</app>
|
||||
</net-zone>
|
||||
</wopi-discovery>`,
|
||||
{ status: 200 },
|
||||
)
|
||||
);
|
||||
|
||||
try {
|
||||
const url = await getCollaboraActionUrl("text/plain", "edit");
|
||||
assertEquals(url, "http://collabora:9980/edit/txt?");
|
||||
} finally {
|
||||
restoreFetch();
|
||||
clearDiscoveryCache();
|
||||
}
|
||||
});
|
||||
|
||||
Deno.test("getCollaboraActionUrl — returns null for unknown mimetype", async () => {
|
||||
clearDiscoveryCache();
|
||||
mockFetch(() =>
|
||||
new Response(
|
||||
`<wopi-discovery>
|
||||
<net-zone>
|
||||
<app name="text/plain">
|
||||
<action name="edit" ext="txt" urlsrc="http://collabora:9980/edit/txt?" />
|
||||
</app>
|
||||
</net-zone>
|
||||
</wopi-discovery>`,
|
||||
{ status: 200 },
|
||||
)
|
||||
);
|
||||
|
||||
try {
|
||||
const url = await getCollaboraActionUrl("application/unknown", "edit");
|
||||
assertEquals(url, null);
|
||||
} finally {
|
||||
restoreFetch();
|
||||
clearDiscoveryCache();
|
||||
}
|
||||
});
|
||||
|
||||
Deno.test("getCollaboraActionUrl — returns null for unknown action", async () => {
|
||||
clearDiscoveryCache();
|
||||
mockFetch(() =>
|
||||
new Response(
|
||||
`<wopi-discovery>
|
||||
<net-zone>
|
||||
<app name="text/plain">
|
||||
<action name="edit" ext="txt" urlsrc="http://collabora:9980/edit/txt?" />
|
||||
</app>
|
||||
</net-zone>
|
||||
</wopi-discovery>`,
|
||||
{ status: 200 },
|
||||
)
|
||||
);
|
||||
|
||||
try {
|
||||
const url = await getCollaboraActionUrl("text/plain", "view");
|
||||
assertEquals(url, null);
|
||||
} finally {
|
||||
restoreFetch();
|
||||
clearDiscoveryCache();
|
||||
}
|
||||
});
|
||||
|
||||
Deno.test("getCollaboraActionUrl — defaults to 'edit' action", async () => {
|
||||
clearDiscoveryCache();
|
||||
mockFetch(() =>
|
||||
new Response(
|
||||
`<wopi-discovery>
|
||||
<net-zone>
|
||||
<app name="text/plain">
|
||||
<action name="edit" ext="txt" urlsrc="http://collabora:9980/edit/txt?" />
|
||||
</app>
|
||||
</net-zone>
|
||||
</wopi-discovery>`,
|
||||
{ status: 200 },
|
||||
)
|
||||
);
|
||||
|
||||
try {
|
||||
const url = await getCollaboraActionUrl("text/plain");
|
||||
assertEquals(url, "http://collabora:9980/edit/txt?");
|
||||
} finally {
|
||||
restoreFetch();
|
||||
clearDiscoveryCache();
|
||||
}
|
||||
});
|
||||
|
||||
Deno.test("getCollaboraActionUrl — uses cache on second call", async () => {
|
||||
clearDiscoveryCache();
|
||||
let fetchCount = 0;
|
||||
mockFetch(() => {
|
||||
fetchCount++;
|
||||
return new Response(
|
||||
`<wopi-discovery>
|
||||
<net-zone>
|
||||
<app name="text/plain">
|
||||
<action name="edit" ext="txt" urlsrc="http://collabora:9980/edit/txt?" />
|
||||
</app>
|
||||
</net-zone>
|
||||
</wopi-discovery>`,
|
||||
{ status: 200 },
|
||||
);
|
||||
});
|
||||
|
||||
try {
|
||||
await getCollaboraActionUrl("text/plain", "edit");
|
||||
await getCollaboraActionUrl("text/plain", "edit");
|
||||
assertEquals(fetchCount, 1); // Should only fetch once due to cache
|
||||
} finally {
|
||||
restoreFetch();
|
||||
clearDiscoveryCache();
|
||||
}
|
||||
});
|
||||
|
||||
Deno.test("getCollaboraActionUrl — throws after 3 failed retries", async () => {
|
||||
clearDiscoveryCache();
|
||||
mockFetch(() => new Response("Server Error", { status: 500 }));
|
||||
|
||||
try {
|
||||
await assertRejects(
|
||||
() => getCollaboraActionUrl("text/plain", "edit"),
|
||||
Error,
|
||||
"Collabora discovery fetch failed",
|
||||
);
|
||||
} finally {
|
||||
restoreFetch();
|
||||
clearDiscoveryCache();
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user