github.com/openfga/openfga@v1.5.4-rc1/pkg/storage/mysql/mysql_test.go (about)

     1  package mysql
     2  
     3  import (
     4  	"context"
     5  	"testing"
     6  	"time"
     7  
     8  	sq "github.com/Masterminds/squirrel"
     9  	"github.com/oklog/ulid/v2"
    10  	openfgav1 "github.com/openfga/api/proto/openfga/v1"
    11  	"github.com/stretchr/testify/require"
    12  	"google.golang.org/protobuf/proto"
    13  
    14  	"github.com/openfga/openfga/pkg/storage"
    15  	"github.com/openfga/openfga/pkg/storage/sqlcommon"
    16  	"github.com/openfga/openfga/pkg/storage/test"
    17  	storagefixtures "github.com/openfga/openfga/pkg/testfixtures/storage"
    18  	"github.com/openfga/openfga/pkg/tuple"
    19  	"github.com/openfga/openfga/pkg/typesystem"
    20  )
    21  
    22  func TestMySQLDatastore(t *testing.T) {
    23  	testDatastore := storagefixtures.RunDatastoreTestContainer(t, "mysql")
    24  
    25  	uri := testDatastore.GetConnectionURI(true)
    26  	ds, err := New(uri, sqlcommon.NewConfig())
    27  	require.NoError(t, err)
    28  	defer ds.Close()
    29  	test.RunAllTests(t, ds)
    30  }
    31  
    32  func TestMySQLDatastoreAfterCloseIsNotReady(t *testing.T) {
    33  	testDatastore := storagefixtures.RunDatastoreTestContainer(t, "mysql")
    34  
    35  	uri := testDatastore.GetConnectionURI(true)
    36  	ds, err := New(uri, sqlcommon.NewConfig())
    37  	require.NoError(t, err)
    38  	ds.Close()
    39  	status, err := ds.IsReady(context.Background())
    40  	require.Error(t, err)
    41  	require.False(t, status.IsReady)
    42  }
    43  
    44  // TestReadEnsureNoOrder asserts that the read response is not ordered by ulid.
    45  func TestReadEnsureNoOrder(t *testing.T) {
    46  	testDatastore := storagefixtures.RunDatastoreTestContainer(t, "mysql")
    47  
    48  	uri := testDatastore.GetConnectionURI(true)
    49  	ds, err := New(uri, sqlcommon.NewConfig())
    50  	require.NoError(t, err)
    51  	defer ds.Close()
    52  
    53  	ctx := context.Background()
    54  	store := "store"
    55  	firstTuple := tuple.NewTupleKey("doc:object_id_1", "relation", "user:user_1")
    56  	secondTuple := tuple.NewTupleKey("doc:object_id_2", "relation", "user:user_2")
    57  
    58  	err = sqlcommon.Write(ctx,
    59  		sqlcommon.NewDBInfo(ds.db, ds.stbl, sq.Expr("NOW()")),
    60  		store,
    61  		[]*openfgav1.TupleKeyWithoutCondition{},
    62  		[]*openfgav1.TupleKey{firstTuple},
    63  		time.Now())
    64  	require.NoError(t, err)
    65  
    66  	// Tweak time so that ULID is smaller.
    67  	err = sqlcommon.Write(ctx,
    68  		sqlcommon.NewDBInfo(ds.db, ds.stbl, sq.Expr("NOW()")),
    69  		store,
    70  		[]*openfgav1.TupleKeyWithoutCondition{},
    71  		[]*openfgav1.TupleKey{secondTuple},
    72  		time.Now().Add(time.Minute*-1))
    73  	require.NoError(t, err)
    74  
    75  	iter, err := ds.Read(ctx,
    76  		store,
    77  		tuple.NewTupleKey("doc:", "relation", ""))
    78  	defer iter.Stop()
    79  
    80  	require.NoError(t, err)
    81  
    82  	// We expect that objectID1 will return first because it is inserted first.
    83  	curTuple, err := iter.Next(ctx)
    84  	require.NoError(t, err)
    85  	require.Equal(t, firstTuple, curTuple.GetKey())
    86  
    87  	curTuple, err = iter.Next(ctx)
    88  	require.NoError(t, err)
    89  	require.Equal(t, secondTuple, curTuple.GetKey())
    90  }
    91  
    92  // TestReadPageEnsureNoOrder asserts that the read page is ordered by ulid.
    93  func TestReadPageEnsureOrder(t *testing.T) {
    94  	testDatastore := storagefixtures.RunDatastoreTestContainer(t, "mysql")
    95  
    96  	uri := testDatastore.GetConnectionURI(true)
    97  	ds, err := New(uri, sqlcommon.NewConfig())
    98  	require.NoError(t, err)
    99  	defer ds.Close()
   100  
   101  	ctx := context.Background()
   102  
   103  	store := "store"
   104  	firstTuple := tuple.NewTupleKey("doc:object_id_1", "relation", "user:user_1")
   105  	secondTuple := tuple.NewTupleKey("doc:object_id_2", "relation", "user:user_2")
   106  
   107  	err = sqlcommon.Write(ctx,
   108  		sqlcommon.NewDBInfo(ds.db, ds.stbl, sq.Expr("NOW()")),
   109  		store,
   110  		[]*openfgav1.TupleKeyWithoutCondition{},
   111  		[]*openfgav1.TupleKey{firstTuple},
   112  		time.Now())
   113  	require.NoError(t, err)
   114  
   115  	// Tweak time so that ULID is smaller.
   116  	err = sqlcommon.Write(ctx,
   117  		sqlcommon.NewDBInfo(ds.db, ds.stbl, sq.Expr("NOW()")),
   118  		store,
   119  		[]*openfgav1.TupleKeyWithoutCondition{},
   120  		[]*openfgav1.TupleKey{secondTuple},
   121  		time.Now().Add(time.Minute*-1))
   122  	require.NoError(t, err)
   123  
   124  	tuples, _, err := ds.ReadPage(ctx,
   125  		store,
   126  		tuple.NewTupleKey("doc:", "relation", ""),
   127  		storage.NewPaginationOptions(0, ""))
   128  	require.NoError(t, err)
   129  
   130  	require.Len(t, tuples, 2)
   131  	// We expect that objectID2 will return first because it has a smaller ulid.
   132  	require.Equal(t, secondTuple, tuples[0].GetKey())
   133  	require.Equal(t, firstTuple, tuples[1].GetKey())
   134  }
   135  
   136  func TestReadAuthorizationModelUnmarshallError(t *testing.T) {
   137  	testDatastore := storagefixtures.RunDatastoreTestContainer(t, "mysql")
   138  
   139  	uri := testDatastore.GetConnectionURI(true)
   140  	ds, err := New(uri, sqlcommon.NewConfig())
   141  	require.NoError(t, err)
   142  
   143  	ctx := context.Background()
   144  	defer ds.Close()
   145  	store := "store"
   146  	modelID := "foo"
   147  	schemaVersion := typesystem.SchemaVersion1_0
   148  
   149  	bytes, err := proto.Marshal(&openfgav1.TypeDefinition{Type: "document"})
   150  	require.NoError(t, err)
   151  	pbdata := []byte{0x01, 0x02, 0x03}
   152  
   153  	_, err = ds.db.ExecContext(ctx, "INSERT INTO authorization_model (store, authorization_model_id, schema_version, type, type_definition, serialized_protobuf) VALUES (?, ?, ?, ?, ?, ?)", store, modelID, schemaVersion, "document", bytes, pbdata)
   154  	require.NoError(t, err)
   155  
   156  	_, err = ds.ReadAuthorizationModel(ctx, store, modelID)
   157  	require.Error(t, err)
   158  	require.Contains(t, err.Error(), "cannot parse invalid wire-format data")
   159  }
   160  
   161  // TestAllowNullCondition tests that tuple and changelog rows existing before
   162  // migration 005_add_conditions_to_tuples can be successfully read.
   163  func TestAllowNullCondition(t *testing.T) {
   164  	ctx := context.Background()
   165  	testDatastore := storagefixtures.RunDatastoreTestContainer(t, "mysql")
   166  
   167  	uri := testDatastore.GetConnectionURI(true)
   168  	ds, err := New(uri, sqlcommon.NewConfig())
   169  	require.NoError(t, err)
   170  	defer ds.Close()
   171  
   172  	stmt := `
   173  		INSERT INTO tuple (
   174  			store, object_type, object_id, relation, _user, user_type, ulid,
   175  			condition_name, condition_context, inserted_at
   176  		) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NOW());
   177  	`
   178  	_, err = ds.db.ExecContext(
   179  		ctx, stmt, "store", "folder", "2021-budget", "owner", "user:anne", "user",
   180  		ulid.Make().String(), nil, nil,
   181  	)
   182  	require.NoError(t, err)
   183  
   184  	tk := tuple.NewTupleKey("folder:2021-budget", "owner", "user:anne")
   185  	iter, err := ds.Read(ctx, "store", tk)
   186  	require.NoError(t, err)
   187  	defer iter.Stop()
   188  
   189  	curTuple, err := iter.Next(ctx)
   190  	require.NoError(t, err)
   191  	require.Equal(t, tk, curTuple.GetKey())
   192  
   193  	tuples, _, err := ds.ReadPage(ctx, "store", &openfgav1.TupleKey{}, storage.PaginationOptions{
   194  		PageSize: 2,
   195  	})
   196  	require.NoError(t, err)
   197  	require.Len(t, tuples, 1)
   198  	require.Equal(t, tk, tuples[0].GetKey())
   199  
   200  	userTuple, err := ds.ReadUserTuple(ctx, "store", tk)
   201  	require.NoError(t, err)
   202  	require.Equal(t, tk, userTuple.GetKey())
   203  
   204  	tk2 := tuple.NewTupleKey("folder:2022-budget", "viewer", "user:anne")
   205  	_, err = ds.db.ExecContext(
   206  		ctx, stmt, "store", "folder", "2022-budget", "viewer", "user:anne", "userset",
   207  		ulid.Make().String(), nil, nil,
   208  	)
   209  
   210  	require.NoError(t, err)
   211  	iter, err = ds.ReadUsersetTuples(ctx, "store", storage.ReadUsersetTuplesFilter{Object: "folder:2022-budget"})
   212  	require.NoError(t, err)
   213  	defer iter.Stop()
   214  
   215  	curTuple, err = iter.Next(ctx)
   216  	require.NoError(t, err)
   217  	require.Equal(t, tk2, curTuple.GetKey())
   218  
   219  	iter, err = ds.ReadStartingWithUser(ctx, "store", storage.ReadStartingWithUserFilter{
   220  		ObjectType: "folder",
   221  		Relation:   "owner",
   222  		UserFilter: []*openfgav1.ObjectRelation{
   223  			{Object: "user:anne"},
   224  		},
   225  	})
   226  	require.NoError(t, err)
   227  	defer iter.Stop()
   228  
   229  	curTuple, err = iter.Next(ctx)
   230  	require.NoError(t, err)
   231  	require.Equal(t, tk, curTuple.GetKey())
   232  
   233  	stmt = `
   234  	INSERT INTO changelog (
   235  		store, object_type, object_id, relation, _user, ulid,
   236  		condition_name, condition_context, inserted_at, operation
   237  	) VALUES (?, ?, ?, ?, ?, ?, ?, ?, NOW(), ?);
   238  `
   239  	_, err = ds.db.ExecContext(
   240  		ctx, stmt, "store", "folder", "2021-budget", "owner", "user:anne",
   241  		ulid.Make().String(), nil, nil, openfgav1.TupleOperation_TUPLE_OPERATION_WRITE,
   242  	)
   243  	require.NoError(t, err)
   244  
   245  	_, err = ds.db.ExecContext(
   246  		ctx, stmt, "store", "folder", "2021-budget", "owner", "user:anne",
   247  		ulid.Make().String(), nil, nil, openfgav1.TupleOperation_TUPLE_OPERATION_DELETE,
   248  	)
   249  	require.NoError(t, err)
   250  
   251  	changes, _, err := ds.ReadChanges(ctx, "store", "folder", storage.PaginationOptions{}, 0)
   252  	require.NoError(t, err)
   253  	require.Len(t, changes, 2)
   254  	require.Equal(t, tk, changes[0].GetTupleKey())
   255  	require.Equal(t, tk, changes[1].GetTupleKey())
   256  }
   257  
   258  // TestMarshalledAssertions tests that previously persisted marshalled
   259  // assertions can be read back. In any case where the Assertions proto model
   260  // needs to change, we'll likely need to introduce a series of data migrations.
   261  func TestMarshalledAssertions(t *testing.T) {
   262  	ctx := context.Background()
   263  	testDatastore := storagefixtures.RunDatastoreTestContainer(t, "mysql")
   264  
   265  	uri := testDatastore.GetConnectionURI(true)
   266  	ds, err := New(uri, sqlcommon.NewConfig())
   267  	require.NoError(t, err)
   268  	defer ds.Close()
   269  
   270  	// Note: this represents an assertion written on v1.3.7.
   271  	stmt := `
   272  		INSERT INTO assertion (
   273  			store, authorization_model_id, assertions
   274  		) VALUES (?, ?, UNHEX('0A2B0A270A12666F6C6465723A323032312D62756467657412056F776E65721A0A757365723A616E6E657A1001'));
   275  	`
   276  	_, err = ds.db.ExecContext(ctx, stmt, "store", "model")
   277  	require.NoError(t, err)
   278  
   279  	assertions, err := ds.ReadAssertions(ctx, "store", "model")
   280  	require.NoError(t, err)
   281  
   282  	expectedAssertions := []*openfgav1.Assertion{
   283  		{
   284  			TupleKey: &openfgav1.AssertionTupleKey{
   285  				Object:   "folder:2021-budget",
   286  				Relation: "owner",
   287  				User:     "user:annez",
   288  			},
   289  			Expectation: true,
   290  		},
   291  	}
   292  	require.Equal(t, expectedAssertions, assertions)
   293  }