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/wopi_lock_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

292 lines
9.3 KiB
TypeScript

/**
* Tests for WOPI lock service using in-memory store.
*/
import {
assertEquals,
assertNotEquals,
} from "https://deno.land/std@0.224.0/assert/mod.ts";
import {
InMemoryLockStore,
setLockStore,
acquireLock,
getLock,
refreshLock,
releaseLock,
unlockAndRelock,
} from "../../server/wopi/lock.ts";
// Use in-memory store for all tests
function setup(): InMemoryLockStore {
const store = new InMemoryLockStore();
setLockStore(store);
return store;
}
Deno.test("acquireLock succeeds on unlocked file", async () => {
setup();
const result = await acquireLock("file-1", "lock-aaa");
assertEquals(result.success, true);
assertEquals(result.existingLockId, undefined);
});
Deno.test("acquireLock fails when different lock exists", async () => {
setup();
await acquireLock("file-1", "lock-aaa");
const result = await acquireLock("file-1", "lock-bbb");
assertEquals(result.success, false);
assertEquals(result.existingLockId, "lock-aaa");
});
Deno.test("acquireLock succeeds when same lock exists (refresh)", async () => {
setup();
await acquireLock("file-1", "lock-aaa");
const result = await acquireLock("file-1", "lock-aaa");
assertEquals(result.success, true);
});
Deno.test("getLock returns null for unlocked file", async () => {
setup();
const lock = await getLock("file-nonexistent");
assertEquals(lock, null);
});
Deno.test("getLock returns lock id for locked file", async () => {
setup();
await acquireLock("file-1", "lock-xyz");
const lock = await getLock("file-1");
assertEquals(lock, "lock-xyz");
});
Deno.test("refreshLock succeeds with matching lock", async () => {
setup();
await acquireLock("file-1", "lock-aaa");
const result = await refreshLock("file-1", "lock-aaa");
assertEquals(result.success, true);
});
Deno.test("refreshLock fails with mismatched lock", async () => {
setup();
await acquireLock("file-1", "lock-aaa");
const result = await refreshLock("file-1", "lock-bbb");
assertEquals(result.success, false);
assertEquals(result.existingLockId, "lock-aaa");
});
Deno.test("refreshLock fails on unlocked file", async () => {
setup();
const result = await refreshLock("file-1", "lock-aaa");
assertEquals(result.success, false);
});
Deno.test("releaseLock succeeds with matching lock", async () => {
setup();
await acquireLock("file-1", "lock-aaa");
const result = await releaseLock("file-1", "lock-aaa");
assertEquals(result.success, true);
// Verify lock is gone
const lock = await getLock("file-1");
assertEquals(lock, null);
});
Deno.test("releaseLock fails with mismatched lock", async () => {
setup();
await acquireLock("file-1", "lock-aaa");
const result = await releaseLock("file-1", "lock-bbb");
assertEquals(result.success, false);
assertEquals(result.existingLockId, "lock-aaa");
// Lock should still exist
const lock = await getLock("file-1");
assertEquals(lock, "lock-aaa");
});
Deno.test("releaseLock succeeds on already unlocked file", async () => {
setup();
const result = await releaseLock("file-1", "lock-aaa");
assertEquals(result.success, true);
});
Deno.test("unlockAndRelock succeeds with matching old lock", async () => {
setup();
await acquireLock("file-1", "lock-old");
const result = await unlockAndRelock("file-1", "lock-old", "lock-new");
assertEquals(result.success, true);
// New lock should be set
const lock = await getLock("file-1");
assertEquals(lock, "lock-new");
});
Deno.test("unlockAndRelock fails with mismatched old lock", async () => {
setup();
await acquireLock("file-1", "lock-aaa");
const result = await unlockAndRelock("file-1", "lock-wrong", "lock-new");
assertEquals(result.success, false);
assertEquals(result.existingLockId, "lock-aaa");
// Original lock should remain
const lock = await getLock("file-1");
assertEquals(lock, "lock-aaa");
});
Deno.test("unlockAndRelock fails when no lock exists", async () => {
setup();
const result = await unlockAndRelock("file-1", "lock-old", "lock-new");
assertEquals(result.success, false);
assertEquals(result.existingLockId, undefined);
});
Deno.test("different files have independent locks", async () => {
setup();
await acquireLock("file-1", "lock-1");
await acquireLock("file-2", "lock-2");
assertEquals(await getLock("file-1"), "lock-1");
assertEquals(await getLock("file-2"), "lock-2");
// Releasing one doesn't affect the other
await releaseLock("file-1", "lock-1");
assertEquals(await getLock("file-1"), null);
assertEquals(await getLock("file-2"), "lock-2");
});
Deno.test("full lock lifecycle: acquire -> refresh -> release", async () => {
setup();
// Acquire
const a = await acquireLock("file-1", "lock-abc");
assertEquals(a.success, true);
// Refresh
const r = await refreshLock("file-1", "lock-abc");
assertEquals(r.success, true);
// Still locked
assertNotEquals(await getLock("file-1"), null);
// Release
const rel = await releaseLock("file-1", "lock-abc");
assertEquals(rel.success, true);
// Gone
assertEquals(await getLock("file-1"), null);
});
// ── InMemoryLockStore direct tests ──────────────────────────────────────────
Deno.test("InMemoryLockStore — get returns null for nonexistent key", async () => {
const store = new InMemoryLockStore();
assertEquals(await store.get("nonexistent"), null);
});
Deno.test("InMemoryLockStore — setNX sets value and returns true", async () => {
const store = new InMemoryLockStore();
const result = await store.setNX("key1", "value1", 60);
assertEquals(result, true);
assertEquals(await store.get("key1"), "value1");
});
Deno.test("InMemoryLockStore — setNX returns false if key exists", async () => {
const store = new InMemoryLockStore();
await store.setNX("key1", "value1", 60);
const result = await store.setNX("key1", "value2", 60);
assertEquals(result, false);
assertEquals(await store.get("key1"), "value1");
});
Deno.test("InMemoryLockStore — set overwrites unconditionally", async () => {
const store = new InMemoryLockStore();
await store.setNX("key1", "value1", 60);
await store.set("key1", "value2", 60);
assertEquals(await store.get("key1"), "value2");
});
Deno.test("InMemoryLockStore — del removes key", async () => {
const store = new InMemoryLockStore();
await store.setNX("key1", "value1", 60);
await store.del("key1");
assertEquals(await store.get("key1"), null);
});
Deno.test("InMemoryLockStore — del on nonexistent key is no-op", async () => {
const store = new InMemoryLockStore();
await store.del("nonexistent");
// Should not throw
});
Deno.test("InMemoryLockStore — expire returns false for nonexistent key", async () => {
const store = new InMemoryLockStore();
const result = await store.expire("nonexistent", 60);
assertEquals(result, false);
});
Deno.test("InMemoryLockStore — expire returns true for existing key", async () => {
const store = new InMemoryLockStore();
await store.setNX("key1", "value1", 60);
const result = await store.expire("key1", 120);
assertEquals(result, true);
assertEquals(await store.get("key1"), "value1");
});
Deno.test("InMemoryLockStore — expired key returns null on get", async () => {
const store = new InMemoryLockStore();
// Set with 0 TTL so it expires immediately
await store.set("key1", "value1", 0);
// Wait briefly to ensure expiry (0 seconds TTL)
await new Promise((r) => setTimeout(r, 10));
assertEquals(await store.get("key1"), null);
});
Deno.test("InMemoryLockStore — expired key allows setNX", async () => {
const store = new InMemoryLockStore();
await store.set("key1", "value1", 0);
await new Promise((r) => setTimeout(r, 10));
const result = await store.setNX("key1", "value2", 60);
assertEquals(result, true);
assertEquals(await store.get("key1"), "value2");
});
Deno.test("InMemoryLockStore — expire on expired key returns false", async () => {
const store = new InMemoryLockStore();
await store.set("key1", "value1", 0);
await new Promise((r) => setTimeout(r, 10));
const result = await store.expire("key1", 60);
assertEquals(result, false);
});
// ── Lock TTL-related tests ──────────────────────────────────────────────────
Deno.test("acquireLock then getLock after TTL expiry returns null", async () => {
const store = new InMemoryLockStore();
setLockStore(store);
// Directly set a lock with very short TTL via the store
await store.set("wopi:lock:file-expiry", "lock-ttl", 0);
await new Promise((r) => setTimeout(r, 10));
const lock = await store.get("wopi:lock:file-expiry");
assertEquals(lock, null);
});
Deno.test("concurrent lock attempts — second attempt fails", async () => {
setup();
const [r1, r2] = await Promise.all([
acquireLock("file-concurrent", "lock-a"),
acquireLock("file-concurrent", "lock-b"),
]);
// One should succeed, one should fail (or both succeed with same lock)
const successes = [r1, r2].filter((r) => r.success);
const failures = [r1, r2].filter((r) => !r.success);
// At least one should succeed
assertEquals(successes.length >= 1, true);
if (failures.length > 0) {
// If one failed, it should report the existing lock
assertNotEquals(failures[0].existingLockId, undefined);
}
});