github.com/authzed/spicedb@v1.32.1-0.20240520085336-ebda56537386/internal/dispatch/keys/computed_test.go (about)

     1  package keys
     2  
     3  import (
     4  	"encoding/hex"
     5  	"fmt"
     6  	"strings"
     7  	"testing"
     8  
     9  	"github.com/stretchr/testify/require"
    10  	"google.golang.org/protobuf/types/known/structpb"
    11  
    12  	"github.com/authzed/spicedb/pkg/genutil/mapz"
    13  	core "github.com/authzed/spicedb/pkg/proto/core/v1"
    14  	v1 "github.com/authzed/spicedb/pkg/proto/dispatch/v1"
    15  	"github.com/authzed/spicedb/pkg/tuple"
    16  )
    17  
    18  func TestKeyPrefixOverlap(t *testing.T) {
    19  	encountered := map[string]struct{}{}
    20  	for _, prefix := range cachePrefixes {
    21  		_, ok := encountered[string(prefix)]
    22  		require.False(t, ok)
    23  		encountered[string(prefix)] = struct{}{}
    24  	}
    25  }
    26  
    27  var (
    28  	ONR = tuple.ObjectAndRelation
    29  	RR  = tuple.RelationReference
    30  )
    31  
    32  func TestStableCacheKeys(t *testing.T) {
    33  	tcs := []struct {
    34  		name      string
    35  		createKey func() DispatchCacheKey
    36  		expected  string
    37  	}{
    38  		{
    39  			"basic check",
    40  			func() DispatchCacheKey {
    41  				return checkRequestToKey(&v1.DispatchCheckRequest{
    42  					ResourceRelation: RR("document", "view"),
    43  					ResourceIds:      []string{"foo", "bar"},
    44  					Subject:          ONR("user", "tom", "..."),
    45  					Metadata: &v1.ResolverMeta{
    46  						AtRevision: "1234",
    47  					},
    48  				}, computeBothHashes)
    49  			},
    50  			"e09cbca18290f7afae01",
    51  		},
    52  		{
    53  			"basic check with canonical ordering",
    54  			func() DispatchCacheKey {
    55  				return checkRequestToKey(&v1.DispatchCheckRequest{
    56  					ResourceRelation: RR("document", "view"),
    57  					ResourceIds:      []string{"bar", "foo"},
    58  					Subject:          ONR("user", "tom", "..."),
    59  					Metadata: &v1.ResolverMeta{
    60  						AtRevision: "1234",
    61  					},
    62  				}, computeBothHashes)
    63  			},
    64  			"e09cbca18290f7afae01",
    65  		},
    66  		{
    67  			"different check",
    68  			func() DispatchCacheKey {
    69  				return checkRequestToKey(&v1.DispatchCheckRequest{
    70  					ResourceRelation: RR("document", "edit"),
    71  					ResourceIds:      []string{"foo"},
    72  					Subject:          ONR("user", "sarah", "..."),
    73  					Metadata: &v1.ResolverMeta{
    74  						AtRevision: "123456",
    75  					},
    76  				}, computeBothHashes)
    77  			},
    78  			"d586cee091f9e591c301",
    79  		},
    80  		{
    81  			"canonical check",
    82  			func() DispatchCacheKey {
    83  				key, _ := checkRequestToKeyWithCanonical(&v1.DispatchCheckRequest{
    84  					ResourceRelation: RR("document", "view"),
    85  					ResourceIds:      []string{"foo", "bar"},
    86  					Subject:          ONR("user", "tom", "..."),
    87  					Metadata: &v1.ResolverMeta{
    88  						AtRevision: "1234",
    89  					},
    90  				}, "view")
    91  				return key
    92  			},
    93  			"a1ebd1d6a7a8b18fff01",
    94  		},
    95  		{
    96  			"expand",
    97  			func() DispatchCacheKey {
    98  				return expandRequestToKey(&v1.DispatchExpandRequest{
    99  					ResourceAndRelation: ONR("document", "foo", "view"),
   100  					Metadata: &v1.ResolverMeta{
   101  						AtRevision: "1234",
   102  					},
   103  				}, computeBothHashes)
   104  			},
   105  			"8afff68e91a7cbb3ef01",
   106  		},
   107  		{
   108  			"expand different resource",
   109  			func() DispatchCacheKey {
   110  				return expandRequestToKey(&v1.DispatchExpandRequest{
   111  					ResourceAndRelation: ONR("document", "foo2", "view"),
   112  					Metadata: &v1.ResolverMeta{
   113  						AtRevision: "1234",
   114  					},
   115  				}, computeBothHashes)
   116  			},
   117  			"9dd1e0c9cba88edc6b",
   118  		},
   119  		{
   120  			"expand different revision",
   121  			func() DispatchCacheKey {
   122  				return expandRequestToKey(&v1.DispatchExpandRequest{
   123  					ResourceAndRelation: ONR("document", "foo2", "view"),
   124  					Metadata: &v1.ResolverMeta{
   125  						AtRevision: "1235",
   126  					},
   127  				}, computeBothHashes)
   128  			},
   129  			"f1b396da87bdeae2bd01",
   130  		},
   131  		{
   132  			"lookup resources",
   133  			func() DispatchCacheKey {
   134  				return lookupResourcesRequestToKey(&v1.DispatchLookupResourcesRequest{
   135  					ObjectRelation: RR("document", "view"),
   136  					Subject:        ONR("user", "mariah", "..."),
   137  					Metadata: &v1.ResolverMeta{
   138  						AtRevision: "1234",
   139  					},
   140  				}, computeBothHashes)
   141  			},
   142  			"b4989ce7d2f695c251",
   143  		},
   144  		{
   145  			"lookup resources with zero limit",
   146  			func() DispatchCacheKey {
   147  				return lookupResourcesRequestToKey(&v1.DispatchLookupResourcesRequest{
   148  					ObjectRelation: RR("document", "view"),
   149  					Subject:        ONR("user", "mariah", "..."),
   150  					Metadata: &v1.ResolverMeta{
   151  						AtRevision: "1234",
   152  					},
   153  					OptionalLimit: 0,
   154  				}, computeBothHashes)
   155  			},
   156  			"b4989ce7d2f695c251",
   157  		},
   158  		{
   159  			"lookup resources with non-zero limit",
   160  			func() DispatchCacheKey {
   161  				return lookupResourcesRequestToKey(&v1.DispatchLookupResourcesRequest{
   162  					ObjectRelation: RR("document", "view"),
   163  					Subject:        ONR("user", "mariah", "..."),
   164  					Metadata: &v1.ResolverMeta{
   165  						AtRevision: "1234",
   166  					},
   167  					OptionalLimit: 42,
   168  				}, computeBothHashes)
   169  			},
   170  			"a1bcf8c7e581fb9be401",
   171  		},
   172  		{
   173  			"lookup resources with nil context",
   174  			func() DispatchCacheKey {
   175  				return lookupResourcesRequestToKey(&v1.DispatchLookupResourcesRequest{
   176  					ObjectRelation: RR("document", "view"),
   177  					Subject:        ONR("user", "mariah", "..."),
   178  					Metadata: &v1.ResolverMeta{
   179  						AtRevision: "1234",
   180  					},
   181  					Context: nil,
   182  				}, computeBothHashes)
   183  			},
   184  			"b4989ce7d2f695c251",
   185  		},
   186  		{
   187  			"lookup resources with empty context",
   188  			func() DispatchCacheKey {
   189  				return lookupResourcesRequestToKey(&v1.DispatchLookupResourcesRequest{
   190  					ObjectRelation: RR("document", "view"),
   191  					Subject:        ONR("user", "mariah", "..."),
   192  					Metadata: &v1.ResolverMeta{
   193  						AtRevision: "1234",
   194  					},
   195  					Context: func() *structpb.Struct {
   196  						v, _ := structpb.NewStruct(map[string]any{})
   197  						return v
   198  					}(),
   199  				}, computeBothHashes)
   200  			},
   201  			"b4989ce7d2f695c251",
   202  		},
   203  		{
   204  			"lookup resources with context",
   205  			func() DispatchCacheKey {
   206  				return lookupResourcesRequestToKey(&v1.DispatchLookupResourcesRequest{
   207  					ObjectRelation: RR("document", "view"),
   208  					Subject:        ONR("user", "mariah", "..."),
   209  					Metadata: &v1.ResolverMeta{
   210  						AtRevision: "1234",
   211  					},
   212  					Context: func() *structpb.Struct {
   213  						v, _ := structpb.NewStruct(map[string]any{
   214  							"foo": 1,
   215  							"bar": true,
   216  						})
   217  						return v
   218  					}(),
   219  				}, computeBothHashes)
   220  			},
   221  			"ccffc5dde799b4879401",
   222  		},
   223  		{
   224  			"lookup resources with different context",
   225  			func() DispatchCacheKey {
   226  				return lookupResourcesRequestToKey(&v1.DispatchLookupResourcesRequest{
   227  					ObjectRelation: RR("document", "view"),
   228  					Subject:        ONR("user", "mariah", "..."),
   229  					Metadata: &v1.ResolverMeta{
   230  						AtRevision: "1234",
   231  					},
   232  					Context: func() *structpb.Struct {
   233  						v, _ := structpb.NewStruct(map[string]any{
   234  							"foo": 2,
   235  							"bar": true,
   236  						})
   237  						return v
   238  					}(),
   239  				}, computeBothHashes)
   240  			},
   241  			"dea2e0b3fbdcafdaca01",
   242  		},
   243  		{
   244  			"lookup resources with escaped string",
   245  			func() DispatchCacheKey {
   246  				return lookupResourcesRequestToKey(&v1.DispatchLookupResourcesRequest{
   247  					ObjectRelation: RR("document", "view"),
   248  					Subject:        ONR("user", "mariah", "..."),
   249  					Metadata: &v1.ResolverMeta{
   250  						AtRevision: "1234",
   251  					},
   252  					Context: func() *structpb.Struct {
   253  						v, _ := structpb.NewStruct(map[string]any{
   254  							"foo": "this is an `escaped` string\nhi",
   255  						})
   256  						return v
   257  					}(),
   258  				}, computeBothHashes)
   259  			},
   260  			"949b95adaabcaba6e001",
   261  		},
   262  		{
   263  			"lookup resources with nested context",
   264  			func() DispatchCacheKey {
   265  				return lookupResourcesRequestToKey(&v1.DispatchLookupResourcesRequest{
   266  					ObjectRelation: RR("document", "view"),
   267  					Subject:        ONR("user", "mariah", "..."),
   268  					Metadata: &v1.ResolverMeta{
   269  						AtRevision: "1234",
   270  					},
   271  					Context: func() *structpb.Struct {
   272  						v, _ := structpb.NewStruct(map[string]any{
   273  							"foo": 1,
   274  							"bar": map[string]any{
   275  								"meh": "hiya",
   276  								"baz": "yo",
   277  							},
   278  						})
   279  						return v
   280  					}(),
   281  				}, computeBothHashes)
   282  			},
   283  			"d19a9c9c82d885e13b",
   284  		},
   285  		{
   286  			"lookup resources with empty cursor",
   287  			func() DispatchCacheKey {
   288  				return lookupResourcesRequestToKey(&v1.DispatchLookupResourcesRequest{
   289  					ObjectRelation: RR("document", "view"),
   290  					Subject:        ONR("user", "mariah", "..."),
   291  					Metadata: &v1.ResolverMeta{
   292  						AtRevision: "1234",
   293  					},
   294  					OptionalCursor: &v1.Cursor{},
   295  				}, computeBothHashes)
   296  			},
   297  			"b4989ce7d2f695c251",
   298  		},
   299  		{
   300  			"lookup resources with non-empty cursor",
   301  			func() DispatchCacheKey {
   302  				return lookupResourcesRequestToKey(&v1.DispatchLookupResourcesRequest{
   303  					ObjectRelation: RR("document", "view"),
   304  					Subject:        ONR("user", "mariah", "..."),
   305  					Metadata: &v1.ResolverMeta{
   306  						AtRevision: "1234",
   307  					},
   308  					OptionalCursor: &v1.Cursor{
   309  						Sections: []string{"foo"},
   310  					},
   311  				}, computeBothHashes)
   312  			},
   313  			"d3899bc2cdb9a2d47f",
   314  		},
   315  		{
   316  			"lookup resources with different cursor",
   317  			func() DispatchCacheKey {
   318  				return lookupResourcesRequestToKey(&v1.DispatchLookupResourcesRequest{
   319  					ObjectRelation: RR("document", "view"),
   320  					Subject:        ONR("user", "mariah", "..."),
   321  					Metadata: &v1.ResolverMeta{
   322  						AtRevision: "1234",
   323  					},
   324  					OptionalCursor: &v1.Cursor{
   325  						Sections: []string{"foo", "bar"},
   326  					},
   327  				}, computeBothHashes)
   328  			},
   329  			"f7c18ddf8abc8da3d701",
   330  		},
   331  		{
   332  			"reachable resources",
   333  			func() DispatchCacheKey {
   334  				return reachableResourcesRequestToKey(&v1.DispatchReachableResourcesRequest{
   335  					ResourceRelation: RR("document", "view"),
   336  					SubjectRelation:  RR("user", "..."),
   337  					SubjectIds:       []string{"mariah", "tom"},
   338  					Metadata: &v1.ResolverMeta{
   339  						AtRevision: "1234",
   340  					},
   341  				}, computeBothHashes)
   342  			},
   343  			"c0918ce6b3b0efcc3e",
   344  		},
   345  		{
   346  			"reachable resources with limit",
   347  			func() DispatchCacheKey {
   348  				return reachableResourcesRequestToKey(&v1.DispatchReachableResourcesRequest{
   349  					ResourceRelation: RR("document", "view"),
   350  					SubjectRelation:  RR("user", "..."),
   351  					SubjectIds:       []string{"mariah", "tom"},
   352  					Metadata: &v1.ResolverMeta{
   353  						AtRevision: "1234",
   354  					},
   355  					OptionalLimit: 42,
   356  				}, computeBothHashes)
   357  			},
   358  			"cab5fbaecddc9dbbd501",
   359  		},
   360  		{
   361  			"reachable resources with cursor",
   362  			func() DispatchCacheKey {
   363  				return reachableResourcesRequestToKey(&v1.DispatchReachableResourcesRequest{
   364  					ResourceRelation: RR("document", "view"),
   365  					SubjectRelation:  RR("user", "..."),
   366  					SubjectIds:       []string{"mariah", "tom"},
   367  					Metadata: &v1.ResolverMeta{
   368  						AtRevision: "1234",
   369  					},
   370  					OptionalCursor: &v1.Cursor{
   371  						Sections: []string{"foo"},
   372  					},
   373  				}, computeBothHashes)
   374  			},
   375  			"9a82c4b1abe1cdff68",
   376  		},
   377  		{
   378  			"reachable resources with different cursor",
   379  			func() DispatchCacheKey {
   380  				return reachableResourcesRequestToKey(&v1.DispatchReachableResourcesRequest{
   381  					ResourceRelation: RR("document", "view"),
   382  					SubjectRelation:  RR("user", "..."),
   383  					SubjectIds:       []string{"mariah", "tom"},
   384  					Metadata: &v1.ResolverMeta{
   385  						AtRevision: "1234",
   386  					},
   387  					OptionalCursor: &v1.Cursor{
   388  						Sections: []string{"foo", "bar"},
   389  					},
   390  				}, computeBothHashes)
   391  			},
   392  			"d1acd88b828fce96c701",
   393  		},
   394  		{
   395  			"lookup subjects",
   396  			func() DispatchCacheKey {
   397  				return lookupSubjectsRequestToKey(&v1.DispatchLookupSubjectsRequest{
   398  					ResourceRelation: RR("document", "view"),
   399  					SubjectRelation:  RR("user", "..."),
   400  					ResourceIds:      []string{"mariah", "tom"},
   401  					Metadata: &v1.ResolverMeta{
   402  						AtRevision: "1234",
   403  					},
   404  				}, computeBothHashes)
   405  			},
   406  			"c2b2d3fcb3aa94f5a801",
   407  		},
   408  		{
   409  			"lookup subjects with default limit",
   410  			func() DispatchCacheKey {
   411  				return lookupSubjectsRequestToKey(&v1.DispatchLookupSubjectsRequest{
   412  					ResourceRelation: RR("document", "view"),
   413  					SubjectRelation:  RR("user", "..."),
   414  					ResourceIds:      []string{"mariah", "tom"},
   415  					Metadata: &v1.ResolverMeta{
   416  						AtRevision: "1234",
   417  					},
   418  					OptionalLimit: 0,
   419  				}, computeBothHashes)
   420  			},
   421  			"c2b2d3fcb3aa94f5a801",
   422  		},
   423  		{
   424  			"lookup subjects with different limit",
   425  			func() DispatchCacheKey {
   426  				return lookupSubjectsRequestToKey(&v1.DispatchLookupSubjectsRequest{
   427  					ResourceRelation: RR("document", "view"),
   428  					SubjectRelation:  RR("user", "..."),
   429  					ResourceIds:      []string{"mariah", "tom"},
   430  					Metadata: &v1.ResolverMeta{
   431  						AtRevision: "1234",
   432  					},
   433  					OptionalLimit: 10,
   434  				}, computeBothHashes)
   435  			},
   436  			"ca98fbc58abac8983b",
   437  		},
   438  		{
   439  			"lookup subjects with cursor",
   440  			func() DispatchCacheKey {
   441  				return lookupSubjectsRequestToKey(&v1.DispatchLookupSubjectsRequest{
   442  					ResourceRelation: RR("document", "view"),
   443  					SubjectRelation:  RR("user", "..."),
   444  					ResourceIds:      []string{"mariah", "tom"},
   445  					Metadata: &v1.ResolverMeta{
   446  						AtRevision: "1234",
   447  					},
   448  					OptionalCursor: &v1.Cursor{
   449  						Sections: []string{"foo", "bar"},
   450  					},
   451  				}, computeBothHashes)
   452  			},
   453  			"e7d38be4d395cfc3fc01",
   454  		},
   455  		{
   456  			"lookup subjects with different cursor",
   457  			func() DispatchCacheKey {
   458  				return lookupSubjectsRequestToKey(&v1.DispatchLookupSubjectsRequest{
   459  					ResourceRelation: RR("document", "view"),
   460  					SubjectRelation:  RR("user", "..."),
   461  					ResourceIds:      []string{"mariah", "tom"},
   462  					Metadata: &v1.ResolverMeta{
   463  						AtRevision: "1234",
   464  					},
   465  					OptionalCursor: &v1.Cursor{
   466  						Sections: []string{"foo", "baz"},
   467  					},
   468  				}, computeBothHashes)
   469  			},
   470  			"fccbc38e9cdbcc8cf901",
   471  		},
   472  	}
   473  
   474  	for _, tc := range tcs {
   475  		tc := tc
   476  		t.Run(tc.name, func(t *testing.T) {
   477  			key := tc.createKey()
   478  			require.Equal(t, tc.expected, hex.EncodeToString(key.StableSumAsBytes()))
   479  		})
   480  	}
   481  }
   482  
   483  type generatorFunc func(
   484  	resourceIds []string,
   485  	subjectIds []string,
   486  	resourceRelation *core.RelationReference,
   487  	subjectRelation *core.RelationReference,
   488  	revision *v1.ResolverMeta,
   489  ) (DispatchCacheKey, []string)
   490  
   491  var generatorFuncs = map[string]generatorFunc{
   492  	// Check.
   493  	string(checkViaRelationPrefix): func(
   494  		resourceIds []string,
   495  		subjectIds []string,
   496  		resourceRelation *core.RelationReference,
   497  		subjectRelation *core.RelationReference,
   498  		metadata *v1.ResolverMeta,
   499  	) (DispatchCacheKey, []string) {
   500  		return checkRequestToKey(&v1.DispatchCheckRequest{
   501  				ResourceRelation: resourceRelation,
   502  				ResourceIds:      resourceIds,
   503  				Subject:          ONR(subjectRelation.Namespace, subjectIds[0], subjectRelation.Relation),
   504  				Metadata:         metadata,
   505  			}, computeBothHashes), []string{
   506  				resourceRelation.Namespace,
   507  				resourceRelation.Relation,
   508  				subjectRelation.Namespace,
   509  				subjectIds[0],
   510  				subjectRelation.Relation,
   511  			}
   512  	},
   513  
   514  	// Canonical Check.
   515  	string(checkViaCanonicalPrefix): func(
   516  		resourceIds []string,
   517  		subjectIds []string,
   518  		resourceRelation *core.RelationReference,
   519  		subjectRelation *core.RelationReference,
   520  		metadata *v1.ResolverMeta,
   521  	) (DispatchCacheKey, []string) {
   522  		key, _ := checkRequestToKeyWithCanonical(&v1.DispatchCheckRequest{
   523  			ResourceRelation: resourceRelation,
   524  			ResourceIds:      resourceIds,
   525  			Subject:          ONR(subjectRelation.Namespace, subjectIds[0], subjectRelation.Relation),
   526  			Metadata:         metadata,
   527  		}, resourceRelation.Relation)
   528  		return key, append([]string{
   529  			resourceRelation.Namespace,
   530  			resourceRelation.Relation,
   531  			subjectRelation.Namespace,
   532  			subjectIds[0],
   533  			subjectRelation.Relation,
   534  		}, resourceIds...)
   535  	},
   536  
   537  	// Lookup Resources.
   538  	string(lookupPrefix): func(
   539  		resourceIds []string,
   540  		subjectIds []string,
   541  		resourceRelation *core.RelationReference,
   542  		subjectRelation *core.RelationReference,
   543  		metadata *v1.ResolverMeta,
   544  	) (DispatchCacheKey, []string) {
   545  		return lookupResourcesRequestToKey(&v1.DispatchLookupResourcesRequest{
   546  				ObjectRelation: resourceRelation,
   547  				Subject:        ONR(subjectRelation.Namespace, subjectIds[0], subjectRelation.Relation),
   548  				Metadata:       metadata,
   549  			}, computeBothHashes), []string{
   550  				resourceRelation.Namespace,
   551  				resourceRelation.Relation,
   552  				subjectRelation.Namespace,
   553  				subjectIds[0],
   554  				subjectRelation.Relation,
   555  			}
   556  	},
   557  
   558  	// Expand.
   559  	string(expandPrefix): func(
   560  		resourceIds []string,
   561  		subjectIds []string,
   562  		resourceRelation *core.RelationReference,
   563  		subjectRelation *core.RelationReference,
   564  		metadata *v1.ResolverMeta,
   565  	) (DispatchCacheKey, []string) {
   566  		return expandRequestToKey(&v1.DispatchExpandRequest{
   567  				ResourceAndRelation: ONR(resourceRelation.Namespace, resourceIds[0], resourceRelation.Relation),
   568  				Metadata:            metadata,
   569  			}, computeBothHashes), []string{
   570  				resourceRelation.Namespace,
   571  				resourceIds[0],
   572  				resourceRelation.Relation,
   573  			}
   574  	},
   575  
   576  	// Reachable Resources.
   577  	string(reachableResourcesPrefix): func(
   578  		resourceIds []string,
   579  		subjectIds []string,
   580  		resourceRelation *core.RelationReference,
   581  		subjectRelation *core.RelationReference,
   582  		metadata *v1.ResolverMeta,
   583  	) (DispatchCacheKey, []string) {
   584  		return reachableResourcesRequestToKey(&v1.DispatchReachableResourcesRequest{
   585  				ResourceRelation: resourceRelation,
   586  				SubjectRelation:  subjectRelation,
   587  				SubjectIds:       subjectIds,
   588  				Metadata:         metadata,
   589  			}, computeBothHashes), append([]string{
   590  				resourceRelation.Namespace,
   591  				resourceRelation.Relation,
   592  				subjectRelation.Namespace,
   593  				subjectRelation.Relation,
   594  			}, subjectIds...)
   595  	},
   596  
   597  	// Lookup Subjects.
   598  	string(lookupSubjectsPrefix): func(
   599  		resourceIds []string,
   600  		subjectIds []string,
   601  		resourceRelation *core.RelationReference,
   602  		subjectRelation *core.RelationReference,
   603  		metadata *v1.ResolverMeta,
   604  	) (DispatchCacheKey, []string) {
   605  		return lookupSubjectsRequestToKey(&v1.DispatchLookupSubjectsRequest{
   606  				ResourceRelation: resourceRelation,
   607  				SubjectRelation:  subjectRelation,
   608  				ResourceIds:      resourceIds,
   609  				Metadata:         metadata,
   610  			}, computeBothHashes), append([]string{
   611  				resourceRelation.Namespace,
   612  				resourceRelation.Relation,
   613  				subjectRelation.Namespace,
   614  				subjectRelation.Relation,
   615  			}, resourceIds...)
   616  	},
   617  }
   618  
   619  func TestCacheKeyNoOverlap(t *testing.T) {
   620  	allResourceIds := [][]string{
   621  		{"1"},
   622  		{"1", "2"},
   623  		{"1", "2", "3"},
   624  		{"hi"},
   625  	}
   626  
   627  	allSubjectIds := [][]string{
   628  		{"tom"},
   629  		{"mariah", "tom"},
   630  		{"sarah", "mariah", "tom"},
   631  	}
   632  
   633  	resourceRelations := []*core.RelationReference{
   634  		RR("document", "view"),
   635  		RR("document", "viewer"),
   636  		RR("document", "edit"),
   637  		RR("folder", "view"),
   638  	}
   639  
   640  	subjectRelations := []*core.RelationReference{
   641  		RR("user", "..."),
   642  		RR("user", "token"),
   643  		RR("folder", "parent"),
   644  		RR("group", "member"),
   645  	}
   646  
   647  	revisions := []string{"1234", "4567", "1235"}
   648  
   649  	dataCombinationSeen := mapz.NewSet[string]()
   650  	stableCacheKeysSeen := mapz.NewSet[string]()
   651  	unstableCacheKeysSeen := mapz.NewSet[uint64]()
   652  
   653  	// Ensure all key functions are generated.
   654  	require.Equal(t, len(generatorFuncs), len(cachePrefixes))
   655  
   656  	for _, resourceIds := range allResourceIds {
   657  		resourceIds := resourceIds
   658  		t.Run(strings.Join(resourceIds, ","), func(t *testing.T) {
   659  			for _, subjectIds := range allSubjectIds {
   660  				subjectIds := subjectIds
   661  				t.Run(strings.Join(subjectIds, ","), func(t *testing.T) {
   662  					for _, resourceRelation := range resourceRelations {
   663  						resourceRelation := resourceRelation
   664  						t.Run(tuple.StringRR(resourceRelation), func(t *testing.T) {
   665  							for _, subjectRelation := range subjectRelations {
   666  								subjectRelation := subjectRelation
   667  								t.Run(tuple.StringRR(subjectRelation), func(t *testing.T) {
   668  									for _, revision := range revisions {
   669  										revision := revision
   670  										t.Run(revision, func(t *testing.T) {
   671  											metadata := &v1.ResolverMeta{
   672  												AtRevision: revision,
   673  											}
   674  
   675  											for prefix, f := range generatorFuncs {
   676  												prefix := prefix
   677  												f := f
   678  												t.Run(prefix, func(t *testing.T) {
   679  													generated, usedData := f(resourceIds, subjectIds, resourceRelation, subjectRelation, metadata)
   680  													usedDataString := fmt.Sprintf("%s:%s", prefix, strings.Join(usedData, ","))
   681  													if dataCombinationSeen.Add(usedDataString) {
   682  														require.True(t, stableCacheKeysSeen.Add(hex.EncodeToString((generated.StableSumAsBytes()))))
   683  														require.True(t, unstableCacheKeysSeen.Add(generated.processSpecificSum))
   684  													}
   685  												})
   686  											}
   687  										})
   688  									}
   689  								})
   690  							}
   691  						})
   692  					}
   693  				})
   694  			}
   695  		})
   696  	}
   697  }
   698  
   699  func TestComputeOnlyStableHash(t *testing.T) {
   700  	result := checkRequestToKey(&v1.DispatchCheckRequest{
   701  		ResourceRelation: RR("document", "view"),
   702  		ResourceIds:      []string{"foo", "bar"},
   703  		Subject:          ONR("user", "tom", "..."),
   704  		Metadata: &v1.ResolverMeta{
   705  			AtRevision: "1234",
   706  		},
   707  	}, computeOnlyStableHash)
   708  
   709  	require.Equal(t, uint64(0), result.processSpecificSum)
   710  }
   711  
   712  func TestComputeContextHash(t *testing.T) {
   713  	result := lookupResourcesRequestToKey(&v1.DispatchLookupResourcesRequest{
   714  		ObjectRelation: RR("document", "view"),
   715  		Subject:        ONR("user", "mariah", "..."),
   716  		Metadata: &v1.ResolverMeta{
   717  			AtRevision: "1234",
   718  		},
   719  		Context: func() *structpb.Struct {
   720  			v, _ := structpb.NewStruct(map[string]any{
   721  				"null": nil,
   722  				"list": []any{
   723  					1, true, "3",
   724  				},
   725  				"nested": map[string]any{
   726  					"a": "hi",
   727  					"b": "hello",
   728  					"c": 123,
   729  				},
   730  			})
   731  			return v
   732  		}(),
   733  	}, computeBothHashes)
   734  
   735  	require.Equal(t, "a4eacff68ec68bca62", hex.EncodeToString(result.StableSumAsBytes()))
   736  }