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