fix: deduplicate owner access alerts per recipient (#896)

* fix: prevent duplicate access alerts to the same recipient

* fix: address recipient alert recovery review findings

* fix: reconcile unsent alerts and wait for startup migration

* fix: preserve legacy recipient alerts through upgrades

* fix: reconcile alert results and legacy recovery races

* fix: fence dispatch and clean cancelled alert metadata

* fix: fence alert preparation and retain in-flight recovery

* fix: atomically commit alert dispatch and retry deferred recovery

* fix: keep recovery lookups from blocking upstream file reads
This commit is contained in:
Thomas Durieux authored and GitHub committed 2026-10-13 13:55:10 +01:00
1 parent eb9ac7e13d
commit 445a479339
20 files changed
+1724 -89

No files matched your search

+101
View File
@@ -0,0 +1,101 @@
const { expect } = require("chai");
const { spawnSync } = require("child_process");
const fs = require("fs");
const os = require("os");
const path = require("path");
describe("owner alert migration deployment", function () {
this.timeout(10000);
function deploy(options = {}) {
const directory = fs.mkdtempSync(path.join(os.tmpdir(), "owner-alert-deploy-"));
const statePath = path.join(directory, "state.json"), logPath = path.join(directory, "calls.jsonl");
fs.mkdirSync(path.join(directory, "scripts"));
fs.copyFileSync(path.join(__dirname, "../scripts/deploy.sh"), path.join(directory, "scripts/deploy.sh"));
fs.writeFileSync(statePath, JSON.stringify({ ...options }));
fs.writeFileSync(path.join(directory, "docker"), `#!/usr/bin/env node
const fs = require("fs");
const args = process.argv.slice(2), state = JSON.parse(fs.readFileSync(process.env.TASK_DOCKER_STATE));
fs.appendFileSync(process.env.TASK_DOCKER_LOG, JSON.stringify(args) + "\\n");
const finish = (output = "", code = 0) => { fs.writeFileSync(process.env.TASK_DOCKER_STATE, JSON.stringify(state)); process.stdout.write(output); process.exit(code); };
if (args[0] === "image" && args[1] === "inspect") {
if (args.some(a => a.includes("Labels"))) finish(state.badProtocol ? "<no value>" : "recipient-v1");
finish(state.built ? "sha:new" : "sha:old");
}
if (args[0] === "inspect") {
if (args.some(a => a.includes("Labels"))) finish("<no value>");
if (args.includes("{{.State.Running}}")) finish(state.stopped && !state.refuseStop ? "false" : "true");
finish("healthy");
}
if (args[0] === "compose") {
if (args[1] === "build") { if (state.buildFails) finish("", 1); state.built = true; finish(); }
if (args[1] === "stop") { state.stopped = true; finish(); }
if (args[1] === "run") { if (state.reconcileFails) finish("", 1); state.reconciled = true; finish(); }
if (args[1] === "up") { if (args.includes("streamer")) state.streamers = true; else state.api = true; finish(); }
if (args[1] === "ps") {
if (args.includes("-q")) finish(args.includes("streamer") ? "old-api\\nold-1\\nold-2\\nold-3\\nold-4\\n" : "new-api");
if (args.includes("streamer")) finish(state.streamers ? "new-1\\nnew-2\\nnew-3\\nnew-4\\n" : "old-1\\nold-2\\nold-3\\nold-4\\n");
finish();
}
}
finish("unexpected docker call", 2);
`, { mode: 0o755 });
try {
const result = spawnSync("bash", [path.join(directory, "scripts/deploy.sh")], {
encoding: "utf8", timeout: 8000,
env: { ...process.env, PATH: directory + path.delimiter + process.env.PATH,
FORCE: String(options.force ?? 1), STREAMER_REPLICAS: "4", TASK_DOCKER_STATE: statePath, TASK_DOCKER_LOG: logPath },
});
return { ...result, calls: fs.readFileSync(logPath, "utf8").trim().split("\n").map(line => JSON.parse(line)) };
} finally { fs.rmSync(directory, { recursive: true, force: true }); }
}
it("stops and verifies every legacy writer before starting any migration-capable instance", () => {
const result = deploy();
expect(result.status, result.stderr).to.equal(0);
const stop = result.calls.findIndex(args => args[0] === "compose" && args[1] === "stop");
const up = result.calls.findIndex(args => args[0] === "compose" && args[1] === "up");
expect(stop).to.be.greaterThan(-1); expect(up).to.be.greaterThan(stop);
const reconcile = result.calls.findIndex(args => args[0] === "compose" && args[1] === "run");
expect(reconcile).to.be.greaterThan(stop); expect(reconcile).to.be.lessThan(up);
expect(result.calls[reconcile]).to.include("./build/scripts/reconcile-legacy-owner-alerts.js");
const checked = result.calls.slice(stop + 1, up).filter(args => args[0] === "inspect").map(args => args.at(-1));
expect(checked).to.deep.equal(["old-api", "old-1", "old-2", "old-3", "old-4"]);
expect(result.calls[stop]).to.include.members(["anonymous_github", "streamer"]);
expect(result.calls[up]).to.include.members(["--force-recreate", "streamer=4"]);
const api = result.calls.findIndex(args => args[0] === "compose" && args[1] === "up" && args.at(-1) === "anonymous_github");
expect(api).to.be.greaterThan(up);
for (const name of ["new-1", "new-2", "new-3", "new-4"]) {
expect(result.calls.slice(up + 1, api).some(args => args[0] === "inspect" && args.at(-1) === name)).to.equal(true);
}
});
it("aborts before migration starts if a legacy writer remains running", () => {
const result = deploy({ refuseStop: true });
expect(result.status).not.to.equal(0);
expect(result.stderr).to.include("still running");
expect(result.calls.some(args => args[0] === "compose" && args[1] === "up")).to.equal(false);
});
it("keeps old containers running if the image build fails", () => {
const result = deploy({ buildFails: true });
expect(result.status).not.to.equal(0);
expect(result.calls.some(args => args[0] === "compose" && ["stop", "up"].includes(args[1]))).to.equal(false);
});
it("keeps old containers running if the replacement lacks migration support", () => {
const result = deploy({ badProtocol: true });
expect(result.status).not.to.equal(0);
expect(result.stderr).to.include("lacks recipient alert migration support");
expect(result.calls.some(args => args[0] === "compose" && ["stop", "up"].includes(args[1]))).to.equal(false);
});
it("starts no application instance if rollback reconciliation cannot be prepared", () => {
const result = deploy({ reconcileFails: true });
expect(result.status).not.to.equal(0);
expect(result.calls.some(args => args[0] === "compose" && args[1] === "up")).to.equal(false);
});
for (const stopped of [true, false]) it(`reconciles legacy ${stopped ? "stopped" : "running"} containers when the replacement was already built`, () => {
const result = deploy({ built: true, stopped, force: 0 });
expect(result.status, result.stderr).to.equal(0);
const reconcile = result.calls.findIndex(args => args[0] === "compose" && args[1] === "run");
const up = result.calls.findIndex(args => args[0] === "compose" && args[1] === "up");
expect(reconcile).to.be.greaterThan(-1); expect(up).to.be.greaterThan(reconcile);
expect(result.stdout).not.to.include("nothing to deploy");
});
});
+191 -36
View File
@@ -11,37 +11,122 @@ describe("owner access notifications", () => {
const ownerId = "507f1f77bcf86cd799439011";
const resource = { kind: "repository", id: "507f1f77bcf86cd799439012" };
const other = { kind: "repository", id: "507f1f77bcf86cd799439013" };
let owner, sent, claims, rows, generations;
let owner, ownerState, sent, claims, rows, generations, resourceRevisions, attemptIds, recipients, events;
beforeEach(() => {
owner = { status: "active", emails: [{ email: "owner@example.com", default: true }] };
sent = []; claims = []; rows = new Map(); generations = new Map();
ownerState = {};
sent = []; claims = []; rows = new Map(); generations = new Map(); recipients = new Map(); events = [];
resourceRevisions = new Map(); attemptIds = new Map();
stub(config, "RESEND_API_KEY", "test-resend-key");
stub(config, "EMAIL_FROM", "alerts@example.com");
stub(config, "APP_HOSTNAME", "anonymous.example.com");
stub(User, "findOne", () => ({ lean: async () => ["removed", "banned"].includes(owner?.status) ? null : owner }));
stub(User, "findById", () => ({ select: () => ({ lean: async () => owner }) }));
Object.defineProperty(User.db, "readyState", { configurable: true, value: 1 });
restores.push(() => { delete User.db.readyState; });
stub(User.db, "collection", name => ({
findOne: async filter => {
const key = `${name}:${filter.owner}:${filter._id}`;
if (!generations.has(key)) generations.set(key, undefined);
return { accessAlertGeneration: generations.get(key) };
},
updateOne: async (filter, update) => {
claims.push(filter);
const key = `${name}:${filter.owner}:${filter._id}`;
if (rows.has(key) || generations.get(key) !== (typeof filter.accessAlertGeneration === "string" ? filter.accessAlertGeneration : undefined)) return { modifiedCount: 0 };
rows.set(key, update.$set.accessAlertClaimedAt);
return { modifiedCount: 1 };
},
updateMany: async (filter, update) => {
for (const key of generations.keys()) if (key.startsWith(`${name}:${filter.owner}:`) && (!filter._id || key.endsWith(`:${filter._id}`))) {
rows.delete(key);
generations.set(key, update.$set.accessAlertGeneration);
}
},
}));
stub(global, "fetch", async (url, options) => { sent.push({ url, options, body: JSON.parse(options.body) }); return { ok: true }; });
stub(User.db, "collection", name => {
if (name === "users") return {
findOne: async filter => String(filter._id) === ownerId ? { ...ownerState } : null,
findOneAndUpdate: async (filter, update) => {
if (String(filter._id) !== ownerId) return { value: null };
ownerState.accessAlertResetRevision = (ownerState.accessAlertResetRevision || 0) + update.$inc.accessAlertResetRevision;
Object.assign(ownerState, update.$set); return { value: { ...ownerState } };
},
updateOne: async (filter, update) => {
if (String(filter._id) !== ownerId) return { matchedCount: 0 };
if ("accessAlertResetRevision" in filter && ownerState.accessAlertResetRevision !== (typeof filter.accessAlertResetRevision === "number" ? filter.accessAlertResetRevision : undefined)) return { matchedCount: 0 };
if (filter.$or && ownerState.accessAlertResetUntil?.getTime() > Date.now()) return { matchedCount: 0 };
const get = path => path.split(".").reduce((value, key) => value?.[key], ownerState);
const set = (path, value) => {
const keys = path.split("."), last = keys.pop(); let target = ownerState;
for (const key of keys) target = target[key] ||= {};
if (value === undefined) delete target[last]; else target[last] = value;
};
for (const [key, value] of Object.entries(filter)) if (key.startsWith("accessAlertDispatches.") && get(key) !== value) return { matchedCount: 0 };
if (Array.isArray(update)) {
const cutoff = update[0].$set.accessAlertDispatches.$arrayToObject.$filter.cond.$or[1].$gt[1];
ownerState.accessAlertDispatches = Object.fromEntries(Object.entries(ownerState.accessAlertDispatches || {}).filter(([, value]) => value.state === "committed" || value.claimedAt > cutoff));
for (const [key, value] of Object.entries(update[1].$set)) set(key, value);
} else {
if (update.$inc) for (const [key, value] of Object.entries(update.$inc)) set(key, (get(key) || 0) + value);
if (update.$set) for (const [key, value] of Object.entries(update.$set)) set(key, value);
if (update.$unset) for (const key of Object.keys(update.$unset)) set(key, undefined);
}
return { matchedCount: 1 };
},
};
if (name === "owner_access_alert_events") return { insertOne: async record => events.push(record), findOne: async filter => events.find(event => event._id === filter._id) };
if (name === notifications.RECIPIENT_ALERTS) return {
findOne: async filter => recipients.get(filter._id),
find: filter => {
const matching = () => [...recipients.values()].filter(r => {
if (filter.deferredRecoveries) return r.deferredRecoveries?.some(recovery => String(recovery.owner) === String(filter.deferredRecoveries.$elemMatch.owner));
const claim = filter.$or[0];
return String(r.owner) === String(claim.owner) && r.resourceKind === claim.resourceKind
&& String(r.resourceId) === String(claim.resourceId) && r.generation !== claim.generation.$ne && filter.state.$in.includes(r.state);
}).map(r => ({ ...r }));
return { toArray: async () => matching(), async *[Symbol.asyncIterator]() { yield* matching(); } };
},
updateMany: async filter => { if (filter.legacyClaims?.$elemMatch) return; const claim = filter; for (const r of recipients.values()) if (String(r.owner) === String(claim.owner)
&& (!claim.resourceId || String(r.resourceId) === String(claim.resourceId))) r.resetRevision = (r.resetRevision || 0) + 1; },
insertOne: async record => { if (recipients.has(record._id)) throw Object.assign(new Error("duplicate"), { code: 11000 }); recipients.set(record._id, { ...record }); },
updateOne: async (filter, update) => { const record = recipients.get(filter._id);
const stateMatches = !filter.state || (typeof filter.state === "string" ? record?.state === filter.state : filter.state.$in.includes(record?.state));
const revisionMatches = !("resetRevision" in filter) || record?.resetRevision === (typeof filter.resetRevision === "number" ? filter.resetRevision : undefined);
if (!record || record.attemptId !== filter.attemptId || !stateMatches || !revisionMatches) return { matchedCount: 0 };
if (Array.isArray(update)) {
const recovery = update[0].$set.deferredRecoveries.$concatArrays[1][0];
record.deferredRecoveries = [recovery];
} else Object.assign(record, update.$set);
return { matchedCount: 1 };
},
deleteOne: async filter => { const r = recipients.get(filter._id);
const stateMatches = typeof filter.state === "string" ? r?.state === filter.state : filter.state.$in.includes(r?.state);
const revisionMatches = !("resetRevision" in filter) || r?.resetRevision === (typeof filter.resetRevision === "number" ? filter.resetRevision : undefined);
if (r?.attemptId === filter.attemptId && stateMatches && revisionMatches) { recipients.delete(filter._id); return { deletedCount: 1 }; }
return { deletedCount: 0 };
},
deleteMany: async filter => { let deletedCount = 0; for (const [key, r] of recipients) {
if (String(r.owner) === String(filter.owner) && r.resourceKind === filter.resourceKind && String(r.resourceId) === String(filter.resourceId)
&& r.generation !== filter.generation.$ne && filter.state.$in.includes(r.state)) { recipients.delete(key); deletedCount++; }
} return { deletedCount }; },
};
return {
findOne: async filter => {
const key = `${name}:${filter.owner}:${filter._id}`;
if (!generations.has(key)) generations.set(key, undefined);
if ("accessAlertGeneration" in filter && generations.get(key) !== (typeof filter.accessAlertGeneration === "string" ? filter.accessAlertGeneration : undefined)) return null;
if (filter.accessAlertClaimedAt?.$exists && !rows.has(key)) return null;
if ("accessAlertClaimRevision" in filter && (resourceRevisions.get(key) || 0) !== (typeof filter.accessAlertClaimRevision === "number" ? filter.accessAlertClaimRevision : 0)) return null;
if (filter.accessAlertAttemptId && attemptIds.get(key) !== filter.accessAlertAttemptId) return null;
return { accessAlertGeneration: generations.get(key), accessAlertClaimedAt: rows.get(key), accessAlertClaimRevision: resourceRevisions.get(key), status: "ready" };
},
updateOne: async (filter, update) => {
claims.push(filter); const key = `${name}:${filter.owner}:${filter._id}`;
if (generations.get(key) !== (typeof filter.accessAlertGeneration === "string" ? filter.accessAlertGeneration : undefined)) return { matchedCount: 0 };
const revision = resourceRevisions.get(key) || 0;
if (Array.isArray(update)) {
const first = filter.$or[0].accessAlertClaimRevision;
const expected = typeof first === "number" ? first : 0;
const attemptId = update[0].$set.accessAlertAttemptId.$cond[0].$eq[1];
if (revision !== expected && !(revision === expected + 1 && attemptIds.get(key) === attemptId)) return { matchedCount: 0 };
resourceRevisions.set(key, revision + 1);
if (attemptIds.get(key) === attemptId) { rows.delete(key); attemptIds.delete(key); }
} else {
if ("accessAlertClaimRevision" in filter && revision !== (typeof filter.accessAlertClaimRevision === "number" ? filter.accessAlertClaimRevision : 0)) return { matchedCount: 0 };
rows.set(key, update.$set.accessAlertClaimedAt); attemptIds.set(key, update.$set.accessAlertAttemptId);
resourceRevisions.set(key, revision + update.$inc.accessAlertClaimRevision);
}
return { matchedCount: 1 };
},
updateMany: async (filter, update) => {
for (const key of generations.keys()) if (key.startsWith(`${name}:${filter.owner}:`) && (!filter._id || key.endsWith(`:${filter._id}`))) {
rows.delete(key); attemptIds.delete(key); generations.set(key, update.$set.accessAlertGeneration);
}
},
};
});
stub(global, "fetch", async (url, options) => { sent.push({ url, options, body: JSON.parse(options.body) }); return { ok: true, status: 200 }; });
});
afterEach(() => { while (restores.length) restores.pop()(); });
@@ -53,30 +138,55 @@ describe("owner access notifications", () => {
expect(notificationEmail([{ email: "first@example.com", default: false }, { email: "primary@example.com", default: true }])).to.equal("primary@example.com");
});
it("sends once per resource across concurrent readers and repeated errors, with no time expiry", async () => {
it("sends once per recipient across resources and concurrent readers, with no time expiry", async () => {
await Promise.all(Array.from({ length: 10 }, () => notifyOwnerAccessProblem(ownerId, "token_expired", resource)));
expect(sent).to.have.length(1);
for (const key of rows.keys()) rows.set(key, new Date("2000-01-01"));
await notifyOwnerAccessProblem(ownerId, "repo_not_found", resource);
expect(sent).to.have.length(1);
await notifyOwnerAccessProblem(ownerId, "repo_not_found", other);
expect(sent).to.have.length(2);
expect(sent).to.have.length(1);
expect(sent[0].url).to.equal("https://api.resend.com/emails");
expect(sent[0].body.to).to.deep.equal(["owner@example.com"]);
expect(sent[0].body.text).to.include("https://anonymous.example.com/connections");
expect(claims[0].accessAlertClaimedAt).to.deep.equal({ $exists: false });
expect(sent[0].options.headers["Idempotency-Key"]).to.match(/^owner-access-alert\//);
expect(events[0].state).to.equal("prepared");
expect(events[1].state).to.equal("accepted");
});
it("owner action resets only the selected resource and cannot reset another owner's alerts", async () => {
it("reopens an alert only after owner action and successful access to its triggering resource", async () => {
await notifyOwnerAccessProblem(ownerId, "token_expired", resource);
await notifyOwnerAccessProblem(ownerId, "token_expired", other);
await notifications.resetOwnerAccessAlerts("507f1f77bcf86cd799439099", resource);
await notifyOwnerAccessProblem(ownerId, "token_expired", resource);
expect(sent).to.have.length(2);
expect(sent).to.have.length(1);
await notifications.resetOwnerAccessAlerts(ownerId, resource);
await notifyOwnerAccessProblem(ownerId, "token_expired", resource);
await notifyOwnerAccessProblem(ownerId, "token_expired", other);
expect(sent).to.have.length(3);
expect(sent).to.have.length(1);
await notifications.withOwnerAccessRecovery(ownerId, other, async () => {});
expect(recipients.size).to.equal(1);
await notifications.withOwnerAccessRecovery(ownerId, resource, async () => {});
await notifyOwnerAccessProblem(ownerId, "token_expired", resource);
expect(sent).to.have.length(2);
expect(events).to.have.length(4);
});
it("cannot clear another owner's recipient guard", async () => {
await notifyOwnerAccessProblem(ownerId, "token_expired", resource);
await notifications.resetOwnerAccessAlerts("507f1f77bcf86cd799439099", resource);
await notifications.withOwnerAccessRecovery("507f1f77bcf86cd799439099", resource, async () => {});
expect(recipients.size).to.equal(1);
});
it("does not reopen on background success without owner action", async () => {
await notifyOwnerAccessProblem(ownerId, "token_expired", resource);
await notifications.withOwnerAccessRecovery(ownerId, resource, async () => {});
expect(recipients.size).to.equal(1);
});
it("retains the guard when access recovery fails", async () => {
await notifyOwnerAccessProblem(ownerId, "token_expired", resource);
await notifications.resetOwnerAccessAlerts(ownerId, resource);
try { await notifications.withOwnerAccessRecovery(ownerId, resource, async () => { throw new Error("still forbidden"); }); } catch { /* Expected upstream failure. */ }
expect(recipients.size).to.equal(1);
});
it("rejects a pre-action failure paused during owner lookup", async () => {
@@ -98,16 +208,16 @@ describe("owner access notifications", () => {
expect(sent).to.have.length(1);
});
it("reconnecting resets all of the owner's resource types", async () => {
it("reconnecting leaves the recipient blocked across all resource types", async () => {
const resources = [resource, { ...resource, kind: "pull-request" }, { ...resource, kind: "gist" }];
for (const item of resources) await notifyOwnerAccessProblem(ownerId, "token_expired", item);
expect(sent).to.have.length(3);
expect(sent).to.have.length(1);
await notifications.resetOwnerAccessAlerts(ownerId);
for (const item of resources) await notifyOwnerAccessProblem(ownerId, "token_expired", item);
expect(sent).to.have.length(6);
expect(sent).to.have.length(1);
});
for (const failure of ["rejected", "timeout"]) it(`retains the claim after ${failure} until owner action`, async () => {
for (const failure of ["rejected", "timeout"]) it(`retains the claim after ${failure} until confirmed recovery`, async () => {
let attempts = 0;
stub(global, "fetch", async () => { attempts++; if (failure === "timeout") throw new Error("timeout"); return { ok: false }; });
await notifyOwnerAccessProblem(ownerId, "token_expired", resource);
@@ -115,6 +225,9 @@ describe("owner access notifications", () => {
expect(attempts).to.equal(1);
await notifications.resetOwnerAccessAlerts(ownerId, resource);
await notifyOwnerAccessProblem(ownerId, "token_expired", resource);
expect(attempts).to.equal(1);
await notifications.withOwnerAccessRecovery(ownerId, resource, async () => {});
await notifyOwnerAccessProblem(ownerId, "token_expired", resource);
expect(attempts).to.equal(2);
});
@@ -423,12 +536,54 @@ describe("repository probes and truncated file recovery", () => {
const Files = require("../src/core/model/files/files.model").default;
const File = require("../src/core/AnonymizedFile").default;
const GitHubStream = require("../src/core/source/GitHubStream").default;
const AnonymizedRepositories = require("../src/core/model/anonymizedRepositories/anonymizedRepositories.model").default;
const storage = require("../src/core/storage").default;
const { Readable } = require("node:stream");
const { classifyGitHubMissError } = require("../src/core/source/GitHubBase");
const restores = [];
const stub = (object, key, value) => { const old = object[key]; restores.push(() => object[key] = old); object[key] = value; };
const source = { getToken: async () => "probe-test", repoId: "anon", organization: "owner", repoName: "repo", commit: "sha" };
const missing = () => Object.assign(new Error("Not found"), { status: 404 });
afterEach(() => { while (restores.length) restores.pop()(); });
for (const failed of [false, true]) it(`continues truncated metadata reads after recovery lookup failure: ${failed ? "upstream failure" : "success"}`, async () => {
const upstreamError = Object.assign(new Error("GitHub unavailable"), { status: 503 });
let reads = 0;
stub(db, "isConnected", true);
stub(AnonymizedRepositories, "findOne", () => ({ select: () => ({ lean: () => ({ exec: async () => { throw new Error("recovery database unavailable"); } }) }) }));
stub(notifications, "withOwnerAccessRecovery", () => { throw new Error("recovery must be skipped without metadata"); });
stub(github, "octokit", () => ({ repos: { getContent: async () => {
reads++; if (failed) throw upstreamError;
return { data: { type: "file", sha: "blob", size: 11 } };
} } }));
const result = await new GitHubStream(source).fetchFileInfoFromPath("large/file.txt").then(value => ({ value }), error => ({ error }));
expect(reads).to.equal(1);
if (failed) expect(result.error).to.equal(upstreamError);
else expect(result.value).to.include({ name: "file.txt", path: "large", sha: "blob", size: 11 });
});
for (const failed of [false, true]) it(`continues uncached file reads after recovery lookup failure: ${failed ? "upstream failure" : "success"}`, async () => {
const upstreamError = Object.assign(new Error("GitHub unavailable"), { status: 503 });
const data = { ...source, repoId: `lookup-failure-${failed}` };
let cached, downloads = 0, recoveryLookups = 0;
stub(db, "isConnected", true);
stub(AnonymizedRepositories, "findOne", () => ({ select: fields => ({ lean: () => ({ exec: async () => {
if (fields === "owner source githubAccess") { recoveryLookups++; throw new Error("recovery database unavailable"); }
return { status: "ready" };
} }) }) }));
stub(notifications, "withOwnerAccessRecovery", () => { throw new Error("recovery must be skipped without metadata"); });
stub(storage, "fileInfo", async () => { if (!cached) throw Object.assign(new Error("missing"), { code: "ENOENT" }); return { size: cached.length }; });
stub(storage, "write", async (_repo, _path, input) => { const chunks = []; for await (const chunk of input) chunks.push(chunk); cached = Buffer.concat(chunks); });
stub(storage, "read", async () => Readable.from([cached]));
const stream = new GitHubStream(data);
stream.downloadWithFallback = async () => { downloads++; if (failed) throw upstreamError; return Readable.from([Buffer.from("fresh-bytes")]); };
const result = await stream.getFileContentCache("file.txt", data.repoId, () => ({ sha: "blob", size: 11 }))
.then(value => ({ value }), error => ({ error }));
expect(downloads).to.equal(1); expect(recoveryLookups).to.equal(1);
if (failed) { expect(result.error.message).to.equal("upstream_error"); expect(result.error.cause).to.equal(upstreamError); }
else {
const chunks = []; for await (const chunk of result.value) chunks.push(chunk);
expect(Buffer.concat(chunks).toString()).to.equal("fresh-bytes");
}
});
for (const stage of ["name", "id"]) for (const status of [undefined, 429, 503]) {
it(`propagates inconclusive ${stage} probe ${status}`, async () => {
const error = Object.assign(new Error("probe failed"), { status });
+742
View File
@@ -0,0 +1,742 @@
const { expect } = require("chai");
const mongoose = require("mongoose");
require("ts-node/register/transpile-only");
const Users = require("../src/core/model/users/users.model").default;
const config = require("../src/config").default;
const alerts = require("../src/core/owner-notifications");
const { notifyOwnerAccessProblem: notify, resetOwnerAccessAlerts: reset, withOwnerAccessRecovery: recover } = alerts;
const describeMongo = process.env.TEST_MONGODB_URI ? describe : describe.skip;
describeMongo("recipient access alerts in MongoDB", function () {
this.timeout(15000);
let owner, otherOwner, resource, other, sent, nativeFetch, originalConfig;
const coll = kind => mongoose.connection.collection({ repository: "anonymizedrepositories", gist: "anonymizedgists", "pull-request": "anonymizedpullrequests" }[kind]);
const guards = () => mongoose.connection.collection(alerts.RECIPIENT_ALERTS);
const recipient = () => alerts.recipientAlertKey("owner@example.com");
async function create(ownerId, kind = "repository") {
const id = new mongoose.Types.ObjectId(); await coll(kind).insertOne({ _id: id, owner: ownerId, repoId: String(id), status: "ready" }); return { kind, id: String(id) };
}
before(async () => {
nativeFetch = global.fetch; originalConfig = { ...config };
await mongoose.connect(process.env.TEST_MONGODB_URI, { dbName: "recipient_alerts_" + Date.now() });
Object.assign(config, { RESEND_API_KEY: "test-key", EMAIL_FROM: "alerts@example.com", APP_HOSTNAME: "anonymous.example.com" });
});
after(async () => { global.fetch = nativeFetch; Object.assign(config, originalConfig); await mongoose.connection.dropDatabase(); await mongoose.disconnect(); });
beforeEach(async () => {
for (const c of Object.values(mongoose.connection.collections)) await c.deleteMany({});
owner = await Users.create({ username: "owner", emails: [{ email: "owner@example.com", default: true }] });
otherOwner = await Users.create({ username: "other-owner", notificationEmail: "OWNER@example.com" });
resource = await create(owner._id); other = await create(otherOwner._id, "gist"); sent = [];
global.fetch = async (_url, options) => { sent.push(options); return { ok: true, status: 200 }; };
});
it("allows only one sender across owners, resources and case variations under concurrent failures", async () => {
await Promise.all(Array.from({ length: 40 }, (_, i) => notify(i % 2 ? owner.id : otherOwner.id, "token_expired", i % 2 ? resource : other)));
expect(sent).to.have.length(1); expect(await guards().countDocuments({ _id: recipient() })).to.equal(1);
expect(await mongoose.connection.collection("owner_access_alert_events").countDocuments({ state: "accepted" })).to.equal(1);
});
it("does not reopen after reconnect alone, unrelated resource success, or failed recovery", async () => {
await notify(owner.id, "token_expired", resource); await reset(owner.id);
await recover(otherOwner.id, other, async () => {});
try { await recover(owner.id, resource, async () => { throw new Error("Forbidden"); }); } catch { /* Expected. */ }
await notify(owner.id, "token_expired", resource); await notify(otherOwner.id, "token_expired", other);
expect(sent).to.have.length(1);
});
it("requires owner action before a successful upstream read can reopen the recipient", async () => {
await notify(owner.id, "token_expired", resource);
await recover(owner.id, resource, async () => {}); expect(await guards().findOne({ _id: recipient() })).not.to.equal(null);
await reset(owner.id, resource); await recover(owner.id, resource, async () => {});
expect(await guards().findOne({ _id: recipient() })).to.equal(null);
await notify(owner.id, "token_expired", resource); expect(sent).to.have.length(2);
expect(sent[0].headers["Idempotency-Key"]).not.to.equal(sent[1].headers["Idempotency-Key"]);
expect(await mongoose.connection.collection("owner_access_alert_events").countDocuments()).to.equal(4);
});
it("does not release a pending send when recovery races with provider delivery", async () => {
let release, entered; const gate = new Promise(resolve => { entered = resolve; });
global.fetch = async (_url, options) => { sent.push(options); entered(); await new Promise(resolve => { release = resolve; }); return { ok: true, status: 200 }; };
const pending = notify(owner.id, "token_expired", resource); await gate;
await reset(owner.id, resource); await recover(owner.id, resource, async () => {});
await notify(otherOwner.id, "token_expired", other); expect(sent).to.have.length(1);
release(); await pending;
});
it("rejects stale recovery after another owner action changes the generation", async () => {
await notify(owner.id, "token_expired", resource); await reset(owner.id, resource);
await recover(owner.id, resource, async () => { await reset(owner.id, resource); });
expect(await guards().findOne({ _id: recipient() })).not.to.equal(null);
});
it("cancels an unsent claim before a paused sender can start dispatch", async () => {
const collection = mongoose.connection.collection("owner_access_alert_events"), insert = collection.insertOne;
let entered, release; const gate = new Promise(resolve => { entered = resolve; });
collection.insertOne = async function (...args) { entered(); await new Promise(resolve => { release = resolve; }); return insert.apply(this, args); };
try {
const pending = notify(owner.id, "token_expired", resource); await gate;
await reset(owner.id, resource); await recover(owner.id, resource, async () => {});
release(); await pending; expect(sent).to.have.length(0);
expect(await guards().findOne({ _id: recipient() })).to.equal(null);
} finally { collection.insertOne = insert; }
});
it("fences a second owner action that races after recovery's final resource check", async () => {
await notify(owner.id, "token_expired", resource); await reset(owner.id, resource);
const collection = guards(), remove = collection.deleteOne;
let entered, release; const gate = new Promise(resolve => { entered = resolve; });
collection.deleteOne = async function (...args) { entered(); await new Promise(resolve => { release = resolve; }); return remove.apply(this, args); };
try {
const pending = recover(owner.id, resource, async () => {}); await gate;
await reset(owner.id, resource); release(); await pending;
expect(await guards().findOne({ _id: recipient() })).not.to.equal(null);
} finally { collection.deleteOne = remove; }
});
it("does not accept a successful read of a stale source binding", async () => {
await notify(owner.id, "token_expired", resource); await reset(owner.id, resource);
await coll(resource.kind).updateOne({ _id: new mongoose.Types.ObjectId(resource.id) }, { $set: { "source.repositoryName": "new/source" } });
await recover(owner.id, resource, async () => {}, { "source.repositoryName": "old/source" });
expect(await guards().findOne({ _id: recipient() })).not.to.equal(null);
});
for (const failure of ["rejection", "timeout"]) it(`retains the recipient guard after ${failure}`, async () => {
global.fetch = async (_url, options) => { sent.push(options); if (failure === "timeout") throw new Error("timeout"); return { ok: false, status: 429 }; };
await notify(owner.id, "token_expired", resource); await reset(owner.id);
await notify(otherOwner.id, "repo_not_found", other); expect(sent).to.have.length(1);
expect((await guards().findOne({ _id: recipient() })).state).to.equal("failed");
});
it("releases a guard for a removed resource without erasing delivery history", async () => {
await notify(owner.id, "token_expired", resource);
await coll(resource.kind).updateOne({ _id: new mongoose.Types.ObjectId(resource.id) }, { $set: { status: "removed" } });
await notify(otherOwner.id, "token_expired", other); expect(sent).to.have.length(2);
expect(await mongoose.connection.collection("owner_access_alert_events").countDocuments()).to.equal(4);
});
for (const step of ["resource update", "resource verification", "audit insertion", "dispatch transition"]) it(`releases an unsent guard after ${step} fails`, async () => {
const collection = step.startsWith("resource") ? coll(resource.kind) : step === "audit insertion" ? mongoose.connection.collection("owner_access_alert_events") : guards();
const method = step === "resource verification" ? "findOne" : step === "audit insertion" ? "insertOne" : "updateOne";
const original = collection[method]; let calls = 0;
collection[method] = async function (...args) {
calls++;
if ((step === "resource verification" && calls === 3)
|| (step === "dispatch transition" && args[1].$set?.state === "sending")
|| (step === "resource update" && args[1].$set?.accessAlertClaimedAt)
|| step === "audit insertion") throw new Error("transient database error");
return original.apply(this, args);
};
try { await notify(owner.id, "token_expired", resource); }
finally { collection[method] = original; }
expect(sent).to.have.length(0); expect(await guards().findOne({ _id: recipient() })).to.equal(null);
expect((await coll(resource.kind).findOne({ _id: new mongoose.Types.ObjectId(resource.id) })).accessAlertClaimedAt).to.equal(undefined);
await notify(owner.id, "token_expired", resource); expect(sent).to.have.length(1);
});
it("retains audit evidence while provider delivery is still in flight", async () => {
let release, entered; const gate = new Promise(resolve => { entered = resolve; });
global.fetch = async () => { entered(); await new Promise(resolve => { release = resolve; }); return { ok: true, status: 200 }; };
const pending = notify(owner.id, "token_expired", resource); await gate;
try {
const record = await guards().findOne({ _id: recipient() });
const event = await mongoose.connection.collection("owner_access_alert_events").findOne({ _id: record.attemptId });
expect(event.state).to.equal("prepared"); expect(record.state).to.equal("sending");
} finally { release(); await pending; }
});
it("reclaims a crashed pre-dispatch attempt without owner action under concurrent failures", async () => {
await guards().insertOne({ _id: recipient(), owner: owner._id, resourceKind: resource.kind,
resourceId: new mongoose.Types.ObjectId(resource.id), attemptId: "crashed-pending", state: "pending", claimedAt: new Date(0) });
await Promise.all(Array.from({ length: 40 }, (_, i) => notify(i % 2 ? owner.id : otherOwner.id, "token_expired", i % 2 ? resource : other)));
expect(sent).to.have.length(1);
expect((await guards().findOne({ _id: recipient() })).state).to.equal("accepted");
});
it("refreshes the resource fence when an old claim is reclaimed before guard insertion", async () => {
await guards().insertOne({ _id: recipient(), owner: owner._id, resourceKind: resource.kind,
resourceId: new mongoose.Types.ObjectId(resource.id), attemptId: "old-pending", state: "pending", claimedAt: new Date(0) });
const users = mongoose.connection.collection("users"), update = users.updateOne;
let entered, release; const gate = new Promise(resolve => { entered = resolve; });
users.updateOne = async function (filter, operation, ...args) {
if (Array.isArray(operation)) { entered(); await new Promise(resolve => { release = resolve; }); }
return update.call(this, filter, operation, ...args);
};
const pending = notify(owner.id, "token_expired", resource);
try { await gate; await alerts.initializeOwnerAccessAlerts(); }
finally { users.updateOne = update; release(); await pending; }
expect(sent).to.have.length(1);
expect((await guards().findOne({ _id: recipient() })).resourceRevision).to.equal(1);
expect((await coll(resource.kind).findOne({ _id: new mongoose.Types.ObjectId(resource.id) })).accessAlertClaimRevision).to.equal(2);
});
it("keeps recent pending claims and old sending claims blocked", async () => {
await guards().insertOne({ _id: recipient(), owner: owner._id, resourceKind: resource.kind,
resourceId: new mongoose.Types.ObjectId(resource.id), attemptId: "active-pending", state: "pending", claimedAt: new Date() });
await notify(otherOwner.id, "token_expired", other); expect(sent).to.have.length(0);
await guards().updateOne({ _id: recipient() }, { $set: { state: "sending", claimedAt: new Date(0) } });
await notify(otherOwner.id, "token_expired", other); expect(sent).to.have.length(0);
await alerts.initializeOwnerAccessAlerts();
expect((await guards().findOne({ _id: recipient() })).state).to.equal("sending");
});
it("reaps only expired unsent claims during startup and never reseeds them", async () => {
await guards().insertOne({ _id: "recipient-alerts-v1", state: "ready" });
await coll(resource.kind).updateOne({ _id: new mongoose.Types.ObjectId(resource.id) }, { $set: { accessAlertClaimedAt: new Date(0), accessAlertAttemptId: recipient() } });
for (const [key, state, date] of [[recipient(), "pending", new Date(0)], ["recent", "pending", new Date()],
["sending", "sending", new Date(0)], ["accepted", "accepted", new Date(0)], ["failed", "failed", new Date(0)]]) {
await guards().insertOne({ _id: key, owner: owner._id, resourceKind: resource.kind,
resourceId: new mongoose.Types.ObjectId(resource.id), attemptId: key, state, claimedAt: date });
}
await alerts.initializeOwnerAccessAlerts();
expect(await guards().findOne({ _id: recipient() })).to.equal(null);
for (const key of ["recent", "sending", "accepted", "failed"]) expect(await guards().findOne({ _id: key })).not.to.equal(null);
await alerts.initializeOwnerAccessAlerts();
expect(await guards().findOne({ _id: recipient() })).to.equal(null);
await notify(owner.id, "token_expired", resource); expect(sent).to.have.length(1);
});
it("fences a paused sender after another reader reclaims its expired pending claim", async () => {
const collection = mongoose.connection.collection("owner_access_alert_events"), insert = collection.insertOne;
let entered, release; const gate = new Promise(resolve => { entered = resolve; });
let pausedOnce = false;
collection.insertOne = async function (...args) {
if (!pausedOnce) { pausedOnce = true; entered(); await new Promise(resolve => { release = resolve; }); }
return insert.apply(this, args);
};
const paused = notify(owner.id, "token_expired", resource);
try {
await gate;
await guards().updateOne({ _id: recipient() }, { $set: { claimedAt: new Date(0) } });
await notify(otherOwner.id, "token_expired", other); expect(sent).to.have.length(1);
} finally { collection.insertOne = insert; release(); await paused; }
expect(sent).to.have.length(1);
expect((await guards().findOne({ _id: recipient() })).owner.toString()).to.equal(otherOwner.id);
});
it("cannot reclaim a pending attempt that starts dispatch before cancellation", async () => {
await guards().insertOne({ _id: recipient(), owner: owner._id, resourceKind: resource.kind,
resourceId: new mongoose.Types.ObjectId(resource.id), attemptId: "racing-dispatch", state: "pending", claimedAt: new Date(0) });
await coll(resource.kind).updateOne({ _id: new mongoose.Types.ObjectId(resource.id) }, { $set: { accessAlertClaimedAt: new Date(0), accessAlertAttemptId: "racing-dispatch" } });
const collection = guards(), update = collection.updateOne;
collection.updateOne = async function (filter, operation, ...args) {
if (operation.$set?.state === "cancelled") await update.call(this, { _id: recipient(), state: "pending", attemptId: "racing-dispatch" }, { $set: { state: "sending" } });
return update.call(this, filter, operation, ...args);
};
try { await notify(otherOwner.id, "token_expired", other); }
finally { collection.updateOne = update; }
expect(sent).to.have.length(0); expect((await guards().findOne({ _id: recipient() })).state).to.equal("sending");
expect((await coll(resource.kind).findOne({ _id: new mongoose.Types.ObjectId(resource.id) })).accessAlertAttemptId).to.equal("racing-dispatch");
});
for (const state of ["pending", "cancelled"]) it(`clears a crashed unsent ${state} attempt's legacy fields before migration`, async () => {
const attemptId = `crashed-${state}`;
await guards().insertOne({ _id: recipient(), owner: owner._id, resourceKind: resource.kind,
resourceId: new mongoose.Types.ObjectId(resource.id), generation: "old-generation", attemptId, state,
claimedAt: state === "pending" ? new Date(0) : new Date() });
await coll(resource.kind).updateOne({ _id: new mongoose.Types.ObjectId(resource.id) }, { $set: {
accessAlertGeneration: "old-generation", accessAlertClaimedAt: new Date(0), accessAlertAttemptId: attemptId,
} });
await alerts.initializeOwnerAccessAlerts();
expect(await guards().findOne({ _id: recipient() })).to.equal(null);
const row = await coll(resource.kind).findOne({ _id: new mongoose.Types.ObjectId(resource.id) });
expect(row.accessAlertClaimedAt).to.equal(undefined); expect(row.accessAlertAttemptId).to.equal(undefined);
expect(sent).to.have.length(0);
await notify(owner.id, "token_expired", resource); expect(sent).to.have.length(1);
});
for (const changed of ["generation", "attempt"]) it(`preserves newer legacy fields when the ${changed} no longer matches the cancelled attempt`, async () => {
await guards().insertOne({ _id: "recipient-alerts-v1", state: "ready" });
await guards().insertOne({ _id: recipient(), owner: owner._id, resourceKind: resource.kind,
resourceId: new mongoose.Types.ObjectId(resource.id), generation: "old-generation", attemptId: "old-attempt", state: "pending", claimedAt: new Date(0) });
const generation = changed === "generation" ? "new-generation" : "old-generation";
const attemptId = changed === "attempt" ? "new-attempt" : "old-attempt";
await coll(resource.kind).updateOne({ _id: new mongoose.Types.ObjectId(resource.id) }, { $set: {
accessAlertGeneration: generation, accessAlertClaimedAt: new Date(1234), accessAlertAttemptId: attemptId,
} });
await alerts.initializeOwnerAccessAlerts();
expect(await guards().findOne({ _id: recipient() })).to.equal(null);
const row = await coll(resource.kind).findOne({ _id: new mongoose.Types.ObjectId(resource.id) });
expect(row.accessAlertClaimedAt.getTime()).to.equal(1234); expect(row.accessAlertAttemptId).to.equal(attemptId);
});
it("resumes cancelled metadata cleanup after a database failure without creating false legacy evidence", async () => {
const collection = coll(resource.kind), update = collection.updateOne, read = collection.findOne;
let reads = 0;
collection.findOne = async function (...args) { if (++reads === 3) throw new Error("verification failed"); return read.apply(this, args); };
collection.updateOne = async function (filter, operation, ...args) {
if (Array.isArray(operation)) throw new Error("cleanup unavailable");
return update.call(this, filter, operation, ...args);
};
try { await notify(owner.id, "token_expired", resource); }
finally { collection.findOne = read; collection.updateOne = update; }
expect(sent).to.have.length(0); expect((await guards().findOne({ _id: recipient() })).state).to.equal("cancelled");
await alerts.initializeOwnerAccessAlerts();
expect(await guards().findOne({ _id: recipient() })).to.equal(null);
expect((await collection.findOne({ _id: new mongoose.Types.ObjectId(resource.id) })).accessAlertClaimedAt).to.equal(undefined);
await notify(owner.id, "token_expired", resource); expect(sent).to.have.length(1);
});
it("cleans recent pending legacy fields once deployment has stopped every writer", async () => {
await guards().insertOne({ _id: "recipient-alerts-v1", state: "ready" });
await guards().insertOne({ _id: recipient(), owner: owner._id, resourceKind: resource.kind,
resourceId: new mongoose.Types.ObjectId(resource.id), attemptId: "recent-unsent", state: "pending", claimedAt: new Date() });
await coll(resource.kind).updateOne({ _id: new mongoose.Types.ObjectId(resource.id) }, { $set: {
accessAlertClaimedAt: new Date(), accessAlertAttemptId: "recent-unsent",
} });
await alerts.prepareLegacyOwnerAlertMigration(); await alerts.initializeOwnerAccessAlerts();
expect(await guards().findOne({ _id: recipient() })).to.equal(null);
expect((await coll(resource.kind).findOne({ _id: new mongoose.Types.ObjectId(resource.id) })).accessAlertClaimedAt).to.equal(undefined);
});
it("preserves an untagged legacy delivery written during rollback while cancelling a native pending attempt", async () => {
await guards().insertOne({ _id: "recipient-alerts-v1", state: "ready" });
await guards().insertOne({ _id: recipient(), owner: owner._id, resourceKind: resource.kind,
resourceId: new mongoose.Types.ObjectId(resource.id), attemptId: "native-unsent", state: "pending", claimedAt: new Date() });
await coll(resource.kind).updateOne({ _id: new mongoose.Types.ObjectId(resource.id) }, { $set: { accessAlertClaimedAt: new Date() } });
await alerts.prepareLegacyOwnerAlertMigration(); await alerts.initializeOwnerAccessAlerts();
expect((await guards().findOne({ _id: recipient() })).state).to.equal("legacy");
await notify(owner.id, "token_expired", resource); expect(sent).to.have.length(0);
await reset(owner.id, resource); await recover(owner.id, resource, async () => {});
await notify(owner.id, "token_expired", resource); expect(sent).to.have.length(1);
});
it("blocks dispatch while an owner reset has advanced the guard revision but not the resource generation", async () => {
const events = mongoose.connection.collection("owner_access_alert_events"), insert = events.insertOne;
const collection = coll(resource.kind), updateMany = collection.updateMany;
let prepared, releasePrepared, resetting, releaseReset;
const atPrepared = new Promise(resolve => { prepared = resolve; });
const atReset = new Promise(resolve => { resetting = resolve; });
events.insertOne = async function (...args) { prepared(); await new Promise(resolve => { releasePrepared = resolve; }); return insert.apply(this, args); };
collection.updateMany = async function (...args) { resetting(); await new Promise(resolve => { releaseReset = resolve; }); return updateMany.apply(this, args); };
const pending = notify(owner.id, "token_expired", resource);
let action;
try {
await atPrepared; action = reset(owner.id, resource); await atReset;
expect((await guards().findOne({ _id: recipient() })).resetRevision).to.equal(1);
releasePrepared(); await pending;
expect(sent).to.have.length(0);
const row = await collection.findOne({ _id: new mongoose.Types.ObjectId(resource.id) });
expect(row.accessAlertGeneration).to.equal(undefined); expect(row.accessAlertClaimedAt).to.equal(undefined);
} finally {
events.insertOne = insert; collection.updateMany = updateMany;
if (releasePrepared) releasePrepared(); if (releaseReset) releaseReset();
await pending; if (action) await action;
}
await notify(owner.id, "token_expired", resource); expect(sent).to.have.length(1);
});
it("invalidates an old failure when reset starts before its recipient guard exists", async () => {
const lookup = Users.findOne, collection = coll(resource.kind), updateMany = collection.updateMany;
let entered, release, resetting, releaseReset;
const atLookup = new Promise(resolve => { entered = resolve; });
const atReset = new Promise(resolve => { resetting = resolve; });
Users.findOne = function (...args) { return { lean: async () => {
entered(); await new Promise(resolve => { release = resolve; }); return lookup.apply(Users, args).lean();
} }; };
collection.updateMany = async function (...args) { resetting(); await new Promise(resolve => { releaseReset = resolve; }); return updateMany.apply(this, args); };
const pending = notify(owner.id, "token_expired", resource); let action;
try {
await atLookup; action = reset(owner.id, resource); await atReset;
expect(await guards().findOne({ _id: recipient() })).to.equal(null);
release(); await pending; expect(sent).to.have.length(0);
expect((await collection.findOne({ _id: new mongoose.Types.ObjectId(resource.id) })).accessAlertClaimedAt).to.equal(undefined);
} finally {
Users.findOne = lookup; collection.updateMany = updateMany;
if (release) release(); if (releaseReset) releaseReset(); await pending; if (action) await action;
}
await notify(owner.id, "token_expired", resource); expect(sent).to.have.length(1);
});
it("prevents a cancelled sender from recreating legacy fields after its database write resumes", async () => {
const collection = coll(resource.kind), update = collection.updateOne;
let entered, release; const gate = new Promise(resolve => { entered = resolve; });
collection.updateOne = async function (filter, operation, ...args) {
if (operation.$set?.accessAlertClaimedAt) { entered(); await new Promise(resolve => { release = resolve; }); }
return update.call(this, filter, operation, ...args);
};
const pending = notify(owner.id, "token_expired", resource);
try {
await gate; await guards().updateOne({ _id: recipient() }, { $set: { claimedAt: new Date(0) } });
await notify(otherOwner.id, "token_expired", other); expect(sent).to.have.length(1);
} finally { collection.updateOne = update; release(); await pending; }
const row = await collection.findOne({ _id: new mongoose.Types.ObjectId(resource.id) });
expect(row.accessAlertClaimedAt).to.equal(undefined); expect(row.accessAlertClaimRevision).to.equal(1);
expect(sent).to.have.length(1);
});
it("reaps a crashed pre-provider attempt after legacy preparation completed", async () => {
await guards().insertOne({ _id: recipient(), owner: owner._id, resourceKind: resource.kind,
resourceId: new mongoose.Types.ObjectId(resource.id), attemptId: "prepared-not-sent", state: "pending", resourceRevision: 0, claimedAt: new Date(0) });
await coll(resource.kind).updateOne({ _id: new mongoose.Types.ObjectId(resource.id) }, { $set: {
accessAlertClaimedAt: new Date(0), accessAlertAttemptId: "prepared-not-sent", accessAlertClaimRevision: 1,
} });
await alerts.initializeOwnerAccessAlerts();
expect(await guards().findOne({ _id: recipient() })).to.equal(null); expect(sent).to.have.length(0);
const row = await coll(resource.kind).findOne({ _id: new mongoose.Types.ObjectId(resource.id) });
expect(row.accessAlertClaimedAt).to.equal(undefined); expect(row.accessAlertClaimRevision).to.equal(2);
await notify(owner.id, "token_expired", resource); expect(sent).to.have.length(1);
});
for (const accepted of [true, false]) it(`applies recovery recorded during an in-flight ${accepted ? "accepted" : "rejected"} delivery`, async () => {
let entered, release; const gate = new Promise(resolve => { entered = resolve; });
global.fetch = async (_url, options) => {
sent.push(options); if (sent.length === 1) { entered(); await new Promise(resolve => { release = resolve; }); }
return { ok: accepted, status: accepted ? 200 : 403 };
};
const pending = notify(owner.id, "token_expired", resource);
try {
await gate; await reset(owner.id, resource); await recover(owner.id, resource, async () => {});
const record = await guards().findOne({ _id: recipient() }); expect(record.state).to.equal("sending");
expect(record.deferredRecoveries).to.have.length(1);
await notify(otherOwner.id, "token_expired", other); expect(sent).to.have.length(1);
} finally { release(); await pending; }
expect(await guards().findOne({ _id: recipient() })).to.equal(null);
await notify(otherOwner.id, "token_expired", other); expect(sent).to.have.length(2);
});
it("invalidates in-flight recovery evidence after a newer reset", async () => {
let entered, release; const gate = new Promise(resolve => { entered = resolve; });
global.fetch = async (_url, options) => {
sent.push(options); if (sent.length === 1) { entered(); await new Promise(resolve => { release = resolve; }); }
return { ok: true, status: 200 };
};
const pending = notify(owner.id, "token_expired", resource);
try {
await gate; await reset(owner.id, resource); await recover(owner.id, resource, async () => {}); await reset(owner.id, resource);
} finally { release(); await pending; }
expect((await guards().findOne({ _id: recipient() })).state).to.equal("accepted");
await notify(otherOwner.id, "token_expired", other); expect(sent).to.have.length(1);
await recover(owner.id, resource, async () => {});
await notify(otherOwner.id, "token_expired", other); expect(sent).to.have.length(2);
});
it("invalidates deferred recovery if its source binding changes before delivery completes", async () => {
await coll(resource.kind).updateOne({ _id: new mongoose.Types.ObjectId(resource.id) }, { $set: { source: { repositoryName: "owner/repo" } } });
let entered, release; const gate = new Promise(resolve => { entered = resolve; });
global.fetch = async (_url, options) => {
sent.push(options); if (sent.length === 1) { entered(); await new Promise(resolve => { release = resolve; }); }
return { ok: true, status: 200 };
};
const pending = notify(owner.id, "token_expired", resource);
try {
await gate; await reset(owner.id, resource);
await recover(owner.id, resource, async () => {}, { "source.repositoryName": "owner/repo" });
await coll(resource.kind).updateOne({ _id: new mongoose.Types.ObjectId(resource.id) }, { $set: { "source.repositoryName": "new/repo" } });
} finally { release(); await pending; }
expect((await guards().findOne({ _id: recipient() })).state).to.equal("accepted");
await recover(owner.id, resource, async () => {}, { "source.repositoryName": "new/repo" });
await notify(otherOwner.id, "token_expired", other); expect(sent).to.have.length(2);
});
it("rejects dispatch when reset starts after the guard reservation but before the owner commitment", async () => {
const collection = guards(), update = collection.updateOne, updateMany = collection.updateMany;
let entered, release, resetting, releaseReset;
const gate = new Promise(resolve => { entered = resolve; }), resetGate = new Promise(resolve => { resetting = resolve; });
collection.updateOne = async function (filter, operation, ...args) {
const result = await update.call(this, filter, operation, ...args);
if (operation.$set?.state === "sending") { entered(); await new Promise(resolve => { release = resolve; }); }
return result;
};
collection.updateMany = async function (...args) { resetting(); await new Promise(resolve => { releaseReset = resolve; }); return updateMany.apply(this, args); };
const pending = notify(owner.id, "token_expired", resource); let action;
try {
await gate; action = reset(owner.id, resource); await resetGate;
expect((await guards().findOne({ _id: recipient() })).state).to.equal("sending");
release(); await pending; expect(sent).to.have.length(0);
expect(await guards().findOne({ _id: recipient() })).to.equal(null);
} finally {
collection.updateOne = update; collection.updateMany = updateMany;
if (release) release(); if (releaseReset) releaseReset(); await pending; if (action) await action;
}
await notify(owner.id, "token_expired", resource); expect(sent).to.have.length(1);
});
it("applies fresh recovery recorded before the reset lease is cleared without another upstream read", async () => {
await notify(owner.id, "token_expired", resource);
const users = mongoose.connection.collection("users"), update = users.updateOne;
let entered, release; const gate = new Promise(resolve => { entered = resolve; });
users.updateOne = async function (filter, operation, ...args) {
if (operation.$unset?.accessAlertResetUntil !== undefined) { entered(); await new Promise(resolve => { release = resolve; }); }
return update.call(this, filter, operation, ...args);
};
const action = reset(owner.id, resource);
try {
await gate; await recover(owner.id, resource, async () => {});
expect((await guards().findOne({ _id: recipient() })).deferredRecoveries).to.have.length(1);
} finally { users.updateOne = update; release(); await action; }
expect(await guards().findOne({ _id: recipient() })).to.equal(null);
await notify(otherOwner.id, "token_expired", other); expect(sent).to.have.length(2);
});
it("retries saved recovery after a crashed reset lease expires", async () => {
await notify(owner.id, "token_expired", resource); await reset(owner.id, resource);
await mongoose.connection.collection("users").updateOne({ _id: owner._id }, { $set: { accessAlertResetUntil: new Date(Date.now() + 150) } });
await recover(owner.id, resource, async () => {});
expect(await guards().findOne({ _id: recipient() })).not.to.equal(null);
for (let i = 0; i < 100 && await guards().findOne({ _id: recipient() }); i++) await new Promise(resolve => global.setTimeout(resolve, 20));
expect(await guards().findOne({ _id: recipient() })).to.equal(null);
await notify(otherOwner.id, "token_expired", other); expect(sent).to.have.length(2);
});
for (const state of ["prepared", "cancelled", "committed"]) it(`reconciles interrupted guard reservation with a ${state} owner dispatch record`, async () => {
const attemptId = `interrupted-${state}`;
await guards().insertMany([{ _id: "recipient-alerts-v1", state: "ready" }, {
_id: recipient(), owner: owner._id, resourceKind: resource.kind, resourceId: new mongoose.Types.ObjectId(resource.id),
attemptId, state: "sending", dispatchPrepared: true, resourceRevision: 0, claimedAt: new Date(0),
}]);
await mongoose.connection.collection("users").updateOne({ _id: owner._id }, { $set: {
[`accessAlertDispatches.${attemptId}`]: { state, claimedAt: new Date(0) },
} });
await coll(resource.kind).updateOne({ _id: new mongoose.Types.ObjectId(resource.id) }, { $set: {
accessAlertClaimedAt: new Date(0), accessAlertAttemptId: attemptId, accessAlertClaimRevision: 1,
} });
await alerts.initializeOwnerAccessAlerts();
const record = await guards().findOne({ _id: recipient() });
const account = await mongoose.connection.collection("users").findOne({ _id: owner._id });
if (state === "committed") {
expect(record.state).to.equal("sending"); expect(account.accessAlertDispatches[attemptId].state).to.equal(state);
} else {
expect(record).to.equal(null); expect(account.accessAlertDispatches[attemptId]).to.equal(undefined);
expect((await coll(resource.kind).findOne({ _id: new mongoose.Types.ObjectId(resource.id) })).accessAlertClaimedAt).to.equal(undefined);
}
await notify(otherOwner.id, "token_expired", other); expect(sent).to.have.length(state === "committed" ? 0 : 1);
});
it("prunes expired uncommitted owner dispatch records and cleans completed attempts", async () => {
await mongoose.connection.collection("users").updateOne({ _id: owner._id }, { $set: { accessAlertDispatches: {
abandoned: { state: "prepared", claimedAt: new Date(0) }, cancelled: { state: "cancelled", claimedAt: new Date(0) },
unknown: { state: "committed", claimedAt: new Date(0) },
} } });
await notify(owner.id, "token_expired", resource); expect(sent).to.have.length(1);
const account = await mongoose.connection.collection("users").findOne({ _id: owner._id });
expect(Object.keys(account.accessAlertDispatches)).to.deep.equal(["unknown"]);
});
it("allows active accounts to receive alerts when a shared-address guard belongs to a banned owner", async () => {
await notify(owner.id, "token_expired", resource);
await Users.updateOne({ _id: owner._id }, { $set: { status: "banned" } });
await notify(otherOwner.id, "token_expired", other); expect(sent).to.have.length(2);
});
it("resumes an indexed migration from its last completed checkpoint", async () => {
await Users.updateOne({ _id: otherOwner._id }, { $set: { notificationEmail: "second@example.com" } });
const second = await create(otherOwner._id);
await coll("repository").updateOne({ _id: new mongoose.Types.ObjectId(resource.id) }, { $set: { accessAlertClaimedAt: new Date(1) } });
await coll("repository").updateOne({ _id: new mongoose.Types.ObjectId(second.id) }, { $set: { accessAlertClaimedAt: new Date(2) } });
await guards().insertOne({ _id: "recipient-alerts-v1", state: "new", leaseUntil: new Date(0),
progress: { repository: { claimedAt: new Date(1), id: new mongoose.Types.ObjectId(resource.id) } } });
await alerts.initializeOwnerAccessAlerts();
expect(await guards().findOne({ _id: recipient() })).to.equal(null);
expect(await guards().findOne({ _id: alerts.recipientAlertKey("second@example.com") })).not.to.equal(null);
expect((await guards().findOne({ _id: "recipient-alerts-v1" })).progress.repository.claimedAt.getTime()).to.equal(2);
expect((await coll("repository").indexes()).some(i => i.name === "owner_access_alert_backfill" && i.partialFilterExpression)).to.equal(true);
});
it("confirms recovery on a fresh uncached file read but never on a cache hit", async () => {
const database = require("../src/server/database");
const GitHubStream = require("../src/core/source/GitHubStream").default;
const storage = require("../src/core/storage").default;
const { Readable } = require("stream");
const originals = { isConnected: database.isConnected, fileInfo: storage.fileInfo, write: storage.write, read: storage.read };
await coll("repository").updateOne({ _id: new mongoose.Types.ObjectId(resource.id) }, { $set: { repoId: resource.id, source: { repositoryName: "owner/repo", commit: "pinned" } } });
let cached = false;
database.isConnected = true;
storage.fileInfo = async () => { if (!cached) throw Object.assign(new Error("miss"), { code: "ENOENT" }); return { size: 11, contentType: "text/plain" }; };
storage.write = async (_repo, _path, input) => { for await (const _chunk of input) { /* Consume the upstream stream. */ } cached = true; };
storage.read = async () => Readable.from(["fresh-bytes"]);
try {
await notify(owner.id, "token_expired", resource); await reset(owner.id, resource);
const source = new GitHubStream({ repoId: resource.id, organization: "owner", repoName: "repo", commit: "pinned", getToken: async () => "mock-token" });
source.downloadWithFallback = async () => Readable.from(["fresh-bytes"]);
const read = () => source.getFileContentCache("a.txt", resource.id, () => ({ sha: "a".repeat(40), size: 11 }));
(await read()).destroy(); expect(await guards().findOne({ _id: recipient() })).to.equal(null);
await notify(owner.id, "token_expired", resource); await reset(owner.id, resource);
(await read()).destroy(); expect(await guards().findOne({ _id: recipient() })).not.to.equal(null);
} finally { database.isConnected = originals.isConnected; storage.fileInfo = originals.fileInfo; storage.write = originals.write; storage.read = originals.read; }
});
it("confirms recovery when a successful scheduled refresh finds the same commit", async () => {
const Repository = require("../src/core/Repository").default;
const Model = require("../src/core/model/anonymizedRepositories/anonymizedRepositories.model").default;
const github = require("../src/core/source/GitHubRepository");
const original = github.getRepositoryFromGitHub;
const source = { repositoryName: "owner/repo", commit: "pinned", branch: "main" };
await coll("repository").updateOne({ _id: new mongoose.Types.ObjectId(resource.id) }, { $set: { source } });
const repo = new Repository(new Model({ _id: resource.id, owner: owner._id, repoId: resource.id, status: "ready",
source, lastView: new Date(0), options: { update: true, expirationMode: "never" } }));
repo.getToken = async () => "mock-token";
github.getRepositoryFromGitHub = async () => ({ fullName: "owner/repo", model: { defaultBranch: "main" },
branches: async () => [{ name: "main", commit: "pinned" }], getCommitInfo: async () => { throw new Error("Unexpected commit fetch"); } });
try {
await notify(owner.id, "token_expired", resource); await reset(owner.id, resource);
await repo.updateIfNeeded({ force: false });
expect(await guards().findOne({ _id: recipient() })).to.equal(null);
} finally { github.getRepositoryFromGitHub = original; }
});
it("seeds old claims once while five instances start concurrently", async () => {
await coll(resource.kind).updateOne({ _id: new mongoose.Types.ObjectId(resource.id) }, { $set: { accessAlertClaimedAt: new Date() } });
await Promise.all(Array.from({ length: 5 }, () => alerts.initializeOwnerAccessAlerts()));
expect((await guards().findOne({ _id: recipient() })).state).to.equal("legacy");
await notify(otherOwner.id, "repo_not_found", other); expect(sent).to.have.length(0);
await reset(owner.id, resource); await recover(owner.id, resource, async () => {});
await alerts.initializeOwnerAccessAlerts(); expect((await guards().findOne({ _id: recipient() })).state).to.equal("recovered");
});
it("preserves every legacy claim for a shared address until each triggering resource recovers", async () => {
await coll(resource.kind).updateOne({ _id: new mongoose.Types.ObjectId(resource.id) }, { $set: { accessAlertClaimedAt: new Date(1) } });
await coll(other.kind).updateOne({ _id: new mongoose.Types.ObjectId(other.id) }, { $set: { accessAlertClaimedAt: new Date(2) } });
await alerts.initializeOwnerAccessAlerts();
expect((await guards().findOne({ _id: recipient() })).legacyClaims).to.have.length(2);
await reset(owner.id, resource); await recover(owner.id, resource, async () => {});
expect((await guards().findOne({ _id: recipient() })).legacyClaims.filter(claim => !claim.recovered)).to.have.length(1);
await notify(otherOwner.id, "token_expired", other); expect(sent).to.have.length(0);
await reset(otherOwner.id, other); await recover(otherOwner.id, other, async () => {});
expect((await guards().findOne({ _id: recipient() })).state).to.equal("recovered");
await notify(otherOwner.id, "token_expired", other); expect(sent).to.have.length(1);
});
it("does not retire a legacy group when only its first triggering resource is removed", async () => {
await coll(resource.kind).updateOne({ _id: new mongoose.Types.ObjectId(resource.id) }, { $set: { accessAlertClaimedAt: new Date(1) } });
await coll(other.kind).updateOne({ _id: new mongoose.Types.ObjectId(other.id) }, { $set: { accessAlertClaimedAt: new Date(2) } });
await alerts.initializeOwnerAccessAlerts();
await coll(resource.kind).updateOne({ _id: new mongoose.Types.ObjectId(resource.id) }, { $set: { status: "removed" } });
await notify(otherOwner.id, "token_expired", other); expect(sent).to.have.length(0);
await reset(otherOwner.id, other); await recover(otherOwner.id, other, async () => {});
await notify(otherOwner.id, "token_expired", other); expect(sent).to.have.length(1);
});
it("atomically releases the final legacy guard during concurrent recoveries", async () => {
await coll(resource.kind).updateOne({ _id: new mongoose.Types.ObjectId(resource.id) }, { $set: { accessAlertClaimedAt: new Date(1) } });
await coll(other.kind).updateOne({ _id: new mongoose.Types.ObjectId(other.id) }, { $set: { accessAlertClaimedAt: new Date(2) } });
await alerts.initializeOwnerAccessAlerts(); await reset(owner.id, resource); await reset(otherOwner.id, other);
await Promise.all([recover(owner.id, resource, async () => {}), recover(otherOwner.id, other, async () => {})]);
expect((await guards().findOne({ _id: recipient() })).state).to.equal("recovered");
});
it("requires a fresh read after a newer reset of a partially recovered legacy group", async () => {
for (const item of [resource, other]) await coll(item.kind).updateOne({ _id: new mongoose.Types.ObjectId(item.id) }, { $set: { accessAlertClaimedAt: new Date(1) } });
await alerts.initializeOwnerAccessAlerts();
await reset(owner.id, resource); await recover(owner.id, resource, async () => {});
await reset(owner.id, resource);
await reset(otherOwner.id, other); await recover(otherOwner.id, other, async () => {});
await notify(otherOwner.id, "token_expired", other); expect(sent).to.have.length(0);
expect((await guards().findOne({ _id: recipient() })).state).to.equal("legacy");
await recover(owner.id, resource, async () => {});
await notify(otherOwner.id, "token_expired", other); expect(sent).to.have.length(1);
});
it("fences legacy claim mutation when reset races after the final resource check", async () => {
await coll(resource.kind).updateOne({ _id: new mongoose.Types.ObjectId(resource.id) }, { $set: { accessAlertClaimedAt: new Date(1) } });
await alerts.initializeOwnerAccessAlerts(); await reset(owner.id, resource);
const collection = guards(), update = collection.updateOne;
let entered, release; const gate = new Promise(resolve => { entered = resolve; });
collection.updateOne = async function (filter, operation, ...args) {
if (filter.attemptId && Array.isArray(operation)) { entered(); await new Promise(resolve => { release = resolve; }); }
return update.call(this, filter, operation, ...args);
};
const pending = recover(owner.id, resource, async () => {});
try { await gate; await reset(owner.id, resource); }
finally { collection.updateOne = update; release(); await pending; }
const record = await guards().findOne({ _id: recipient() });
expect(record.state).to.equal("legacy"); expect(record.legacyClaims[0].recovered).to.equal(false);
});
it("reconciles claims written by legacy instances after rollback without reseeding ordinary restarts", async () => {
await coll(resource.kind).updateOne({ _id: new mongoose.Types.ObjectId(resource.id) }, { $set: { accessAlertClaimedAt: new Date(1) } });
await alerts.initializeOwnerAccessAlerts(); await reset(owner.id, resource); await recover(owner.id, resource, async () => {});
await alerts.initializeOwnerAccessAlerts(); expect((await guards().findOne({ _id: recipient() })).state).to.equal("recovered");
await coll(other.kind).updateOne({ _id: new mongoose.Types.ObjectId(other.id) }, { $set: { accessAlertClaimedAt: new Date() } });
await alerts.prepareLegacyOwnerAlertMigration();
const marker = await guards().findOne({ _id: "recipient-alerts-v1" }); expect(marker.state).to.equal("new"); expect(marker.progress).to.equal(undefined);
await alerts.initializeOwnerAccessAlerts(); await notify(otherOwner.id, "token_expired", other); expect(sent).to.have.length(0);
expect((await guards().findOne({ _id: recipient() })).legacyClaims.filter(claim => !claim.recovered)).to.have.length(1);
await reset(otherOwner.id, other); await recover(otherOwner.id, other, async () => {});
await notify(otherOwner.id, "token_expired", other); expect(sent).to.have.length(1);
});
it("preserves a native guard's origin when rollback reconciliation adds another legacy claim", async () => {
await alerts.initializeOwnerAccessAlerts(); await notify(owner.id, "token_expired", resource); await reset(owner.id, resource);
await coll(other.kind).updateOne({ _id: new mongoose.Types.ObjectId(other.id) }, { $set: { accessAlertClaimedAt: new Date() } });
await alerts.prepareLegacyOwnerAlertMigration(); await alerts.initializeOwnerAccessAlerts();
expect((await guards().findOne({ _id: recipient() })).legacyClaims.filter(claim => !claim.recovered)).to.have.length(2);
await reset(otherOwner.id, other); await recover(otherOwner.id, other, async () => {});
await notify(otherOwner.id, "token_expired", other); expect(sent).to.have.length(1);
await recover(owner.id, resource, async () => {});
await notify(otherOwner.id, "token_expired", other); expect(sent).to.have.length(2);
});
it("fences completed-guard retirement against a concurrent reactivation and reset", async () => {
await notify(owner.id, "token_expired", resource);
await coll(resource.kind).updateOne({ _id: new mongoose.Types.ObjectId(resource.id) }, { $set: { status: "removed" } });
const collection = guards(), remove = collection.deleteOne;
let entered, release; const gate = new Promise(resolve => { entered = resolve; });
collection.deleteOne = async function (...args) { entered(); await new Promise(resolve => { release = resolve; }); return remove.apply(this, args); };
const pending = notify(otherOwner.id, "token_expired", other);
try {
await gate;
await coll(resource.kind).updateOne({ _id: new mongoose.Types.ObjectId(resource.id) }, { $set: { status: "ready" } });
await reset(owner.id, resource);
} finally { collection.deleteOne = remove; release(); await pending; }
expect(sent).to.have.length(1); expect(await guards().findOne({ _id: recipient() })).not.to.equal(null);
await recover(owner.id, resource, async () => {}); await notify(otherOwner.id, "token_expired", other); expect(sent).to.have.length(2);
});
for (const accepted of [true, false]) it(`retries the guard update after a definite ${accepted ? "accepted" : "rejected"} provider response`, async () => {
const collection = guards(), update = collection.updateOne; let writes = 0;
global.fetch = async (_url, options) => { sent.push(options); return { ok: accepted, status: accepted ? 200 : 403 }; };
collection.updateOne = async function (filter, operation, ...args) {
if (["accepted", "failed"].includes(operation.$set?.state)) {
expect(await mongoose.connection.collection("owner_access_alert_events").findOne({ _id: `${filter.attemptId}/completion` })).not.to.equal(null);
if (++writes === 1) throw new Error("transient result write failure");
}
return update.call(this, filter, operation, ...args);
};
try { await notify(owner.id, "token_expired", resource); }
finally { collection.updateOne = update; }
expect(writes).to.equal(2); expect(sent).to.have.length(1);
expect((await guards().findOne({ _id: recipient() })).state).to.equal(accepted ? "accepted" : "failed");
});
for (const startup of [true, false]) it(`reconciles a durable provider result during ${startup ? "startup" : "the next notification"}`, async () => {
await alerts.initializeOwnerAccessAlerts();
const collection = guards(), update = collection.updateOne; let writes = 0;
collection.updateOne = async function (filter, operation, ...args) {
if (["accepted", "failed"].includes(operation.$set?.state)) { writes++; throw new Error("database unavailable"); }
return update.call(this, filter, operation, ...args);
};
try { await notify(owner.id, "token_expired", resource); }
finally { collection.updateOne = update; }
expect(writes).to.equal(3); expect((await guards().findOne({ _id: recipient() })).state).to.equal("sending");
if (startup) await alerts.initializeOwnerAccessAlerts(); else await notify(otherOwner.id, "token_expired", other);
expect((await guards().findOne({ _id: recipient() })).state).to.equal("accepted"); expect(sent).to.have.length(1);
await reset(owner.id, resource); await recover(owner.id, resource, async () => {});
await notify(otherOwner.id, "token_expired", other); expect(sent).to.have.length(2);
});
it("records a provider completion once after its insertion acknowledgement is lost", async () => {
const collection = mongoose.connection.collection("owner_access_alert_events"), insert = collection.insertOne;
let lost = false;
collection.insertOne = async function (record, ...args) {
const result = await insert.call(this, record, ...args);
if (record.state === "accepted" && !lost) { lost = true; throw new Error("lost acknowledgement"); }
return result;
};
try { await notify(owner.id, "token_expired", resource); }
finally { collection.insertOne = insert; }
expect(lost).to.equal(true); expect(sent).to.have.length(1);
expect(await collection.countDocuments({ state: "accepted" })).to.equal(1);
expect((await guards().findOne({ _id: recipient() })).state).to.equal("accepted");
});
it("does not let a late provider-result write resurrect a recovered guard", async () => {
const collection = guards(), update = collection.updateOne;
let entered, release, paused = false; const gate = new Promise(resolve => { entered = resolve; });
collection.updateOne = async function (filter, operation, ...args) {
if (operation.$set?.state === "accepted" && !paused) { paused = true; entered(); await new Promise(resolve => { release = resolve; }); }
return update.call(this, filter, operation, ...args);
};
const pending = notify(owner.id, "token_expired", resource);
try {
await gate; await notify(otherOwner.id, "token_expired", other);
await reset(owner.id, resource); await recover(owner.id, resource, async () => {});
} finally { collection.updateOne = update; release(); await pending; }
expect(sent).to.have.length(1); expect(await guards().findOne({ _id: recipient() })).to.equal(null);
});
it("confirms truncated-file metadata recovery only on a successful fresh matching-source probe", async () => {
const database = require("../src/server/database");
const GitHubStream = require("../src/core/source/GitHubStream").default;
const utils = require("../src/core/GitHubUtils");
const originals = { isConnected: database.isConnected, octokit: utils.octokit };
await coll(resource.kind).updateOne({ _id: new mongoose.Types.ObjectId(resource.id) }, { $set: { repoId: resource.id, source: { repositoryName: "owner/repo", commit: "pinned" } } });
let response = { type: "file", sha: "a".repeat(40), size: 11 };
database.isConnected = true;
utils.octokit = () => ({ repos: { getContent: async () => { if (response instanceof Error) throw response; return { data: response }; } } });
const source = new GitHubStream({ repoId: resource.id, organization: "owner", repoName: "repo", commit: "pinned", getToken: async () => "mock-token" });
try {
await notify(owner.id, "token_expired", resource); await reset(owner.id, resource);
expect((await source.fetchFileInfoFromPath("a.txt")).sha).to.equal("a".repeat(40));
expect(await guards().findOne({ _id: recipient() })).to.equal(null);
await notify(owner.id, "token_expired", resource); await reset(owner.id, resource);
response = { type: "dir" }; expect(await source.fetchFileInfoFromPath("a.txt")).to.equal(null);
expect(await guards().findOne({ _id: recipient() })).not.to.equal(null);
response = Object.assign(new Error("Forbidden"), { status: 403 });
try { await source.fetchFileInfoFromPath("a.txt"); throw new Error("Expected a failed probe"); }
catch (error) { expect(error.message).to.equal("Forbidden"); }
expect(await guards().findOne({ _id: recipient() })).not.to.equal(null);
response = { type: "file", sha: "a".repeat(40), size: 11 };
await coll(resource.kind).updateOne({ _id: new mongoose.Types.ObjectId(resource.id) }, { $set: { "source.repositoryName": "different/source" } });
await source.fetchFileInfoFromPath("a.txt");
expect(await guards().findOne({ _id: recipient() })).not.to.equal(null);
} finally { database.isConnected = originals.isConnected; utils.octokit = originals.octokit; }
});
it("resumes a migration after a startup crashed and its lease expired", async () => {
await guards().insertOne({ _id: "recipient-alerts-v1", state: "new", lease: "crashed", leaseUntil: new Date(0) });
await alerts.initializeOwnerAccessAlerts(); expect((await guards().findOne({ _id: "recipient-alerts-v1" })).state).to.equal("ready");
});
it("continues waiting beyond 300 polls while another migration holds a healthy lease", async () => {
await guards().insertOne({ _id: "recipient-alerts-v1", state: "new", lease: "active-migration", leaseUntil: new Date(Date.now() + 600000) });
const collection = guards(), update = collection.updateOne, timeout = global.setTimeout;
let polls = 0;
collection.updateOne = async function (filter, ...args) {
if (filter._id === "recipient-alerts-v1" && filter.$or && ++polls === 305) {
await update.call(this, { _id: "recipient-alerts-v1" }, { $set: { state: "ready" } });
}
return update.call(this, filter, ...args);
};
global.setTimeout = (callback, delay, ...args) => delay === 250 ? timeout(callback, 0, ...args) : timeout(callback, delay, ...args);
try { await alerts.initializeOwnerAccessAlerts(); }
finally { collection.updateOne = update; global.setTimeout = timeout; }
expect(polls).to.equal(305);
expect((await guards().findOne({ _id: "recipient-alerts-v1" })).state).to.equal("ready");
});
});
+12 -3
View File
@@ -17,9 +17,9 @@ const verified = (email, primary = true) => ({ email, primary, verified: true })
describe("registration email collection", function () {
const restores = [];
const stub = (object, key, value) => { const old = object[key]; restores.push(() => object[key] = old); object[key] = value; };
let response, calls, writes, user, saved, login;
let response, calls, writes, user, saved, login, resetCalls;
beforeEach(() => {
response = [verified("primary@example.com")]; calls = []; writes = []; saved = [];
response = [verified("primary@example.com")]; calls = []; writes = []; saved = []; resetCalls = [];
user = new Users({ username: "owner", externalIDs: { github: "10" }, emails: [] });
stub(global, "fetch", async (url, options) => {
calls.push({ url, options });
@@ -29,7 +29,7 @@ describe("registration email collection", function () {
stub(Users, "findOne", async () => user);
stub(Users.prototype, "save", async function () { saved.push(this); return this; });
stub(credentials, "setCredential", async () => {});
stub(notifications, "resetOwnerAccessAlerts", async () => {});
stub(notifications, "resetOwnerAccessAlerts", async id => { resetCalls.push(id); });
stub(github, "saveAppGrant", async () => {});
stub(github, "exchangeAppToken", async () => ({ access_token: "fresh-app-token" }));
stub(github, "githubRequest", async path => {
@@ -97,6 +97,9 @@ describe("registration email collection", function () {
expect(saved).to.have.length(1); expect(writes).to.have.length(1);
expect(identity.emails[0].email).to.equal("primary@example.com");
});
it(`does not reset access alerts during ordinary ${provider} sign-in`, async () => {
await callback(); expect(resetCalls).to.have.length(0);
});
it(`fills missing email on existing ${provider} sign-in`, async () => {
expect((await callback()).emails[0].email).to.equal("primary@example.com");
expect(saved).to.have.length(0);
@@ -110,6 +113,12 @@ describe("registration email collection", function () {
});
}
it("prepares recovery for an explicit OAuth reconnect", async () => {
await new Promise((resolve, reject) => verify({ githubOAuthContext: { ownerId: user.id, githubId: "10", expires: Date.now() + 60000 } },
"token", "", { id: "10", username: "owner" }, error => error ? reject(error) : resolve()));
expect(resetCalls).to.deep.equal([user.id]);
});
it("rejects an identity mismatch before requesting or saving email", async () => {
const error = await new Promise(resolve => verify({ githubOAuthContext: { githubId: "different", expires: Date.now() + 60000 } },
"token", "", { id: "10", username: "owner" }, resolve));