feat(auth): streamline verified Google onboarding

This commit is contained in:
SimpleTest 2026-07-25 02:03:20 +03:00
parent 51d55fa73f
commit bcbe417a79
9 changed files with 482 additions and 60 deletions

View File

@ -32,6 +32,7 @@ defmodule WhoNeedHelp.Accounts do
"display_name" => :display_name,
"email" => :email,
"email_verified" => :email_verified,
"hosted_domain" => :hosted_domain,
"locale" => :locale,
"provider_uid" => :provider_uid,
"terms_accepted" => :terms_accepted
@ -423,6 +424,21 @@ defmodule WhoNeedHelp.Accounts do
end
end
def change_google_registration(identity_attrs, registration_attrs \\ %{})
when is_map(registration_attrs) do
case normalize_google_identity(identity_attrs) do
{:ok, identity_attrs} ->
%User{}
|> User.registration_changeset(
google_registration_attrs(identity_attrs, registration_attrs),
validate_unique: false
)
{:error, _reason} ->
User.registration_changeset(%User{}, %{}, validate_unique: false)
end
end
def register_user_by_google(identity_attrs, registration_attrs)
when is_map(registration_attrs) do
with {:ok, identity_attrs} <- normalize_google_identity(identity_attrs) do
@ -451,6 +467,36 @@ defmodule WhoNeedHelp.Accounts do
def register_user_by_google(_identity_attrs, _registration_attrs),
do: {:error, :invalid_registration}
def link_google_identity_by_authoritative_email(%User{id: user_id}, identity_attrs) do
with {:ok, identity_attrs} <- normalize_google_identity(identity_attrs),
true <- WhoNeedHelp.GoogleAuth.authoritative_email?(identity_attrs) do
Repo.transact(fn ->
user =
User
|> where([user], user.id == ^user_id)
|> lock("FOR UPDATE")
|> Repo.one()
cond do
is_nil(user) or user.moderation_status == :suspended ->
{:error, :not_found}
normalized_email(user.email) != identity_attrs.email ->
{:error, :email_mismatch}
true ->
with {:ok, user} <- confirm_google_email_owner(user),
{:ok, identity} <- upsert_google_identity_for_user(user, identity_attrs) do
{:ok, {user, identity}}
end
end
end)
else
false -> {:error, :email_not_authoritative}
{:error, _reason} = error -> error
end
end
def link_google_identity(%User{id: user_id}, identity_attrs) do
with {:ok, identity_attrs} <- normalize_google_identity(identity_attrs) do
Repo.transact(fn ->
@ -499,12 +545,7 @@ defmodule WhoNeedHelp.Accounts do
if existing_user do
{:error, :email_already_registered}
else
attrs = %{
"email" => identity_attrs.email,
"display_name" => identity_attrs.display_name,
"locale" => registration_value(registration_attrs, "locale", "en"),
"terms_accepted" => registration_value(registration_attrs, "terms_accepted", false)
}
attrs = google_registration_attrs(identity_attrs, registration_attrs)
with {:ok, user} <- register_user(attrs),
{:ok, user} <- user |> User.confirm_changeset() |> Repo.update(),
@ -585,11 +626,30 @@ defmodule WhoNeedHelp.Accounts do
end
end
defp confirm_google_email_owner(%User{confirmed_at: nil} = user) do
user
|> User.confirm_changeset()
|> Repo.update()
end
defp confirm_google_email_owner(%User{} = user), do: {:ok, user}
defp google_registration_attrs(identity_attrs, registration_attrs) do
%{
"email" => identity_attrs.email,
"display_name" =>
registration_value(registration_attrs, "display_name", identity_attrs.display_name),
"locale" => registration_value(registration_attrs, "locale", "en"),
"terms_accepted" => registration_value(registration_attrs, "terms_accepted", false)
}
end
defp normalize_google_identity(identity_attrs) when is_map(identity_attrs) do
provider_uid = registration_value(identity_attrs, "provider_uid", nil)
email = registration_value(identity_attrs, "email", nil)
email_verified = registration_value(identity_attrs, "email_verified", false)
display_name = registration_value(identity_attrs, "display_name", nil)
hosted_domain = registration_value(identity_attrs, "hosted_domain", nil)
cond do
email_verified != true ->
@ -610,7 +670,8 @@ defmodule WhoNeedHelp.Accounts do
provider_uid: provider_uid,
email: email |> String.trim() |> String.downcase(),
email_verified: true,
display_name: display_name |> String.trim() |> String.slice(0, 80)
display_name: display_name |> String.trim() |> String.slice(0, 80),
hosted_domain: normalize_hosted_domain(hosted_domain)
}}
end
end
@ -618,6 +679,18 @@ defmodule WhoNeedHelp.Accounts do
defp normalize_google_identity(_identity_attrs),
do: {:error, :invalid_provider_identity}
defp normalize_hosted_domain(domain) when is_binary(domain) do
case domain |> String.trim() |> String.downcase() do
"" -> nil
normalized -> String.slice(normalized, 0, 255)
end
end
defp normalize_hosted_domain(_domain), do: nil
defp normalized_email(email) when is_binary(email),
do: email |> String.trim() |> String.downcase()
defp registration_value(attrs, key, default) do
case Map.fetch(attrs, key) do
{:ok, value} ->

