github.com/authzed/spicedb@v1.32.1-0.20240520085336-ebda56537386/pkg/datastore/datastore_test.go (about)

     1  package datastore
     2  
     3  import (
     4  	"context"
     5  	"testing"
     6  
     7  	"github.com/authzed/spicedb/pkg/datastore/options"
     8  	"github.com/authzed/spicedb/pkg/tuple"
     9  
    10  	v1 "github.com/authzed/authzed-go/proto/authzed/api/v1"
    11  	"github.com/stretchr/testify/require"
    12  )
    13  
    14  func TestRelationshipsFilterFromPublicFilter(t *testing.T) {
    15  	tests := []struct {
    16  		name          string
    17  		input         *v1.RelationshipFilter
    18  		expected      RelationshipsFilter
    19  		expectedError string
    20  	}{
    21  		{
    22  			"empty",
    23  			&v1.RelationshipFilter{},
    24  			RelationshipsFilter{},
    25  			"at least one filter field must be set",
    26  		},
    27  		{
    28  			"resource id prefix",
    29  			&v1.RelationshipFilter{OptionalResourceIdPrefix: "someprefix"},
    30  			RelationshipsFilter{OptionalResourceIDPrefix: "someprefix"},
    31  			"",
    32  		},
    33  		{
    34  			"resource id and prefix",
    35  			&v1.RelationshipFilter{OptionalResourceIdPrefix: "someprefix", OptionalResourceId: "someid"},
    36  			RelationshipsFilter{},
    37  			"cannot specify both OptionalResourceId and OptionalResourceIDPrefix",
    38  		},
    39  		{
    40  			"only resource name",
    41  			&v1.RelationshipFilter{ResourceType: "sometype"},
    42  			RelationshipsFilter{OptionalResourceType: "sometype"},
    43  			"",
    44  		},
    45  		{
    46  			"only relation name",
    47  			&v1.RelationshipFilter{OptionalRelation: "somerel"},
    48  			RelationshipsFilter{OptionalResourceRelation: "somerel"},
    49  			"",
    50  		},
    51  		{
    52  			"resource name and id",
    53  			&v1.RelationshipFilter{ResourceType: "sometype", OptionalResourceId: "someid"},
    54  			RelationshipsFilter{OptionalResourceType: "sometype", OptionalResourceIds: []string{"someid"}},
    55  			"",
    56  		},
    57  		{
    58  			"resource name and relation",
    59  			&v1.RelationshipFilter{ResourceType: "sometype", OptionalRelation: "somerel"},
    60  			RelationshipsFilter{OptionalResourceType: "sometype", OptionalResourceRelation: "somerel"},
    61  			"",
    62  		},
    63  		{
    64  			"resource and subject",
    65  			&v1.RelationshipFilter{ResourceType: "sometype", OptionalSubjectFilter: &v1.SubjectFilter{SubjectType: "someothertype"}},
    66  			RelationshipsFilter{OptionalResourceType: "sometype", OptionalSubjectsSelectors: []SubjectsSelector{
    67  				{
    68  					OptionalSubjectType: "someothertype",
    69  				},
    70  			}},
    71  			"",
    72  		},
    73  		{
    74  			"resource and subject with optional relation",
    75  			&v1.RelationshipFilter{
    76  				ResourceType: "sometype",
    77  				OptionalSubjectFilter: &v1.SubjectFilter{
    78  					SubjectType:      "someothertype",
    79  					OptionalRelation: &v1.SubjectFilter_RelationFilter{Relation: "somerel"},
    80  				},
    81  			},
    82  			RelationshipsFilter{OptionalResourceType: "sometype", OptionalSubjectsSelectors: []SubjectsSelector{
    83  				{
    84  					OptionalSubjectType: "someothertype",
    85  					RelationFilter:      SubjectRelationFilter{}.WithNonEllipsisRelation("somerel"),
    86  				},
    87  			}},
    88  			"",
    89  		},
    90  		{
    91  			"resource and subject with ellipsis relation",
    92  			&v1.RelationshipFilter{
    93  				ResourceType: "sometype",
    94  				OptionalSubjectFilter: &v1.SubjectFilter{
    95  					SubjectType:      "someothertype",
    96  					OptionalRelation: &v1.SubjectFilter_RelationFilter{Relation: ""},
    97  				},
    98  			},
    99  			RelationshipsFilter{OptionalResourceType: "sometype", OptionalSubjectsSelectors: []SubjectsSelector{
   100  				{
   101  					OptionalSubjectType: "someothertype",
   102  					RelationFilter:      SubjectRelationFilter{}.WithEllipsisRelation(),
   103  				},
   104  			}},
   105  			"",
   106  		},
   107  		{
   108  			"full",
   109  			&v1.RelationshipFilter{
   110  				ResourceType:       "sometype",
   111  				OptionalResourceId: "someid",
   112  				OptionalRelation:   "somerel",
   113  				OptionalSubjectFilter: &v1.SubjectFilter{
   114  					SubjectType:       "someothertype",
   115  					OptionalSubjectId: "somesubjectid",
   116  					OptionalRelation:  &v1.SubjectFilter_RelationFilter{Relation: ""},
   117  				},
   118  			},
   119  			RelationshipsFilter{
   120  				OptionalResourceType:     "sometype",
   121  				OptionalResourceIds:      []string{"someid"},
   122  				OptionalResourceRelation: "somerel",
   123  				OptionalSubjectsSelectors: []SubjectsSelector{
   124  					{
   125  						OptionalSubjectType: "someothertype",
   126  						OptionalSubjectIds:  []string{"somesubjectid"},
   127  						RelationFilter:      SubjectRelationFilter{}.WithEllipsisRelation(),
   128  					},
   129  				},
   130  			},
   131  			"",
   132  		},
   133  	}
   134  
   135  	for _, test := range tests {
   136  		test := test
   137  		t.Run(test.name, func(t *testing.T) {
   138  			computed, err := RelationshipsFilterFromPublicFilter(test.input)
   139  			if test.expectedError != "" {
   140  				require.ErrorContains(t, err, test.expectedError)
   141  				return
   142  			}
   143  
   144  			require.Equal(t, test.expected, computed)
   145  			require.NoError(t, err)
   146  		})
   147  	}
   148  }
   149  
   150  func TestConvertedRelationshipFilterTest(t *testing.T) {
   151  	tcs := []struct {
   152  		name               string
   153  		filter             *v1.RelationshipFilter
   154  		relationshipString string
   155  		expected           bool
   156  	}{
   157  		{
   158  			name: "namespace filter match",
   159  			filter: &v1.RelationshipFilter{
   160  				ResourceType: "foo",
   161  			},
   162  			relationshipString: "foo:something#viewer@user:fred",
   163  			expected:           true,
   164  		},
   165  		{
   166  			name: "namespace filter mismatch",
   167  			filter: &v1.RelationshipFilter{
   168  				ResourceType: "foo",
   169  			},
   170  			relationshipString: "bar:something#viewer@user:fred",
   171  			expected:           false,
   172  		},
   173  		{
   174  			name: "resource id filter match",
   175  			filter: &v1.RelationshipFilter{
   176  				ResourceType:       "foo",
   177  				OptionalResourceId: "something",
   178  			},
   179  			relationshipString: "foo:something#viewer@user:fred",
   180  			expected:           true,
   181  		},
   182  		{
   183  			name: "resource id filter mismatch",
   184  			filter: &v1.RelationshipFilter{
   185  				ResourceType:       "foo",
   186  				OptionalResourceId: "something",
   187  			},
   188  			relationshipString: "foo:somethingelse#viewer@user:fred",
   189  			expected:           false,
   190  		},
   191  		{
   192  			name: "resource id prefix filter match",
   193  			filter: &v1.RelationshipFilter{
   194  				OptionalResourceIdPrefix: "some",
   195  			},
   196  			relationshipString: "foo:something#viewer@user:fred",
   197  			expected:           true,
   198  		},
   199  		{
   200  			name: "resource id prefix filter mismatch",
   201  			filter: &v1.RelationshipFilter{
   202  				OptionalResourceIdPrefix: "some",
   203  			},
   204  			relationshipString: "foo:else#viewer@user:fred",
   205  			expected:           false,
   206  		},
   207  		{
   208  			name: "relation filter match",
   209  			filter: &v1.RelationshipFilter{
   210  				ResourceType:     "foo",
   211  				OptionalRelation: "viewer",
   212  			},
   213  			relationshipString: "foo:something#viewer@user:fred",
   214  			expected:           true,
   215  		},
   216  		{
   217  			name: "relation filter mismatch",
   218  			filter: &v1.RelationshipFilter{
   219  				ResourceType:     "foo",
   220  				OptionalRelation: "viewer",
   221  			},
   222  			relationshipString: "foo:something#editor@user:fred",
   223  			expected:           false,
   224  		},
   225  		{
   226  			name: "subject type filter match",
   227  			filter: &v1.RelationshipFilter{
   228  				ResourceType: "foo",
   229  				OptionalSubjectFilter: &v1.SubjectFilter{
   230  					SubjectType: "user",
   231  				},
   232  			},
   233  			relationshipString: "foo:something#viewer@user:fred",
   234  			expected:           true,
   235  		},
   236  		{
   237  			name: "subject type filter mismatch",
   238  			filter: &v1.RelationshipFilter{
   239  				ResourceType: "foo",
   240  				OptionalSubjectFilter: &v1.SubjectFilter{
   241  					SubjectType: "user",
   242  				},
   243  			},
   244  			relationshipString: "foo:something#viewer@group:foo",
   245  			expected:           false,
   246  		},
   247  		{
   248  			name: "subject id filter match",
   249  			filter: &v1.RelationshipFilter{
   250  				ResourceType: "foo",
   251  				OptionalSubjectFilter: &v1.SubjectFilter{
   252  					SubjectType:       "user",
   253  					OptionalSubjectId: "fred",
   254  				},
   255  			},
   256  			relationshipString: "foo:something#viewer@user:fred",
   257  			expected:           true,
   258  		},
   259  		{
   260  			name: "subject id filter mismatch",
   261  			filter: &v1.RelationshipFilter{
   262  				ResourceType: "foo",
   263  				OptionalSubjectFilter: &v1.SubjectFilter{
   264  					SubjectType:       "user",
   265  					OptionalSubjectId: "fred",
   266  				},
   267  			},
   268  			relationshipString: "foo:something#viewer@user:alice",
   269  			expected:           false,
   270  		},
   271  		{
   272  			name: "subject relation filter match",
   273  			filter: &v1.RelationshipFilter{
   274  				ResourceType: "foo",
   275  				OptionalSubjectFilter: &v1.SubjectFilter{
   276  					SubjectType:      "user",
   277  					OptionalRelation: &v1.SubjectFilter_RelationFilter{Relation: "far"},
   278  				},
   279  			},
   280  			relationshipString: "foo:something#viewer@user:fred#far",
   281  			expected:           true,
   282  		},
   283  		{
   284  			name: "subject relation filter mismatch",
   285  			filter: &v1.RelationshipFilter{
   286  				ResourceType: "foo",
   287  				OptionalSubjectFilter: &v1.SubjectFilter{
   288  					SubjectType:      "user",
   289  					OptionalRelation: &v1.SubjectFilter_RelationFilter{Relation: "far"},
   290  				},
   291  			},
   292  			relationshipString: "foo:something#viewer@user:fred#bar",
   293  			expected:           false,
   294  		},
   295  	}
   296  
   297  	for _, tc := range tcs {
   298  		tc := tc
   299  		t.Run(tc.name, func(t *testing.T) {
   300  			filter, err := RelationshipsFilterFromPublicFilter(tc.filter)
   301  			require.NoError(t, err)
   302  
   303  			relationship := tuple.MustParse(tc.relationshipString)
   304  			require.Equal(t, tc.expected, filter.Test(relationship))
   305  		})
   306  	}
   307  }
   308  
   309  func TestRelationshipsFilterTest(t *testing.T) {
   310  	tcs := []struct {
   311  		name               string
   312  		filter             RelationshipsFilter
   313  		relationshipString string
   314  		expected           bool
   315  	}{
   316  		{
   317  			name:               "namespace filter match",
   318  			filter:             RelationshipsFilter{OptionalResourceType: "foo"},
   319  			relationshipString: "foo:something#viewer@user:fred",
   320  			expected:           true,
   321  		},
   322  		{
   323  			name: "resource id filter match",
   324  			filter: RelationshipsFilter{
   325  				OptionalResourceType: "foo",
   326  				OptionalResourceIds:  []string{"something"},
   327  			},
   328  			relationshipString: "foo:something#viewer@user:fred",
   329  			expected:           true,
   330  		},
   331  		{
   332  			name: "resource id filter mismatch",
   333  			filter: RelationshipsFilter{
   334  				OptionalResourceType: "foo",
   335  				OptionalResourceIds:  []string{"something"},
   336  			},
   337  			relationshipString: "foo:somethingelse#viewer@user:fred",
   338  			expected:           false,
   339  		},
   340  		{
   341  			name: "resource id prefix filter match",
   342  			filter: RelationshipsFilter{
   343  				OptionalResourceIDPrefix: "some",
   344  			},
   345  			relationshipString: "foo:something#viewer@user:fred",
   346  			expected:           true,
   347  		},
   348  		{
   349  			name: "resource id prefix filter mismatch",
   350  			filter: RelationshipsFilter{
   351  				OptionalResourceIDPrefix: "some",
   352  			},
   353  			relationshipString: "foo:else#viewer@user:fred",
   354  			expected:           false,
   355  		},
   356  		{
   357  			name: "relation filter match",
   358  			filter: RelationshipsFilter{
   359  				OptionalResourceType:     "foo",
   360  				OptionalResourceRelation: "viewer",
   361  			},
   362  			relationshipString: "foo:something#viewer@user:fred",
   363  			expected:           true,
   364  		},
   365  		{
   366  			name: "relation filter mismatch",
   367  			filter: RelationshipsFilter{
   368  				OptionalResourceType:     "foo",
   369  				OptionalResourceRelation: "viewer",
   370  			},
   371  			relationshipString: "foo:something#editor@user:fred",
   372  			expected:           false,
   373  		},
   374  		{
   375  			name: "subject type filter match",
   376  			filter: RelationshipsFilter{
   377  				OptionalResourceType: "foo",
   378  				OptionalSubjectsSelectors: []SubjectsSelector{
   379  					{
   380  						OptionalSubjectType: "user",
   381  					},
   382  				},
   383  			},
   384  			relationshipString: "foo:something#viewer@user:fred",
   385  			expected:           true,
   386  		},
   387  		{
   388  			name: "subject type filter mismatch",
   389  			filter: RelationshipsFilter{
   390  				OptionalResourceType: "foo",
   391  				OptionalSubjectsSelectors: []SubjectsSelector{
   392  					{
   393  						OptionalSubjectType: "user",
   394  					},
   395  				},
   396  			},
   397  			relationshipString: "foo:something#viewer@group:foo",
   398  			expected:           false,
   399  		},
   400  		{
   401  			name: "subject id filter match",
   402  			filter: RelationshipsFilter{
   403  				OptionalResourceType: "foo",
   404  				OptionalSubjectsSelectors: []SubjectsSelector{
   405  					{
   406  						OptionalSubjectType: "user",
   407  						OptionalSubjectIds:  []string{"fred"},
   408  					},
   409  				},
   410  			},
   411  			relationshipString: "foo:something#viewer@user:fred",
   412  			expected:           true,
   413  		},
   414  		{
   415  			name: "subject id filter mismatch",
   416  			filter: RelationshipsFilter{
   417  				OptionalResourceType: "foo",
   418  				OptionalSubjectsSelectors: []SubjectsSelector{
   419  					{
   420  						OptionalSubjectType: "user",
   421  						OptionalSubjectIds:  []string{"fred"},
   422  					},
   423  				},
   424  			},
   425  			relationshipString: "foo:something#viewer@user:alice",
   426  			expected:           false,
   427  		},
   428  		{
   429  			name: "subject relation filter match",
   430  			filter: RelationshipsFilter{
   431  				OptionalResourceType: "foo",
   432  				OptionalSubjectsSelectors: []SubjectsSelector{
   433  					{
   434  						OptionalSubjectType: "user",
   435  						RelationFilter:      SubjectRelationFilter{}.WithNonEllipsisRelation("far"),
   436  					},
   437  				},
   438  			},
   439  			relationshipString: "foo:something#viewer@user:fred#far",
   440  			expected:           true,
   441  		},
   442  		{
   443  			name: "subject relation filter mismatch",
   444  			filter: RelationshipsFilter{
   445  				OptionalResourceType: "foo",
   446  				OptionalSubjectsSelectors: []SubjectsSelector{
   447  					{
   448  						OptionalSubjectType: "user",
   449  						RelationFilter:      SubjectRelationFilter{}.WithNonEllipsisRelation("far"),
   450  					},
   451  				},
   452  			},
   453  			relationshipString: "foo:something#viewer@user:fred#bar",
   454  			expected:           false,
   455  		},
   456  		{
   457  			name: "multiple subject filter matches",
   458  			filter: RelationshipsFilter{
   459  				OptionalResourceType: "foo",
   460  				OptionalSubjectsSelectors: []SubjectsSelector{
   461  					{
   462  						OptionalSubjectType: "user",
   463  						OptionalSubjectIds:  []string{"fred"},
   464  					},
   465  					{
   466  						OptionalSubjectType: "user",
   467  						OptionalSubjectIds:  []string{"alice"},
   468  					},
   469  				},
   470  			},
   471  			relationshipString: "foo:something#viewer@user:alice",
   472  			expected:           true,
   473  		},
   474  		{
   475  			name: "multiple subject filter mismatches",
   476  			filter: RelationshipsFilter{
   477  				OptionalResourceType: "foo",
   478  				OptionalSubjectsSelectors: []SubjectsSelector{
   479  					{
   480  						OptionalSubjectType: "user",
   481  						OptionalSubjectIds:  []string{"fred"},
   482  					},
   483  					{
   484  						OptionalSubjectType: "user",
   485  						OptionalSubjectIds:  []string{"alice"},
   486  					},
   487  				},
   488  			},
   489  			relationshipString: "foo:something#viewer@user:tom",
   490  			expected:           false,
   491  		},
   492  		{
   493  			name: "caveat filter match",
   494  			filter: RelationshipsFilter{
   495  				OptionalCaveatName: "bar",
   496  			},
   497  			relationshipString: "foo:something#viewer@user:fred[bar]",
   498  			expected:           true,
   499  		},
   500  		{
   501  			name: "caveat filter mismatch",
   502  			filter: RelationshipsFilter{
   503  				OptionalCaveatName: "bar",
   504  			},
   505  			relationshipString: "foo:something#viewer@user:fred[baz]",
   506  			expected:           false,
   507  		},
   508  		{
   509  			name: "non-ellipsis subject filter match",
   510  			filter: RelationshipsFilter{
   511  				OptionalSubjectsSelectors: []SubjectsSelector{
   512  					{
   513  						OptionalSubjectType: "user",
   514  						RelationFilter:      SubjectRelationFilter{}.WithOnlyNonEllipsisRelations(),
   515  					},
   516  				},
   517  			},
   518  			relationshipString: "foo:something#viewer@user:fred#foo",
   519  			expected:           true,
   520  		},
   521  		{
   522  			name: "non-ellipsis subject filter mismatch",
   523  			filter: RelationshipsFilter{
   524  				OptionalSubjectsSelectors: []SubjectsSelector{
   525  					{
   526  						OptionalSubjectType: "user",
   527  						RelationFilter:      SubjectRelationFilter{}.WithOnlyNonEllipsisRelations(),
   528  					},
   529  				},
   530  			},
   531  			relationshipString: "foo:something#viewer@user:fred",
   532  			expected:           false,
   533  		},
   534  		{
   535  			name: "multi subject filter match",
   536  			filter: RelationshipsFilter{
   537  				OptionalSubjectsSelectors: []SubjectsSelector{
   538  					{
   539  						OptionalSubjectType: "user",
   540  						RelationFilter:      SubjectRelationFilter{}.WithEllipsisRelation().WithNonEllipsisRelation("foo"),
   541  					},
   542  				},
   543  			},
   544  			relationshipString: "foo:something#viewer@user:fred",
   545  			expected:           true,
   546  		},
   547  	}
   548  
   549  	for _, tc := range tcs {
   550  		tc := tc
   551  		t.Run(tc.name, func(t *testing.T) {
   552  			relationship := tuple.MustParse(tc.relationshipString)
   553  			require.Equal(t, tc.expected, tc.filter.Test(relationship))
   554  		})
   555  	}
   556  }
   557  
   558  func TestUnwrapAs(t *testing.T) {
   559  	result := UnwrapAs[error](nil)
   560  	require.Nil(t, result)
   561  
   562  	ds := fakeDatastore{delegate: fakeDatastore{fakeDatastoreError{}}}
   563  	result = UnwrapAs[error](ds)
   564  	require.NotNil(t, result)
   565  	require.IsType(t, fakeDatastoreError{}, result)
   566  
   567  	errorable := fakeDatastoreError{}
   568  	result = UnwrapAs[error](errorable)
   569  	require.NotNil(t, result)
   570  	require.IsType(t, fakeDatastoreError{}, result)
   571  }
   572  
   573  type fakeDatastoreError struct {
   574  	fakeDatastore
   575  }
   576  
   577  func (e fakeDatastoreError) Error() string {
   578  	return ""
   579  }
   580  
   581  type fakeDatastore struct {
   582  	delegate Datastore
   583  }
   584  
   585  func (f fakeDatastore) Unwrap() Datastore {
   586  	return f.delegate
   587  }
   588  
   589  func (f fakeDatastore) SnapshotReader(_ Revision) Reader {
   590  	return nil
   591  }
   592  
   593  func (f fakeDatastore) ReadWriteTx(_ context.Context, _ TxUserFunc, _ ...options.RWTOptionsOption) (Revision, error) {
   594  	return nil, nil
   595  }
   596  
   597  func (f fakeDatastore) OptimizedRevision(_ context.Context) (Revision, error) {
   598  	return nil, nil
   599  }
   600  
   601  func (f fakeDatastore) HeadRevision(_ context.Context) (Revision, error) {
   602  	return nil, nil
   603  }
   604  
   605  func (f fakeDatastore) CheckRevision(_ context.Context, _ Revision) error {
   606  	return nil
   607  }
   608  
   609  func (f fakeDatastore) RevisionFromString(_ string) (Revision, error) {
   610  	return nil, nil
   611  }
   612  
   613  func (f fakeDatastore) Watch(_ context.Context, _ Revision, _ WatchOptions) (<-chan *RevisionChanges, <-chan error) {
   614  	return nil, nil
   615  }
   616  
   617  func (f fakeDatastore) ReadyState(_ context.Context) (ReadyState, error) {
   618  	return ReadyState{}, nil
   619  }
   620  
   621  func (f fakeDatastore) Features(_ context.Context) (*Features, error) {
   622  	return nil, nil
   623  }
   624  
   625  func (f fakeDatastore) Statistics(_ context.Context) (Stats, error) {
   626  	return Stats{}, nil
   627  }
   628  
   629  func (f fakeDatastore) Close() error {
   630  	return nil
   631  }