Files
notification-wakeup-service/test/alertSearch/retrieve.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

511 lines
16 KiB
TypeScript

import assert from "node:assert/strict";
import { describe, it } from "node:test";
import {
fetchEndorserAlertSearchPage,
fetchPartnerAlertSearchPage,
nextEndorserBeforeId,
type FetchLike,
} from "../../src/alertSearch/client.js";
import { retrieveAlertSearch } from "../../src/alertSearch/retrieve.js";
import { ALERT_SEARCH_PAGE_SIZE, type EndorserAlertSearchData } from "../../src/alertSearch/types.js";
const JWT = "delegated.jwt.token";
const ENDORSER_BASE = "https://api.endorser.ch";
const PARTNER_BASE = "https://partner-api.endorser.ch";
const AFTER_E = "01H0000000000000000000000A";
const AFTER_P = "01H0000000000000000000000B";
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 jsonResponse(body: unknown, status = 200): Response {
return new Response(JSON.stringify(body), {
status,
headers: { "Content-Type": "application/json" },
});
}
function captureFetch(
handler: (url: string, init?: RequestInit) => Response | Promise<Response>
): { fetch: FetchLike; urls: string[]; auths: string[] } {
const urls: string[] = [];
const auths: string[] = [];
const fetchImpl: FetchLike = async (input, init) => {
urls.push(input);
auths.push(new Headers(init?.headers).get("Authorization") ?? "");
assert.equal(init?.method, "GET");
return handler(input, init);
};
return { fetch: fetchImpl, urls, auths };
}
function ulid(n: number): string {
return `01H${String(n).padStart(23, "0")}`;
}
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),
}));
}
describe("alertSearch HTTP pages", () => {
it("sends the delegated JWT as Bearer on Endorser GET", async () => {
const cap = captureFetch(() => jsonResponse(emptyEndorserBody()));
await fetchEndorserAlertSearchPage({
baseUrl: ENDORSER_BASE,
jwt: JWT,
query: {},
config: { fetch: cap.fetch },
});
assert.equal(cap.auths[0], "Bearer delegated.jwt.token");
assert.equal(cap.urls[0], `${ENDORSER_BASE}/api/v2/report/alertSearch`);
});
it("sends the delegated JWT as Bearer on Partner GET", async () => {
const cap = captureFetch(() => jsonResponse(emptyPartnerBody()));
await fetchPartnerAlertSearchPage({
baseUrl: PARTNER_BASE,
jwt: JWT,
query: {},
config: { fetch: cap.fetch },
});
assert.equal(cap.auths[0], "Bearer delegated.jwt.token");
assert.equal(cap.urls[0], `${PARTNER_BASE}/api/partner/alertSearch`);
});
it("classifies auth failure", async () => {
const cap = captureFetch(() =>
jsonResponse({ error: "Request must include a valid Authorization JWT" }, 400)
);
const result = await fetchEndorserAlertSearchPage({
baseUrl: ENDORSER_BASE,
jwt: JWT,
query: {},
config: { fetch: cap.fetch },
});
assert.equal(result.ok, false);
if (!result.ok) assert.equal(result.reason, "auth");
});
it("classifies network failure", async () => {
const fetchImpl: FetchLike = async () => {
throw new TypeError("fetch failed");
};
const result = await fetchPartnerAlertSearchPage({
baseUrl: PARTNER_BASE,
jwt: JWT,
query: {},
config: { fetch: fetchImpl },
});
assert.equal(result.ok, false);
if (!result.ok) assert.equal(result.reason, "network");
});
it("classifies malformed Endorser envelope", async () => {
const cap = captureFetch(() => jsonResponse({ data: { hitLimit: true } }));
const result = await fetchEndorserAlertSearchPage({
baseUrl: ENDORSER_BASE,
jwt: JWT,
query: {},
config: { fetch: cap.fetch },
});
assert.equal(result.ok, false);
if (!result.ok) assert.equal(result.reason, "malformed");
});
it("classifies timeout", async () => {
const fetchImpl: FetchLike = async () => {
const err = new Error("The operation was aborted");
err.name = "TimeoutError";
throw err;
};
const result = await fetchEndorserAlertSearchPage({
baseUrl: ENDORSER_BASE,
jwt: JWT,
query: {},
config: { fetch: fetchImpl },
});
assert.equal(result.ok, false);
if (!result.ok) assert.equal(result.reason, "timeout");
});
});
describe("nextEndorserBeforeId", () => {
it("uses MAX of per-bucket minima when multiple buckets hit 50", () => {
const data: EndorserAlertSearchData = {
claims: claimRows(200, ALERT_SEARCH_PAGE_SIZE),
personalPlanContributions: claimRows(50, ALERT_SEARCH_PAGE_SIZE),
trackedPlanUpdates: [],
trackedPlanClaims: [],
plansNearby: [],
};
const claimsMin = ulid(200);
const contributionsMin = ulid(50);
assert.equal(nextEndorserBeforeId(data), claimsMin);
assert.notEqual(nextEndorserBeforeId(data), contributionsMin);
});
it("uses the single truncated bucket min", () => {
const data: EndorserAlertSearchData = {
claims: claimRows(100, ALERT_SEARCH_PAGE_SIZE),
personalPlanContributions: [],
trackedPlanUpdates: [],
trackedPlanClaims: [],
plansNearby: [],
};
assert.equal(nextEndorserBeforeId(data), ulid(100));
});
});
describe("retrieveAlertSearch", () => {
it("first run omits afterId on both hosts", async () => {
const cap = captureFetch((url) => {
if (url.includes("/api/partner/")) {
return jsonResponse(emptyPartnerBody());
}
return jsonResponse(emptyEndorserBody());
});
const result = await retrieveAlertSearch({
jwt: JWT,
endorserBaseUrl: ENDORSER_BASE,
partnerBaseUrl: PARTNER_BASE,
config: { fetch: cap.fetch },
});
assert.equal(result.endorser.outcome, "empty");
assert.equal(result.partner.outcome, "empty");
assert.equal(result.empty, true);
assert.equal(cap.urls.length, 2);
for (const url of cap.urls) {
assert.equal(url.includes("afterId="), false);
}
});
it("passes independent afterId cursors", async () => {
const cap = captureFetch((url) => {
if (url.includes("/api/partner/")) {
return jsonResponse(emptyPartnerBody());
}
return jsonResponse(emptyEndorserBody());
});
await retrieveAlertSearch({
jwt: JWT,
endorserAfterId: AFTER_E,
partnerAfterId: AFTER_P,
endorserBaseUrl: ENDORSER_BASE,
partnerBaseUrl: PARTNER_BASE,
config: { fetch: cap.fetch },
});
const endorserUrl = cap.urls.find((u) => u.includes("/api/v2/report/")) ?? "";
const partnerUrl = cap.urls.find((u) => u.includes("/api/partner/")) ?? "";
assert.equal(endorserUrl.includes(AFTER_E), true);
assert.equal(endorserUrl.includes(AFTER_P), false);
assert.equal(partnerUrl.includes(AFTER_P), true);
assert.equal(partnerUrl.includes(AFTER_E), false);
});
it("paginates Endorser with beforeId from the limited bucket", async () => {
const page1Ids = Array.from({ length: ALERT_SEARCH_PAGE_SIZE }, (_, i) =>
ulid(100 + i)
);
const minId = [...page1Ids].sort()[0];
let endorserPages = 0;
const cap = captureFetch((url) => {
if (url.includes("/api/partner/")) {
return jsonResponse(emptyPartnerBody());
}
endorserPages += 1;
if (endorserPages === 1) {
assert.equal(url.includes("beforeId="), false);
return jsonResponse(
emptyEndorserBody({
claims: page1Ids.map((id) => ({
id,
issuedAt: "2026-01-01T00:00:00Z",
issuer: "did:ethr:0x1",
})),
})
);
}
assert.equal(url.includes(`beforeId=${minId}`), true);
return jsonResponse(
emptyEndorserBody({
claims: [
{
id: ulid(1),
issuedAt: "2026-01-01T00:00:00Z",
issuer: "did:ethr:0x1",
},
],
})
);
});
const result = await retrieveAlertSearch({
jwt: JWT,
endorserBaseUrl: ENDORSER_BASE,
partnerBaseUrl: PARTNER_BASE,
config: { fetch: cap.fetch },
});
assert.equal(result.endorser.outcome, "success");
assert.equal(result.endorser.pageCount, 2);
assert.equal(result.data.claims.length, ALERT_SEARCH_PAGE_SIZE + 1);
});
it("uses MAX of per-bucket minima as shared beforeId when two buckets hit 50", async () => {
const claimsMin = ulid(200);
const contributionsMin = ulid(50);
let endorserPages = 0;
const cap = captureFetch((url) => {
if (url.includes("/api/partner/")) {
return jsonResponse(emptyPartnerBody());
}
endorserPages += 1;
if (endorserPages === 1) {
return jsonResponse(
emptyEndorserBody({
claims: claimRows(200, ALERT_SEARCH_PAGE_SIZE),
personalPlanContributions: claimRows(50, ALERT_SEARCH_PAGE_SIZE),
})
);
}
assert.equal(url.includes(`beforeId=${claimsMin}`), true);
assert.equal(url.includes(`beforeId=${contributionsMin}`), false);
return jsonResponse(
emptyEndorserBody({
claims: claimRows(180, 5),
})
);
});
const result = await retrieveAlertSearch({
jwt: JWT,
endorserBaseUrl: ENDORSER_BASE,
partnerBaseUrl: PARTNER_BASE,
config: { fetch: cap.fetch },
});
assert.equal(result.endorser.outcome, "success");
assert.equal(result.endorser.pageCount, 2);
assert.equal(result.data.claims.length, ALERT_SEARCH_PAGE_SIZE + 5);
assert.equal(result.data.personalPlanContributions.length, ALERT_SEARCH_PAGE_SIZE);
});
it("classifies plansNearby hitting 50 as incomplete pagination and keeps records", async () => {
let endorserPages = 0;
const cap = captureFetch((url) => {
if (url.includes("/api/partner/")) {
return jsonResponse(emptyPartnerBody());
}
endorserPages += 1;
if (endorserPages === 1) {
return jsonResponse(
emptyEndorserBody({
claims: claimRows(1, 3),
plansNearby: planRows(300, ALERT_SEARCH_PAGE_SIZE),
})
);
}
return jsonResponse(
emptyEndorserBody({
plansNearby: planRows(250, 4),
})
);
});
const result = await retrieveAlertSearch({
jwt: JWT,
endorserBaseUrl: ENDORSER_BASE,
partnerBaseUrl: PARTNER_BASE,
config: { fetch: cap.fetch },
});
assert.equal(result.endorser.outcome, "pagination");
assert.equal(result.endorser.reason, "pagination");
assert.match(result.endorser.message ?? "", /plansNearby/);
assert.equal(result.data.plansNearby.length, ALERT_SEARCH_PAGE_SIZE + 4);
assert.equal(result.data.claims.length, 3);
});
it("paginates Partner with beforeDate independently", async () => {
const times = Array.from(
{ length: ALERT_SEARCH_PAGE_SIZE },
(_, i) => `2026-01-01T00:${String(i).padStart(2, "0")}:00.000Z`
);
const minTime = [...times].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(url.includes("beforeDate="), false);
return jsonResponse(
emptyPartnerBody({
profilesNearby: times.map((updatedAt, i) => ({
issuerDid: `did:ethr:0x${i}`,
description: "p",
updatedAt,
rowId: i,
})),
})
);
}
assert.equal(decodeURIComponent(url).includes(`beforeDate=${minTime}`), true);
return jsonResponse(emptyPartnerBody());
});
const result = await retrieveAlertSearch({
jwt: JWT,
endorserBaseUrl: ENDORSER_BASE,
partnerBaseUrl: PARTNER_BASE,
config: { fetch: cap.fetch },
});
assert.equal(result.partner.pageCount, 2);
assert.equal(result.partner.outcome, "success");
});
it("Partner full page with tied oldest updatedAt is incomplete, not a second exclusive page", async () => {
const tied = "2026-01-01T12:00:00.000Z";
let partnerPages = 0;
const cap = captureFetch((url) => {
if (url.includes("/api/v2/report/")) {
return jsonResponse(emptyEndorserBody());
}
partnerPages += 1;
assert.equal(url.includes("rowId"), false);
assert.equal(url.includes("beforeRowId"), false);
return jsonResponse(
emptyPartnerBody({
profilesNearby: Array.from(
{ length: ALERT_SEARCH_PAGE_SIZE },
(_, i) => ({
issuerDid: `did:ethr:0x${i}`,
description: "p",
updatedAt: tied,
rowId: i,
})
),
})
);
});
const result = await retrieveAlertSearch({
jwt: JWT,
endorserBaseUrl: ENDORSER_BASE,
partnerBaseUrl: PARTNER_BASE,
config: { fetch: cap.fetch },
});
assert.equal(partnerPages, 1);
assert.equal(result.partner.pageCount, 1);
assert.equal(result.partner.outcome, "pagination");
assert.equal(result.data.profilesNearby.length, ALERT_SEARCH_PAGE_SIZE);
});
it("returns success with empty buckets", async () => {
const cap = captureFetch((url) => {
if (url.includes("/api/partner/")) return jsonResponse(emptyPartnerBody());
return jsonResponse(emptyEndorserBody());
});
const result = await retrieveAlertSearch({
jwt: JWT,
endorserBaseUrl: ENDORSER_BASE,
partnerBaseUrl: PARTNER_BASE,
config: { fetch: cap.fetch },
});
assert.equal(result.empty, true);
assert.equal(result.endorser.outcome, "empty");
assert.equal(result.partner.outcome, "empty");
assert.ok("claims" in result.data);
assert.ok("profilesNearby" in result.data);
});
it("preserves all six result categories", async () => {
const cap = captureFetch((url) => {
if (url.includes("/api/partner/")) {
return jsonResponse(
emptyPartnerBody({
profilesNearby: [
{ issuerDid: "did:ethr:0x2", description: "near", extra: true },
],
})
);
}
return jsonResponse({
data: {
claims: [{ id: ulid(1), issuedAt: "t", issuer: "did:ethr:0x1", extra: 1 }],
personalPlanContributions: [
{ id: ulid(2), issuedAt: "t", issuer: "did:ethr:0x1", claim: "{}" },
],
trackedPlanUpdates: [{ handleId: "h", jwtId: ulid(3) }],
trackedPlanClaims: [
{ id: ulid(4), issuedAt: "t", issuer: "did:ethr:0x1" },
],
plansNearby: [{ handleId: "p", jwtId: ulid(5), locLat: 1 }],
},
});
});
const result = await retrieveAlertSearch({
jwt: JWT,
endorserBaseUrl: ENDORSER_BASE,
partnerBaseUrl: PARTNER_BASE,
config: { fetch: cap.fetch },
});
assert.equal(result.endorser.outcome, "success");
assert.equal(result.partner.outcome, "success");
assert.equal(result.data.claims[0]?.extra, 1);
assert.equal(result.data.profilesNearby[0]?.extra, true);
assert.equal(result.data.personalPlanContributions.length, 1);
assert.equal(result.data.trackedPlanUpdates.length, 1);
assert.equal(result.data.trackedPlanClaims.length, 1);
assert.equal(result.data.plansNearby.length, 1);
});
it("classifies truncation userMessage as incomplete pagination", async () => {
const cap = captureFetch((url) => {
if (url.includes("/api/partner/")) return jsonResponse(emptyPartnerBody());
return jsonResponse({
data: {
claims: [],
personalPlanContributions: [],
trackedPlanUpdates: [],
trackedPlanClaims: [],
plansNearby: [],
},
userMessage:
"Some data was not available in this search. Check the detail screens for the full set of data.",
});
});
const result = await retrieveAlertSearch({
jwt: JWT,
endorserBaseUrl: ENDORSER_BASE,
partnerBaseUrl: PARTNER_BASE,
config: { fetch: cap.fetch },
});
assert.equal(result.endorser.outcome, "pagination");
assert.equal(result.endorser.reason, "pagination");
});
});