Files
notification-wakeup-service/test/alertSearch/cycle.test.ts
T
Jose Olarte III 226925278f Move remaining alertSearch tests under test/.
Keep them beside the existing SMS alertSearch tests and update imports so they still load src/.
2026-09-18 14:53:41 +08:00

531 lines
18 KiB
TypeScript

import assert from "node:assert/strict";
import { mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
import { afterEach, beforeEach, describe, it } from "node:test";
import { alertSearchCursorsDb } from "../../src/db/alertSearchCursorsSqlite.js";
import { closeDatabase } from "../../src/db/sqlite.js";
import {
partnerPageHasTiedBeforeDate,
type FetchLike,
} from "../../src/alertSearch/client.js";
import { runAlertSearchCycle } from "../../src/alertSearch/cycle.js";
import { ALERT_SEARCH_PAGE_SIZE } from "../../src/alertSearch/types.js";
const USER = "did:ethr:0xcycleuser";
const JWT = "delegated.jwt.token";
const ENDORSER_BASE = "https://api.endorser.ch";
const PARTNER_BASE = "https://partner-api.endorser.ch";
function ulid(n: number): string {
return `01H${String(n).padStart(23, "0")}`;
}
function jsonResponse(body: unknown, status = 200): Response {
return new Response(JSON.stringify(body), {
status,
headers: { "Content-Type": "application/json" },
});
}
function emptyEndorserBody(overrides?: Record<string, unknown>) {
return {
data: {
claims: [],
personalPlanContributions: [],
trackedPlanUpdates: [],
trackedPlanClaims: [],
plansNearby: [],
...overrides,
},
};
}
function emptyPartnerBody(overrides?: Record<string, unknown>) {
return { data: { profilesNearby: [], ...overrides } };
}
function claimRows(from: number, count: number) {
return Array.from({ length: count }, (_, i) => ({
id: ulid(from + i),
issuedAt: "2026-01-01T00:00:00Z",
issuer: "did:ethr:0x1",
}));
}
function planRows(from: number, count: number) {
return Array.from({ length: count }, (_, i) => ({
handleId: `plan-${from + i}`,
jwtId: ulid(from + i),
}));
}
function profileRows(
count: number,
updatedAt: (index: number) => string
) {
return Array.from({ length: count }, (_, i) => ({
issuerDid: `did:ethr:0x${i}`,
description: "p",
updatedAt: updatedAt(i),
rowId: i + 1,
}));
}
function captureFetch(
handler: (url: string) => Response | Promise<Response>
): { fetch: FetchLike; urls: string[] } {
const urls: string[] = [];
return {
urls,
fetch: async (url) => {
urls.push(url);
return handler(url);
},
};
}
const cycleOpts = {
jwt: JWT,
endorserBaseUrl: ENDORSER_BASE,
partnerBaseUrl: PARTNER_BASE,
};
describe("partnerPageHasTiedBeforeDate", () => {
it("is false when the page is under the server limit", () => {
assert.equal(
partnerPageHasTiedBeforeDate({
profilesNearby: profileRows(10, () => "2026-01-01T00:00:00.000Z"),
}),
false
);
});
it("is true when a full page shares the oldest updatedAt", () => {
const tied = "2026-01-01T12:00:00.000Z";
assert.equal(
partnerPageHasTiedBeforeDate({
profilesNearby: profileRows(ALERT_SEARCH_PAGE_SIZE, () => tied),
}),
true
);
});
it("is true when two or more rows share the page minimum", () => {
const rows = profileRows(ALERT_SEARCH_PAGE_SIZE, (i) =>
i < 2 ? "2026-01-01T00:00:00.000Z" : `2026-02-01T00:${String(i).padStart(2, "0")}:00.000Z`
);
assert.equal(partnerPageHasTiedBeforeDate({ profilesNearby: rows }), true);
});
it("is false when the full page has a unique oldest timestamp", () => {
const rows = profileRows(
ALERT_SEARCH_PAGE_SIZE,
(i) => `2026-01-01T00:${String(i).padStart(2, "0")}:00.000Z`
);
assert.equal(partnerPageHasTiedBeforeDate({ profilesNearby: rows }), false);
});
});
describe("alertSearch cycle integration", () => {
let dir: string;
let previousDataDir: string | undefined;
beforeEach(async () => {
previousDataDir = process.env.NOTIFY_DATA_DIR;
dir = await mkdtemp(path.join(tmpdir(), "alert-search-cycle-"));
process.env.NOTIFY_DATA_DIR = dir;
closeDatabase();
});
afterEach(async () => {
closeDatabase();
if (previousDataDir === undefined) {
delete process.env.NOTIFY_DATA_DIR;
} else {
process.env.NOTIFY_DATA_DIR = previousDataDir;
}
await rm(dir, { recursive: true, force: true });
});
it("first run omits cursors and persists after a complete non-empty retrieve", async () => {
const cap = captureFetch((url) => {
if (url.includes("/api/partner/")) {
return jsonResponse(
emptyPartnerBody({
profilesNearby: [
{
issuerDid: "did:ethr:0x1",
description: "p",
updatedAt: "2026-03-01T00:00:00.000Z",
rowId: 1,
},
],
})
);
}
return jsonResponse(
emptyEndorserBody({
claims: claimRows(10, 1),
trackedPlanUpdates: planRows(15, 1),
})
);
});
const cycle = await runAlertSearchCycle(USER, {
...cycleOpts,
config: { fetch: cap.fetch },
});
assert.equal(cycle.loaded.endorserAfterId, undefined);
assert.equal(cycle.loaded.partnerAfterDate, undefined);
for (const url of cap.urls) {
assert.equal(url.includes("afterId="), false);
assert.equal(url.includes("afterDate="), false);
}
assert.equal(cycle.retrieved.endorser.outcome, "success");
assert.equal(cycle.retrieved.partner.outcome, "success");
assert.equal(cycle.advanced.endorserAdvanced, true);
assert.equal(cycle.advanced.partnerAdvanced, true);
assert.equal(cycle.advanced.endorserAfterId, ulid(15));
assert.equal(cycle.advanced.partnerAfterAt, "2026-03-01T00:00:00.000Z");
});
it("loads Endorser afterId, paginates with MAX per-bucket minima, then advances to max ULID", async () => {
await alertSearchCursorsDb.setEndorserAfterId(USER, ulid(1));
let endorserPages = 0;
const cap = captureFetch((url) => {
if (url.includes("/api/partner/")) {
return jsonResponse(emptyPartnerBody());
}
endorserPages += 1;
if (endorserPages === 1) {
assert.equal(url.includes(ulid(1)), true);
assert.equal(url.includes("beforeId="), false);
return jsonResponse(
emptyEndorserBody({
claims: claimRows(200, ALERT_SEARCH_PAGE_SIZE),
personalPlanContributions: claimRows(50, ALERT_SEARCH_PAGE_SIZE),
})
);
}
assert.equal(url.includes(`beforeId=${ulid(200)}`), true);
assert.equal(url.includes(`beforeId=${ulid(50)}`), false);
return jsonResponse(
emptyEndorserBody({
claims: claimRows(180, 3),
})
);
});
const cycle = await runAlertSearchCycle(USER, {
...cycleOpts,
config: { fetch: cap.fetch },
});
assert.equal(cycle.retrieved.endorser.outcome, "success");
assert.equal(cycle.retrieved.endorser.pageCount, 2);
assert.equal(cycle.advanced.endorserAdvanced, true);
assert.equal(cycle.advanced.endorserAfterId, ulid(249));
assert.equal(cycle.advanced.partnerAdvanced, false);
});
it("does not advance Endorser on empty retrieve", async () => {
await alertSearchCursorsDb.setEndorserAfterId(USER, ulid(9));
const cap = captureFetch((url) => {
if (url.includes("/api/partner/")) return jsonResponse(emptyPartnerBody());
return jsonResponse(emptyEndorserBody());
});
const cycle = await runAlertSearchCycle(USER, {
...cycleOpts,
config: { fetch: cap.fetch },
});
assert.equal(cycle.retrieved.endorser.outcome, "empty");
assert.equal(cycle.advanced.endorserAdvanced, false);
assert.equal(cycle.advanced.endorserAfterId, ulid(9));
});
it("does not advance Endorser when plansNearby hits 50", async () => {
await alertSearchCursorsDb.setEndorserAfterId(USER, ulid(4));
let endorserPages = 0;
const cap = captureFetch((url) => {
if (url.includes("/api/partner/")) return jsonResponse(emptyPartnerBody());
endorserPages += 1;
if (endorserPages === 1) {
return jsonResponse(
emptyEndorserBody({
plansNearby: planRows(300, ALERT_SEARCH_PAGE_SIZE),
})
);
}
return jsonResponse(
emptyEndorserBody({ plansNearby: planRows(250, 2) })
);
});
const cycle = await runAlertSearchCycle(USER, {
...cycleOpts,
config: { fetch: cap.fetch },
});
assert.equal(cycle.retrieved.endorser.outcome, "pagination");
assert.equal(cycle.retrieved.data.plansNearby.length, ALERT_SEARCH_PAGE_SIZE + 2);
assert.equal(cycle.advanced.endorserAdvanced, false);
assert.equal(cycle.advanced.endorserAfterId, ulid(4));
});
it("does not advance Endorser on HTTP auth failure", async () => {
await alertSearchCursorsDb.setEndorserAfterId(USER, ulid(4));
const cap = captureFetch((url) => {
if (url.includes("/api/partner/")) return jsonResponse(emptyPartnerBody());
return jsonResponse({ error: "Unauthorized" }, 401);
});
const cycle = await runAlertSearchCycle(USER, {
...cycleOpts,
config: { fetch: cap.fetch },
});
assert.equal(cycle.retrieved.endorser.outcome, "auth");
assert.equal(cycle.advanced.endorserAdvanced, false);
assert.equal(cycle.advanced.endorserAfterId, ulid(4));
});
it("loads Partner afterDate, paginates with beforeDate, advances max updatedAt", async () => {
await alertSearchCursorsDb.setPartnerAfterAt(
USER,
"2026-01-01T00:00:00.000Z"
);
const page1 = profileRows(
ALERT_SEARCH_PAGE_SIZE,
(i) => `2026-02-01T00:${String(i).padStart(2, "0")}:00.000Z`
);
const minTime = [...page1.map((p) => p.updatedAt)].sort()[0];
let partnerPages = 0;
const cap = captureFetch((url) => {
if (url.includes("/api/v2/report/")) {
return jsonResponse(emptyEndorserBody());
}
partnerPages += 1;
if (partnerPages === 1) {
assert.equal(
decodeURIComponent(url).includes("afterDate=2026-01-01T00:00:00.000Z"),
true
);
return jsonResponse(emptyPartnerBody({ profilesNearby: page1 }));
}
assert.equal(decodeURIComponent(url).includes(`beforeDate=${minTime}`), true);
return jsonResponse(
emptyPartnerBody({
profilesNearby: [
{
issuerDid: "did:ethr:0x99",
description: "p",
updatedAt: "2026-01-15T00:00:00.000Z",
rowId: 99,
},
],
})
);
});
const cycle = await runAlertSearchCycle(USER, {
...cycleOpts,
config: { fetch: cap.fetch },
});
assert.equal(cycle.retrieved.partner.outcome, "success");
assert.equal(cycle.retrieved.partner.pageCount, 2);
assert.equal(cycle.advanced.partnerAdvanced, true);
assert.equal(cycle.advanced.partnerAfterAt, page1.map((p) => p.updatedAt).sort()[page1.length - 1]);
});
it("does not advance Partner on empty retrieve", async () => {
await alertSearchCursorsDb.setPartnerAfterAt(USER, "2026-01-01T00:00:00.000Z");
const cap = captureFetch((url) => {
if (url.includes("/api/partner/")) return jsonResponse(emptyPartnerBody());
return jsonResponse(emptyEndorserBody());
});
const cycle = await runAlertSearchCycle(USER, {
...cycleOpts,
config: { fetch: cap.fetch },
});
assert.equal(cycle.retrieved.partner.outcome, "empty");
assert.equal(cycle.advanced.partnerAdvanced, false);
assert.equal(cycle.advanced.partnerAfterAt, "2026-01-01T00:00:00.000Z");
});
it("does not advance Partner when a full page has tied oldest updatedAt", async () => {
await alertSearchCursorsDb.setPartnerAfterAt(USER, "2026-01-01T00:00:00.000Z");
const tied = "2026-04-01T00:00:00.000Z";
const cap = captureFetch((url) => {
if (url.includes("/api/v2/report/")) {
return jsonResponse(emptyEndorserBody());
}
return jsonResponse(
emptyPartnerBody({
profilesNearby: profileRows(ALERT_SEARCH_PAGE_SIZE, () => tied),
})
);
});
const cycle = await runAlertSearchCycle(USER, {
...cycleOpts,
config: { fetch: cap.fetch },
});
assert.equal(cycle.retrieved.partner.outcome, "pagination");
assert.match(cycle.retrieved.partner.message ?? "", /updatedAt/);
assert.equal(
cycle.retrieved.data.profilesNearby.length,
ALERT_SEARCH_PAGE_SIZE
);
assert.equal(cycle.advanced.partnerAdvanced, false);
assert.equal(cycle.advanced.partnerAfterAt, "2026-01-01T00:00:00.000Z");
});
it("A: both success → both advance", async () => {
const cap = captureFetch((url) => {
if (url.includes("/api/partner/")) {
return jsonResponse(
emptyPartnerBody({
profilesNearby: [
{
issuerDid: "did:ethr:0x1",
description: "p",
updatedAt: "2026-05-01T00:00:00.000Z",
},
],
})
);
}
return jsonResponse(emptyEndorserBody({ claims: claimRows(20, 1) }));
});
const cycle = await runAlertSearchCycle(USER, {
...cycleOpts,
config: { fetch: cap.fetch },
});
assert.equal(cycle.advanced.endorserAdvanced, true);
assert.equal(cycle.advanced.partnerAdvanced, true);
assert.equal(cycle.advanced.endorserAfterId, ulid(20));
assert.equal(cycle.advanced.partnerAfterAt, "2026-05-01T00:00:00.000Z");
});
it("B: Endorser success + Partner failure → Endorser only", async () => {
const cap = captureFetch((url) => {
if (url.includes("/api/partner/")) {
return jsonResponse({ error: "Unauthorized" }, 401);
}
return jsonResponse(emptyEndorserBody({ claims: claimRows(21, 1) }));
});
const cycle = await runAlertSearchCycle(USER, {
...cycleOpts,
config: { fetch: cap.fetch },
});
assert.equal(cycle.retrieved.endorser.outcome, "success");
assert.equal(cycle.retrieved.partner.outcome, "auth");
assert.equal(cycle.advanced.endorserAdvanced, true);
assert.equal(cycle.advanced.partnerAdvanced, false);
assert.equal(cycle.advanced.endorserAfterId, ulid(21));
assert.equal(cycle.advanced.partnerAfterAt, null);
});
it("C: Endorser failure + Partner success → Partner only", async () => {
const cap = captureFetch((url) => {
if (url.includes("/api/partner/")) {
return jsonResponse(
emptyPartnerBody({
profilesNearby: [
{
issuerDid: "did:ethr:0x1",
description: "p",
updatedAt: "2026-06-01T00:00:00.000Z",
},
],
})
);
}
return jsonResponse({ error: "fail" }, 500);
});
const cycle = await runAlertSearchCycle(USER, {
...cycleOpts,
config: { fetch: cap.fetch },
});
assert.equal(cycle.retrieved.endorser.outcome, "http");
assert.equal(cycle.retrieved.partner.outcome, "success");
assert.equal(cycle.advanced.endorserAdvanced, false);
assert.equal(cycle.advanced.partnerAdvanced, true);
assert.equal(cycle.advanced.endorserAfterId, null);
assert.equal(cycle.advanced.partnerAfterAt, "2026-06-01T00:00:00.000Z");
});
it("D: both fail → neither advances", async () => {
await alertSearchCursorsDb.setEndorserAfterId(USER, ulid(3));
await alertSearchCursorsDb.setPartnerAfterAt(USER, "2026-01-01T00:00:00.000Z");
const cap = captureFetch(() => jsonResponse({ error: "down" }, 503));
const cycle = await runAlertSearchCycle(USER, {
...cycleOpts,
config: { fetch: cap.fetch },
});
assert.equal(cycle.advanced.endorserAdvanced, false);
assert.equal(cycle.advanced.partnerAdvanced, false);
assert.equal(cycle.advanced.endorserAfterId, ulid(3));
assert.equal(cycle.advanced.partnerAfterAt, "2026-01-01T00:00:00.000Z");
});
it("E: Partner incomplete + Endorser success → Endorser only", async () => {
const tied = "2026-07-01T00:00:00.000Z";
const cap = captureFetch((url) => {
if (url.includes("/api/partner/")) {
return jsonResponse(
emptyPartnerBody({
profilesNearby: profileRows(ALERT_SEARCH_PAGE_SIZE, () => tied),
})
);
}
return jsonResponse(emptyEndorserBody({ claims: claimRows(22, 1) }));
});
const cycle = await runAlertSearchCycle(USER, {
...cycleOpts,
config: { fetch: cap.fetch },
});
assert.equal(cycle.retrieved.endorser.outcome, "success");
assert.equal(cycle.retrieved.partner.outcome, "pagination");
assert.equal(cycle.advanced.endorserAdvanced, true);
assert.equal(cycle.advanced.partnerAdvanced, false);
assert.equal(cycle.advanced.endorserAfterId, ulid(22));
});
it("persists cursors across close/reopen and sends them on the next retrieve", async () => {
const first = captureFetch((url) => {
if (url.includes("/api/partner/")) {
return jsonResponse(
emptyPartnerBody({
profilesNearby: [
{
issuerDid: "did:ethr:0x1",
description: "p",
updatedAt: "2026-08-01T00:00:00.000Z",
},
],
})
);
}
return jsonResponse(emptyEndorserBody({ claims: claimRows(30, 1) }));
});
await runAlertSearchCycle(USER, {
...cycleOpts,
config: { fetch: first.fetch },
});
closeDatabase();
const second = captureFetch((url) => {
if (url.includes("/api/partner/")) return jsonResponse(emptyPartnerBody());
return jsonResponse(emptyEndorserBody());
});
const cycle = await runAlertSearchCycle(USER, {
...cycleOpts,
config: { fetch: second.fetch },
});
assert.equal(cycle.loaded.endorserAfterId, ulid(30));
assert.equal(cycle.loaded.partnerAfterDate, "2026-08-01T00:00:00.000Z");
const endorserUrl =
second.urls.find((u) => u.includes("/api/v2/report/")) ?? "";
const partnerUrl = second.urls.find((u) => u.includes("/api/partner/")) ?? "";
assert.equal(endorserUrl.includes(ulid(30)), true);
assert.equal(
decodeURIComponent(partnerUrl).includes("afterDate=2026-08-01T00:00:00.000Z"),
true
);
assert.equal(partnerUrl.includes("afterId=0"), false);
});
});