Skip to content

Commit 2a4b978

Browse files
committed
✅ server: verify locale-aware push notification content
1 parent 88a3fd1 commit 2a4b978

3 files changed

Lines changed: 135 additions & 0 deletions

File tree

server/test/hooks/manteca.test.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,7 @@ describe("manteca hook", () => {
173173
describe("when a deposit is detected", () => {
174174
it("converts to USDC", async () => {
175175
vi.spyOn(manteca, "convertBalanceToUsdc").mockResolvedValue();
176+
const sendPushNotification = vi.spyOn(onesignal, "sendPushNotification");
176177
const payload = {
177178
event: "DEPOSIT_DETECTED",
178179
data: {
@@ -193,6 +194,11 @@ describe("manteca hook", () => {
193194
expect(response.status).toBe(200);
194195
await expect(response.json()).resolves.toStrictEqual({ code: "ok" });
195196
expect(manteca.convertBalanceToUsdc).toHaveBeenCalledWith("456", "ARS");
197+
expect(sendPushNotification).toHaveBeenCalledWith({
198+
userId: account,
199+
headings: { en: "Deposited funds", es: "Fondos depositados" }, // cspell:ignore Fondos depositados
200+
contents: { en: "1,000 ARS deposited", es: "1.000 ARS depositados" }, // cspell:ignore depositados
201+
});
196202
});
197203

198204
it("returns ok if credential does not exist", async () => {

server/test/hooks/panda.test.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ import { proposalManager } from "@exactly/plugin/deploy.json";
4747
import database, { cards, credentials, transactions } from "../../database";
4848
import app from "../../hooks/panda";
4949
import keeper from "../../utils/keeper";
50+
import * as onesignal from "../../utils/onesignal";
5051
import * as panda from "../../utils/panda";
5152
import publicClient from "../../utils/publicClient";
5253
import * as sardine from "../../utils/sardine";
@@ -601,6 +602,44 @@ describe("card operations", () => {
601602
expect(response.status).toBe(200);
602603
});
603604

605+
it("sends locale-aware card purchase notification", async () => {
606+
const sendPushNotificationSpy = vi
607+
.spyOn(onesignal, "sendPushNotification")
608+
.mockImplementation(() => Promise.resolve(undefined as never));
609+
// @ts-expect-error mock implementation
610+
vi.spyOn(keeper, "exaSend").mockImplementation(async (...args) => {
611+
await args[2]?.onHash?.(zeroHash as Hash);
612+
});
613+
const localAmount = 123_456;
614+
const cardId = "locale-notify";
615+
await database.insert(cards).values([{ id: cardId, credentialId: "cred", lastFour: "9999", mode: 0 }]);
616+
617+
const response = await appClient.index.$post({
618+
...authorization,
619+
json: {
620+
...authorization.json,
621+
action: "created",
622+
body: {
623+
...authorization.json.body,
624+
id: cardId,
625+
spend: { ...authorization.json.body.spend, cardId, localAmount, localCurrency: "ars" },
626+
},
627+
},
628+
});
629+
630+
expect(response.status).toBe(200);
631+
await vi.waitFor(() => expect(sendPushNotificationSpy).toHaveBeenCalled());
632+
const call = sendPushNotificationSpy.mock.calls[0]?.[0];
633+
expect(call?.headings).toStrictEqual({ en: "Card purchase", es: "Compra con tarjeta" }); // cspell:ignore Compra tarjeta
634+
const enAmount = (localAmount / 100).toLocaleString("en-US", { style: "currency", currency: "ars" });
635+
const esAmount = (localAmount / 100).toLocaleString("es-AR", { style: "currency", currency: "ars" });
636+
expect(enAmount).not.toBe(esAmount);
637+
expect(call?.contents).toStrictEqual({
638+
en: `${enAmount} at 99999. Paid with USDC`,
639+
es: `${esAmount} en 99999. Pagado con USDC`, // cspell:ignore Pagado
640+
});
641+
});
642+
604643
it("fails with transaction timeout", async () => {
605644
const error = new Error("timeout");
606645
const track = vi.spyOn(segment, "track").mockReturnValue();
@@ -896,6 +935,7 @@ describe("card operations", () => {
896935
afterEach(() => vi.restoreAllMocks());
897936

898937
it("handles reversal", async () => {
938+
const sendPushNotification = vi.spyOn(onesignal, "sendPushNotification");
899939
const amount = 2073;
900940
const cardId = "card";
901941
await keeper.exaSend(
@@ -954,6 +994,14 @@ describe("card operations", () => {
954994

955995
expect(deposit?.args.assets).toBe(BigInt(amount * 1e4));
956996
expect(response.status).toBe(200);
997+
expect(sendPushNotification).toHaveBeenCalledWith({
998+
userId: account,
999+
headings: { en: "Refund processed", es: "Reembolso procesado" }, // cspell:ignore Reembolso procesado
1000+
contents: {
1001+
en: "20.73 USDC from 99999 have been refunded to your account",
1002+
es: "20,73 USDC de 99999 fueron reembolsados a tu cuenta", // cspell:ignore reembolsados cuenta fueron
1003+
},
1004+
});
9571005
});
9581006

9591007
it("returns ok on reversal replay", async () => {

server/test/i18n.test.ts

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { describe, expect, it } from "vitest";
2+
3+
import t, { formatAmount } from "../i18n";
4+
5+
describe("formatAmount()", () => {
6+
describe("number input", () => {
7+
it("preserves sub-micro values", () => {
8+
expect(formatAmount(0.000_000_9)).toStrictEqual({ en: "0.0000009", es: "0,0000009" });
9+
});
10+
11+
it("preserves 6 fractional digits", () => {
12+
expect(formatAmount(0.000_001)).toStrictEqual({ en: "0.000001", es: "0,000001" });
13+
});
14+
15+
it("formats regular decimals", () => {
16+
expect(formatAmount(99.973)).toStrictEqual({ en: "99.973", es: "99,973" });
17+
});
18+
19+
it("formats integers", () => {
20+
expect(formatAmount(5)).toStrictEqual({ en: "5", es: "5" });
21+
});
22+
23+
it("formats thousands with separator", () => {
24+
expect(formatAmount(1000)).toStrictEqual({ en: "1,000", es: "1.000" });
25+
});
26+
});
27+
28+
describe("string input", () => {
29+
it("formats regular decimals", () => {
30+
expect(formatAmount("99.973")).toStrictEqual({ en: "99.973", es: "99,973" });
31+
});
32+
33+
it("preserves sub-micro values and trims trailing zeros", () => {
34+
expect(formatAmount("0.00000090")).toStrictEqual({ en: "0.0000009", es: "0,0000009" });
35+
});
36+
37+
it("formats integers", () => {
38+
expect(formatAmount("5")).toStrictEqual({ en: "5", es: "5" });
39+
});
40+
});
41+
});
42+
43+
describe("t()", () => {
44+
it("returns en and es translations with no options", () => {
45+
const result = t("Card purchase");
46+
expect(result).toStrictEqual({ en: "Card purchase", es: "Compra con tarjeta" }); // cspell:ignore Compra tarjeta
47+
});
48+
49+
it("interpolates plain string values into both languages", () => {
50+
const result = t("{{localAmount}} at {{merchantName}}. Paid with USDC", {
51+
localAmount: "$1,234.56",
52+
merchantName: "Store",
53+
});
54+
expect(result).toStrictEqual({
55+
en: "$1,234.56 at Store. Paid with USDC",
56+
es: "$1,234.56 en Store. Pagado con USDC", // cspell:ignore Pagado
57+
});
58+
});
59+
60+
it("resolves per-language objects in interpolation values", () => {
61+
const result = t("{{localAmount}} at {{merchantName}}. Paid with USDC", {
62+
localAmount: { en: "$1,234.56", es: "$ 1.234,56" },
63+
merchantName: "Store",
64+
});
65+
expect(result).toStrictEqual({
66+
en: "$1,234.56 at Store. Paid with USDC",
67+
es: "$ 1.234,56 en Store. Pagado con USDC", // cspell:ignore Pagado
68+
});
69+
});
70+
71+
it("mixes per-language and plain values", () => {
72+
const result = t("{{localAmount}} at {{merchantName}}. Paid with credit", {
73+
localAmount: { en: "A", es: "B" },
74+
merchantName: "Store",
75+
});
76+
expect(result).toStrictEqual({
77+
en: "A at Store. Paid with credit",
78+
es: "B en Store. Pagado con crédito", // cspell:ignore Pagado crédito
79+
});
80+
});
81+
});

0 commit comments

Comments
 (0)