View File

@ -10,7 +10,8 @@ defmodule WhoNeedHelp.GoogleAuth do
provider_uid: String.t(),
email: String.t(),
email_verified: true,
display_name: String.t()
display_name: String.t(),
hosted_domain: String.t() | nil
}
@callback enabled?() :: boolean()
@ -33,6 +34,36 @@ defmodule WhoNeedHelp.GoogleAuth do
def verify_id_token(id_token, nonce),
do: adapter().verify_id_token(id_token, nonce)
@doc """
Returns whether Google is authoritative for the identity's current email.
Gmail addresses are hosted by Google. A non-empty hosted-domain claim marks
a verified Google Workspace identity. Other third-party email addresses still
require the user to prove access to the existing local account before linking.
"""
def authoritative_email?(
%{
email: email,
email_verified: true,
hosted_domain: hosted_domain
} = identity
)
when is_binary(email) and is_binary(hosted_domain) do
String.trim(hosted_domain) != "" or authoritative_email?(Map.delete(identity, :hosted_domain))
end
def authoritative_email?(%{
email: email,
email_verified: true
})
when is_binary(email) do
normalized_email = email |> String.trim() |> String.downcase()
String.ends_with?(normalized_email, "@gmail.com")
end
def authoritative_email?(_identity), do: false
defp adapter do
Application.get_env(
:who_need_help,

View File

@ -94,7 +94,8 @@ defmodule WhoNeedHelp.GoogleAuth.AssentAdapter do
provider_uid: provider_uid,
email: normalized_email,
email_verified: true,
display_name: display_name(normalized_email, Map.get(claims, "name"))
display_name: display_name(normalized_email, Map.get(claims, "name")),
hosted_domain: hosted_domain(Map.get(claims, "hd"))
}}
end
@ -132,6 +133,15 @@ defmodule WhoNeedHelp.GoogleAuth.AssentAdapter do
else: "Google user"
end
defp hosted_domain(domain) when is_binary(domain) do
case domain |> String.trim() |> String.downcase() do
"" -> nil
normalized -> String.slice(normalized, 0, 255)
end
end
defp hosted_domain(_domain), do: nil
defp random_url_token(length) do
length
|> :crypto.strong_rand_bytes()

View File

