108 lines
7.1 KiB
JavaScript
108 lines
7.1 KiB
JavaScript
const assert = require("node:assert/strict");
|
|
const fs = require("node:fs");
|
|
const vm = require("node:vm");
|
|
const { test } = require("node:test");
|
|
const { loadBackground } = require("./support/characterization.cjs");
|
|
function setup() {
|
|
let now = 100000, hours = 24, user = "a@example.invalid";
|
|
const factory = new vm.Script(fs.readFileSync("src/features/preferences/request-cache.js", "utf8") + "\ncreateFormRequestCache;").runInNewContext({ Date: { now: () => now } });
|
|
const cache = factory({ getHours: () => hours, getUserId: () => user });
|
|
return { cache, advance: value => { now += value; }, hours: value => { hours = value; }, user: value => { user = value; } };
|
|
}
|
|
const message = (name, fields = {}) => ({ type: `opportunitiesExtension.${name}`, ...fields });
|
|
test("all four form query families reuse successes with parameter-specific keys and live duration", async () => {
|
|
const h = setup(); let calls = 0;
|
|
const fetch = async request => { calls++; assert.equal(request.cacheMaxAgeMs, 24 * 3600000); return { ok: true, items: [{ name: "Original" }], debugRequests: [{ name: "network" }] }; };
|
|
for (const name of ["requestWeekDetails", "requestActiveServiceRequests", "requestServiceRequestExpand", "requestLookupTaskType", "requestActionTeamMembers", "requestActionFindLookup", "requestSrLookupPrimaryPillar", "requestSrLookupLanguage", "requestSrLookupTechPlatform", "requestSrLookupOpportunityBusiness", "requestLookupTypePlatform", "requestInternalSrServices"]) {
|
|
const msg = message(name, { id: 1 });
|
|
const result = await h.cache.send(msg, fetch); result.items[0].name = "Edited";
|
|
const cached = await h.cache.send(msg, fetch);
|
|
assert.equal(cached.fromCache, true); assert.equal(cached.items[0].name, "Original"); assert.equal(cached.debugRequests.length, 0);
|
|
await h.cache.send(message(name, { id: 2 }), fetch);
|
|
}
|
|
assert.equal(calls, 24);
|
|
h.advance(2 * 3600000); h.hours(1);
|
|
await h.cache.send(message("requestWeekDetails", { id: 1 }), async request => { calls++; assert.equal(request.cacheMaxAgeMs, 3600000); return { ok: true }; });
|
|
assert.equal(calls, 25);
|
|
});
|
|
test("failed results are never cached and identity changes discard previous responses", async () => {
|
|
const h = setup(); let calls = 0;
|
|
const send = async () => { calls++; return { ok: calls > 1 }; };
|
|
const msg = message("requestLookupTaskType");
|
|
await h.cache.send(msg, send); await h.cache.send(msg, send); await h.cache.send(msg, send); assert.equal(calls, 2);
|
|
h.user("b@example.invalid"); await h.cache.send(msg, send); assert.equal(calls, 3);
|
|
h.user(""); await h.cache.send(msg, send); await h.cache.send(msg, send); assert.equal(calls, 5);
|
|
});
|
|
test("writes and token requests always execute; successful saves and Refresh all invalidate queries", async () => {
|
|
for (const name of ["requestTimeEntriesSave", "requestTimeEntriesDelete", "requestMgmtAction", "requestPostServiceRequest", "requestPostInternalServiceRequest", "requestWorkloadForecastUpdate", "invalidateProductsCache"]) {
|
|
const h = setup(); let queries = 0, writes = 0;
|
|
const query = message("requestActionFindLookup");
|
|
const queryTransport = async () => { queries++; return { ok: true }; };
|
|
await h.cache.send(query, queryTransport);
|
|
for (let i = 0; i < 2; i++) await h.cache.send(message(name, { phase: "confirm" }), async () => { writes++; return { ok: true }; });
|
|
await h.cache.send(query, queryTransport);
|
|
assert.equal(writes, 2); assert.equal(queries, 2);
|
|
}
|
|
const h = setup(); let calls = 0;
|
|
for (let i = 0; i < 2; i++) await h.cache.send(message("requestSrTokenCatalog"), async () => { calls++; return { ok: true }; });
|
|
assert.equal(calls, 2);
|
|
});
|
|
test("reopening workloads reuses account cache across token renewals and keeps original persistent expiry", async () => {
|
|
const h = setup(); let calls = 0;
|
|
h.hours(1);
|
|
const transport = async () => { calls++; return { ok: true, workloads: [{ id: 1, forecast: [] }], cachedAt: 50000, fromCache: true }; };
|
|
const msg = message("requestWorkloads", { customerId: 123, principal: "a@example.invalid", authorization: { value: "old-token" } });
|
|
const first = await h.cache.send(msg, transport);
|
|
first.workloads[0].forecast.push({ month: 1 });
|
|
const second = await h.cache.send({ ...msg, customerId: "123", cacheMaxAgeMs: 3600000, authorization: { value: "new-token" } }, transport);
|
|
assert.equal(calls, 1);
|
|
assert.equal(second.fromCache, true);
|
|
assert.equal(second.cachedAt, 50000);
|
|
assert.equal(second.workloads[0].forecast.length, 0);
|
|
h.advance(3550000);
|
|
await h.cache.send(msg, transport);
|
|
assert.equal(calls, 2, "expires from persistent fetchedAt rather than modal opening time");
|
|
await h.cache.send({ ...msg, customerId: 456 }, transport);
|
|
assert.equal(calls, 3);
|
|
h.user("b@example.invalid");
|
|
await h.cache.send({ ...msg, principal: "b@example.invalid" }, transport);
|
|
assert.equal(calls, 4);
|
|
});
|
|
test("workload cache survives failed updates but invalidates after success and Refresh all", async () => {
|
|
const h = setup(); let calls = 0;
|
|
const msg = message("requestWorkloads", { customerId: 123 });
|
|
const transport = async () => { calls++; return { ok: true, workloads: [] }; };
|
|
await h.cache.send(msg, transport);
|
|
await h.cache.send(message("requestWorkloadForecastUpdate"), async () => ({ ok: false }));
|
|
await h.cache.send(msg, transport);
|
|
assert.equal(calls, 1);
|
|
await h.cache.send(message("requestWorkloadForecastUpdate"), async () => ({ ok: true }));
|
|
await h.cache.send(msg, transport);
|
|
assert.equal(calls, 2);
|
|
await h.cache.send(message("invalidateProductsCache"), async () => ({ ok: true }));
|
|
await h.cache.send(msg, transport);
|
|
assert.equal(calls, 3);
|
|
});
|
|
test("concurrent queries share work but old responses cannot repopulate an invalidated cache", async () => {
|
|
const h = setup(); let resolve, calls = 0;
|
|
const msg = message("requestLookupTypePlatform");
|
|
const transport = () => { calls++; return new Promise(done => { resolve = done; }); };
|
|
const first = h.cache.send(msg, transport), second = h.cache.send(msg, transport);
|
|
assert.equal(calls, 1); h.cache.invalidate(); resolve({ ok: true, items: [] }); await Promise.all([first, second]);
|
|
await h.cache.send(msg, async () => { calls++; return { ok: true }; }); assert.equal(calls, 2);
|
|
});
|
|
test("resource user respects duration and Refresh all invalidation", async () => {
|
|
let calls = 0;
|
|
const app = loadBackground({ fetch: async () => { calls++; return new Response(JSON.stringify({ resourcePartyId: 123 })); } });
|
|
const msg = message("requestResourceUser", { emailAddress: "a@example.invalid", cacheMaxAgeMs: 3600000 });
|
|
await app.dispatch(msg); await app.dispatch(msg); assert.equal(calls, 1);
|
|
app.advance(3600000); await app.dispatch(msg); assert.equal(calls, 2);
|
|
await app.dispatch(message("invalidateProductsCache")); await app.dispatch(msg); assert.equal(calls, 3);
|
|
});
|
|
test("Preferences displays generic cache duration while preserving the saved preference key", () => {
|
|
const source = fs.readFileSync("src/features/preferences/view.js", "utf8");
|
|
assert.match(source, /cacheLabel.textContent = "Cache duration"/);
|
|
assert.doesNotMatch(source, /Product cache duration/);
|
|
assert.match(source, /cacheSelect.name = "opptyProductsCacheHours"/);
|
|
});
|