Skip to content

Commit bb3f52e

Browse files
committed
Improve tests
1 parent 636123f commit bb3f52e

File tree

9 files changed

+692
-662
lines changed

9 files changed

+692
-662
lines changed

.github/workflows/main.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ jobs:
6262
env:
6363
HEXPM_OTP: OTP-28.1
6464
HEXPM_ELIXIR: v1.18.4
65+
HEXPM_BRANCH: ericmj/oauth-device
6566
HEXPM_PATH: hexpm
6667
HEXPM_ELIXIR_PATH: hexpm_elixir
6768
HEXPM_OTP_PATH: hexpm_otp
@@ -92,7 +93,7 @@ jobs:
9293
9394
- name: Set up hexpm
9495
run: |
95-
git clone https://github.com/hexpm/hexpm.git hexpm
96+
git clone -b ${HEXPM_BRANCH} https://github.com/hexpm/hexpm.git hexpm
9697
cd hexpm; PATH=$(pwd)/../${HEXPM_ELIXIR_PATH}/bin:$(pwd)/../${HEXPM_OTP_PATH}/bin:${PATH} MIX_HOME=$(pwd)/../${HEXPM_MIX_HOME} MIX_ARCHIVES=$(pwd)/../${HEXPM_MIX_HOME} MIX_ENV=hex ../${HEXPM_ELIXIR_PATH}/bin/mix deps.get; cd ..
9798
cd hexpm; PATH=$(pwd)/../${HEXPM_ELIXIR_PATH}/bin:$(pwd)/../${HEXPM_OTP_PATH}/bin:${PATH} MIX_HOME=$(pwd)/../${HEXPM_MIX_HOME} MIX_ARCHIVES=$(pwd)/../${HEXPM_MIX_HOME} MIX_ENV=hex ../${HEXPM_ELIXIR_PATH}/bin/mix compile; cd ..
9899

lib/hex/oauth.ex

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -118,17 +118,7 @@ defmodule Hex.OAuth do
118118
end
119119

120120
defp get_stored_tokens do
121-
# Try to get from state first (in memory), then from config
122-
Hex.State.get(:oauth_tokens) ||
123-
case Hex.Config.read()[:"$oauth_tokens"] do
124-
nil ->
125-
nil
126-
127-
tokens ->
128-
# Cache in state for faster access
129-
Hex.State.put(:oauth_tokens, tokens)
130-
tokens
131-
end
121+
Hex.State.get(:oauth_tokens)
132122
end
133123

134124
defp valid_token?(token_data) do

lib/mix/tasks/hex.ex

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -300,12 +300,6 @@ defmodule Mix.Tasks.Hex do
300300
"#{key || hostname}-repositories"
301301
end
302302

303-
@doc false
304-
def clear_oauth_tokens do
305-
Hex.OAuth.clear_tokens()
306-
Hex.Shell.info("OAuth tokens cleared.")
307-
end
308-
309303
@doc false
310304
def revoke_existing_oauth_tokens do
311305
case Hex.Config.read()[:"$oauth_tokens"] do

lib/mix/tasks/hex.user.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ defmodule Mix.Tasks.Hex.User do
8585
defp deauth() do
8686
# Revoke and clear OAuth tokens
8787
Mix.Tasks.Hex.revoke_existing_oauth_tokens()
88-
Mix.Tasks.Hex.clear_oauth_tokens()
88+
Hex.OAuth.clear_tokens()
8989

9090
# Revoke and cleanup old API keys
9191
Mix.Tasks.Hex.revoke_and_cleanup_old_api_keys()

test/hex/api/oauth_test.exs

Lines changed: 78 additions & 142 deletions
Original file line numberDiff line numberDiff line change
@@ -1,191 +1,127 @@
11
defmodule Hex.API.OAuthTest do
22
use HexTest.IntegrationCase
33

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
95

106
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
278
assert {:ok, {200, _headers, response}} =
289
Hex.API.OAuth.device_authorization("api repositories")
2910

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"])
3117
end
3218

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()
3921

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"])
4125
end
4226

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")
4931

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]
5234
end
5335
end
5436

5537
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"]
7342

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)
7546
end
7647

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")
8951
end
9052

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("")
9756

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"]
10058
end
10159
end
10260

10361
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")
11866

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
12170

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"]
12378
end
12479

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")
13184

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"]
13487
end
13588
end
13689

13790
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"]
15598
end
15699

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")
163104

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"]
166114
end
167115
end
168116

169117
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")
178121
end
179122

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("")
189125
end
190126
end
191127
end

0 commit comments

Comments
 (0)