Files
opportunities-extension/tests/activityCache.test.cjs

145 lines
7.9 KiB
JavaScript

const assert = require("node:assert/strict");
const { test } = require("node:test");
const { loadApplication } = require("./support/application.cjs");
const flush = () => new Promise(resolve => setImmediate(resolve));
function setup(family, saved = {}, principal = "user@example.invalid") {
const local = {
get: async key => key === null ? { ...saved } : ({ [key]: saved[key] }),
set: async values => Object.assign(saved, JSON.parse(JSON.stringify(values))),
remove: async keys => keys.forEach(key => delete saved[key])
};
const api = family === "firefox" ? { browser: { storage: { local } } } : {
chrome: { runtime: {}, storage: { local: {
get: (key, done) => local.get(key).then(done), set: (values, done) => local.set(values).then(done),
remove: (keys, done) => local.remove(keys).then(done)
} } }
};
const document = { createElement: () => ({ append() {}, setAttribute() {}, addEventListener() {} }) };
document.createElementNS = (_, tag) => document.createElement(tag);
const app = loadApplication("content", { ...api, document });
app.authSessionState.principal = principal;
app.currentUser.allowsCrmActivities = () => true;
app.preferencesState.opptyProductsCacheHours = 24;
app.opportunitiesQueryState.periodRequestVersion = 1;
app.activityProgress.update = () => {};
app.productsContentJobs.queueProductsTableRender = () => {};
app.debugContentLog.appendBackgroundRequestLog = () => {};
app.authContentSession.requestConsumerTokenServiceOnce = async () => ({ workloadActions: { value: "synthetic-token" } });
const calls = [];
app.fusionCrmServiceRequests.requestCrmServiceRequest = async () => {
calls.push("sr");
return [{ SrNumber: "SR1", ReportedHours_c: 4, OpportunityName_Id_c: { OptyNumber: "OP1" } }];
};
app.platformContentMessages.sendRuntimeMessage = async () => {
calls.push("actions");
return { ok: true, actions: [{ owner: principal, name: "Test action" }], hasNext: false };
};
async function load(cachedAt, cacheOnly) {
app.opportunityCrmServiceRequests.reset();
await app.opportunityCrmServiceRequests.load("synthetic-token", 1, { cachedAt, cacheOnly });
const products = { items: [{ RevnId: "123" }], cachedAt, fromCache: cacheOnly };
app.productsContentState.opptyProductsByNumber.set("OP1", products);
app.opportunityWorkloadActions.loadProducts(products, true, { cachedAt, cacheOnly });
await flush();
}
return { app, calls, load, saved, local };
}
for (const family of ["chrome", "firefox"]) {
test(`${family}: Current Quarter -> Next Quarter -> Current Quarter restores SR hours and filters without requests`, async () => {
const h = setup(family);
const currentStamp = Date.now() - 2000;
const nextStamp = currentStamp + 1000;
let hours = 2;
h.app.fusionCrmServiceRequests.requestCrmServiceRequest = async () => {
h.calls.push("sr");
return [{ SrNumber: "SR1", ReportedHours_c: hours, OpportunityName_Id_c: { OptyNumber: "OP1" } }];
};
await h.load(currentStamp, false);
hours = 8;
await h.load(nextStamp, false);
assert.equal(h.app.opportunityCrmServiceRequests.getSummary("OP1").reportedHours, 8);
const count = h.calls.length;
await h.load(currentStamp, true);
assert.equal(h.calls.length, count);
assert.equal(h.app.opportunityCrmServiceRequests.createBadge("OP1").textContent, "2h");
h.app.opportunitiesTable.renderOpportunitiesTable = () => {};
h.app.activityFilters.toggle("sr", "under4");
assert.equal(h.app.activityFilters.matches({ OptyNumber: "OP1" }), true);
await h.load(nextStamp, true);
assert.equal(h.calls.length, count);
assert.equal(h.app.activityFilters.matches({ OptyNumber: "OP1" }), false);
const reopened = setup(family, h.saved);
await reopened.load(currentStamp, true);
assert.deepEqual(reopened.calls, []);
assert.equal(reopened.app.opportunityCrmServiceRequests.getSummary("OP1").reportedHours, 2);
});
test(`${family}: activities persist across instances with product timestamps and no credential storage`, async () => {
const h = setup(family);
const stamp = Date.now() - 1000;
await h.load(stamp, false);
assert.deepEqual(h.calls, ["sr", "actions"]);
assert.equal(Object.keys(h.saved).length, 2);
Object.values(h.saved).forEach(entry => assert.equal(entry.fetchedAt, stamp));
assert.doesNotMatch(JSON.stringify(h.saved), /synthetic-token|debugRequests|authorization/i);
const restored = setup(family, h.saved);
await restored.load(stamp, true);
assert.deepEqual(restored.calls, []);
assert.equal(restored.app.opportunityCrmServiceRequests.createBadge("OP1").textContent, "4h");
assert.equal(restored.app.opportunityWorkloadActions.createBadge("OP1").textContent, "1");
await restored.load(stamp, false);
assert.deepEqual(restored.calls, [], "valid matching cache also deduplicates repeated fresh-product notifications");
const other = setup(family, h.saved, "other@example.invalid");
await other.load(stamp, true);
assert.deepEqual(other.calls, []);
assert.equal(other.app.opportunityCrmServiceRequests.createBadge("OP1"), null);
assert.equal(other.app.opportunityWorkloadActions.createBadge("OP1").textContent, "?");
await restored.load(stamp + 1, false);
assert.deepEqual(restored.calls, ["sr", "actions"]);
});
}
test("TTL follows product preferences, rejects stale/mismatched cache and stores empty success", async () => {
const h = setup("firefox");
const stamp = Date.now() - 2 * 3600000;
await h.app.activityCache.write("sr", "all", stamp, []);
assert.ok(await h.app.activityCache.read("sr", "all", stamp));
h.app.preferencesState.opptyProductsCacheHours = 1;
assert.equal(await h.app.activityCache.read("sr", "all", stamp), null);
h.app.preferencesState.opptyProductsCacheHours = 24;
assert.equal(await h.app.activityCache.read("sr", "all", stamp + 1), null);
assert.equal(await h.app.activityCache.read("sr", "all", Date.now() + 100000), null);
await h.load(stamp + 1, true);
assert.deepEqual(h.calls, []);
});
test("versioned SR cache reads matching legacy entries and prunes expired snapshots only", async () => {
const h = setup("firefox");
const legacyKey = `opportunitiesExtension.activities.v1:${JSON.stringify(["user@example.invalid", "sr", "all"])}`;
const stamp = Date.now() - 1000;
h.saved[legacyKey] = { fetchedAt: stamp, items: [], hasNext: false };
assert.ok(await h.app.activityCache.read("sr", "all", stamp));
assert.equal(await h.app.activityCache.read("sr", "all", stamp + 1), null);
const expiredStamp = Date.now() - 25 * 3600000;
const expiredKey = `opportunitiesExtension.activities.v1:${JSON.stringify(["user@example.invalid", "sr", "all", expiredStamp])}`;
h.saved[expiredKey] = { fetchedAt: expiredStamp, items: [], hasNext: false };
h.saved.preferences = { keep: true };
await h.app.activityCache.write("sr", "all", stamp + 1, []);
assert.equal(h.saved[expiredKey], undefined);
assert.ok(h.saved[legacyKey]);
assert.equal(h.saved.preferences.keep, true);
await h.app.activityCache.invalidate();
assert.equal(await h.app.activityCache.read("sr", "all", stamp + 1), null);
assert.equal(h.saved.preferences.keep, true);
});
test("storage failure does not lose live activity results and failures are never cached", async () => {
const h = setup("firefox");
h.local.get = async () => { throw new Error("Synthetic storage read failure"); };
h.local.set = async () => { throw new Error("Synthetic storage write failure"); };
await h.load(Date.now() - 1000, false);
assert.equal(h.app.opportunityCrmServiceRequests.createBadge("OP1").textContent, "4h");
const failed = setup("firefox");
failed.app.fusionCrmServiceRequests.requestCrmServiceRequest = async () => { throw new Error("Synthetic failure"); };
failed.app.platformContentMessages.sendRuntimeMessage = async () => ({ ok: false });
await failed.load(Date.now() - 1000, false);
assert.deepEqual(failed.saved, {});
});