1import { describe, expect, it } from "@effect/vitest";
2import * as DateTime from "effect/DateTime";
3import * as Effect from "effect/Effect";
4import * as Layer from "effect/Layer";
5import * as Option from "effect/Option";
6
7import * as RelayDb from "../db.ts";
8import { relayDpopProofs } from "../persistence/schema.ts";
9import * as DpopProofs from "./DpopProofs.ts";
10
11describe("DpopProofReplay", () => {
12 it.effect("consumes proof ids without pruning expired rows on the request path", () => {
13 const calls: Array<string> = [];
14 const insertedValues: Array<{
15 readonly thumbprint: string;
16 readonly jti: string;
17 readonly iat: number;
18 readonly expiresAt: string;
19 readonly createdAt: string;
20 }> = [];
21 const fakeDb = {
22 insert: (table: unknown) => {
23 expect(table).toBe(relayDpopProofs);
24 calls.push("insert");
25 return {
26 values: (values: (typeof insertedValues)[number]) => {
27 insertedValues.push(values);
28 calls.push("insert.values");
29 return {
30 onConflictDoNothing: () => {
31 calls.push("insert.onConflictDoNothing");
32 return {
33 returning: (selection: unknown) => {
34 expect(selection).toBeDefined();
35 calls.push("insert.returning");
36 return Effect.succeed([{ jti: values.jti }]);
37 },
38 };
39 },
40 };
41 },
42 };
43 },
44 } as unknown as RelayDb.RelayDb["Service"];
45
46 return Effect.gen(function* () {
47 const replay = yield* DpopProofs.DpopProofReplay;
48 const consumed = yield* replay.consume({
49 thumbprint: "thumbprint",
50 jti: "jti",
51 iat: 1_771_000_000,
52 expiresAt: Option.getOrThrow(DateTime.make("2026-05-25T12:00:00.000Z")),
53 });
54
55 expect(consumed).toBe(true);
56 expect(calls).toEqual([
57 "insert",
58 "insert.values",
59 "insert.onConflictDoNothing",
60 "insert.returning",
61 ]);
62 expect(insertedValues).toMatchObject([
63 {
64 thumbprint: "thumbprint",
65 jti: "jti",
66 iat: 1_771_000_000,
67 expiresAt: "2026-05-25T12:00:00.000Z",
68 },
69 ]);
70 }).pipe(
71 Effect.provide(DpopProofs.layer.pipe(Layer.provide(Layer.succeed(RelayDb.RelayDb, fakeDb)))),
72 );
73 });
74
75 it.effect("prunes expired proof rows from the maintenance path", () => {
76 const calls: Array<string> = [];
77 const fakeDb = {
78 delete: (table: unknown) => {
79 expect(table).toBe(relayDpopProofs);
80 calls.push("delete");
81 return {
82 where: (condition: unknown) => {
83 expect(condition).toBeDefined();
84 calls.push("delete.where");
85 return Effect.void;
86 },
87 };
88 },
89 } as unknown as RelayDb.RelayDb["Service"];
90
91 return Effect.gen(function* () {
92 const replay = yield* DpopProofs.DpopProofReplay;
93 yield* replay.pruneExpired;
94 expect(calls).toEqual(["delete", "delete.where"]);
95 }).pipe(
96 Effect.provide(DpopProofs.layer.pipe(Layer.provide(Layer.succeed(RelayDb.RelayDb, fakeDb)))),
97 );
98 });
99
100 it.effect("retains the prune cutoff and database failure", () => {
101 const cause = new Error("database unavailable");
102 const fakeDb = {
103 delete: (table: unknown) => {
104 expect(table).toBe(relayDpopProofs);
105 return {
106 where: () => Effect.fail(cause),
107 };
108 },
109 } as unknown as RelayDb.RelayDb["Service"];
110
111 return Effect.gen(function* () {
112 const replay = yield* DpopProofs.DpopProofReplay;
113 const error = yield* Effect.flip(replay.pruneExpired);
114
115 expect(error).toMatchObject({
116 _tag: "DpopProofReplayPersistenceError",
117 operation: "prune-expired",
118 });
119 expect(Date.parse(error.expiresBefore ?? "")).not.toBeNaN();
120 expect(error.cause).toBe(cause);
121 }).pipe(
122 Effect.provide(DpopProofs.layer.pipe(Layer.provide(Layer.succeed(RelayDb.RelayDb, fakeDb)))),
123 );
124 });
125});