Keep them beside the existing SMS alertSearch tests and update imports so they still load src/.
511 lines
16 KiB
TypeScript
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");
|
|
});
|
|
});
|