github.com/letsencrypt/boulder@v0.20251208.0/db/map_test.go (about) 1 package db 2 3 import ( 4 "context" 5 "database/sql" 6 "errors" 7 "fmt" 8 "testing" 9 10 "github.com/letsencrypt/borp" 11 12 "github.com/go-sql-driver/mysql" 13 14 "github.com/letsencrypt/boulder/core" 15 "github.com/letsencrypt/boulder/test" 16 "github.com/letsencrypt/boulder/test/vars" 17 ) 18 19 func TestErrDatabaseOpError(t *testing.T) { 20 testErr := errors.New("computers are cancelled") 21 testCases := []struct { 22 name string 23 err error 24 expected string 25 }{ 26 { 27 name: "error with table", 28 err: ErrDatabaseOp{ 29 Op: "test", 30 Table: "testTable", 31 Err: testErr, 32 }, 33 expected: fmt.Sprintf("failed to test testTable: %s", testErr), 34 }, 35 { 36 name: "error with no table", 37 err: ErrDatabaseOp{ 38 Op: "test", 39 Err: testErr, 40 }, 41 expected: fmt.Sprintf("failed to test: %s", testErr), 42 }, 43 } 44 45 for _, tc := range testCases { 46 t.Run(tc.name, func(t *testing.T) { 47 test.AssertEquals(t, tc.err.Error(), tc.expected) 48 }) 49 } 50 } 51 52 func TestIsNoRows(t *testing.T) { 53 testCases := []struct { 54 name string 55 err ErrDatabaseOp 56 expectedNoRows bool 57 }{ 58 { 59 name: "underlying err is sql.ErrNoRows", 60 err: ErrDatabaseOp{ 61 Op: "test", 62 Table: "testTable", 63 Err: fmt.Errorf("some wrapper around %w", sql.ErrNoRows), 64 }, 65 expectedNoRows: true, 66 }, 67 { 68 name: "underlying err is not sql.ErrNoRows", 69 err: ErrDatabaseOp{ 70 Op: "test", 71 Table: "testTable", 72 Err: fmt.Errorf("some wrapper around %w", errors.New("lots of rows. too many rows.")), 73 }, 74 expectedNoRows: false, 75 }, 76 } 77 78 for _, tc := range testCases { 79 t.Run(tc.name, func(t *testing.T) { 80 test.AssertEquals(t, IsNoRows(tc.err), tc.expectedNoRows) 81 }) 82 } 83 } 84 85 func TestIsDuplicate(t *testing.T) { 86 testCases := []struct { 87 name string 88 err ErrDatabaseOp 89 expectDuplicate bool 90 }{ 91 { 92 name: "underlying err has duplicate prefix", 93 err: ErrDatabaseOp{ 94 Op: "test", 95 Table: "testTable", 96 Err: fmt.Errorf("some wrapper around %w", &mysql.MySQLError{Number: 1062}), 97 }, 98 expectDuplicate: true, 99 }, 100 { 101 name: "underlying err doesn't have duplicate prefix", 102 err: ErrDatabaseOp{ 103 Op: "test", 104 Table: "testTable", 105 Err: fmt.Errorf("some wrapper around %w", &mysql.MySQLError{Number: 1234}), 106 }, 107 expectDuplicate: false, 108 }, 109 } 110 111 for _, tc := range testCases { 112 t.Run(tc.name, func(t *testing.T) { 113 test.AssertEquals(t, IsDuplicate(tc.err), tc.expectDuplicate) 114 }) 115 } 116 } 117 118 func TestTableFromQuery(t *testing.T) { 119 // A sample of example queries logged by the SA during Boulder 120 // unit/integration tests. 121 testCases := []struct { 122 query string 123 expectedTable string 124 }{ 125 { 126 query: "SELECT id, jwk, jwk_sha256, contact, agreement, createdAt, status FROM registrations WHERE jwk_sha256 = ?", 127 expectedTable: "registrations", 128 }, 129 { 130 query: "\n\t\t\t\t\tSELECT orderID, registrationID\n\t\t\t\t\tFROM orderFqdnSets\n\t\t\t\t\tWHERE setHash = ?\n\t\t\t\t\tAND expires > ?\n\t\t\t\t\tORDER BY expires ASC\n\t\t\t\t\tLIMIT 1", 131 expectedTable: "orderFqdnSets", 132 }, 133 { 134 query: "SELECT id, identifierType, identifierValue, registrationID, status, expires, challenges, attempted, token, validationError, validationRecord FROM authz2 WHERE\n\t\t\tregistrationID = :regID AND\n\t\t\tstatus = :status AND\n\t\t\texpires > :validUntil AND\n\t\t\tidentifierType = :dnsType AND\n\t\t\tidentifierValue = :ident\n\t\t\tORDER BY expires ASC\n\t\t\tLIMIT 1 ", 135 expectedTable: "authz2", 136 }, 137 { 138 query: "insert into `registrations` (`id`,`jwk`,`jwk_sha256`,`contact`,`agreement`,`createdAt`,`status`) values (null,?,?,?,?,?,?,?);", 139 expectedTable: "`registrations`", 140 }, 141 { 142 query: "update `registrations` set `jwk`=?, `jwk_sha256`=?, `contact`=?, `agreement`=?, `createdAt`=?, `status`=? where `id`=?;", 143 expectedTable: "`registrations`", 144 }, 145 { 146 query: "SELECT COUNT(*) FROM registrations WHERE ? < createdAt AND createdAt <= ?", 147 expectedTable: "registrations", 148 }, 149 { 150 query: "SELECT COUNT(*) FROM orders WHERE registrationID = ? AND created >= ? AND created < ?", 151 expectedTable: "orders", 152 }, 153 { 154 query: " SELECT id, identifierType, identifierValue, registrationID, status, expires, challenges, attempted, token, validationError, validationRecord FROM authz2 WHERE registrationID = ? AND status IN (?,?) AND expires > ? AND identifierType = ? AND identifierValue IN (?)", 155 expectedTable: "authz2", 156 }, 157 { 158 query: "insert into `authz2` (`id`,`identifierType`,`identifierValue`,`registrationID`,`status`,`expires`,`challenges`,`attempted`,`token`,`validationError`,`validationRecord`) values (null,?,?,?,?,?,?,?,?,?,?);", 159 expectedTable: "`authz2`", 160 }, 161 { 162 query: "insert into `orders` (`ID`,`RegistrationID`,`Expires`,`Created`,`Error`,`CertificateSerial`,`BeganProcessing`) values (null,?,?,?,?,?,?)", 163 expectedTable: "`orders`", 164 }, 165 { 166 query: "insert into `orderToAuthz2` (`OrderID`,`AuthzID`) values (?,?);", 167 expectedTable: "`orderToAuthz2`", 168 }, 169 { 170 query: "UPDATE authz2 SET status = :status, attempted = :attempted, validationRecord = :validationRecord, validationError = :validationError, expires = :expires WHERE id = :id AND status = :pending", 171 expectedTable: "authz2", 172 }, 173 { 174 query: "insert into `precertificates` (`ID`,`Serial`,`RegistrationID`,`DER`,`Issued`,`Expires`) values (null,?,?,?,?,?);", 175 expectedTable: "`precertificates`", 176 }, 177 { 178 query: "INSERT INTO certificateStatus (serial, status, ocspLastUpdated, revokedDate, revokedReason, lastExpirationNagSent, ocspResponse, notAfter, isExpired, issuerID) VALUES (?,?,?,?,?,?,?,?,?,?)", 179 expectedTable: "certificateStatus", 180 }, 181 { 182 query: "INSERT INTO issuedNames (reversedName, serial, notBefore, renewal) VALUES (?, ?, ?, ?);", 183 expectedTable: "issuedNames", 184 }, 185 { 186 query: "insert into `certificates` (`registrationID`,`serial`,`digest`,`der`,`issued`,`expires`) values (?,?,?,?,?,?);", 187 expectedTable: "`certificates`", 188 }, 189 { 190 query: "insert into `fqdnSets` (`ID`,`SetHash`,`Serial`,`Issued`,`Expires`) values (null,?,?,?,?);", 191 expectedTable: "`fqdnSets`", 192 }, 193 { 194 query: "UPDATE orders SET certificateSerial = ? WHERE id = ? AND beganProcessing = true", 195 expectedTable: "orders", 196 }, 197 { 198 query: "DELETE FROM orderFqdnSets WHERE orderID = ?", 199 expectedTable: "orderFqdnSets", 200 }, 201 { 202 query: "insert into `serials` (`ID`,`Serial`,`RegistrationID`,`Created`,`Expires`) values (null,?,?,?,?);", 203 expectedTable: "`serials`", 204 }, 205 { 206 query: "UPDATE orders SET beganProcessing = ? WHERE id = ? AND beganProcessing = ?", 207 expectedTable: "orders", 208 }, 209 } 210 211 for i, tc := range testCases { 212 t.Run(fmt.Sprintf("testCases.%d", i), func(t *testing.T) { 213 table := tableFromQuery(tc.query) 214 test.AssertEquals(t, table, tc.expectedTable) 215 }) 216 } 217 } 218 219 func testDbMap(t *testing.T) *WrappedMap { 220 // NOTE(@cpu): We avoid using sa.NewDBMapFromConfig here because it would 221 // create a cyclic dependency. The `sa` package depends on `db` for 222 // `WithTransaction`. The `db` package can't depend on the `sa` for creating 223 // a DBMap. Since we only need a map for simple unit tests we can make our 224 // own dbMap by hand (how artisanal). 225 var config *mysql.Config 226 config, err := mysql.ParseDSN(vars.DBConnSA) 227 test.AssertNotError(t, err, "parsing DBConnSA DSN") 228 229 dbConn, err := sql.Open("mysql", config.FormatDSN()) 230 test.AssertNotError(t, err, "opening DB connection") 231 232 dialect := borp.MySQLDialect{Engine: "InnoDB", Encoding: "UTF8"} 233 // NOTE(@cpu): We avoid giving a sa.BoulderTypeConverter to the DbMap field to 234 // avoid the cyclic dep. We don't need to convert any types in the db tests. 235 dbMap := &borp.DbMap{Db: dbConn, Dialect: dialect, TypeConverter: nil} 236 return &WrappedMap{dbMap: dbMap} 237 } 238 239 func TestWrappedMap(t *testing.T) { 240 mustDbErr := func(err error) ErrDatabaseOp { 241 t.Helper() 242 var dbOpErr ErrDatabaseOp 243 test.AssertErrorWraps(t, err, &dbOpErr) 244 return dbOpErr 245 } 246 247 ctx := context.Background() 248 249 testWrapper := func(dbMap Executor) { 250 reg := &core.Registration{} 251 252 // Test wrapped Get 253 _, err := dbMap.Get(ctx, reg) 254 test.AssertError(t, err, "expected err Getting Registration w/o type converter") 255 dbOpErr := mustDbErr(err) 256 test.AssertEquals(t, dbOpErr.Op, "get") 257 test.AssertEquals(t, dbOpErr.Table, "*core.Registration") 258 test.AssertError(t, dbOpErr.Err, "expected non-nil underlying err") 259 260 // Test wrapped Insert 261 err = dbMap.Insert(ctx, reg) 262 test.AssertError(t, err, "expected err Inserting Registration w/o type converter") 263 dbOpErr = mustDbErr(err) 264 test.AssertEquals(t, dbOpErr.Op, "insert") 265 test.AssertEquals(t, dbOpErr.Table, "*core.Registration") 266 test.AssertError(t, dbOpErr.Err, "expected non-nil underlying err") 267 268 // Test wrapped Update 269 _, err = dbMap.Update(ctx, reg) 270 test.AssertError(t, err, "expected err Updating Registration w/o type converter") 271 dbOpErr = mustDbErr(err) 272 test.AssertEquals(t, dbOpErr.Op, "update") 273 test.AssertEquals(t, dbOpErr.Table, "*core.Registration") 274 test.AssertError(t, dbOpErr.Err, "expected non-nil underlying err") 275 276 // Test wrapped Delete 277 _, err = dbMap.Delete(ctx, reg) 278 test.AssertError(t, err, "expected err Deleting Registration w/o type converter") 279 dbOpErr = mustDbErr(err) 280 test.AssertEquals(t, dbOpErr.Op, "delete") 281 test.AssertEquals(t, dbOpErr.Table, "*core.Registration") 282 test.AssertError(t, dbOpErr.Err, "expected non-nil underlying err") 283 284 // Test wrapped Select with a bogus query 285 _, err = dbMap.Select(ctx, reg, "blah") 286 test.AssertError(t, err, "expected err Selecting Registration w/o type converter") 287 dbOpErr = mustDbErr(err) 288 test.AssertEquals(t, dbOpErr.Op, "select") 289 test.AssertEquals(t, dbOpErr.Table, "*core.Registration (unknown table)") 290 test.AssertError(t, dbOpErr.Err, "expected non-nil underlying err") 291 292 // Test wrapped Select with a valid query 293 _, err = dbMap.Select(ctx, reg, "SELECT id, contact FROM registrationzzz WHERE id > 1;") 294 test.AssertError(t, err, "expected err Selecting Registration w/o type converter") 295 dbOpErr = mustDbErr(err) 296 test.AssertEquals(t, dbOpErr.Op, "select") 297 test.AssertEquals(t, dbOpErr.Table, "registrationzzz") 298 test.AssertError(t, dbOpErr.Err, "expected non-nil underlying err") 299 300 // Test wrapped SelectOne with a bogus query 301 err = dbMap.SelectOne(ctx, reg, "blah") 302 test.AssertError(t, err, "expected err SelectOne-ing Registration w/o type converter") 303 dbOpErr = mustDbErr(err) 304 test.AssertEquals(t, dbOpErr.Op, "select one") 305 test.AssertEquals(t, dbOpErr.Table, "*core.Registration (unknown table)") 306 test.AssertError(t, dbOpErr.Err, "expected non-nil underlying err") 307 308 // Test wrapped SelectOne with a valid query 309 err = dbMap.SelectOne(ctx, reg, "SELECT contact FROM doesNotExist WHERE id=1;") 310 test.AssertError(t, err, "expected err SelectOne-ing Registration w/o type converter") 311 dbOpErr = mustDbErr(err) 312 test.AssertEquals(t, dbOpErr.Op, "select one") 313 test.AssertEquals(t, dbOpErr.Table, "doesNotExist") 314 test.AssertError(t, dbOpErr.Err, "expected non-nil underlying err") 315 316 // Test wrapped Exec 317 _, err = dbMap.ExecContext(ctx, "INSERT INTO whatever (id) VALUES (?) WHERE id = ?", 10) 318 test.AssertError(t, err, "expected err Exec-ing bad query") 319 dbOpErr = mustDbErr(err) 320 test.AssertEquals(t, dbOpErr.Op, "exec") 321 test.AssertEquals(t, dbOpErr.Table, "whatever") 322 test.AssertError(t, dbOpErr.Err, "expected non-nil underlying err") 323 } 324 325 // Create a test wrapped map. It won't have a type converted registered. 326 dbMap := testDbMap(t) 327 328 // A top level WrappedMap should operate as expected with respect to wrapping 329 // database errors. 330 testWrapper(dbMap) 331 332 // Using Begin to start a transaction with the dbMap should return a 333 // transaction that continues to operate in the expected fashion. 334 tx, err := dbMap.BeginTx(ctx) 335 defer func() { _ = tx.Rollback() }() 336 test.AssertNotError(t, err, "unexpected error beginning transaction") 337 testWrapper(tx) 338 }