defmodule ToweropsWeb.InvitationControllerTest do use ToweropsWeb.ConnCase, async: true import Towerops.AccountsFixtures import Towerops.OrganizationsFixtures alias Towerops.Organizations setup do user = user_fixture() organization = organization_fixture(user.id) %{user: user, organization: organization} end defp create_invitation(org, user, attrs \\ %{}) do defaults = %{ organization_id: org.id, invited_by_id: user.id, email: "invited-#{System.unique_integer()}@example.com", role: "member" } {:ok, invitation} = Organizations.create_invitation(Map.merge(defaults, attrs)) invitation end describe "GET /invitations/:token" do test "redirects to registration with token when not logged in", %{ conn: conn, organization: organization, user: user } do invitation = create_invitation(organization, user) conn = get(conn, ~p"/invitations/#{invitation.token}") assert redirected_to(conn) =~ ~p"/users/register" assert redirected_to(conn) =~ "invitation_token=#{invitation.token}" end test "accepts invitation and redirects when logged in", %{ conn: conn, organization: organization, user: inviter } do invitation = create_invitation(organization, inviter) # Log in as a different user who will accept the invitation accepting_user = user_fixture() conn = log_in_user(conn, accepting_user) conn = get(conn, ~p"/invitations/#{invitation.token}") assert redirected_to(conn) =~ "/settings" assert Phoenix.Flash.get(conn.assigns.flash, :info) =~ "joined" end test "handles invalid/non-existent token", %{conn: conn} do conn = get(conn, ~p"/invitations/invalid-token-abc123") assert redirected_to(conn) == ~p"/" assert Phoenix.Flash.get(conn.assigns.flash, :error) =~ "invalid" end test "handles expired invitation", %{conn: conn, organization: organization, user: user} do # Create an invitation with an already-passed expiration invitation = create_invitation(organization, user) # Manually expire it invitation |> Ecto.Changeset.change(%{expires_at: DateTime.utc_now() |> DateTime.add(-1, :day) |> DateTime.truncate(:second)}) |> Towerops.Repo.update!() conn = get(conn, ~p"/invitations/#{invitation.token}") assert redirected_to(conn) == ~p"/" assert Phoenix.Flash.get(conn.assigns.flash, :error) =~ "invalid" end test "handles already-accepted invitation", %{conn: conn, organization: organization, user: user} do invitation = create_invitation(organization, user) # Mark as accepted invitation |> Ecto.Changeset.change(%{accepted_at: Towerops.Time.now()}) |> Towerops.Repo.update!() conn = get(conn, ~p"/invitations/#{invitation.token}") assert redirected_to(conn) == ~p"/" assert Phoenix.Flash.get(conn.assigns.flash, :error) =~ "invalid" end test "shows error when user is already a member", %{ conn: conn, organization: organization, user: inviter } do invitation = create_invitation(organization, inviter) # The inviter is already a member, so accepting should fail conn = log_in_user(conn, inviter) conn = get(conn, ~p"/invitations/#{invitation.token}") # Should redirect with an error about already being a member assert redirected_to(conn) =~ "/dashboard" assert Phoenix.Flash.get(conn.assigns.flash, :error) =~ "already" end end end