Initial commit
This commit is contained in:
153
tests/drop-store.test.mjs
Normal file
153
tests/drop-store.test.mjs
Normal file
@@ -0,0 +1,153 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { mkdtemp, readFile, rm, stat, utimes, access } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { loadModule } from "./test-helpers.mjs";
|
||||
|
||||
const { DropStore, DropStoreError, pathsFor } = await loadModule(
|
||||
"server/utils/drop-store.ts",
|
||||
);
|
||||
const { CHUNK_SIZE } = await loadModule("shared/types/drop.ts");
|
||||
const noncePrefix = "AQIDBAUGBwg";
|
||||
let roots = [];
|
||||
async function fresh(clock = { now: () => Date.now() }) {
|
||||
const root = await mkdtemp(join(tmpdir(), "quickdrop-test-"));
|
||||
roots.push(root);
|
||||
return { store: new DropStore({ root, clock }), root };
|
||||
}
|
||||
async function reserve(store, maxReads = 1, expiresIn = "24h") {
|
||||
return store.reserve({
|
||||
version: 2,
|
||||
chunkSize: CHUNK_SIZE,
|
||||
chunkCount: 1,
|
||||
noncePrefix,
|
||||
expiresIn,
|
||||
maxReads,
|
||||
});
|
||||
}
|
||||
async function ready(store, maxReads = 1, expiresIn = "24h") {
|
||||
const result = await reserve(store, maxReads, expiresIn);
|
||||
await store.putChunk(result.id, 0, new Uint8Array(16));
|
||||
await store.finalize(result.id, new Uint8Array(17));
|
||||
return result;
|
||||
}
|
||||
async function exists(path) {
|
||||
try {
|
||||
await access(path);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
test.after(async () => {
|
||||
await Promise.all(
|
||||
roots.map((root) => rm(root, { recursive: true, force: true })),
|
||||
);
|
||||
});
|
||||
|
||||
test("finalize starts expiry and rejects missing chunks", async () => {
|
||||
let now = 1000;
|
||||
const { store } = await fresh({ now: () => now });
|
||||
const result = await reserve(store, 1, "1h");
|
||||
await assert.rejects(
|
||||
store.finalize(result.id, new Uint8Array(17)),
|
||||
(error) => error instanceof DropStoreError && error.code === "incomplete",
|
||||
);
|
||||
await store.putChunk(result.id, 0, new Uint8Array(16));
|
||||
const metadata = await store.finalize(result.id, new Uint8Array(17));
|
||||
assert.equal(metadata.expiresAt, now + 3600000);
|
||||
now += 3599999;
|
||||
assert.equal(
|
||||
(await store.descriptor(result.id, result.access)).id,
|
||||
result.id,
|
||||
);
|
||||
now++;
|
||||
await assert.rejects(
|
||||
store.descriptor(result.id, result.access),
|
||||
(error) => error instanceof DropStoreError && error.code === "expired",
|
||||
);
|
||||
});
|
||||
|
||||
test("chunk retries are idempotent and conflicting contents are rejected", async () => {
|
||||
const { store } = await fresh();
|
||||
const result = await reserve(store);
|
||||
const body = new Uint8Array(16).fill(7);
|
||||
assert.equal(await store.putChunk(result.id, 0, body), "created");
|
||||
assert.equal(await store.putChunk(result.id, 0, body), "same");
|
||||
await assert.rejects(
|
||||
store.putChunk(result.id, 0, new Uint8Array(16).fill(8)),
|
||||
(error) => error instanceof DropStoreError && error.code === "conflict",
|
||||
);
|
||||
});
|
||||
|
||||
test("maxReads claims are serialized, valid lease reuse does not increment, and completion cleans up", async () => {
|
||||
const { store, root } = await fresh();
|
||||
const result = await ready(store, 1);
|
||||
const claims = await Promise.allSettled([
|
||||
store.claim(result.id, result.access),
|
||||
store.claim(result.id, result.access),
|
||||
]);
|
||||
assert.equal(
|
||||
claims.filter((value) => value.status === "fulfilled").length,
|
||||
1,
|
||||
);
|
||||
assert.equal(claims.filter((value) => value.status === "rejected").length, 1);
|
||||
const first = claims.find((value) => value.status === "fulfilled").value;
|
||||
const metadataBefore = await store.readMetadata(result.id);
|
||||
const reused = await store.claim(result.id, result.access, first.token);
|
||||
assert.equal(reused.reused, true);
|
||||
assert.equal(
|
||||
(await store.readMetadata(result.id)).readsClaimed,
|
||||
metadataBefore.readsClaimed,
|
||||
);
|
||||
const chunk = await store.readChunk(result.id, 0, first.token);
|
||||
assert.equal(chunk.size, 16);
|
||||
await chunk.handle.close();
|
||||
await store.complete(result.id, first.token);
|
||||
assert.equal(await exists(pathsFor(result.id, root).directory), false);
|
||||
});
|
||||
|
||||
test("unlimited drops allow multiple leases and metadata remains parseable", async () => {
|
||||
const { store, root } = await fresh();
|
||||
const result = await ready(store, null);
|
||||
const one = await store.claim(result.id, result.access);
|
||||
const two = await store.claim(result.id, result.access);
|
||||
assert.notEqual(one.token, two.token);
|
||||
assert.equal((await store.readMetadata(result.id)).readsClaimed, 2);
|
||||
JSON.parse(await readFile(pathsFor(result.id, root).metadata, "utf8"));
|
||||
await store.complete(result.id, one.token);
|
||||
assert.equal(await exists(pathsFor(result.id, root).directory), true);
|
||||
});
|
||||
|
||||
test("idle lease cleanup and stale temporary files are safe", async () => {
|
||||
let now = 100000;
|
||||
const { store, root } = await fresh({ now: () => now });
|
||||
const result = await ready(store, 1);
|
||||
await store.claim(result.id, result.access);
|
||||
const paths = pathsFor(result.id, root);
|
||||
const stale = join(paths.directory, ".chunk-stale");
|
||||
await (await import("node:fs/promises")).writeFile(stale, "stale");
|
||||
const old = new Date(now - 2 * 3600000);
|
||||
await utimes(stale, old, old);
|
||||
now += 16 * 60 * 1000;
|
||||
await store.cleanup();
|
||||
assert.equal(await exists(stale), false);
|
||||
assert.equal(await exists(paths.directory), false);
|
||||
});
|
||||
|
||||
test("metadata-less valid directories are removed only after grace period", async () => {
|
||||
let now = 10_000_000;
|
||||
const { store, root } = await fresh({ now: () => now });
|
||||
const result = await reserve(store);
|
||||
const paths = pathsFor(result.id, root);
|
||||
await rm(paths.metadata);
|
||||
const recent = new Date(now);
|
||||
await utimes(paths.directory, recent, recent);
|
||||
await store.cleanup();
|
||||
assert.equal(await exists(paths.directory), true);
|
||||
const old = new Date(now - 2 * 3600000);
|
||||
await utimes(paths.directory, old, old);
|
||||
await store.cleanup();
|
||||
assert.equal(await exists(paths.directory), false);
|
||||
});
|
||||
Reference in New Issue
Block a user