Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
171 changes: 171 additions & 0 deletions test/cadet/token_exchange_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
defmodule Cadet.TokenExchangeTest do
use Cadet.DataCase

alias Cadet.TokenExchange

describe "get_by_code" do
test "returns error when code not found" do
result = TokenExchange.get_by_code("nonexistent_code")
assert {:error, "Not found"} == result
end

test "returns error when code is expired" do
user = insert(:user)

TokenExchange.insert(%{
code: "expired_code",
generated_at: Timex.shift(Timex.now(), hours: -2),
expires_at: Timex.shift(Timex.now(), hours: -1),
user_id: user.id
})

result = TokenExchange.get_by_code("expired_code")
assert {:error, "Expired"} == result
end

test "returns ok with user when code is valid and not expired" do
user = insert(:user)
code = "valid_code_123"

{:ok, token} =
TokenExchange.insert(%{
code: code,
generated_at: Timex.now(),
expires_at: Timex.shift(Timex.now(), minutes: 5),
user_id: user.id
})

assert {:ok, struct} = TokenExchange.get_by_code(code)
assert struct.code == code
assert struct.user.id == user.id
end

test "deletes the code after successful retrieval" do
user = insert(:user)
code = "code_to_delete"

TokenExchange.insert(%{
code: code,
generated_at: Timex.now(),
expires_at: Timex.shift(Timex.now(), minutes: 5),
user_id: user.id
})

# First retrieval should succeed
assert {:ok, _struct} = TokenExchange.get_by_code(code)

# Second retrieval should fail as the code was deleted
assert {:error, "Not found"} = TokenExchange.get_by_code(code)
end

test "preloads user association" do
user = insert(:user, name: "Test User")
code = "code_with_user"

TokenExchange.insert(%{
code: code,
generated_at: Timex.now(),
expires_at: Timex.shift(Timex.now(), minutes: 5),
user_id: user.id
})

{:ok, struct} = TokenExchange.get_by_code(code)
assert struct.user.name == "Test User"
end
end

describe "delete_expired" do
test "deletes all expired tokens" do
user1 = insert(:user)
user2 = insert(:user)

# Insert expired tokens
TokenExchange.insert(%{
code: "expired_1",
generated_at: Timex.shift(Timex.now(), hours: -2),
expires_at: Timex.shift(Timex.now(), minutes: -30),
user_id: user1.id
})

TokenExchange.insert(%{
code: "expired_2",
generated_at: Timex.shift(Timex.now(), hours: -1),
expires_at: Timex.shift(Timex.now(), minutes: -15),
user_id: user2.id
})

# Insert valid token
TokenExchange.insert(%{
code: "valid_token",
generated_at: Timex.now(),
expires_at: Timex.shift(Timex.now(), minutes: 10),
user_id: user1.id
})

# Execute delete_expired
{deleted_count, _} = TokenExchange.delete_expired()

assert deleted_count == 2
# Verify valid token still exists
assert {:ok, _} = TokenExchange.get_by_code("valid_token")
end
end

describe "insert" do
test "creates a new token exchange record" do
user = insert(:user)
code = "test_code_insert"

{:ok, token} =
TokenExchange.insert(%{
code: code,
generated_at: Timex.now(),
expires_at: Timex.shift(Timex.now(), minutes: 5),
user_id: user.id
})

assert token.code == code
assert token.user_id == user.id
end

test "fails when required fields are missing" do
user = insert(:user)

{:error, changeset} =
TokenExchange.insert(%{
code: "incomplete_code",
user_id: user.id
})

refute changeset.valid?
end
end

describe "changeset" do
test "validates required fields" do
user = insert(:user)

changeset =
TokenExchange.changeset(%TokenExchange{}, %{
code: "test_code",
generated_at: Timex.now(),
expires_at: Timex.shift(Timex.now(), minutes: 5),
user_id: user.id
})

assert changeset.valid?
end

test "marks changeset invalid when required fields are missing" do
changeset =
TokenExchange.changeset(%TokenExchange{}, %{
code: "test_code"
})

refute changeset.valid?
assert Keyword.has_key?(changeset.errors, :generated_at)
assert Keyword.has_key?(changeset.errors, :expires_at)
assert Keyword.has_key?(changeset.errors, :user_id)
end
end
end
112 changes: 112 additions & 0 deletions test/cadet_web/controllers/auth_controller_test.exs
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
defmodule CadetWeb.AuthControllerTest do
use CadetWeb.ConnCase
use ExVCR.Mock, adapter: ExVCR.Adapter.Hackney
use Mock

import Cadet.Factory
import Mock

alias Cadet.Auth.Guardian
alias CadetWeb.AuthController
alias Cadet.TokenExchange

setup_all do
HTTPoison.start()
Expand Down Expand Up @@ -218,4 +220,114 @@ defmodule CadetWeb.AuthControllerTest do
assert {:error, _} = Guardian.decode_and_verify(refresh_token)
end
end

describe "GET /auth/exchange" do
test "returns 403 when code is not found", %{conn: conn} do
conn =
get(conn, "/v2/auth/exchange", %{
"code" => "nonexistent_code",
"provider" => "test"
})

