Skip to content

Commit d533eee

Browse files
test: add unit tests for PR#1240
1 parent 495aa50 commit d533eee

File tree

5 files changed

+327
-0
lines changed

5 files changed

+327
-0
lines changed

test/cadet/token_exchange_test.exs

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
defmodule Cadet.TokenExchangeTest do
2+
use Cadet.DataCase
3+
4+
alias Cadet.TokenExchange
5+
6+
describe "get_by_code" do
7+
test "returns error when code not found" do
8+
result = TokenExchange.get_by_code("nonexistent_code")
9+
assert {:error, "Not found"} == result
10+
end
11+
12+
test "returns error when code is expired" do
13+
user = insert(:user)
14+
15+
TokenExchange.insert(%{
16+
code: "expired_code",
17+
generated_at: Timex.shift(Timex.now(), hours: -2),
18+
expires_at: Timex.shift(Timex.now(), hours: -1),
19+
user_id: user.id
20+
})
21+
22+
result = TokenExchange.get_by_code("expired_code")
23+
assert {:error, "Expired"} == result
24+
end
25+
26+
test "returns ok with user when code is valid and not expired" do
27+
user = insert(:user)
28+
code = "valid_code_123"
29+
30+
{:ok, token} =
31+
TokenExchange.insert(%{
32+
code: code,
33+
generated_at: Timex.now(),
34+
expires_at: Timex.shift(Timex.now(), minutes: 5),
35+
user_id: user.id
36+
})
37+
38+
assert {:ok, struct} = TokenExchange.get_by_code(code)
39+
assert struct.code == code
40+
assert struct.user.id == user.id
41+
end
42+
43+
test "deletes the code after successful retrieval" do
44+
user = insert(:user)
45+
code = "code_to_delete"
46+
47+
TokenExchange.insert(%{
48+
code: code,
49+
generated_at: Timex.now(),
50+
expires_at: Timex.shift(Timex.now(), minutes: 5),
51+
user_id: user.id
52+
})
53+
54+
# First retrieval should succeed
55+
assert {:ok, _struct} = TokenExchange.get_by_code(code)
56+
57+
# Second retrieval should fail as the code was deleted
58+
assert {:error, "Not found"} = TokenExchange.get_by_code(code)
59+
end
60+
61+
test "preloads user association" do
62+
user = insert(:user, name: "Test User")
63+
code = "code_with_user"
64+
65+
TokenExchange.insert(%{
66+
code: code,
67+
generated_at: Timex.now(),
68+
expires_at: Timex.shift(Timex.now(), minutes: 5),
69+
user_id: user.id
70+
})
71+
72+
{:ok, struct} = TokenExchange.get_by_code(code)
73+
assert struct.user.name == "Test User"
74+
end
75+
end
76+
77+
describe "delete_expired" do
78+
test "deletes all expired tokens" do
79+
user1 = insert(:user)
80+
user2 = insert(:user)
81+
82+
# Insert expired tokens
83+
TokenExchange.insert(%{
84+
code: "expired_1",
85+
generated_at: Timex.shift(Timex.now(), hours: -2),
86+
expires_at: Timex.shift(Timex.now(), minutes: -30),
87+
user_id: user1.id
88+
})
89+
90+
TokenExchange.insert(%{
91+
code: "expired_2",
92+
generated_at: Timex.shift(Timex.now(), hours: -1),
93+
expires_at: Timex.shift(Timex.now(), minutes: -15),
94+
user_id: user2.id
95+
})
96+
97+
# Insert valid token
98+
TokenExchange.insert(%{
99+
code: "valid_token",
100+
generated_at: Timex.now(),
101+
expires_at: Timex.shift(Timex.now(), minutes: 10),
102+
user_id: user1.id
103+
})
104+
105+
# Execute delete_expired
106+
{deleted_count, _} = TokenExchange.delete_expired()
107+
108+
assert deleted_count == 2
109+
# Verify valid token still exists
110+
assert {:ok, _} = TokenExchange.get_by_code("valid_token")
111+
end
112+
end
113+
114+
describe "insert" do
115+
test "creates a new token exchange record" do
116+
user = insert(:user)
117+
code = "test_code_insert"
118+
119+
{:ok, token} =
120+
TokenExchange.insert(%{
121+
code: code,
122+
generated_at: Timex.now(),
123+
expires_at: Timex.shift(Timex.now(), minutes: 5),
124+
user_id: user.id
125+
})
126+
127+
assert token.code == code
128+
assert token.user_id == user.id
129+
end
130+
131+
test "fails when required fields are missing" do
132+
user = insert(:user)
133+
134+
{:error, changeset} =
135+
TokenExchange.insert(%{
136+
code: "incomplete_code",
137+
user_id: user.id
138+
})
139+
140+
refute changeset.valid?
141+
end
142+
end
143+
144+
describe "changeset" do
145+
test "validates required fields" do
146+
user = insert(:user)
147+
148+
changeset =
149+
TokenExchange.changeset(%TokenExchange{}, %{
150+
code: "test_code",
151+
generated_at: Timex.now(),
152+
expires_at: Timex.shift(Timex.now(), minutes: 5),
153+
user_id: user.id
154+
})
155+
156+
assert changeset.valid?
157+
end
158+
159+
test "marks changeset invalid when required fields are missing" do
160+
changeset =
161+
TokenExchange.changeset(%TokenExchange{}, %{
162+
code: "test_code"
163+
})
164+
165+
refute changeset.valid?
166+
assert Keyword.has_key?(changeset.errors, :generated_at)
167+
assert Keyword.has_key?(changeset.errors, :expires_at)
168+
assert Keyword.has_key?(changeset.errors, :user_id)
169+
end
170+
end
171+
end

