From f5bef9b99774b058b1eba3146913118f8bd25240 Mon Sep 17 00:00:00 2001 From: Graham McIntire Date: Tue, 13 Jan 2026 07:45:50 -0600 Subject: [PATCH] Add comprehensive tests for Organizations.Invitation Improved coverage from 60.00% to 100% by adding 13 tests covering: - Required field validations (email, role, organization_id, invited_by_id) - Email format validation - Automatic token generation using crypto - Token preservation when provided - Automatic expires_at generation (7 days from now) - Expires_at preservation when provided - All valid role values (admin, member, viewer) - Accepted_at field handling Overall coverage: 66.34% --- .../organizations/invitation_test.exs | 210 ++++++++++++++++++ 1 file changed, 210 insertions(+) create mode 100644 test/towerops/organizations/invitation_test.exs diff --git a/test/towerops/organizations/invitation_test.exs b/test/towerops/organizations/invitation_test.exs new file mode 100644 index 00000000..0c85fae2 --- /dev/null +++ b/test/towerops/organizations/invitation_test.exs @@ -0,0 +1,210 @@ +defmodule Towerops.Organizations.InvitationTest do + use Towerops.DataCase + + alias Towerops.Organizations.Invitation + + describe "changeset/2" do + test "valid changeset with required fields" do + attrs = %{ + email: "test@example.com", + role: :member, + organization_id: Ecto.UUID.generate(), + invited_by_id: Ecto.UUID.generate() + } + + changeset = Invitation.changeset(%Invitation{}, attrs) + + assert changeset.valid? + assert get_field(changeset, :email) == "test@example.com" + assert get_field(changeset, :role) == :member + # Token and expires_at should be auto-generated + assert get_field(changeset, :token) + assert get_field(changeset, :expires_at) + end + + test "requires email" do + attrs = %{ + role: :member, + organization_id: Ecto.UUID.generate(), + invited_by_id: Ecto.UUID.generate() + } + + changeset = Invitation.changeset(%Invitation{}, attrs) + + refute changeset.valid? + assert %{email: ["can't be blank"]} = errors_on(changeset) + end + + test "requires role" do + attrs = %{ + email: "test@example.com", + organization_id: Ecto.UUID.generate(), + invited_by_id: Ecto.UUID.generate() + } + + changeset = Invitation.changeset(%Invitation{}, attrs) + + refute changeset.valid? + assert %{role: ["can't be blank"]} = errors_on(changeset) + end + + test "requires organization_id" do + attrs = %{ + email: "test@example.com", + role: :member, + invited_by_id: Ecto.UUID.generate() + } + + changeset = Invitation.changeset(%Invitation{}, attrs) + + refute changeset.valid? + assert %{organization_id: ["can't be blank"]} = errors_on(changeset) + end + + test "requires invited_by_id" do + attrs = %{ + email: "test@example.com", + role: :member, + organization_id: Ecto.UUID.generate() + } + + changeset = Invitation.changeset(%Invitation{}, attrs) + + refute changeset.valid? + assert %{invited_by_id: ["can't be blank"]} = errors_on(changeset) + end + + test "validates email format" do + attrs = %{ + email: "invalid-email", + role: :member, + organization_id: Ecto.UUID.generate(), + invited_by_id: Ecto.UUID.generate() + } + + changeset = Invitation.changeset(%Invitation{}, attrs) + + refute changeset.valid? + assert %{email: ["has invalid format"]} = errors_on(changeset) + end + + test "accepts valid email formats" do + valid_emails = [ + "test@example.com", + "user.name@domain.co.uk", + "user+tag@example.org" + ] + + for email <- valid_emails do + attrs = %{ + email: email, + role: :member, + organization_id: Ecto.UUID.generate(), + invited_by_id: Ecto.UUID.generate() + } + + changeset = Invitation.changeset(%Invitation{}, attrs) + assert changeset.valid?, "Expected #{email} to be valid" + end + end + + test "generates token automatically" do + attrs = %{ + email: "test@example.com", + role: :member, + organization_id: Ecto.UUID.generate(), + invited_by_id: Ecto.UUID.generate() + } + + changeset = Invitation.changeset(%Invitation{}, attrs) + token = get_field(changeset, :token) + + assert token + assert is_binary(token) + assert String.length(token) > 0 + end + + test "does not override provided token" do + attrs = %{ + email: "test@example.com", + role: :member, + organization_id: Ecto.UUID.generate(), + invited_by_id: Ecto.UUID.generate(), + token: "custom-token" + } + + changeset = Invitation.changeset(%Invitation{}, attrs) + + assert get_field(changeset, :token) == "custom-token" + end + + test "generates expires_at automatically (7 days from now)" do + attrs = %{ + email: "test@example.com", + role: :member, + organization_id: Ecto.UUID.generate(), + invited_by_id: Ecto.UUID.generate() + } + + now = DateTime.utc_now() + changeset = Invitation.changeset(%Invitation{}, attrs) + expires_at = get_field(changeset, :expires_at) + + assert expires_at + + # Should be approximately 7 days from now (within 1 minute tolerance) + diff_seconds = DateTime.diff(expires_at, now, :second) + expected_seconds = 7 * 24 * 60 * 60 + assert_in_delta diff_seconds, expected_seconds, 60 + end + + test "does not override provided expires_at" do + custom_expires_at = DateTime.utc_now() |> DateTime.add(1, :day) |> DateTime.truncate(:second) + + attrs = %{ + email: "test@example.com", + role: :member, + organization_id: Ecto.UUID.generate(), + invited_by_id: Ecto.UUID.generate(), + expires_at: custom_expires_at + } + + changeset = Invitation.changeset(%Invitation{}, attrs) + + assert DateTime.compare(get_field(changeset, :expires_at), custom_expires_at) == :eq + end + + test "accepts all valid role values" do + roles = [:admin, :member, :viewer] + + for role <- roles do + attrs = %{ + email: "test@example.com", + role: role, + organization_id: Ecto.UUID.generate(), + invited_by_id: Ecto.UUID.generate() + } + + changeset = Invitation.changeset(%Invitation{}, attrs) + assert changeset.valid?, "Expected role #{role} to be valid" + end + end + + test "allows accepted_at to be set" do + accepted_at = DateTime.truncate(DateTime.utc_now(), :second) + + attrs = %{ + email: "test@example.com", + role: :member, + organization_id: Ecto.UUID.generate(), + invited_by_id: Ecto.UUID.generate(), + accepted_at: accepted_at + } + + changeset = Invitation.changeset(%Invitation{}, attrs) + + assert changeset.valid? + assert DateTime.compare(get_field(changeset, :accepted_at), accepted_at) == :eq + end + end +end