assert response(conn, 403) == "Invalid code"
end

test "returns 403 when code is expired", %{conn: conn} do
user = insert(:user)

TokenExchange.insert(%{
code: "expired_code",
generated_at: Timex.shift(Timex.now(), hours: -2),
expires_at: Timex.shift(Timex.now(), hours: -1),
user_id: user.id
})

conn =
get(conn, "/v2/auth/exchange", %{
"code" => "expired_code",
"provider" => "test"
})

assert response(conn, 403) == "Invalid code"
end

test "exchanges valid code for tokens and redirects", %{conn: conn} do
user = insert(:user)
code = "valid_exchange_code"

TokenExchange.insert(%{
code: code,
generated_at: Timex.now(),
expires_at: Timex.shift(Timex.now(), minutes: 5),
user_id: user.id
})

# Need to configure the identity provider with post-exchange redirect URL
original_config = Application.get_env(:cadet, :identity_providers)

config_with_redirect =
Map.put(original_config, "test", {
Cadet.Auth.Providers.Config,
original_config["test"] |> elem(1) |> Enum.map(& &1)
})

Application.put_env(:cadet, :identity_providers, %{
"test" => {
elem(config_with_redirect["test"], 0),
elem(config_with_redirect["test"], 1),
client_post_exchange_redirect_url: "http://localhost:3000/callback"
}
})

try do
conn =
get(conn, "/v2/auth/exchange", %{
"code" => code,
"provider" => "test"
})

assert response(conn, 302)
assert get_resp_header(conn, "location") != []

location = get_resp_header(conn, "location") |> hd()
assert String.contains?(location, "access_token=")
assert String.contains?(location, "refresh_token=")
after
Application.put_env(:cadet, :identity_providers, original_config)
end
end
end

describe "GET /auth/saml_redirect_vscode" do
test "missing parameter", %{conn: conn} do
conn = get(conn, "/v2/auth/saml_redirect_vscode", %{})
# The controller doesn't have a clause for missing params, so it will just pass through
# Check if response is not a successful auth
end

test_with_mock "success with saml redirect vscode", %{conn: conn}, Samly, [],
get_active_assertion: fn _ ->
%{attributes: %{"SamAccountName" => "username", "DisplayName" => "name"}}
end do
original_config = Application.get_env(:cadet, :identity_providers)

Application.put_env(:cadet, :identity_providers, %{
"saml" => {
Cadet.Auth.Providers.SAML,
%{
assertion_extractor: Cadet.Auth.Providers.NusstfAssertionExtractor,
vscode_redirect_url_prefix: "vscode://source-academy.source-academy/sso"
}
}
})

try do
conn = get(conn, "/v2/auth/saml_redirect_vscode", %{"provider" => "saml"})
assert response(conn, 302)
assert get_resp_header(conn, "location") != []
after
Application.put_env(:cadet, :identity_providers, original_config)
end
end
end
end
20 changes: 20 additions & 0 deletions test/cadet_web/router_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,30 @@ defmodule CadetWeb.RouterTest do
use CadetWeb.ConnCase

alias CadetWeb.Router
alias Cadet.TokenExchange

test "Swagger", %{conn: conn} do
Router.swagger_info()
conn = get(conn, "/swagger/index.html")
assert response(conn, 200)
end

describe "route definitions" do
test "GET /auth/saml_redirect_vscode route exists", %{conn: conn} do
# This test verifies the route is defined. The actual endpoint behavior is tested in auth_controller_test
assert CadetWeb.Router.Helpers.auth_path(CadetWeb.Endpoint, :saml_redirect_vscode) ==
"/v2/auth/saml_redirect_vscode"
end

test "GET /auth/exchange route exists", %{conn: conn} do
# This test verifies the route is defined. The actual endpoint behavior is tested in auth_controller_test
assert CadetWeb.Router.Helpers.auth_path(CadetWeb.Endpoint, :exchange) ==
"/v2/auth/exchange"
end

test "POST /auth/refresh route still exists", %{conn: conn} do
assert CadetWeb.Router.Helpers.auth_path(CadetWeb.Endpoint, :refresh) ==
"/v2/auth/refresh"
end
end
end
1 change: 1 addition & 0 deletions test/factories/factory.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ defmodule Cadet.Factory do
use ExMachina.Ecto, repo: Cadet.Repo

use Cadet.Accounts.{
TokenExchangeFactory,
NotificationFactory,
UserFactory,
CourseRegistrationFactory,
Expand Down
23 changes: 23 additions & 0 deletions test/factories/token_exchange_factory.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
defmodule Cadet.TokenExchangeFactory do
@moduledoc """
Factory for TokenExchange entity
"""

defmacro __using__(_opts) do
quote do
alias Cadet.TokenExchange

def token_exchange_factory do
user = build(:user)
code_ttl = 60

%TokenExchange{
code: TokenExchange |> generate_code(),
generated_at: Timex.now(),
expires_at: Timex.add(Timex.now(), Timex.Duration.from_seconds(code_ttl)),
user_id: user.id
}
end
end
end
end
Loading