chore: Allow RecordingAuthorizer to record multiple rbac authz calls (#6024)

* chore: Allow RecordingAuthorizer to record multiple rbac authz calls

Prior iteration only recorded the last call. This is required for
more comprehensive testing
This commit is contained in:
Steven Masley 2023-02-03 13:03:46 -06:00 committed by GitHub
parent 571f5d0e02
commit b359dbbd8b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 720 additions and 53 deletions

View File

@ -7,17 +7,17 @@ import (
"net/http"
"strconv"
"strings"
"sync"
"testing"
"time"
"github.com/coder/coder/coderd/database/dbfake"
"github.com/go-chi/chi/v5"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/xerrors"
"github.com/coder/coder/coderd"
"github.com/coder/coder/coderd/database/dbfake"
"github.com/coder/coder/coderd/rbac"
"github.com/coder/coder/coderd/rbac/regosql"
"github.com/coder/coder/codersdk"
@ -443,7 +443,9 @@ func NewAuthTester(ctx context.Context, t *testing.T, client *codersdk.Client, a
func (a *AuthTester) Test(ctx context.Context, assertRoute map[string]RouteCheck, skipRoutes map[string]string) {
// Always fail auth from this point forward
a.authorizer.AlwaysReturn = rbac.ForbiddenWithInternal(xerrors.New("fake implementation"), nil, nil)
a.authorizer.Wrapped = &FakeAuthorizer{
AlwaysReturn: rbac.ForbiddenWithInternal(xerrors.New("fake implementation"), nil, nil),
}
routeMissing := make(map[string]bool)
for k, v := range assertRoute {
@ -483,7 +485,7 @@ func (a *AuthTester) Test(ctx context.Context, assertRoute map[string]RouteCheck
return nil
}
a.t.Run(name, func(t *testing.T) {
a.authorizer.reset()
a.authorizer.Reset()
routeKey := strings.TrimRight(name, "/")
routeAssertions, ok := assertRoute[routeKey]
@ -514,18 +516,19 @@ func (a *AuthTester) Test(ctx context.Context, assertRoute map[string]RouteCheck
assert.Equal(t, http.StatusForbidden, resp.StatusCode, "expect unauthorized")
}
}
if a.authorizer.Called != nil {
if a.authorizer.lastCall() != nil {
last := a.authorizer.lastCall()
if routeAssertions.AssertAction != "" {
assert.Equal(t, routeAssertions.AssertAction, a.authorizer.Called.Action, "resource action")
assert.Equal(t, routeAssertions.AssertAction, last.Action, "resource action")
}
if routeAssertions.AssertObject.Type != "" {
assert.Equal(t, routeAssertions.AssertObject.Type, a.authorizer.Called.Object.Type, "resource type")
assert.Equal(t, routeAssertions.AssertObject.Type, last.Object.Type, "resource type")
}
if routeAssertions.AssertObject.Owner != "" {
assert.Equal(t, routeAssertions.AssertObject.Owner, a.authorizer.Called.Object.Owner, "resource owner")
assert.Equal(t, routeAssertions.AssertObject.Owner, last.Object.Owner, "resource owner")
}
if routeAssertions.AssertObject.OrgID != "" {
assert.Equal(t, routeAssertions.AssertObject.OrgID, a.authorizer.Called.Object.OrgID, "resource org")
assert.Equal(t, routeAssertions.AssertObject.OrgID, last.Object.OrgID, "resource org")
}
}
} else {
@ -539,52 +542,195 @@ func (a *AuthTester) Test(ctx context.Context, assertRoute map[string]RouteCheck
}
type authCall struct {
Subject rbac.Subject
Action rbac.Action
Object rbac.Object
}
Actor rbac.Subject
Action rbac.Action
Object rbac.Object
type RecordingAuthorizer struct {
Called *authCall
AlwaysReturn error
asserted bool
}
var _ rbac.Authorizer = (*RecordingAuthorizer)(nil)
// AuthorizeSQL does not record the call. This matches the postgres behavior
// of not calling Authorize()
func (r *RecordingAuthorizer) AuthorizeSQL(_ context.Context, _ rbac.Subject, _ rbac.Action, _ rbac.Object) error {
return r.AlwaysReturn
// RecordingAuthorizer wraps any rbac.Authorizer and records all Authorize()
// calls made. This is useful for testing as these calls can later be asserted.
type RecordingAuthorizer struct {
sync.RWMutex
Called []authCall
Wrapped rbac.Authorizer
}
func (r *RecordingAuthorizer) Authorize(_ context.Context, subject rbac.Subject, action rbac.Action, object rbac.Object) error {
r.Called = &authCall{
Subject: subject,
Action: action,
Object: object,
type ActionObjectPair struct {
Action rbac.Action
Object rbac.Object
}
// Pair is on the RecordingAuthorizer to be easy to find and keep the pkg
// interface smaller.
func (*RecordingAuthorizer) Pair(action rbac.Action, object rbac.Objecter) ActionObjectPair {
return ActionObjectPair{
Action: action,
Object: object.RBACObject(),
}
return r.AlwaysReturn
}
func (r *RecordingAuthorizer) Prepare(_ context.Context, subject rbac.Subject, action rbac.Action, _ string) (rbac.PreparedAuthorized, error) {
return &fakePreparedAuthorizer{
Original: r,
Subject: subject,
Action: action,
HardCodedSQLString: "true",
// AllAsserted returns an error if all calls to Authorize() have not been
// asserted and checked. This is useful for testing to ensure that all
// Authorize() calls are checked in the unit test.
func (r *RecordingAuthorizer) AllAsserted() error {
r.RLock()
defer r.RUnlock()
missed := []authCall{}
for _, c := range r.Called {
if !c.asserted {
missed = append(missed, c)
}
}
if len(missed) > 0 {
return xerrors.Errorf("missed calls: %+v", missed)
}
return nil
}
// AssertActor asserts in order. If the order of authz calls does not match,
// this will fail.
func (r *RecordingAuthorizer) AssertActor(t *testing.T, actor rbac.Subject, did ...ActionObjectPair) {
r.RLock()
defer r.RUnlock()
ptr := 0
for i, call := range r.Called {
if ptr == len(did) {
// Finished all assertions
return
}
if call.Actor.ID == actor.ID {
action, object := did[ptr].Action, did[ptr].Object
assert.Equalf(t, action, call.Action, "assert action %d", ptr)
assert.Equalf(t, object, call.Object, "assert object %d", ptr)
r.Called[i].asserted = true
ptr++
}
}
assert.Equalf(t, len(did), ptr, "assert actor: didn't find all actions, %d missing actions", len(did)-ptr)
}
// recordAuthorize is the internal method that records the Authorize() call.
func (r *RecordingAuthorizer) recordAuthorize(subject rbac.Subject, action rbac.Action, object rbac.Object) {
r.Lock()
defer r.Unlock()
r.Called = append(r.Called, authCall{
Actor: subject,
Action: action,
Object: object,
})
}
func (r *RecordingAuthorizer) Authorize(ctx context.Context, subject rbac.Subject, action rbac.Action, object rbac.Object) error {
r.recordAuthorize(subject, action, object)
if r.Wrapped == nil {
panic("Developer error: RecordingAuthorizer.Wrapped is nil")
}
return r.Wrapped.Authorize(ctx, subject, action, object)
}
func (r *RecordingAuthorizer) Prepare(ctx context.Context, subject rbac.Subject, action rbac.Action, objectType string) (rbac.PreparedAuthorized, error) {
r.RLock()
defer r.RUnlock()
if r.Wrapped == nil {
panic("Developer error: RecordingAuthorizer.Wrapped is nil")
}
prep, err := r.Wrapped.Prepare(ctx, subject, action, objectType)
if err != nil {
return nil, err
}
return &PreparedRecorder{
rec: r,
prepped: prep,
subject: subject,
action: action,
}, nil
}
func (r *RecordingAuthorizer) reset() {
// Reset clears the recorded Authorize() calls.
func (r *RecordingAuthorizer) Reset() {
r.Lock()
defer r.Unlock()
r.Called = nil
}
// lastCall is implemented to support legacy tests.
// Deprecated
func (r *RecordingAuthorizer) lastCall() *authCall {
r.RLock()
defer r.RUnlock()
if len(r.Called) == 0 {
return nil
}
return &r.Called[len(r.Called)-1]
}
// PreparedRecorder is the prepared version of the RecordingAuthorizer.
// It records the Authorize() calls to the original recorder. If the caller
// uses CompileToSQL, all recording stops. This is to support parity between
// memory and SQL backed dbs.
type PreparedRecorder struct {
rec *RecordingAuthorizer
prepped rbac.PreparedAuthorized
subject rbac.Subject
action rbac.Action
rw sync.Mutex
usingSQL bool
}
func (s *PreparedRecorder) Authorize(ctx context.Context, object rbac.Object) error {
s.rw.Lock()
defer s.rw.Unlock()
if !s.usingSQL {
s.rec.recordAuthorize(s.subject, s.action, object)
}
return s.prepped.Authorize(ctx, object)
}
func (s *PreparedRecorder) CompileToSQL(ctx context.Context, cfg regosql.ConvertConfig) (string, error) {
s.rw.Lock()
defer s.rw.Unlock()
s.usingSQL = true
return s.prepped.CompileToSQL(ctx, cfg)
}
// FakeAuthorizer is an Authorizer that always returns the same error.
type FakeAuthorizer struct {
// AlwaysReturn is the error that will be returned by Authorize.
AlwaysReturn error
}
var _ rbac.Authorizer = (*FakeAuthorizer)(nil)
func (d *FakeAuthorizer) Authorize(_ context.Context, _ rbac.Subject, _ rbac.Action, _ rbac.Object) error {
return d.AlwaysReturn
}
func (d *FakeAuthorizer) Prepare(_ context.Context, subject rbac.Subject, action rbac.Action, _ string) (rbac.PreparedAuthorized, error) {
return &fakePreparedAuthorizer{
Original: d,
Subject: subject,
Action: action,
}, nil
}
var _ rbac.PreparedAuthorized = (*fakePreparedAuthorizer)(nil)
// fakePreparedAuthorizer is the prepared version of a FakeAuthorizer. It will
// return the same error as the original FakeAuthorizer.
type fakePreparedAuthorizer struct {
Original *RecordingAuthorizer
Subject rbac.Subject
Action rbac.Action
HardCodedSQLString string
HardCodedRegoString string
sync.RWMutex
Original *FakeAuthorizer
Subject rbac.Subject
Action rbac.Action
}
func (f *fakePreparedAuthorizer) Authorize(ctx context.Context, object rbac.Object) error {
@ -593,17 +739,6 @@ func (f *fakePreparedAuthorizer) Authorize(ctx context.Context, object rbac.Obje
// CompileToSQL returns a compiled version of the authorizer that will work for
// in memory databases. This fake version will not work against a SQL database.
func (fakePreparedAuthorizer) CompileToSQL(_ context.Context, _ regosql.ConvertConfig) (string, error) {
return "", xerrors.New("not implemented")
}
func (f *fakePreparedAuthorizer) Eval(object rbac.Object) bool {
return f.Original.AuthorizeSQL(context.Background(), f.Subject, f.Action, object) == nil
}
func (f fakePreparedAuthorizer) RegoString() string {
if f.HardCodedRegoString != "" {
return f.HardCodedRegoString
}
panic("not implemented")
func (*fakePreparedAuthorizer) CompileToSQL(_ context.Context, _ regosql.ConvertConfig) (string, error) {
return "not a valid sql string", nil
}

View File

@ -4,7 +4,11 @@ import (
"context"
"testing"
"github.com/moby/moby/pkg/namesgenerator"
"github.com/stretchr/testify/require"
"github.com/coder/coder/coderd/coderdtest"
"github.com/coder/coder/coderd/rbac"
)
func TestAuthorizeAllEndpoints(t *testing.T) {
@ -12,7 +16,7 @@ func TestAuthorizeAllEndpoints(t *testing.T) {
client, _, api := coderdtest.NewWithAPI(t, &coderdtest.Options{
// Required for any subdomain-based proxy tests to pass.
AppHostname: "*.test.coder.com",
Authorizer: &coderdtest.RecordingAuthorizer{},
Authorizer: &coderdtest.RecordingAuthorizer{Wrapped: &coderdtest.FakeAuthorizer{}},
IncludeProvisionerDaemon: true,
})
admin := coderdtest.CreateFirstUser(t, client)
@ -20,3 +24,111 @@ func TestAuthorizeAllEndpoints(t *testing.T) {
skipRoute, assertRoute := coderdtest.AGPLRoutes(a)
a.Test(context.Background(), assertRoute, skipRoute)
}
func TestAuthzRecorder(t *testing.T) {
t.Parallel()
t.Run("Authorize", func(t *testing.T) {
t.Parallel()
rec := &coderdtest.RecordingAuthorizer{
Wrapped: &coderdtest.FakeAuthorizer{},
}
sub := randomSubject()
pairs := fuzzAuthz(t, sub, rec, 10)
rec.AssertActor(t, sub, pairs...)
require.NoError(t, rec.AllAsserted(), "all assertions should have been made")
})
t.Run("Authorize2Subjects", func(t *testing.T) {
t.Parallel()
rec := &coderdtest.RecordingAuthorizer{
Wrapped: &coderdtest.FakeAuthorizer{},
}
a := randomSubject()
aPairs := fuzzAuthz(t, a, rec, 10)
b := randomSubject()
bPairs := fuzzAuthz(t, b, rec, 10)
rec.AssertActor(t, b, bPairs...)
rec.AssertActor(t, a, aPairs...)
require.NoError(t, rec.AllAsserted(), "all assertions should have been made")
})
t.Run("Authorize&Prepared", func(t *testing.T) {
t.Parallel()
rec := &coderdtest.RecordingAuthorizer{
Wrapped: &coderdtest.FakeAuthorizer{},
}
a := randomSubject()
aPairs := fuzzAuthz(t, a, rec, 10)
b := randomSubject()
act, objTy := randomAction(), randomObject().Type
prep, _ := rec.Prepare(context.Background(), b, act, objTy)
bPairs := fuzzAuthzPrep(t, prep, 10, act, objTy)
rec.AssertActor(t, b, bPairs...)
rec.AssertActor(t, a, aPairs...)
require.NoError(t, rec.AllAsserted(), "all assertions should have been made")
})
}
// fuzzAuthzPrep has same action and object types for all calls.
func fuzzAuthzPrep(t *testing.T, prep rbac.PreparedAuthorized, n int, action rbac.Action, objectType string) []coderdtest.ActionObjectPair {
t.Helper()
pairs := make([]coderdtest.ActionObjectPair, 0, n)
for i := 0; i < n; i++ {
obj := randomObject()
obj.Type = objectType
p := coderdtest.ActionObjectPair{Action: action, Object: obj}
_ = prep.Authorize(context.Background(), p.Object)
pairs = append(pairs, p)
}
return pairs
}
func fuzzAuthz(t *testing.T, sub rbac.Subject, rec rbac.Authorizer, n int) []coderdtest.ActionObjectPair {
t.Helper()
pairs := make([]coderdtest.ActionObjectPair, 0, n)
for i := 0; i < n; i++ {
p := coderdtest.ActionObjectPair{Action: randomAction(), Object: randomObject()}
_ = rec.Authorize(context.Background(), sub, p.Action, p.Object)
pairs = append(pairs, p)
}
return pairs
}
func randomAction() rbac.Action {
return rbac.Action(namesgenerator.GetRandomName(1))
}
func randomObject() rbac.Object {
return rbac.Object{
ID: namesgenerator.GetRandomName(1),
Owner: namesgenerator.GetRandomName(1),
OrgID: namesgenerator.GetRandomName(1),
Type: namesgenerator.GetRandomName(1),
ACLUserList: map[string][]rbac.Action{
namesgenerator.GetRandomName(1): {rbac.ActionRead},
},
ACLGroupList: map[string][]rbac.Action{
namesgenerator.GetRandomName(1): {rbac.ActionRead},
},
}
}
func randomSubject() rbac.Subject {
return rbac.Subject{
ID: namesgenerator.GetRandomName(1),
Roles: rbac.RoleNames{rbac.RoleMember()},
Groups: []string{namesgenerator.GetRandomName(1)},
Scope: rbac.ScopeAll,
}
}

View File

@ -15,6 +15,7 @@ import (
"github.com/coder/coder/coderd/rbac/regosql"
"github.com/coder/coder/coderd/tracing"
"github.com/coder/coder/coderd/util/slice"
)
// Subject is a struct that contains all the elements of a subject in an rbac
@ -26,6 +27,25 @@ type Subject struct {
Scope ExpandableScope
}
func (s Subject) Equal(b Subject) bool {
if s.ID != b.ID {
return false
}
if !slice.SameElements(s.Groups, b.Groups) {
return false
}
if !slice.SameElements(s.SafeRoleNames(), b.SafeRoleNames()) {
return false
}
if s.SafeScopeName() != b.SafeScopeName() {
return false
}
return true
}
// SafeScopeName prevent nil pointer dereference.
func (s Subject) SafeScopeName() string {
if s.Scope == nil {

View File

@ -176,6 +176,49 @@ type Object struct {
ACLGroupList map[string][]Action ` json:"acl_group_list"`
}
func (z Object) Equal(b Object) bool {
if z.ID != b.ID {
return false
}
if z.Owner != b.Owner {
return false
}
if z.OrgID != b.OrgID {
return false
}
if z.Type != b.Type {
return false
}
if !equalACLLists(z.ACLUserList, b.ACLUserList) {
return false
}
if !equalACLLists(z.ACLGroupList, b.ACLGroupList) {
return false
}
return true
}
func equalACLLists(a, b map[string][]Action) bool {
if len(a) != len(b) {
return false
}
for k, actions := range a {
if len(actions) != len(b[k]) {
return false
}
for i, a := range actions {
if a != b[k][i] {
return false
}
}
}
return true
}
func (z Object) RBACObject() Object {
return z
}

176
coderd/rbac/object_test.go Normal file
View File

@ -0,0 +1,176 @@
package rbac_test
import (
"testing"
"github.com/coder/coder/coderd/rbac"
)
func TestObjectEqual(t *testing.T) {
t.Parallel()
testCases := []struct {
Name string
A rbac.Object
B rbac.Object
Expected bool
}{
{
Name: "Empty",
A: rbac.Object{},
B: rbac.Object{},
Expected: true,
},
{
Name: "NilVs0",
A: rbac.Object{
ACLGroupList: map[string][]rbac.Action{},
ACLUserList: map[string][]rbac.Action{},
},
B: rbac.Object{},
Expected: true,
},
{
Name: "Same",
A: rbac.Object{
ID: "id",
Owner: "owner",
OrgID: "orgID",
Type: "type",
ACLUserList: map[string][]rbac.Action{},
ACLGroupList: map[string][]rbac.Action{},
},
B: rbac.Object{
ID: "id",
Owner: "owner",
OrgID: "orgID",
Type: "type",
ACLUserList: map[string][]rbac.Action{},
ACLGroupList: map[string][]rbac.Action{},
},
Expected: true,
},
{
Name: "DifferentID",
A: rbac.Object{
ID: "id",
},
B: rbac.Object{
ID: "id2",
},
Expected: false,
},
{
Name: "DifferentOwner",
A: rbac.Object{
Owner: "owner",
},
B: rbac.Object{
Owner: "owner2",
},
Expected: false,
},
{
Name: "DifferentOrgID",
A: rbac.Object{
OrgID: "orgID",
},
B: rbac.Object{
OrgID: "orgID2",
},
Expected: false,
},
{
Name: "DifferentType",
A: rbac.Object{
Type: "type",
},
B: rbac.Object{
Type: "type2",
},
Expected: false,
},
{
Name: "DifferentACLUserList",
A: rbac.Object{
ACLUserList: map[string][]rbac.Action{
"user1": {rbac.ActionRead},
},
},
B: rbac.Object{
ACLUserList: map[string][]rbac.Action{
"user2": {rbac.ActionRead},
},
},
Expected: false,
},
{
Name: "ACLUserDiff#Actions",
A: rbac.Object{
ACLUserList: map[string][]rbac.Action{
"user1": {rbac.ActionRead},
},
},
B: rbac.Object{
ACLUserList: map[string][]rbac.Action{
"user1": {rbac.ActionRead, rbac.ActionUpdate},
},
},
Expected: false,
},
{
Name: "ACLUserDiffAction",
A: rbac.Object{
ACLUserList: map[string][]rbac.Action{
"user1": {rbac.ActionRead},
},
},
B: rbac.Object{
ACLUserList: map[string][]rbac.Action{
"user1": {rbac.ActionUpdate},
},
},
Expected: false,
},
{
Name: "ACLUserDiff#Users",
A: rbac.Object{
ACLUserList: map[string][]rbac.Action{
"user1": {rbac.ActionRead},
},
},
B: rbac.Object{
ACLUserList: map[string][]rbac.Action{
"user1": {rbac.ActionRead},
"user2": {rbac.ActionRead},
},
},
Expected: false,
},
{
Name: "DifferentACLGroupList",
A: rbac.Object{
ACLGroupList: map[string][]rbac.Action{
"group1": {rbac.ActionRead},
},
},
B: rbac.Object{
ACLGroupList: map[string][]rbac.Action{
"group2": {rbac.ActionRead},
},
},
Expected: false,
},
}
for _, tc := range testCases {
tc := tc
t.Run(tc.Name, func(t *testing.T) {
t.Parallel()
actual := tc.A.Equal(tc.B)
if actual != tc.Expected {
t.Errorf("expected %v, got %v", tc.Expected, actual)
}
})
}
}

132
coderd/rbac/subject_test.go Normal file
View File

@ -0,0 +1,132 @@
package rbac_test
import (
"testing"
"github.com/coder/coder/coderd/rbac"
)
func TestSubjectEqual(t *testing.T) {
t.Parallel()
testCases := []struct {
Name string
A rbac.Subject
B rbac.Subject
Expected bool
}{
{
Name: "Empty",
A: rbac.Subject{},
B: rbac.Subject{},
Expected: true,
},
{
Name: "Same",
A: rbac.Subject{
ID: "id",
Roles: rbac.RoleNames{rbac.RoleMember()},
Groups: []string{"group"},
Scope: rbac.ScopeAll,
},
B: rbac.Subject{
ID: "id",
Roles: rbac.RoleNames{rbac.RoleMember()},
Groups: []string{"group"},
Scope: rbac.ScopeAll,
},
Expected: true,
},
{
Name: "DifferentID",
A: rbac.Subject{
ID: "id",
},
B: rbac.Subject{
ID: "id2",
},
Expected: false,
},
{
Name: "RolesNilVs0",
A: rbac.Subject{
Roles: rbac.RoleNames{},
},
B: rbac.Subject{
Roles: nil,
},
Expected: true,
},
{
Name: "GroupsNilVs0",
A: rbac.Subject{
Groups: []string{},
},
B: rbac.Subject{
Groups: nil,
},
Expected: true,
},
{
Name: "DifferentRoles",
A: rbac.Subject{
Roles: rbac.RoleNames{rbac.RoleMember()},
},
B: rbac.Subject{
Roles: rbac.RoleNames{rbac.RoleOwner()},
},
Expected: false,
},
{
Name: "Different#Roles",
A: rbac.Subject{
Roles: rbac.RoleNames{rbac.RoleMember()},
},
B: rbac.Subject{
Roles: rbac.RoleNames{rbac.RoleMember(), rbac.RoleOwner()},
},
Expected: false,
},
{
Name: "DifferentGroups",
A: rbac.Subject{
Groups: []string{"group1"},
},
B: rbac.Subject{
Groups: []string{"group2"},
},
Expected: false,
},
{
Name: "Different#Groups",
A: rbac.Subject{
Groups: []string{"group1"},
},
B: rbac.Subject{
Groups: []string{"group1", "group2"},
},
Expected: false,
},
{
Name: "DifferentScope",
A: rbac.Subject{
Scope: rbac.ScopeAll,
},
B: rbac.Subject{
Scope: rbac.ScopeApplicationConnect,
},
Expected: false,
},
}
for _, tc := range testCases {
tc := tc
t.Run(tc.Name, func(t *testing.T) {
t.Parallel()
actual := tc.A.Equal(tc.B)
if actual != tc.Expected {
t.Errorf("expected %v, got %v", tc.Expected, actual)
}
})
}
}

View File

@ -1,5 +1,25 @@
package slice
// New is a convenience method for creating []T.
func New[T any](items ...T) []T {
return items
}
// SameElements returns true if the 2 lists have the same elements in any
// order.
func SameElements[T comparable](a []T, b []T) bool {
if len(a) != len(b) {
return false
}
for _, element := range a {
if !Contains(b, element) {
return false
}
}
return true
}
func ContainsCompare[T any](haystack []T, needle T, equal func(a, b T) bool) bool {
for _, hay := range haystack {
if equal(needle, hay) {

View File

@ -1,14 +1,43 @@
package slice_test
import (
"math/rand"
"testing"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/coder/coder/coderd/util/slice"
)
func TestSameElements(t *testing.T) {
t.Parallel()
// True
assertSameElements(t, []int{})
assertSameElements(t, []int{1, 2, 3})
assertSameElements(t, slice.New("a", "b", "c"))
assertSameElements(t, slice.New(uuid.New(), uuid.New(), uuid.New()))
// False
assert.False(t, slice.SameElements([]int{1, 2, 3}, []int{1, 2, 3, 4}))
assert.False(t, slice.SameElements([]int{1, 2, 3}, []int{1, 2}))
assert.False(t, slice.SameElements([]int{1, 2, 3}, []int{}))
assert.False(t, slice.SameElements([]int{}, []int{1, 2, 3}))
assert.False(t, slice.SameElements([]int{1, 2, 3}, []int{1, 2, 4}))
assert.False(t, slice.SameElements([]int{1}, []int{2}))
}
func assertSameElements[T comparable](t *testing.T, elements []T) {
cpy := make([]T, len(elements))
copy(cpy, elements)
rand.Shuffle(len(cpy), func(i, j int) {
cpy[i], cpy[j] = cpy[j], cpy[i]
})
assert.True(t, slice.SameElements(elements, cpy))
}
func TestUnique(t *testing.T) {
t.Parallel()

View File

@ -27,7 +27,7 @@ func TestAuthorizeAllEndpoints(t *testing.T) {
Options: &coderdtest.Options{
// Required for any subdomain-based proxy tests to pass.
AppHostname: "*.test.coder.com",
Authorizer: &coderdtest.RecordingAuthorizer{},
Authorizer: &coderdtest.RecordingAuthorizer{Wrapped: &coderdtest.FakeAuthorizer{}},
IncludeProvisionerDaemon: true,
},
})