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 }