mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-11 14:00:29 +00:00
360 lines
12 KiB
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)
|
|
}
|
|
})
|
|
}
|
|
}
|