github.com/openfga/openfga@v1.5.4-rc1/pkg/server/commands/reverseexpand/reverse_expand_test.go (about) 1 package reverseexpand 2 3 import ( 4 "context" 5 "fmt" 6 "strconv" 7 "testing" 8 "time" 9 10 "github.com/oklog/ulid/v2" 11 openfgav1 "github.com/openfga/api/proto/openfga/v1" 12 "github.com/stretchr/testify/require" 13 "go.uber.org/goleak" 14 "go.uber.org/mock/gomock" 15 16 "github.com/openfga/openfga/internal/mocks" 17 "github.com/openfga/openfga/pkg/storage" 18 "github.com/openfga/openfga/pkg/storage/memory" 19 "github.com/openfga/openfga/pkg/testutils" 20 "github.com/openfga/openfga/pkg/tuple" 21 "github.com/openfga/openfga/pkg/typesystem" 22 ) 23 24 func TestReverseExpandResultChannelClosed(t *testing.T) { 25 defer goleak.VerifyNone(t) 26 27 store := ulid.Make().String() 28 29 model := testutils.MustTransformDSLToProtoWithID(`model 30 schema 1.1 31 type user 32 type document 33 relations 34 define viewer: [user]`) 35 36 typeSystem := typesystem.New(model) 37 mockController := gomock.NewController(t) 38 defer mockController.Finish() 39 40 var tuples []*openfgav1.Tuple 41 42 mockDatastore := mocks.NewMockOpenFGADatastore(mockController) 43 mockDatastore.EXPECT().ReadStartingWithUser(gomock.Any(), store, gomock.Any()). 44 Times(1). 45 DoAndReturn(func(_ context.Context, _ string, _ storage.ReadStartingWithUserFilter) (storage.TupleIterator, error) { 46 iterator := storage.NewStaticTupleIterator(tuples) 47 return iterator, nil 48 }) 49 50 ctx := context.Background() 51 52 resultChan := make(chan *ReverseExpandResult) 53 errChan := make(chan error, 1) 54 55 // process query in one goroutine, but it will be cancelled almost right away 56 go func() { 57 reverseExpandQuery := NewReverseExpandQuery(mockDatastore, typeSystem) 58 t.Logf("before execute reverse expand") 59 err := reverseExpandQuery.Execute(ctx, &ReverseExpandRequest{ 60 StoreID: store, 61 ObjectType: "document", 62 Relation: "viewer", 63 User: &UserRefObject{ 64 Object: &openfgav1.Object{ 65 Type: "user", 66 Id: "maria", 67 }, 68 }, 69 ContextualTuples: []*openfgav1.TupleKey{}, 70 }, resultChan, NewResolutionMetadata()) 71 t.Logf("after execute reverse expand") 72 73 if err != nil { 74 errChan <- err 75 } 76 }() 77 78 select { 79 case _, open := <-resultChan: 80 if open { 81 require.FailNow(t, "expected immediate closure of result channel") 82 } 83 case err := <-errChan: 84 require.FailNow(t, "unexpected error received on error channel :%v", err) 85 case <-time.After(30 * time.Millisecond): 86 require.FailNow(t, "unexpected timeout on channel receive, expected receive on error channel") 87 } 88 } 89 90 func TestReverseExpandRespectsContextCancellation(t *testing.T) { 91 defer goleak.VerifyNone(t) 92 93 store := ulid.Make().String() 94 95 model := testutils.MustTransformDSLToProtoWithID(`model 96 schema 1.1 97 type user 98 type document 99 relations 100 define viewer: [user]`) 101 102 typeSystem := typesystem.New(model) 103 mockController := gomock.NewController(t) 104 defer mockController.Finish() 105 106 var tuples []*openfgav1.Tuple 107 for i := 0; i < 100; i++ { 108 obj := fmt.Sprintf("document:%s", strconv.Itoa(i)) 109 tuples = append(tuples, &openfgav1.Tuple{Key: tuple.NewTupleKey(obj, "viewer", "user:maria")}) 110 } 111 112 mockDatastore := mocks.NewMockOpenFGADatastore(mockController) 113 mockDatastore.EXPECT().ReadStartingWithUser(gomock.Any(), store, gomock.Any()). 114 Times(1). 115 DoAndReturn(func(_ context.Context, _ string, _ storage.ReadStartingWithUserFilter) (storage.TupleIterator, error) { 116 // simulate many goroutines trying to write to the results channel 117 iterator := storage.NewStaticTupleIterator(tuples) 118 t.Logf("returning tuple iterator") 119 return iterator, nil 120 }) 121 ctx, cancelFunc := context.WithCancel(context.Background()) 122 123 resultChan := make(chan *ReverseExpandResult) 124 errChan := make(chan error, 1) 125 126 // process query in one goroutine, but it will be cancelled almost right away 127 go func() { 128 reverseExpandQuery := NewReverseExpandQuery(mockDatastore, typeSystem) 129 t.Logf("before execute reverse expand") 130 err := reverseExpandQuery.Execute(ctx, &ReverseExpandRequest{ 131 StoreID: store, 132 ObjectType: "document", 133 Relation: "viewer", 134 User: &UserRefObject{ 135 Object: &openfgav1.Object{ 136 Type: "user", 137 Id: "maria", 138 }, 139 }, 140 ContextualTuples: []*openfgav1.TupleKey{}, 141 }, resultChan, NewResolutionMetadata()) 142 t.Logf("after execute reverse expand") 143 144 if err != nil { 145 errChan <- err 146 } 147 }() 148 go func() { 149 // simulate max_results=1 150 t.Logf("before receive one result") 151 res := <-resultChan 152 t.Logf("after receive one result") 153 154 // send cancellation to the other goroutine 155 cancelFunc() 156 157 // this check it not the goal of this test, it's here just as sanity check 158 if res.Object == "" { 159 panic("expected object, got nil") 160 } 161 t.Logf("received object %s ", res.Object) 162 }() 163 164 select { 165 case err := <-errChan: 166 require.ErrorContains(t, err, "context canceled") 167 case <-time.After(30 * time.Millisecond): 168 require.FailNow(t, "unexpected timeout on channel receive, expected receive on error channel") 169 } 170 } 171 172 func TestReverseExpandRespectsContextTimeout(t *testing.T) { 173 defer goleak.VerifyNone(t) 174 175 store := ulid.Make().String() 176 177 model := testutils.MustTransformDSLToProtoWithID(`model 178 schema 1.1 179 type user 180 type document 181 relations 182 define allowed: [user] 183 define viewer: [user] and allowed`) 184 185 typeSystem := typesystem.New(model) 186 mockController := gomock.NewController(t) 187 defer mockController.Finish() 188 189 mockDatastore := mocks.NewMockOpenFGADatastore(mockController) 190 mockDatastore.EXPECT().ReadStartingWithUser(gomock.Any(), store, gomock.Any()). 191 MaxTimes(2) // we expect it to be 0 most of the time 192 193 timeoutCtx, cancel := context.WithTimeout(context.Background(), time.Nanosecond) 194 defer cancel() 195 resultChan := make(chan *ReverseExpandResult) 196 errChan := make(chan error, 1) 197 198 go func() { 199 reverseExpandQuery := NewReverseExpandQuery(mockDatastore, typeSystem) 200 err := reverseExpandQuery.Execute(timeoutCtx, &ReverseExpandRequest{ 201 StoreID: store, 202 ObjectType: "document", 203 Relation: "viewer", 204 User: &UserRefObject{ 205 Object: &openfgav1.Object{ 206 Type: "user", 207 Id: "maria", 208 }, 209 }, 210 ContextualTuples: []*openfgav1.TupleKey{}, 211 }, resultChan, NewResolutionMetadata()) 212 213 if err != nil { 214 errChan <- err 215 } 216 }() 217 select { 218 case _, open := <-resultChan: 219 if !open { 220 require.FailNow(t, "unexpected closure of result channel") 221 } 222 case err := <-errChan: 223 require.Error(t, err) 224 case <-time.After(1 * time.Second): 225 require.FailNow(t, "unexpected timeout encountered, expected other receive") 226 } 227 } 228 229 func TestReverseExpandErrorInTuples(t *testing.T) { 230 defer goleak.VerifyNone(t) 231 232 store := ulid.Make().String() 233 234 model := testutils.MustTransformDSLToProtoWithID(`model 235 schema 1.1 236 type user 237 type document 238 relations 239 define viewer: [user]`) 240 241 typeSystem := typesystem.New(model) 242 mockController := gomock.NewController(t) 243 defer mockController.Finish() 244 245 var tuples []*openfgav1.Tuple 246 for i := 0; i < 100; i++ { 247 obj := fmt.Sprintf("document:%s", strconv.Itoa(i)) 248 tuples = append(tuples, &openfgav1.Tuple{Key: tuple.NewTupleKey(obj, "viewer", "user:maria")}) 249 } 250 251 mockDatastore := mocks.NewMockOpenFGADatastore(mockController) 252 mockDatastore.EXPECT().ReadStartingWithUser(gomock.Any(), store, gomock.Any()). 253 DoAndReturn(func(_ context.Context, _ string, _ storage.ReadStartingWithUserFilter) (storage.TupleIterator, error) { 254 iterator := mocks.NewErrorTupleIterator(tuples) 255 return iterator, nil 256 }) 257 258 ctx, cancelFunc := context.WithCancel(context.Background()) 259 defer cancelFunc() 260 261 resultChan := make(chan *ReverseExpandResult) 262 errChan := make(chan error, 1) 263 264 // process query in one goroutine, but it will be cancelled almost right away 265 go func() { 266 reverseExpandQuery := NewReverseExpandQuery(mockDatastore, typeSystem) 267 err := reverseExpandQuery.Execute(ctx, &ReverseExpandRequest{ 268 StoreID: store, 269 ObjectType: "document", 270 Relation: "viewer", 271 User: &UserRefObject{ 272 Object: &openfgav1.Object{ 273 Type: "user", 274 Id: "maria", 275 }, 276 }, 277 ContextualTuples: []*openfgav1.TupleKey{}, 278 }, resultChan, NewResolutionMetadata()) 279 if err != nil { 280 errChan <- err 281 } 282 }() 283 284 ConsumerLoop: 285 for { 286 select { 287 case _, open := <-resultChan: 288 if !open { 289 require.FailNow(t, "unexpected closure of result channel") 290 } 291 292 cancelFunc() 293 case err := <-errChan: 294 require.Error(t, err) 295 break ConsumerLoop 296 case <-time.After(30 * time.Millisecond): 297 require.FailNow(t, "unexpected timeout waiting for channel receive, expected an error on the error channel") 298 } 299 } 300 } 301 302 func TestReverseExpandSendsAllErrorsThroughChannel(t *testing.T) { 303 defer goleak.VerifyNone(t) 304 305 store := ulid.Make().String() 306 307 model := testutils.MustTransformDSLToProtoWithID(`model 308 schema 1.1 309 type user 310 type document 311 relations 312 define viewer: [user]`) 313 314 mockDatastore := mocks.NewMockSlowDataStorage(memory.New(), 1*time.Second) 315 316 for i := 0; i < 50; i++ { 317 t.Logf("iteration %d", i) 318 ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(1*time.Nanosecond)) 319 t.Cleanup(cancel) 320 321 resultChan := make(chan *ReverseExpandResult) 322 errChan := make(chan error, 1) 323 324 go func() { 325 reverseExpandQuery := NewReverseExpandQuery(mockDatastore, typesystem.New(model)) 326 t.Logf("before produce") 327 err := reverseExpandQuery.Execute(ctx, &ReverseExpandRequest{ 328 StoreID: store, 329 ObjectType: "document", 330 Relation: "viewer", 331 User: &UserRefObject{ 332 Object: &openfgav1.Object{ 333 Type: "user", 334 Id: "maria", 335 }, 336 }, 337 ContextualTuples: []*openfgav1.TupleKey{}, 338 }, resultChan, NewResolutionMetadata()) 339 t.Logf("after produce") 340 341 if err != nil { 342 errChan <- err 343 } 344 }() 345 346 select { 347 case _, channelOpen := <-resultChan: 348 if !channelOpen { 349 require.FailNow(t, "unexpected closure of result channel") 350 } 351 case err := <-errChan: 352 require.Error(t, err) 353 case <-time.After(3 * time.Second): 354 require.FailNow(t, "unexpected timeout waiting for channel receive, expected an error on the error channel") 355 } 356 } 357 } 358 359 func TestReverseExpandIgnoresInvalidTuples(t *testing.T) { 360 t.Cleanup(func() { 361 goleak.VerifyNone(t) 362 }) 363 364 storeID := ulid.Make().String() 365 366 model := testutils.MustTransformDSLToProtoWithID(` 367 model 368 schema 1.1 369 type user 370 type group 371 relations 372 define member: [user, group#member]`) 373 374 mockController := gomock.NewController(t) 375 t.Cleanup(func() { 376 mockController.Finish() 377 }) 378 379 mockDatastore := mocks.NewMockOpenFGADatastore(mockController) 380 gomock.InAnyOrder([]*gomock.Call{ 381 mockDatastore.EXPECT().ReadStartingWithUser(gomock.Any(), storeID, storage.ReadStartingWithUserFilter{ 382 ObjectType: "group", 383 Relation: "member", 384 UserFilter: []*openfgav1.ObjectRelation{{Object: "user:anne"}}, 385 }). 386 Times(1). 387 DoAndReturn(func(_ context.Context, _ string, _ storage.ReadStartingWithUserFilter) (storage.TupleIterator, error) { 388 return storage.NewStaticTupleIterator([]*openfgav1.Tuple{ 389 {Key: tuple.NewTupleKey("group:fga", "member", "user:anne")}, 390 }), nil 391 }), 392 393 mockDatastore.EXPECT().ReadStartingWithUser(gomock.Any(), storeID, storage.ReadStartingWithUserFilter{ 394 ObjectType: "group", 395 Relation: "member", 396 UserFilter: []*openfgav1.ObjectRelation{{Object: "group:fga", Relation: "member"}}, 397 }). 398 Times(1). 399 DoAndReturn(func(_ context.Context, _ string, _ storage.ReadStartingWithUserFilter) (storage.TupleIterator, error) { 400 return storage.NewStaticTupleIterator([]*openfgav1.Tuple{ 401 // NOTE this tuple is invalid 402 {Key: tuple.NewTupleKey("group:eng#member", "member", "group:fga#member")}, 403 }), nil 404 }), 405 }, 406 ) 407 408 ctx := context.Background() 409 410 resultChan := make(chan *ReverseExpandResult, 2) 411 errChan := make(chan error, 1) 412 413 go func() { 414 reverseExpandQuery := NewReverseExpandQuery(mockDatastore, typesystem.New(model)) 415 err := reverseExpandQuery.Execute(ctx, &ReverseExpandRequest{ 416 StoreID: storeID, 417 ObjectType: "group", 418 Relation: "member", 419 User: &UserRefObject{Object: &openfgav1.Object{Type: "user", Id: "anne"}}, 420 ContextualTuples: []*openfgav1.TupleKey{}, 421 }, resultChan, NewResolutionMetadata()) 422 423 if err != nil { 424 errChan <- err 425 } 426 }() 427 428 var results []string 429 430 for { 431 select { 432 case res, open := <-resultChan: 433 if !open { 434 require.ElementsMatch(t, []string{"group:fga"}, results) 435 return 436 } 437 results = append(results, res.Object) 438 case err := <-errChan: 439 require.FailNow(t, "unexpected error received on error channel :%v", err) 440 return 441 case <-ctx.Done(): 442 return 443 } 444 } 445 }