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{}