@ -71,12 +71,15 @@ defmodule WhoNeedHelpWeb.GoogleAuthController do
|> GoogleAuthPending.delete()
|> google_account_unavailable(~p"/users/log-in")
existing_account ->
render(conn, :complete,
pending: pending,
existing_account: not is_nil(existing_account),
page_title: gettext("Continue with Google")
)
%Accounts.User{} = existing_account ->
if GoogleAuth.authoritative_email?(identity_attrs(pending)) do
connect_authoritative_google_and_log_in(conn, existing_account, pending)
else
render_google_completion(conn, pending, true)
end
nil ->
render_google_completion(conn, pending, false)
end
{:error, _reason} ->
@ -88,17 +91,14 @@ defmodule WhoNeedHelpWeb.GoogleAuthController do
end
def complete_registration(conn, %{"google_registration" => params}) when is_map(params) do
terms_accepted = params["terms_accepted"] in [true, "true", "on", "1"]
with true <- terms_accepted,
{:ok, pending} <- GoogleAuthPending.fetch(conn),
with {:ok, pending} <- GoogleAuthPending.fetch(conn),
nil <- Accounts.get_user_by_email(pending.email),
{:ok, _limit} <- RateLimiter.check(:registration_email, pending.email),
{:ok, {user, _identity}} <-
Accounts.register_user_by_google(identity_attrs(pending), %{
"locale" => normalize_locale(params["locale"] || pending.locale),
"terms_accepted" => true
}) do
Accounts.register_user_by_google(
identity_attrs(pending),
normalize_google_registration_params(params, pending)
) do
_ = ProductAnalytics.increment_for_user(user, "account.registered", "google")
conn
@ -106,13 +106,10 @@ defmodule WhoNeedHelpWeb.GoogleAuthController do
|> put_flash(:info, gettext("Your account was created with Google."))
|> UserAuth.log_in_user(user)
else
false ->
{:error, %Ecto.Changeset{} = changeset} ->
conn
|> put_flash(
:error,
gettext("Confirm that you are 18 or older and accept the safety rules first.")
)
|> redirect(to: ~p"/auth/google/complete")
|> put_status(:unprocessable_entity)
|> render_google_completion_from_session(changeset)
%Accounts.User{} ->
conn
@ -354,10 +351,7 @@ defmodule WhoNeedHelpWeb.GoogleAuthController do
|> UserAuth.log_in_user(user)
{:error, :not_linked} ->
case GoogleAuthPending.put(conn, identity_attrs, flow_locale(flow)) do
{:ok, conn} -> redirect(conn, to: ~p"/auth/google/complete")
{:error, _reason} -> google_account_unavailable(conn, ~p"/users/log-in")
end
continue_unlinked_google(conn, flow, identity_attrs, ~p"/users/log-in")
{:error, _reason} ->
google_account_unavailable(conn, ~p"/users/log-in")
@ -365,13 +359,7 @@ defmodule WhoNeedHelpWeb.GoogleAuthController do
end
defp finish_flow(conn, "register", flow, identity_attrs) do
case GoogleAuthPending.put(conn, identity_attrs, flow_locale(flow)) do
{:ok, conn} ->
redirect(conn, to: ~p"/auth/google/complete")
{:error, _reason} ->
google_account_unavailable(conn, ~p"/users/register")
end
continue_unlinked_google(conn, flow, identity_attrs, ~p"/users/register")
end
defp finish_flow(conn, "link", flow, identity_attrs) do
@ -421,6 +409,90 @@ defmodule WhoNeedHelpWeb.GoogleAuthController do
end)
end
defp connect_authoritative_google_and_log_in(conn, existing_account, pending) do
result =
Repo.transact(fn ->
with {:ok, {user, identity}} <-
Accounts.link_google_identity_by_authoritative_email(
existing_account,
identity_attrs(pending)
),
{:ok, _audit} <-
Trust.audit(
user.id,
"auth_identity.connected",
"auth_identity",
identity.id,
%{"provider" => "google", "method" => "authoritative_google_email"}
) do
{:ok, user}
end
end)
case result do
{:ok, user} ->
conn
|> GoogleAuthPending.delete()
|> put_flash(:info, gettext("Welcome back!"))
|> UserAuth.log_in_user(user)
{:error, reason} ->
Logger.warning("Unable to connect authoritative Google identity (#{error_name(reason)})")
conn
|> GoogleAuthPending.delete()
|> google_account_unavailable(~p"/users/log-in")
end
end
defp continue_unlinked_google(conn, flow, identity_attrs, failure_path) do
case Accounts.get_user_by_email(identity_attrs.email) do
%Accounts.User{moderation_status: :suspended} ->
google_account_unavailable(conn, failure_path)
%Accounts.User{} = existing_account ->
if GoogleAuth.authoritative_email?(identity_attrs) do
pending = Map.put(identity_attrs, :locale, flow_locale(flow))
connect_authoritative_google_and_log_in(conn, existing_account, pending)
else
store_pending_google(conn, flow, identity_attrs, failure_path)
end
nil ->
store_pending_google(conn, flow, identity_attrs, failure_path)
end
end
defp store_pending_google(conn, flow, identity_attrs, failure_path) do
case GoogleAuthPending.put(conn, identity_attrs, flow_locale(flow)) do
{:ok, conn} -> redirect(conn, to: ~p"/auth/google/complete")
{:error, _reason} -> google_account_unavailable(conn, failure_path)
end
end
defp render_google_completion_from_session(conn, changeset) do
case GoogleAuthPending.fetch(conn) do
{:ok, pending} -> render_google_completion(conn, pending, false, changeset)
{:error, _reason} -> expired_pending_redirect(conn)
end
end
defp render_google_completion(conn, pending, existing_account, changeset \\ nil) do
changeset =
changeset ||
Accounts.change_google_registration(identity_attrs(pending), %{
"display_name" => pending.display_name,
"locale" => pending.locale
})
render(conn, :complete,
pending: pending,
existing_account: existing_account,
registration_form: Phoenix.Component.to_form(changeset, as: :google_registration),
page_title: gettext("Continue with Google")
)
end
defp unlink_google_identity_and_audit(current_user) do
Repo.transact(fn ->
with {:ok, identity} <- Accounts.unlink_google_identity(current_user),
@ -474,10 +546,27 @@ defmodule WhoNeedHelpWeb.GoogleAuthController do
defp normalize_locale(locale) when locale in @supported_locales, do: locale
defp normalize_locale(_locale), do: "en"
defp normalize_google_registration_params(params, pending) do
%{
"display_name" => normalize_display_name(params["display_name"]),
"locale" => normalize_locale(params["locale"] || pending.locale),
"terms_accepted" => params["terms_accepted"] in [true, "true", "on", "1"]
}
end
defp normalize_display_name(name) when is_binary(name), do: String.trim(name)
defp normalize_display_name(_name), do: ""
defp flow_locale(flow), do: normalize_locale(flow["locale"])
defp identity_attrs(pending) do
Map.take(pending, [:provider_uid, :email, :email_verified, :display_name])
Map.take(pending, [
:provider_uid,
:email,
:email_verified,
:display_name,
:hosted_domain
])
end
defp error_name(error) when is_atom(error), do: Atom.to_string(error)

View File

@ -67,17 +67,37 @@
</div>
<.form
for={%{}}
as={:google_registration}
for={@registration_form}
action={~p"/auth/google/complete-registration"}
class="space-y-3"
class="space-y-4"
>
<input type="hidden" name="google_registration[locale]" value={@pending.locale} />
<div>
<.input
field={@registration_form[:display_name]}
type="text"
label={gettext("Display name")}
autocomplete="name"
minlength="2"
maxlength="80"
required
/>
<p class="text-xs text-base-content/60">
{gettext("Other people will see this name on requests, activities, and reviews.")}
</p>
</div>
<.input
field={@registration_form[:locale]}
type="select"
label={gettext("Interface language")}
options={[{"English", "en"}, {"Українська", "uk"}, {"Русский", "ru"}]}
required
/>
<.input
field={@registration_form[:terms_accepted]}
type="checkbox"
id="google_completion_terms"
name="google_registration[terms_accepted]"
value="false"
label={gettext("I am 18 or older and accept the Terms and Safety Rules")}
required
/>

View File

@ -87,6 +87,7 @@ defmodule WhoNeedHelpWeb.GoogleAuthPending do
email = value(attrs, :email)
email_verified = value(attrs, :email_verified)
display_name = value(attrs, :display_name)
hosted_domain = normalize_hosted_domain(value(attrs, :hosted_domain))
cond do
email_verified != true ->
@ -108,6 +109,7 @@ defmodule WhoNeedHelpWeb.GoogleAuthPending do
email: normalized_email(email),
email_verified: true,
display_name: display_name |> String.trim() |> String.slice(0, 80),
hosted_domain: hosted_domain,
locale: normalize_locale(locale)
}}
end
@ -116,13 +118,28 @@ defmodule WhoNeedHelpWeb.GoogleAuthPending do
defp normalize(_attrs, _locale), do: {:error, :invalid}
defp identity_attrs(pending) do
Map.take(pending, [:provider_uid, :email, :email_verified, :display_name])
Map.take(pending, [
:provider_uid,
:email,
:email_verified,
:display_name,
:hosted_domain
])
end
defp value(attrs, key), do: Map.get(attrs, key) || Map.get(attrs, Atom.to_string(key))
defp normalized_email(email), do: email |> String.trim() |> String.downcase()
defp normalize_hosted_domain(domain) when is_binary(domain) do
case domain |> String.trim() |> String.downcase() do
"" -> nil
normalized -> String.slice(normalized, 0, 255)
end
end
defp normalize_hosted_domain(_domain), do: nil
defp normalize_locale(locale) when locale in @supported_locales, do: locale
defp normalize_locale(_locale), do: "en"
end

