1// @effect-diagnostics-next-line nodeBuiltinImport:off -- Effect's Crypto has no generateKeyPairSync.
2import * as NodeCrypto from "node:crypto";
3
4import { describe, expect, it } from "@effect/vitest";
5import * as Effect from "effect/Effect";
6import * as Redacted from "effect/Redacted";
7import * as Schema from "effect/Schema";
8
9import * as ApnsProviderTokens from "./ApnsProviderTokens.ts";
10
11const { privateKey } = NodeCrypto.generateKeyPairSync("ec", {
12 namedCurve: "prime256v1",
13 privateKeyEncoding: { type: "pkcs8", format: "pem" },
14 publicKeyEncoding: { type: "spki", format: "pem" },
15});
16
17const signingInput = {
18 teamId: "team-1",
19 keyId: "key-1",
20 privateKey: Redacted.make(privateKey),
21};
22
23const WINDOW = ApnsProviderTokens.APNS_JWT_REUSE_SECONDS;
24
25const decodeJwtPayload = Schema.decodeEffect(
26 Schema.fromJsonString(Schema.Struct({ iat: Schema.Number })),
27);
28
29describe("ApnsProviderTokens", () => {
30 it.effect("derives the byte-identical token across isolates within a window", () => {
31 ApnsProviderTokens.__resetApnsProviderTokenCacheForTest();
32 return Effect.gen(function* () {
33 const tokens = yield* ApnsProviderTokens.ApnsProviderTokens;
34 const first = yield* tokens.getJwt({ ...signingInput, issuedAtUnixSeconds: WINDOW + 10 });
35
36 // A fresh isolate has no cache; deterministic signing plus quantized
37 // iat must still reproduce the exact same JWT for the same window.
38 ApnsProviderTokens.__resetApnsProviderTokenCacheForTest();
39 const second = yield* tokens.getJwt({
40 ...signingInput,
41 issuedAtUnixSeconds: WINDOW * 2 - 1,
42 });
43 expect(second).toBe(first);
44
45 const payload = yield* decodeJwtPayload(
46 Buffer.from(first.split(".")[1]!, "base64url").toString("utf8"),
47 );
48 expect(payload.iat).toBe(WINDOW);
49 ApnsProviderTokens.__resetApnsProviderTokenCacheForTest();
50 }).pipe(Effect.provide(ApnsProviderTokens.layer));
51 });
52
53 it.effect("rolls to a new token at the window boundary", () => {
54 ApnsProviderTokens.__resetApnsProviderTokenCacheForTest();
55 return Effect.gen(function* () {
56 const tokens = yield* ApnsProviderTokens.ApnsProviderTokens;
57 const first = yield* tokens.getJwt({ ...signingInput, issuedAtUnixSeconds: WINDOW + 10 });
58 const next = yield* tokens.getJwt({ ...signingInput, issuedAtUnixSeconds: WINDOW * 2 });
59 expect(next).not.toBe(first);
60 ApnsProviderTokens.__resetApnsProviderTokenCacheForTest();
61 }).pipe(Effect.provide(ApnsProviderTokens.layer));
62 });
63
64 it.effect("serves repeat pushes from the isolate cache without re-signing", () => {
65 ApnsProviderTokens.__resetApnsProviderTokenCacheForTest();
66 return Effect.gen(function* () {
67 const tokens = yield* ApnsProviderTokens.ApnsProviderTokens;
68 const first = yield* tokens.getJwt({ ...signingInput, issuedAtUnixSeconds: WINDOW + 10 });
69 const again = yield* tokens.getJwt({ ...signingInput, issuedAtUnixSeconds: WINDOW + 500 });
70 // Deterministic signing makes equality hold either way; toBe on the
71 // exact string documents the cache contract.
72 expect(again).toBe(first);
73 ApnsProviderTokens.__resetApnsProviderTokenCacheForTest();
74 }).pipe(Effect.provide(ApnsProviderTokens.layer));
75 });
76});