Add actor parameter to all tests requiring authorization
This commit adds actor: system_actor to all Ash operations in tests that require authorization.
This commit is contained in:
parent
686f69c9e9
commit
0f48a9b15a
75 changed files with 4686 additions and 2859 deletions
|
|
@ -8,8 +8,13 @@ defmodule MvWeb.OidcE2EFlowTest do
|
|||
use MvWeb.ConnCase, async: true
|
||||
require Ash.Query
|
||||
|
||||
setup do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
%{actor: system_actor}
|
||||
end
|
||||
|
||||
describe "E2E: New OIDC user registration" do
|
||||
test "new user can register via OIDC", %{conn: _conn} do
|
||||
test "new user can register via OIDC", %{conn: _conn, actor: actor} do
|
||||
# Simulate OIDC callback for brand new user
|
||||
user_info = %{
|
||||
"sub" => "new_oidc_user_123",
|
||||
|
|
@ -18,10 +23,13 @@ defmodule MvWeb.OidcE2EFlowTest do
|
|||
|
||||
# Call register action
|
||||
result =
|
||||
Mv.Accounts.create_register_with_rauthy(%{
|
||||
user_info: user_info,
|
||||
oauth_tokens: %{}
|
||||
})
|
||||
Mv.Accounts.create_register_with_rauthy(
|
||||
%{
|
||||
user_info: user_info,
|
||||
oauth_tokens: %{}
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
assert {:ok, new_user} = result
|
||||
assert to_string(new_user.email) == "newuser@example.com"
|
||||
|
|
@ -30,17 +38,20 @@ defmodule MvWeb.OidcE2EFlowTest do
|
|||
|
||||
# Verify user can be found by oidc_id
|
||||
{:ok, [found_user]} =
|
||||
Mv.Accounts.read_sign_in_with_rauthy(%{
|
||||
user_info: user_info,
|
||||
oauth_tokens: %{}
|
||||
})
|
||||
Mv.Accounts.read_sign_in_with_rauthy(
|
||||
%{
|
||||
user_info: user_info,
|
||||
oauth_tokens: %{}
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
assert found_user.id == new_user.id
|
||||
end
|
||||
end
|
||||
|
||||
describe "E2E: Existing OIDC user sign-in" do
|
||||
test "existing OIDC user can sign in and email updates", %{conn: _conn} do
|
||||
test "existing OIDC user can sign in and email updates", %{conn: _conn, actor: actor} do
|
||||
# Create OIDC user
|
||||
user =
|
||||
create_test_user(%{
|
||||
|
|
@ -56,10 +67,13 @@ defmodule MvWeb.OidcE2EFlowTest do
|
|||
|
||||
# Register (upsert) with new email
|
||||
{:ok, updated_user} =
|
||||
Mv.Accounts.create_register_with_rauthy(%{
|
||||
user_info: updated_user_info,
|
||||
oauth_tokens: %{}
|
||||
})
|
||||
Mv.Accounts.create_register_with_rauthy(
|
||||
%{
|
||||
user_info: updated_user_info,
|
||||
oauth_tokens: %{}
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
# Same user, updated email
|
||||
assert updated_user.id == user.id
|
||||
|
|
@ -70,7 +84,7 @@ defmodule MvWeb.OidcE2EFlowTest do
|
|||
|
||||
describe "E2E: OIDC with existing password account (Email Collision)" do
|
||||
test "OIDC registration with password account email triggers PasswordVerificationRequired",
|
||||
%{conn: _conn} do
|
||||
%{conn: _conn, actor: actor} do
|
||||
# Step 1: Create a password-only user
|
||||
password_user =
|
||||
create_test_user(%{
|
||||
|
|
@ -86,10 +100,13 @@ defmodule MvWeb.OidcE2EFlowTest do
|
|||
}
|
||||
|
||||
result =
|
||||
Mv.Accounts.create_register_with_rauthy(%{
|
||||
user_info: user_info,
|
||||
oauth_tokens: %{}
|
||||
})
|
||||
Mv.Accounts.create_register_with_rauthy(
|
||||
%{
|
||||
user_info: user_info,
|
||||
oauth_tokens: %{}
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
# Step 3: Should fail with PasswordVerificationRequired
|
||||
assert {:error, %Ash.Error.Invalid{errors: errors}} = result
|
||||
|
|
@ -106,7 +123,7 @@ defmodule MvWeb.OidcE2EFlowTest do
|
|||
end
|
||||
|
||||
test "full E2E flow: OIDC collision -> password verification -> account linked",
|
||||
%{conn: _conn} do
|
||||
%{conn: _conn, actor: actor} do
|
||||
# Step 1: Create password user
|
||||
password_user =
|
||||
create_test_user(%{
|
||||
|
|
@ -122,10 +139,13 @@ defmodule MvWeb.OidcE2EFlowTest do
|
|||
}
|
||||
|
||||
{:error, %Ash.Error.Invalid{errors: errors}} =
|
||||
Mv.Accounts.create_register_with_rauthy(%{
|
||||
user_info: user_info,
|
||||
oauth_tokens: %{}
|
||||
})
|
||||
Mv.Accounts.create_register_with_rauthy(
|
||||
%{
|
||||
user_info: user_info,
|
||||
oauth_tokens: %{}
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
# Extract the error
|
||||
password_error =
|
||||
|
|
@ -142,12 +162,12 @@ defmodule MvWeb.OidcE2EFlowTest do
|
|||
{:ok, linked_user} =
|
||||
Mv.Accounts.User
|
||||
|> Ash.Query.filter(id == ^password_user.id)
|
||||
|> Ash.read_one!()
|
||||
|> Ash.read_one!(actor: actor)
|
||||
|> Ash.Changeset.for_update(:link_oidc_id, %{
|
||||
oidc_id: user_info["sub"],
|
||||
oidc_user_info: user_info
|
||||
})
|
||||
|> Ash.update()
|
||||
|> Ash.update(actor: actor)
|
||||
|
||||
# Verify account is now linked
|
||||
assert linked_user.id == password_user.id
|
||||
|
|
@ -158,17 +178,20 @@ defmodule MvWeb.OidcE2EFlowTest do
|
|||
|
||||
# Step 5: User can now sign in via OIDC
|
||||
{:ok, [signed_in_user]} =
|
||||
Mv.Accounts.read_sign_in_with_rauthy(%{
|
||||
user_info: user_info,
|
||||
oauth_tokens: %{}
|
||||
})
|
||||
Mv.Accounts.read_sign_in_with_rauthy(
|
||||
%{
|
||||
user_info: user_info,
|
||||
oauth_tokens: %{}
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
assert signed_in_user.id == password_user.id
|
||||
assert signed_in_user.oidc_id == "oidc_link_888"
|
||||
end
|
||||
|
||||
test "E2E: OIDC collision with different email at provider updates email after linking",
|
||||
%{conn: _conn} do
|
||||
%{conn: _conn, actor: actor} do
|
||||
# Password user with old email
|
||||
password_user =
|
||||
create_test_user(%{
|
||||
|
|
@ -199,12 +222,12 @@ defmodule MvWeb.OidcE2EFlowTest do
|
|||
{:ok, linked_user} =
|
||||
Mv.Accounts.User
|
||||
|> Ash.Query.filter(id == ^password_user.id)
|
||||
|> Ash.read_one!()
|
||||
|> Ash.read_one!(actor: actor)
|
||||
|> Ash.Changeset.for_update(:link_oidc_id, %{
|
||||
oidc_id: updated_user_info["sub"],
|
||||
oidc_user_info: updated_user_info
|
||||
})
|
||||
|> Ash.update()
|
||||
|> Ash.update(actor: actor)
|
||||
|
||||
# Email should be updated to match OIDC provider
|
||||
assert to_string(linked_user.email) == "new@example.com"
|
||||
|
|
@ -213,7 +236,10 @@ defmodule MvWeb.OidcE2EFlowTest do
|
|||
end
|
||||
|
||||
describe "E2E: OIDC with linked member" do
|
||||
test "E2E: email sync to member when linking OIDC to password account", %{conn: _conn} do
|
||||
test "E2E: email sync to member when linking OIDC to password account", %{
|
||||
conn: _conn,
|
||||
actor: actor
|
||||
} do
|
||||
# Create member
|
||||
member =
|
||||
Ash.Seed.seed!(Mv.Membership.Member, %{
|
||||
|
|
@ -239,10 +265,13 @@ defmodule MvWeb.OidcE2EFlowTest do
|
|||
|
||||
# Collision detected
|
||||
{:error, %Ash.Error.Invalid{}} =
|
||||
Mv.Accounts.create_register_with_rauthy(%{
|
||||
user_info: user_info,
|
||||
oauth_tokens: %{}
|
||||
})
|
||||
Mv.Accounts.create_register_with_rauthy(
|
||||
%{
|
||||
user_info: user_info,
|
||||
oauth_tokens: %{}
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
# After password verification, link OIDC with NEW email
|
||||
updated_user_info = %{
|
||||
|
|
@ -253,24 +282,27 @@ defmodule MvWeb.OidcE2EFlowTest do
|
|||
{:ok, linked_user} =
|
||||
Mv.Accounts.User
|
||||
|> Ash.Query.filter(id == ^password_user.id)
|
||||
|> Ash.read_one!()
|
||||
|> Ash.read_one!(actor: actor)
|
||||
|> Ash.Changeset.for_update(:link_oidc_id, %{
|
||||
oidc_id: updated_user_info["sub"],
|
||||
oidc_user_info: updated_user_info
|
||||
})
|
||||
|> Ash.update()
|
||||
|> Ash.update(actor: actor)
|
||||
|
||||
# User email updated
|
||||
assert to_string(linked_user.email) == "newmember@example.com"
|
||||
|
||||
# Member email should be synced
|
||||
{:ok, updated_member} = Ash.get(Mv.Membership.Member, member.id)
|
||||
{:ok, updated_member} = Ash.get(Mv.Membership.Member, member.id, actor: actor)
|
||||
assert to_string(updated_member.email) == "newmember@example.com"
|
||||
end
|
||||
end
|
||||
|
||||
describe "E2E: Security scenarios" do
|
||||
test "E2E: password-only user cannot be accessed via OIDC without password", %{conn: _conn} do
|
||||
test "E2E: password-only user cannot be accessed via OIDC without password", %{
|
||||
conn: _conn,
|
||||
actor: actor
|
||||
} do
|
||||
# Create password user
|
||||
_password_user =
|
||||
create_test_user(%{
|
||||
|
|
@ -287,10 +319,13 @@ defmodule MvWeb.OidcE2EFlowTest do
|
|||
|
||||
# Sign-in should fail (no matching oidc_id)
|
||||
result =
|
||||
Mv.Accounts.read_sign_in_with_rauthy(%{
|
||||
user_info: user_info,
|
||||
oauth_tokens: %{}
|
||||
})
|
||||
Mv.Accounts.read_sign_in_with_rauthy(
|
||||
%{
|
||||
user_info: user_info,
|
||||
oauth_tokens: %{}
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
case result do
|
||||
{:ok, []} ->
|
||||
|
|
@ -305,17 +340,23 @@ defmodule MvWeb.OidcE2EFlowTest do
|
|||
|
||||
# Registration should trigger password requirement
|
||||
{:error, %Ash.Error.Invalid{errors: errors}} =
|
||||
Mv.Accounts.create_register_with_rauthy(%{
|
||||
user_info: user_info,
|
||||
oauth_tokens: %{}
|
||||
})
|
||||
Mv.Accounts.create_register_with_rauthy(
|
||||
%{
|
||||
user_info: user_info,
|
||||
oauth_tokens: %{}
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
assert Enum.any?(errors, fn err ->
|
||||
match?(%Mv.Accounts.User.Errors.PasswordVerificationRequired{}, err)
|
||||
end)
|
||||
end
|
||||
|
||||
test "E2E: user with oidc_id cannot be hijacked by different OIDC provider", %{conn: _conn} do
|
||||
test "E2E: user with oidc_id cannot be hijacked by different OIDC provider", %{
|
||||
conn: _conn,
|
||||
actor: actor
|
||||
} do
|
||||
# User linked to OIDC provider A
|
||||
_user =
|
||||
create_test_user(%{
|
||||
|
|
@ -331,10 +372,13 @@ defmodule MvWeb.OidcE2EFlowTest do
|
|||
|
||||
# Should trigger hard error (not PasswordVerificationRequired)
|
||||
{:error, %Ash.Error.Invalid{errors: errors}} =
|
||||
Mv.Accounts.create_register_with_rauthy(%{
|
||||
user_info: user_info,
|
||||
oauth_tokens: %{}
|
||||
})
|
||||
Mv.Accounts.create_register_with_rauthy(
|
||||
%{
|
||||
user_info: user_info,
|
||||
oauth_tokens: %{}
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
# Should have hard error about "already linked to a different OIDC account"
|
||||
assert Enum.any?(errors, fn
|
||||
|
|
@ -351,7 +395,10 @@ defmodule MvWeb.OidcE2EFlowTest do
|
|||
end)
|
||||
end
|
||||
|
||||
test "E2E: empty string oidc_id is treated as password-only account", %{conn: _conn} do
|
||||
test "E2E: empty string oidc_id is treated as password-only account", %{
|
||||
conn: _conn,
|
||||
actor: actor
|
||||
} do
|
||||
# User with empty oidc_id
|
||||
_password_user =
|
||||
create_test_user(%{
|
||||
|
|
@ -367,10 +414,13 @@ defmodule MvWeb.OidcE2EFlowTest do
|
|||
}
|
||||
|
||||
{:error, %Ash.Error.Invalid{errors: errors}} =
|
||||
Mv.Accounts.create_register_with_rauthy(%{
|
||||
user_info: user_info,
|
||||
oauth_tokens: %{}
|
||||
})
|
||||
Mv.Accounts.create_register_with_rauthy(
|
||||
%{
|
||||
user_info: user_info,
|
||||
oauth_tokens: %{}
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
# Should require password (empty string = no OIDC)
|
||||
assert Enum.any?(errors, fn err ->
|
||||
|
|
@ -380,32 +430,38 @@ defmodule MvWeb.OidcE2EFlowTest do
|
|||
end
|
||||
|
||||
describe "E2E: Error scenarios" do
|
||||
test "E2E: OIDC registration without oidc_id fails", %{conn: _conn} do
|
||||
test "E2E: OIDC registration without oidc_id fails", %{conn: _conn, actor: actor} do
|
||||
user_info = %{
|
||||
"preferred_username" => "noid@example.com"
|
||||
}
|
||||
|
||||
{:error, %Ash.Error.Invalid{errors: errors}} =
|
||||
Mv.Accounts.create_register_with_rauthy(%{
|
||||
user_info: user_info,
|
||||
oauth_tokens: %{}
|
||||
})
|
||||
Mv.Accounts.create_register_with_rauthy(
|
||||
%{
|
||||
user_info: user_info,
|
||||
oauth_tokens: %{}
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
assert Enum.any?(errors, fn err ->
|
||||
match?(%Ash.Error.Changes.InvalidChanges{}, err)
|
||||
end)
|
||||
end
|
||||
|
||||
test "E2E: OIDC registration without email fails", %{conn: _conn} do
|
||||
test "E2E: OIDC registration without email fails", %{conn: _conn, actor: actor} do
|
||||
user_info = %{
|
||||
"sub" => "noemail_123"
|
||||
}
|
||||
|
||||
{:error, %Ash.Error.Invalid{errors: errors}} =
|
||||
Mv.Accounts.create_register_with_rauthy(%{
|
||||
user_info: user_info,
|
||||
oauth_tokens: %{}
|
||||
})
|
||||
Mv.Accounts.create_register_with_rauthy(
|
||||
%{
|
||||
user_info: user_info,
|
||||
oauth_tokens: %{}
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
assert Enum.any?(errors, fn err ->
|
||||
match?(%Ash.Error.Changes.Required{field: :email}, err)
|
||||
|
|
|
|||
|
|
@ -5,8 +5,13 @@ defmodule MvWeb.OidcEmailUpdateTest do
|
|||
"""
|
||||
use MvWeb.ConnCase, async: true
|
||||
|
||||
setup do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
%{actor: system_actor}
|
||||
end
|
||||
|
||||
describe "OIDC user updates email to available email" do
|
||||
test "should succeed and update email" do
|
||||
test "should succeed and update email", %{actor: actor} do
|
||||
# Create OIDC user
|
||||
{:ok, oidc_user} =
|
||||
Mv.Accounts.User
|
||||
|
|
@ -14,7 +19,7 @@ defmodule MvWeb.OidcEmailUpdateTest do
|
|||
email: "original@example.com"
|
||||
})
|
||||
|> Ash.Changeset.force_change_attribute(:oidc_id, "oidc_123")
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
# User logs in via OIDC with NEW email
|
||||
user_info = %{
|
||||
|
|
@ -23,10 +28,13 @@ defmodule MvWeb.OidcEmailUpdateTest do
|
|||
}
|
||||
|
||||
result =
|
||||
Mv.Accounts.create_register_with_rauthy(%{
|
||||
user_info: user_info,
|
||||
oauth_tokens: %{"access_token" => "test_token"}
|
||||
})
|
||||
Mv.Accounts.create_register_with_rauthy(
|
||||
%{
|
||||
user_info: user_info,
|
||||
oauth_tokens: %{"access_token" => "test_token"}
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
# Should succeed and email should be updated
|
||||
assert {:ok, updated_user} = result
|
||||
|
|
@ -37,7 +45,7 @@ defmodule MvWeb.OidcEmailUpdateTest do
|
|||
end
|
||||
|
||||
describe "OIDC user updates email to email of passwordless user" do
|
||||
test "should fail with clear error message" do
|
||||
test "should fail with clear error message", %{actor: actor} do
|
||||
# Create OIDC user
|
||||
{:ok, _oidc_user} =
|
||||
Mv.Accounts.User
|
||||
|
|
@ -45,7 +53,7 @@ defmodule MvWeb.OidcEmailUpdateTest do
|
|||
email: "oidcuser@example.com"
|
||||
})
|
||||
|> Ash.Changeset.force_change_attribute(:oidc_id, "oidc_456")
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
# Create passwordless user with target email
|
||||
{:ok, _passwordless_user} =
|
||||
|
|
@ -53,7 +61,7 @@ defmodule MvWeb.OidcEmailUpdateTest do
|
|||
|> Ash.Changeset.for_create(:create_user, %{
|
||||
email: "taken@example.com"
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
# OIDC user tries to update email to taken email
|
||||
user_info = %{
|
||||
|
|
@ -62,10 +70,13 @@ defmodule MvWeb.OidcEmailUpdateTest do
|
|||
}
|
||||
|
||||
result =
|
||||
Mv.Accounts.create_register_with_rauthy(%{
|
||||
user_info: user_info,
|
||||
oauth_tokens: %{"access_token" => "test_token"}
|
||||
})
|
||||
Mv.Accounts.create_register_with_rauthy(
|
||||
%{
|
||||
user_info: user_info,
|
||||
oauth_tokens: %{"access_token" => "test_token"}
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
# Should fail with email update conflict error
|
||||
assert {:error, %Ash.Error.Invalid{errors: errors}} = result
|
||||
|
|
@ -88,7 +99,7 @@ defmodule MvWeb.OidcEmailUpdateTest do
|
|||
end
|
||||
|
||||
describe "OIDC user updates email to email of password-protected user" do
|
||||
test "should fail with clear error message" do
|
||||
test "should fail with clear error message", %{actor: actor} do
|
||||
# Create OIDC user
|
||||
{:ok, _oidc_user} =
|
||||
Mv.Accounts.User
|
||||
|
|
@ -96,7 +107,7 @@ defmodule MvWeb.OidcEmailUpdateTest do
|
|||
email: "oidcuser2@example.com"
|
||||
})
|
||||
|> Ash.Changeset.force_change_attribute(:oidc_id, "oidc_789")
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
# Create password user with target email (explicitly NO oidc_id)
|
||||
password_user =
|
||||
|
|
@ -106,14 +117,14 @@ defmodule MvWeb.OidcEmailUpdateTest do
|
|||
})
|
||||
|
||||
# Ensure it's a password-only user
|
||||
{:ok, password_user} = Ash.reload(password_user)
|
||||
{:ok, password_user} = Ash.reload(password_user, actor: actor)
|
||||
assert not is_nil(password_user.hashed_password)
|
||||
# Force oidc_id to be nil to avoid any confusion
|
||||
{:ok, password_user} =
|
||||
password_user
|
||||
|> Ash.Changeset.for_update(:update, %{})
|
||||
|> Ash.Changeset.force_change_attribute(:oidc_id, nil)
|
||||
|> Ash.update()
|
||||
|> Ash.update(actor: actor)
|
||||
|
||||
assert is_nil(password_user.oidc_id)
|
||||
|
||||
|
|
@ -124,10 +135,13 @@ defmodule MvWeb.OidcEmailUpdateTest do
|
|||
}
|
||||
|
||||
result =
|
||||
Mv.Accounts.create_register_with_rauthy(%{
|
||||
user_info: user_info,
|
||||
oauth_tokens: %{"access_token" => "test_token"}
|
||||
})
|
||||
Mv.Accounts.create_register_with_rauthy(
|
||||
%{
|
||||
user_info: user_info,
|
||||
oauth_tokens: %{"access_token" => "test_token"}
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
# Should fail with email update conflict error
|
||||
assert {:error, %Ash.Error.Invalid{errors: errors}} = result
|
||||
|
|
@ -150,7 +164,7 @@ defmodule MvWeb.OidcEmailUpdateTest do
|
|||
end
|
||||
|
||||
describe "OIDC user updates email to email of different OIDC user" do
|
||||
test "should fail with clear error message about different OIDC account" do
|
||||
test "should fail with clear error message about different OIDC account", %{actor: actor} do
|
||||
# Create first OIDC user
|
||||
{:ok, _oidc_user1} =
|
||||
Mv.Accounts.User
|
||||
|
|
@ -158,7 +172,7 @@ defmodule MvWeb.OidcEmailUpdateTest do
|
|||
email: "oidcuser1@example.com"
|
||||
})
|
||||
|> Ash.Changeset.force_change_attribute(:oidc_id, "oidc_aaa")
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
# Create second OIDC user with target email
|
||||
{:ok, _oidc_user2} =
|
||||
|
|
@ -167,7 +181,7 @@ defmodule MvWeb.OidcEmailUpdateTest do
|
|||
email: "oidcuser2@example.com"
|
||||
})
|
||||
|> Ash.Changeset.force_change_attribute(:oidc_id, "oidc_bbb")
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
# First OIDC user tries to update email to second user's email
|
||||
user_info = %{
|
||||
|
|
@ -176,10 +190,13 @@ defmodule MvWeb.OidcEmailUpdateTest do
|
|||
}
|
||||
|
||||
result =
|
||||
Mv.Accounts.create_register_with_rauthy(%{
|
||||
user_info: user_info,
|
||||
oauth_tokens: %{"access_token" => "test_token"}
|
||||
})
|
||||
Mv.Accounts.create_register_with_rauthy(
|
||||
%{
|
||||
user_info: user_info,
|
||||
oauth_tokens: %{"access_token" => "test_token"}
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
# Should fail with "already linked to different OIDC account" error
|
||||
assert {:error, %Ash.Error.Invalid{errors: errors}} = result
|
||||
|
|
@ -201,14 +218,14 @@ defmodule MvWeb.OidcEmailUpdateTest do
|
|||
end
|
||||
|
||||
describe "New OIDC user registration scenarios (for comparison)" do
|
||||
test "new OIDC user with email of passwordless user triggers linking flow" do
|
||||
test "new OIDC user with email of passwordless user triggers linking flow", %{actor: actor} do
|
||||
# Create passwordless user
|
||||
{:ok, passwordless_user} =
|
||||
Mv.Accounts.User
|
||||
|> Ash.Changeset.for_create(:create_user, %{
|
||||
email: "passwordless@example.com"
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
# New OIDC user tries to register
|
||||
user_info = %{
|
||||
|
|
@ -217,10 +234,13 @@ defmodule MvWeb.OidcEmailUpdateTest do
|
|||
}
|
||||
|
||||
result =
|
||||
Mv.Accounts.create_register_with_rauthy(%{
|
||||
user_info: user_info,
|
||||
oauth_tokens: %{"access_token" => "test_token"}
|
||||
})
|
||||
Mv.Accounts.create_register_with_rauthy(
|
||||
%{
|
||||
user_info: user_info,
|
||||
oauth_tokens: %{"access_token" => "test_token"}
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
# Should trigger PasswordVerificationRequired (linking flow)
|
||||
assert {:error, %Ash.Error.Invalid{errors: errors}} = result
|
||||
|
|
@ -234,7 +254,7 @@ defmodule MvWeb.OidcEmailUpdateTest do
|
|||
end)
|
||||
end
|
||||
|
||||
test "new OIDC user with email of existing OIDC user shows hard error" do
|
||||
test "new OIDC user with email of existing OIDC user shows hard error", %{actor: actor} do
|
||||
# Create existing OIDC user
|
||||
{:ok, _existing_oidc_user} =
|
||||
Mv.Accounts.User
|
||||
|
|
@ -242,7 +262,7 @@ defmodule MvWeb.OidcEmailUpdateTest do
|
|||
email: "existing@example.com"
|
||||
})
|
||||
|> Ash.Changeset.force_change_attribute(:oidc_id, "oidc_existing")
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
# New OIDC user tries to register with same email
|
||||
user_info = %{
|
||||
|
|
@ -251,10 +271,13 @@ defmodule MvWeb.OidcEmailUpdateTest do
|
|||
}
|
||||
|
||||
result =
|
||||
Mv.Accounts.create_register_with_rauthy(%{
|
||||
user_info: user_info,
|
||||
oauth_tokens: %{"access_token" => "test_token"}
|
||||
})
|
||||
Mv.Accounts.create_register_with_rauthy(
|
||||
%{
|
||||
user_info: user_info,
|
||||
oauth_tokens: %{"access_token" => "test_token"}
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
# Should fail with "already linked to different OIDC account" error
|
||||
assert {:error, %Ash.Error.Invalid{errors: errors}} = result
|
||||
|
|
|
|||
|
|
@ -4,6 +4,11 @@ defmodule MvWeb.OidcIntegrationTest do
|
|||
# Test OIDC callback scenarios by directly calling the actions
|
||||
# This simulates what happens during real OIDC authentication
|
||||
|
||||
setup do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
%{actor: system_actor}
|
||||
end
|
||||
|
||||
describe "OIDC sign-in scenarios" do
|
||||
test "existing OIDC user with unchanged email can sign in" do
|
||||
# Create user with OIDC ID
|
||||
|
|
@ -20,11 +25,16 @@ defmodule MvWeb.OidcIntegrationTest do
|
|||
}
|
||||
|
||||
# Test sign_in_with_rauthy action directly
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
{:ok, [found_user]} =
|
||||
Mv.Accounts.read_sign_in_with_rauthy(%{
|
||||
user_info: user_info,
|
||||
oauth_tokens: %{}
|
||||
})
|
||||
Mv.Accounts.read_sign_in_with_rauthy(
|
||||
%{
|
||||
user_info: user_info,
|
||||
oauth_tokens: %{}
|
||||
},
|
||||
actor: system_actor
|
||||
)
|
||||
|
||||
assert found_user.id == user.id
|
||||
assert to_string(found_user.email) == "existing@example.com"
|
||||
|
|
@ -39,10 +49,15 @@ defmodule MvWeb.OidcIntegrationTest do
|
|||
}
|
||||
|
||||
# Test register_with_rauthy action
|
||||
case Mv.Accounts.create_register_with_rauthy(%{
|
||||
user_info: user_info,
|
||||
oauth_tokens: %{}
|
||||
}) do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
case Mv.Accounts.create_register_with_rauthy(
|
||||
%{
|
||||
user_info: user_info,
|
||||
oauth_tokens: %{}
|
||||
},
|
||||
actor: system_actor
|
||||
) do
|
||||
{:ok, new_user} ->
|
||||
assert to_string(new_user.email) == "newuser@example.com"
|
||||
assert new_user.oidc_id == "brand_new_oidc_456"
|
||||
|
|
@ -73,11 +88,16 @@ defmodule MvWeb.OidcIntegrationTest do
|
|||
}
|
||||
|
||||
# Should NOT find any user (security requirement)
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
result =
|
||||
Mv.Accounts.read_sign_in_with_rauthy(%{
|
||||
user_info: user_info,
|
||||
oauth_tokens: %{}
|
||||
})
|
||||
Mv.Accounts.read_sign_in_with_rauthy(
|
||||
%{
|
||||
user_info: user_info,
|
||||
oauth_tokens: %{}
|
||||
},
|
||||
actor: system_actor
|
||||
)
|
||||
|
||||
# Either returns empty list OR authentication error - both mean "user not found"
|
||||
case result do
|
||||
|
|
@ -107,11 +127,16 @@ defmodule MvWeb.OidcIntegrationTest do
|
|||
"preferred_username" => "oidc.user@example.com"
|
||||
}
|
||||
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
{:ok, [found_user]} =
|
||||
Mv.Accounts.read_sign_in_with_rauthy(%{
|
||||
user_info: correct_user_info,
|
||||
oauth_tokens: %{}
|
||||
})
|
||||
Mv.Accounts.read_sign_in_with_rauthy(
|
||||
%{
|
||||
user_info: correct_user_info,
|
||||
oauth_tokens: %{}
|
||||
},
|
||||
actor: system_actor
|
||||
)
|
||||
|
||||
assert found_user.id == user.id
|
||||
|
||||
|
|
@ -122,10 +147,13 @@ defmodule MvWeb.OidcIntegrationTest do
|
|||
}
|
||||
|
||||
result =
|
||||
Mv.Accounts.read_sign_in_with_rauthy(%{
|
||||
user_info: wrong_user_info,
|
||||
oauth_tokens: %{}
|
||||
})
|
||||
Mv.Accounts.read_sign_in_with_rauthy(
|
||||
%{
|
||||
user_info: wrong_user_info,
|
||||
oauth_tokens: %{}
|
||||
},
|
||||
actor: system_actor
|
||||
)
|
||||
|
||||
# Either returns empty list OR authentication error - both mean "user not found"
|
||||
case result do
|
||||
|
|
@ -154,11 +182,16 @@ defmodule MvWeb.OidcIntegrationTest do
|
|||
"preferred_username" => "empty.oidc@example.com"
|
||||
}
|
||||
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
result =
|
||||
Mv.Accounts.read_sign_in_with_rauthy(%{
|
||||
user_info: user_info,
|
||||
oauth_tokens: %{}
|
||||
})
|
||||
Mv.Accounts.read_sign_in_with_rauthy(
|
||||
%{
|
||||
user_info: user_info,
|
||||
oauth_tokens: %{}
|
||||
},
|
||||
actor: system_actor
|
||||
)
|
||||
|
||||
# Either returns empty list OR authentication error - both mean "user not found"
|
||||
case result do
|
||||
|
|
@ -189,11 +222,16 @@ defmodule MvWeb.OidcIntegrationTest do
|
|||
"preferred_username" => "conflict@example.com"
|
||||
}
|
||||
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
result =
|
||||
Mv.Accounts.create_register_with_rauthy(%{
|
||||
user_info: user_info,
|
||||
oauth_tokens: %{}
|
||||
})
|
||||
Mv.Accounts.create_register_with_rauthy(
|
||||
%{
|
||||
user_info: user_info,
|
||||
oauth_tokens: %{}
|
||||
},
|
||||
actor: system_actor
|
||||
)
|
||||
|
||||
# Should fail with hard error (not PasswordVerificationRequired)
|
||||
# This prevents someone with OIDC provider B from taking over an account
|
||||
|
|
@ -220,11 +258,16 @@ defmodule MvWeb.OidcIntegrationTest do
|
|||
"preferred_username" => "nosub@example.com"
|
||||
}
|
||||
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
result =
|
||||
Mv.Accounts.create_register_with_rauthy(%{
|
||||
user_info: user_info,
|
||||
oauth_tokens: %{}
|
||||
})
|
||||
Mv.Accounts.create_register_with_rauthy(
|
||||
%{
|
||||
user_info: user_info,
|
||||
oauth_tokens: %{}
|
||||
},
|
||||
actor: system_actor
|
||||
)
|
||||
|
||||
assert {:error,
|
||||
%Ash.Error.Invalid{
|
||||
|
|
@ -239,11 +282,16 @@ defmodule MvWeb.OidcIntegrationTest do
|
|||
"sub" => "noemail_oidc_123"
|
||||
}
|
||||
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
result =
|
||||
Mv.Accounts.create_register_with_rauthy(%{
|
||||
user_info: user_info,
|
||||
oauth_tokens: %{}
|
||||
})
|
||||
Mv.Accounts.create_register_with_rauthy(
|
||||
%{
|
||||
user_info: user_info,
|
||||
oauth_tokens: %{}
|
||||
},
|
||||
actor: system_actor
|
||||
)
|
||||
|
||||
assert {:error, %Ash.Error.Invalid{errors: errors}} = result
|
||||
|
||||
|
|
@ -264,11 +312,16 @@ defmodule MvWeb.OidcIntegrationTest do
|
|||
"preferred_username" => "new@example.com"
|
||||
}
|
||||
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
{:ok, user} =
|
||||
Mv.Accounts.create_register_with_rauthy(%{
|
||||
user_info: user_info,
|
||||
oauth_tokens: %{}
|
||||
})
|
||||
Mv.Accounts.create_register_with_rauthy(
|
||||
%{
|
||||
user_info: user_info,
|
||||
oauth_tokens: %{}
|
||||
},
|
||||
actor: system_actor
|
||||
)
|
||||
|
||||
assert user.id == existing_user.id
|
||||
assert to_string(user.email) == "new@example.com"
|
||||
|
|
@ -281,11 +334,16 @@ defmodule MvWeb.OidcIntegrationTest do
|
|||
"preferred_username" => "altid@example.com"
|
||||
}
|
||||
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
{:ok, user} =
|
||||
Mv.Accounts.create_register_with_rauthy(%{
|
||||
user_info: user_info,
|
||||
oauth_tokens: %{}
|
||||
})
|
||||
Mv.Accounts.create_register_with_rauthy(
|
||||
%{
|
||||
user_info: user_info,
|
||||
oauth_tokens: %{}
|
||||
},
|
||||
actor: system_actor
|
||||
)
|
||||
|
||||
assert user.oidc_id == "alt_oidc_id_123"
|
||||
assert to_string(user.email) == "altid@example.com"
|
||||
|
|
|
|||
|
|
@ -8,9 +8,15 @@ defmodule MvWeb.OidcPasswordLinkingTest do
|
|||
use MvWeb.ConnCase, async: true
|
||||
require Ash.Query
|
||||
|
||||
setup do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
%{actor: system_actor}
|
||||
end
|
||||
|
||||
describe "OIDC login with existing email (no oidc_id) - Email Collision" do
|
||||
@tag :test_proposal
|
||||
test "OIDC register with existing password user email fails with PasswordVerificationRequired" do
|
||||
test "OIDC register with existing password user email fails with PasswordVerificationRequired",
|
||||
%{actor: actor} do
|
||||
# Create password-only user
|
||||
existing_user =
|
||||
create_test_user(%{
|
||||
|
|
@ -26,10 +32,13 @@ defmodule MvWeb.OidcPasswordLinkingTest do
|
|||
}
|
||||
|
||||
result =
|
||||
Mv.Accounts.create_register_with_rauthy(%{
|
||||
user_info: user_info,
|
||||
oauth_tokens: %{}
|
||||
})
|
||||
Mv.Accounts.create_register_with_rauthy(
|
||||
%{
|
||||
user_info: user_info,
|
||||
oauth_tokens: %{}
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
# Should fail with PasswordVerificationRequired error
|
||||
assert {:error, %Ash.Error.Invalid{errors: errors}} = result
|
||||
|
|
@ -47,7 +56,7 @@ defmodule MvWeb.OidcPasswordLinkingTest do
|
|||
end
|
||||
|
||||
@tag :test_proposal
|
||||
test "PasswordVerificationRequired error contains necessary context" do
|
||||
test "PasswordVerificationRequired error contains necessary context", %{actor: actor} do
|
||||
existing_user =
|
||||
create_test_user(%{
|
||||
email: "test@example.com",
|
||||
|
|
@ -61,10 +70,13 @@ defmodule MvWeb.OidcPasswordLinkingTest do
|
|||
}
|
||||
|
||||
{:error, %Ash.Error.Invalid{errors: errors}} =
|
||||
Mv.Accounts.create_register_with_rauthy(%{
|
||||
user_info: user_info,
|
||||
oauth_tokens: %{}
|
||||
})
|
||||
Mv.Accounts.create_register_with_rauthy(
|
||||
%{
|
||||
user_info: user_info,
|
||||
oauth_tokens: %{}
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
password_error =
|
||||
Enum.find(errors, fn err ->
|
||||
|
|
@ -78,7 +90,7 @@ defmodule MvWeb.OidcPasswordLinkingTest do
|
|||
end
|
||||
|
||||
@tag :test_proposal
|
||||
test "after successful password verification, oidc_id can be set" do
|
||||
test "after successful password verification, oidc_id can be set", %{actor: actor} do
|
||||
# Create password user
|
||||
user =
|
||||
create_test_user(%{
|
||||
|
|
@ -97,12 +109,12 @@ defmodule MvWeb.OidcPasswordLinkingTest do
|
|||
{:ok, updated_user} =
|
||||
Mv.Accounts.User
|
||||
|> Ash.Query.filter(id == ^user.id)
|
||||
|> Ash.read_one!()
|
||||
|> Ash.read_one!(actor: actor)
|
||||
|> Ash.Changeset.for_update(:link_oidc_id, %{
|
||||
oidc_id: user_info["sub"],
|
||||
oidc_user_info: user_info
|
||||
})
|
||||
|> Ash.update()
|
||||
|> Ash.update(actor: actor)
|
||||
|
||||
assert updated_user.id == user.id
|
||||
assert updated_user.oidc_id == "linked_oidc_555"
|
||||
|
|
@ -112,7 +124,7 @@ defmodule MvWeb.OidcPasswordLinkingTest do
|
|||
end
|
||||
|
||||
@tag :test_proposal
|
||||
test "password verification with wrong password keeps oidc_id as nil" do
|
||||
test "password verification with wrong password keeps oidc_id as nil", %{actor: actor} do
|
||||
# This test verifies that if password verification fails,
|
||||
# the oidc_id should NOT be set
|
||||
|
||||
|
|
@ -131,7 +143,7 @@ defmodule MvWeb.OidcPasswordLinkingTest do
|
|||
# before link_oidc_id is called, so here we just verify the user state
|
||||
|
||||
# User should still have no oidc_id (no linking happened)
|
||||
{:ok, unchanged_user} = Ash.get(Mv.Accounts.User, user.id)
|
||||
{:ok, unchanged_user} = Ash.get(Mv.Accounts.User, user.id, actor: actor)
|
||||
assert is_nil(unchanged_user.oidc_id)
|
||||
assert unchanged_user.hashed_password == user.hashed_password
|
||||
end
|
||||
|
|
@ -139,7 +151,7 @@ defmodule MvWeb.OidcPasswordLinkingTest do
|
|||
|
||||
describe "OIDC login with email of user having different oidc_id - Account Conflict" do
|
||||
@tag :test_proposal
|
||||
test "OIDC register with email of user having different oidc_id fails" do
|
||||
test "OIDC register with email of user having different oidc_id fails", %{actor: actor} do
|
||||
# User already linked to OIDC provider A
|
||||
_existing_user =
|
||||
create_test_user(%{
|
||||
|
|
@ -155,10 +167,13 @@ defmodule MvWeb.OidcPasswordLinkingTest do
|
|||
}
|
||||
|
||||
result =
|
||||
Mv.Accounts.create_register_with_rauthy(%{
|
||||
user_info: user_info,
|
||||
oauth_tokens: %{}
|
||||
})
|
||||
Mv.Accounts.create_register_with_rauthy(
|
||||
%{
|
||||
user_info: user_info,
|
||||
oauth_tokens: %{}
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
# Should fail - cannot link different OIDC account to same email
|
||||
assert {:error, %Ash.Error.Invalid{errors: errors}} = result
|
||||
|
|
@ -171,7 +186,7 @@ defmodule MvWeb.OidcPasswordLinkingTest do
|
|||
end
|
||||
|
||||
@tag :test_proposal
|
||||
test "existing OIDC user email remains unchanged when oidc_id matches" do
|
||||
test "existing OIDC user email remains unchanged when oidc_id matches", %{actor: actor} do
|
||||
user =
|
||||
create_test_user(%{
|
||||
email: "oidc@example.com",
|
||||
|
|
@ -186,10 +201,13 @@ defmodule MvWeb.OidcPasswordLinkingTest do
|
|||
|
||||
# This should work via upsert
|
||||
{:ok, updated_user} =
|
||||
Mv.Accounts.create_register_with_rauthy(%{
|
||||
user_info: user_info,
|
||||
oauth_tokens: %{}
|
||||
})
|
||||
Mv.Accounts.create_register_with_rauthy(
|
||||
%{
|
||||
user_info: user_info,
|
||||
oauth_tokens: %{}
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
assert updated_user.id == user.id
|
||||
assert updated_user.oidc_id == "oidc_stable_789"
|
||||
|
|
@ -199,7 +217,7 @@ defmodule MvWeb.OidcPasswordLinkingTest do
|
|||
|
||||
describe "Email update during OIDC linking" do
|
||||
@tag :test_proposal
|
||||
test "linking OIDC to password account updates email if different in OIDC" do
|
||||
test "linking OIDC to password account updates email if different in OIDC", %{actor: actor} do
|
||||
# Password user with old email
|
||||
user =
|
||||
create_test_user(%{
|
||||
|
|
@ -218,19 +236,19 @@ defmodule MvWeb.OidcPasswordLinkingTest do
|
|||
{:ok, updated_user} =
|
||||
Mv.Accounts.User
|
||||
|> Ash.Query.filter(id == ^user.id)
|
||||
|> Ash.read_one!()
|
||||
|> Ash.read_one!(actor: actor)
|
||||
|> Ash.Changeset.for_update(:link_oidc_id, %{
|
||||
oidc_id: user_info["sub"],
|
||||
oidc_user_info: user_info
|
||||
})
|
||||
|> Ash.update()
|
||||
|> Ash.update(actor: actor)
|
||||
|
||||
assert updated_user.oidc_id == "oidc_link_999"
|
||||
assert to_string(updated_user.email) == "newemail@example.com"
|
||||
end
|
||||
|
||||
@tag :test_proposal
|
||||
test "email change during linking triggers member email sync" do
|
||||
test "email change during linking triggers member email sync", %{actor: actor} do
|
||||
# Create member
|
||||
member =
|
||||
Ash.Seed.seed!(Mv.Membership.Member, %{
|
||||
|
|
@ -257,25 +275,25 @@ defmodule MvWeb.OidcPasswordLinkingTest do
|
|||
{:ok, updated_user} =
|
||||
Mv.Accounts.User
|
||||
|> Ash.Query.filter(id == ^user.id)
|
||||
|> Ash.read_one!()
|
||||
|> Ash.read_one!(actor: actor)
|
||||
|> Ash.Changeset.for_update(:link_oidc_id, %{
|
||||
oidc_id: user_info["sub"],
|
||||
oidc_user_info: user_info
|
||||
})
|
||||
|> Ash.update()
|
||||
|> Ash.update(actor: actor)
|
||||
|
||||
# Verify user email changed
|
||||
assert to_string(updated_user.email) == "newemail@example.com"
|
||||
|
||||
# Verify member email was synced
|
||||
{:ok, updated_member} = Ash.get(Mv.Membership.Member, member.id)
|
||||
{:ok, updated_member} = Ash.get(Mv.Membership.Member, member.id, actor: actor)
|
||||
assert to_string(updated_member.email) == "newemail@example.com"
|
||||
end
|
||||
end
|
||||
|
||||
describe "Edge cases" do
|
||||
@tag :test_proposal
|
||||
test "user with empty string oidc_id is treated as password-only user" do
|
||||
test "user with empty string oidc_id is treated as password-only user", %{actor: actor} do
|
||||
_user =
|
||||
create_test_user(%{
|
||||
email: "empty@example.com",
|
||||
|
|
@ -290,10 +308,13 @@ defmodule MvWeb.OidcPasswordLinkingTest do
|
|||
}
|
||||
|
||||
result =
|
||||
Mv.Accounts.create_register_with_rauthy(%{
|
||||
user_info: user_info,
|
||||
oauth_tokens: %{}
|
||||
})
|
||||
Mv.Accounts.create_register_with_rauthy(
|
||||
%{
|
||||
user_info: user_info,
|
||||
oauth_tokens: %{}
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
# Should trigger PasswordVerificationRequired (empty string = no OIDC)
|
||||
assert {:error, %Ash.Error.Invalid{errors: errors}} = result
|
||||
|
|
@ -307,7 +328,7 @@ defmodule MvWeb.OidcPasswordLinkingTest do
|
|||
end
|
||||
|
||||
@tag :test_proposal
|
||||
test "cannot link same oidc_id to multiple users" do
|
||||
test "cannot link same oidc_id to multiple users", %{actor: actor} do
|
||||
# User 1 with OIDC
|
||||
_user1 =
|
||||
create_test_user(%{
|
||||
|
|
@ -323,7 +344,7 @@ defmodule MvWeb.OidcPasswordLinkingTest do
|
|||
email: "user2@example.com"
|
||||
})
|
||||
|> Ash.Changeset.force_change_attribute(:oidc_id, "shared_oidc_333")
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
# Should fail due to unique constraint on oidc_id
|
||||
assert match?({:error, %Ash.Error.Invalid{}}, result)
|
||||
|
|
@ -337,14 +358,16 @@ defmodule MvWeb.OidcPasswordLinkingTest do
|
|||
end
|
||||
|
||||
describe "OIDC login with passwordless user - Requires Linking Flow" do
|
||||
test "user without password and without oidc_id triggers PasswordVerificationRequired" do
|
||||
test "user without password and without oidc_id triggers PasswordVerificationRequired", %{
|
||||
actor: actor
|
||||
} do
|
||||
# Create user without password (e.g., invited user)
|
||||
{:ok, existing_user} =
|
||||
Mv.Accounts.User
|
||||
|> Ash.Changeset.for_create(:create_user, %{
|
||||
email: "invited@example.com"
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
# Verify user has no password and no oidc_id
|
||||
assert is_nil(existing_user.hashed_password)
|
||||
|
|
@ -372,14 +395,14 @@ defmodule MvWeb.OidcPasswordLinkingTest do
|
|||
end)
|
||||
end
|
||||
|
||||
test "user without password but WITH password later requires verification" do
|
||||
test "user without password but WITH password later requires verification", %{actor: actor} do
|
||||
# Create user without password first
|
||||
{:ok, user} =
|
||||
Mv.Accounts.User
|
||||
|> Ash.Changeset.for_create(:create_user, %{
|
||||
email: "added-password@example.com"
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
# User sets password later (using admin action)
|
||||
{:ok, user_with_password} =
|
||||
|
|
@ -387,7 +410,7 @@ defmodule MvWeb.OidcPasswordLinkingTest do
|
|||
|> Ash.Changeset.for_update(:admin_set_password, %{
|
||||
password: "newpassword123"
|
||||
})
|
||||
|> Ash.update()
|
||||
|> Ash.update(actor: actor)
|
||||
|
||||
assert not is_nil(user_with_password.hashed_password)
|
||||
|
||||
|
|
@ -398,10 +421,13 @@ defmodule MvWeb.OidcPasswordLinkingTest do
|
|||
}
|
||||
|
||||
result =
|
||||
Mv.Accounts.create_register_with_rauthy(%{
|
||||
user_info: user_info,
|
||||
oauth_tokens: %{"access_token" => "test_token"}
|
||||
})
|
||||
Mv.Accounts.create_register_with_rauthy(
|
||||
%{
|
||||
user_info: user_info,
|
||||
oauth_tokens: %{"access_token" => "test_token"}
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
# Should fail with PasswordVerificationRequired
|
||||
assert {:error, %Ash.Error.Invalid{}} = result
|
||||
|
|
@ -414,7 +440,7 @@ defmodule MvWeb.OidcPasswordLinkingTest do
|
|||
end
|
||||
|
||||
describe "OIDC login with different oidc_id - Hard Error" do
|
||||
test "user with different oidc_id cannot be linked (hard error)" do
|
||||
test "user with different oidc_id cannot be linked (hard error)", %{actor: actor} do
|
||||
# Create user with existing OIDC ID
|
||||
{:ok, existing_user} =
|
||||
Mv.Accounts.User
|
||||
|
|
@ -422,7 +448,7 @@ defmodule MvWeb.OidcPasswordLinkingTest do
|
|||
email: "already-linked@example.com"
|
||||
})
|
||||
|> Ash.Changeset.force_change_attribute(:oidc_id, "original_oidc_999")
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
assert existing_user.oidc_id == "original_oidc_999"
|
||||
|
||||
|
|
@ -433,10 +459,13 @@ defmodule MvWeb.OidcPasswordLinkingTest do
|
|||
}
|
||||
|
||||
result =
|
||||
Mv.Accounts.create_register_with_rauthy(%{
|
||||
user_info: user_info,
|
||||
oauth_tokens: %{"access_token" => "test_token"}
|
||||
})
|
||||
Mv.Accounts.create_register_with_rauthy(
|
||||
%{
|
||||
user_info: user_info,
|
||||
oauth_tokens: %{"access_token" => "test_token"}
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
# Should fail with hard error (not PasswordVerificationRequired)
|
||||
assert {:error, %Ash.Error.Invalid{}} = result
|
||||
|
|
@ -459,7 +488,7 @@ defmodule MvWeb.OidcPasswordLinkingTest do
|
|||
end)
|
||||
end
|
||||
|
||||
test "cannot link different oidc_id even with password verification" do
|
||||
test "cannot link different oidc_id even with password verification", %{actor: actor} do
|
||||
# Create user with password AND existing OIDC ID
|
||||
existing_user =
|
||||
create_test_user(%{
|
||||
|
|
@ -478,10 +507,13 @@ defmodule MvWeb.OidcPasswordLinkingTest do
|
|||
}
|
||||
|
||||
result =
|
||||
Mv.Accounts.create_register_with_rauthy(%{
|
||||
user_info: user_info,
|
||||
oauth_tokens: %{"access_token" => "test_token"}
|
||||
})
|
||||
Mv.Accounts.create_register_with_rauthy(
|
||||
%{
|
||||
user_info: user_info,
|
||||
oauth_tokens: %{"access_token" => "test_token"}
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
# Should fail - cannot link different OIDC ID
|
||||
assert {:error, %Ash.Error.Invalid{}} = result
|
||||
|
|
|
|||
|
|
@ -7,15 +7,20 @@ defmodule MvWeb.OidcPasswordlessLinkingTest do
|
|||
"""
|
||||
use MvWeb.ConnCase, async: true
|
||||
|
||||
setup do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
%{actor: system_actor}
|
||||
end
|
||||
|
||||
describe "Passwordless user - Automatic linking via special action" do
|
||||
test "passwordless user can be linked via link_passwordless_oidc action" do
|
||||
test "passwordless user can be linked via link_passwordless_oidc action", %{actor: actor} do
|
||||
# Create user without password (e.g., invited user)
|
||||
{:ok, existing_user} =
|
||||
Mv.Accounts.User
|
||||
|> Ash.Changeset.for_create(:create_user, %{
|
||||
email: "invited@example.com"
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
# Verify user has no password and no oidc_id
|
||||
assert is_nil(existing_user.hashed_password)
|
||||
|
|
@ -31,7 +36,7 @@ defmodule MvWeb.OidcPasswordlessLinkingTest do
|
|||
"preferred_username" => "invited@example.com"
|
||||
}
|
||||
})
|
||||
|> Ash.update()
|
||||
|> Ash.update(actor: actor)
|
||||
|
||||
# User should now have oidc_id linked
|
||||
assert linked_user.oidc_id == "auto_link_oidc_123"
|
||||
|
|
@ -47,20 +52,22 @@ defmodule MvWeb.OidcPasswordlessLinkingTest do
|
|||
},
|
||||
oauth_tokens: %{"access_token" => "test_token"}
|
||||
})
|
||||
|> Ash.read_one()
|
||||
|> Ash.read_one(actor: actor)
|
||||
|
||||
assert {:ok, signed_in_user} = result
|
||||
assert signed_in_user.id == existing_user.id
|
||||
end
|
||||
|
||||
test "passwordless user triggers PasswordVerificationRequired for linking flow" do
|
||||
test "passwordless user triggers PasswordVerificationRequired for linking flow", %{
|
||||
actor: actor
|
||||
} do
|
||||
# Create passwordless user
|
||||
{:ok, existing_user} =
|
||||
Mv.Accounts.User
|
||||
|> Ash.Changeset.for_create(:create_user, %{
|
||||
email: "passwordless@example.com"
|
||||
})
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
assert is_nil(existing_user.hashed_password)
|
||||
assert is_nil(existing_user.oidc_id)
|
||||
|
|
@ -95,7 +102,7 @@ defmodule MvWeb.OidcPasswordlessLinkingTest do
|
|||
end
|
||||
|
||||
describe "User with different OIDC ID - Hard Error" do
|
||||
test "user with different oidc_id gets hard error, not password verification" do
|
||||
test "user with different oidc_id gets hard error, not password verification", %{actor: actor} do
|
||||
# Create user with existing OIDC ID
|
||||
{:ok, _existing_user} =
|
||||
Mv.Accounts.User
|
||||
|
|
@ -103,7 +110,7 @@ defmodule MvWeb.OidcPasswordlessLinkingTest do
|
|||
email: "already-linked@example.com"
|
||||
})
|
||||
|> Ash.Changeset.force_change_attribute(:oidc_id, "original_oidc_999")
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
# Try to register with same email but different OIDC ID
|
||||
user_info = %{
|
||||
|
|
@ -138,7 +145,7 @@ defmodule MvWeb.OidcPasswordlessLinkingTest do
|
|||
end)
|
||||
end
|
||||
|
||||
test "passwordless user with different oidc_id also gets hard error" do
|
||||
test "passwordless user with different oidc_id also gets hard error", %{actor: actor} do
|
||||
# Create passwordless user with OIDC ID
|
||||
{:ok, existing_user} =
|
||||
Mv.Accounts.User
|
||||
|
|
@ -146,7 +153,7 @@ defmodule MvWeb.OidcPasswordlessLinkingTest do
|
|||
email: "passwordless-linked@example.com"
|
||||
})
|
||||
|> Ash.Changeset.force_change_attribute(:oidc_id, "first_oidc_777")
|
||||
|> Ash.create()
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
assert is_nil(existing_user.hashed_password)
|
||||
assert existing_user.oidc_id == "first_oidc_777"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue