1import { EnvironmentAuthInvalidError } from "@t3tools/contracts";
2import {
3 RelayAuthInvalidError,
4 RelayEnvironmentConnectNotAuthorizedError,
5 RelayEnvironmentEndpointTimedOutError,
6} from "@t3tools/contracts/relay";
7import { describe, expect, it } from "@effect/vitest";
8
9import {
10 mapManagedRelayError,
11 mapRemoteDpopEnvironmentError,
12 mapRemoteEnvironmentError,
13} from "./errors.ts";
14import { DPOP_RETRY_HINT, DPOP_UNKNOWN_HINT } from "../relay/errorPresentation.ts";
15import { ManagedRelayRequestFailedError } from "../relay/managedRelay.ts";
16import { NETWORK_BLOCKING_HINT } from "../errors/network.ts";
17import { RemoteEnvironmentAuthFetchError, RemoteEnvironmentAuthTimeoutError } from "../rpc/http.ts";
18
19describe("mapManagedRelayError", () => {
20 it("keeps a timeout reported by the relay distinct from a local network failure", () => {
21 const relayError = new RelayEnvironmentEndpointTimedOutError({
22 code: "environment_endpoint_timed_out",
23 traceId: "trace-server-timeout",
24 });
25 const mapped = mapManagedRelayError(
26 new ManagedRelayRequestFailedError({
27 action: "connect relay environment",
28 cause: relayError,
29 relayError,
30 }),
31 );
32 expect(mapped).toMatchObject({
33 reason: "timeout",
34 detail: "Relay timed out while contacting the environment endpoint.",
35 traceId: "trace-server-timeout",
36 });
37 });
38
39 it("retries a tunnel that is still being set up, and blocks other refusals", () => {
40 const mapConnectRefusal = (reason: RelayEnvironmentConnectNotAuthorizedError["reason"]) => {
41 const relayError = new RelayEnvironmentConnectNotAuthorizedError({
42 code: "environment_connect_not_authorized",
43 reason,
44 traceId: "trace-connect-refused",
45 });
46 return mapManagedRelayError(
47 new ManagedRelayRequestFailedError({
48 action: "connect relay environment",
49 cause: relayError,
50 relayError,
51 }),
52 );
53 };
54
55 expect(mapConnectRefusal("managed_endpoint_allocation_not_ready")).toMatchObject({
56 _tag: "ConnectionTransientError",
57 reason: "endpoint-unavailable",
58 detail: "The environment's T3 Connect tunnel is still being set up.",
59 traceId: "trace-connect-refused",
60 });
61 expect(mapConnectRefusal("endpoint_provider_not_managed")).toMatchObject({
62 _tag: "ConnectionBlockedError",
63 reason: "permission",
64 detail: expect.stringContaining("turn T3 Connect on"),
65 });
66 expect(mapConnectRefusal(undefined)).toMatchObject({
67 _tag: "ConnectionBlockedError",
68 reason: "permission",
69 });
70 });
71
72 it("presents clock skew as one possible cause for a generic DPoP error", () => {
73 const mapped = mapManagedRelayError(
74 new ManagedRelayRequestFailedError({
75 action: "connect relay environment",
76 cause: new Error("request failed"),
77 relayError: new RelayAuthInvalidError({
78 code: "auth_invalid",
79 reason: "invalid_dpop",
80 traceId: "trace-1",
81 }),
82 traceId: "trace-1",
83 }),
84 );
85
86 expect(mapped).toMatchObject({
87 _tag: "ConnectionBlockedError",
88 reason: "authentication",
89 detail: `Relay rejected the DPoP proof. ${DPOP_UNKNOWN_HINT}`,
90 traceId: "trace-1",
91 });
92 });
93
94 it("uses a neutral hint when the relay identifies a non-clock DPoP error", () => {
95 const mapped = mapManagedRelayError(
96 new ManagedRelayRequestFailedError({
97 action: "connect relay environment",
98 cause: new Error("request failed"),
99 relayError: new RelayAuthInvalidError({
100 code: "auth_invalid",
101 reason: "invalid_dpop",
102 dpopFailureReason: "key_mismatch",
103 traceId: "trace-1",
104 }),
105 }),
106 );
107
108 expect(mapped.message).toBe(`Relay rejected the DPoP proof. ${DPOP_RETRY_HINT}`);
109 });
110});
111
112describe("mapRemoteDpopEnvironmentError", () => {
113 it("keeps relay descriptor auth failures distinct from DPoP proof failures", () => {
114 const error = new EnvironmentAuthInvalidError({
115 code: "auth_invalid",
116 reason: "invalid_credential",
117 traceId: "trace-descriptor",
118 });
119 expect(mapRemoteEnvironmentError(error, "relay").message).toBe(
120 "The environment credential is invalid.",
121 );
122 expect(mapRemoteDpopEnvironmentError(error).message).toBe(
123 `The environment credential is invalid. ${DPOP_UNKNOWN_HINT}`,
124 );
125 });
126
127 it.each([
128 new RemoteEnvironmentAuthFetchError({
129 message: "Failed to fetch remote environment endpoint.",
130 cause: new TypeError("Failed to fetch"),
131 }),
132 new RemoteEnvironmentAuthTimeoutError("https://environment.example.test", 10_000),
133 ])("suggests another network when the relay endpoint cannot be reached: $_tag", (error) => {
134 const mapped = mapRemoteDpopEnvironmentError(error);
135 expect(mapped).toMatchObject({
136 _tag: "ConnectionTransientError",
137 detail: `${error.message} ${NETWORK_BLOCKING_HINT}`,
138 });
139 });
140
141 it("does not present a generic environment auth error as confirmed clock skew", () => {
142 const mapped = mapRemoteDpopEnvironmentError(
143 new EnvironmentAuthInvalidError({
144 code: "auth_invalid",
145 reason: "invalid_credential",
146 traceId: "trace-1",
147 }),
148 );
149
150 expect(mapped.message).toBe(`The environment credential is invalid. ${DPOP_UNKNOWN_HINT}`);
151 });
152
153 it("uses a neutral hint for a non-clock DPoP error from a new server", () => {
154 const mapped = mapRemoteDpopEnvironmentError(
155 new EnvironmentAuthInvalidError({
156 code: "auth_invalid",
157 reason: "invalid_credential",
158 dpopFailureReason: "key_mismatch",
159 traceId: "trace-1",
160 }),
161 );
162
163 expect(mapped.message).toBe(`The environment credential is invalid. ${DPOP_RETRY_HINT}`);
164 });
165});