github.com/mre-fog/trillianxx@v1.1.2-0.20180615153820-ae375a99d36a/quota/mysqlqm/mysql_quota_test.go (about) 1 // Copyright 2017 Google Inc. All Rights Reserved. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package mysqlqm_test 16 17 import ( 18 "context" 19 "crypto" 20 "database/sql" 21 "fmt" 22 "testing" 23 "time" 24 25 "github.com/google/trillian" 26 "github.com/google/trillian/quota" 27 "github.com/google/trillian/quota/mysqlqm" 28 "github.com/google/trillian/storage" 29 "github.com/google/trillian/storage/mysql" 30 "github.com/google/trillian/storage/testdb" 31 "github.com/google/trillian/testonly" 32 "github.com/google/trillian/trees" 33 "github.com/google/trillian/types" 34 "github.com/kylelemons/godebug/pretty" 35 36 tcrypto "github.com/google/trillian/crypto" 37 stestonly "github.com/google/trillian/storage/testonly" 38 ) 39 40 func TestQuotaManager_GetTokens(t *testing.T) { 41 testdb.SkipIfNoMySQL(t) 42 ctx := context.Background() 43 44 db, err := testdb.NewTrillianDB(ctx) 45 if err != nil { 46 t.Fatalf("GetTestDB() returned err = %v", err) 47 } 48 defer db.Close() 49 50 tree, err := createTree(ctx, db) 51 if err != nil { 52 t.Fatalf("createTree() returned err = %v", err) 53 } 54 user := (&mysqlqm.QuotaManager{}).GetUser(ctx, nil /* req */) 55 56 tests := []struct { 57 desc string 58 unsequencedRows, maxUnsequencedRows, numTokens int 59 specs []quota.Spec 60 wantErr bool 61 }{ 62 { 63 desc: "globalWriteSingleToken", 64 unsequencedRows: 10, 65 maxUnsequencedRows: 20, 66 numTokens: 1, 67 specs: []quota.Spec{{Group: quota.Global, Kind: quota.Write}}, 68 }, 69 { 70 desc: "globalWriteMultiToken", 71 unsequencedRows: 10, 72 maxUnsequencedRows: 20, 73 numTokens: 5, 74 specs: []quota.Spec{{Group: quota.Global, Kind: quota.Write}}, 75 }, 76 { 77 desc: "globalWriteOverQuota1", 78 unsequencedRows: 20, 79 maxUnsequencedRows: 20, 80 numTokens: 1, 81 specs: []quota.Spec{{Group: quota.Global, Kind: quota.Write}}, 82 wantErr: true, 83 }, 84 { 85 desc: "globalWriteOverQuota2", 86 unsequencedRows: 15, 87 maxUnsequencedRows: 20, 88 numTokens: 10, 89 specs: []quota.Spec{{Group: quota.Global, Kind: quota.Write}}, 90 wantErr: true, 91 }, 92 { 93 desc: "unlimitedQuotas", 94 numTokens: 10, 95 specs: []quota.Spec{ 96 {Group: quota.User, Kind: quota.Read, User: user}, 97 {Group: quota.Tree, Kind: quota.Read, TreeID: tree.TreeId}, 98 {Group: quota.Global, Kind: quota.Read}, 99 {Group: quota.User, Kind: quota.Write, User: user}, 100 {Group: quota.Tree, Kind: quota.Write, TreeID: tree.TreeId}, 101 }, 102 }, 103 } 104 105 for _, test := range tests { 106 if err := setUnsequencedRows(ctx, db, tree, test.unsequencedRows); err != nil { 107 t.Errorf("setUnsequencedRows() returned err = %v", err) 108 continue 109 } 110 111 // Test general cases using select count(*) to avoid flakiness / allow for more 112 // precise assertions. 113 // See TestQuotaManager_GetTokens_InformationSchema for information schema tests. 114 qm := &mysqlqm.QuotaManager{DB: db, MaxUnsequencedRows: test.maxUnsequencedRows, UseSelectCount: true} 115 err := qm.GetTokens(ctx, test.numTokens, test.specs) 116 if hasErr := err == mysqlqm.ErrTooManyUnsequencedRows; hasErr != test.wantErr { 117 t.Errorf("%v: GetTokens() returned err = %q, wantErr = %v", test.desc, err, test.wantErr) 118 } 119 } 120 } 121 122 func TestQuotaManager_GetTokens_InformationSchema(t *testing.T) { 123 testdb.SkipIfNoMySQL(t) 124 ctx := context.Background() 125 126 maxUnsequenced := 20 127 globalWriteSpec := []quota.Spec{{Group: quota.Global, Kind: quota.Write}} 128 129 // Make both variants go through the test. 130 tests := []struct { 131 useSelectCount bool 132 }{ 133 {useSelectCount: true}, 134 {useSelectCount: false}, 135 } 136 for _, test := range tests { 137 desc := fmt.Sprintf("useSelectCount = %v", test.useSelectCount) 138 t.Run(desc, func(t *testing.T) { 139 db, err := testdb.NewTrillianDB(ctx) 140 if err != nil { 141 t.Fatalf("NewTrillianDB() returned err = %v", err) 142 } 143 defer db.Close() 144 145 tree, err := createTree(ctx, db) 146 if err != nil { 147 t.Fatalf("createTree() returned err = %v", err) 148 } 149 150 qm := &mysqlqm.QuotaManager{DB: db, MaxUnsequencedRows: maxUnsequenced, UseSelectCount: test.useSelectCount} 151 152 // All GetTokens() calls where leaves < maxUnsequenced should succeed: 153 // information_schema may be outdated, but it should refer to a valid point in the 154 // past. 155 for i := 0; i < maxUnsequenced-1; i++ { 156 if err := queueLeaves(ctx, db, tree, i /* firstID */, 1 /* num */); err != nil { 157 t.Fatalf("queueLeaves() returned err = %v", err) 158 } 159 if err := qm.GetTokens(ctx, 1 /* numTokens */, globalWriteSpec); err != nil { 160 t.Errorf("GetTokens() returned err = %v (%v leaves)", err, i+1) 161 } 162 } 163 164 // Make leaves = maxUnsequenced 165 if err := queueLeaves(ctx, db, tree, maxUnsequenced-1 /* firstID */, 1 /* num */); err != nil { 166 t.Fatalf("queueLeaves() returned err = %v", err) 167 } 168 169 // Allow some time for information_schema to "catch up". 170 stop := false 171 timeout := time.After(1 * time.Second) 172 for !stop { 173 select { 174 case <-timeout: 175 t.Errorf("timed out") 176 stop = true 177 default: 178 // An error means that GetTokens is working correctly 179 stop = qm.GetTokens(ctx, 1 /* numTokens */, globalWriteSpec) == mysqlqm.ErrTooManyUnsequencedRows 180 } 181 } 182 }) 183 } 184 } 185 186 func TestQuotaManager_PeekTokens(t *testing.T) { 187 testdb.SkipIfNoMySQL(t) 188 ctx := context.Background() 189 190 db, err := testdb.NewTrillianDB(ctx) 191 if err != nil { 192 t.Fatalf("GetTestDB() returned err = %v", err) 193 } 194 defer db.Close() 195 196 tree, err := createTree(ctx, db) 197 if err != nil { 198 t.Fatalf("createTree() returned err = %v", err) 199 } 200 201 unsequencedRows := 10 202 maxUnsequencedRows := 1000 203 wantRows := maxUnsequencedRows - unsequencedRows 204 if err := setUnsequencedRows(ctx, db, tree, unsequencedRows); err != nil { 205 t.Fatalf("setUnsequencedRows() returned err = %v", err) 206 } 207 208 // Test using select count(*) to allow for precise assertions without flakiness. 209 qm := &mysqlqm.QuotaManager{DB: db, MaxUnsequencedRows: maxUnsequencedRows, UseSelectCount: true} 210 specs := allSpecs(ctx, qm, tree.TreeId) 211 tokens, err := qm.PeekTokens(ctx, specs) 212 if err != nil { 213 t.Fatalf("PeekTokens() returned err = %v", err) 214 } 215 216 // All specs but Global/Write are infinite 217 wantTokens := make(map[quota.Spec]int) 218 for _, spec := range specs { 219 wantTokens[spec] = quota.MaxTokens 220 } 221 wantTokens[quota.Spec{Group: quota.Global, Kind: quota.Write}] = wantRows 222 223 if diff := pretty.Compare(tokens, wantTokens); diff != "" { 224 t.Errorf("post-PeekTokens() diff:\n%v", diff) 225 } 226 } 227 228 func TestQuotaManager_Noops(t *testing.T) { 229 testdb.SkipIfNoMySQL(t) 230 ctx := context.Background() 231 232 db, err := testdb.NewTrillianDB(ctx) 233 if err != nil { 234 t.Fatalf("GetTestDB() returned err = %v", err) 235 } 236 defer db.Close() 237 238 qm := &mysqlqm.QuotaManager{DB: db, MaxUnsequencedRows: 1000} 239 specs := allSpecs(ctx, qm, 10 /* treeID */) 240 241 tests := []struct { 242 desc string 243 fn func() error 244 }{ 245 { 246 desc: "GetUser", 247 fn: func() error { 248 _ = qm.GetUser(ctx, nil /* req */) 249 return nil 250 }, 251 }, 252 { 253 desc: "PutTokens", 254 fn: func() error { 255 return qm.PutTokens(ctx, 10 /* numTokens */, specs) 256 }, 257 }, 258 { 259 desc: "ResetQuota", 260 fn: func() error { 261 return qm.ResetQuota(ctx, specs) 262 }, 263 }, 264 } 265 for _, test := range tests { 266 if err := test.fn(); err != nil { 267 t.Errorf("%v: got err = %v", test.desc, err) 268 } 269 } 270 } 271 272 func allSpecs(ctx context.Context, qm quota.Manager, treeID int64) []quota.Spec { 273 user := qm.GetUser(ctx, nil /* req */) 274 return []quota.Spec{ 275 {Group: quota.User, Kind: quota.Read, User: user}, 276 {Group: quota.Tree, Kind: quota.Read, TreeID: treeID}, 277 {Group: quota.Global, Kind: quota.Read}, 278 {Group: quota.User, Kind: quota.Write, User: user}, 279 {Group: quota.Tree, Kind: quota.Write, TreeID: treeID}, 280 {Group: quota.Global, Kind: quota.Write}, 281 } 282 } 283 284 func countUnsequenced(ctx context.Context, db *sql.DB) (int, error) { 285 var count int 286 if err := db.QueryRowContext(ctx, "SELECT COUNT(*) FROM Unsequenced").Scan(&count); err != nil { 287 return 0, err 288 } 289 return count, nil 290 } 291 292 func createTree(ctx context.Context, db *sql.DB) (*trillian.Tree, error) { 293 var tree *trillian.Tree 294 295 { 296 as := mysql.NewAdminStorage(db) 297 err := as.ReadWriteTransaction(ctx, func(ctx context.Context, tx storage.AdminTX) error { 298 var err error 299 tree, err = tx.CreateTree(ctx, stestonly.LogTree) 300 return err 301 }) 302 if err != nil { 303 return nil, err 304 } 305 } 306 307 { 308 ls := mysql.NewLogStorage(db, nil) 309 err := ls.ReadWriteTransaction(ctx, tree, func(ctx context.Context, tx storage.LogTreeTX) error { 310 signer := tcrypto.NewSigner(0, testonly.NewSignerWithFixedSig(nil, []byte("notempty")), crypto.SHA256) 311 slr, err := signer.SignLogRoot(&types.LogRootV1{RootHash: []byte{0}}) 312 if err != nil { 313 return err 314 } 315 return tx.StoreSignedLogRoot(ctx, *slr) 316 }) 317 if err != nil { 318 return nil, err 319 } 320 } 321 322 return tree, nil 323 } 324 325 func queueLeaves(ctx context.Context, db *sql.DB, tree *trillian.Tree, firstID, num int) error { 326 hasherFn, err := trees.Hash(tree) 327 if err != nil { 328 return err 329 } 330 hasher := hasherFn.New() 331 332 leaves := []*trillian.LogLeaf{} 333 for i := 0; i < num; i++ { 334 value := []byte(fmt.Sprintf("leaf-%v", firstID+i)) 335 hasher.Reset() 336 if _, err := hasher.Write(value); err != nil { 337 return err 338 } 339 hash := hasher.Sum(nil) 340 leaves = append(leaves, &trillian.LogLeaf{ 341 MerkleLeafHash: hash, 342 LeafValue: value, 343 ExtraData: []byte("extra data"), 344 LeafIdentityHash: hash, 345 }) 346 } 347 348 ls := mysql.NewLogStorage(db, nil) 349 return ls.ReadWriteTransaction(ctx, tree, func(ctx context.Context, tx storage.LogTreeTX) error { 350 _, err := tx.QueueLeaves(ctx, leaves, time.Now()) 351 return err 352 }) 353 } 354 355 func setUnsequencedRows(ctx context.Context, db *sql.DB, tree *trillian.Tree, wantRows int) error { 356 count, err := countUnsequenced(ctx, db) 357 if err != nil { 358 return err 359 } 360 if count == wantRows { 361 return nil 362 } 363 364 // Clear the tables and re-create leaves from scratch. It's easier than having to reason 365 // about duplicate entries. 366 if _, err := db.ExecContext(ctx, "DELETE FROM LeafData"); err != nil { 367 return err 368 } 369 if _, err := db.ExecContext(ctx, "DELETE FROM Unsequenced"); err != nil { 370 return err 371 } 372 if err := queueLeaves(ctx, db, tree, 0 /* firstID */, wantRows); err != nil { 373 return err 374 } 375 376 // Sanity check the final count 377 count, err = countUnsequenced(ctx, db) 378 if err != nil { 379 return err 380 } 381 if count != wantRows { 382 return fmt.Errorf("got %v unsequenced rows, want = %v", count, wantRows) 383 } 384 385 return nil 386 }