A Deno-compatible AT Protocol OAuth client that serves as a drop-in replacement for @atproto/oauth-client-node
1import { assert, assertEquals, assertInstanceOf } from "@std/assert";
2import {
3 AuthorizationError,
4 DPoPError,
5 HandleResolutionError,
6 InvalidHandleError,
7 InvalidStateError,
8 IssuerMismatchError,
9 MetadataValidationError,
10 OAuthError,
11 PDSDiscoveryError,
12 SessionError,
13 TokenExchangeError,
14 TokenValidationError,
15} from "../src/errors.ts";
16
17Deno.test("OAuthError", async (t) => {
18 await t.step("should create basic error with message", () => {
19 const error = new OAuthError("Test message");
20 assertEquals(error.message, "Test message");
21 assertEquals(error.name, "OAuthError");
22 assertEquals(error.cause, undefined);
23 });
24
25 await t.step("should create error with cause", () => {
26 const cause = new Error("Original error");
27 const error = new OAuthError("Test message", cause);
28 assertEquals(error.message, "Test message");
29 assertEquals(error.name, "OAuthError");
30 assertEquals(error.cause, cause);
31 });
32
33 await t.step("should be instance of Error", () => {
34 const error = new OAuthError("Test message");
35 assertInstanceOf(error, Error);
36 assertInstanceOf(error, OAuthError);
37 });
38});
39
40Deno.test("InvalidHandleError", async (t) => {
41 await t.step("should create error with handle in message", () => {
42 const error = new InvalidHandleError("invalid.handle");
43 assertEquals(error.message, "Invalid AT Protocol handle: invalid.handle");
44 assertEquals(error.name, "InvalidHandleError");
45 });
46
47 await t.step("should be instance of OAuthError", () => {
48 const error = new InvalidHandleError("invalid.handle");
49 assertInstanceOf(error, OAuthError);
50 assertInstanceOf(error, InvalidHandleError);
51 });
52});
53
54Deno.test("HandleResolutionError", async (t) => {
55 await t.step("should create error with handle in message", () => {
56 const error = new HandleResolutionError("test.handle");
57 assertEquals(error.message, "Failed to resolve handle test.handle to DID and PDS");
58 assertEquals(error.name, "HandleResolutionError");
59 assertEquals(error.cause, undefined);
60 });
61
62 await t.step("should create error with cause", () => {
63 const cause = new Error("Network error");
64 const error = new HandleResolutionError("test.handle", cause);
65 assertEquals(error.message, "Failed to resolve handle test.handle to DID and PDS");
66 assertEquals(error.name, "HandleResolutionError");
67 assertEquals(error.cause, cause);
68 });
69
70 await t.step("should be instance of OAuthError", () => {
71 const error = new HandleResolutionError("test.handle");
72 assertInstanceOf(error, OAuthError);
73 assertInstanceOf(error, HandleResolutionError);
74 });
75});
76
77Deno.test("PDSDiscoveryError", async (t) => {
78 await t.step("should create error with PDS URL in message", () => {
79 const error = new PDSDiscoveryError("https://example.com");
80 assertEquals(error.message, "Failed to discover OAuth endpoints for PDS: https://example.com");
81 assertEquals(error.name, "PDSDiscoveryError");
82 assertEquals(error.cause, undefined);
83 });
84
85 await t.step("should create error with cause", () => {
86 const cause = new Error("Discovery failed");
87 const error = new PDSDiscoveryError("https://example.com", cause);
88 assertEquals(error.message, "Failed to discover OAuth endpoints for PDS: https://example.com");
89 assertEquals(error.name, "PDSDiscoveryError");
90 assertEquals(error.cause, cause);
91 });
92
93 await t.step("should be instance of OAuthError", () => {
94 const error = new PDSDiscoveryError("https://example.com");
95 assertInstanceOf(error, OAuthError);
96 assertInstanceOf(error, PDSDiscoveryError);
97 });
98});
99
100Deno.test("TokenExchangeError", async (t) => {
101 await t.step("should create error with message", () => {
102 const error = new TokenExchangeError("Invalid grant");
103 assertEquals(error.message, "Token exchange failed: Invalid grant");
104 assertEquals(error.name, "TokenExchangeError");
105 assertEquals(error.errorCode, undefined);
106 assertEquals(error.cause, undefined);
107 });
108
109 await t.step("should create error with error code", () => {
110 const error = new TokenExchangeError("Invalid grant", "invalid_grant");
111 assertEquals(error.message, "Token exchange failed: Invalid grant");
112 assertEquals(error.name, "TokenExchangeError");
113 assertEquals(error.errorCode, "invalid_grant");
114 assertEquals(error.cause, undefined);
115 });
116
117 await t.step("should create error with error code and cause", () => {
118 const cause = new Error("HTTP 400");
119 const error = new TokenExchangeError("Invalid grant", "invalid_grant", cause);
120 assertEquals(error.message, "Token exchange failed: Invalid grant");
121 assertEquals(error.name, "TokenExchangeError");
122 assertEquals(error.errorCode, "invalid_grant");
123 assertEquals(error.cause, cause);
124 });
125
126 await t.step("should be instance of OAuthError", () => {
127 const error = new TokenExchangeError("Invalid grant");
128 assertInstanceOf(error, OAuthError);
129 assertInstanceOf(error, TokenExchangeError);
130 });
131});
132
133Deno.test("DPoPError", async (t) => {
134 await t.step("should create error with message", () => {
135 const error = new DPoPError("Key generation failed");
136 assertEquals(error.message, "DPoP operation failed: Key generation failed");
137 assertEquals(error.name, "DPoPError");
138 assertEquals(error.cause, undefined);
139 });
140
141 await t.step("should create error with cause", () => {
142 const cause = new Error("Crypto error");
143 const error = new DPoPError("Key generation failed", cause);
144 assertEquals(error.message, "DPoP operation failed: Key generation failed");
145 assertEquals(error.name, "DPoPError");
146 assertEquals(error.cause, cause);
147 });
148
149 await t.step("should be instance of OAuthError", () => {
150 const error = new DPoPError("Key generation failed");
151 assertInstanceOf(error, OAuthError);
152 assertInstanceOf(error, DPoPError);
153 });
154});
155
156Deno.test("SessionError", async (t) => {
157 await t.step("should create error with message", () => {
158 const error = new SessionError("Invalid session data");
159 assertEquals(error.message, "Session error: Invalid session data");
160 assertEquals(error.name, "SessionError");
161 assertEquals(error.cause, undefined);
162 });
163
164 await t.step("should create error with cause", () => {
165 const cause = new Error("Serialization failed");
166 const error = new SessionError("Invalid session data", cause);
167 assertEquals(error.message, "Session error: Invalid session data");
168 assertEquals(error.name, "SessionError");
169 assertEquals(error.cause, cause);
170 });
171
172 await t.step("should be instance of OAuthError", () => {
173 const error = new SessionError("Invalid session data");
174 assertInstanceOf(error, OAuthError);
175 assertInstanceOf(error, SessionError);
176 });
177});
178
179Deno.test("InvalidStateError", async (t) => {
180 await t.step("should create error with fixed message", () => {
181 const error = new InvalidStateError();
182 assertEquals(error.message, "Invalid or expired OAuth state parameter");
183 assertEquals(error.name, "InvalidStateError");
184 assertEquals(error.cause, undefined);
185 });
186
187 await t.step("should be instance of OAuthError", () => {
188 const error = new InvalidStateError();
189 assertInstanceOf(error, OAuthError);
190 assertInstanceOf(error, InvalidStateError);
191 });
192});
193
194Deno.test("AuthorizationError", async (t) => {
195 await t.step("should create error with error code only", () => {
196 const error = new AuthorizationError("access_denied");
197 assertEquals(error.message, "Authorization failed: access_denied");
198 assertEquals(error.name, "AuthorizationError");
199 });
200
201 await t.step("should create error with error code and description", () => {
202 const error = new AuthorizationError("access_denied", "User denied the request");
203 assertEquals(error.message, "Authorization failed: access_denied - User denied the request");
204 assertEquals(error.name, "AuthorizationError");
205 });
206
207 await t.step("should be instance of OAuthError", () => {
208 const error = new AuthorizationError("access_denied");
209 assertInstanceOf(error, OAuthError);
210 assertInstanceOf(error, AuthorizationError);
211 });
212});
213
214// New error types
215
216Deno.test("MetadataValidationError", async (t) => {
217 await t.step("should create error with message", () => {
218 const error = new MetadataValidationError("missing issuer field");
219 assertEquals(error.name, "MetadataValidationError");
220 assert(error.message.includes("missing issuer field"));
221 assertInstanceOf(error, OAuthError);
222 });
223
224 await t.step("should chain cause", () => {
225 const cause = new Error("parse error");
226 const error = new MetadataValidationError("invalid metadata", cause);
227 assertEquals(error.cause, cause);
228 });
229});
230
231Deno.test("IssuerMismatchError", async (t) => {
232 await t.step("should include expected and actual issuers", () => {
233 const error = new IssuerMismatchError("https://expected.com", "https://actual.com");
234 assertEquals(error.name, "IssuerMismatchError");
235 assertEquals(error.expected, "https://expected.com");
236 assertEquals(error.actual, "https://actual.com");
237 assert(error.message.includes("https://expected.com"));
238 assert(error.message.includes("https://actual.com"));
239 assertInstanceOf(error, OAuthError);
240 });
241});
242
243Deno.test("TokenValidationError", async (t) => {
244 await t.step("should create error with message", () => {
245 const error = new TokenValidationError("missing sub claim");
246 assertEquals(error.name, "TokenValidationError");
247 assert(error.message.includes("missing sub claim"));
248 assertInstanceOf(error, TokenExchangeError);
249 assertInstanceOf(error, OAuthError);
250 });
251});