test/cadet_web/controllers/auth_controller_test.exs

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
defmodule CadetWeb.AuthControllerTest do
22
use CadetWeb.ConnCase
33
use ExVCR.Mock, adapter: ExVCR.Adapter.Hackney
4+
use Mock
45

56
import Cadet.Factory
67
import Mock
78

89
alias Cadet.Auth.Guardian
910
alias CadetWeb.AuthController
11+
alias Cadet.TokenExchange
1012

1113
setup_all do
1214
HTTPoison.start()
@@ -218,4 +220,114 @@ defmodule CadetWeb.AuthControllerTest do
218220
assert {:error, _} = Guardian.decode_and_verify(refresh_token)
219221
end
220222
end
223+
224+
describe "GET /auth/exchange" do
225+
test "returns 403 when code is not found", %{conn: conn} do
226+
conn =
227+
get(conn, "/v2/auth/exchange", %{
228+
"code" => "nonexistent_code",
229+
"provider" => "test"
230+
})
231+
232+
assert response(conn, 403) == "Invalid code"
233+
end
234+
235+
test "returns 403 when code is expired", %{conn: conn} do
236+
user = insert(:user)
237+
238+
TokenExchange.insert(%{
239+
code: "expired_code",
240+
generated_at: Timex.shift(Timex.now(), hours: -2),
241+
expires_at: Timex.shift(Timex.now(), hours: -1),
242+
user_id: user.id
243+
})
244+
245+
conn =
246+
get(conn, "/v2/auth/exchange", %{
247+
"code" => "expired_code",
248+
"provider" => "test"
249+
})
250+
251+
assert response(conn, 403) == "Invalid code"
252+
end
253+
254+
test "exchanges valid code for tokens and redirects", %{conn: conn} do
255+
user = insert(:user)
256+
code = "valid_exchange_code"
257+
258+
TokenExchange.insert(%{
259+
code: code,
260+
generated_at: Timex.now(),
261+
expires_at: Timex.shift(Timex.now(), minutes: 5),
262+
user_id: user.id
263+
})
264+
265+
# Need to configure the identity provider with post-exchange redirect URL
266+
original_config = Application.get_env(:cadet, :identity_providers)
267+
268+
config_with_redirect =
269+
Map.put(original_config, "test", {
270+
Cadet.Auth.Providers.Config,
271+
original_config["test"] |> elem(1) |> Enum.map(& &1)
272+
})
273+
274+
Application.put_env(:cadet, :identity_providers, %{
275+
"test" => {
276+
elem(config_with_redirect["test"], 0),
277+
elem(config_with_redirect["test"], 1),
278+
client_post_exchange_redirect_url: "http://localhost:3000/callback"
279+
}
280+
})
281+
282+
try do
283+
conn =
284+
get(conn, "/v2/auth/exchange", %{
285+
"code" => code,
286+
"provider" => "test"
287+
})
288+
289+
assert response(conn, 302)
290+
assert get_resp_header(conn, "location") != []
291+
292+
location = get_resp_header(conn, "location") |> hd()
293+
assert String.contains?(location, "access_token=")
294+
assert String.contains?(location, "refresh_token=")
295+
after
296+
Application.put_env(:cadet, :identity_providers, original_config)
297+
end
298+
end
299+
end
300+
301+
describe "GET /auth/saml_redirect_vscode" do
302+
test "missing parameter", %{conn: conn} do
303+
conn = get(conn, "/v2/auth/saml_redirect_vscode", %{})
304+
# The controller doesn't have a clause for missing params, so it will just pass through
305+
# Check if response is not a successful auth
306+
end
307+
308+
test_with_mock "success with saml redirect vscode", %{conn: conn}, Samly, [],
309+
get_active_assertion: fn _ ->
310+
%{attributes: %{"SamAccountName" => "username", "DisplayName" => "name"}}
311+
end do
312+
original_config = Application.get_env(:cadet, :identity_providers)
313+
314+
Application.put_env(:cadet, :identity_providers, %{
315+
"saml" => {
316+
Cadet.Auth.Providers.SAML,
317+
%{
318+
assertion_extractor: Cadet.Auth.Providers.NusstfAssertionExtractor,
319+
vscode_redirect_url_prefix: "vscode://source-academy.source-academy/sso"
320+
}
321+
}
322+
})
323+
324+
try do
325+
conn = get(conn, "/v2/auth/saml_redirect_vscode", %{"provider" => "saml"})
326+
assert response(conn, 302)
327+
assert get_resp_header(conn, "location") != []
328+
after
329+
Application.put_env(:cadet, :identity_providers, original_config)
330+
end
331+
end
332+
end
221333
end

