|
1 | 1 | defmodule Hex.API.OAuthTest do |
2 | 2 | use HexTest.IntegrationCase |
3 | 3 |
|
4 | | - setup do |
5 | | - bypass = Bypass.open() |
6 | | - Hex.State.put(:api_url, "http://localhost:#{bypass.port}/api") |
7 | | - {:ok, bypass: bypass} |
8 | | - end |
| 4 | + # Using real test server at localhost:4043 with OAuth client configured |
9 | 5 |
|
10 | 6 | describe "device_authorization/1" do |
11 | | - test "returns device authorization data", %{bypass: bypass} do |
12 | | - expected_response = %{ |
13 | | - "device_code" => "test_device_code", |
14 | | - "user_code" => "TEST123", |
15 | | - "verification_uri" => "https://hex.pm/oauth/device", |
16 | | - "verification_uri_complete" => "https://hex.pm/oauth/device?user_code=TEST123", |
17 | | - "expires_in" => 600, |
18 | | - "interval" => 5 |
19 | | - } |
20 | | - |
21 | | - Bypass.expect(bypass, "POST", "/api/oauth/device_authorization", fn conn -> |
22 | | - conn |
23 | | - |> Plug.Conn.put_resp_header("content-type", "application/vnd.hex+erlang") |
24 | | - |> Plug.Conn.resp(200, Hex.Utils.safe_serialize_erlang(expected_response)) |
25 | | - end) |
26 | | - |
| 7 | + test "returns device authorization data" do |
27 | 8 | assert {:ok, {200, _headers, response}} = |
28 | 9 | Hex.API.OAuth.device_authorization("api repositories") |
29 | 10 |
|
30 | | - assert response == expected_response |
| 11 | + # Verify the response has the expected structure from the real server |
| 12 | + assert is_binary(response["device_code"]) |
| 13 | + assert is_binary(response["user_code"]) |
| 14 | + assert is_binary(response["verification_uri"]) |
| 15 | + assert is_integer(response["expires_in"]) |
| 16 | + assert is_integer(response["interval"]) |
31 | 17 | end |
32 | 18 |
|
33 | | - test "defaults to api repositories scope", %{bypass: bypass} do |
34 | | - Bypass.expect(bypass, "POST", "/api/oauth/device_authorization", fn conn -> |
35 | | - conn |
36 | | - |> Plug.Conn.put_resp_header("content-type", "application/vnd.hex+erlang") |
37 | | - |> Plug.Conn.resp(200, Hex.Utils.safe_serialize_erlang(%{})) |
38 | | - end) |
| 19 | + test "defaults to api repositories scope" do |
| 20 | + assert {:ok, {200, _headers, response}} = Hex.API.OAuth.device_authorization() |
39 | 21 |
|
40 | | - Hex.API.OAuth.device_authorization() |
| 22 | + # Should return valid device authorization data |
| 23 | + assert is_binary(response["device_code"]) |
| 24 | + assert is_binary(response["user_code"]) |
41 | 25 | end |
42 | 26 |
|
43 | | - test "handles error response", %{bypass: bypass} do |
44 | | - Bypass.expect(bypass, "POST", "/api/oauth/device_authorization", fn conn -> |
45 | | - conn |
46 | | - |> Plug.Conn.put_resp_header("content-type", "application/vnd.hex+erlang") |
47 | | - |> Plug.Conn.resp(400, Hex.Utils.safe_serialize_erlang(%{"error" => "invalid_request"})) |
48 | | - end) |
| 27 | + test "handles invalid scope" do |
| 28 | + # The real server should handle invalid scopes - may accept or reject |
| 29 | + assert {:ok, {status, _headers, _response}} = |
| 30 | + Hex.API.OAuth.device_authorization("invalid_scope") |
49 | 31 |
|
50 | | - assert {:ok, {400, _headers, %{"error" => "invalid_request"}}} = |
51 | | - Hex.API.OAuth.device_authorization() |
| 32 | + # Server may return 200 (accepted), 400 (invalid scope), or 401 (invalid client) |
| 33 | + assert status in [200, 400, 401] |
52 | 34 | end |
53 | 35 | end |
54 | 36 |
|
55 | 37 | describe "poll_device_token/1" do |
56 | | - test "returns token on successful authorization", %{bypass: bypass} do |
57 | | - expected_response = %{ |
58 | | - "access_token" => "access_token_value", |
59 | | - "token_type" => "bearer", |
60 | | - "expires_in" => 3600, |
61 | | - "refresh_token" => "refresh_token_value", |
62 | | - "scope" => "api repositories" |
63 | | - } |
64 | | - |
65 | | - Bypass.expect(bypass, "POST", "/api/oauth/token", fn conn -> |
66 | | - conn |
67 | | - |> Plug.Conn.put_resp_header("content-type", "application/vnd.hex+erlang") |
68 | | - |> Plug.Conn.resp(200, Hex.Utils.safe_serialize_erlang(expected_response)) |
69 | | - end) |
70 | | - |
71 | | - assert {:ok, {200, _headers, response}} = |
72 | | - Hex.API.OAuth.poll_device_token("test_device_code") |
| 38 | + test "returns authorization_pending for valid device code" do |
| 39 | + # First get a valid device code |
| 40 | + {:ok, {200, _headers, device_response}} = Hex.API.OAuth.device_authorization() |
| 41 | + device_code = device_response["device_code"] |
73 | 42 |
|
74 | | - assert response == expected_response |
| 43 | + # Polling should return authorization_pending since user hasn't authorized |
| 44 | + assert {:ok, {400, _headers, %{"error" => "authorization_pending"}}} = |
| 45 | + Hex.API.OAuth.poll_device_token(device_code) |
75 | 46 | end |
76 | 47 |
|
77 | | - test "returns authorization_pending when user hasn't authorized yet", %{bypass: bypass} do |
78 | | - Bypass.expect(bypass, "POST", "/api/oauth/token", fn conn -> |
79 | | - conn |
80 | | - |> Plug.Conn.put_resp_header("content-type", "application/vnd.hex+erlang") |
81 | | - |> Plug.Conn.resp( |
82 | | - 400, |
83 | | - Hex.Utils.safe_serialize_erlang(%{"error" => "authorization_pending"}) |
84 | | - ) |
85 | | - end) |
86 | | - |
87 | | - assert {:ok, {400, _headers, %{"error" => "authorization_pending"}}} = |
88 | | - Hex.API.OAuth.poll_device_token("test_device_code") |
| 48 | + test "returns invalid_grant for invalid device code" do |
| 49 | + assert {:ok, {400, _headers, %{"error" => "invalid_grant"}}} = |
| 50 | + Hex.API.OAuth.poll_device_token("invalid_device_code") |
89 | 51 | end |
90 | 52 |
|
91 | | - test "returns slow_down when polling too frequently", %{bypass: bypass} do |
92 | | - Bypass.expect(bypass, "POST", "/api/oauth/token", fn conn -> |
93 | | - conn |
94 | | - |> Plug.Conn.put_resp_header("content-type", "application/vnd.hex+erlang") |
95 | | - |> Plug.Conn.resp(400, Hex.Utils.safe_serialize_erlang(%{"error" => "slow_down"})) |
96 | | - end) |
| 53 | + test "handles malformed device code" do |
| 54 | + assert {:ok, {400, _headers, %{"error" => error}}} = |
| 55 | + Hex.API.OAuth.poll_device_token("") |
97 | 56 |
|
98 | | - assert {:ok, {400, _headers, %{"error" => "slow_down"}}} = |
99 | | - Hex.API.OAuth.poll_device_token("test_device_code") |
| 57 | + assert error in ["invalid_grant", "invalid_request"] |
100 | 58 | end |
101 | 59 | end |
102 | 60 |
|
103 | 61 | describe "exchange_token/2" do |
104 | | - test "exchanges token for more limited scope", %{bypass: bypass} do |
105 | | - expected_response = %{ |
106 | | - "access_token" => "limited_access_token", |
107 | | - "token_type" => "bearer", |
108 | | - "expires_in" => 3600, |
109 | | - "refresh_token" => "limited_refresh_token", |
110 | | - "scope" => "api:write" |
111 | | - } |
112 | | - |
113 | | - Bypass.expect(bypass, "POST", "/api/oauth/token", fn conn -> |
114 | | - conn |
115 | | - |> Plug.Conn.put_resp_header("content-type", "application/vnd.hex+erlang") |
116 | | - |> Plug.Conn.resp(200, Hex.Utils.safe_serialize_erlang(expected_response)) |
117 | | - end) |
| 62 | + test "handles invalid token exchange" do |
| 63 | + # Test with a completely invalid token |
| 64 | + assert {:ok, {status, _headers, %{"error" => error}}} = |
| 65 | + Hex.API.OAuth.exchange_token("invalid_token", "api:write") |
118 | 66 |
|
119 | | - assert {:ok, {200, _headers, response}} = |
120 | | - Hex.API.OAuth.exchange_token("original_token", "api:write") |
| 67 | + assert status in [400, 401] |
| 68 | + assert error in ["invalid_token", "invalid_grant"] |
| 69 | + end |
121 | 70 |
|
122 | | - assert response == expected_response |
| 71 | + test "handles token exchange with invalid scope" do |
| 72 | + # Test with invalid scope and invalid token (expect token error first) |
| 73 | + assert {:ok, {status, _headers, %{"error" => error}}} = |
| 74 | + Hex.API.OAuth.exchange_token("invalid_token", "invalid_scope") |
| 75 | + |
| 76 | + assert status in [400, 401] |
| 77 | + assert error in ["invalid_token", "invalid_grant", "invalid_scope"] |
123 | 78 | end |
124 | 79 |
|
125 | | - test "handles invalid token", %{bypass: bypass} do |
126 | | - Bypass.expect(bypass, "POST", "/api/oauth/token", fn conn -> |
127 | | - conn |
128 | | - |> Plug.Conn.put_resp_header("content-type", "application/vnd.hex+erlang") |
129 | | - |> Plug.Conn.resp(400, Hex.Utils.safe_serialize_erlang(%{"error" => "invalid_grant"})) |
130 | | - end) |
| 80 | + test "validates token format" do |
| 81 | + # Test with malformed token |
| 82 | + assert {:ok, {status, _headers, %{"error" => error}}} = |
| 83 | + Hex.API.OAuth.exchange_token("malformed_token", "api:write") |
131 | 84 |
|
132 | | - assert {:ok, {400, _headers, %{"error" => "invalid_grant"}}} = |
133 | | - Hex.API.OAuth.exchange_token("invalid_token", "api:write") |
| 85 | + assert status in [400, 401] |
| 86 | + assert error in ["invalid_grant", "invalid_token"] |
134 | 87 | end |
135 | 88 | end |
136 | 89 |
|
137 | 90 | describe "refresh_token/1" do |
138 | | - test "refreshes access token", %{bypass: bypass} do |
139 | | - expected_response = %{ |
140 | | - "access_token" => "new_access_token", |
141 | | - "token_type" => "bearer", |
142 | | - "expires_in" => 3600, |
143 | | - "refresh_token" => "new_refresh_token", |
144 | | - "scope" => "api:write" |
145 | | - } |
146 | | - |
147 | | - Bypass.expect(bypass, "POST", "/api/oauth/token", fn conn -> |
148 | | - conn |
149 | | - |> Plug.Conn.put_resp_header("content-type", "application/vnd.hex+erlang") |
150 | | - |> Plug.Conn.resp(200, Hex.Utils.safe_serialize_erlang(expected_response)) |
151 | | - end) |
152 | | - |
153 | | - assert {:ok, {200, _headers, response}} = Hex.API.OAuth.refresh_token("refresh_token_value") |
154 | | - assert response == expected_response |
| 91 | + test "handles invalid refresh token" do |
| 92 | + # Test with a completely invalid refresh token |
| 93 | + assert {:ok, {status, _headers, %{"error" => error}}} = |
| 94 | + Hex.API.OAuth.refresh_token("invalid_refresh_token") |
| 95 | + |
| 96 | + assert status in [400, 401] |
| 97 | + assert error in ["invalid_token", "invalid_grant"] |
155 | 98 | end |
156 | 99 |
|
157 | | - test "handles invalid refresh token", %{bypass: bypass} do |
158 | | - Bypass.expect(bypass, "POST", "/api/oauth/token", fn conn -> |
159 | | - conn |
160 | | - |> Plug.Conn.put_resp_header("content-type", "application/vnd.hex+erlang") |
161 | | - |> Plug.Conn.resp(400, Hex.Utils.safe_serialize_erlang(%{"error" => "invalid_grant"})) |
162 | | - end) |
| 100 | + test "handles malformed refresh token" do |
| 101 | + # Test with malformed refresh token |
| 102 | + assert {:ok, {status, _headers, %{"error" => error}}} = |
| 103 | + Hex.API.OAuth.refresh_token("malformed_token") |
163 | 104 |
|
164 | | - assert {:ok, {400, _headers, %{"error" => "invalid_grant"}}} = |
165 | | - Hex.API.OAuth.refresh_token("invalid_refresh_token") |
| 105 | + assert status in [400, 401] |
| 106 | + assert error in ["invalid_token", "invalid_grant"] |
| 107 | + end |
| 108 | + |
| 109 | + test "handles empty refresh token" do |
| 110 | + assert {:ok, {400, _headers, %{"error" => error}}} = |
| 111 | + Hex.API.OAuth.refresh_token("") |
| 112 | + |
| 113 | + assert error in ["invalid_grant", "invalid_request"] |
166 | 114 | end |
167 | 115 | end |
168 | 116 |
|
169 | 117 | describe "revoke_token/1" do |
170 | | - test "revokes token successfully", %{bypass: bypass} do |
171 | | - Bypass.expect(bypass, "POST", "/api/oauth/revoke", fn conn -> |
172 | | - conn |
173 | | - |> Plug.Conn.put_resp_header("content-type", "application/vnd.hex+erlang") |
174 | | - |> Plug.Conn.resp(200, Hex.Utils.safe_serialize_erlang("")) |
175 | | - end) |
176 | | - |
177 | | - assert {:ok, {200, _headers, ""}} = Hex.API.OAuth.revoke_token("token_to_revoke") |
| 118 | + test "returns 200 for token revocation" do |
| 119 | + # OAuth revoke endpoint returns 200 even for invalid tokens (per RFC 7009) |
| 120 | + assert {:ok, {200, _headers, _body}} = Hex.API.OAuth.revoke_token("any_token") |
178 | 121 | end |
179 | 122 |
|
180 | | - test "handles invalid token", %{bypass: bypass} do |
181 | | - Bypass.expect(bypass, "POST", "/api/oauth/revoke", fn conn -> |
182 | | - conn |
183 | | - |> Plug.Conn.put_resp_header("content-type", "application/vnd.hex+erlang") |
184 | | - |> Plug.Conn.resp(400, Hex.Utils.safe_serialize_erlang(%{"error" => "invalid_request"})) |
185 | | - end) |
186 | | - |
187 | | - assert {:ok, {400, _headers, %{"error" => "invalid_request"}}} = |
188 | | - Hex.API.OAuth.revoke_token("invalid_token") |
| 123 | + test "handles empty token" do |
| 124 | + assert {:ok, {200, _headers, _body}} = Hex.API.OAuth.revoke_token("") |
189 | 125 | end |
190 | 126 | end |
191 | 127 | end |
0 commit comments