AtlatestRepositorysigil-oauth

sigil-oauth / tree / testsoauth-test.sgl

1;;; Tests for (sigil oauth)
2
3(import (sigil core)
4 (only (scheme base) string->utf8)
5 (sigil test)
6 (sigil dict)
7 (sigil string)
8 (sigil struct)
9 (sigil json)
10 (sigil crypto)
11 (sigil oauth))
13;; ---------------------------------------------------------------
14;; Test helpers
15;; ---------------------------------------------------------------
17;;; Check if haystack contains needle as a substring.
18(define (str-contains? haystack needle)
19 (let ((hlen (string-length haystack))
20 (nlen (string-length needle)))
21 (if (> nlen hlen)
22 #f
23 (let loop ((i 0))
24 (cond
25 ((> (+ i nlen) hlen) #f)
26 ((string=? (substring haystack i (+ i nlen)) needle) #t)
27 (else (loop (+ i 1))))))))
29;; ---------------------------------------------------------------
30;; base64url encoding
31;; ---------------------------------------------------------------
33(test-group "base64url-encode"
35 (test "encodes empty bytevector"
36 (assert-equal "" (base64url-encode #u8())))
38 (test "strips padding"
39 ;; base64("f") = "Zg==" -> base64url should be "Zg"
40 (let ((result (base64url-encode (string->utf8 "f"))))
41 (assert-equal #f (str-contains? result "="))))
43 (test "replaces + and / with URL-safe chars"
44 ;; 0xFB 0xFF -> standard base64 = "+/8=" -> base64url = "-_8"
45 (let ((result (base64url-encode #u8(251 255))))
46 (assert-equal #f (str-contains? result "+"))
47 (assert-equal #f (str-contains? result "/"))
48 (assert-equal "-_8" result))))
50;; ---------------------------------------------------------------
51;; State generation
52;; ---------------------------------------------------------------
54(test-group "oauth-generate-state"
56 (test "returns a non-empty string"
57 (let ((state (oauth-generate-state)))
58 (assert-true (string? state))
59 (assert-true (> (string-length state) 0))))
61 (test "returns different values each time"
62 (let ((s1 (oauth-generate-state))
63 (s2 (oauth-generate-state)))
64 (assert-true (not (string=? s1 s2)))))
66 (test "uses only URL-safe characters"
67 (let ((state (oauth-generate-state)))
68 (assert-equal #f (str-contains? state "+"))
69 (assert-equal #f (str-contains? state "/"))
70 (assert-equal #f (str-contains? state "=")))))
72;; ---------------------------------------------------------------
73;; PKCE generation
74;; ---------------------------------------------------------------
76(test-group "oauth-generate-pkce"
78 (test "returns an oauth-pkce record"
79 (let ((pkce (oauth-generate-pkce)))
80 (assert-true (oauth-pkce? pkce))))
82 (test "verifier is non-empty URL-safe string"
83 (let ((pkce (oauth-generate-pkce)))
84 (let ((v (oauth-pkce-verifier pkce)))
85 (assert-true (string? v))
86 (assert-true (> (string-length v) 0))
87 (assert-equal #f (str-contains? v "+"))
88 (assert-equal #f (str-contains? v "/"))
89 (assert-equal #f (str-contains? v "=")))))
91 (test "challenge is SHA256 of verifier in base64url"
92 (let* ((pkce (oauth-generate-pkce))
93 (verifier (oauth-pkce-verifier pkce))
94 (expected (base64url-encode (sha256-bytes (string->utf8 verifier)))))
95 (assert-equal expected (oauth-pkce-challenge pkce))))
97 (test "method is S256"
98 (let ((pkce (oauth-generate-pkce)))
99 (assert-equal "S256" (oauth-pkce-method pkce))))
101 (test "different calls produce different verifiers"
102 (let ((p1 (oauth-generate-pkce))
103 (p2 (oauth-generate-pkce)))
104 (assert-true (not (string=? (oauth-pkce-verifier p1)
105 (oauth-pkce-verifier p2)))))))
107;; ---------------------------------------------------------------
108;; Authorization URL
109;; ---------------------------------------------------------------
111(define test-config
112 (oauth-config
113 client-id: "test-client-id"
114 client-secret: "test-secret"
115 auth-url: "https://example.com/auth"
116 token-url: "https://example.com/token"
117 scopes: '("read" "write")
118 redirect-uri: "http://localhost:8085/callback"))
120(test-group "oauth-authorization-url"
122 (test "includes required parameters"
123 (let ((url (oauth-authorization-url test-config "test-state")))
124 (assert-true (str-contains? url "response_type=code"))
125 (assert-true (str-contains? url "client_id=test-client-id"))
126 (assert-true (str-contains? url "state=test-state"))
127 (assert-true (str-contains? url "redirect_uri="))
128 (assert-true (str-contains? url "scope=read+write"))))
130 (test "starts with auth-url"
131 (let ((url (oauth-authorization-url test-config "s")))
132 (assert-true (string-starts-with? url "https://example.com/auth?"))))
134 (test "includes PKCE parameters when provided"
135 (let* ((pkce (oauth-generate-pkce))
136 (url (oauth-authorization-url test-config "s"
137 #{ pkce: pkce })))
138 (assert-true (str-contains? url "code_challenge="))
139 (assert-true (str-contains? url "code_challenge_method=S256"))))
141 (test "omits PKCE when not provided"
142 (let ((url (oauth-authorization-url test-config "s")))
143 (assert-equal #f (str-contains? url "code_challenge"))))
145 (test "includes extra params from config"
146 (let* ((config (oauth-config
147 client-id: "id"
148 auth-url: "https://ex.com/auth"
149 token-url: "https://ex.com/token"
150 extra-params: #{ access_type: "offline" }))
151 (url (oauth-authorization-url config "s")))
152 (assert-true (str-contains? url "access_type=offline"))))
154 (test "includes extra params from opts"
155 (let ((url (oauth-authorization-url test-config "s"
156 #{ extra-params: #{ prompt: "consent" } })))
157 (assert-true (str-contains? url "prompt=consent")))))
159;; ---------------------------------------------------------------
160;; Token response parsing
161;; ---------------------------------------------------------------
163(test-group "parse-token-response"
165 (test "parses complete response"
166 (let* ((data #{ access_token: "at123"
167 refresh_token: "rt456"
168 expires_in: 3600
169 token_type: "Bearer"
170 scope: "read write" })
171 (tokens (parse-token-response data 1000)))
172 (assert-true (oauth-tokens? tokens))
173 (assert-equal "at123" (oauth-tokens-access-token tokens))
174 (assert-equal "rt456" (oauth-tokens-refresh-token tokens))
175 (assert-equal 4600 (oauth-tokens-expires-at tokens))
176 (assert-equal "Bearer" (oauth-tokens-token-type tokens))
177 (assert-equal '("read" "write") (oauth-tokens-scopes tokens))))
179 (test "handles missing optional fields"
180 (let* ((data #{ access_token: "at123" })
181 (tokens (parse-token-response data 1000)))
182 (assert-equal "at123" (oauth-tokens-access-token tokens))
183 (assert-equal #f (oauth-tokens-refresh-token tokens))
184 (assert-equal #f (oauth-tokens-expires-at tokens))
185 (assert-equal '() (oauth-tokens-scopes tokens))))
187 (test "computes expires-at from current time + expires_in"
188 (let* ((data #{ access_token: "at" expires_in: 7200 })
189 (tokens (parse-token-response data 5000)))
190 (assert-equal 12200 (oauth-tokens-expires-at tokens)))))
192;; ---------------------------------------------------------------
193;; Token expiry checking
194;; ---------------------------------------------------------------
196(test-group "oauth-token-expired?"
198 (test "not expired when well before expiry"
199 (let ((tokens (oauth-tokens
200 access-token: "t"
201 expires-at: 2000)))
202 (assert-equal #f (oauth-token-expired? tokens 1000))))
204 (test "expired when past expiry"
205 (let ((tokens (oauth-tokens
206 access-token: "t"
207 expires-at: 1000)))
208 (assert-true (oauth-token-expired? tokens 1500))))
210 (test "expired within grace period (default 60s)"
211 (let ((tokens (oauth-tokens
212 access-token: "t"
213 expires-at: 1050)))
214 (assert-true (oauth-token-expired? tokens 1000))))
216 (test "not expired just outside grace period"
217 (let ((tokens (oauth-tokens
218 access-token: "t"
219 expires-at: 1100)))
220 (assert-equal #f (oauth-token-expired? tokens 1000))))
222 (test "custom grace period"
223 (let ((tokens (oauth-tokens
224 access-token: "t"
225 expires-at: 1200)))
226 ;; 300s grace: 1200 - 300 = 900, current = 950, so expired
227 (assert-true (oauth-token-expired? tokens 950 300))
228 ;; current = 800, so not expired
229 (assert-equal #f (oauth-token-expired? tokens 800 300))))
231 (test "no expiry info returns not-expired"
232 (let ((tokens (oauth-tokens
233 access-token: "t"
234 expires-at: #f)))
235 (assert-equal #f (oauth-token-expired? tokens 99999)))))
237;; ---------------------------------------------------------------
238;; Provider presets
239;; ---------------------------------------------------------------
241(test-group "oauth-google-config"
243 (test "sets correct Google auth URLs"
244 (let ((config (oauth-google-config "cid" "csec"
245 '("https://www.googleapis.com/auth/youtube"))))
246 (assert-equal "https://accounts.google.com/o/oauth2/v2/auth"
247 (oauth-config-auth-url config))
248 (assert-equal "https://oauth2.googleapis.com/token"
249 (oauth-config-token-url config))))
251 (test "includes access_type=offline in extra params"
252 (let ((config (oauth-google-config "cid" "csec" '())))
253 (assert-equal "offline"
254 (dict-ref (oauth-config-extra-params config)
255 access_type:))))
257 (test "preserves client credentials and scopes"
258 (let ((config (oauth-google-config "my-id" "my-secret"
259 '("scope1" "scope2"))))
260 (assert-equal "my-id" (oauth-config-client-id config))
261 (assert-equal "my-secret" (oauth-config-client-secret config))
262 (assert-equal '("scope1" "scope2") (oauth-config-scopes config)))))
264(test-group "oauth-twitch-config"
266 (test "sets correct Twitch auth URLs"
267 (let ((config (oauth-twitch-config "cid" "csec"
268 '("channel:manage:broadcast"))))
269 (assert-equal "https://id.twitch.tv/oauth2/authorize"
270 (oauth-config-auth-url config))
271 (assert-equal "https://id.twitch.tv/oauth2/token"
272 (oauth-config-token-url config))))
274 (test "preserves scopes"
275 (let ((config (oauth-twitch-config "c" "s"
276 '("channel:manage:broadcast"
277 "user:read:chat"))))
278 (assert-equal '("channel:manage:broadcast" "user:read:chat")
279 (oauth-config-scopes config))))
281 (test "accepts custom redirect URI"
282 (let ((config (oauth-twitch-config "c" "s" '()
283 #{ redirect-uri: "http://localhost:9999/cb" })))
284 (assert-equal "http://localhost:9999/cb"
285 (oauth-config-redirect-uri config)))))