Files
web/organizations/role_invitations_test.go
T
gamertan c0986168bc
verify / verify (push) Successful in 4m17s
Support atomic organization role sets and owner-managed invitations
Signed-off-by: Cole Speelman <crspeelman@gmail.com>
2026-09-05 00:10:08 -04:00

102 lines
4.1 KiB
Go

// SPDX-License-Identifier: MPL-2.0
package organizations
import (
"context"
"crypto/sha256"
"errors"
"slices"
"strings"
"testing"
"time"
)
type roleInvitationRepositoryStub struct {
repositoryStub
audit AuditEvent
ownerRole string
created int
}
func (r *roleInvitationRepositoryStub) CreateInvitationWithRoles(_ context.Context, invitation Invitation, ownerRole string, audit AuditEvent) error {
r.created++
r.invitation, r.audit, r.ownerRole = invitation, audit, ownerRole
return nil
}
func (r *roleInvitationRepositoryStub) AcceptInvitationWithRoles(_ context.Context, _ [32]byte, userID, ownerRole string, _ time.Time, audit AuditEvent) error {
r.acceptedUser, r.ownerRole, r.audit = userID, ownerRole, audit
return nil
}
func TestInvitationRoleNamesAreBoundedAndUnambiguous(t *testing.T) {
input := []string{"buyer", "billing"}
roles, err := (Invitation{DirectRoles: input}).RoleNames()
if err != nil || !slices.Equal(roles, []string{"billing", "buyer"}) || !slices.Equal(input, []string{"buyer", "billing"}) {
t.Fatalf("roles=%v input=%v err=%v", roles, input, err)
}
for _, invitation := range []Invitation{
{DirectRole: "owner", DirectRoles: []string{"buyer"}},
{DirectRoles: []string{"buyer", "buyer"}},
{DirectRoles: []string{"not a role"}},
{DirectRoles: make([]string, 17)},
} {
if _, err := invitation.RoleNames(); err == nil {
t.Fatalf("invalid roles accepted=%+v", invitation)
}
}
if roles, err = (Invitation{DirectRole: "buyer"}).RoleNames(); err != nil || !slices.Equal(roles, []string{"buyer"}) {
t.Fatalf("legacy=%v err=%v", roles, err)
}
}
func TestRoleInvitationServicePreservesOwnerRequirementAndRequest(t *testing.T) {
r := &roleInvitationRepositoryStub{}
service, err := New(r, Options{OwnerRole: "owner", OwnerManagedInvitations: true})
if err != nil {
t.Fatal(err)
}
input := InviteWithAccess{OrganizationID: "organization-123", InvitedByUserID: "owner-12345678", Email: " Member@example.test ", DirectRoles: []string{"buyer", "billing"}, Lifetime: time.Hour, RequestID: "request-invite"}
raw, invitation, err := service.InviteWithAccess(t.Context(), input)
if err != nil {
t.Fatal(err)
}
if invitation.Digest != sha256.Sum256([]byte(raw)) || invitation.RequiredOwnerRole != "owner" || r.ownerRole != "owner" || r.audit.RequestID != input.RequestID || !slices.Equal(invitation.DirectRoles, []string{"billing", "buyer"}) || invitation.Email != "member@example.test" {
t.Fatalf("invitation or audit mismatch: %+v %+v", invitation, r.audit)
}
if !slices.Equal(input.DirectRoles, []string{"buyer", "billing"}) {
t.Fatal("caller roles mutated")
}
if err = service.AcceptInvitation(t.Context(), raw, "member-12345678"); err != nil || r.ownerRole != "owner" || r.acceptedUser != "member-12345678" {
t.Fatalf("accept=%v owner=%q user=%q", err, r.ownerRole, r.acceptedUser)
}
input.RequestID = strings.Repeat("x", 129)
if _, _, err = service.InviteWithAccess(t.Context(), input); err == nil || r.created != 1 {
t.Fatal("oversized request accepted")
}
if _, err = New(r, Options{OwnerManagedInvitations: true}); err == nil {
t.Fatal("owner-managed service without owner accepted")
}
}
func TestRoleInvitationHasNoPartialLegacyFallback(t *testing.T) {
for _, input := range []InviteWithAccess{
{DirectRoles: []string{"buyer", "billing"}}, {DirectRole: "owner"}, {},
} {
r := &repositoryStub{}
service, err := New(r, Options{OwnerRole: "owner", OwnerManagedInvitations: true})
if err != nil {
t.Fatal(err)
}
input.OrganizationID, input.InvitedByUserID, input.Email, input.Lifetime = "organization-123", "owner-12345678", "member@example.test", time.Hour
if _, _, err = service.InviteWithAccess(t.Context(), input); !errors.Is(err, ErrRoleInvitationUnsupported) || r.invitation.ID != "" {
t.Fatalf("legacy fallback=%v", err)
}
r.invitation = Invitation{OrganizationID: input.OrganizationID, ID: "invitation-1234", RequiredOwnerRole: "owner"}
if err = service.AcceptInvitation(t.Context(), strings.Repeat("a", 43), "member-12345678"); !errors.Is(err, ErrRoleInvitationUnsupported) || r.acceptedUser != "" {
t.Fatalf("legacy acceptance fallback=%v", err)
}
}
}