Auto-indexing service and GraphQL API for AT Protocol Records
quickslice.slices.network/
atproto
gleam
graphql
1import atproto_auth
2import database/executor.{type Executor, Int, Text}
3import database/repositories/oauth_access_tokens
4import database/repositories/oauth_atp_sessions
5import database/types.{Bearer, OAuthAccessToken, OAuthAtpSession}
6import gleam/option.{None, Some}
7import gleeunit/should
8import lib/oauth/did_cache
9import test_helpers
10
11fn setup_db() -> Executor {
12 let assert Ok(exec) = test_helpers.create_test_db()
13 let assert Ok(_) = test_helpers.create_oauth_tables(exec)
14 // Create the test client that access tokens reference
15 let assert Ok(_) =
16 executor.exec(
17 exec,
18 "INSERT INTO oauth_client (client_id, client_name, redirect_uris, grant_types, response_types, token_endpoint_auth_method, client_type, created_at, updated_at)
19 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
20 [
21 Text("test-client"),
22 Text("Test Client"),
23 Text("[\"http://localhost\"]"),
24 Text("[\"authorization_code\"]"),
25 Text("[\"code\"]"),
26 Text("none"),
27 Text("public"),
28 Int(1000),
29 Int(1000),
30 ],
31 )
32 exec
33}
34
35pub fn verify_token_valid_returns_user_info_test() {
36 let exec = setup_db()
37
38 // Insert a valid token
39 let token =
40 OAuthAccessToken(
41 token: "valid-token",
42 token_type: Bearer,
43 client_id: "test-client",
44 user_id: Some("did:plc:testuser123"),
45 session_id: Some("session-123"),
46 session_iteration: Some(1),
47 scope: Some("atproto"),
48 created_at: 1000,
49 expires_at: 9_999_999_999,
50 revoked: False,
51 dpop_jkt: None,
52 )
53 let assert Ok(_) = oauth_access_tokens.insert(exec, token)
54
55 let result = atproto_auth.verify_token(exec, "valid-token")
56
57 result |> should.be_ok
58 let assert Ok(user_info) = result
59 user_info.did |> should.equal("did:plc:testuser123")
60}
61
62pub fn verify_token_not_found_returns_error_test() {
63 let exec = setup_db()
64
65 let result = atproto_auth.verify_token(exec, "nonexistent-token")
66
67 result |> should.be_error
68 let assert Error(err) = result
69 err |> should.equal(atproto_auth.UnauthorizedToken)
70}
71
72pub fn verify_token_revoked_returns_error_test() {
73 let exec = setup_db()
74
75 let token =
76 OAuthAccessToken(
77 token: "revoked-token",
78 token_type: Bearer,
79 client_id: "test-client",
80 user_id: Some("did:plc:testuser123"),
81 session_id: Some("session-123"),
82 session_iteration: Some(1),
83 scope: Some("atproto"),
84 created_at: 1000,
85 expires_at: 9_999_999_999,
86 revoked: True,
87 dpop_jkt: None,
88 )
89 let assert Ok(_) = oauth_access_tokens.insert(exec, token)
90
91 let result = atproto_auth.verify_token(exec, "revoked-token")
92
93 result |> should.be_error
94 let assert Error(err) = result
95 err |> should.equal(atproto_auth.UnauthorizedToken)
96}
97
98pub fn verify_token_expired_returns_error_test() {
99 let exec = setup_db()
100
101 let token =
102 OAuthAccessToken(
103 token: "expired-token",
104 token_type: Bearer,
105 client_id: "test-client",
106 user_id: Some("did:plc:testuser123"),
107 session_id: Some("session-123"),
108 session_iteration: Some(1),
109 scope: Some("atproto"),
110 created_at: 1000,
111 expires_at: 1,
112 // Already expired
113 revoked: False,
114 dpop_jkt: None,
115 )
116 let assert Ok(_) = oauth_access_tokens.insert(exec, token)
117
118 let result = atproto_auth.verify_token(exec, "expired-token")
119
120 result |> should.be_error
121 let assert Error(err) = result
122 err |> should.equal(atproto_auth.TokenExpired)
123}
124
125pub fn verify_token_no_user_id_returns_error_test() {
126 let exec = setup_db()
127
128 let token =
129 OAuthAccessToken(
130 token: "no-user-token",
131 token_type: Bearer,
132 client_id: "test-client",
133 user_id: None,
134 session_id: Some("session-123"),
135 session_iteration: Some(1),
136 scope: Some("atproto"),
137 created_at: 1000,
138 expires_at: 9_999_999_999,
139 revoked: False,
140 dpop_jkt: None,
141 )
142 let assert Ok(_) = oauth_access_tokens.insert(exec, token)
143
144 let result = atproto_auth.verify_token(exec, "no-user-token")
145
146 result |> should.be_error
147 let assert Error(err) = result
148 err |> should.equal(atproto_auth.UnauthorizedToken)
149}
150
151// ===== get_atp_session tests =====
152
153pub fn get_atp_session_no_session_id_returns_error_test() {
154 let exec = setup_db()
155
156 // Token without session_id
157 let token =
158 OAuthAccessToken(
159 token: "no-session-token",
160 token_type: Bearer,
161 client_id: "test-client",
162 user_id: Some("did:plc:testuser123"),
163 session_id: None,
164 session_iteration: None,
165 scope: Some("atproto"),
166 created_at: 1000,
167 expires_at: 9_999_999_999,
168 revoked: False,
169 dpop_jkt: None,
170 )
171 let assert Ok(_) = oauth_access_tokens.insert(exec, token)
172
173 let assert Ok(cache) = did_cache.start()
174
175 let result =
176 atproto_auth.get_atp_session(exec, cache, "no-session-token", None, "")
177
178 result |> should.be_error
179 let assert Error(err) = result
180 err |> should.equal(atproto_auth.SessionNotFound)
181}
182
183pub fn get_atp_session_not_exchanged_returns_error_test() {
184 let exec = setup_db()
185
186 // Token with session_id
187 let token =
188 OAuthAccessToken(
189 token: "has-session-token",
190 token_type: Bearer,
191 client_id: "test-client",
192 user_id: Some("did:plc:testuser123"),
193 session_id: Some("session-123"),
194 session_iteration: Some(1),
195 scope: Some("atproto"),
196 created_at: 1000,
197 expires_at: 9_999_999_999,
198 revoked: False,
199 dpop_jkt: None,
200 )
201 let assert Ok(_) = oauth_access_tokens.insert(exec, token)
202
203 // ATP session that hasn't been exchanged yet (no access_token, no session_exchanged_at)
204 let atp_session =
205 OAuthAtpSession(
206 session_id: "session-123",
207 iteration: 1,
208 did: Some("did:plc:testuser123"),
209 session_created_at: 1000,
210 atp_oauth_state: "state-123",
211 signing_key_jkt: "jkt-123",
212 dpop_key: "{\"kty\":\"EC\"}",
213 access_token: None,
214 refresh_token: None,
215 access_token_created_at: None,
216 access_token_expires_at: None,
217 access_token_scopes: None,
218 session_exchanged_at: None,
219 exchange_error: None,
220 )
221 let assert Ok(_) = oauth_atp_sessions.insert(exec, atp_session)
222
223 let assert Ok(cache) = did_cache.start()
224
225 let result =
226 atproto_auth.get_atp_session(exec, cache, "has-session-token", None, "")
227
228 result |> should.be_error
229 let assert Error(err) = result
230 err |> should.equal(atproto_auth.SessionNotReady)
231}
232
233pub fn get_atp_session_with_exchange_error_returns_error_test() {
234 let exec = setup_db()
235
236 let token =
237 OAuthAccessToken(
238 token: "error-session-token",
239 token_type: Bearer,
240 client_id: "test-client",
241 user_id: Some("did:plc:testuser123"),
242 session_id: Some("session-456"),
243 session_iteration: Some(1),
244 scope: Some("atproto"),
245 created_at: 1000,
246 expires_at: 9_999_999_999,
247 revoked: False,
248 dpop_jkt: None,
249 )
250 let assert Ok(_) = oauth_access_tokens.insert(exec, token)
251
252 // ATP session with exchange error
253 let atp_session =
254 OAuthAtpSession(
255 session_id: "session-456",
256 iteration: 1,
257 did: Some("did:plc:testuser123"),
258 session_created_at: 1000,
259 atp_oauth_state: "state-456",
260 signing_key_jkt: "jkt-456",
261 dpop_key: "{\"kty\":\"EC\"}",
262 access_token: Some("token"),
263 refresh_token: Some("refresh"),
264 access_token_created_at: Some(1000),
265 access_token_expires_at: Some(9_999_999_999),
266 access_token_scopes: Some("atproto"),
267 session_exchanged_at: Some(1000),
268 exchange_error: Some("exchange failed"),
269 )
270 let assert Ok(_) = oauth_atp_sessions.insert(exec, atp_session)
271
272 let assert Ok(cache) = did_cache.start()
273
274 let result =
275 atproto_auth.get_atp_session(exec, cache, "error-session-token", None, "")
276
277 result |> should.be_error
278 let assert Error(err) = result
279 err |> should.equal(atproto_auth.SessionNotReady)
280}