271 lines
8.9 KiB
Elixir
271 lines
8.9 KiB
Elixir
defmodule MvWeb.OidcEmailUpdateTest do
|
|
@moduledoc """
|
|
Tests for OIDC email updates - when an existing OIDC user changes their email
|
|
in the OIDC provider and logs in again.
|
|
"""
|
|
use MvWeb.ConnCase, async: true
|
|
|
|
describe "OIDC user updates email to available email" do
|
|
test "should succeed and update email" do
|
|
# Create OIDC user
|
|
{:ok, oidc_user} =
|
|
Mv.Accounts.User
|
|
|> Ash.Changeset.for_create(:create_user, %{
|
|
email: "original@example.com"
|
|
})
|
|
|> Ash.Changeset.force_change_attribute(:oidc_id, "oidc_123")
|
|
|> Ash.create()
|
|
|
|
# User logs in via OIDC with NEW email
|
|
user_info = %{
|
|
"sub" => "oidc_123",
|
|
"preferred_username" => "newemail@example.com"
|
|
}
|
|
|
|
result =
|
|
Mv.Accounts.create_register_with_rauthy(%{
|
|
user_info: user_info,
|
|
oauth_tokens: %{"access_token" => "test_token"}
|
|
})
|
|
|
|
# Should succeed and email should be updated
|
|
assert {:ok, updated_user} = result
|
|
assert updated_user.id == oidc_user.id
|
|
assert to_string(updated_user.email) == "newemail@example.com"
|
|
assert updated_user.oidc_id == "oidc_123"
|
|
end
|
|
end
|
|
|
|
describe "OIDC user updates email to email of passwordless user" do
|
|
test "should fail with clear error message" do
|
|
# Create OIDC user
|
|
{:ok, _oidc_user} =
|
|
Mv.Accounts.User
|
|
|> Ash.Changeset.for_create(:create_user, %{
|
|
email: "oidcuser@example.com"
|
|
})
|
|
|> Ash.Changeset.force_change_attribute(:oidc_id, "oidc_456")
|
|
|> Ash.create()
|
|
|
|
# Create passwordless user with target email
|
|
{:ok, _passwordless_user} =
|
|
Mv.Accounts.User
|
|
|> Ash.Changeset.for_create(:create_user, %{
|
|
email: "taken@example.com"
|
|
})
|
|
|> Ash.create()
|
|
|
|
# OIDC user tries to update email to taken email
|
|
user_info = %{
|
|
"sub" => "oidc_456",
|
|
"preferred_username" => "taken@example.com"
|
|
}
|
|
|
|
result =
|
|
Mv.Accounts.create_register_with_rauthy(%{
|
|
user_info: user_info,
|
|
oauth_tokens: %{"access_token" => "test_token"}
|
|
})
|
|
|
|
# Should fail with email update conflict error
|
|
assert {:error, %Ash.Error.Invalid{errors: errors}} = result
|
|
|
|
# Should contain error about email being registered to another account
|
|
assert Enum.any?(errors, fn
|
|
%Ash.Error.Changes.InvalidAttribute{field: :email, message: message} ->
|
|
String.contains?(message, "Cannot update email to") and
|
|
String.contains?(message, "already registered to another account")
|
|
|
|
_ ->
|
|
false
|
|
end)
|
|
|
|
# Should NOT contain PasswordVerificationRequired
|
|
refute Enum.any?(errors, fn err ->
|
|
match?(%Mv.Accounts.User.Errors.PasswordVerificationRequired{}, err)
|
|
end)
|
|
end
|
|
end
|
|
|
|
describe "OIDC user updates email to email of password-protected user" do
|
|
test "should fail with clear error message" do
|
|
# Create OIDC user
|
|
{:ok, _oidc_user} =
|
|
Mv.Accounts.User
|
|
|> Ash.Changeset.for_create(:create_user, %{
|
|
email: "oidcuser2@example.com"
|
|
})
|
|
|> Ash.Changeset.force_change_attribute(:oidc_id, "oidc_789")
|
|
|> Ash.create()
|
|
|
|
# Create password user with target email (explicitly NO oidc_id)
|
|
password_user =
|
|
create_test_user(%{
|
|
email: "passworduser@example.com",
|
|
password: "securepass123"
|
|
})
|
|
|
|
# Ensure it's a password-only user
|
|
{:ok, password_user} = Ash.reload(password_user)
|
|
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()
|
|
|
|
assert is_nil(password_user.oidc_id)
|
|
|
|
# OIDC user tries to update email to password user's email
|
|
user_info = %{
|
|
"sub" => "oidc_789",
|
|
"preferred_username" => "passworduser@example.com"
|
|
}
|
|
|
|
result =
|
|
Mv.Accounts.create_register_with_rauthy(%{
|
|
user_info: user_info,
|
|
oauth_tokens: %{"access_token" => "test_token"}
|
|
})
|
|
|
|
# Should fail with email update conflict error
|
|
assert {:error, %Ash.Error.Invalid{errors: errors}} = result
|
|
|
|
# Should contain error about email being registered to another account
|
|
assert Enum.any?(errors, fn
|
|
%Ash.Error.Changes.InvalidAttribute{field: :email, message: message} ->
|
|
String.contains?(message, "Cannot update email to") and
|
|
String.contains?(message, "already registered to another account")
|
|
|
|
_ ->
|
|
false
|
|
end)
|
|
|
|
# Should NOT contain PasswordVerificationRequired
|
|
refute Enum.any?(errors, fn err ->
|
|
match?(%Mv.Accounts.User.Errors.PasswordVerificationRequired{}, err)
|
|
end)
|
|
end
|
|
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
|
|
# Create first OIDC user
|
|
{:ok, _oidc_user1} =
|
|
Mv.Accounts.User
|
|
|> Ash.Changeset.for_create(:create_user, %{
|
|
email: "oidcuser1@example.com"
|
|
})
|
|
|> Ash.Changeset.force_change_attribute(:oidc_id, "oidc_aaa")
|
|
|> Ash.create()
|
|
|
|
# Create second OIDC user with target email
|
|
{:ok, _oidc_user2} =
|
|
Mv.Accounts.User
|
|
|> Ash.Changeset.for_create(:create_user, %{
|
|
email: "oidcuser2@example.com"
|
|
})
|
|
|> Ash.Changeset.force_change_attribute(:oidc_id, "oidc_bbb")
|
|
|> Ash.create()
|
|
|
|
# First OIDC user tries to update email to second user's email
|
|
user_info = %{
|
|
"sub" => "oidc_aaa",
|
|
"preferred_username" => "oidcuser2@example.com"
|
|
}
|
|
|
|
result =
|
|
Mv.Accounts.create_register_with_rauthy(%{
|
|
user_info: user_info,
|
|
oauth_tokens: %{"access_token" => "test_token"}
|
|
})
|
|
|
|
# Should fail with "already linked to different OIDC account" error
|
|
assert {:error, %Ash.Error.Invalid{errors: errors}} = result
|
|
|
|
# Should contain error about different OIDC account
|
|
assert Enum.any?(errors, fn
|
|
%Ash.Error.Changes.InvalidAttribute{field: :email, message: message} ->
|
|
String.contains?(message, "already linked to a different OIDC account")
|
|
|
|
_ ->
|
|
false
|
|
end)
|
|
|
|
# Should NOT contain PasswordVerificationRequired
|
|
refute Enum.any?(errors, fn err ->
|
|
match?(%Mv.Accounts.User.Errors.PasswordVerificationRequired{}, err)
|
|
end)
|
|
end
|
|
end
|
|
|
|
describe "New OIDC user registration scenarios (for comparison)" do
|
|
test "new OIDC user with email of passwordless user triggers linking flow" do
|
|
# Create passwordless user
|
|
{:ok, passwordless_user} =
|
|
Mv.Accounts.User
|
|
|> Ash.Changeset.for_create(:create_user, %{
|
|
email: "passwordless@example.com"
|
|
})
|
|
|> Ash.create()
|
|
|
|
# New OIDC user tries to register
|
|
user_info = %{
|
|
"sub" => "new_oidc_999",
|
|
"preferred_username" => "passwordless@example.com"
|
|
}
|
|
|
|
result =
|
|
Mv.Accounts.create_register_with_rauthy(%{
|
|
user_info: user_info,
|
|
oauth_tokens: %{"access_token" => "test_token"}
|
|
})
|
|
|
|
# Should trigger PasswordVerificationRequired (linking flow)
|
|
assert {:error, %Ash.Error.Invalid{errors: errors}} = result
|
|
|
|
assert Enum.any?(errors, fn
|
|
%Mv.Accounts.User.Errors.PasswordVerificationRequired{user_id: user_id} ->
|
|
user_id == passwordless_user.id
|
|
|
|
_ ->
|
|
false
|
|
end)
|
|
end
|
|
|
|
test "new OIDC user with email of existing OIDC user shows hard error" do
|
|
# Create existing OIDC user
|
|
{:ok, _existing_oidc_user} =
|
|
Mv.Accounts.User
|
|
|> Ash.Changeset.for_create(:create_user, %{
|
|
email: "existing@example.com"
|
|
})
|
|
|> Ash.Changeset.force_change_attribute(:oidc_id, "oidc_existing")
|
|
|> Ash.create()
|
|
|
|
# New OIDC user tries to register with same email
|
|
user_info = %{
|
|
"sub" => "oidc_new",
|
|
"preferred_username" => "existing@example.com"
|
|
}
|
|
|
|
result =
|
|
Mv.Accounts.create_register_with_rauthy(%{
|
|
user_info: user_info,
|
|
oauth_tokens: %{"access_token" => "test_token"}
|
|
})
|
|
|
|
# Should fail with "already linked to different OIDC account" error
|
|
assert {:error, %Ash.Error.Invalid{errors: errors}} = result
|
|
|
|
assert Enum.any?(errors, fn
|
|
%Ash.Error.Changes.InvalidAttribute{field: :email, message: message} ->
|
|
String.contains?(message, "already linked to a different OIDC account")
|
|
|
|
_ ->
|
|
false
|
|
end)
|
|
end
|
|
end
|
|
end
|