GitHub OAuth helpers
at main 172 lines 6.3 kB view raw
1(* Tests for Github_oauth *) 2 3(* String helper *) 4let is_substring str ~substring = 5 let len = String.length substring in 6 let rec check i = 7 if i + len > String.length str then false 8 else if String.sub str i len = substring then true 9 else check (i + 1) 10 in 11 check 0 12 13let test_generate_state () = 14 let state = Github_oauth.generate_state () in 15 Alcotest.(check int) "state length is 64" 64 (String.length state); 16 (* Check it's all hex characters *) 17 String.iter 18 (fun c -> 19 let is_hex = (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') in 20 Alcotest.(check bool) "is hex char" true is_hex) 21 state 22 23let test_generate_state_unique () = 24 let state1 = Github_oauth.generate_state () in 25 let state2 = Github_oauth.generate_state () in 26 Alcotest.(check bool) "states are different" true (state1 <> state2) 27 28let test_authorization_url_basic () = 29 let url = 30 Github_oauth.authorization_url ~client_id:"test_client" 31 ~callback_url:"https://example.com/callback" ~state:"test_state" 32 ~scope:[ "repo" ] 33 in 34 Alcotest.(check bool) 35 "contains github.com" true 36 (String.sub url 0 24 = "https://github.com/login"); 37 Alcotest.(check bool) 38 "contains client_id" true 39 (is_substring url ~substring:"client_id=test_client"); 40 Alcotest.(check bool) 41 "contains state" true 42 (is_substring url ~substring:"state=test_state"); 43 Alcotest.(check bool) 44 "contains scope" true 45 (is_substring url ~substring:"scope=repo") 46 47let test_authorization_url_no_scope () = 48 let url = 49 Github_oauth.authorization_url ~client_id:"test_client" 50 ~callback_url:"https://example.com/callback" ~state:"test_state" ~scope:[] 51 in 52 Alcotest.(check bool) 53 "no scope param" true 54 (not (is_substring url ~substring:"scope=")) 55 56let test_authorization_url_multiple_scopes () = 57 let url = 58 Github_oauth.authorization_url ~client_id:"test_client" 59 ~callback_url:"https://example.com/callback" ~state:"test_state" 60 ~scope:[ "repo"; "user"; "read:org" ] 61 in 62 (* Scopes are space-separated, URL-encoded *) 63 Alcotest.(check bool) 64 "contains scope" true 65 (is_substring url ~substring:"scope=") 66 67let test_exchange_request_body () = 68 let body = 69 Github_oauth.exchange_request_body ~client_id:"test_client" 70 ~client_secret:"test_secret" ~code:"auth_code" 71 ~redirect_uri:"https://example.com/callback" 72 in 73 Alcotest.(check bool) 74 "contains client_id" true 75 (is_substring body ~substring:"\"client_id\""); 76 Alcotest.(check bool) 77 "contains client_secret" true 78 (is_substring body ~substring:"\"client_secret\""); 79 Alcotest.(check bool) 80 "contains code" true 81 (is_substring body ~substring:"\"code\""); 82 Alcotest.(check bool) 83 "contains redirect_uri" true 84 (is_substring body ~substring:"\"redirect_uri\"") 85 86let test_parse_token_oauth_app () = 87 let json = {|{"access_token":"gho_abc123"}|} in 88 match Github_oauth.parse_token_response json with 89 | Ok t -> 90 Alcotest.(check string) "access_token" "gho_abc123" t.access_token; 91 Alcotest.(check (option int)) "no expires_in" None t.expires_in; 92 Alcotest.(check (option string)) "no refresh_token" None t.refresh_token 93 | Error e -> 94 Alcotest.fail 95 (Fmt.str "parse failed: %a" Github_oauth.pp_parse_token_error e) 96 97let test_parse_token_github_app () = 98 let json = 99 {|{"access_token":"ghu_abc123","expires_in":28800,"refresh_token":"ghr_xyz789","refresh_token_expires_in":15897600}|} 100 in 101 match Github_oauth.parse_token_response json with 102 | Ok t -> 103 Alcotest.(check string) "access_token" "ghu_abc123" t.access_token; 104 Alcotest.(check (option int)) "expires_in" (Some 28800) t.expires_in; 105 Alcotest.(check (option string)) 106 "refresh_token" (Some "ghr_xyz789") t.refresh_token; 107 Alcotest.(check (option int)) 108 "refresh_token_expires_in" (Some 15897600) t.refresh_token_expires_in 109 | Error e -> 110 Alcotest.fail 111 (Fmt.str "parse failed: %a" Github_oauth.pp_parse_token_error e) 112 113let test_parse_token_extra_fields () = 114 (* GitHub may add extra fields - should be ignored *) 115 let json = 116 {|{"access_token":"gho_test","token_type":"bearer","scope":"repo"}|} 117 in 118 match Github_oauth.parse_token_response json with 119 | Ok t -> Alcotest.(check string) "access_token" "gho_test" t.access_token 120 | Error e -> 121 Alcotest.fail 122 (Fmt.str "parse failed: %a" Github_oauth.pp_parse_token_error e) 123 124let test_parse_token_invalid_json () = 125 let json = "not json" in 126 match Github_oauth.parse_token_response json with 127 | Ok _ -> Alcotest.fail "should have failed" 128 | Error e -> 129 Alcotest.(check bool) 130 "is Invalid_json" true 131 (e = Github_oauth.Invalid_json) 132 133let test_refresh_request_body () = 134 let body = 135 Github_oauth.refresh_request_body ~client_id:"test_client" 136 ~client_secret:"test_secret" ~refresh_token:"ghr_abc123" 137 in 138 Alcotest.(check bool) 139 "contains client_id" true 140 (is_substring body ~substring:"\"client_id\""); 141 Alcotest.(check bool) 142 "contains grant_type" true 143 (is_substring body ~substring:"\"grant_type\""); 144 Alcotest.(check bool) 145 "contains refresh_token" true 146 (is_substring body ~substring:"\"refresh_token\"") 147 148let suite = 149 ( "github_oauth", 150 [ 151 Alcotest.test_case "generate_state length and format" `Quick 152 test_generate_state; 153 Alcotest.test_case "generate_state unique" `Quick 154 test_generate_state_unique; 155 Alcotest.test_case "authorization_url basic" `Quick 156 test_authorization_url_basic; 157 Alcotest.test_case "authorization_url no scope" `Quick 158 test_authorization_url_no_scope; 159 Alcotest.test_case "authorization_url multiple scopes" `Quick 160 test_authorization_url_multiple_scopes; 161 Alcotest.test_case "exchange request body" `Quick 162 test_exchange_request_body; 163 Alcotest.test_case "parse_token OAuth App" `Quick 164 test_parse_token_oauth_app; 165 Alcotest.test_case "parse_token GitHub App" `Quick 166 test_parse_token_github_app; 167 Alcotest.test_case "parse_token extra fields" `Quick 168 test_parse_token_extra_fields; 169 Alcotest.test_case "parse_token invalid json" `Quick 170 test_parse_token_invalid_json; 171 Alcotest.test_case "refresh request body" `Quick test_refresh_request_body; 172 ] )