1import { describe, expect, it } from "@effect/vitest";
2import * as Effect from "effect/Effect";
3import * as Layer from "effect/Layer";
4
5import * as RelayDb from "../db.ts";
6import {
7 relayEnvironmentLinks,
8 relayManagedEndpointAllocations,
9 relayManagedTunnelLimits,
10} from "../persistence/schema.ts";
11import * as ManagedTunnelLimits from "./ManagedTunnelLimits.ts";
12
13const layerWithDb = (db: RelayDb.RelayDb["Service"]) =>
14 ManagedTunnelLimits.layer.pipe(Layer.provide(Layer.succeed(RelayDb.RelayDb, db)));
15
16function makeFakeDb(input: {
17 readonly overrideRows?: Effect.Effect<ReadonlyArray<{ readonly maxTunnels: number }>, Error>;
18 readonly countRows?: Effect.Effect<ReadonlyArray<{ readonly activeTunnels: number }>, Error>;
19}) {
20 return {
21 select: () => ({
22 from: (table: unknown) => {
23 if (table === relayManagedTunnelLimits) {
24 return {
25 where: () => ({
26 limit: () => input.overrideRows ?? Effect.succeed([]),
27 }),
28 };
29 }
30 expect(table).toBe(relayManagedEndpointAllocations);
31 return {
32 innerJoin: (table: unknown, condition: unknown) => {
33 expect(table).toBe(relayEnvironmentLinks);
34 expect(condition).toBeDefined();
35 return {
36 where: (where: unknown) => {
37 expect(where).toBeDefined();
38 return input.countRows ?? Effect.succeed([{ activeTunnels: 0 }]);
39 },
40 };
41 },
42 };
43 },
44 }),
45 } as unknown as RelayDb.RelayDb["Service"];
46}
47
48describe("ManagedTunnelLimits", () => {
49 it.effect("allows provisioning below the default limit", () => {
50 const fakeDb = makeFakeDb({
51 countRows: Effect.succeed([
52 { activeTunnels: ManagedTunnelLimits.DEFAULT_MANAGED_TUNNEL_LIMIT - 1 },
53 ]),
54 });
55
56 return Effect.gen(function* () {
57 const limits = yield* ManagedTunnelLimits.ManagedTunnelLimits;
58 yield* limits.ensureCapacity({ userId: "user-1", environmentId: "environment-1" });
59 }).pipe(Effect.provide(layerWithDb(fakeDb)));
60 });
61
62 it.effect("rejects provisioning at the default limit of 3", () => {
63 const fakeDb = makeFakeDb({
64 countRows: Effect.succeed([{ activeTunnels: 3 }]),
65 });
66
67 return Effect.gen(function* () {
68 const limits = yield* ManagedTunnelLimits.ManagedTunnelLimits;
69 const error = yield* Effect.flip(
70 limits.ensureCapacity({ userId: "user-1", environmentId: "environment-1" }),
71 );
72
73 expect(error).toMatchObject({
74 _tag: "ManagedTunnelLimitExceeded",
75 userId: "user-1",
76 environmentId: "environment-1",
77 maxTunnels: 3,
78 activeTunnels: 3,
79 });
80 }).pipe(Effect.provide(layerWithDb(fakeDb)));
81 });
82
83 it.effect("honors a per-user override above the default", () => {
84 const fakeDb = makeFakeDb({
85 overrideRows: Effect.succeed([{ maxTunnels: 25 }]),
86 countRows: Effect.succeed([{ activeTunnels: 10 }]),
87 });
88
89 return Effect.gen(function* () {
90 const limits = yield* ManagedTunnelLimits.ManagedTunnelLimits;
91 yield* limits.ensureCapacity({ userId: "user-1", environmentId: "environment-1" });
92 }).pipe(Effect.provide(layerWithDb(fakeDb)));
93 });
94
95 it.effect("honors a per-user override below the default", () => {
96 const fakeDb = makeFakeDb({
97 overrideRows: Effect.succeed([{ maxTunnels: 1 }]),
98 countRows: Effect.succeed([{ activeTunnels: 1 }]),
99 });
100
101 return Effect.gen(function* () {
102 const limits = yield* ManagedTunnelLimits.ManagedTunnelLimits;
103 const error = yield* Effect.flip(
104 limits.ensureCapacity({ userId: "user-1", environmentId: "environment-1" }),
105 );
106
107 expect(error).toMatchObject({
108 _tag: "ManagedTunnelLimitExceeded",
109 maxTunnels: 1,
110 activeTunnels: 1,
111 });
112 }).pipe(Effect.provide(layerWithDb(fakeDb)));
113 });
114
115 it.effect("retains database failures with operation and user identity", () => {
116 const cause = new Error("database unavailable");
117 const fakeDb = makeFakeDb({
118 overrideRows: Effect.fail(cause),
119 });
120
121 return Effect.gen(function* () {
122 const limits = yield* ManagedTunnelLimits.ManagedTunnelLimits;
123 const error = yield* Effect.flip(
124 limits.ensureCapacity({ userId: "user-1", environmentId: "environment-1" }),
125 );
126
127 expect(error).toMatchObject({
128 _tag: "ManagedTunnelLimitPersistenceError",
129 operation: "load-limit",
130 userId: "user-1",
131 });
132 expect(error).toHaveProperty("cause", cause);
133 }).pipe(Effect.provide(layerWithDb(fakeDb)));
134 });
135
136 it.effect("retains count failures with operation and user identity", () => {
137 const cause = new Error("database unavailable");
138 const fakeDb = makeFakeDb({
139 countRows: Effect.fail(cause),
140 });
141
142 return Effect.gen(function* () {
143 const limits = yield* ManagedTunnelLimits.ManagedTunnelLimits;
144 const error = yield* Effect.flip(
145 limits.ensureCapacity({ userId: "user-1", environmentId: "environment-1" }),
146 );
147
148 expect(error).toMatchObject({
149 _tag: "ManagedTunnelLimitPersistenceError",
150 operation: "count-tunnels",
151 userId: "user-1",
152 });
153 }).pipe(Effect.provide(layerWithDb(fakeDb)));
154 });
155});