github.com/authzed/spicedb@v1.32.1-0.20240520085336-ebda56537386/pkg/development/wasm/operations_test.go (about)

     1  //go:build wasm
     2  // +build wasm
     3  
     4  package main
     5  
     6  import (
     7  	"testing"
     8  
     9  	"github.com/stretchr/testify/require"
    10  	"google.golang.org/protobuf/types/known/structpb"
    11  
    12  	core "github.com/authzed/spicedb/pkg/proto/core/v1"
    13  	devinterface "github.com/authzed/spicedb/pkg/proto/developer/v1"
    14  	"github.com/authzed/spicedb/pkg/testutil"
    15  	"github.com/authzed/spicedb/pkg/tuple"
    16  )
    17  
    18  type editCheckResult struct {
    19  	Relationship  *core.RelationTuple
    20  	IsMember      bool
    21  	Error         *devinterface.DeveloperError
    22  	IsConditional bool
    23  }
    24  
    25  func TestCheckOperation(t *testing.T) {
    26  	type testCase struct {
    27  		name              string
    28  		schema            string
    29  		relationships     []*core.RelationTuple
    30  		checkRelationship *core.RelationTuple
    31  		caveatContext     map[string]any
    32  		expectedError     *devinterface.DeveloperError
    33  		expectedResult    *editCheckResult
    34  	}
    35  
    36  	tests := []testCase{
    37  		{
    38  			"invalid keyword",
    39  			`def foo {
    40  				relation bar:
    41  			}`,
    42  			[]*core.RelationTuple{},
    43  			tuple.MustParse("somenamespace:someobj#anotherrel@user:foo"),
    44  			nil,
    45  			&devinterface.DeveloperError{
    46  				Message: "Unexpected token at root level: TokenTypeIdentifier",
    47  				Kind:    devinterface.DeveloperError_SCHEMA_ISSUE,
    48  				Source:  devinterface.DeveloperError_SCHEMA,
    49  				Line:    1,
    50  				Column:  1,
    51  				Context: "def",
    52  			},
    53  			nil,
    54  		},
    55  		{
    56  			"invalid namespace",
    57  			`definition foo {
    58  				relation bar:
    59  			}`,
    60  			[]*core.RelationTuple{},
    61  			tuple.MustParse("somenamespace:someobj#anotherrel@user:foo"),
    62  			nil,
    63  			&devinterface.DeveloperError{
    64  				Message: "Expected identifier, found token TokenTypeRightBrace",
    65  				Kind:    devinterface.DeveloperError_SCHEMA_ISSUE,
    66  				Source:  devinterface.DeveloperError_SCHEMA,
    67  				Line:    3,
    68  				Column:  4,
    69  				Context: "}",
    70  			},
    71  			nil,
    72  		},
    73  		{
    74  			"invalid namespace name",
    75  			`definition fo {}`,
    76  			[]*core.RelationTuple{},
    77  			tuple.MustParse("somenamespace:someobj#anotherrel@user:foo"),
    78  			nil,
    79  			&devinterface.DeveloperError{
    80  				Message: "error in object definition fo: invalid NamespaceDefinition.Name: value does not match regex pattern \"^([a-z][a-z0-9_]{1,62}[a-z0-9]/)*[a-z][a-z0-9_]{1,62}[a-z0-9]$\"",
    81  				Kind:    devinterface.DeveloperError_SCHEMA_ISSUE,
    82  				Source:  devinterface.DeveloperError_SCHEMA,
    83  				Line:    1,
    84  				Column:  1,
    85  			},
    86  			nil,
    87  		},
    88  		{
    89  			"invalid shared name",
    90  			`definition user {}
    91  			
    92  			definition resource {
    93  				relation writer: user
    94  				 permission writer = writer
    95  			}`,
    96  			[]*core.RelationTuple{},
    97  			tuple.MustParse("somenamespace:someobj#anotherrel@user:foo"),
    98  			nil,
    99  			&devinterface.DeveloperError{
   100  				Message: "found duplicate relation/permission name `writer` under definition `resource`",
   101  				Kind:    devinterface.DeveloperError_SCHEMA_ISSUE,
   102  				Source:  devinterface.DeveloperError_SCHEMA,
   103  				Line:    5,
   104  				Column:  6,
   105  				Context: "writer",
   106  			},
   107  			nil,
   108  		},
   109  		{
   110  			"invalid check",
   111  			`
   112  				definition user {}
   113  				definition somenamespace {
   114  					relation somerel: user
   115  				}
   116  			`,
   117  			[]*core.RelationTuple{
   118  				tuple.MustParse("somenamespace:someobj#somerel@user:foo"),
   119  			},
   120  			tuple.MustParse("somenamespace:someobj#anotherrel@user:foo"),
   121  			nil,
   122  			nil,
   123  			&editCheckResult{
   124  				Relationship: tuple.MustParse("somenamespace:someobj#anotherrel@user:foo"),
   125  				Error: &devinterface.DeveloperError{
   126  					Message: "relation/permission `anotherrel` not found under definition `somenamespace`",
   127  					Kind:    devinterface.DeveloperError_UNKNOWN_RELATION,
   128  					Source:  devinterface.DeveloperError_CHECK_WATCH,
   129  					Context: "somenamespace:someobj#anotherrel@user:foo",
   130  				},
   131  			},
   132  		},
   133  		{
   134  			"valid check",
   135  			`
   136  				definition user {}
   137  				definition somenamespace {
   138  					relation somerel: user
   139  				}
   140  			`,
   141  			[]*core.RelationTuple{
   142  				tuple.MustParse("somenamespace:someobj#somerel@user:foo"),
   143  			},
   144  			tuple.MustParse("somenamespace:someobj#somerel@user:foo"),
   145  			nil,
   146  			nil,
   147  			&editCheckResult{
   148  				Relationship: tuple.MustParse("somenamespace:someobj#somerel@user:foo"),
   149  				IsMember:     true,
   150  			},
   151  		},
   152  		{
   153  			"valid negative check",
   154  			`
   155  				definition user {}
   156  				definition somenamespace {
   157  					relation somerel: user
   158  				}
   159  			`,
   160  			[]*core.RelationTuple{
   161  				tuple.MustParse("somenamespace:someobj#somerel@user:foo"),
   162  			},
   163  			tuple.MustParse("somenamespace:someobj#somerel@user:bar"),
   164  			nil,
   165  			nil,
   166  			&editCheckResult{
   167  				Relationship: tuple.MustParse("somenamespace:someobj#somerel@user:bar"),
   168  				IsMember:     false,
   169  			},
   170  		},
   171  		{
   172  			"valid wildcard check",
   173  			`
   174  				definition user {}
   175  				definition somenamespace {
   176  					relation somerel: user | user:*
   177  				}
   178  			`,
   179  			[]*core.RelationTuple{
   180  				tuple.MustParse("somenamespace:someobj#somerel@user:*"),
   181  			},
   182  			tuple.MustParse("somenamespace:someobj#somerel@user:foo"),
   183  			nil,
   184  			nil,
   185  			&editCheckResult{
   186  				Relationship: tuple.MustParse("somenamespace:someobj#somerel@user:foo"),
   187  				IsMember:     true,
   188  			},
   189  		},
   190  		{
   191  			"valid nil check",
   192  			`
   193  				definition user {}
   194  				definition somenamespace {
   195  					permission empty = nil
   196  				}
   197  			`,
   198  			[]*core.RelationTuple{},
   199  			tuple.MustParse("somenamespace:someobj#empty@user:foo"),
   200  			nil,
   201  			nil,
   202  			&editCheckResult{
   203  				Relationship: tuple.MustParse("somenamespace:someobj#empty@user:foo"),
   204  				IsMember:     false,
   205  			},
   206  		},
   207  		{
   208  			"recursive check",
   209  			`
   210  				definition user {}
   211  				definition document {
   212  					relation viewer: user | document#viewer
   213  				}
   214  			`,
   215  			[]*core.RelationTuple{
   216  				tuple.MustParse("document:someobj#viewer@document:someobj#viewer"),
   217  			},
   218  			tuple.MustParse("document:someobj#viewer@user:foo"),
   219  			nil,
   220  			nil,
   221  			&editCheckResult{
   222  				Relationship: tuple.MustParse("document:someobj#viewer@user:foo"),
   223  				Error: &devinterface.DeveloperError{
   224  					Message: "max depth exceeded: this usually indicates a recursive or too deep data dependency",
   225  					Kind:    devinterface.DeveloperError_MAXIMUM_RECURSION,
   226  					Source:  devinterface.DeveloperError_CHECK_WATCH,
   227  					Context: "document:someobj#viewer@user:foo",
   228  				},
   229  			},
   230  		},
   231  		{
   232  			"valid caveated check to negative",
   233  			`
   234  				caveat somecaveat(somecondition int) {
   235  					somecondition == 42
   236  				}
   237  
   238  				definition user {}
   239  				definition somenamespace {
   240  					relation somerel: user with somecaveat
   241  				}
   242  			`,
   243  			[]*core.RelationTuple{
   244  				tuple.MustParse("somenamespace:someobj#somerel@user:foo[somecaveat]"),
   245  			},
   246  			tuple.MustParse("somenamespace:someobj#somerel@user:foo"),
   247  			map[string]any{"somecondition": 41},
   248  			nil,
   249  			&editCheckResult{
   250  				Relationship: tuple.MustParse("somenamespace:someobj#somerel@user:foo"),
   251  				IsMember:     false,
   252  			},
   253  		},
   254  		{
   255  			"valid caveated check to positive",
   256  			`
   257  				caveat somecaveat(somecondition int) {
   258  					somecondition == 42
   259  				}
   260  
   261  				definition user {}
   262  				definition somenamespace {
   263  					relation somerel: user with somecaveat
   264  				}
   265  			`,
   266  			[]*core.RelationTuple{
   267  				tuple.MustParse("somenamespace:someobj#somerel@user:foo[somecaveat]"),
   268  			},
   269  			tuple.MustParse("somenamespace:someobj#somerel@user:foo"),
   270  			map[string]any{"somecondition": 42},
   271  			nil,
   272  			&editCheckResult{
   273  				Relationship: tuple.MustParse("somenamespace:someobj#somerel@user:foo"),
   274  				IsMember:     true,
   275  			},
   276  		},
   277  		{
   278  			"valid caveated check to conditional",
   279  			`
   280  				caveat somecaveat(somecondition int) {
   281  					somecondition == 42
   282  				}
   283  
   284  				definition user {}
   285  				definition somenamespace {
   286  					relation somerel: user with somecaveat
   287  				}
   288  			`,
   289  			[]*core.RelationTuple{
   290  				tuple.MustParse("somenamespace:someobj#somerel@user:foo[somecaveat]"),
   291  			},
   292  			tuple.MustParse("somenamespace:someobj#somerel@user:foo"),
   293  			map[string]any{"anothercondition": 42},
   294  			nil,
   295  			&editCheckResult{
   296  				Relationship:  tuple.MustParse("somenamespace:someobj#somerel@user:foo"),
   297  				IsMember:      false,
   298  				IsConditional: true,
   299  			},
   300  		},
   301  		{
   302  			"invalid relationship subject type",
   303  			`definition user {}
   304  			
   305  			definition resource {
   306  				relation viewer: user
   307  				 permission view = viewer
   308  			}`,
   309  			[]*core.RelationTuple{tuple.MustParse("resource:someobj#viewer@resource:foo")},
   310  			tuple.MustParse("resource:someobj#view@user:foo"),
   311  			nil,
   312  			&devinterface.DeveloperError{
   313  				Message: "subjects of type `resource` are not allowed on relation `resource#viewer`",
   314  				Kind:    devinterface.DeveloperError_INVALID_SUBJECT_TYPE,
   315  				Source:  devinterface.DeveloperError_RELATIONSHIP,
   316  				Context: "resource:someobj#viewer@resource:foo",
   317  			},
   318  			nil,
   319  		},
   320  		{
   321  			"invalid relationship with caveated subject type",
   322  			`definition user {}
   323  			
   324  			caveat somecaveat(somecondition int) {
   325  				somecondition == 42
   326  			}
   327  
   328  			definition resource {
   329  				relation viewer: user with somecaveat
   330  				 permission view = viewer
   331  			}`,
   332  			[]*core.RelationTuple{tuple.MustParse("resource:someobj#viewer@user:foo")},
   333  			tuple.MustParse("resource:someobj#view@user:foo"),
   334  			nil,
   335  			&devinterface.DeveloperError{
   336  				Message: "subjects of type `user` are not allowed on relation `resource#viewer`",
   337  				Kind:    devinterface.DeveloperError_INVALID_SUBJECT_TYPE,
   338  				Source:  devinterface.DeveloperError_RELATIONSHIP,
   339  				Context: "resource:someobj#viewer@user:foo",
   340  			},
   341  			nil,
   342  		},
   343  		{
   344  			"valid extended ID",
   345  			`
   346  				definition user {}
   347  				definition somenamespace {
   348  					permission empty = nil
   349  				}
   350  			`,
   351  			[]*core.RelationTuple{},
   352  			tuple.MustParse("somenamespace:--=base64YWZzZGZh-ZHNmZHPwn5iK8J+YivC/fmIrwn5iK==#empty@user:veryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryverylong"),
   353  			nil,
   354  			nil,
   355  			&editCheckResult{
   356  				Relationship: tuple.MustParse("somenamespace:--=base64YWZzZGZh-ZHNmZHPwn5iK8J+YivC/fmIrwn5iK==#empty@user:veryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryverylong"),
   357  				IsMember:     false,
   358  			},
   359  		},
   360  	}
   361  
   362  	for _, tc := range tests {
   363  		t.Run(tc.name, func(t *testing.T) {
   364  			var caveatContext *structpb.Struct
   365  			if len(tc.caveatContext) > 0 {
   366  				cc, err := structpb.NewStruct(tc.caveatContext)
   367  				require.NoError(t, err)
   368  				caveatContext = cc
   369  			}
   370  
   371  			response := run(t, &devinterface.DeveloperRequest{
   372  				Context: &devinterface.RequestContext{
   373  					Schema:        tc.schema,
   374  					Relationships: tc.relationships,
   375  				},
   376  				Operations: []*devinterface.Operation{
   377  					{
   378  						CheckParameters: &devinterface.CheckOperationParameters{
   379  							Resource:      tc.checkRelationship.ResourceAndRelation,
   380  							Subject:       tc.checkRelationship.Subject,
   381  							CaveatContext: caveatContext,
   382  						},
   383  					},
   384  				},
   385  			})
   386  
   387  			if tc.expectedError != nil {
   388  				require.NotNil(t, response.GetDeveloperErrors())
   389  				require.Equal(t, 1, len(response.GetDeveloperErrors().InputErrors))
   390  				testutil.RequireProtoEqual(t, tc.expectedError, response.GetDeveloperErrors().InputErrors[0], "found mismatching error")
   391  			} else {
   392  				require.Equal(t, "", response.GetInternalError())
   393  				require.Nil(t, response.GetDeveloperErrors())
   394  
   395  				checkResult := response.GetOperationsResults().Results[0].GetCheckResult()
   396  				require.NotNil(t, checkResult)
   397  
   398  				if tc.expectedResult.Error != nil {
   399  					testutil.RequireProtoEqual(t, tc.expectedResult.Error, checkResult.CheckError, "found mismatching error")
   400  				} else {
   401  					require.Nil(t, checkResult.CheckError)
   402  					require.Equal(t, tc.expectedResult.IsMember, checkResult.Membership == devinterface.CheckOperationsResult_MEMBER)
   403  					require.Equal(t, tc.expectedResult.IsConditional, checkResult.Membership == devinterface.CheckOperationsResult_CAVEATED_MEMBER)
   404  				}
   405  			}
   406  		})
   407  	}
   408  }
   409  
   410  func TestFormatSchemaOperation(t *testing.T) {
   411  	require := require.New(t)
   412  	response := run(t, &devinterface.DeveloperRequest{
   413  		Context: &devinterface.RequestContext{
   414  			Schema: "/** hi there */definition foos {} definition bars{}",
   415  		},
   416  		Operations: []*devinterface.Operation{
   417  			{
   418  				FormatSchemaParameters: &devinterface.FormatSchemaParameters{},
   419  			},
   420  		},
   421  	})
   422  
   423  	formatResult := response.GetOperationsResults().Results[0].GetFormatSchemaResult()
   424  	require.Equal("/** hi there */\ndefinition foos {}\n\ndefinition bars {}", formatResult.FormattedSchema)
   425  }
   426  
   427  func TestRunAssertionsAndValidationOperations(t *testing.T) {
   428  	type testCase struct {
   429  		name                   string
   430  		schema                 string
   431  		relationships          []*core.RelationTuple
   432  		validationYaml         string
   433  		assertionsYaml         string
   434  		expectedError          *devinterface.DeveloperError
   435  		expectCheckTraces      bool
   436  		expectedValidationYaml string
   437  	}
   438  
   439  	tests := []testCase{
   440  		{
   441  			"valid namespace",
   442  			`definition somenamespace {}`,
   443  			[]*core.RelationTuple{},
   444  			"",
   445  			"",
   446  			nil,
   447  			false,
   448  			"{}\n",
   449  		},
   450  		{
   451  			"invalid validation yaml",
   452  			`definition somenamespace {}`,
   453  			[]*core.RelationTuple{},
   454  			`asdkjhgasd`,
   455  			"",
   456  			&devinterface.DeveloperError{
   457  				Message: "unexpected value `asdkjhg`",
   458  				Kind:    devinterface.DeveloperError_PARSE_ERROR,
   459  				Source:  devinterface.DeveloperError_VALIDATION_YAML,
   460  				Context: "asdkjhg",
   461  				Line:    1,
   462  			},
   463  			false,
   464  			"",
   465  		},
   466  		{
   467  			"invalid assertions yaml",
   468  			`definition somenamespace {}`,
   469  			[]*core.RelationTuple{},
   470  			"",
   471  			`asdhasjdkhjasd`,
   472  			&devinterface.DeveloperError{
   473  				Message: "unexpected value `asdhasj`",
   474  				Kind:    devinterface.DeveloperError_PARSE_ERROR,
   475  				Source:  devinterface.DeveloperError_ASSERTION,
   476  				Context: "asdhasj",
   477  				Line:    1,
   478  			},
   479  			false,
   480  			"",
   481  		},
   482  		{
   483  			"assertions yaml with garbage",
   484  			`definition somenamespace {}`,
   485  			[]*core.RelationTuple{},
   486  			"",
   487  			`assertTrue:
   488  - document:firstdoc#view@user:tom
   489  - document:firstdoc#view@user:fred
   490  - document:seconddoc#view@user:tom
   491  assertFalse: garbage
   492  - document:seconddoc#view@user:fred`,
   493  			&devinterface.DeveloperError{
   494  				Message: "did not find expected key",
   495  				Kind:    devinterface.DeveloperError_PARSE_ERROR,
   496  				Source:  devinterface.DeveloperError_ASSERTION,
   497  				Line:    5,
   498  			},
   499  			false,
   500  			"",
   501  		},
   502  		{
   503  			"assertions yaml with indented garbage",
   504  			`definition somenamespace {}`,
   505  			[]*core.RelationTuple{},
   506  			"",
   507  			`assertTrue:
   508    - document:firstdoc#view@user:tom
   509    - document:firstdoc#view@user:fred
   510    - document:seconddoc#view@user:tom
   511  assertFalse: garbage
   512    - document:seconddoc#view@user:fred`,
   513  			&devinterface.DeveloperError{
   514  				Message: "unexpected value `garbage`",
   515  				Kind:    devinterface.DeveloperError_PARSE_ERROR,
   516  				Source:  devinterface.DeveloperError_ASSERTION,
   517  				Line:    5,
   518  				Column:  0,
   519  				Context: "garbage",
   520  			},
   521  			false,
   522  			"",
   523  		},
   524  		{
   525  			"invalid assertions true yaml",
   526  			`definition somenamespace {}`,
   527  			[]*core.RelationTuple{},
   528  			"",
   529  			`assertTrue:
   530  - something`,
   531  			&devinterface.DeveloperError{
   532  				Message: "error parsing relationship in assertion `something`",
   533  				Kind:    devinterface.DeveloperError_PARSE_ERROR,
   534  				Source:  devinterface.DeveloperError_ASSERTION,
   535  				Line:    2,
   536  				Column:  3,
   537  				Context: "something",
   538  			},
   539  			false,
   540  			"",
   541  		},
   542  		{
   543  			"assertion true failure",
   544  			`
   545  				definition user {}
   546  				definition document {
   547  					relation viewer: user
   548  				}
   549  			`,
   550  			[]*core.RelationTuple{tuple.MustParse("document:somedoc#viewer@user:jimmy")},
   551  			"",
   552  			`assertTrue:
   553  - document:somedoc#viewer@user:jake`,
   554  			&devinterface.DeveloperError{
   555  				Message: "Expected relation or permission document:somedoc#viewer@user:jake to exist",
   556  				Kind:    devinterface.DeveloperError_ASSERTION_FAILED,
   557  				Source:  devinterface.DeveloperError_ASSERTION,
   558  				Context: "document:somedoc#viewer@user:jake",
   559  				Line:    2,
   560  				Column:  3,
   561  			},
   562  			true,
   563  			"{}\n",
   564  		},
   565  		{
   566  			"assertion false failure",
   567  			`
   568  				definition user {}
   569  				definition document {
   570  					relation viewer: user
   571  				}
   572  			`,
   573  			[]*core.RelationTuple{tuple.MustParse("document:somedoc#viewer@user:jimmy")},
   574  			"",
   575  			`assertFalse:
   576  - document:somedoc#viewer@user:jimmy`,
   577  			&devinterface.DeveloperError{
   578  				Message: "Expected relation or permission document:somedoc#viewer@user:jimmy to not exist",
   579  				Kind:    devinterface.DeveloperError_ASSERTION_FAILED,
   580  				Source:  devinterface.DeveloperError_ASSERTION,
   581  				Context: "document:somedoc#viewer@user:jimmy",
   582  				Line:    2,
   583  				Column:  3,
   584  			},
   585  			true,
   586  			"{}\n",
   587  		},
   588  		{
   589  			"assertion invalid caveated relation",
   590  			`
   591  				definition user {}
   592  				definition document {}
   593  			`,
   594  			[]*core.RelationTuple{},
   595  			"",
   596  			`assertFalse:
   597  - document:somedoc#viewer@user:jimmy[somecaveat]`,
   598  			&devinterface.DeveloperError{
   599  				Message: "cannot specify a caveat on an assertion: `document:somedoc#viewer@user:jimmy[somecaveat]`",
   600  				Kind:    devinterface.DeveloperError_UNKNOWN_RELATION,
   601  				Source:  devinterface.DeveloperError_ASSERTION,
   602  				Context: "document:somedoc#viewer@user:jimmy[somecaveat]",
   603  				Line:    2,
   604  				Column:  3,
   605  			},
   606  			false,
   607  			"{}\n",
   608  		},
   609  		{
   610  			"assertion invalid relation",
   611  			`
   612  				definition user {}
   613  				definition document {}
   614  			`,
   615  			[]*core.RelationTuple{},
   616  			"",
   617  			`assertFalse:
   618  - document:somedoc#viewer@user:jimmy`,
   619  			&devinterface.DeveloperError{
   620  				Message: "relation/permission `viewer` not found under definition `document`",
   621  				Kind:    devinterface.DeveloperError_UNKNOWN_RELATION,
   622  				Source:  devinterface.DeveloperError_ASSERTION,
   623  				Context: "document:somedoc#viewer@user:jimmy",
   624  				Line:    2,
   625  				Column:  3,
   626  			},
   627  			false,
   628  			"{}\n",
   629  		},
   630  		{
   631  			"missing subject",
   632  			`
   633  			definition user {}
   634  			definition document {
   635  				relation writer: user
   636  				relation viewer: user
   637  				permission view = viewer + writer
   638  			}
   639  			`,
   640  			[]*core.RelationTuple{tuple.MustParse("document:somedoc#writer@user:jimmy")},
   641  			`"document:somedoc#view":`,
   642  			`assertTrue:
   643  - document:somedoc#view@user:jimmy`,
   644  			&devinterface.DeveloperError{
   645  				Message: "For object and permission/relation `document:somedoc#view`, subject `user:jimmy` found but missing from specified",
   646  				Kind:    devinterface.DeveloperError_EXTRA_RELATIONSHIP_FOUND,
   647  				Source:  devinterface.DeveloperError_VALIDATION_YAML,
   648  				Context: "document:somedoc#view",
   649  				Line:    1,
   650  				Column:  1,
   651  			},
   652  			false,
   653  			`document:somedoc#view:
   654  - '[user:jimmy] is <document:somedoc#writer>'
   655  `,
   656  		},
   657  		{
   658  			"extra subject",
   659  			`
   660  			definition user {}
   661  			definition document {
   662  				relation writer: user
   663  				relation viewer: user
   664  				permission view = viewer + writer
   665  			}
   666  			`,
   667  			[]*core.RelationTuple{tuple.MustParse("document:somedoc#writer@user:jimmy")},
   668  			`"document:somedoc#view":
   669  - "[user:jimmy] is <document:somedoc#writer>"
   670  - "[user:jake] is <document:somedoc#viewer>"`,
   671  			`assertTrue:
   672  - document:somedoc#view@user:jimmy`,
   673  			&devinterface.DeveloperError{
   674  				Message: "For object and permission/relation `document:somedoc#view`, missing expected subject `user:jake`",
   675  				Kind:    devinterface.DeveloperError_MISSING_EXPECTED_RELATIONSHIP,
   676  				Source:  devinterface.DeveloperError_VALIDATION_YAML,
   677  				Context: "[user:jake] is <document:somedoc#viewer>",
   678  				Line:    3,
   679  				Column:  3,
   680  			},
   681  			false,
   682  			`document:somedoc#view:
   683  - '[user:jimmy] is <document:somedoc#writer>'
   684  `,
   685  		},
   686  		{
   687  			"parse error in validation",
   688  			`
   689  			definition user {}
   690  			definition document {
   691  				relation writer: user
   692  				relation viewer: user
   693  				permission view = viewer + writer
   694  			}
   695  			`,
   696  			[]*core.RelationTuple{tuple.MustParse("document:somedoc#writer@user:jimmy")},
   697  			`"document:somedoc#view":
   698  - "[user] is <document:somedoc#writer>"`,
   699  			`assertTrue:
   700  - document:somedoc#view@user:jimmy`,
   701  			&devinterface.DeveloperError{
   702  				Message: "invalid subject: `user`",
   703  				Kind:    devinterface.DeveloperError_PARSE_ERROR,
   704  				Source:  devinterface.DeveloperError_VALIDATION_YAML,
   705  				Context: "user",
   706  				Line:    2,
   707  				Column:  3,
   708  			},
   709  			false,
   710  			``,
   711  		},
   712  		{
   713  			"parse error in validation relationships",
   714  			`
   715  			definition user {}
   716  			definition document {
   717  				relation writer: user
   718  				relation viewer: user
   719  				permission view = viewer + writer
   720  			}
   721  			`,
   722  			[]*core.RelationTuple{tuple.MustParse("document:somedoc#writer@user:jimmy")},
   723  			`"document:somedoc#view":
   724  - "[user:jimmy] is <document:som>"`,
   725  			`assertTrue:
   726  - document:somedoc#view@user:jimmy`,
   727  			&devinterface.DeveloperError{
   728  				Message: "invalid resource and relation: `document:som`",
   729  				Kind:    devinterface.DeveloperError_PARSE_ERROR,
   730  				Source:  devinterface.DeveloperError_VALIDATION_YAML,
   731  				Context: "document:som",
   732  				Line:    2,
   733  				Column:  3,
   734  			},
   735  			false,
   736  			``,
   737  		},
   738  		{
   739  			"different relations",
   740  			`
   741  			definition user {}
   742  			definition document {
   743  				relation writer: user
   744  				relation viewer: user
   745  				permission view = viewer + writer
   746  			}
   747  			`,
   748  			[]*core.RelationTuple{tuple.MustParse("document:somedoc#writer@user:jimmy")},
   749  			`"document:somedoc#view":
   750  - "[user:jimmy] is <document:somedoc#viewer>"`,
   751  			`assertTrue:
   752  - document:somedoc#view@user:jimmy`,
   753  			&devinterface.DeveloperError{
   754  				Message: "For object and permission/relation `document:somedoc#view`, found different relationships for subject `user:jimmy`: Specified: `<document:somedoc#viewer>`, Computed: `<document:somedoc#writer>`",
   755  				Kind:    devinterface.DeveloperError_MISSING_EXPECTED_RELATIONSHIP,
   756  				Source:  devinterface.DeveloperError_VALIDATION_YAML,
   757  				Context: `[user:jimmy] is <document:somedoc#viewer>`,
   758  				Line:    2,
   759  				Column:  3,
   760  			},
   761  			false,
   762  			`document:somedoc#view:
   763  - '[user:jimmy] is <document:somedoc#writer>'
   764  `,
   765  		},
   766  		{
   767  			"full valid",
   768  			`
   769  			definition user {}
   770  
   771  			caveat testcaveat(somecondition int) {
   772  				somecondition == 42
   773  			}
   774  
   775  			definition document {
   776  				relation writer: user
   777  				relation viewer: user | user with testcaveat
   778  				permission view = viewer + writer
   779  			}
   780  			`,
   781  			[]*core.RelationTuple{
   782  				tuple.MustParse("document:somedoc#writer@user:jimmy"),
   783  				tuple.MustParse("document:somedoc#viewer@user:jake"),
   784  				tuple.MustParse("document:somedoc#viewer@user:sarah[testcaveat]"),
   785  				tuple.MustParse(`document:somedoc#viewer@user:tom[testcaveat:{"somecondition": 42}]`),
   786  				tuple.MustParse(`document:somedoc#viewer@user:fred[testcaveat:{"somecondition": 53}]`),
   787  			},
   788  			`"document:somedoc#view":
   789  - '[user:fred[...]] is <document:somedoc#viewer>'
   790  - '[user:jake] is <document:somedoc#viewer>'
   791  - '[user:jimmy] is <document:somedoc#writer>'
   792  - '[user:sarah[...]] is <document:somedoc#viewer>'
   793  - '[user:tom[...]] is <document:somedoc#viewer>'
   794  `,
   795  			`assertTrue:
   796  - document:somedoc#writer@user:jimmy
   797  - document:somedoc#view@user:jimmy
   798  - document:somedoc#viewer@user:jake
   799  - document:somedoc#viewer@user:tom
   800  - 'document:somedoc#viewer@user:sarah with {"somecondition": "42"}'
   801  assertCaveated:
   802  - document:somedoc#viewer@user:sarah
   803  assertFalse:
   804  - document:somedoc#writer@user:sarah
   805  - document:somedoc#writer@user:jake
   806  - document:somedoc#viewer@user:fred
   807  - 'document:somedoc#viewer@user:sarah with {"somecondition": "45"}'
   808  `,
   809  			nil,
   810  			false,
   811  			`document:somedoc#view:
   812  - '[user:fred[...]] is <document:somedoc#viewer>'
   813  - '[user:jake] is <document:somedoc#viewer>'
   814  - '[user:jimmy] is <document:somedoc#writer>'
   815  - '[user:sarah[...]] is <document:somedoc#viewer>'
   816  - '[user:tom[...]] is <document:somedoc#viewer>'
   817  `,
   818  		},
   819  		{
   820  			"multipath",
   821  			`
   822  			definition user {}
   823  			definition document {
   824  				relation writer: user
   825  				relation viewer: user
   826  				permission view = viewer + writer
   827  			}
   828  			`,
   829  			[]*core.RelationTuple{
   830  				tuple.MustParse("document:somedoc#writer@user:jimmy"),
   831  				tuple.MustParse("document:somedoc#viewer@user:jimmy"),
   832  			},
   833  			`"document:somedoc#view":
   834  - "[user:jimmy] is <document:somedoc#writer>/<document:somedoc#viewer>"`,
   835  			`assertTrue:
   836  - document:somedoc#writer@user:jimmy
   837  `,
   838  			nil,
   839  			false,
   840  			`document:somedoc#view:
   841  - '[user:jimmy] is <document:somedoc#viewer>/<document:somedoc#writer>'
   842  `,
   843  		},
   844  		{
   845  			"multipath missing relationship",
   846  			`
   847  			definition user {}
   848  			definition document {
   849  				relation writer: user
   850  				relation viewer: user
   851  				permission view = viewer + writer
   852  			}
   853  			`,
   854  			[]*core.RelationTuple{
   855  				tuple.MustParse("document:somedoc#writer@user:jimmy"),
   856  				tuple.MustParse("document:somedoc#viewer@user:jimmy"),
   857  			},
   858  			`"document:somedoc#view":
   859  - "[user:jimmy] is <document:somedoc#writer>"`,
   860  			`assertTrue:
   861  - document:somedoc#writer@user:jimmy
   862  `,
   863  			&devinterface.DeveloperError{
   864  				Message: "For object and permission/relation `document:somedoc#view`, found different relationships for subject `user:jimmy`: Specified: `<document:somedoc#writer>`, Computed: `<document:somedoc#viewer>/<document:somedoc#writer>`",
   865  				Kind:    devinterface.DeveloperError_MISSING_EXPECTED_RELATIONSHIP,
   866  				Source:  devinterface.DeveloperError_VALIDATION_YAML,
   867  				Context: `[user:jimmy] is <document:somedoc#writer>`,
   868  				Line:    2,
   869  				Column:  3,
   870  			},
   871  			false,
   872  			`document:somedoc#view:
   873  - '[user:jimmy] is <document:somedoc#viewer>/<document:somedoc#writer>'
   874  `,
   875  		},
   876  		{
   877  			"invalid namespace on tuple",
   878  			`
   879  			definition user {}
   880  			`,
   881  			[]*core.RelationTuple{tuple.MustParse("document:somedoc#writer@user:jimmy")},
   882  			``,
   883  			``,
   884  			&devinterface.DeveloperError{
   885  				Message: "object definition `document` not found",
   886  				Kind:    devinterface.DeveloperError_UNKNOWN_OBJECT_TYPE,
   887  				Source:  devinterface.DeveloperError_RELATIONSHIP,
   888  				Context: `document:somedoc#writer@user:jimmy`,
   889  			},
   890  			false,
   891  			``,
   892  		},
   893  		{
   894  			"invalid relation on tuple",
   895  			`
   896  			definition user {}
   897  			definition document {}
   898  			`,
   899  			[]*core.RelationTuple{tuple.MustParse("document:somedoc#writer@user:jimmy")},
   900  			``,
   901  			``,
   902  			&devinterface.DeveloperError{
   903  				Message: "relation/permission `writer` not found under definition `document`",
   904  				Kind:    devinterface.DeveloperError_UNKNOWN_RELATION,
   905  				Source:  devinterface.DeveloperError_RELATIONSHIP,
   906  				Context: `document:somedoc#writer@user:jimmy`,
   907  			},
   908  			false,
   909  			``,
   910  		},
   911  		{
   912  			"wildcard relationship",
   913  			`
   914  		   			definition user {}
   915  		   			definition document {
   916  		   				relation writer: user
   917  		   				relation viewer: user | user:*
   918  		   				permission view = viewer + writer
   919  		   			}
   920  		   			`,
   921  			[]*core.RelationTuple{
   922  				tuple.MustParse("document:somedoc#writer@user:jimmy"),
   923  				tuple.MustParse("document:somedoc#viewer@user:*"),
   924  			},
   925  			`"document:somedoc#view":
   926  - "[user:*] is <document:somedoc#viewer>"
   927  - "[user:jimmy] is <document:somedoc#viewer>/<document:somedoc#writer>"`,
   928  			`assertTrue:
   929  - document:somedoc#writer@user:jimmy
   930  - document:somedoc#viewer@user:jimmy
   931  - document:somedoc#viewer@user:somegal
   932  assertFalse:
   933  - document:somedoc#writer@user:somegal`,
   934  			nil,
   935  			false,
   936  			`document:somedoc#view:
   937  - '[user:*] is <document:somedoc#viewer>'
   938  - '[user:jimmy] is <document:somedoc#writer>'
   939  `,
   940  		},
   941  		{
   942  			"wildcard exclusion",
   943  			`
   944  		   			definition user {}
   945  		   			definition document {
   946  		   				relation banned: user
   947  		   				relation viewer: user | user:*
   948  		   				permission view = viewer - banned
   949  		   			}
   950  		   			`,
   951  			[]*core.RelationTuple{
   952  				tuple.MustParse("document:somedoc#banned@user:jimmy"),
   953  				tuple.MustParse("document:somedoc#viewer@user:*"),
   954  			},
   955  			`"document:somedoc#view":
   956  - "[user:* - {user:jimmy}] is <document:somedoc#viewer>"`,
   957  			`assertTrue:
   958  - document:somedoc#view@user:somegal
   959  assertFalse:
   960  - document:somedoc#view@user:jimmy`,
   961  			nil,
   962  			false,
   963  			`document:somedoc#view:
   964  - '[user:* - {user:jimmy}] is <document:somedoc#viewer>'
   965  `,
   966  		},
   967  		{
   968  			"wildcard exclusion under intersection",
   969  			`
   970  		   			definition user {}
   971  		   			definition document {
   972  		   				relation banned: user
   973  		   				relation viewer: user | user:*
   974  		   				relation other: user
   975  		   				permission view = (viewer - banned) & (viewer - other)
   976  		   			}
   977  		   			`,
   978  			[]*core.RelationTuple{
   979  				tuple.MustParse("document:somedoc#other@user:sarah"),
   980  				tuple.MustParse("document:somedoc#banned@user:jimmy"),
   981  				tuple.MustParse("document:somedoc#viewer@user:*"),
   982  			},
   983  			`"document:somedoc#view":
   984  - "[user:* - {user:jimmy}] is <document:somedoc#viewer>"`,
   985  			`assertTrue:
   986  - document:somedoc#view@user:somegal
   987  assertFalse:
   988  - document:somedoc#view@user:jimmy
   989  - document:somedoc#view@user:sarah`,
   990  			nil,
   991  			false,
   992  			`document:somedoc#view:
   993  - '[user:* - {user:jimmy, user:sarah}] is <document:somedoc#viewer>'
   994  `,
   995  		},
   996  		{
   997  			"nil handling",
   998  			`
   999  		   			definition user {}
  1000  		   			definition document {
  1001  		   				relation viewer: user
  1002  		   				permission view = viewer
  1003  						permission empty = nil
  1004  		   			}
  1005  		   			`,
  1006  			[]*core.RelationTuple{
  1007  				tuple.MustParse("document:somedoc#viewer@user:jill"),
  1008  				tuple.MustParse("document:somedoc#viewer@user:tom"),
  1009  			},
  1010  			`"document:somedoc#view":
  1011  - "[user:jill] is <document:somedoc#viewer>"
  1012  - "[user:tom] is <document:somedoc#viewer>"
  1013  "document:somedoc#empty": []`,
  1014  			`assertTrue:
  1015  - document:somedoc#view@user:jill
  1016  - document:somedoc#view@user:tom
  1017  assertFalse:
  1018  - document:somedoc#empty@user:jill
  1019  - document:somedoc#empty@user:tom`,
  1020  			nil,
  1021  			false,
  1022  			"document:somedoc#empty: []\ndocument:somedoc#view:\n- '[user:jill] is <document:somedoc#viewer>'\n- '[user:tom] is <document:somedoc#viewer>'\n",
  1023  		},
  1024  		{
  1025  			"no expected subject or relation",
  1026  			`
  1027  		   			definition user {}
  1028  		   			definition document {
  1029  		   				relation viewer: user
  1030  		   				permission view = viewer
  1031  		   			}
  1032  		   			`,
  1033  			[]*core.RelationTuple{
  1034  				tuple.MustParse("document:somedoc#viewer@user:jill"),
  1035  				tuple.MustParse("document:somedoc#viewer@user:tom"),
  1036  			},
  1037  			`"document:somedoc#view":
  1038  - "is <document:somedoc#viewer>"
  1039  - "[user:tom] is "`,
  1040  			`assertTrue:
  1041  - document:somedoc#view@user:jill
  1042  - document:somedoc#view@user:tom`,
  1043  			&devinterface.DeveloperError{
  1044  				Message: "For object and permission/relation `document:somedoc#view`, no expected subject specified in `is <document:somedoc#viewer>`",
  1045  				Kind:    devinterface.DeveloperError_MISSING_EXPECTED_RELATIONSHIP,
  1046  				Source:  devinterface.DeveloperError_VALIDATION_YAML,
  1047  				Context: `is <document:somedoc#viewer>`,
  1048  				Line:    2,
  1049  				Column:  3,
  1050  			},
  1051  			false,
  1052  			"document:somedoc#view:\n- '[user:jill] is <document:somedoc#viewer>'\n- '[user:tom] is <document:somedoc#viewer>'\n",
  1053  		},
  1054  
  1055  		{
  1056  			"expected relations containing uncaveated subject that should be caveated",
  1057  			`
  1058  			definition user {}
  1059  
  1060  			caveat testcaveat(somecondition int) {
  1061  				somecondition == 42
  1062  			}
  1063  
  1064  			definition document {
  1065  				relation viewer: user with testcaveat
  1066  				permission view = viewer
  1067  			}
  1068  			`,
  1069  			[]*core.RelationTuple{
  1070  				tuple.MustParse("document:somedoc#viewer@user:sarah[testcaveat]"),
  1071  			},
  1072  			`"document:somedoc#view":
  1073  - '[user:sarah] is <document:somedoc#viewer>'
  1074  `,
  1075  			`assertTrue:
  1076  - 'document:somedoc#viewer@user:sarah with {"somecondition": "42"}'
  1077  assertCaveated:
  1078  - document:somedoc#viewer@user:sarah
  1079  assertFalse:
  1080  - 'document:somedoc#viewer@user:sarah with {"somecondition": "45"}'
  1081  `,
  1082  			&devinterface.DeveloperError{
  1083  				Message: "For object and permission/relation `document:somedoc#view`, found caveat mismatch",
  1084  				Line:    2,
  1085  				Column:  3,
  1086  				Source:  devinterface.DeveloperError_VALIDATION_YAML,
  1087  				Kind:    devinterface.DeveloperError_MISSING_EXPECTED_RELATIONSHIP,
  1088  				Context: "[user:sarah] is <document:somedoc#viewer>",
  1089  			},
  1090  			false,
  1091  			`document:somedoc#view:
  1092  - '[user:sarah[...]] is <document:somedoc#viewer>'
  1093  `,
  1094  		},
  1095  	}
  1096  
  1097  	for _, tc := range tests {
  1098  		t.Run(tc.name, func(t *testing.T) {
  1099  			require := require.New(t)
  1100  
  1101  			response := run(t, &devinterface.DeveloperRequest{
  1102  				Context: &devinterface.RequestContext{
  1103  					Schema:        tc.schema,
  1104  					Relationships: tc.relationships,
  1105  				},
  1106  				Operations: []*devinterface.Operation{
  1107  					{
  1108  						AssertionsParameters: &devinterface.RunAssertionsParameters{
  1109  							AssertionsYaml: tc.assertionsYaml,
  1110  						},
  1111  					},
  1112  					{
  1113  						ValidationParameters: &devinterface.RunValidationParameters{
  1114  							ValidationYaml: tc.validationYaml,
  1115  						},
  1116  					},
  1117  				},
  1118  			})
  1119  
  1120  			if tc.expectedError != nil {
  1121  				errors := []*devinterface.DeveloperError{}
  1122  
  1123  				if response.GetDeveloperErrors() != nil {
  1124  					errors = append(errors, response.GetDeveloperErrors().InputErrors...)
  1125  				} else {
  1126  					require.NotNil(response.GetOperationsResults(), "found nil results: %v", response)
  1127  					require.GreaterOrEqual(len(response.GetOperationsResults().Results), 2, "found insufficient results")
  1128  
  1129  					if response.GetOperationsResults().Results[0].GetAssertionsResult().InputError != nil {
  1130  						errors = append(errors, response.GetOperationsResults().Results[0].GetAssertionsResult().InputError)
  1131  					}
  1132  
  1133  					if response.GetOperationsResults().Results[1].GetValidationResult().InputError != nil {
  1134  						errors = append(errors, response.GetOperationsResults().Results[1].GetValidationResult().InputError)
  1135  					}
  1136  
  1137  					if len(response.GetOperationsResults().Results[0].GetAssertionsResult().ValidationErrors) > 0 {
  1138  						errors = append(errors, response.GetOperationsResults().Results[0].GetAssertionsResult().ValidationErrors...)
  1139  					}
  1140  
  1141  					if len(response.GetOperationsResults().Results[1].GetValidationResult().ValidationErrors) > 0 {
  1142  						errors = append(errors, response.GetOperationsResults().Results[1].GetValidationResult().ValidationErrors...)
  1143  					}
  1144  				}
  1145  
  1146  				if tc.expectCheckTraces {
  1147  					require.NotNil(t, errors[0].CheckDebugInformation)
  1148  					require.NotNil(t, errors[0].CheckResolvedDebugInformation)
  1149  
  1150  					// Unset these values to avoid the need to specify above
  1151  					// in the test data. This is necessary because the debug
  1152  					// information contains the revision timestamp, which changes
  1153  					// on every call.
  1154  					errors[0].CheckDebugInformation = nil
  1155  					errors[0].CheckResolvedDebugInformation = nil
  1156  				}
  1157  
  1158  				testutil.RequireProtoEqual(t, tc.expectedError, errors[0], "mismatch on errors")
  1159  			} else {
  1160  				require.Equal(0, len(response.GetOperationsResults().Results[0].GetAssertionsResult().ValidationErrors), "Failed assertion", response.GetOperationsResults().Results[0].GetAssertionsResult().ValidationErrors)
  1161  			}
  1162  
  1163  			if tc.expectedValidationYaml != "" && response.GetOperationsResults() != nil {
  1164  				require.Equal(tc.expectedValidationYaml, response.GetOperationsResults().Results[1].GetValidationResult().UpdatedValidationYaml)
  1165  			}
  1166  		})
  1167  	}
  1168  }