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

386 lines
13 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 {
advanceAlertSearchCursors,
loadAlertSearchCursors,
maxEndorserAlertSearchUlid,
} from "../../src/alertSearch/cursors.js";
import { retrieveAlertSearch } from "../../src/alertSearch/retrieve.js";
import type { AlertSearchSourceResult, RetrieveAlertSearchResult } from "../../src/alertSearch/retrieve.js";
import { ALERT_SEARCH_PAGE_SIZE } from "../../src/alertSearch/types.js";
import type { EndorserAlertSearchData, PartnerAlertSearchData } from "../../src/alertSearch/types.js";
const USER = "did:ethr:0xcursoruser";
const OTHER_USER = "did:ethr:0xother";
const JWT = "delegated.jwt.token";
function ulid(n: number): string {
return `01H${String(n).padStart(23, "0")}`;
}
function emptyEndorser(): EndorserAlertSearchData {
return {
claims: [],
personalPlanContributions: [],
trackedPlanUpdates: [],
trackedPlanClaims: [],
plansNearby: [],
};
}
function emptyPartner(): PartnerAlertSearchData {
return { profilesNearby: [] };
}
function endorserResult(
outcome: AlertSearchSourceResult<EndorserAlertSearchData>["outcome"],
data: EndorserAlertSearchData = emptyEndorser()
): AlertSearchSourceResult<EndorserAlertSearchData> {
return { outcome, pageCount: 1, data };
}
function partnerResult(
outcome: AlertSearchSourceResult<PartnerAlertSearchData>["outcome"],
data: PartnerAlertSearchData = emptyPartner()
): AlertSearchSourceResult<PartnerAlertSearchData> {
return { outcome, pageCount: 1, data };
}
function combined(
endorser: AlertSearchSourceResult<EndorserAlertSearchData>,
partner: AlertSearchSourceResult<PartnerAlertSearchData>
): RetrieveAlertSearchResult {
return {
data: { ...endorser.data, ...partner.data },
empty: false,
endorser,
partner,
};
}
function jsonResponse(body: unknown, status = 200): Response {
return new Response(JSON.stringify(body), {
status,
headers: { "Content-Type": "application/json" },
});
}
function emptyEndorserBody() {
return {
data: {
claims: [],
personalPlanContributions: [],
trackedPlanUpdates: [],
trackedPlanClaims: [],
plansNearby: [],
},
};
}
function emptyPartnerBody(overrides?: Record<string, unknown>) {
return { data: { profilesNearby: [], ...overrides } };
}
describe("alertSearch cursor persistence", () => {
let dir: string;
let previousDataDir: string | undefined;
beforeEach(async () => {
previousDataDir = process.env.NOTIFY_DATA_DIR;
dir = await mkdtemp(path.join(tmpdir(), "alert-search-cursors-"));
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("no existing cursor → retrieval receives no afterId or afterDate", async () => {
const loaded = await loadAlertSearchCursors(USER);
assert.equal(loaded.endorserAfterId, undefined);
assert.equal(loaded.partnerAfterDate, undefined);
const urls: string[] = [];
const result = await retrieveAlertSearch({
jwt: JWT,
...loaded,
endorserBaseUrl: "https://api.endorser.ch",
partnerBaseUrl: "https://partner-api.endorser.ch",
config: {
fetch: async (url) => {
urls.push(url);
if (url.includes("/api/partner/")) {
return jsonResponse(emptyPartnerBody());
}
return jsonResponse(emptyEndorserBody());
},
},
});
assert.equal(result.endorser.outcome, "empty");
for (const url of urls) {
assert.equal(url.includes("afterId="), false);
assert.equal(url.includes("afterDate="), false);
}
});
it("complete Endorser retrieval → cursor becomes maximum returned ULID", async () => {
const data = emptyEndorser();
data.claims = [
{ id: ulid(10), issuedAt: "t", issuer: "did:ethr:0x1" },
{ id: ulid(30), issuedAt: "t", issuer: "did:ethr:0x1" },
];
data.plansNearby = [{ handleId: "p", jwtId: ulid(20) }];
assert.equal(maxEndorserAlertSearchUlid(data), ulid(30));
const advanced = await advanceAlertSearchCursors(
USER,
combined(endorserResult("success", data), partnerResult("empty"))
);
assert.equal(advanced.endorserAdvanced, true);
assert.equal(advanced.endorserAfterId, ulid(30));
assert.equal(advanced.partnerAdvanced, false);
});
it("Endorser empty retrieval → existing cursor unchanged", async () => {
await alertSearchCursorsDb.setEndorserAfterId(USER, ulid(99));
const advanced = await advanceAlertSearchCursors(
USER,
combined(endorserResult("empty"), partnerResult("empty"))
);
assert.equal(advanced.endorserAdvanced, false);
assert.equal(advanced.endorserAfterId, ulid(99));
});
it("Endorser pagination → cursor unchanged", async () => {
await alertSearchCursorsDb.setEndorserAfterId(USER, ulid(5));
const data = emptyEndorser();
data.claims = Array.from({ length: ALERT_SEARCH_PAGE_SIZE }, (_, i) => ({
id: ulid(100 + i),
issuedAt: "t",
issuer: "did:ethr:0x1",
}));
const advanced = await advanceAlertSearchCursors(
USER,
combined(endorserResult("pagination", data), partnerResult("empty"))
);
assert.equal(advanced.endorserAdvanced, false);
assert.equal(advanced.endorserAfterId, ulid(5));
});
it("Endorser error → cursor unchanged", async () => {
await alertSearchCursorsDb.setEndorserAfterId(USER, ulid(5));
for (const outcome of ["auth", "timeout", "network", "malformed", "http"] as const) {
const advanced = await advanceAlertSearchCursors(
USER,
combined(endorserResult(outcome), partnerResult("empty"))
);
assert.equal(advanced.endorserAdvanced, false, outcome);
assert.equal(advanced.endorserAfterId, ulid(5), outcome);
}
});
it("complete Partner retrieval → cursor becomes maximum returned updatedAt", async () => {
const data = emptyPartner();
data.profilesNearby = [
{
issuerDid: "did:ethr:0x1",
description: "a",
updatedAt: "2026-01-01T00:00:00.000Z",
},
{
issuerDid: "did:ethr:0x2",
description: "b",
updatedAt: "2026-03-01T12:00:00.000Z",
},
];
const advanced = await advanceAlertSearchCursors(
USER,
combined(endorserResult("empty"), partnerResult("success", data))
);
assert.equal(advanced.partnerAdvanced, true);
assert.equal(advanced.partnerAfterAt, "2026-03-01T12:00:00.000Z");
});
it("Partner empty retrieval → existing cursor unchanged", async () => {
await alertSearchCursorsDb.setPartnerAfterAt(USER, "2026-02-01T00:00:00.000Z");
const advanced = await advanceAlertSearchCursors(
USER,
combined(endorserResult("empty"), partnerResult("empty"))
);
assert.equal(advanced.partnerAdvanced, false);
assert.equal(advanced.partnerAfterAt, "2026-02-01T00:00:00.000Z");
});
it("Partner pagination → cursor unchanged", async () => {
await alertSearchCursorsDb.setPartnerAfterAt(USER, "2026-02-01T00:00:00.000Z");
const data = emptyPartner();
data.profilesNearby = Array.from({ length: ALERT_SEARCH_PAGE_SIZE }, (_, i) => ({
issuerDid: `did:ethr:0x${i}`,
description: "p",
updatedAt: "2026-04-01T00:00:00.000Z",
}));
const advanced = await advanceAlertSearchCursors(
USER,
combined(endorserResult("empty"), partnerResult("pagination", data))
);
assert.equal(advanced.partnerAdvanced, false);
assert.equal(advanced.partnerAfterAt, "2026-02-01T00:00:00.000Z");
});
it("Partner error → cursor unchanged", async () => {
await alertSearchCursorsDb.setPartnerAfterAt(USER, "2026-02-01T00:00:00.000Z");
const advanced = await advanceAlertSearchCursors(
USER,
combined(endorserResult("empty"), partnerResult("network"))
);
assert.equal(advanced.partnerAdvanced, false);
assert.equal(advanced.partnerAfterAt, "2026-02-01T00:00:00.000Z");
});
it("Endorser success + Partner failure → only Endorser advances", async () => {
const endorserData = emptyEndorser();
endorserData.claims = [
{ id: ulid(40), issuedAt: "t", issuer: "did:ethr:0x1" },
];
const advanced = await advanceAlertSearchCursors(
USER,
combined(endorserResult("success", endorserData), partnerResult("auth"))
);
assert.equal(advanced.endorserAdvanced, true);
assert.equal(advanced.endorserAfterId, ulid(40));
assert.equal(advanced.partnerAdvanced, false);
assert.equal(advanced.partnerAfterAt, null);
});
it("Endorser failure + Partner success → only Partner advances", async () => {
const partnerData = emptyPartner();
partnerData.profilesNearby = [
{
issuerDid: "did:ethr:0x1",
description: "p",
updatedAt: "2026-05-01T00:00:00.000Z",
},
];
const advanced = await advanceAlertSearchCursors(
USER,
combined(endorserResult("timeout"), partnerResult("success", partnerData))
);
assert.equal(advanced.endorserAdvanced, false);
assert.equal(advanced.endorserAfterId, null);
assert.equal(advanced.partnerAdvanced, true);
assert.equal(advanced.partnerAfterAt, "2026-05-01T00:00:00.000Z");
});
it("both successful → both advance", async () => {
const endorserData = emptyEndorser();
endorserData.trackedPlanClaims = [
{ id: ulid(7), issuedAt: "t", issuer: "did:ethr:0x1" },
];
const partnerData = emptyPartner();
partnerData.profilesNearby = [
{
issuerDid: "did:ethr:0x1",
description: "p",
updatedAt: "2026-06-01T00:00:00.000Z",
},
];
const advanced = await advanceAlertSearchCursors(
USER,
combined(
endorserResult("success", endorserData),
partnerResult("success", partnerData)
)
);
assert.equal(advanced.endorserAdvanced, true);
assert.equal(advanced.partnerAdvanced, true);
assert.equal(advanced.endorserAfterId, ulid(7));
assert.equal(advanced.partnerAfterAt, "2026-06-01T00:00:00.000Z");
});
it("no usable returned cursor → existing cursor unchanged", async () => {
await alertSearchCursorsDb.setEndorserAfterId(USER, ulid(8));
await alertSearchCursorsDb.setPartnerAfterAt(USER, "2026-01-01T00:00:00.000Z");
const endorserData = emptyEndorser();
endorserData.claims = [
{ id: "not-a-ulid", issuedAt: "t", issuer: "did:ethr:0x1" },
];
const partnerData = emptyPartner();
partnerData.profilesNearby = [
{ issuerDid: "did:ethr:0x1", description: "p" },
];
const advanced = await advanceAlertSearchCursors(
USER,
combined(
endorserResult("success", endorserData),
partnerResult("success", partnerData)
)
);
assert.equal(advanced.endorserAdvanced, false);
assert.equal(advanced.partnerAdvanced, false);
assert.equal(advanced.endorserAfterId, ulid(8));
assert.equal(advanced.partnerAfterAt, "2026-01-01T00:00:00.000Z");
});
it("survives close and reopen of the SQLite connection", async () => {
await alertSearchCursorsDb.setEndorserAfterId(USER, ulid(11));
await alertSearchCursorsDb.setPartnerAfterAt(
USER,
"2026-07-01T00:00:00.000Z"
);
closeDatabase();
const loaded = await loadAlertSearchCursors(USER);
assert.equal(loaded.endorserAfterId, ulid(11));
assert.equal(loaded.partnerAfterDate, "2026-07-01T00:00:00.000Z");
const other = await loadAlertSearchCursors(OTHER_USER);
assert.equal(other.endorserAfterId, undefined);
assert.equal(other.partnerAfterDate, undefined);
});
it("loaded Partner cursor is sent as afterDate, not afterId 0", async () => {
await alertSearchCursorsDb.setPartnerAfterAt(
USER,
"2026-08-01T00:00:00.000Z"
);
await alertSearchCursorsDb.setEndorserAfterId(USER, ulid(12));
const loaded = await loadAlertSearchCursors(USER);
const urls: string[] = [];
await retrieveAlertSearch({
jwt: JWT,
...loaded,
endorserBaseUrl: "https://api.endorser.ch",
partnerBaseUrl: "https://partner-api.endorser.ch",
config: {
fetch: async (url) => {
urls.push(url);
if (url.includes("/api/partner/")) {
return jsonResponse(emptyPartnerBody());
}
return jsonResponse(emptyEndorserBody());
},
},
});
const endorserUrl = urls.find((u) => u.includes("/api/v2/report/")) ?? "";
const partnerUrl = urls.find((u) => u.includes("/api/partner/")) ?? "";
assert.equal(endorserUrl.includes(ulid(12)), true);
assert.equal(partnerUrl.includes("afterId=0"), false);
assert.equal(
decodeURIComponent(partnerUrl).includes("afterDate=2026-08-01T00:00:00.000Z"),
true
);
});
});