View File

@ -39,7 +39,8 @@ defmodule WhoNeedHelp.GoogleAuthFake do
provider_uid: params["uid"] || "google-user-42",
email: email,
email_verified: true,
display_name: params["name"] || "Google Neighbor"
display_name: params["name"] || "Google Neighbor",
hosted_domain: normalize_hosted_domain(params["hd"])
}}
end
end
@ -60,10 +61,20 @@ defmodule WhoNeedHelp.GoogleAuthFake do
provider_uid: provider_uid,
email: String.downcase(email),
email_verified: true,
display_name: name
display_name: name,
hosted_domain: nil
}}
else
_invalid_or_replayed -> {:error, :invalid_id_token}
end
end
defp normalize_hosted_domain(domain) when is_binary(domain) do
case domain |> String.trim() |> String.downcase() do
"" -> nil
normalized -> normalized
end
end
defp normalize_hosted_domain(_domain), do: nil
end

View File

@ -1,6 +1,7 @@
defmodule WhoNeedHelp.GoogleAuthTest do
use ExUnit.Case, async: false
alias WhoNeedHelp.GoogleAuth
alias WhoNeedHelp.GoogleAuth.AssentAdapter
defmodule GoogleJwksHTTPAdapter do
@ -66,13 +67,46 @@ defmodule WhoNeedHelp.GoogleAuthTest do
provider_uid: "google-subject-123",
email: "alice@example.com",
email_verified: true,
display_name: "Alice Neighbor"
display_name: "Alice Neighbor",
hosted_domain: nil
}
refute Map.has_key?(identity, :access_token)
refute Map.has_key?(identity, :id_token)
end
test "normalizes the hosted-domain claim and applies Google's authoritative-email rules" do
assert {:ok, workspace_identity} =
AssentAdapter.normalize_identity(%{
"sub" => "workspace-subject",
"email" => "Owner@Example.ORG",
"email_verified" => true,
"name" => "Workspace Owner",
"hd" => " Example.ORG "
})
assert workspace_identity.hosted_domain == "example.org"
assert GoogleAuth.authoritative_email?(workspace_identity)
assert GoogleAuth.authoritative_email?(%{
email: "OWNER@GMAIL.COM",
email_verified: true,
hosted_domain: nil
})
refute GoogleAuth.authoritative_email?(%{
email: "owner@example.org",
email_verified: true,
hosted_domain: nil
})
refute GoogleAuth.authoritative_email?(%{
email: "owner@gmail.com",
email_verified: false,
hosted_domain: "gmail.com"
})
end
test "rejects an unverified or incomplete Google email" do
assert {:error, :email_not_verified} =
AssentAdapter.normalize_identity(%{
@ -130,7 +164,8 @@ defmodule WhoNeedHelp.GoogleAuthTest do
provider_uid: "native-subject",
email: "native@example.com",
email_verified: true,
display_name: "Native Neighbor"
display_name: "Native Neighbor",
hosted_domain: nil
}} = AssentAdapter.verify_id_token(token, nonce)
assert {:error, _reason} = AssentAdapter.verify_id_token(token, "different-nonce")
@ -203,7 +238,8 @@ defmodule WhoNeedHelp.GoogleAuthTest do
provider_uid: "cross-client-subject",
email: "crossclient@example.com",
email_verified: true,
display_name: "Cross Client"
display_name: "Cross Client",
hosted_domain: nil
}} = AssentAdapter.verify_id_token(token, nonce)
unauthorized_token =

