github.com/authzed/spicedb@v1.32.1-0.20240520085336-ebda56537386/internal/services/v1/expreflection_test.go (about)

     1  package v1
     2  
     3  import (
     4  	"reflect"
     5  	"strings"
     6  	"testing"
     7  
     8  	v1 "github.com/authzed/authzed-go/proto/authzed/api/v1"
     9  	"github.com/ettle/strcase"
    10  	"github.com/stretchr/testify/require"
    11  
    12  	"github.com/authzed/spicedb/pkg/datastore/revisionparsing"
    13  	"github.com/authzed/spicedb/pkg/diff"
    14  	"github.com/authzed/spicedb/pkg/genutil/mapz"
    15  	"github.com/authzed/spicedb/pkg/schemadsl/compiler"
    16  	"github.com/authzed/spicedb/pkg/schemadsl/input"
    17  	"github.com/authzed/spicedb/pkg/testutil"
    18  )
    19  
    20  func TestConvertDiff(t *testing.T) {
    21  	tcs := []struct {
    22  		name             string
    23  		existingSchema   string
    24  		comparisonSchema string
    25  		expectedResponse *v1.ExperimentalDiffSchemaResponse
    26  	}{
    27  		{
    28  			"no diff",
    29  			`definition user {}`,
    30  			`definition user {}`,
    31  			&v1.ExperimentalDiffSchemaResponse{
    32  				Diffs: []*v1.ExpSchemaDiff{},
    33  			},
    34  		},
    35  		{
    36  			"add namespace",
    37  			``,
    38  			`definition user {}`,
    39  			&v1.ExperimentalDiffSchemaResponse{
    40  				Diffs: []*v1.ExpSchemaDiff{
    41  					{
    42  						Diff: &v1.ExpSchemaDiff_DefinitionAdded{
    43  							DefinitionAdded: &v1.ExpDefinition{
    44  								Name:    "user",
    45  								Comment: "",
    46  							},
    47  						},
    48  					},
    49  				},
    50  			},
    51  		},
    52  		{
    53  			"remove namespace",
    54  			`definition user {}`,
    55  			``,
    56  			&v1.ExperimentalDiffSchemaResponse{
    57  				Diffs: []*v1.ExpSchemaDiff{
    58  					{
    59  						Diff: &v1.ExpSchemaDiff_DefinitionRemoved{
    60  							DefinitionRemoved: &v1.ExpDefinition{
    61  								Name:    "user",
    62  								Comment: "",
    63  							},
    64  						},
    65  					},
    66  				},
    67  			},
    68  		},
    69  		{
    70  			"change namespace comment",
    71  			`definition user {}`,
    72  			`// user has a comment
    73  			definition user {}`,
    74  			&v1.ExperimentalDiffSchemaResponse{
    75  				Diffs: []*v1.ExpSchemaDiff{
    76  					{
    77  						Diff: &v1.ExpSchemaDiff_DefinitionDocCommentChanged{
    78  							DefinitionDocCommentChanged: &v1.ExpDefinition{
    79  								Name:    "user",
    80  								Comment: "// user has a comment",
    81  							},
    82  						},
    83  					},
    84  				},
    85  			},
    86  		},
    87  		{
    88  			"add caveat",
    89  			``,
    90  			`caveat someCaveat(someparam int) { someparam < 42 }`,
    91  			&v1.ExperimentalDiffSchemaResponse{
    92  				Diffs: []*v1.ExpSchemaDiff{
    93  					{
    94  						Diff: &v1.ExpSchemaDiff_CaveatAdded{
    95  							CaveatAdded: &v1.ExpCaveat{
    96  								Name:       "someCaveat",
    97  								Comment:    "",
    98  								Expression: "someparam < 42",
    99  								Parameters: []*v1.ExpCaveatParameter{
   100  									{
   101  										Name:             "someparam",
   102  										Type:             "int",
   103  										ParentCaveatName: "someCaveat",
   104  									},
   105  								},
   106  							},
   107  						},
   108  					},
   109  				},
   110  			},
   111  		},
   112  		{
   113  			"remove caveat",
   114  			`caveat someCaveat(someparam int) { someparam < 42 }`,
   115  			``,
   116  			&v1.ExperimentalDiffSchemaResponse{
   117  				Diffs: []*v1.ExpSchemaDiff{
   118  					{
   119  						Diff: &v1.ExpSchemaDiff_CaveatRemoved{
   120  							CaveatRemoved: &v1.ExpCaveat{
   121  								Name:       "someCaveat",
   122  								Comment:    "",
   123  								Expression: "someparam < 42",
   124  								Parameters: []*v1.ExpCaveatParameter{
   125  									{
   126  										Name:             "someparam",
   127  										Type:             "int",
   128  										ParentCaveatName: "someCaveat",
   129  									},
   130  								},
   131  							},
   132  						},
   133  					},
   134  				},
   135  			},
   136  		},
   137  		{
   138  			"change caveat comment",
   139  			`// someCaveat has a comment
   140  			caveat someCaveat(someparam int) { someparam < 42 }`,
   141  			`// someCaveat has b comment
   142  			caveat someCaveat(someparam int) { someparam < 42 }`,
   143  			&v1.ExperimentalDiffSchemaResponse{
   144  				Diffs: []*v1.ExpSchemaDiff{
   145  					{
   146  						Diff: &v1.ExpSchemaDiff_CaveatDocCommentChanged{
   147  							CaveatDocCommentChanged: &v1.ExpCaveat{
   148  								Name:       "someCaveat",
   149  								Comment:    "// someCaveat has b comment",
   150  								Expression: "someparam < 42",
   151  								Parameters: []*v1.ExpCaveatParameter{
   152  									{
   153  										Name:             "someparam",
   154  										Type:             "int",
   155  										ParentCaveatName: "someCaveat",
   156  									},
   157  								},
   158  							},
   159  						},
   160  					},
   161  				},
   162  			},
   163  		},
   164  		{
   165  			"added relation",
   166  			`definition user {}`,
   167  			`definition user { relation somerel: user; }`,
   168  			&v1.ExperimentalDiffSchemaResponse{
   169  				Diffs: []*v1.ExpSchemaDiff{
   170  					{
   171  						Diff: &v1.ExpSchemaDiff_RelationAdded{
   172  							RelationAdded: &v1.ExpRelation{
   173  								Name:                 "somerel",
   174  								Comment:              "",
   175  								ParentDefinitionName: "user",
   176  								SubjectTypes: []*v1.ExpTypeReference{
   177  									{
   178  										SubjectDefinitionName: "user",
   179  										Typeref:               &v1.ExpTypeReference_IsTerminalSubject{},
   180  									},
   181  								},
   182  							},
   183  						},
   184  					},
   185  				},
   186  			},
   187  		},
   188  		{
   189  			"removed relation",
   190  			`definition user { relation somerel: user; }`,
   191  			`definition user {}`,
   192  			&v1.ExperimentalDiffSchemaResponse{
   193  				Diffs: []*v1.ExpSchemaDiff{
   194  					{
   195  						Diff: &v1.ExpSchemaDiff_RelationRemoved{
   196  							RelationRemoved: &v1.ExpRelation{
   197  								Name:                 "somerel",
   198  								Comment:              "",
   199  								ParentDefinitionName: "user",
   200  								SubjectTypes: []*v1.ExpTypeReference{
   201  									{
   202  										SubjectDefinitionName: "user",
   203  										Typeref:               &v1.ExpTypeReference_IsTerminalSubject{},
   204  									},
   205  								},
   206  							},
   207  						},
   208  					},
   209  				},
   210  			},
   211  		},
   212  		{
   213  			"relation type added",
   214  			`definition user {}
   215  			
   216  			 definition anon {}
   217  
   218  			 definition resource {
   219  				relation viewer: anon
   220  			 }
   221  			`,
   222  			`definition user {}
   223  			
   224  			definition anon {}
   225  
   226  			definition resource {
   227  			   relation viewer: user | anon
   228  			}
   229  		   `,
   230  			&v1.ExperimentalDiffSchemaResponse{
   231  				Diffs: []*v1.ExpSchemaDiff{
   232  					{
   233  						Diff: &v1.ExpSchemaDiff_RelationSubjectTypeAdded{
   234  							RelationSubjectTypeAdded: &v1.ExpRelationSubjectTypeChange{
   235  								Relation: &v1.ExpRelation{
   236  									Name:                 "viewer",
   237  									Comment:              "",
   238  									ParentDefinitionName: "resource",
   239  									SubjectTypes: []*v1.ExpTypeReference{
   240  										{
   241  											SubjectDefinitionName: "user",
   242  											Typeref:               &v1.ExpTypeReference_IsTerminalSubject{},
   243  										},
   244  										{
   245  											SubjectDefinitionName: "anon",
   246  											Typeref:               &v1.ExpTypeReference_IsTerminalSubject{},
   247  										},
   248  									},
   249  								},
   250  								ChangedSubjectType: &v1.ExpTypeReference{
   251  									SubjectDefinitionName: "user",
   252  									Typeref:               &v1.ExpTypeReference_IsTerminalSubject{},
   253  								},
   254  							},
   255  						},
   256  					},
   257  				},
   258  			},
   259  		},
   260  		{
   261  			"relation type removed",
   262  			`definition user {}
   263  			
   264  			 definition anon {}
   265  
   266  			 definition resource {
   267  				relation viewer: anon | user
   268  			 }
   269  			`,
   270  			`definition user {}
   271  			
   272  			definition anon {}
   273  
   274  			definition resource {
   275  			   relation viewer: user
   276  			}
   277  		   `,
   278  			&v1.ExperimentalDiffSchemaResponse{
   279  				Diffs: []*v1.ExpSchemaDiff{
   280  					{
   281  						Diff: &v1.ExpSchemaDiff_RelationSubjectTypeRemoved{
   282  							RelationSubjectTypeRemoved: &v1.ExpRelationSubjectTypeChange{
   283  								Relation: &v1.ExpRelation{
   284  									Name:                 "viewer",
   285  									Comment:              "",
   286  									ParentDefinitionName: "resource",
   287  									SubjectTypes: []*v1.ExpTypeReference{
   288  										{
   289  											SubjectDefinitionName: "user",
   290  											Typeref:               &v1.ExpTypeReference_IsTerminalSubject{},
   291  										},
   292  									},
   293  								},
   294  								ChangedSubjectType: &v1.ExpTypeReference{
   295  									SubjectDefinitionName: "anon",
   296  									Typeref:               &v1.ExpTypeReference_IsTerminalSubject{},
   297  								},
   298  							},
   299  						},
   300  					},
   301  				},
   302  			},
   303  		},
   304  		{
   305  			"relation comment changed",
   306  			`definition user {}
   307  
   308  			 definition resource {
   309  				relation viewer: user
   310  			 }`,
   311  			`definition user {}
   312  
   313  			definition resource {
   314  				// viewer has a comment
   315  				relation viewer: user
   316  			}`,
   317  			&v1.ExperimentalDiffSchemaResponse{
   318  				Diffs: []*v1.ExpSchemaDiff{
   319  					{
   320  						Diff: &v1.ExpSchemaDiff_RelationDocCommentChanged{
   321  							RelationDocCommentChanged: &v1.ExpRelation{
   322  								Name:                 "viewer",
   323  								Comment:              "// viewer has a comment",
   324  								ParentDefinitionName: "resource",
   325  								SubjectTypes: []*v1.ExpTypeReference{
   326  									{
   327  										SubjectDefinitionName: "user",
   328  										Typeref:               &v1.ExpTypeReference_IsTerminalSubject{},
   329  									},
   330  								},
   331  							},
   332  						},
   333  					},
   334  				},
   335  			},
   336  		},
   337  		{
   338  			"added permission",
   339  			`definition user {}
   340  			
   341  			 definition resource {
   342  			 }
   343  			`,
   344  			`definition user {}
   345  			
   346  			definition resource {
   347  				permission foo = nil
   348  			}`,
   349  			&v1.ExperimentalDiffSchemaResponse{
   350  				Diffs: []*v1.ExpSchemaDiff{
   351  					{
   352  						Diff: &v1.ExpSchemaDiff_PermissionAdded{
   353  							PermissionAdded: &v1.ExpPermission{
   354  								Name:                 "foo",
   355  								Comment:              "",
   356  								ParentDefinitionName: "resource",
   357  							},
   358  						},
   359  					},
   360  				},
   361  			},
   362  		},
   363  		{
   364  			"removed permission",
   365  			`definition user {}
   366  
   367  			 definition resource {
   368  				permission foo = nil
   369  			 }`,
   370  			`definition user {}
   371  
   372  			definition resource {
   373  			}`,
   374  			&v1.ExperimentalDiffSchemaResponse{
   375  				Diffs: []*v1.ExpSchemaDiff{
   376  					{
   377  						Diff: &v1.ExpSchemaDiff_PermissionRemoved{
   378  							PermissionRemoved: &v1.ExpPermission{
   379  								Name:                 "foo",
   380  								Comment:              "",
   381  								ParentDefinitionName: "resource",
   382  							},
   383  						},
   384  					},
   385  				},
   386  			},
   387  		},
   388  		{
   389  			"permission comment changed",
   390  			`definition user {}
   391  
   392  			 definition resource {
   393  				// foo has a comment
   394  				permission foo = nil
   395  			 }`,
   396  			`definition user {}
   397  			
   398  			definition resource {
   399  				// foo has a new comment
   400  				permission foo = nil
   401  			}`,
   402  			&v1.ExperimentalDiffSchemaResponse{
   403  				Diffs: []*v1.ExpSchemaDiff{
   404  					{
   405  						Diff: &v1.ExpSchemaDiff_PermissionDocCommentChanged{
   406  							PermissionDocCommentChanged: &v1.ExpPermission{
   407  								Name:                 "foo",
   408  								Comment:              "// foo has a new comment",
   409  								ParentDefinitionName: "resource",
   410  							},
   411  						},
   412  					},
   413  				},
   414  			},
   415  		},
   416  		{
   417  			"permission expression changed",
   418  			`definition resource {
   419  				permission foo = nil
   420  			}`,
   421  			`definition resource {
   422  				permission foo = foo
   423  			}`,
   424  			&v1.ExperimentalDiffSchemaResponse{
   425  				Diffs: []*v1.ExpSchemaDiff{
   426  					{
   427  						Diff: &v1.ExpSchemaDiff_PermissionExprChanged{
   428  							PermissionExprChanged: &v1.ExpPermission{
   429  								Name:                 "foo",
   430  								Comment:              "",
   431  								ParentDefinitionName: "resource",
   432  							},
   433  						},
   434  					},
   435  				},
   436  			},
   437  		},
   438  		{
   439  			"caveat parameter added",
   440  			`caveat someCaveat(someparam int) { someparam < 42 }`,
   441  			`caveat someCaveat(someparam int, someparam2 string) { someparam < 42 }`,
   442  			&v1.ExperimentalDiffSchemaResponse{
   443  				Diffs: []*v1.ExpSchemaDiff{
   444  					{
   445  						Diff: &v1.ExpSchemaDiff_CaveatParameterAdded{
   446  							CaveatParameterAdded: &v1.ExpCaveatParameter{
   447  								Name:             "someparam2",
   448  								Type:             "string",
   449  								ParentCaveatName: "someCaveat",
   450  							},
   451  						},
   452  					},
   453  				},
   454  			},
   455  		},
   456  		{
   457  			"caveat parameter removed",
   458  			`caveat someCaveat(someparam int, someparam2 string) { someparam < 42 }`,
   459  			`caveat someCaveat(someparam int) { someparam < 42 }`,
   460  			&v1.ExperimentalDiffSchemaResponse{
   461  				Diffs: []*v1.ExpSchemaDiff{
   462  					{
   463  						Diff: &v1.ExpSchemaDiff_CaveatParameterRemoved{
   464  							CaveatParameterRemoved: &v1.ExpCaveatParameter{
   465  								Name:             "someparam2",
   466  								Type:             "string",
   467  								ParentCaveatName: "someCaveat",
   468  							},
   469  						},
   470  					},
   471  				},
   472  			},
   473  		},
   474  		{
   475  			"caveat parameter type changed",
   476  			`caveat someCaveat(someparam int) { someparam < 42 }`,
   477  			`caveat someCaveat(someparam uint) { someparam < 42 }`,
   478  			&v1.ExperimentalDiffSchemaResponse{
   479  				Diffs: []*v1.ExpSchemaDiff{
   480  					{
   481  						Diff: &v1.ExpSchemaDiff_CaveatParameterTypeChanged{
   482  							CaveatParameterTypeChanged: &v1.ExpCaveatParameterTypeChange{
   483  								Parameter: &v1.ExpCaveatParameter{
   484  									Name:             "someparam",
   485  									Type:             "uint",
   486  									ParentCaveatName: "someCaveat",
   487  								},
   488  								PreviousType: "int",
   489  							},
   490  						},
   491  					},
   492  				},
   493  			},
   494  		},
   495  		{
   496  			"caveat expression changes",
   497  			`caveat someCaveat(someparam int) { someparam < 42 }`,
   498  			`caveat someCaveat(someparam int) { someparam < 43 }`,
   499  			&v1.ExperimentalDiffSchemaResponse{
   500  				Diffs: []*v1.ExpSchemaDiff{
   501  					{
   502  						Diff: &v1.ExpSchemaDiff_CaveatExprChanged{
   503  							CaveatExprChanged: &v1.ExpCaveat{
   504  								Name:       "someCaveat",
   505  								Comment:    "",
   506  								Expression: "someparam < 43",
   507  								Parameters: []*v1.ExpCaveatParameter{
   508  									{
   509  										Name:             "someparam",
   510  										Type:             "int",
   511  										ParentCaveatName: "someCaveat",
   512  									},
   513  								},
   514  							},
   515  						},
   516  					},
   517  				},
   518  			},
   519  		},
   520  	}
   521  
   522  	encounteredDiffTypes := mapz.NewSet[string]()
   523  	casesRun := 0
   524  
   525  	for _, tc := range tcs {
   526  		t.Run(tc.name, func(t *testing.T) {
   527  			casesRun++
   528  
   529  			existingSchema, err := compiler.Compile(compiler.InputSchema{
   530  				Source:       input.Source("schema"),
   531  				SchemaString: tc.existingSchema,
   532  			}, compiler.AllowUnprefixedObjectType())
   533  			require.NoError(t, err)
   534  
   535  			comparisonSchema, err := compiler.Compile(compiler.InputSchema{
   536  				Source:       input.Source("schema"),
   537  				SchemaString: tc.comparisonSchema,
   538  			}, compiler.AllowUnprefixedObjectType())
   539  			require.NoError(t, err)
   540  
   541  			es := diff.NewDiffableSchemaFromCompiledSchema(existingSchema)
   542  			cs := diff.NewDiffableSchemaFromCompiledSchema(comparisonSchema)
   543  
   544  			diff, err := diff.DiffSchemas(es, cs)
   545  			require.NoError(t, err)
   546  
   547  			resp, err := convertDiff(
   548  				diff,
   549  				&es,
   550  				&cs,
   551  				revisionparsing.MustParseRevisionForTest("1"),
   552  			)
   553  			if err != nil {
   554  				t.Fatalf("unexpected error: %v", err)
   555  			}
   556  			require.NotNil(t, resp.ReadAt)
   557  			resp.ReadAt = nil
   558  
   559  			testutil.RequireProtoEqual(t, tc.expectedResponse, resp, "got mismatch")
   560  
   561  			for _, diff := range resp.Diffs {
   562  				name := reflect.TypeOf(diff.GetDiff()).String()
   563  				encounteredDiffTypes.Add(strings.ToLower(strings.Split(name, "_")[1]))
   564  			}
   565  		})
   566  	}
   567  
   568  	if casesRun == len(tcs) {
   569  		msg := &v1.ExpSchemaDiff{}
   570  
   571  		allDiffTypes := mapz.NewSet[string]()
   572  		fields := msg.ProtoReflect().Descriptor().Oneofs().ByName("diff").Fields()
   573  		for i := 0; i < fields.Len(); i++ {
   574  			allDiffTypes.Add(strings.ToLower(strcase.ToCamel(string(fields.Get(i).Name()))))
   575  		}
   576  
   577  		require.Empty(t, allDiffTypes.Subtract(encounteredDiffTypes).AsSlice())
   578  	}
   579  }
   580  
   581  type filterCheck func(sf *schemaFilters) bool
   582  
   583  func TestSchemaFiltering(t *testing.T) {
   584  	tcs := []struct {
   585  		name     string
   586  		filters  []*v1.ExpSchemaFilter
   587  		checkers []filterCheck
   588  	}{
   589  		{
   590  			"no filters",
   591  			[]*v1.ExpSchemaFilter{},
   592  			[]filterCheck{
   593  				func(sf *schemaFilters) bool { return sf.HasNamespaces() },
   594  				func(sf *schemaFilters) bool { return sf.HasCaveats() },
   595  				func(sf *schemaFilters) bool { return sf.HasNamespace("foo") },
   596  				func(sf *schemaFilters) bool { return sf.HasCaveat("foo") },
   597  				func(sf *schemaFilters) bool { return sf.HasRelation("document", "viewer") },
   598  				func(sf *schemaFilters) bool { return sf.HasPermission("document", "view") },
   599  			},
   600  		},
   601  		{
   602  			"namespace filter",
   603  			[]*v1.ExpSchemaFilter{
   604  				{
   605  					OptionalDefinitionNameFilter: "doc",
   606  				},
   607  			},
   608  			[]filterCheck{
   609  				func(sf *schemaFilters) bool { return sf.HasNamespaces() },
   610  				func(sf *schemaFilters) bool { return !sf.HasCaveats() },
   611  				func(sf *schemaFilters) bool { return sf.HasNamespace("document") },
   612  				func(sf *schemaFilters) bool { return !sf.HasNamespace("foo") },
   613  				func(sf *schemaFilters) bool { return sf.HasRelation("document", "viewer") },
   614  				func(sf *schemaFilters) bool { return sf.HasPermission("document", "view") },
   615  			},
   616  		},
   617  		{
   618  			"caveat filter",
   619  			[]*v1.ExpSchemaFilter{
   620  				{
   621  					OptionalCaveatNameFilter: "somec",
   622  				},
   623  			},
   624  			[]filterCheck{
   625  				func(sf *schemaFilters) bool { return !sf.HasNamespaces() },
   626  				func(sf *schemaFilters) bool { return sf.HasCaveats() },
   627  				func(sf *schemaFilters) bool { return sf.HasCaveat("somecaveat") },
   628  				func(sf *schemaFilters) bool { return !sf.HasCaveat("foo") },
   629  			},
   630  		},
   631  		{
   632  			"multiple namespace filters",
   633  			[]*v1.ExpSchemaFilter{
   634  				{
   635  					OptionalDefinitionNameFilter: "doc",
   636  				},
   637  				{
   638  					OptionalDefinitionNameFilter: "user",
   639  				},
   640  			},
   641  			[]filterCheck{
   642  				func(sf *schemaFilters) bool { return sf.HasNamespaces() },
   643  				func(sf *schemaFilters) bool { return !sf.HasCaveats() },
   644  				func(sf *schemaFilters) bool { return sf.HasNamespace("document") },
   645  				func(sf *schemaFilters) bool { return sf.HasNamespace("user") },
   646  				func(sf *schemaFilters) bool { return !sf.HasNamespace("foo") },
   647  				func(sf *schemaFilters) bool { return sf.HasRelation("document", "viewer") },
   648  				func(sf *schemaFilters) bool { return sf.HasPermission("document", "view") },
   649  				func(sf *schemaFilters) bool { return sf.HasRelation("user", "viewer") },
   650  				func(sf *schemaFilters) bool { return sf.HasPermission("user", "view") },
   651  			},
   652  		},
   653  		{
   654  			"multiple caveat filters",
   655  			[]*v1.ExpSchemaFilter{
   656  				{
   657  					OptionalCaveatNameFilter: "somec",
   658  				},
   659  				{
   660  					OptionalCaveatNameFilter: "somec2",
   661  				},
   662  			},
   663  			[]filterCheck{
   664  				func(sf *schemaFilters) bool { return !sf.HasNamespaces() },
   665  				func(sf *schemaFilters) bool { return sf.HasCaveats() },
   666  				func(sf *schemaFilters) bool { return sf.HasCaveat("somecaveat") },
   667  				func(sf *schemaFilters) bool { return sf.HasCaveat("somecaveat2") },
   668  				func(sf *schemaFilters) bool { return !sf.HasCaveat("foo") },
   669  			},
   670  		},
   671  		{
   672  			"namespace and caveat filters",
   673  			[]*v1.ExpSchemaFilter{
   674  				{
   675  					OptionalDefinitionNameFilter: "doc",
   676  				},
   677  				{
   678  					OptionalCaveatNameFilter: "somec",
   679  				},
   680  			},
   681  			[]filterCheck{
   682  				func(sf *schemaFilters) bool { return sf.HasNamespaces() },
   683  				func(sf *schemaFilters) bool { return sf.HasCaveats() },
   684  				func(sf *schemaFilters) bool { return sf.HasNamespace("document") },
   685  				func(sf *schemaFilters) bool { return sf.HasCaveat("somecaveat") },
   686  				func(sf *schemaFilters) bool { return !sf.HasNamespace("foo") },
   687  				func(sf *schemaFilters) bool { return !sf.HasCaveat("foo") },
   688  			},
   689  		},
   690  		{
   691  			"relation filter",
   692  			[]*v1.ExpSchemaFilter{
   693  				{
   694  					OptionalDefinitionNameFilter: "doc",
   695  					OptionalRelationNameFilter:   "v",
   696  				},
   697  			},
   698  			[]filterCheck{
   699  				func(sf *schemaFilters) bool { return sf.HasNamespaces() },
   700  				func(sf *schemaFilters) bool { return !sf.HasCaveats() },
   701  				func(sf *schemaFilters) bool { return sf.HasNamespace("document") },
   702  				func(sf *schemaFilters) bool { return sf.HasRelation("document", "viewer") },
   703  				func(sf *schemaFilters) bool { return !sf.HasRelation("document", "foo") },
   704  			},
   705  		},
   706  		{
   707  			"permission filter",
   708  			[]*v1.ExpSchemaFilter{
   709  				{
   710  					OptionalDefinitionNameFilter: "doc",
   711  					OptionalPermissionNameFilter: "v",
   712  				},
   713  			},
   714  			[]filterCheck{
   715  				func(sf *schemaFilters) bool { return sf.HasNamespaces() },
   716  				func(sf *schemaFilters) bool { return !sf.HasCaveats() },
   717  				func(sf *schemaFilters) bool { return sf.HasNamespace("document") },
   718  				func(sf *schemaFilters) bool { return sf.HasPermission("document", "view") },
   719  				func(sf *schemaFilters) bool { return !sf.HasPermission("document", "foo") },
   720  			},
   721  		},
   722  		{
   723  			"permission and relation filter",
   724  			[]*v1.ExpSchemaFilter{
   725  				{
   726  					OptionalDefinitionNameFilter: "doc",
   727  					OptionalPermissionNameFilter: "r",
   728  				},
   729  				{
   730  					OptionalDefinitionNameFilter: "doc",
   731  					OptionalRelationNameFilter:   "v",
   732  				},
   733  			},
   734  			[]filterCheck{
   735  				func(sf *schemaFilters) bool { return sf.HasNamespaces() },
   736  				func(sf *schemaFilters) bool { return !sf.HasCaveats() },
   737  				func(sf *schemaFilters) bool { return sf.HasNamespace("document") },
   738  				func(sf *schemaFilters) bool { return sf.HasRelation("document", "viewer") },
   739  				func(sf *schemaFilters) bool { return sf.HasPermission("document", "read") },
   740  				func(sf *schemaFilters) bool { return !sf.HasRelation("document", "foo") },
   741  				func(sf *schemaFilters) bool { return !sf.HasPermission("document", "foo") },
   742  			},
   743  		},
   744  		{
   745  			"permission and relation filter over different definitions",
   746  			[]*v1.ExpSchemaFilter{
   747  				{
   748  					OptionalDefinitionNameFilter: "doc",
   749  					OptionalPermissionNameFilter: "r",
   750  				},
   751  				{
   752  					OptionalDefinitionNameFilter: "user",
   753  					OptionalRelationNameFilter:   "v",
   754  				},
   755  			},
   756  			[]filterCheck{
   757  				func(sf *schemaFilters) bool { return sf.HasNamespaces() },
   758  				func(sf *schemaFilters) bool { return !sf.HasCaveats() },
   759  				func(sf *schemaFilters) bool { return sf.HasNamespace("document") },
   760  				func(sf *schemaFilters) bool { return sf.HasNamespace("user") },
   761  				func(sf *schemaFilters) bool { return sf.HasRelation("user", "viewer") },
   762  				func(sf *schemaFilters) bool { return sf.HasPermission("document", "read") },
   763  				func(sf *schemaFilters) bool { return !sf.HasRelation("document", "viewer") },
   764  				func(sf *schemaFilters) bool { return !sf.HasPermission("user", "read") },
   765  			},
   766  		},
   767  	}
   768  
   769  	for _, tc := range tcs {
   770  		tc := tc
   771  		t.Run(tc.name, func(t *testing.T) {
   772  			sf, err := newSchemaFilters(tc.filters)
   773  			require.NoError(t, err)
   774  
   775  			for index, check := range tc.checkers {
   776  				require.True(t, check(sf), "check failed: #%d", index)
   777  			}
   778  		})
   779  	}
   780  }
   781  
   782  func TestNewSchemaFilters(t *testing.T) {
   783  	tcs := []struct {
   784  		name    string
   785  		filters []*v1.ExpSchemaFilter
   786  		err     string
   787  	}{
   788  		{
   789  			"no filters",
   790  			[]*v1.ExpSchemaFilter{},
   791  			"",
   792  		},
   793  		{
   794  			"namespace filter",
   795  			[]*v1.ExpSchemaFilter{
   796  				{
   797  					OptionalDefinitionNameFilter: "doc",
   798  				},
   799  			},
   800  			"",
   801  		},
   802  		{
   803  			"caveat filter",
   804  			[]*v1.ExpSchemaFilter{
   805  				{
   806  					OptionalCaveatNameFilter: "somec",
   807  				},
   808  			},
   809  			"",
   810  		},
   811  		{
   812  			"relation filter",
   813  			[]*v1.ExpSchemaFilter{
   814  				{
   815  					OptionalDefinitionNameFilter: "doc",
   816  					OptionalRelationNameFilter:   "v",
   817  				},
   818  			},
   819  			"",
   820  		},
   821  		{
   822  			"permission filter",
   823  			[]*v1.ExpSchemaFilter{
   824  				{
   825  					OptionalDefinitionNameFilter: "doc",
   826  					OptionalPermissionNameFilter: "v",
   827  				},
   828  			},
   829  			"",
   830  		},
   831  		{
   832  			"permission and relation filter",
   833  			[]*v1.ExpSchemaFilter{
   834  				{
   835  					OptionalDefinitionNameFilter: "doc",
   836  					OptionalPermissionNameFilter: "r",
   837  				},
   838  				{
   839  					OptionalDefinitionNameFilter: "doc",
   840  					OptionalRelationNameFilter:   "v",
   841  				},
   842  			},
   843  			"",
   844  		},
   845  		{
   846  			"relation filter without definition",
   847  			[]*v1.ExpSchemaFilter{
   848  				{
   849  					OptionalRelationNameFilter: "v",
   850  				},
   851  			},
   852  			"relation name match requires definition name match",
   853  		},
   854  		{
   855  			"permission filter without definition",
   856  			[]*v1.ExpSchemaFilter{
   857  				{
   858  					OptionalPermissionNameFilter: "v",
   859  				},
   860  			},
   861  			"permission name match requires definition name match",
   862  		},
   863  		{
   864  			"filter with both definition and caveat",
   865  			[]*v1.ExpSchemaFilter{
   866  				{
   867  					OptionalDefinitionNameFilter: "doc",
   868  					OptionalCaveatNameFilter:     "somec",
   869  				},
   870  			},
   871  			"cannot filter by both definition and caveat name",
   872  		},
   873  		{
   874  			"filter with both relation and permission",
   875  			[]*v1.ExpSchemaFilter{
   876  				{
   877  					OptionalDefinitionNameFilter: "doc",
   878  					OptionalRelationNameFilter:   "v",
   879  					OptionalPermissionNameFilter: "r",
   880  				},
   881  			},
   882  			"cannot filter by both relation and permission name",
   883  		},
   884  	}
   885  
   886  	for _, tc := range tcs {
   887  		tc := tc
   888  		t.Run(tc.name, func(t *testing.T) {
   889  			_, err := newSchemaFilters(tc.filters)
   890  			if tc.err == "" {
   891  				require.NoError(t, err)
   892  			} else {
   893  				require.ErrorContains(t, err, tc.err)
   894  			}
   895  		})
   896  	}
   897  }