AtlatestRepositorysigil-oauth
sigil-oauth / tree / testsoauth-test.sgl
1
;;; Tests for (sigil oauth)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 helpers15
;; ---------------------------------------------------------------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
#f23
(let loop ((i 0))24
(cond25
((> (+ i nlen) hlen) #f)26
((string=? (substring haystack i (+ i nlen)) needle) #t)27
(else (loop (+ i 1))))))))29
;; ---------------------------------------------------------------30
;; base64url encoding31
;; ---------------------------------------------------------------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 generation52
;; ---------------------------------------------------------------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 generation74
;; ---------------------------------------------------------------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 URL109
;; ---------------------------------------------------------------111
(define test-config112
(oauth-config113
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-config147
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 parsing161
;; ---------------------------------------------------------------163
(test-group "parse-token-response"165
(test "parses complete response"166
(let* ((data #{ access_token: "at123"167
refresh_token: "rt456"168
expires_in: 3600169
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 checking194
;; ---------------------------------------------------------------196
(test-group "oauth-token-expired?"198
(test "not expired when well before expiry"199
(let ((tokens (oauth-tokens200
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-tokens206
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-tokens212
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-tokens218
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-tokens224
access-token: "t"225
expires-at: 1200)))226
;; 300s grace: 1200 - 300 = 900, current = 950, so expired227
(assert-true (oauth-token-expired? tokens 950 300))228
;; current = 800, so not expired229
(assert-equal #f (oauth-token-expired? tokens 800 300))))231
(test "no expiry info returns not-expired"232
(let ((tokens (oauth-tokens233
access-token: "t"234
expires-at: #f)))235
(assert-equal #f (oauth-token-expired? tokens 99999)))))237
;; ---------------------------------------------------------------238
;; Provider presets239
;; ---------------------------------------------------------------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)))))