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

     1  package tuple
     2  
     3  import (
     4  	"strings"
     5  	"testing"
     6  
     7  	v1 "github.com/authzed/authzed-go/proto/authzed/api/v1"
     8  	"github.com/stretchr/testify/require"
     9  	"google.golang.org/protobuf/types/known/structpb"
    10  
    11  	core "github.com/authzed/spicedb/pkg/proto/core/v1"
    12  	"github.com/authzed/spicedb/pkg/testutil"
    13  )
    14  
    15  func makeTuple(onr *core.ObjectAndRelation, subject *core.ObjectAndRelation) *core.RelationTuple {
    16  	return &core.RelationTuple{
    17  		ResourceAndRelation: onr,
    18  		Subject:             subject,
    19  	}
    20  }
    21  
    22  func rel(resType, resID, relation, subType, subID, subRel string) *v1.Relationship {
    23  	return &v1.Relationship{
    24  		Resource: &v1.ObjectReference{
    25  			ObjectType: resType,
    26  			ObjectId:   resID,
    27  		},
    28  		Relation: relation,
    29  		Subject: &v1.SubjectReference{
    30  			Object: &v1.ObjectReference{
    31  				ObjectType: subType,
    32  				ObjectId:   subID,
    33  			},
    34  			OptionalRelation: subRel,
    35  		},
    36  	}
    37  }
    38  
    39  func crel(resType, resID, relation, subType, subID, subRel, caveatName string, caveatContext map[string]any) *v1.Relationship {
    40  	context, err := structpb.NewStruct(caveatContext)
    41  	if err != nil {
    42  		panic(err)
    43  	}
    44  
    45  	if len(context.Fields) == 0 {
    46  		context = nil
    47  	}
    48  
    49  	return &v1.Relationship{
    50  		Resource: &v1.ObjectReference{
    51  			ObjectType: resType,
    52  			ObjectId:   resID,
    53  		},
    54  		Relation: relation,
    55  		Subject: &v1.SubjectReference{
    56  			Object: &v1.ObjectReference{
    57  				ObjectType: subType,
    58  				ObjectId:   subID,
    59  			},
    60  			OptionalRelation: subRel,
    61  		},
    62  		OptionalCaveat: &v1.ContextualizedCaveat{
    63  			CaveatName: caveatName,
    64  			Context:    context,
    65  		},
    66  	}
    67  }
    68  
    69  var superLongID = strings.Repeat("f", 1024)
    70  
    71  var testCases = []struct {
    72  	input          string
    73  	expectedOutput string
    74  	tupleFormat    *core.RelationTuple
    75  	relFormat      *v1.Relationship
    76  }{
    77  	{
    78  		input:          "testns:testobj#testrel@user:testusr",
    79  		expectedOutput: "testns:testobj#testrel@user:testusr",
    80  		tupleFormat: makeTuple(
    81  			ObjectAndRelation("testns", "testobj", "testrel"),
    82  			ObjectAndRelation("user", "testusr", "..."),
    83  		),
    84  		relFormat: rel("testns", "testobj", "testrel", "user", "testusr", ""),
    85  	},
    86  	{
    87  		input:          "testns:testobj#testrel@user:testusr#...",
    88  		expectedOutput: "testns:testobj#testrel@user:testusr",
    89  		tupleFormat: makeTuple(
    90  			ObjectAndRelation("testns", "testobj", "testrel"),
    91  			ObjectAndRelation("user", "testusr", "..."),
    92  		),
    93  		relFormat: rel("testns", "testobj", "testrel", "user", "testusr", ""),
    94  	},
    95  	{
    96  		input:          "tenant/testns:testobj#testrel@tenant/user:testusr",
    97  		expectedOutput: "tenant/testns:testobj#testrel@tenant/user:testusr",
    98  		tupleFormat: makeTuple(
    99  			ObjectAndRelation("tenant/testns", "testobj", "testrel"),
   100  			ObjectAndRelation("tenant/user", "testusr", "..."),
   101  		),
   102  		relFormat: rel("tenant/testns", "testobj", "testrel", "tenant/user", "testusr", ""),
   103  	},
   104  	{
   105  		input:          "tenant/testns:testobj#testrel@tenant/user:testusr#...",
   106  		expectedOutput: "tenant/testns:testobj#testrel@tenant/user:testusr",
   107  		tupleFormat: makeTuple(
   108  			ObjectAndRelation("tenant/testns", "testobj", "testrel"),
   109  			ObjectAndRelation("tenant/user", "testusr", "..."),
   110  		),
   111  		relFormat: rel("tenant/testns", "testobj", "testrel", "tenant/user", "testusr", ""),
   112  	},
   113  	{
   114  		input:          "tenant/testns:testobj#testrel@tenant/user:testusr#somerel",
   115  		expectedOutput: "tenant/testns:testobj#testrel@tenant/user:testusr#somerel",
   116  		tupleFormat: makeTuple(
   117  			ObjectAndRelation("tenant/testns", "testobj", "testrel"),
   118  			ObjectAndRelation("tenant/user", "testusr", "somerel"),
   119  		),
   120  		relFormat: rel("tenant/testns", "testobj", "testrel", "tenant/user", "testusr", "somerel"),
   121  	},
   122  	{
   123  		input:          "org/division/team/testns:testobj#testrel@org/division/identity_team/user:testusr#somerel",
   124  		expectedOutput: "org/division/team/testns:testobj#testrel@org/division/identity_team/user:testusr#somerel",
   125  		tupleFormat: makeTuple(
   126  			ObjectAndRelation("org/division/team/testns", "testobj", "testrel"),
   127  			ObjectAndRelation("org/division/identity_team/user", "testusr", "somerel"),
   128  		),
   129  		relFormat: rel("org/division/team/testns", "testobj", "testrel", "org/division/identity_team/user", "testusr", "somerel"),
   130  	},
   131  	{
   132  		input:          "tenant/testns:testobj#testrel@tenant/user:testusr something",
   133  		expectedOutput: "tenant/testns:testobj#testrel@tenant/user:testusr",
   134  		tupleFormat:    nil,
   135  		relFormat:      nil,
   136  	},
   137  	{
   138  		input:          "tenant/testns:testobj#testrel@tenant/user:testusr:",
   139  		expectedOutput: "tenant/testns:testobj#testrel@tenant/user:testusr",
   140  		tupleFormat:    nil,
   141  		relFormat:      nil,
   142  	},
   143  	{
   144  		input:          "tenant/testns:testobj#testrel@tenant/user:testusr#",
   145  		expectedOutput: "tenant/testns:testobj#testrel@tenant/user:testusr",
   146  		tupleFormat:    nil,
   147  		relFormat:      nil,
   148  	},
   149  	{
   150  		input:          "",
   151  		expectedOutput: "",
   152  		tupleFormat:    nil,
   153  		relFormat:      nil,
   154  	},
   155  	{
   156  		input:          "foos:bar#bazzy@groo:grar#...",
   157  		expectedOutput: "foos:bar#bazzy@groo:grar",
   158  		tupleFormat: makeTuple(
   159  			ObjectAndRelation("foos", "bar", "bazzy"),
   160  			ObjectAndRelation("groo", "grar", "..."),
   161  		),
   162  		relFormat: rel("foos", "bar", "bazzy", "groo", "grar", ""),
   163  	},
   164  	{
   165  		input:          "tenant/testns:testobj#testrel@tenant/user:*#...",
   166  		expectedOutput: "tenant/testns:testobj#testrel@tenant/user:*",
   167  		tupleFormat: makeTuple(
   168  			ObjectAndRelation("tenant/testns", "testobj", "testrel"),
   169  			ObjectAndRelation("tenant/user", "*", "..."),
   170  		),
   171  		relFormat: rel("tenant/testns", "testobj", "testrel", "tenant/user", "*", ""),
   172  	},
   173  	{
   174  		input:          "tenant/testns:testobj#testrel@tenant/user:authn|foo",
   175  		expectedOutput: "tenant/testns:testobj#testrel@tenant/user:authn|foo",
   176  		tupleFormat: makeTuple(
   177  			ObjectAndRelation("tenant/testns", "testobj", "testrel"),
   178  			ObjectAndRelation("tenant/user", "authn|foo", "..."),
   179  		),
   180  		relFormat: rel("tenant/testns", "testobj", "testrel", "tenant/user", "authn|foo", ""),
   181  	},
   182  	{
   183  		input:          "document:foo#viewer@user:tom[somecaveat]",
   184  		expectedOutput: "document:foo#viewer@user:tom[somecaveat]",
   185  		tupleFormat: MustWithCaveat(
   186  			makeTuple(
   187  				ObjectAndRelation("document", "foo", "viewer"),
   188  				ObjectAndRelation("user", "tom", "..."),
   189  			),
   190  			"somecaveat",
   191  		),
   192  		relFormat: crel("document", "foo", "viewer", "user", "tom", "", "somecaveat", nil),
   193  	},
   194  	{
   195  		input:          "document:foo#viewer@user:tom[tenant/somecaveat]",
   196  		expectedOutput: "document:foo#viewer@user:tom[tenant/somecaveat]",
   197  		tupleFormat: MustWithCaveat(
   198  			makeTuple(
   199  				ObjectAndRelation("document", "foo", "viewer"),
   200  				ObjectAndRelation("user", "tom", "..."),
   201  			),
   202  			"tenant/somecaveat",
   203  		),
   204  		relFormat: crel("document", "foo", "viewer", "user", "tom", "", "tenant/somecaveat", nil),
   205  	},
   206  	{
   207  		input:          "document:foo#viewer@user:tom[tenant/division/somecaveat]",
   208  		expectedOutput: "document:foo#viewer@user:tom[tenant/division/somecaveat]",
   209  		tupleFormat: MustWithCaveat(
   210  			makeTuple(
   211  				ObjectAndRelation("document", "foo", "viewer"),
   212  				ObjectAndRelation("user", "tom", "..."),
   213  			),
   214  			"tenant/division/somecaveat",
   215  		),
   216  		relFormat: crel("document", "foo", "viewer", "user", "tom", "", "tenant/division/somecaveat", nil),
   217  	},
   218  	{
   219  		input:          "document:foo#viewer@user:tom[somecaveat",
   220  		expectedOutput: "",
   221  		tupleFormat:    nil,
   222  		relFormat:      nil,
   223  	},
   224  	{
   225  		input:          "document:foo#viewer@user:tom[]",
   226  		expectedOutput: "",
   227  		tupleFormat:    nil,
   228  		relFormat:      nil,
   229  	},
   230  	{
   231  		input:          `document:foo#viewer@user:tom[somecaveat:{"hi": "there"}]`,
   232  		expectedOutput: `document:foo#viewer@user:tom[somecaveat:{"hi":"there"}]`,
   233  		tupleFormat: MustWithCaveat(
   234  			makeTuple(
   235  				ObjectAndRelation("document", "foo", "viewer"),
   236  				ObjectAndRelation("user", "tom", "..."),
   237  			),
   238  			"somecaveat",
   239  			map[string]any{
   240  				"hi": "there",
   241  			},
   242  		),
   243  		relFormat: crel("document", "foo", "viewer", "user", "tom", "", "somecaveat", map[string]any{"hi": "there"}),
   244  	},
   245  	{
   246  		input:          `document:foo#viewer@user:tom[somecaveat:{"hi":{"yo": 123}}]`,
   247  		expectedOutput: `document:foo#viewer@user:tom[somecaveat:{"hi":{"yo":123}}]`,
   248  		tupleFormat: MustWithCaveat(
   249  			makeTuple(
   250  				ObjectAndRelation("document", "foo", "viewer"),
   251  				ObjectAndRelation("user", "tom", "..."),
   252  			),
   253  			"somecaveat",
   254  			map[string]any{
   255  				"hi": map[string]any{
   256  					"yo": 123,
   257  				},
   258  			},
   259  		),
   260  		relFormat: crel("document", "foo", "viewer", "user", "tom", "", "somecaveat", map[string]any{
   261  			"hi": map[string]any{
   262  				"yo": 123,
   263  			},
   264  		}),
   265  	},
   266  	{
   267  		input:          `document:foo#viewer@user:tom[somecaveat:{"hi":{"yo":{"hey":true}}}]`,
   268  		expectedOutput: `document:foo#viewer@user:tom[somecaveat:{"hi":{"yo":{"hey":true}}}]`,
   269  		tupleFormat: MustWithCaveat(
   270  			makeTuple(
   271  				ObjectAndRelation("document", "foo", "viewer"),
   272  				ObjectAndRelation("user", "tom", "..."),
   273  			),
   274  			"somecaveat",
   275  			map[string]any{
   276  				"hi": map[string]any{
   277  					"yo": map[string]any{
   278  						"hey": true,
   279  					},
   280  				},
   281  			},
   282  		),
   283  		relFormat: crel("document", "foo", "viewer", "user", "tom", "", "somecaveat", map[string]any{
   284  			"hi": map[string]any{
   285  				"yo": map[string]any{
   286  					"hey": true,
   287  				},
   288  			},
   289  		}),
   290  	},
   291  	{
   292  		input:          `document:foo#viewer@user:tom[somecaveat:{"hi":{"yo":{"hey":[1,2,3]}}}]`,
   293  		expectedOutput: `document:foo#viewer@user:tom[somecaveat:{"hi":{"yo":{"hey":[1,2,3]}}}]`,
   294  		tupleFormat: MustWithCaveat(
   295  			makeTuple(
   296  				ObjectAndRelation("document", "foo", "viewer"),
   297  				ObjectAndRelation("user", "tom", "..."),
   298  			),
   299  			"somecaveat",
   300  			map[string]any{
   301  				"hi": map[string]any{
   302  					"yo": map[string]any{
   303  						"hey": []any{1, 2, 3},
   304  					},
   305  				},
   306  			},
   307  		),
   308  		relFormat: crel("document", "foo", "viewer", "user", "tom", "", "somecaveat", map[string]any{
   309  			"hi": map[string]any{
   310  				"yo": map[string]any{
   311  					"hey": []any{1, 2, 3},
   312  				},
   313  			},
   314  		}),
   315  	},
   316  	{
   317  		input:          `document:foo#viewer@user:tom[somecaveat:{"hi":{"yo":"hey":true}}}]`,
   318  		expectedOutput: "",
   319  		tupleFormat:    nil,
   320  		relFormat:      nil,
   321  	},
   322  	{
   323  		input:          "testns:" + superLongID + "#testrel@user:testusr",
   324  		expectedOutput: "testns:" + superLongID + "#testrel@user:testusr",
   325  		tupleFormat: makeTuple(
   326  			ObjectAndRelation("testns", superLongID, "testrel"),
   327  			ObjectAndRelation("user", "testusr", "..."),
   328  		),
   329  		relFormat: rel("testns", superLongID, "testrel", "user", "testusr", ""),
   330  	},
   331  	{
   332  		input:          "testns:foo#testrel@user:" + superLongID,
   333  		expectedOutput: "testns:foo#testrel@user:" + superLongID,
   334  		tupleFormat: makeTuple(
   335  			ObjectAndRelation("testns", "foo", "testrel"),
   336  			ObjectAndRelation("user", superLongID, "..."),
   337  		),
   338  		relFormat: rel("testns", "foo", "testrel", "user", superLongID, ""),
   339  	},
   340  	{
   341  		input:          "testns:foo#testrel@user:" + superLongID + "more",
   342  		expectedOutput: "",
   343  		tupleFormat:    nil,
   344  		relFormat:      nil,
   345  	},
   346  	{
   347  		input:          "testns:-base64YWZzZGZh-ZHNmZHPwn5iK8J+YivC/fmIrwn5iK==#testrel@user:-base65YWZzZGZh-ZHNmZHPwn5iK8J+YivC/fmIrwn5iK==",
   348  		expectedOutput: "testns:-base64YWZzZGZh-ZHNmZHPwn5iK8J+YivC/fmIrwn5iK==#testrel@user:-base65YWZzZGZh-ZHNmZHPwn5iK8J+YivC/fmIrwn5iK==",
   349  		tupleFormat: makeTuple(
   350  			ObjectAndRelation("testns", "-base64YWZzZGZh-ZHNmZHPwn5iK8J+YivC/fmIrwn5iK==", "testrel"),
   351  			ObjectAndRelation("user", "-base65YWZzZGZh-ZHNmZHPwn5iK8J+YivC/fmIrwn5iK==", "..."),
   352  		),
   353  		relFormat: rel("testns", "-base64YWZzZGZh-ZHNmZHPwn5iK8J+YivC/fmIrwn5iK==", "testrel", "user", "-base65YWZzZGZh-ZHNmZHPwn5iK8J+YivC/fmIrwn5iK==", ""),
   354  	},
   355  	{
   356  		input:          `document:foo#viewer@user:tom[somecaveat:{"hi":"a@example.com"}]`,
   357  		expectedOutput: `document:foo#viewer@user:tom[somecaveat:{"hi":"a@example.com"}]`,
   358  		tupleFormat: MustWithCaveat(
   359  			makeTuple(
   360  				ObjectAndRelation("document", "foo", "viewer"),
   361  				ObjectAndRelation("user", "tom", "..."),
   362  			),
   363  			"somecaveat",
   364  			map[string]any{
   365  				"hi": "a@example.com",
   366  			},
   367  		),
   368  		relFormat: crel("document", "foo", "viewer", "user", "tom", "", "somecaveat", map[string]any{
   369  			"hi": "a@example.com",
   370  		}),
   371  	},
   372  }
   373  
   374  func TestSerialize(t *testing.T) {
   375  	for _, tc := range testCases {
   376  		tc := tc
   377  		t.Run("tuple/"+tc.input, func(t *testing.T) {
   378  			if tc.tupleFormat == nil {
   379  				return
   380  			}
   381  
   382  			serialized := strings.Replace(MustString(tc.tupleFormat), " ", "", -1)
   383  			require.Equal(t, tc.expectedOutput, serialized)
   384  
   385  			withoutCaveat := StringWithoutCaveat(tc.tupleFormat)
   386  			require.Contains(t, tc.expectedOutput, withoutCaveat)
   387  			require.NotContains(t, withoutCaveat, "[")
   388  		})
   389  	}
   390  
   391  	for _, tc := range testCases {
   392  		tc := tc
   393  		t.Run("relationship/"+tc.input, func(t *testing.T) {
   394  			if tc.relFormat == nil {
   395  				return
   396  			}
   397  
   398  			serialized := strings.Replace(MustRelString(tc.relFormat), " ", "", -1)
   399  			require.Equal(t, tc.expectedOutput, serialized)
   400  
   401  			withoutCaveat := StringRelationshipWithoutCaveat(tc.relFormat)
   402  			require.Contains(t, tc.expectedOutput, withoutCaveat)
   403  			require.NotContains(t, withoutCaveat, "[")
   404  		})
   405  	}
   406  }
   407  
   408  func TestParse(t *testing.T) {
   409  	for _, tc := range testCases {
   410  		tc := tc
   411  		t.Run("tuple/"+tc.input, func(t *testing.T) {
   412  			testutil.RequireProtoEqual(t, tc.tupleFormat, Parse(tc.input), "found difference in parsed tuple")
   413  		})
   414  	}
   415  
   416  	for _, tc := range testCases {
   417  		tc := tc
   418  		t.Run("relationship/"+tc.input, func(t *testing.T) {
   419  			testutil.RequireProtoEqual(t, tc.relFormat, ParseRel(tc.input), "found difference in parsed relationship")
   420  		})
   421  	}
   422  }
   423  
   424  func TestConvert(t *testing.T) {
   425  	for _, tc := range testCases {
   426  		tc := tc
   427  		t.Run(tc.input, func(t *testing.T) {
   428  			require := require.New(t)
   429  
   430  			parsed := Parse(tc.input)
   431  			testutil.RequireProtoEqual(t, tc.tupleFormat, parsed, "found difference in parsed tuple")
   432  			if parsed == nil {
   433  				return
   434  			}
   435  
   436  			relationship := ToRelationship(parsed)
   437  			relString := strings.Replace(MustRelString(relationship), " ", "", -1)
   438  			require.Equal(tc.expectedOutput, relString)
   439  
   440  			backToTpl := FromRelationship[*v1.ObjectReference, *v1.SubjectReference, *v1.ContextualizedCaveat](relationship)
   441  			testutil.RequireProtoEqual(t, tc.tupleFormat, backToTpl, "found difference in converted tuple")
   442  
   443  			serialized := strings.Replace(MustString(backToTpl), " ", "", -1)
   444  			require.Equal(tc.expectedOutput, serialized)
   445  		})
   446  	}
   447  }
   448  
   449  func TestValidate(t *testing.T) {
   450  	for _, tc := range testCases {
   451  		tc := tc
   452  		t.Run("validate/"+tc.input, func(t *testing.T) {
   453  			parsed := ParseRel(tc.input)
   454  			if parsed != nil {
   455  				require.NoError(t, ValidateResourceID(parsed.Resource.ObjectId))
   456  				require.NoError(t, ValidateSubjectID(parsed.Subject.Object.ObjectId))
   457  			}
   458  		})
   459  	}
   460  }
   461  
   462  func TestCopyRelationTupleToRelationship(t *testing.T) {
   463  	testCases := []*core.RelationTuple{
   464  		{
   465  			ResourceAndRelation: &core.ObjectAndRelation{
   466  				Namespace: "abc",
   467  				ObjectId:  "def",
   468  				Relation:  "ghi",
   469  			},
   470  			Subject: &core.ObjectAndRelation{
   471  				Namespace: "jkl",
   472  				ObjectId:  "mno",
   473  				Relation:  "pqr",
   474  			},
   475  		},
   476  		{
   477  			ResourceAndRelation: &core.ObjectAndRelation{
   478  				Namespace: "abc",
   479  				ObjectId:  "def",
   480  				Relation:  "ghi",
   481  			},
   482  			Subject: &core.ObjectAndRelation{
   483  				Namespace: "jkl",
   484  				ObjectId:  "mno",
   485  				Relation:  "...",
   486  			},
   487  		},
   488  		{
   489  			ResourceAndRelation: &core.ObjectAndRelation{
   490  				Namespace: "abc",
   491  				ObjectId:  "def",
   492  				Relation:  "ghi",
   493  			},
   494  			Subject: &core.ObjectAndRelation{
   495  				Namespace: "jkl",
   496  				ObjectId:  "mno",
   497  				Relation:  "pqr",
   498  			},
   499  			Caveat: &core.ContextualizedCaveat{
   500  				CaveatName: "stu",
   501  				Context:    &structpb.Struct{},
   502  			},
   503  		},
   504  		{
   505  			ResourceAndRelation: &core.ObjectAndRelation{
   506  				Namespace: "abc",
   507  				ObjectId:  "def",
   508  				Relation:  "ghi",
   509  			},
   510  			Subject: &core.ObjectAndRelation{
   511  				Namespace: "jkl",
   512  				ObjectId:  "mno",
   513  				Relation:  "pqr",
   514  			},
   515  			Caveat: &core.ContextualizedCaveat{
   516  				CaveatName: "stu",
   517  				Context: &structpb.Struct{
   518  					Fields: map[string]*structpb.Value{
   519  						"vwx": {
   520  							Kind: &structpb.Value_StringValue{
   521  								StringValue: "yz",
   522  							},
   523  						},
   524  					},
   525  				},
   526  			},
   527  		},
   528  	}
   529  
   530  	for _, tc := range testCases {
   531  		t.Run(MustString(tc), func(t *testing.T) {
   532  			require := require.New(t)
   533  
   534  			dst := &v1.Relationship{
   535  				Resource: &v1.ObjectReference{},
   536  				Subject: &v1.SubjectReference{
   537  					Object: &v1.ObjectReference{},
   538  				},
   539  			}
   540  			optionalCaveat := &v1.ContextualizedCaveat{}
   541  
   542  			CopyRelationTupleToRelationship(tc, dst, optionalCaveat)
   543  
   544  			expectedSubjectRelation := tc.Subject.Relation
   545  			if tc.Subject.Relation == "..." {
   546  				expectedSubjectRelation = ""
   547  			}
   548  
   549  			require.Equal(tc.ResourceAndRelation.Namespace, dst.Resource.ObjectType)
   550  			require.Equal(tc.ResourceAndRelation.ObjectId, dst.Resource.ObjectId)
   551  			require.Equal(tc.ResourceAndRelation.Relation, dst.Relation)
   552  			require.Equal(tc.Subject.Namespace, dst.Subject.Object.ObjectType)
   553  			require.Equal(tc.Subject.ObjectId, dst.Subject.Object.ObjectId)
   554  			require.Equal(expectedSubjectRelation, dst.Subject.OptionalRelation)
   555  
   556  			if tc.Caveat != nil {
   557  				require.Equal(tc.Caveat.CaveatName, dst.OptionalCaveat.CaveatName)
   558  				require.Equal(tc.Caveat.Context, dst.OptionalCaveat.Context)
   559  			} else {
   560  				require.Nil(dst.OptionalCaveat)
   561  			}
   562  		})
   563  	}
   564  }
   565  
   566  func TestEqual(t *testing.T) {
   567  	equalTestCases := []*core.RelationTuple{
   568  		makeTuple(
   569  			ObjectAndRelation("testns", "testobj", "testrel"),
   570  			ObjectAndRelation("user", "testusr", "..."),
   571  		),
   572  		MustWithCaveat(
   573  			makeTuple(
   574  				ObjectAndRelation("testns", "testobj", "testrel"),
   575  				ObjectAndRelation("user", "testusr", "..."),
   576  			),
   577  			"somecaveat",
   578  			map[string]any{
   579  				"context": map[string]any{
   580  					"deeply": map[string]any{
   581  						"nested": true,
   582  					},
   583  				},
   584  			},
   585  		),
   586  		MustParse("document:foo#viewer@user:tom[somecaveat:{\"hi\":\"there\"}]"),
   587  		MustParse("document:foo#viewer@user:tom[somecaveat:{\"hi\":{\"yo\":123}}]"),
   588  		MustParse("document:foo#viewer@user:tom[somecaveat:{\"hi\":{\"yo\":{\"hey\":true}}, \"hi2\":{\"yo2\":{\"hey2\":false}}}]"),
   589  		MustParse("document:foo#viewer@user:tom[somecaveat:{\"hi\":{\"yo\":{\"hey\":true}}, \"hi2\":{\"yo2\":{\"hey2\":[1,2,3]}}}]"),
   590  	}
   591  
   592  	for _, tc := range equalTestCases {
   593  		t.Run(MustString(tc), func(t *testing.T) {
   594  			require := require.New(t)
   595  			require.True(Equal(tc, tc.CloneVT()))
   596  			require.True(Equal(tc, MustParse(MustString(tc))))
   597  		})
   598  	}
   599  
   600  	notEqualTestCases := []struct {
   601  		name string
   602  		lhs  *core.RelationTuple
   603  		rhs  *core.RelationTuple
   604  	}{
   605  		{
   606  			name: "Mismatch Resource Type",
   607  			lhs: makeTuple(
   608  				ObjectAndRelation("testns1", "testobj", "testrel"),
   609  				ObjectAndRelation("user", "testusr", "..."),
   610  			),
   611  			rhs: makeTuple(
   612  				ObjectAndRelation("testns2", "testobj", "testrel"),
   613  				ObjectAndRelation("user", "testusr", "..."),
   614  			),
   615  		},
   616  		{
   617  			name: "Mismatch Resource ID",
   618  			lhs: makeTuple(
   619  				ObjectAndRelation("testns", "testobj1", "testrel"),
   620  				ObjectAndRelation("user", "testusr", "..."),
   621  			),
   622  			rhs: makeTuple(
   623  				ObjectAndRelation("testns", "testobj2", "testrel"),
   624  				ObjectAndRelation("user", "testusr", "..."),
   625  			),
   626  		},
   627  		{
   628  			name: "Mismatch Resource Relationship",
   629  			lhs: makeTuple(
   630  				ObjectAndRelation("testns", "testobj", "testrel1"),
   631  				ObjectAndRelation("user", "testusr", "..."),
   632  			),
   633  			rhs: makeTuple(
   634  				ObjectAndRelation("testns", "testobj", "testrel2"),
   635  				ObjectAndRelation("user", "testusr", "..."),
   636  			),
   637  		},
   638  		{
   639  			name: "Mismatch Subject Type",
   640  			lhs: makeTuple(
   641  				ObjectAndRelation("testns", "testobj", "testrel"),
   642  				ObjectAndRelation("user1", "testusr", "..."),
   643  			),
   644  			rhs: makeTuple(
   645  				ObjectAndRelation("testns", "testobj", "testrel"),
   646  				ObjectAndRelation("user2", "testusr", "..."),
   647  			),
   648  		},
   649  		{
   650  			name: "Mismatch Subject ID",
   651  			lhs: makeTuple(
   652  				ObjectAndRelation("testns", "testobj", "testrel"),
   653  				ObjectAndRelation("user", "testusr1", "..."),
   654  			),
   655  			rhs: makeTuple(
   656  				ObjectAndRelation("testns", "testobj", "testrel"),
   657  				ObjectAndRelation("user", "testusr2", "..."),
   658  			),
   659  		},
   660  		{
   661  			name: "Mismatch Subject Relationship",
   662  			lhs: makeTuple(
   663  				ObjectAndRelation("testns", "testobj", "testrel"),
   664  				ObjectAndRelation("user", "testusr", "testrel1"),
   665  			),
   666  			rhs: makeTuple(
   667  				ObjectAndRelation("testns", "testobj", "testrel"),
   668  				ObjectAndRelation("user", "testusr", "testrel2"),
   669  			),
   670  		},
   671  		{
   672  			name: "Mismatch Caveat Name",
   673  			lhs: MustWithCaveat(
   674  				makeTuple(
   675  					ObjectAndRelation("testns", "testobj", "testrel"),
   676  					ObjectAndRelation("user", "testusr", "..."),
   677  				),
   678  				"somecaveat1",
   679  				map[string]any{
   680  					"context": map[string]any{
   681  						"deeply": map[string]any{
   682  							"nested": true,
   683  						},
   684  					},
   685  				},
   686  			),
   687  			rhs: MustWithCaveat(
   688  				makeTuple(
   689  					ObjectAndRelation("testns", "testobj", "testrel"),
   690  					ObjectAndRelation("user", "testusr", "..."),
   691  				),
   692  				"somecaveat2",
   693  				map[string]any{
   694  					"context": map[string]any{
   695  						"deeply": map[string]any{
   696  							"nested": true,
   697  						},
   698  					},
   699  				},
   700  			),
   701  		},
   702  		{
   703  			name: "Mismatch Caveat Content",
   704  			lhs: MustWithCaveat(
   705  				makeTuple(
   706  					ObjectAndRelation("testns", "testobj", "testrel"),
   707  					ObjectAndRelation("user", "testusr", "..."),
   708  				),
   709  				"somecaveat",
   710  				map[string]any{
   711  					"context": map[string]any{
   712  						"deeply": map[string]any{
   713  							"nested": "1",
   714  						},
   715  					},
   716  				},
   717  			),
   718  			rhs: MustWithCaveat(
   719  				makeTuple(
   720  					ObjectAndRelation("testns", "testobj", "testrel"),
   721  					ObjectAndRelation("user", "testusr", "..."),
   722  				),
   723  				"somecaveat",
   724  				map[string]any{
   725  					"context": map[string]any{
   726  						"deeply": map[string]any{
   727  							"nested": "2",
   728  						},
   729  					},
   730  				},
   731  			),
   732  		},
   733  		{
   734  			name: "missing caveat context via string",
   735  			lhs:  MustParse("document:foo#viewer@user:tom[somecaveat:{\"hi\":\"there\"}]"),
   736  			rhs:  MustParse("document:foo#viewer@user:tom[somecaveat]"),
   737  		},
   738  		{
   739  			name: "mismatch caveat context via string",
   740  			lhs:  MustParse("document:foo#viewer@user:tom[somecaveat:{\"hi\":\"there\"}]"),
   741  			rhs:  MustParse("document:foo#viewer@user:tom[somecaveat:{\"hi\":\"there2\"}]"),
   742  		},
   743  		{
   744  			name: "mismatch caveat name",
   745  			lhs:  MustParse("document:foo#viewer@user:tom[somecaveat]"),
   746  			rhs:  MustParse("document:foo#viewer@user:tom[somecaveat2]"),
   747  		},
   748  		{
   749  			name: "mismatch caveat context, deeply nested",
   750  			lhs:  MustParse("document:foo#viewer@user:tom[somecaveat:{\"hi\":{\"yo\":123}}]"),
   751  			rhs:  MustParse("document:foo#viewer@user:tom[somecaveat:{\"hi\":{\"yo\":124}}]"),
   752  		},
   753  		{
   754  			name: "mismatch caveat context, deeply nested with array",
   755  			lhs:  MustParse("document:foo#viewer@user:tom[somecaveat:{\"hi\":{\"yo\":[1,2,3]}}]"),
   756  			rhs:  MustParse("document:foo#viewer@user:tom[somecaveat:{\"hi\":{\"yo\":[1,2,4]}}]"),
   757  		},
   758  	}
   759  
   760  	for _, tc := range notEqualTestCases {
   761  		t.Run(tc.name, func(t *testing.T) {
   762  			require := require.New(t)
   763  			require.False(Equal(tc.lhs, tc.rhs))
   764  			require.False(Equal(tc.rhs, tc.lhs))
   765  			require.False(Equal(tc.lhs, MustParse(MustString(tc.rhs))))
   766  			require.False(Equal(tc.rhs, MustParse(MustString(tc.lhs))))
   767  		})
   768  	}
   769  }