Keep them beside the existing SMS alertSearch tests and update imports so they still load src/.
280 lines
9.6 KiB
TypeScript
280 lines
9.6 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import { describe, it } from "node:test";
|
|
import {
|
|
ALERT_SEARCH_DIGEST_BUCKETS,
|
|
alertSearchDigestDebugSummary,
|
|
buildAlertSearchDigest,
|
|
} from "../../src/alertSearch/digest.js";
|
|
import { emptyEndorserData, emptyPartnerData } from "../../src/alertSearch/client.js";
|
|
import type { AlertSearchSourceResult, RetrieveAlertSearchResult } from "../../src/alertSearch/retrieve.js";
|
|
import type {
|
|
AlertSearchClaimRecord,
|
|
AlertSearchFailureReason,
|
|
AlertSearchJwtWithClaimRecord,
|
|
AlertSearchPlanRecord,
|
|
AlertSearchProfileRecord,
|
|
AlertSearchQueryOutcome,
|
|
CombinedAlertSearchData,
|
|
EndorserAlertSearchData,
|
|
PartnerAlertSearchData,
|
|
} from "../../src/alertSearch/types.js";
|
|
|
|
function ulid(n: number): string {
|
|
return `01H${String(n).padStart(23, "0")}`;
|
|
}
|
|
|
|
function claim(n: number): AlertSearchClaimRecord {
|
|
return {
|
|
id: ulid(n),
|
|
issuedAt: "2026-01-01T00:00:00Z",
|
|
issuer: `did:ethr:0x${n}`,
|
|
};
|
|
}
|
|
|
|
function jwtClaim(n: number): AlertSearchJwtWithClaimRecord {
|
|
return { ...claim(n), claim: `claim-${n}` };
|
|
}
|
|
|
|
function plan(n: number): AlertSearchPlanRecord {
|
|
return { handleId: `plan-${n}`, jwtId: ulid(n), name: `Plan ${n}` };
|
|
}
|
|
|
|
function profile(n: number): AlertSearchProfileRecord {
|
|
return {
|
|
issuerDid: `did:ethr:0xp${n}`,
|
|
description: `profile-${n}`,
|
|
updatedAt: "2026-03-01T00:00:00.000Z",
|
|
rowId: n,
|
|
};
|
|
}
|
|
|
|
function endorserResult(
|
|
outcome: AlertSearchQueryOutcome,
|
|
data: EndorserAlertSearchData = emptyEndorserData(),
|
|
extra?: Partial<AlertSearchSourceResult<EndorserAlertSearchData>>
|
|
): AlertSearchSourceResult<EndorserAlertSearchData> {
|
|
return { outcome, pageCount: 1, data, ...extra };
|
|
}
|
|
|
|
function partnerResult(
|
|
outcome: AlertSearchQueryOutcome,
|
|
data: PartnerAlertSearchData = emptyPartnerData(),
|
|
extra?: Partial<AlertSearchSourceResult<PartnerAlertSearchData>>
|
|
): AlertSearchSourceResult<PartnerAlertSearchData> {
|
|
return { outcome, pageCount: 1, data, ...extra };
|
|
}
|
|
|
|
function retrieved(
|
|
endorser: AlertSearchSourceResult<EndorserAlertSearchData>,
|
|
partner: AlertSearchSourceResult<PartnerAlertSearchData>
|
|
): RetrieveAlertSearchResult {
|
|
const data: CombinedAlertSearchData = {
|
|
...endorser.data,
|
|
...partner.data,
|
|
};
|
|
return { data, empty: false, endorser, partner };
|
|
}
|
|
|
|
function populatedEndorser(): EndorserAlertSearchData {
|
|
return {
|
|
claims: [claim(1), claim(2), claim(3)],
|
|
personalPlanContributions: [jwtClaim(10)],
|
|
trackedPlanUpdates: [plan(20)],
|
|
trackedPlanClaims: [jwtClaim(30), jwtClaim(31)],
|
|
plansNearby: [plan(40)],
|
|
};
|
|
}
|
|
|
|
describe("buildAlertSearchDigest", () => {
|
|
it("preserves all six buckets and their records when populated", () => {
|
|
const endorserData = populatedEndorser();
|
|
const partnerData = {
|
|
profilesNearby: [profile(1), profile(2)],
|
|
};
|
|
const digest = buildAlertSearchDigest(
|
|
retrieved(
|
|
endorserResult("success", endorserData),
|
|
partnerResult("success", partnerData)
|
|
)
|
|
);
|
|
assert.equal(digest.completed, true);
|
|
assert.equal(digest.hasUpdates, true);
|
|
assert.equal(digest.counts.claims, 3);
|
|
assert.equal(digest.counts.personalPlanContributions, 1);
|
|
assert.equal(digest.counts.trackedPlanUpdates, 1);
|
|
assert.equal(digest.counts.trackedPlanClaims, 2);
|
|
assert.equal(digest.counts.plansNearby, 1);
|
|
assert.equal(digest.counts.profilesNearby, 2);
|
|
assert.equal(digest.totalCount, 10);
|
|
assert.deepEqual(digest.records.claims, endorserData.claims);
|
|
assert.deepEqual(
|
|
digest.records.personalPlanContributions,
|
|
endorserData.personalPlanContributions
|
|
);
|
|
assert.deepEqual(digest.records.trackedPlanUpdates, endorserData.trackedPlanUpdates);
|
|
assert.deepEqual(digest.records.trackedPlanClaims, endorserData.trackedPlanClaims);
|
|
assert.deepEqual(digest.records.plansNearby, endorserData.plansNearby);
|
|
assert.deepEqual(digest.records.profilesNearby, partnerData.profilesNearby);
|
|
assert.equal(digest.records.claims[0], endorserData.claims[0]);
|
|
assert.notEqual(digest.records.claims, endorserData.claims);
|
|
});
|
|
|
|
it("supports a single populated bucket", () => {
|
|
const claims = [claim(1), claim(2)];
|
|
const digest = buildAlertSearchDigest(
|
|
retrieved(
|
|
endorserResult("success", { ...emptyEndorserData(), claims }),
|
|
partnerResult("empty")
|
|
)
|
|
);
|
|
assert.equal(digest.completed, true);
|
|
assert.equal(digest.hasUpdates, true);
|
|
assert.equal(digest.totalCount, 2);
|
|
assert.equal(digest.counts.claims, 2);
|
|
for (const bucket of ALERT_SEARCH_DIGEST_BUCKETS) {
|
|
if (bucket !== "claims") assert.equal(digest.counts[bucket], 0);
|
|
}
|
|
assert.equal(digest.records.claims.length, 2);
|
|
assert.equal(digest.records.claims[1].id, ulid(2));
|
|
});
|
|
|
|
it("supports multiple populated buckets without dropping others", () => {
|
|
const digest = buildAlertSearchDigest(
|
|
retrieved(
|
|
endorserResult("success", {
|
|
...emptyEndorserData(),
|
|
claims: [claim(1), claim(2), claim(3)],
|
|
trackedPlanUpdates: [plan(20)],
|
|
}),
|
|
partnerResult("success", { profilesNearby: [profile(1), profile(2)] })
|
|
)
|
|
);
|
|
assert.equal(digest.completed, true);
|
|
assert.equal(digest.hasUpdates, true);
|
|
assert.equal(digest.totalCount, 6);
|
|
assert.equal(digest.counts.claims, 3);
|
|
assert.equal(digest.counts.trackedPlanUpdates, 1);
|
|
assert.equal(digest.counts.profilesNearby, 2);
|
|
assert.equal(digest.records.claims.length, 3);
|
|
assert.equal(digest.records.trackedPlanUpdates[0].handleId, "plan-20");
|
|
assert.equal(digest.records.profilesNearby[1].description, "profile-2");
|
|
});
|
|
|
|
it("treats a successful empty retrieve as complete with no updates", () => {
|
|
const digest = buildAlertSearchDigest(
|
|
retrieved(endorserResult("empty"), partnerResult("empty"))
|
|
);
|
|
assert.equal(digest.completed, true);
|
|
assert.equal(digest.hasUpdates, false);
|
|
assert.equal(digest.totalCount, 0);
|
|
for (const bucket of ALERT_SEARCH_DIGEST_BUCKETS) {
|
|
assert.equal(digest.counts[bucket], 0);
|
|
assert.equal(digest.records[bucket].length, 0);
|
|
}
|
|
assert.equal(
|
|
alertSearchDigestDebugSummary(digest).includes("You have"),
|
|
false
|
|
);
|
|
});
|
|
|
|
it("does not mark a digest complete when Endorser succeeds and Partner fails", () => {
|
|
const digest = buildAlertSearchDigest(
|
|
retrieved(
|
|
endorserResult("success", { ...emptyEndorserData(), claims: [claim(1)] }),
|
|
partnerResult("auth", emptyPartnerData(), {
|
|
reason: "auth",
|
|
message: "HTTP 401",
|
|
})
|
|
)
|
|
);
|
|
assert.equal(digest.completed, false);
|
|
assert.equal(digest.hasUpdates, false);
|
|
assert.equal(digest.endorser.completed, true);
|
|
assert.equal(digest.endorser.outcome, "success");
|
|
assert.equal(digest.partner.completed, false);
|
|
assert.equal(digest.partner.outcome, "auth");
|
|
assert.equal(digest.partner.reason, "auth");
|
|
assert.equal(digest.records.claims.length, 1);
|
|
assert.equal(digest.totalCount, 1);
|
|
});
|
|
|
|
it("does not mark a digest complete when Partner succeeds and Endorser fails", () => {
|
|
const digest = buildAlertSearchDigest(
|
|
retrieved(
|
|
endorserResult("network", emptyEndorserData(), {
|
|
reason: "network",
|
|
message: "fetch failed",
|
|
}),
|
|
partnerResult("success", { profilesNearby: [profile(1)] })
|
|
)
|
|
);
|
|
assert.equal(digest.completed, false);
|
|
assert.equal(digest.hasUpdates, false);
|
|
assert.equal(digest.endorser.outcome, "network");
|
|
assert.equal(digest.partner.outcome, "success");
|
|
assert.equal(digest.records.profilesNearby.length, 1);
|
|
});
|
|
|
|
it("does not treat pagination or other incomplete outcomes as a completed digest", () => {
|
|
const incomplete: AlertSearchFailureReason[] = [
|
|
"pagination",
|
|
"auth",
|
|
"timeout",
|
|
"network",
|
|
"malformed",
|
|
"http",
|
|
];
|
|
for (const outcome of incomplete) {
|
|
const digest = buildAlertSearchDigest(
|
|
retrieved(
|
|
endorserResult(outcome, emptyEndorserData(), { reason: outcome }),
|
|
partnerResult("empty")
|
|
)
|
|
);
|
|
assert.equal(digest.completed, false, outcome);
|
|
assert.equal(digest.hasUpdates, false, outcome);
|
|
assert.equal(digest.endorser.outcome, outcome);
|
|
}
|
|
});
|
|
|
|
it("sums counts across all six buckets", () => {
|
|
const digest = buildAlertSearchDigest(
|
|
retrieved(
|
|
endorserResult("success", populatedEndorser()),
|
|
partnerResult("success", { profilesNearby: [profile(1), profile(2)] })
|
|
)
|
|
);
|
|
const sum = ALERT_SEARCH_DIGEST_BUCKETS.reduce(
|
|
(n, bucket) => n + digest.counts[bucket],
|
|
0
|
|
);
|
|
assert.equal(digest.totalCount, sum);
|
|
assert.equal(digest.totalCount, 10);
|
|
});
|
|
|
|
it("preserves underlying records rather than reducing them to counts", () => {
|
|
const extra = { claimType: "AgreeAction", subject: "did:ethr:0xabc" };
|
|
const row = { ...claim(7), ...extra };
|
|
const digest = buildAlertSearchDigest(
|
|
retrieved(
|
|
endorserResult("success", { ...emptyEndorserData(), claims: [row] }),
|
|
partnerResult("empty")
|
|
)
|
|
);
|
|
assert.equal(digest.counts.claims, 1);
|
|
assert.equal(digest.records.claims[0].id, ulid(7));
|
|
assert.equal(digest.records.claims[0].claimType, "AgreeAction");
|
|
assert.equal(digest.records.claims[0].subject, "did:ethr:0xabc");
|
|
assert.deepEqual(digest.records.claims[0], row);
|
|
});
|
|
|
|
it("is not notification-worthy when total count is zero", () => {
|
|
const digest = buildAlertSearchDigest(
|
|
retrieved(endorserResult("success"), partnerResult("success"))
|
|
);
|
|
assert.equal(digest.completed, true);
|
|
assert.equal(digest.totalCount, 0);
|
|
assert.equal(digest.hasUpdates, false);
|
|
});
|
|
});
|