import type { RelayDeviceRegistrationRequest } from "@t3tools/contracts/relay";import { describe, expect, it } from "@effect/vitest";import type { SQL } from "drizzle-orm";import { PgDialect } from "drizzle-orm/pg-core";import * as Effect from "effect/Effect";import * as Layer from "effect/Layer"; import * as RelayDb from "../db.ts";import { relayLiveActivities, relayMobileDevices } from "../persistence/schema.ts";import * as Devices from "./Devices.ts"; const registration: RelayDeviceRegistrationRequest = { deviceId: "device-1" as RelayDeviceRegistrationRequest["deviceId"], label: "Julius's iPhone", platform: "ios", iosMajorVersion: 18, appVersion: "1.0.0" as RelayDeviceRegistrationRequest["appVersion"], bundleId: "com.t3tools.t3code.preview" as RelayDeviceRegistrationRequest["bundleId"], apsEnvironment: "production", pushToken: "apns-device-token" as RelayDeviceRegistrationRequest["pushToken"], pushToStartToken: "push-to-start-token" as RelayDeviceRegistrationRequest["pushToStartToken"], preferences: { notificationsEnabled: true, liveActivitiesEnabled: true, notifyOnApproval: true, notifyOnInput: true, notifyOnCompletion: true, notifyOnFailure: true, },}; describe("Devices", () => { it.effect("claims APNs tokens globally before upserting the current user device", () => { const calls: Array<string> = []; const updateSets: Array<Record<string, unknown>> = []; const updateConditions: Array<SQL> = []; const insertedValues: Array<Record<string, unknown>> = []; const dialect = new PgDialect(); const fakeDb = { update: (table: unknown) => { expect(table).toBe(relayMobileDevices); calls.push("update"); return { set: (values: Record<string, unknown>) => { updateSets.push(values); calls.push("update.set"); return { where: (condition: SQL) => { expect(condition).toBeDefined(); updateConditions.push(condition); calls.push("update.where"); return Effect.void; }, }; }, }; }, insert: (table: unknown) => { expect(table).toBe(relayMobileDevices); calls.push("insert"); return { values: (values: Record<string, unknown>) => { insertedValues.push(values); calls.push("insert.values"); return { onConflictDoUpdate: (config: unknown) => { expect(config).toBeDefined(); calls.push("insert.onConflictDoUpdate"); return Effect.void; }, }; }, }; }, } as unknown as RelayDb.RelayDb["Service"]; return Effect.gen(function* () { const devices = yield* Devices.Devices; yield* devices.register({ userId: "user-2", registration }); expect(calls).toEqual([ "update", "update.set", "update.where", "update", "update.set", "update.where", "insert", "insert.values", "insert.onConflictDoUpdate", ]); expect(updateSets).toEqual([ expect.objectContaining({ pushToken: null }), expect.objectContaining({ pushToStartToken: null }), ]); expect(updateConditions.map((condition) => dialect.sqlToQuery(condition))).toEqual([ { sql: '"relay_mobile_devices"."push_token" = $1', params: ["apns-device-token"], }, { sql: '"relay_mobile_devices"."push_to_start_token" = $1', params: ["push-to-start-token"], }, ]); expect(insertedValues).toEqual([ expect.objectContaining({ userId: "user-2", deviceId: "device-1", bundleId: "com.t3tools.t3code.preview", apsEnvironment: "production", pushToken: "apns-device-token", pushToStartToken: "push-to-start-token", }), ]); }).pipe( Effect.provide(Devices.layer.pipe(Layer.provide(Layer.succeed(RelayDb.RelayDb, fakeDb)))), ); }); it.effect("unregisters APNs state only for the current user device", () => { const calls: Array<string> = []; const deleteConditions: Array<SQL> = []; const dialect = new PgDialect(); const fakeDb = { delete: (table: unknown) => { calls.push(table === relayLiveActivities ? "delete.liveActivities" : "delete.devices"); return { where: (condition: SQL) => { expect(condition).toBeDefined(); deleteConditions.push(condition); calls.push("delete.where"); return Effect.void; }, }; }, } as unknown as RelayDb.RelayDb["Service"]; return Effect.gen(function* () { const devices = yield* Devices.Devices; yield* devices.unregister({ userId: "user-2", deviceId: "device-1" }); expect(calls).toEqual([ "delete.liveActivities", "delete.where", "delete.devices", "delete.where", ]); expect(deleteConditions.map((condition) => dialect.sqlToQuery(condition))).toEqual([ { sql: '(("relay_live_activities"."user_id" = $1) and ' + '("relay_live_activities"."device_id" = $2))', params: ["user-2", "device-1"], }, { sql: '(("relay_mobile_devices"."user_id" = $1) and ' + '("relay_mobile_devices"."device_id" = $2))', params: ["user-2", "device-1"], }, ]); }).pipe( Effect.provide(Devices.layer.pipe(Layer.provide(Layer.succeed(RelayDb.RelayDb, fakeDb)))), ); }); it.effect("lists safe notification state without exposing APNs tokens", () => { const dialect = new PgDialect(); let condition: SQL | null = null; const fakeDb = { select: () => ({ from: (table: unknown) => { expect(table).toBe(relayMobileDevices); return { where: (nextCondition: SQL) => { condition = nextCondition; return Effect.succeed([ { deviceId: "device-1", label: "Julius's iPhone", platform: "ios" as const, iosMajorVersion: 18, appVersion: "1.0.0", preferences: registration.preferences, updatedAt: "2026-06-01T00:00:00.000Z", }, ]); }, }; }, }), } as unknown as RelayDb.RelayDb["Service"]; return Effect.gen(function* () { const devices = yield* Devices.Devices; const listed = yield* devices.listForUser({ userId: "user-2" }); expect(condition).not.toBeNull(); expect(dialect.sqlToQuery(condition!)).toEqual({ sql: '"relay_mobile_devices"."user_id" = $1', params: ["user-2"], }); expect(listed).toEqual([ { deviceId: "device-1", label: "Julius's iPhone", platform: "ios", iosMajorVersion: 18, appVersion: "1.0.0", notifications: { enabled: true, notifyOnApproval: true, notifyOnInput: true, notifyOnCompletion: true, notifyOnFailure: true, }, liveActivities: { enabled: true, }, updatedAt: "2026-06-01T00:00:00.000Z", }, ]); }).pipe( Effect.provide(Devices.layer.pipe(Layer.provide(Layer.succeed(RelayDb.RelayDb, fakeDb)))), ); }); it.effect("identifies the failed device registration stage", () => { const cause = new Error("push-token claim failed"); const fakeDb = { update: () => ({ set: (values: Record<string, unknown>) => ({ where: () => ("pushToken" in values ? Effect.fail(cause) : Effect.void), }), }), } as unknown as RelayDb.RelayDb["Service"]; return Effect.gen(function* () { const devices = yield* Devices.Devices; const error = yield* devices.register({ userId: "user-2", registration }).pipe(Effect.flip); expect(error).toMatchObject({ userId: "user-2", deviceId: "device-1", stage: "claim-push-token", }); expect(error.cause).toBe(cause); expect(error.message).toBe( "Failed to persist mobile device registration for user-2/device-1 during claim-push-token.", ); }).pipe( Effect.provide(Devices.layer.pipe(Layer.provide(Layer.succeed(RelayDb.RelayDb, fakeDb)))), ); }); it.effect("identifies the failed device unregistration stage", () => { const cause = new Error("live activity delete failed"); const fakeDb = { delete: (table: unknown) => ({ where: () => (table === relayLiveActivities ? Effect.fail(cause) : Effect.void), }), } as unknown as RelayDb.RelayDb["Service"]; return Effect.gen(function* () { const devices = yield* Devices.Devices; const error = yield* devices .unregister({ userId: "user-2", deviceId: "device-1" }) .pipe(Effect.flip); expect(error).toMatchObject({ userId: "user-2", deviceId: "device-1", stage: "delete-live-activity", }); expect(error.cause).toBe(cause); expect(error.message).toBe( "Failed to unregister mobile device user-2/device-1 during delete-live-activity.", ); }).pipe( Effect.provide(Devices.layer.pipe(Layer.provide(Layer.succeed(RelayDb.RelayDb, fakeDb)))), ); }); it.effect("attaches the user to device list failures", () => { const cause = new Error("device list failed"); const fakeDb = { select: () => ({ from: () => ({ where: () => Effect.fail(cause), }), }), } as unknown as RelayDb.RelayDb["Service"]; return Effect.gen(function* () { const devices = yield* Devices.Devices; const error = yield* devices.listForUser({ userId: "user-2" }).pipe(Effect.flip); expect(error).toMatchObject({ userId: "user-2" }); expect(error.cause).toBe(cause); expect(error.message).toBe("Failed to list mobile devices for user-2."); }).pipe( Effect.provide(Devices.layer.pipe(Layer.provide(Layer.succeed(RelayDb.RelayDb, fakeDb)))), ); });});
import type { RelayDeviceRegistrationRequest } from "@t3tools/contracts/relay";
import { describe, expect, it } from "@effect/vitest";
import type { SQL } from "drizzle-orm";
import { PgDialect } from "drizzle-orm/pg-core";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import * as RelayDb from "../db.ts";
import { relayLiveActivities, relayMobileDevices } from "../persistence/schema.ts";
import * as Devices from "./Devices.ts";
const registration: RelayDeviceRegistrationRequest = {
deviceId: "device-1" as RelayDeviceRegistrationRequest["deviceId"],
label: "Julius's iPhone",
platform: "ios",
iosMajorVersion: 18,
appVersion: "1.0.0" as RelayDeviceRegistrationRequest["appVersion"],
bundleId: "com.t3tools.t3code.preview" as RelayDeviceRegistrationRequest["bundleId"],
apsEnvironment: "production",
pushToken: "apns-device-token" as RelayDeviceRegistrationRequest["pushToken"],
pushToStartToken: "push-to-start-token" as RelayDeviceRegistrationRequest["pushToStartToken"],
preferences: {
notificationsEnabled: true,
liveActivitiesEnabled: true,
notifyOnApproval: true,
notifyOnInput: true,
notifyOnCompletion: true,
notifyOnFailure: true,
},
};
describe("Devices", () => {
it.effect("claims APNs tokens globally before upserting the current user device", () => {
const calls: Array<string> = [];
const updateSets: Array<Record<string, unknown>> = [];
const updateConditions: Array<SQL> = [];
const insertedValues: Array<Record<string, unknown>> = [];
const dialect = new PgDialect();
const fakeDb = {
update: (table: unknown) => {
expect(table).toBe(relayMobileDevices);
calls.push("update");
return {
set: (values: Record<string, unknown>) => {
updateSets.push(values);
calls.push("update.set");
return {
where: (condition: SQL) => {
expect(condition).toBeDefined();
updateConditions.push(condition);
calls.push("update.where");
return Effect.void;
},
};
},
};
},
insert: (table: unknown) => {
expect(table).toBe(relayMobileDevices);
calls.push("insert");
return {
values: (values: Record<string, unknown>) => {
insertedValues.push(values);
calls.push("insert.values");
return {
onConflictDoUpdate: (config: unknown) => {
expect(config).toBeDefined();
calls.push("insert.onConflictDoUpdate");
return Effect.void;
},
};
},
};
},
} as unknown as RelayDb.RelayDb["Service"];
return Effect.gen(function* () {
const devices = yield* Devices.Devices;
yield* devices.register({ userId: "user-2", registration });
expect(calls).toEqual([
"update",
"update.set",
"update.where",
"update",
"update.set",
"update.where",
"insert",
"insert.values",
"insert.onConflictDoUpdate",
]);
expect(updateSets).toEqual([
expect.objectContaining({ pushToken: null }),
expect.objectContaining({ pushToStartToken: null }),
]);
expect(updateConditions.map((condition) => dialect.sqlToQuery(condition))).toEqual([
{
sql: '"relay_mobile_devices"."push_token" = $1',
params: ["apns-device-token"],
},
{
sql: '"relay_mobile_devices"."push_to_start_token" = $1',
params: ["push-to-start-token"],
},
]);
expect(insertedValues).toEqual([
expect.objectContaining({
userId: "user-2",
deviceId: "device-1",
bundleId: "com.t3tools.t3code.preview",
apsEnvironment: "production",
pushToken: "apns-device-token",
pushToStartToken: "push-to-start-token",
}),
]);
}).pipe(
Effect.provide(Devices.layer.pipe(Layer.provide(Layer.succeed(RelayDb.RelayDb, fakeDb)))),
);
});
it.effect("unregisters APNs state only for the current user device", () => {
const calls: Array<string> = [];
const deleteConditions: Array<SQL> = [];
const dialect = new PgDialect();
const fakeDb = {
delete: (table: unknown) => {
calls.push(table === relayLiveActivities ? "delete.liveActivities" : "delete.devices");
return {
where: (condition: SQL) => {
expect(condition).toBeDefined();
deleteConditions.push(condition);
calls.push("delete.where");
return Effect.void;
},
};
},
} as unknown as RelayDb.RelayDb["Service"];
return Effect.gen(function* () {
const devices = yield* Devices.Devices;
yield* devices.unregister({ userId: "user-2", deviceId: "device-1" });
expect(calls).toEqual([
"delete.liveActivities",
"delete.where",
"delete.devices",
"delete.where",
]);
expect(deleteConditions.map((condition) => dialect.sqlToQuery(condition))).toEqual([
{
sql:
'(("relay_live_activities"."user_id" = $1) and ' +
'("relay_live_activities"."device_id" = $2))',
params: ["user-2", "device-1"],
},
{
sql:
'(("relay_mobile_devices"."user_id" = $1) and ' +
'("relay_mobile_devices"."device_id" = $2))',
params: ["user-2", "device-1"],
},
]);
}).pipe(
Effect.provide(Devices.layer.pipe(Layer.provide(Layer.succeed(RelayDb.RelayDb, fakeDb)))),
);
});
it.effect("lists safe notification state without exposing APNs tokens", () => {
const dialect = new PgDialect();
let condition: SQL | null = null;
const fakeDb = {
select: () => ({
from: (table: unknown) => {
expect(table).toBe(relayMobileDevices);
return {
where: (nextCondition: SQL) => {
condition = nextCondition;
return Effect.succeed([
{
deviceId: "device-1",
label: "Julius's iPhone",
platform: "ios" as const,
iosMajorVersion: 18,
appVersion: "1.0.0",
preferences: registration.preferences,
updatedAt: "2026-06-01T00:00:00.000Z",
},
]);
},
};
},
}),
} as unknown as RelayDb.RelayDb["Service"];
return Effect.gen(function* () {
const devices = yield* Devices.Devices;
const listed = yield* devices.listForUser({ userId: "user-2" });
expect(condition).not.toBeNull();
expect(dialect.sqlToQuery(condition!)).toEqual({
sql: '"relay_mobile_devices"."user_id" = $1',
params: ["user-2"],
});
expect(listed).toEqual([
{
deviceId: "device-1",
label: "Julius's iPhone",
platform: "ios",
iosMajorVersion: 18,
appVersion: "1.0.0",
notifications: {
enabled: true,
notifyOnApproval: true,
notifyOnInput: true,
notifyOnCompletion: true,
notifyOnFailure: true,
},
liveActivities: {
enabled: true,
},
updatedAt: "2026-06-01T00:00:00.000Z",
},
]);
}).pipe(
Effect.provide(Devices.layer.pipe(Layer.provide(Layer.succeed(RelayDb.RelayDb, fakeDb)))),
);
});
it.effect("identifies the failed device registration stage", () => {
const cause = new Error("push-token claim failed");
const fakeDb = {
update: () => ({
set: (values: Record<string, unknown>) => ({
where: () => ("pushToken" in values ? Effect.fail(cause) : Effect.void),
}),
}),
} as unknown as RelayDb.RelayDb["Service"];
return Effect.gen(function* () {
const devices = yield* Devices.Devices;
const error = yield* devices.register({ userId: "user-2", registration }).pipe(Effect.flip);
expect(error).toMatchObject({
userId: "user-2",
deviceId: "device-1",
stage: "claim-push-token",
});
expect(error.cause).toBe(cause);
expect(error.message).toBe(
"Failed to persist mobile device registration for user-2/device-1 during claim-push-token.",
);
}).pipe(
Effect.provide(Devices.layer.pipe(Layer.provide(Layer.succeed(RelayDb.RelayDb, fakeDb)))),
);
});
it.effect("identifies the failed device unregistration stage", () => {
const cause = new Error("live activity delete failed");
const fakeDb = {
delete: (table: unknown) => ({
where: () => (table === relayLiveActivities ? Effect.fail(cause) : Effect.void),
}),
} as unknown as RelayDb.RelayDb["Service"];
return Effect.gen(function* () {
const devices = yield* Devices.Devices;
const error = yield* devices
.unregister({ userId: "user-2", deviceId: "device-1" })
.pipe(Effect.flip);
expect(error).toMatchObject({
userId: "user-2",
deviceId: "device-1",
stage: "delete-live-activity",
});
expect(error.cause).toBe(cause);
expect(error.message).toBe(
"Failed to unregister mobile device user-2/device-1 during delete-live-activity.",
);
}).pipe(
Effect.provide(Devices.layer.pipe(Layer.provide(Layer.succeed(RelayDb.RelayDb, fakeDb)))),
);
});
it.effect("attaches the user to device list failures", () => {
const cause = new Error("device list failed");
const fakeDb = {
select: () => ({
from: () => ({
where: () => Effect.fail(cause),
}),
}),
} as unknown as RelayDb.RelayDb["Service"];
return Effect.gen(function* () {
const devices = yield* Devices.Devices;
const error = yield* devices.listForUser({ userId: "user-2" }).pipe(Effect.flip);
expect(error).toMatchObject({ userId: "user-2" });
expect(error.cause).toBe(cause);
expect(error.message).toBe("Failed to list mobile devices for user-2.");
}).pipe(
Effect.provide(Devices.layer.pipe(Layer.provide(Layer.succeed(RelayDb.RelayDb, fakeDb)))),
);
});
});