mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-21 14:22:14 +03:00
Merged after reconciling the whole stack onto the tip, operator-reviewed before merge. The core ownership fix for GHSA-wvxc-jp3v-5mg5 had already landed via #13211 with a different implementation of the same endpoint. This branch carries a stricter one, built up in layers: this PR, #13262 (explicit scope, audit log, atomic sweep), #13297 (revoked, deactivated, banned or expired keys rejected) and #13374 (owner-scoped file half, chunked instance sweep — SEC-C/SEC-D). The stack's implementation was kept over #13211's because it is stricter on every point: - **route:** with #13211, a request carrying BOTH a dashboard session cookie and an API key swept the whole instance. Here a presented key always scopes the sweep to that key; only a session without a key sweeps all tenants; neither returns 401. Instance-wide and row-deleting sweeps log at warn as an audit trail, and a failing sweep returns a sanitized 500. - **`deleteCompletedBatches`:** takes an explicit `{ apiKeyId } | { allTenants: true }`. An omitted or blank key throws instead of widening, and passing both throws. - **files:** a key sweep only soft-deletes files the caller owns (`deleteFileOwnedBy`), so a batch referencing another tenant's or an unowned file never nulls its content. - **transactions:** key mode is all-or-nothing across chunks; instance mode commits per 200-id chunk so a large sweep never holds one write lock on the table. #13211's own test was aligned to the explicit-scope API with its assertions unchanged. Its seed now creates the file with the batch's owner, as an upload through that key does in production — without that, SEC-C correctly leaves the unowned file intact. - 51/51 across the six batch suites (#13211's test, this stack's ownership and route-scope tests, `batch-deletion`, `batch-deletion-route-logic`, `files-delete-owned-by`) - ESLint, `typecheck:core`, complexity, cognitive-complexity, changelog integrity: clean ⚠️ base-red inherited: #12732
57 lines
2.1 KiB
TypeScript
57 lines
2.1 KiB
TypeScript
// tests/unit/files-delete-owned-by.test.ts
|
|
import { describe, it, after } from "node:test";
|
|
import assert from "node:assert";
|
|
import fs from "node:fs";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
|
|
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "files-owned-"));
|
|
process.env.DATA_DIR = TEST_DATA_DIR;
|
|
|
|
const { resetDbInstance } = await import("../../src/lib/db/core.ts");
|
|
const { createFile, getFile, getFileContent, deleteFileOwnedBy } =
|
|
await import("../../src/lib/db/files.ts");
|
|
|
|
const seed = (apiKeyId: string | null, label: string) =>
|
|
createFile({
|
|
bytes: label.length,
|
|
filename: `${label}.jsonl`,
|
|
purpose: "batch",
|
|
content: Buffer.from(label),
|
|
apiKeyId,
|
|
});
|
|
|
|
describe("deleteFileOwnedBy — owner-scoped soft delete", () => {
|
|
after(() => {
|
|
resetDbInstance();
|
|
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
|
|
});
|
|
|
|
it("soft-deletes the owner's own file and nulls its content", () => {
|
|
const own = seed("key-A", "own");
|
|
assert.strictEqual(deleteFileOwnedBy(own.id, "key-A"), true);
|
|
assert.strictEqual(getFile(own.id), null, "metadata read hides a soft-deleted file");
|
|
assert.strictEqual(getFileContent(own.id), null);
|
|
});
|
|
|
|
it("returns false and leaves another key's file intact", () => {
|
|
const other = seed("key-B", "other");
|
|
assert.strictEqual(deleteFileOwnedBy(other.id, "key-A"), false);
|
|
assert.ok(getFile(other.id));
|
|
assert.strictEqual(getFileContent(other.id)?.toString(), "other");
|
|
});
|
|
|
|
it("returns false for an unowned file (api_key_id NULL) — only the instance sweep reaches those", () => {
|
|
const unowned = seed(null, "unowned");
|
|
assert.strictEqual(deleteFileOwnedBy(unowned.id, "key-A"), false);
|
|
assert.strictEqual(getFileContent(unowned.id)?.toString(), "unowned");
|
|
});
|
|
|
|
it("throws on an empty apiKeyId instead of widening", () => {
|
|
const own = seed("key-A", "guard");
|
|
assert.throws(() => deleteFileOwnedBy(own.id, ""), /apiKeyId/);
|
|
assert.throws(() => deleteFileOwnedBy(own.id, undefined as unknown as string), /apiKeyId/);
|
|
assert.strictEqual(getFileContent(own.id)?.toString(), "guard");
|
|
});
|
|
});
|