github.com/authzed/spicedb@v1.32.1-0.20240520085336-ebda56537386/internal/dispatch/caching/cachingdispatch_test.go (about) 1 package caching 2 3 import ( 4 "context" 5 "testing" 6 "time" 7 8 "github.com/shopspring/decimal" 9 "github.com/stretchr/testify/mock" 10 "github.com/stretchr/testify/require" 11 12 "github.com/authzed/spicedb/internal/dispatch" 13 core "github.com/authzed/spicedb/pkg/proto/core/v1" 14 v1 "github.com/authzed/spicedb/pkg/proto/dispatch/v1" 15 "github.com/authzed/spicedb/pkg/tuple" 16 ) 17 18 type checkRequest struct { 19 start string 20 goal string 21 atRevision decimal.Decimal 22 depthRequired uint32 23 depthRemaining uint32 24 expectPassthrough bool 25 } 26 27 func RR(namespaceName string, relationName string) *core.RelationReference { 28 return &core.RelationReference{ 29 Namespace: namespaceName, 30 Relation: relationName, 31 } 32 } 33 34 func TestMaxDepthCaching(t *testing.T) { 35 start1 := "document:doc1#read" 36 start2 := "document:doc2#read" 37 user1 := "user:user1#..." 38 user2 := "user:user2#..." 39 40 testCases := []struct { 41 name string 42 script []checkRequest 43 }{ 44 {"single request", []checkRequest{ 45 {start1, user1, decimal.Zero, 1, 50, true}, 46 }}, 47 {"two requests, hit", []checkRequest{ 48 {start1, user1, decimal.Zero, 1, 50, true}, 49 {start1, user1, decimal.Zero, 1, 50, false}, 50 }}, 51 {"many requests, hit", []checkRequest{ 52 {start1, user1, decimal.Zero, 1, 50, true}, 53 {start1, user1, decimal.Zero, 1, 50, false}, 54 {start1, user1, decimal.Zero, 1, 50, false}, 55 {start1, user1, decimal.Zero, 1, 50, false}, 56 {start1, user1, decimal.Zero, 1, 50, false}, 57 }}, 58 {"multiple keys", []checkRequest{ 59 {start1, user1, decimal.Zero, 1, 50, true}, 60 {start2, user2, decimal.Zero, 1, 50, true}, 61 }}, 62 {"same object, different revisions miss", []checkRequest{ 63 {start1, user1, decimal.Zero, 1, 50, true}, 64 {start1, user1, decimal.NewFromInt(50), 1, 50, true}, 65 }}, 66 {"interleaved objects, hit", []checkRequest{ 67 {start1, user1, decimal.Zero, 1, 50, true}, 68 {start2, user2, decimal.Zero, 1, 50, true}, 69 {start1, user1, decimal.Zero, 1, 50, false}, 70 {start2, user2, decimal.Zero, 1, 50, false}, 71 }}, 72 {"insufficient depth", []checkRequest{ 73 {start1, user1, decimal.Zero, 21, 50, true}, 74 {start1, user1, decimal.Zero, 21, 20, true}, 75 }}, 76 {"sufficient depth", []checkRequest{ 77 {start1, user1, decimal.Zero, 1, 40, true}, 78 {start1, user1, decimal.Zero, 1, 50, false}, 79 }}, 80 {"updated cached depth", []checkRequest{ 81 {start1, user1, decimal.Zero, 21, 50, true}, 82 {start1, user1, decimal.Zero, 21, 40, false}, 83 {start1, user1, decimal.Zero, 21, 20, true}, 84 {start1, user1, decimal.Zero, 21, 50, false}, 85 }}, 86 } 87 88 for _, tc := range testCases { 89 tc := tc 90 t.Run(tc.name, func(t *testing.T) { 91 require := require.New(t) 92 93 delegate := delegateDispatchMock{&mock.Mock{}} 94 95 for _, step := range tc.script { 96 if step.expectPassthrough { 97 parsed := tuple.ParseONR(step.start) 98 delegate.On("DispatchCheck", &v1.DispatchCheckRequest{ 99 ResourceRelation: RR(parsed.Namespace, parsed.Relation), 100 ResourceIds: []string{parsed.ObjectId}, 101 Subject: tuple.ParseSubjectONR(step.goal), 102 Metadata: &v1.ResolverMeta{ 103 AtRevision: step.atRevision.String(), 104 DepthRemaining: step.depthRemaining, 105 }, 106 }).Return(&v1.DispatchCheckResponse{ 107 ResultsByResourceId: map[string]*v1.ResourceCheckResult{ 108 parsed.ObjectId: { 109 Membership: v1.ResourceCheckResult_MEMBER, 110 }, 111 }, 112 Metadata: &v1.ResponseMeta{ 113 DispatchCount: 1, 114 DepthRequired: step.depthRequired, 115 }, 116 }, nil).Times(1) 117 } 118 } 119 120 dispatch, err := NewCachingDispatcher(DispatchTestCache(t), false, "", nil) 121 dispatch.SetDelegate(delegate) 122 require.NoError(err) 123 defer dispatch.Close() 124 125 for _, step := range tc.script { 126 parsed := tuple.ParseONR(step.start) 127 resp, err := dispatch.DispatchCheck(context.Background(), &v1.DispatchCheckRequest{ 128 ResourceRelation: RR(parsed.Namespace, parsed.Relation), 129 ResourceIds: []string{parsed.ObjectId}, 130 Subject: tuple.ParseSubjectONR(step.goal), 131 Metadata: &v1.ResolverMeta{ 132 AtRevision: step.atRevision.String(), 133 DepthRemaining: step.depthRemaining, 134 }, 135 }) 136 require.NoError(err) 137 require.Equal(v1.ResourceCheckResult_MEMBER, resp.ResultsByResourceId[parsed.ObjectId].Membership) 138 139 // We have to sleep a while to let the cache converge: 140 // https://github.com/outcaste-io/ristretto/blob/01b9f37dd0fd453225e042d6f3a27cd14f252cd0/cache_test.go#L17 141 time.Sleep(10 * time.Millisecond) 142 } 143 144 delegate.AssertExpectations(t) 145 }) 146 } 147 } 148 149 type delegateDispatchMock struct { 150 *mock.Mock 151 } 152 153 func (ddm delegateDispatchMock) DispatchCheck(_ context.Context, req *v1.DispatchCheckRequest) (*v1.DispatchCheckResponse, error) { 154 args := ddm.Called(req) 155 return args.Get(0).(*v1.DispatchCheckResponse), args.Error(1) 156 } 157 158 func (ddm delegateDispatchMock) DispatchExpand(_ context.Context, _ *v1.DispatchExpandRequest) (*v1.DispatchExpandResponse, error) { 159 return &v1.DispatchExpandResponse{}, nil 160 } 161 162 func (ddm delegateDispatchMock) DispatchReachableResources(_ *v1.DispatchReachableResourcesRequest, _ dispatch.ReachableResourcesStream) error { 163 return nil 164 } 165 166 func (ddm delegateDispatchMock) DispatchLookupResources(_ *v1.DispatchLookupResourcesRequest, _ dispatch.LookupResourcesStream) error { 167 return nil 168 } 169 170 func (ddm delegateDispatchMock) DispatchLookupSubjects(_ *v1.DispatchLookupSubjectsRequest, _ dispatch.LookupSubjectsStream) error { 171 return nil 172 } 173 174 func (ddm delegateDispatchMock) Close() error { 175 return nil 176 } 177 178 func (ddm delegateDispatchMock) ReadyState() dispatch.ReadyState { 179 return dispatch.ReadyState{IsReady: true} 180 } 181 182 var _ dispatch.Dispatcher = &delegateDispatchMock{}