package auth import ( "errors" "testing" ) func TestPreferMembershipRoleNeverDemotesAdmin(t *testing.T) { t.Parallel() cases := []struct { name string existing string invited string want string }{ {name: "admin stays admin on member invite", existing: "admin", invited: "member", want: "admin"}, {name: "admin stays admin on admin invite", existing: "admin", invited: "admin", want: "admin"}, {name: "member promotes to admin", existing: "member", invited: "admin", want: "admin"}, {name: "member stays member", existing: "member", invited: "member", want: "member"}, {name: "unknown invited normalizes to member", existing: "member", invited: "owner", want: "member"}, {name: "empty existing yields invited role", existing: "", invited: "admin", want: "admin"}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { t.Parallel() got := preferMembershipRole(tc.existing, tc.invited) if got != tc.want { t.Fatalf("preferMembershipRole(%q, %q)=%q want %q", tc.existing, tc.invited, got, tc.want) } }) } } func TestHashInviteTokenMatchesAPIKeyHash(t *testing.T) { t.Parallel() const raw = "invite-plaintext-secret" got := HashInviteToken(raw) if got != HashAPIKey(raw) { t.Fatalf("HashInviteToken must match HashAPIKey construction") } if len(got) != 64 { t.Fatalf("expected sha256 hex length 64, got %d", len(got)) } if got == raw { t.Fatal("invite token must not be stored as plaintext") } } func TestNormalizeInviteRole(t *testing.T) { t.Parallel() if normalizeInviteRole("Admin") != "admin" { t.Fatal("expected admin") } if normalizeInviteRole(" MEMBER ") != "member" { t.Fatal("expected member") } if normalizeInviteRole("owner") != "member" { t.Fatal("unknown roles collapse to member") } if NormalizeMembershipRole("Admin") != "admin" { t.Fatal("NormalizeMembershipRole should accept Admin") } } func TestParseMembershipRole(t *testing.T) { t.Parallel() got, err := ParseMembershipRole(" Admin ") if err != nil || got != "admin" { t.Fatalf("admin: got %q err=%v", got, err) } got, err = ParseMembershipRole("MEMBER") if err != nil || got != "member" { t.Fatalf("member: got %q err=%v", got, err) } if _, err := ParseMembershipRole("owner"); !errors.Is(err, ErrInvalidMembershipRole) { t.Fatalf("owner: err=%v want ErrInvalidMembershipRole", err) } if _, err := ParseMembershipRole(""); !errors.Is(err, ErrInvalidMembershipRole) { t.Fatalf("empty: err=%v want ErrInvalidMembershipRole", err) } } func TestIsSyntheticLegacyEmail(t *testing.T) { t.Parallel() if !IsSyntheticLegacyEmail("user_abc@legacy.local") { t.Fatal("expected synthetic") } if !IsSyntheticLegacyEmail(" User@Legacy.Local ") { t.Fatal("expected case-insensitive synthetic") } if IsSyntheticLegacyEmail("real@example.com") { t.Fatal("real email must not be treated as synthetic") } if IsSyntheticLegacyEmail("legacy.local@example.com") { t.Fatal("suffix-only match; local-part must not trigger") } if IsSyntheticLegacyEmail("") { t.Fatal("empty must not be synthetic") } } func TestEmailsEqual(t *testing.T) { t.Parallel() if !EmailsEqual("A@Example.COM", " a@example.com ") { t.Fatal("expected equal after normalize") } if EmailsEqual("a@example.com", "b@example.com") { t.Fatal("expected mismatch") } if !EmailsEqual("", "") { t.Fatal("empty emails should compare equal") } }