View File

@ -148,6 +148,11 @@ defmodule WhoNeedHelpWeb.GoogleAuthControllerTest do
assert html =~ "Create your Who Need Help account"
assert html =~ email
assert html =~ ~s(value="Native Neighbor")
assert Enum.count(
LazyHTML.query(LazyHTML.from_document(html), "option[value='ru'][selected]")
) == 1
end
test "native Google account linking still requires the signed-in sudo owner", %{conn: conn} do
@ -213,7 +218,11 @@ defmodule WhoNeedHelpWeb.GoogleAuthControllerTest do
)
|> recycle()
|> post(~p"/auth/google/complete-registration", %{
"google_registration" => %{"locale" => "ru", "terms_accepted" => "true"}
"google_registration" => %{
"display_name" => "Chosen Helper",
"locale" => "ru",
"terms_accepted" => "true"
}
})
assert redirected_to(conn) == ~p"/"
@ -224,7 +233,7 @@ defmodule WhoNeedHelpWeb.GoogleAuthControllerTest do
assert user.confirmed_at
assert user.accepted_terms_at
assert user.locale == "ru"
assert user.display_name == "Google Helper"
assert user.display_name == "Chosen Helper"
assert is_nil(user.hashed_password)
assert %AuthIdentity{
@ -259,6 +268,98 @@ defmodule WhoNeedHelpWeb.GoogleAuthControllerTest do
assert html =~ "Email me a verification link"
end
test "auto-links a verified Gmail owner and keeps the existing public name", %{conn: conn} do
email = "existing#{System.unique_integer([:positive])}@gmail.com"
user = unconfirmed_user_fixture(%{email: email, display_name: "Local Public Name"})
refute user.confirmed_at
conn =
conn
|> post(~p"/auth/google/login")
|> recycle()
|> get(
~p"/auth/google/callback?code=code&state=google-test-state&uid=gmail-owner&email=#{email}&name=Google%20Profile%20Name"
)
assert redirected_to(conn) == ~p"/"
assert get_session(conn, :user_token)
assert is_nil(get_session(conn, "pending_google_identity"))
reloaded_user = Accounts.get_user_by_email(email)
assert reloaded_user.confirmed_at
assert reloaded_user.display_name == "Local Public Name"
identity =
Repo.get_by!(AuthIdentity, provider: :google, provider_uid: "gmail-owner")
assert identity.user_id == user.id
assert %AuditEvent{actor_id: actor_id, metadata: metadata} =
Repo.get_by!(AuditEvent,
action: "auth_identity.connected",
target_id: identity.id
)
assert actor_id == user.id
assert metadata["method"] == "authoritative_google_email"
end
test "auto-links a verified Google Workspace owner carrying the hosted-domain claim", %{
conn: conn
} do
email = "owner#{System.unique_integer([:positive])}@example.org"
user = user_fixture(%{email: email, display_name: "Workspace Member"})
conn =
conn
|> post(~p"/auth/google/register", %{
"google_registration" => %{"locale" => "uk"}
})
|> recycle()
|> get(
~p"/auth/google/callback?code=code&state=google-test-state&uid=workspace-owner&email=#{email}&hd=example.org"
)
assert Phoenix.Flash.get(conn.assigns.flash, :error) == nil
assert redirected_to(conn) == ~p"/"
assert get_session(conn, :user_token)
assert %AuthIdentity{user_id: user_id} =
Repo.get_by!(AuthIdentity, provider: :google, provider_uid: "workspace-owner")
assert user_id == user.id
end
test "does not replace a different Google identity already connected to the Gmail account", %{
conn: conn
} do
email = "protected#{System.unique_integer([:positive])}@gmail.com"
user = user_fixture(%{email: email})
assert {:ok, original_identity} =
Accounts.link_google_identity(user, %{
provider_uid: "original-google-owner",
email: email,
email_verified: true,
display_name: user.display_name
})
conn =
conn
|> post(~p"/auth/google/login")
|> recycle()
|> get(
~p"/auth/google/callback?code=code&state=google-test-state&uid=different-google-owner&email=#{email}"
)
assert redirected_to(conn) == ~p"/users/log-in"
assert Phoenix.Flash.get(conn.assigns.flash, :error) =~ "cannot be used"
refute get_session(conn, :user_token)
refute Repo.get_by(AuthIdentity, provider: :google, provider_uid: "different-google-owner")
assert Repo.get!(AuthIdentity, original_identity.id).provider_uid == "original-google-owner"
end
test "logs in only through an identity already linked to the local account", %{conn: conn} do
user = user_fixture()
@ -314,7 +415,11 @@ defmodule WhoNeedHelpWeb.GoogleAuthControllerTest do
)
|> recycle()
|> post(~p"/auth/google/complete-registration", %{
"google_registration" => %{"locale" => "uk", "terms_accepted" => "true"}
"google_registration" => %{
"display_name" => "Chosen Continued Helper",
"locale" => "uk",
"terms_accepted" => "true"
}
})
assert redirected_to(conn) == ~p"/"
@ -324,7 +429,7 @@ defmodule WhoNeedHelpWeb.GoogleAuthControllerTest do
assert user.confirmed_at
assert user.accepted_terms_at
assert user.locale == "uk"
assert user.display_name == "Continued Helper"
assert user.display_name == "Chosen Continued Helper"
assert %AuthIdentity{user_id: user_id} =
Repo.get_by!(AuthIdentity, provider: :google, provider_uid: "continue-google")
@ -344,15 +449,45 @@ defmodule WhoNeedHelpWeb.GoogleAuthControllerTest do
)
|> recycle()
|> post(~p"/auth/google/complete-registration", %{
"google_registration" => %{"locale" => "en", "terms_accepted" => "false"}
"google_registration" => %{
"display_name" => "No Terms Helper",
"locale" => "en",
"terms_accepted" => "false"
}
})
assert redirected_to(conn) == ~p"/auth/google/complete"
assert Phoenix.Flash.get(conn.assigns.flash, :error) =~ "18 or older"
html = html_response(conn, 422)
assert html =~ "you must confirm that you are 18+ and accept the rules"
refute Accounts.get_user_by_email(email)
refute Repo.get_by(AuthIdentity, provider: :google, provider_uid: "no-terms-google")
end
test "keeps the Google completion form open when the public name is invalid", %{conn: conn} do
email = unique_user_email()
conn =
conn
|> post(~p"/auth/google/login")
|> recycle()
|> get(
~p"/auth/google/callback?code=code&state=google-test-state&uid=invalid-name-google&email=#{email}&name=Initial%20Name"
)
|> recycle()
|> post(~p"/auth/google/complete-registration", %{
"google_registration" => %{
"display_name" => " ",
"locale" => "en",
"terms_accepted" => "true"
}
})
html = html_response(conn, 422)
assert html =~ "Create your Who Need Help account"
assert html =~ "can&#39;t be blank"
refute Accounts.get_user_by_email(email)
refute Repo.get_by(AuthIdentity, provider: :google, provider_uid: "invalid-name-google")
end
test "links an existing account after one magic-link verification", %{conn: conn} do
user = user_fixture()
assert_email_sent()