1import { describe, expect, it } from "@effect/vitest";
2import { EnvironmentId, ThreadId } from "@t3tools/contracts";
3import type { RelayAgentActivityAggregateState } from "@t3tools/contracts/relay";
4import * as Redacted from "effect/Redacted";
5
6import {
7 makeApnsDeliveryJobPayload,
8 signApnsDeliveryJob,
9 verifySignedApnsDeliveryJob,
10} from "./apnsDeliveryJobs.ts";
11
12const secret = Redacted.make("queue-signing-secret");
13const aggregate: RelayAgentActivityAggregateState = {
14 title: "T3 Code",
15 subtitle: "Agent work in progress",
16 activeCount: 1,
17 updatedAt: "2026-05-25T00:00:00.000Z",
18 activities: [
19 {
20 environmentId: EnvironmentId.make("env"),
21 threadId: ThreadId.make("thread"),
22 projectTitle: "Project",
23 threadTitle: "Thread",
24 modelTitle: "gpt-5.4",
25 phase: "running",
26 status: "Working",
27 updatedAt: "2026-05-25T00:00:00.000Z",
28 deepLink: "/threads/env/thread",
29 },
30 ],
31};
32
33const notification = {
34 title: "Thread",
35 body: "Input: Project",
36 environmentId: "env",
37 threadId: "thread",
38 deepLink: "/threads/env/thread",
39};
40
41describe("apnsDeliveryJobs", () => {
42 it("rejects tampered signed queue jobs", () => {
43 const payload = makeApnsDeliveryJobPayload({
44 kind: "live_activity_end",
45 userId: "user-1",
46 deviceId: "device-1",
47 token: "token-1",
48 aggregate: null,
49 createdAt: "2026-05-25T00:00:00.000Z",
50 expiresAt: "2026-05-25T00:05:00.000Z",
51 jobId: "job-1",
52 });
53 const signed = signApnsDeliveryJob({ secret, payload });
54 const tampered = {
55 ...signed,
56 payload: {
57 ...signed.payload,
58 target: {
59 ...signed.payload.target,
60 token: "attacker-token",
61 },
62 },
63 };
64
65 const result = verifySignedApnsDeliveryJob({
66 secret,
67 job: tampered,
68 nowMs: 0,
69 });
70
71 expect(result).toMatchObject({
72 _tag: "ApnsDeliveryJobSignatureInvalid",
73 jobId: "job-1",
74 kind: "live_activity_end",
75 userId: "user-1",
76 deviceId: "device-1",
77 message: "Invalid signature for APNs delivery job job-1.",
78 });
79 });
80
81 it("rejects Live Activity start jobs without aggregate state", () => {
82 const payload = makeApnsDeliveryJobPayload({
83 kind: "live_activity_start",
84 userId: "user-1",
85 deviceId: "device-1",
86 token: "token-1",
87 aggregate: null,
88 createdAt: "2026-05-25T00:00:00.000Z",
89 expiresAt: "2026-05-25T00:05:00.000Z",
90 jobId: "job-start-invalid",
91 });
92 const signed = signApnsDeliveryJob({ secret, payload });
93
94 const result = verifySignedApnsDeliveryJob({
95 secret,
96 job: signed,
97 nowMs: 0,
98 });
99
100 expect(result).toMatchObject({
101 _tag: "ApnsDeliveryJobLiveActivityAggregateMissing",
102 jobId: "job-start-invalid",
103 kind: "live_activity_start",
104 userId: "user-1",
105 deviceId: "device-1",
106 message: "APNs live activity start job job-start-invalid requires an aggregate.",
107 });
108 });
109
110 it("rejects push notification jobs carrying aggregate state", () => {
111 const payload = makeApnsDeliveryJobPayload({
112 kind: "push_notification",
113 userId: "user-1",
114 deviceId: "device-1",
115 token: "token-1",
116 aggregate,
117 notification,
118 createdAt: "2026-05-25T00:00:00.000Z",
119 expiresAt: "2026-05-25T00:05:00.000Z",
120 jobId: "job-push-invalid",
121 });
122 const signed = signApnsDeliveryJob({ secret, payload });
123
124 const result = verifySignedApnsDeliveryJob({
125 secret,
126 job: signed,
127 nowMs: 0,
128 });
129
130 expect(result).toMatchObject({
131 _tag: "ApnsDeliveryJobPushNotificationAggregateUnexpected",
132 jobId: "job-push-invalid",
133 userId: "user-1",
134 deviceId: "device-1",
135 message: "APNs push notification job job-push-invalid must not carry aggregate state.",
136 });
137 });
138
139 it("accepts minimal kind-specific signed queue jobs", () => {
140 const pushPayload = makeApnsDeliveryJobPayload({
141 kind: "push_notification",
142 userId: "user-1",
143 deviceId: "device-1",
144 token: "token-1",
145 aggregate: null,
146 notification,
147 createdAt: "2026-05-25T00:00:00.000Z",
148 expiresAt: "2026-05-25T00:05:00.000Z",
149 jobId: "job-push-valid",
150 });
151 const liveActivityPayload = makeApnsDeliveryJobPayload({
152 kind: "live_activity_update",
153 userId: "user-1",
154 deviceId: "device-1",
155 token: "token-1",
156 aggregate,
157 createdAt: "2026-05-25T00:00:00.000Z",
158 expiresAt: "2026-05-25T00:05:00.000Z",
159 jobId: "job-live-valid",
160 });
161
162 expect(
163 verifySignedApnsDeliveryJob({
164 secret,
165 job: signApnsDeliveryJob({ secret, payload: pushPayload }),
166 nowMs: 0,
167 }),
168 ).toEqual(pushPayload);
169 expect(
170 verifySignedApnsDeliveryJob({
171 secret,
172 job: signApnsDeliveryJob({ secret, payload: liveActivityPayload }),
173 nowMs: 0,
174 }),
175 ).toEqual(liveActivityPayload);
176 });
177
178 it("rejects jobs with invalid or overlong time windows", () => {
179 const basePayload = makeApnsDeliveryJobPayload({
180 kind: "live_activity_end",
181 userId: "user-1",
182 deviceId: "device-1",
183 token: "token-1",
184 aggregate: null,
185 createdAt: "2026-05-25T00:00:00.000Z",
186 expiresAt: "2026-05-25T00:10:00.000Z",
187 jobId: "job-window",
188 });
189 const invalidCreatedAt = {
190 ...basePayload,
191 createdAt: "not-a-date",
192 };
193 const invertedWindow = {
194 ...basePayload,
195 expiresAt: "2026-05-24T23:59:59.000Z",
196 };
197 const overlongWindow = {
198 ...basePayload,
199 expiresAt: "2026-05-25T00:10:01.000Z",
200 };
201
202 expect(
203 verifySignedApnsDeliveryJob({
204 secret,
205 job: signApnsDeliveryJob({ secret, payload: invalidCreatedAt }),
206 nowMs: 0,
207 }),
208 ).toMatchObject({
209 _tag: "ApnsDeliveryJobCreatedAtInvalid",
210 jobId: "job-window",
211 kind: "live_activity_end",
212 userId: "user-1",
213 deviceId: "device-1",
214 createdAt: "not-a-date",
215 message: "APNs delivery job job-window has invalid creation time not-a-date.",
216 });
217 expect(
218 verifySignedApnsDeliveryJob({
219 secret,
220 job: signApnsDeliveryJob({ secret, payload: invertedWindow }),
221 nowMs: 0,
222 }),
223 ).toMatchObject({
224 _tag: "ApnsDeliveryJobTimeWindowInvalid",
225 jobId: "job-window",
226 kind: "live_activity_end",
227 userId: "user-1",
228 deviceId: "device-1",
229 createdAt: "2026-05-25T00:00:00.000Z",
230 expiresAt: "2026-05-24T23:59:59.000Z",
231 message:
232 "APNs delivery job job-window has invalid time window 2026-05-25T00:00:00.000Z to 2026-05-24T23:59:59.000Z.",
233 });
234 expect(
235 verifySignedApnsDeliveryJob({
236 secret,
237 job: signApnsDeliveryJob({ secret, payload: overlongWindow }),
238 nowMs: 0,
239 }),
240 ).toMatchObject({
241 _tag: "ApnsDeliveryJobTimeWindowTooLong",
242 jobId: "job-window",
243 kind: "live_activity_end",
244 userId: "user-1",
245 deviceId: "device-1",
246 createdAt: "2026-05-25T00:00:00.000Z",
247 expiresAt: "2026-05-25T00:10:01.000Z",
248 message:
249 "APNs delivery job job-window time window 2026-05-25T00:00:00.000Z to 2026-05-25T00:10:01.000Z is too long.",
250 });
251 });
252});