test/cadet_web/router_test.exs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,30 @@ defmodule CadetWeb.RouterTest do
22
use CadetWeb.ConnCase
33

44
alias CadetWeb.Router
5+
alias Cadet.TokenExchange
56

67
test "Swagger", %{conn: conn} do
78
Router.swagger_info()
89
conn = get(conn, "/swagger/index.html")
910
assert response(conn, 200)
1011
end
12+
13+
describe "route definitions" do
14+
test "GET /auth/saml_redirect_vscode route exists", %{conn: conn} do
15+
# This test verifies the route is defined. The actual endpoint behavior is tested in auth_controller_test
16+
assert CadetWeb.Router.Helpers.auth_path(CadetWeb.Endpoint, :saml_redirect_vscode) ==
17+
"/v2/auth/saml_redirect_vscode"
18+
end
19+
20+
test "GET /auth/exchange route exists", %{conn: conn} do
21+
# This test verifies the route is defined. The actual endpoint behavior is tested in auth_controller_test
22+
assert CadetWeb.Router.Helpers.auth_path(CadetWeb.Endpoint, :exchange) ==
23+
"/v2/auth/exchange"
24+
end
25+
26+
test "POST /auth/refresh route still exists", %{conn: conn} do
27+
assert CadetWeb.Router.Helpers.auth_path(CadetWeb.Endpoint, :refresh) ==
28+
"/v2/auth/refresh"
29+
end
30+
end
1131
end

test/factories/factory.ex

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ defmodule Cadet.Factory do
55
use ExMachina.Ecto, repo: Cadet.Repo
66

77
use Cadet.Accounts.{
8+
TokenExchangeFactory,
89
NotificationFactory,
910
UserFactory,
1011
CourseRegistrationFactory,
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
defmodule Cadet.TokenExchangeFactory do
2+
@moduledoc """
3+
Factory for TokenExchange entity
4+
"""
5+
6+
defmacro __using__(_opts) do
7+
quote do
8+
alias Cadet.TokenExchange
9+
10+
def token_exchange_factory do
11+
user = build(:user)
12+
code_ttl = 60
13+
14+
%TokenExchange{
15+
code: TokenExchange |> generate_code(),
16+
generated_at: Timex.now(),
17+
expires_at: Timex.add(Timex.now(), Timex.Duration.from_seconds(code_ttl)),
18+
user_id: user.id
19+
}
20+
end
21+
end
22+
end
23+
end

0 commit comments

Comments
 (0)