Files
pulse/internal/models/organization_additional_test.go

360 lines
12 KiB
Go

package models
import "testing"
func TestOrganizationAccessors(t *testing.T) {
org := &Organization{
ID: "org-1",
OwnerUserID: "owner",
Members: []OrganizationMember{
{UserID: "admin", Role: OrgRoleAdmin},
{UserID: "member", Role: OrgRoleViewer},
},
}
if !org.HasMember("admin") || org.HasMember("missing") {
t.Fatalf("HasMember results unexpected")
}
if role := org.GetMemberRole("admin"); role != OrgRoleAdmin {
t.Fatalf("GetMemberRole = %q, want admin", role)
}
if role := org.GetMemberRole("missing"); role != "" {
t.Fatalf("GetMemberRole for missing = %q, want empty", role)
}
if !org.IsOwner("owner") || org.IsOwner("admin") {
t.Fatalf("IsOwner results unexpected")
}
if !org.CanUserAccess("owner") || !org.CanUserAccess("member") || org.CanUserAccess("missing") {
t.Fatalf("CanUserAccess results unexpected")
}
if !org.CanUserManage("owner") || !org.CanUserManage("admin") || org.CanUserManage("member") {
t.Fatalf("CanUserManage results unexpected")
}
}
func TestOrganizationPrincipalIdentityCanonicalization(t *testing.T) {
org := &Organization{
ID: "org-1",
OwnerUserID: "owner@example.com",
Members: []OrganizationMember{
{UserID: "owner@example.com", Role: OrgRoleOwner, AddedBy: "owner@example.com"},
{UserID: "admin@example.com", Role: OrgRoleAdmin, AddedBy: "owner@example.com"},
},
}
if role := org.GetMemberRoleForPrincipal("u_owner", "OWNER@example.com"); role != OrgRoleOwner {
t.Fatalf("owner role = %q, want %q", role, OrgRoleOwner)
}
if !org.CanonicalizePrincipalIdentity("u_owner", "OWNER@example.com") {
t.Fatal("expected owner identity to be canonicalized")
}
if org.OwnerUserID != "u_owner" || org.OwnerEmail != "owner@example.com" {
t.Fatalf("owner identity = (%q, %q), want stable id + email", org.OwnerUserID, org.OwnerEmail)
}
if got := org.GetMemberRole("u_owner"); got != OrgRoleOwner {
t.Fatalf("stable owner role = %q, want %q", got, OrgRoleOwner)
}
if got := org.GetMemberRole("owner@example.com"); got != OrgRoleOwner {
t.Fatalf("legacy email owner role = %q, want %q", got, OrgRoleOwner)
}
if got := org.Members[0].Email; got != "owner@example.com" {
t.Fatalf("member email = %q, want %q", got, "owner@example.com")
}
}
func TestOrganizationPrincipalIdentityRejectsStableContactEmailCollision(t *testing.T) {
org := &Organization{
ID: "org-1",
OwnerUserID: "u_owner",
OwnerEmail: "owner@example.com",
Members: []OrganizationMember{
{UserID: "u_owner", Email: "owner@example.com", Role: OrgRoleOwner, AddedBy: "u_owner"},
{UserID: "u_admin", Email: "admin@example.com", Role: OrgRoleAdmin, AddedBy: "u_owner"},
},
}
if role := org.GetMemberRoleForPrincipal("u_intruder", "owner@example.com"); role != "" {
t.Fatalf("owner email collision role = %q, want empty", role)
}
if role := org.GetMemberRoleForPrincipal("u_intruder", "admin@example.com"); role != "" {
t.Fatalf("member email collision role = %q, want empty", role)
}
if org.CanonicalizePrincipalIdentity("u_intruder", "owner@example.com") {
t.Fatal("stable owner contact email must not canonicalize to a different principal")
}
if org.OwnerUserID != "u_owner" || org.Members[0].UserID != "u_owner" {
t.Fatalf("owner identity changed to (%q, %q), want stable owner unchanged", org.OwnerUserID, org.Members[0].UserID)
}
if org.CanonicalizePrincipalIdentity("u_intruder", "admin@example.com") {
t.Fatal("stable member contact email must not canonicalize to a different principal")
}
if org.Members[1].UserID != "u_admin" {
t.Fatalf("admin identity changed to %q, want stable admin unchanged", org.Members[1].UserID)
}
}
func TestOrganizationPrincipalIdentityAllowsLegacyEmailKeyedMemberMigration(t *testing.T) {
org := &Organization{
ID: "org-1",
OwnerUserID: "u_owner",
OwnerEmail: "owner@example.com",
Members: []OrganizationMember{
{UserID: "u_owner", Email: "owner@example.com", Role: OrgRoleOwner, AddedBy: "u_owner"},
{UserID: "legacy-admin@example.com", Role: OrgRoleAdmin, AddedBy: "owner@example.com"},
},
}
if role := org.GetMemberRoleForPrincipal("u_admin", "legacy-admin@example.com"); role != OrgRoleAdmin {
t.Fatalf("legacy member role = %q, want %q", role, OrgRoleAdmin)
}
if !org.CanonicalizePrincipalIdentity("u_admin", "legacy-admin@example.com") {
t.Fatal("expected legacy member identity to canonicalize")
}
if org.Members[1].UserID != "u_admin" || org.Members[1].Email != "legacy-admin@example.com" || org.Members[1].AddedBy != "owner@example.com" {
t.Fatalf("member identity = (%q, %q, %q), want stable user with preserved contact metadata", org.Members[1].UserID, org.Members[1].Email, org.Members[1].AddedBy)
}
}
func TestOrganizationStrictUserIDAccessRejectsContactEmail(t *testing.T) {
org := &Organization{
ID: "org-1",
OwnerUserID: "u_owner",
OwnerEmail: "owner@example.com",
Members: []OrganizationMember{
{UserID: "u_owner", Email: "owner@example.com", Role: OrgRoleOwner},
{UserID: "u_admin", Email: "admin@example.com", Role: OrgRoleAdmin},
{UserID: "u_viewer", Email: "viewer@example.com", Role: OrgRoleViewer},
},
}
if !org.CanUserIDAccess("u_owner") || !org.CanUserIDManage("u_admin") {
t.Fatal("strict user ID access should accept stored owner/admin principals")
}
if org.CanUserIDAccess("owner@example.com") || org.CanUserIDManage("admin@example.com") {
t.Fatal("strict user ID access must not authorize contact email")
}
if got := org.GetMemberRoleByUserID("admin@example.com"); got != "" {
t.Fatalf("strict member role for contact email = %q, want empty", got)
}
if got := org.GetMemberRole("admin@example.com"); got != OrgRoleAdmin {
t.Fatalf("legacy member role for contact email = %q, want %q", got, OrgRoleAdmin)
}
}
func TestOrganizationResolvePrincipalByEmail(t *testing.T) {
org := &Organization{
ID: "org-1",
OwnerUserID: "u_owner",
OwnerEmail: "owner@example.com",
Members: []OrganizationMember{
{UserID: "u_owner", Email: "owner@example.com", Role: OrgRoleOwner},
{UserID: "u_admin", Email: "admin@example.com", Role: OrgRoleAdmin},
{UserID: "legacy@example.com", Role: OrgRoleViewer},
},
}
userID, role, ok := org.ResolvePrincipalByEmail("OWNER@example.com")
if !ok || userID != "u_owner" || role != OrgRoleOwner {
t.Fatalf("owner principal = (%q, %q, %v), want stable owner", userID, role, ok)
}
userID, role, ok = org.ResolvePrincipalByEmail("admin@example.com")
if !ok || userID != "u_admin" || role != OrgRoleAdmin {
t.Fatalf("member principal = (%q, %q, %v), want stable admin", userID, role, ok)
}
userID, role, ok = org.ResolvePrincipalByEmail("legacy@example.com")
if !ok || userID != "legacy@example.com" || role != OrgRoleViewer {
t.Fatalf("legacy principal = (%q, %q, %v), want legacy email fallback", userID, role, ok)
}
}
func TestOrganizationResolvePrincipalByEmailRejectsAmbiguousStoredPrincipals(t *testing.T) {
ownerCollision := &Organization{
ID: "org-owner-collision",
OwnerUserID: "u_owner",
OwnerEmail: "shared@example.com",
Members: []OrganizationMember{
{UserID: "u_owner", Email: "shared@example.com", Role: OrgRoleOwner},
{UserID: "u_admin", Email: "shared@example.com", Role: OrgRoleAdmin},
},
}
userID, role, ok := ownerCollision.ResolvePrincipalByEmail("shared@example.com")
if ok || userID != "" || role != "" {
t.Fatalf("owner collision principal = (%q, %q, %v), want rejection", userID, role, ok)
}
memberCollision := &Organization{
ID: "org-member-collision",
Members: []OrganizationMember{
{UserID: "u_admin", Email: "shared@example.com", Role: OrgRoleAdmin},
{UserID: "u_viewer", Email: "shared@example.com", Role: OrgRoleViewer},
},
}
userID, role, ok = memberCollision.ResolvePrincipalByEmail("shared@example.com")
if ok || userID != "" || role != "" {
t.Fatalf("member collision principal = (%q, %q, %v), want rejection", userID, role, ok)
}
}
func TestOrganizationResolvePrincipalByEmailRejectsBlankStoredPrincipal(t *testing.T) {
ownerOnlyEmail := &Organization{
ID: "org-blank-owner",
OwnerEmail: "owner@example.com",
}
userID, role, ok := ownerOnlyEmail.ResolvePrincipalByEmail("owner@example.com")
if ok || userID != "" || role != "" {
t.Fatalf("blank owner principal = (%q, %q, %v), want rejection", userID, role, ok)
}
memberOnlyEmail := &Organization{
ID: "org-blank-member",
Members: []OrganizationMember{
{Email: "member@example.com", Role: OrgRoleViewer},
},
}
userID, role, ok = memberOnlyEmail.ResolvePrincipalByEmail("member@example.com")
if ok || userID != "" || role != "" {
t.Fatalf("blank member principal = (%q, %q, %v), want rejection", userID, role, ok)
}
}
func TestOrganizationRoleNormalization(t *testing.T) {
if got := NormalizeOrganizationRole("member"); got != OrganizationRole("member") {
t.Fatalf("expected member to remain unchanged, got %q", got)
}
if got := NormalizeOrganizationRole("EDITOR"); got != OrgRoleEditor {
t.Fatalf("expected EDITOR to normalize to editor, got %q", got)
}
if !IsValidOrganizationRole(OrgRoleOwner) {
t.Fatalf("expected owner role to be valid")
}
if IsValidOrganizationRole("member") {
t.Fatalf("expected member to be invalid in v6")
}
if IsValidOrganizationRole("unknown") {
t.Fatalf("expected unknown role to be invalid")
}
}
func TestOrganizationRoleAtLeast(t *testing.T) {
testCases := []struct {
name string
actual OrganizationRole
required OrganizationRole
want bool
}{
{name: "viewer satisfies viewer", actual: OrgRoleViewer, required: OrgRoleViewer, want: true},
{name: "viewer does not satisfy editor", actual: OrgRoleViewer, required: OrgRoleEditor, want: false},
{name: "editor satisfies viewer", actual: OrgRoleEditor, required: OrgRoleViewer, want: true},
{name: "editor satisfies editor", actual: OrgRoleEditor, required: OrgRoleEditor, want: true},
{name: "editor does not satisfy admin", actual: OrgRoleEditor, required: OrgRoleAdmin, want: false},
{name: "admin satisfies editor", actual: OrgRoleAdmin, required: OrgRoleEditor, want: true},
{name: "owner satisfies admin", actual: OrgRoleOwner, required: OrgRoleAdmin, want: true},
{name: "invalid actual is denied", actual: "member", required: OrgRoleViewer, want: false},
{name: "invalid required is denied", actual: OrgRoleOwner, required: "member", want: false},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
if got := OrganizationRoleAtLeast(tc.actual, tc.required); got != tc.want {
t.Fatalf("OrganizationRoleAtLeast(%q, %q) = %v, want %v", tc.actual, tc.required, got, tc.want)
}
})
}
}
func TestNormalizeOrgStatus(t *testing.T) {
testCases := []struct {
name string
input OrgStatus
output OrgStatus
}{
{
name: "empty defaults to active",
input: "",
output: OrgStatusActive,
},
{
name: "whitespace defaults to active",
input: " ",
output: OrgStatusActive,
},
{
name: "active case-insensitive",
input: "ACTIVE",
output: OrgStatusActive,
},
{
name: "suspended case-insensitive",
input: "SUSPENDED",
output: OrgStatusSuspended,
},
{
name: "pending deletion case-insensitive",
input: "Pending_Deletion",
output: OrgStatusPendingDeletion,
},
{
name: "unknown trimmed and lowered",
input: " CuStOm ",
output: "custom",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
got := NormalizeOrgStatus(tc.input)
if got != tc.output {
t.Fatalf("NormalizeOrgStatus(%q) = %q, want %q", tc.input, got, tc.output)
}
})
}
}
func TestNormalizeOrganizationShareStatus(t *testing.T) {
testCases := []struct {
name string
input OrganizationShareStatus
output OrganizationShareStatus
valid bool
}{
{
name: "empty defaults to accepted",
input: "",
output: OrganizationShareStatusAccepted,
valid: true,
},
{
name: "pending case-insensitive",
input: "PENDING",
output: OrganizationShareStatusPending,
valid: true,
},
{
name: "accepted case-insensitive",
input: "Accepted",
output: OrganizationShareStatusAccepted,
valid: true,
},
{
name: "unknown trimmed and lowered",
input: " custom ",
output: "custom",
valid: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
got := NormalizeOrganizationShareStatus(tc.input)
if got != tc.output {
t.Fatalf("NormalizeOrganizationShareStatus(%q) = %q, want %q", tc.input, got, tc.output)
}
if valid := IsValidOrganizationShareStatus(tc.input); valid != tc.valid {
t.Fatalf("IsValidOrganizationShareStatus(%q) = %v, want %v", tc.input, valid, tc.valid)
}
})
}
}