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  }