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  }