Support atomic organization role sets and owner-managed invitations
verify / verify (push) Successful in 4m17s
verify / verify (push) Successful in 4m17s
Signed-off-by: Cole Speelman <crspeelman@gmail.com>
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user