From 723c9c3d60c1e79519387012f83660f17adf9f4a Mon Sep 17 00:00:00 2001 From: Mihaela Balutoiu Date: Wed, 21 Sep 2022 21:19:25 +0300 Subject: [PATCH] Add `instances.go` database unit tests Add more test cases regarding the SQL database interactions. Moreover, add `go-sqlmock` dependency used to mock SQL DB instructions. Signed-off-by: Mihaela Balutoiu --- database/sql/instances_test.go | 350 +++++++++-- go.mod | 1 + go.sum | 2 + .../DATA-DOG/go-sqlmock.v1/.gitignore | 3 + .../DATA-DOG/go-sqlmock.v1/.travis.yml | 19 + .../gopkg.in/DATA-DOG/go-sqlmock.v1/LICENSE | 28 + .../gopkg.in/DATA-DOG/go-sqlmock.v1/README.md | 229 ++++++++ .../DATA-DOG/go-sqlmock.v1/argument.go | 24 + .../gopkg.in/DATA-DOG/go-sqlmock.v1/driver.go | 78 +++ .../DATA-DOG/go-sqlmock.v1/expectations.go | 353 ++++++++++++ .../go-sqlmock.v1/expectations_before_go18.go | 52 ++ .../go-sqlmock.v1/expectations_go18.go | 66 +++ .../gopkg.in/DATA-DOG/go-sqlmock.v1/result.go | 39 ++ .../gopkg.in/DATA-DOG/go-sqlmock.v1/rows.go | 157 +++++ .../DATA-DOG/go-sqlmock.v1/rows_go18.go | 20 + .../DATA-DOG/go-sqlmock.v1/sqlmock.go | 542 ++++++++++++++++++ .../DATA-DOG/go-sqlmock.v1/sqlmock_go18.go | 101 ++++ .../DATA-DOG/go-sqlmock.v1/statement.go | 28 + .../gopkg.in/DATA-DOG/go-sqlmock.v1/util.go | 13 + vendor/modules.txt | 3 + 20 files changed, 2064 insertions(+), 44 deletions(-) create mode 100644 vendor/gopkg.in/DATA-DOG/go-sqlmock.v1/.gitignore create mode 100644 vendor/gopkg.in/DATA-DOG/go-sqlmock.v1/.travis.yml create mode 100644 vendor/gopkg.in/DATA-DOG/go-sqlmock.v1/LICENSE create mode 100644 vendor/gopkg.in/DATA-DOG/go-sqlmock.v1/README.md create mode 100644 vendor/gopkg.in/DATA-DOG/go-sqlmock.v1/argument.go create mode 100644 vendor/gopkg.in/DATA-DOG/go-sqlmock.v1/driver.go create mode 100644 vendor/gopkg.in/DATA-DOG/go-sqlmock.v1/expectations.go create mode 100644 vendor/gopkg.in/DATA-DOG/go-sqlmock.v1/expectations_before_go18.go create mode 100644 vendor/gopkg.in/DATA-DOG/go-sqlmock.v1/expectations_go18.go create mode 100644 vendor/gopkg.in/DATA-DOG/go-sqlmock.v1/result.go create mode 100644 vendor/gopkg.in/DATA-DOG/go-sqlmock.v1/rows.go create mode 100644 vendor/gopkg.in/DATA-DOG/go-sqlmock.v1/rows_go18.go create mode 100644 vendor/gopkg.in/DATA-DOG/go-sqlmock.v1/sqlmock.go create mode 100644 vendor/gopkg.in/DATA-DOG/go-sqlmock.v1/sqlmock_go18.go create mode 100644 vendor/gopkg.in/DATA-DOG/go-sqlmock.v1/statement.go create mode 100644 vendor/gopkg.in/DATA-DOG/go-sqlmock.v1/util.go diff --git a/database/sql/instances_test.go b/database/sql/instances_test.go index a20962a0..88278774 100644 --- a/database/sql/instances_test.go +++ b/database/sql/instances_test.go @@ -16,27 +16,38 @@ package sql import ( "context" + "flag" "fmt" dbCommon "garm/database/common" garmTesting "garm/internal/testing" "garm/params" "garm/runner/providers/common" + "regexp" "sort" "testing" + "gopkg.in/DATA-DOG/go-sqlmock.v1" + "github.com/stretchr/testify/suite" + "gorm.io/driver/mysql" + "gorm.io/gorm" + "gorm.io/gorm/logger" ) type InstancesTestFixtures struct { - Org params.Organization - Pool params.Pool - Instances []params.Instance + Org params.Organization + Pool params.Pool + Instances []params.Instance + CreateInstanceParams params.CreateInstanceParams + UpdateInstanceParams params.UpdateInstanceParams + SQLMock sqlmock.Sqlmock } type InstancesTestSuite struct { suite.Suite - Store dbCommon.Store - Fixtures *InstancesTestFixtures + Store dbCommon.Store + StoreSQLMocked *sqlDatabase + Fixtures *InstancesTestFixtures } func (s *InstancesTestSuite) equalInstancesByName(expected, actual []params.Instance) { @@ -50,6 +61,13 @@ func (s *InstancesTestSuite) equalInstancesByName(expected, actual []params.Inst } } +func (s *InstancesTestSuite) assertSQLMockExpectations() { + err := s.Fixtures.SQLMock.ExpectationsWereMet() + if err != nil { + s.FailNow(fmt.Sprintf("failed to meet sqlmock expectations, got error: %v", err)) + } +} + func (s *InstancesTestSuite) SetupTest() { // create testing sqlite database db, err := NewSQLDatabase(context.Background(), garmTesting.GetTestSqliteDBConfig(s.T())) @@ -100,31 +118,70 @@ func (s *InstancesTestSuite) SetupTest() { instances = append(instances, instance) } + // create store with mocked sql connection + sqlDB, sqlMock, err := sqlmock.New() + if err != nil { + s.FailNow(fmt.Sprintf("failed to run 'sqlmock.New()', got error: %v", err)) + } + s.T().Cleanup(func() { sqlDB.Close() }) + mysqlConfig := mysql.Config{ + Conn: sqlDB, + SkipInitializeWithVersion: true, + } + gormConfig := &gorm.Config{} + if flag.Lookup("test.v").Value.String() == "false" { + gormConfig.Logger = logger.Default.LogMode(logger.Silent) + } + gormConn, err := gorm.Open(mysql.New(mysqlConfig), gormConfig) + if err != nil { + s.FailNow(fmt.Sprintf("fail to open gorm connection: %v", err)) + } + s.StoreSQLMocked = &sqlDatabase{ + conn: gormConn, + } + // setup test fixtures fixtures := &InstancesTestFixtures{ Org: org, Pool: pool, Instances: instances, + CreateInstanceParams: params.CreateInstanceParams{ + Name: "test-create-instance", + OSType: "linux", + OSArch: "amd64", + CallbackURL: "https://garm.example.com/", + }, + UpdateInstanceParams: params.UpdateInstanceParams{ + ProviderID: "update-provider-test", + OSName: "ubuntu", + OSVersion: "focal", + Status: common.InstancePendingDelete, + RunnerStatus: common.RunnerActive, + AgentID: 4, + CreateAttempt: 3, + Addresses: []params.Address{ + { + Address: "12.10.12.10", + Type: params.PublicAddress, + }, + { + Address: "10.1.1.2", + Type: params.PrivateAddress, + }, + }, + }, + SQLMock: sqlMock, } s.Fixtures = fixtures } func (s *InstancesTestSuite) TestCreateInstance() { - // setup enviroment for this test - instanceName := "test-create-instance" - createInstanceParams := params.CreateInstanceParams{ - Name: instanceName, - OSType: "linux", - OSArch: "amd64", - CallbackURL: "https://garm.example.com/", - } - // call tested function - instance, err := s.Store.CreateInstance(context.Background(), s.Fixtures.Pool.ID, createInstanceParams) + instance, err := s.Store.CreateInstance(context.Background(), s.Fixtures.Pool.ID, s.Fixtures.CreateInstanceParams) // assertions s.Require().Nil(err) - storeInstance, err := s.Store.GetInstanceByName(context.Background(), instanceName) + storeInstance, err := s.Store.GetInstanceByName(context.Background(), s.Fixtures.CreateInstanceParams.Name) if err != nil { s.FailNow(fmt.Sprintf("failed to get instance: %v", err)) } @@ -141,6 +198,29 @@ func (s *InstancesTestSuite) TestCreateInstanceInvalidPoolID() { s.Require().Equal("fetching pool: parsing id: invalid request", err.Error()) } +func (s *InstancesTestSuite) TestCreateInstanceDBCreateErr() { + pool := s.Fixtures.Pool + + s.Fixtures.SQLMock. + ExpectQuery(regexp.QuoteMeta("SELECT * FROM `pools` WHERE id = ? AND `pools`.`deleted_at` IS NULL ORDER BY `pools`.`id` LIMIT 1")). + WithArgs(pool.ID). + WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(pool.ID)) + s.Fixtures.SQLMock.ExpectBegin() + s.Fixtures.SQLMock. + ExpectExec("INSERT INTO `pools`"). + WillReturnResult(sqlmock.NewResult(1, 1)) + s.Fixtures.SQLMock. + ExpectExec("INSERT INTO `instances`"). + WillReturnError(fmt.Errorf("mocked insert instance error")) + s.Fixtures.SQLMock.ExpectRollback() + + _, err := s.StoreSQLMocked.CreateInstance(context.Background(), pool.ID, s.Fixtures.CreateInstanceParams) + + s.assertSQLMockExpectations() + s.Require().NotNil(err) + s.Require().Equal("creating instance: mocked insert instance error", err.Error()) +} + func (s *InstancesTestSuite) TestGetPoolInstanceByName() { storeInstance := s.Fixtures.Instances[0] // this is already created in `SetupTest()` @@ -181,6 +261,7 @@ func (s *InstancesTestSuite) TestGetInstanceByNameFetchInstanceFailed() { func (s *InstancesTestSuite) TestDeleteInstance() { storeInstance := s.Fixtures.Instances[0] + err := s.Store.DeleteInstance(context.Background(), s.Fixtures.Pool.ID, storeInstance.Name) s.Require().Nil(err) @@ -195,6 +276,73 @@ func (s *InstancesTestSuite) TestDeleteInstanceInvalidPoolID() { s.Require().Equal("deleting instance: fetching pool: parsing id: invalid request", err.Error()) } +func (s *InstancesTestSuite) TestDeleteInstanceDBRecordNotFoundErr() { + pool := s.Fixtures.Pool + instance := s.Fixtures.Instances[0] + + s.Fixtures.SQLMock. + ExpectQuery(regexp.QuoteMeta("SELECT * FROM `pools` WHERE id = ? AND `pools`.`deleted_at` IS NULL ORDER BY `pools`.`id` LIMIT 1")). + WithArgs(pool.ID). + WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(pool.ID)) + s.Fixtures.SQLMock. + ExpectQuery(regexp.QuoteMeta("SELECT * FROM `instances` WHERE (name = ? and pool_id = ?) AND `instances`.`deleted_at` IS NULL ORDER BY `instances`.`id` LIMIT 1")). + WithArgs(instance.Name, pool.ID). + WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(instance.ID)) + s.Fixtures.SQLMock. + ExpectQuery(regexp.QuoteMeta("SELECT * FROM `addresses` WHERE `addresses`.`instance_id` = ? AND `addresses`.`deleted_at` IS NULL")). + WithArgs(instance.ID). + WillReturnRows(sqlmock.NewRows([]string{"address", "type", "instance_id"}).AddRow("10.10.1.10", "private", instance.ID)) + s.Fixtures.SQLMock. + ExpectQuery(regexp.QuoteMeta("SELECT * FROM `instance_status_updates` WHERE `instance_status_updates`.`instance_id` = ? AND `instance_status_updates`.`deleted_at` IS NULL")). + WithArgs(instance.ID). + WillReturnRows(sqlmock.NewRows([]string{"message", "instance_id"}).AddRow("instance sample message", instance.ID)) + s.Fixtures.SQLMock.ExpectBegin() + s.Fixtures.SQLMock. + ExpectExec(regexp.QuoteMeta("DELETE FROM `instances` WHERE `instances`.`id` = ?")). + WithArgs(instance.ID). + WillReturnError(gorm.ErrRecordNotFound) + s.Fixtures.SQLMock.ExpectRollback() + + err := s.StoreSQLMocked.DeleteInstance(context.Background(), pool.ID, instance.Name) + + s.assertSQLMockExpectations() + s.Require().Nil(err) +} + +func (s *InstancesTestSuite) TestDeleteInstanceDBDeleteErr() { + pool := s.Fixtures.Pool + instance := s.Fixtures.Instances[0] + + s.Fixtures.SQLMock. + ExpectQuery(regexp.QuoteMeta("SELECT * FROM `pools` WHERE id = ? AND `pools`.`deleted_at` IS NULL ORDER BY `pools`.`id` LIMIT 1")). + WithArgs(pool.ID). + WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(pool.ID)) + s.Fixtures.SQLMock. + ExpectQuery(regexp.QuoteMeta("SELECT * FROM `instances` WHERE (name = ? and pool_id = ?) AND `instances`.`deleted_at` IS NULL ORDER BY `instances`.`id` LIMIT 1")). + WithArgs(instance.Name, pool.ID). + WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(instance.ID)) + s.Fixtures.SQLMock. + ExpectQuery(regexp.QuoteMeta("SELECT * FROM `addresses` WHERE `addresses`.`instance_id` = ? AND `addresses`.`deleted_at` IS NULL")). + WithArgs(instance.ID). + WillReturnRows(sqlmock.NewRows([]string{"address", "type", "instance_id"}).AddRow("12.10.12.13", "public", instance.ID)) + s.Fixtures.SQLMock. + ExpectQuery(regexp.QuoteMeta("SELECT * FROM `instance_status_updates` WHERE `instance_status_updates`.`instance_id` = ? AND `instance_status_updates`.`deleted_at` IS NULL")). + WithArgs(instance.ID). + WillReturnRows(sqlmock.NewRows([]string{"message", "instance_id"}).AddRow("instance sample message", instance.ID)) + s.Fixtures.SQLMock.ExpectBegin() + s.Fixtures.SQLMock. + ExpectExec(regexp.QuoteMeta("DELETE FROM `instances` WHERE `instances`.`id` = ?")). + WithArgs(instance.ID). + WillReturnError(fmt.Errorf("mocked delete instance error")) + s.Fixtures.SQLMock.ExpectRollback() + + err := s.StoreSQLMocked.DeleteInstance(context.Background(), pool.ID, instance.Name) + + s.assertSQLMockExpectations() + s.Require().NotNil(err) + s.Require().Equal("deleting instance: mocked delete instance error", err.Error()) +} + func (s *InstancesTestSuite) TestAddInstanceStatusMessage() { storeInstance := s.Fixtures.Instances[0] statusMsg := "test-status-message" @@ -216,37 +364,50 @@ func (s *InstancesTestSuite) TestAddInstanceStatusMessageInvalidPoolID() { s.Require().Equal("updating instance: parsing id: invalid request", err.Error()) } -func (s *InstancesTestSuite) TestUpdateInstance() { - updateInstanceParams := params.UpdateInstanceParams{ - ProviderID: "update-provider-test", - OSName: "ubuntu", - OSVersion: "focal", - Status: common.InstancePendingDelete, - RunnerStatus: common.RunnerActive, - AgentID: 4, - CreateAttempt: 3, - Addresses: []params.Address{ - { - Address: "12.10.12.10", - Type: params.PublicAddress, - }, - { - Address: "10.1.1.2", - Type: params.PrivateAddress, - }, - }, - } +func (s *InstancesTestSuite) TestAddInstanceStatusMessageDBUpdateErr() { + instance := s.Fixtures.Instances[0] + statusMsg := "test-status-message" - instance, err := s.Store.UpdateInstance(context.Background(), s.Fixtures.Instances[0].ID, updateInstanceParams) + s.Fixtures.SQLMock. + ExpectQuery(regexp.QuoteMeta("SELECT * FROM `instances` WHERE id = ? AND `instances`.`deleted_at` IS NULL ORDER BY `instances`.`id` LIMIT 1")). + WithArgs(instance.ID). + WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(instance.ID)) + s.Fixtures.SQLMock. + ExpectQuery(regexp.QuoteMeta("SELECT * FROM `addresses` WHERE `addresses`.`instance_id` = ? AND `addresses`.`deleted_at` IS NULL")). + WithArgs(instance.ID). + WillReturnRows(sqlmock.NewRows([]string{"address", "type", "instance_id"}).AddRow("10.10.1.10", "private", instance.ID)) + s.Fixtures.SQLMock. + ExpectQuery(regexp.QuoteMeta("SELECT * FROM `instance_status_updates` WHERE `instance_status_updates`.`instance_id` = ? AND `instance_status_updates`.`deleted_at` IS NULL")). + WithArgs(instance.ID). + WillReturnRows(sqlmock.NewRows([]string{"message", "instance_id"}).AddRow("instance sample message", instance.ID)) + s.Fixtures.SQLMock.ExpectBegin() + s.Fixtures.SQLMock. + ExpectExec(regexp.QuoteMeta("UPDATE `instances` SET `updated_at`=? WHERE `instances`.`deleted_at` IS NULL AND `id` = ?")). + WithArgs(sqlmock.AnyArg(), instance.ID). + WillReturnResult(sqlmock.NewResult(1, 1)) + s.Fixtures.SQLMock. + ExpectExec(regexp.QuoteMeta("INSERT INTO `instance_status_updates`")). + WillReturnError(fmt.Errorf("mocked add status message error")) + s.Fixtures.SQLMock.ExpectRollback() + + err := s.StoreSQLMocked.AddInstanceStatusMessage(context.Background(), instance.ID, statusMsg) + + s.assertSQLMockExpectations() + s.Require().NotNil(err) + s.Require().Equal("adding status message: mocked add status message error", err.Error()) +} + +func (s *InstancesTestSuite) TestUpdateInstance() { + instance, err := s.Store.UpdateInstance(context.Background(), s.Fixtures.Instances[0].ID, s.Fixtures.UpdateInstanceParams) s.Require().Nil(err) - s.Require().Equal(updateInstanceParams.ProviderID, instance.ProviderID) - s.Require().Equal(updateInstanceParams.OSName, instance.OSName) - s.Require().Equal(updateInstanceParams.OSVersion, instance.OSVersion) - s.Require().Equal(updateInstanceParams.Status, instance.Status) - s.Require().Equal(updateInstanceParams.RunnerStatus, instance.RunnerStatus) - s.Require().Equal(updateInstanceParams.AgentID, instance.AgentID) - s.Require().Equal(updateInstanceParams.CreateAttempt, instance.CreateAttempt) + s.Require().Equal(s.Fixtures.UpdateInstanceParams.ProviderID, instance.ProviderID) + s.Require().Equal(s.Fixtures.UpdateInstanceParams.OSName, instance.OSName) + s.Require().Equal(s.Fixtures.UpdateInstanceParams.OSVersion, instance.OSVersion) + s.Require().Equal(s.Fixtures.UpdateInstanceParams.Status, instance.Status) + s.Require().Equal(s.Fixtures.UpdateInstanceParams.RunnerStatus, instance.RunnerStatus) + s.Require().Equal(s.Fixtures.UpdateInstanceParams.AgentID, instance.AgentID) + s.Require().Equal(s.Fixtures.UpdateInstanceParams.CreateAttempt, instance.CreateAttempt) } func (s *InstancesTestSuite) TestUpdateInstanceInvalidPoolID() { @@ -255,6 +416,76 @@ func (s *InstancesTestSuite) TestUpdateInstanceInvalidPoolID() { s.Require().Equal("updating instance: parsing id: invalid request", err.Error()) } +func (s *InstancesTestSuite) TestUpdateInstanceDBUpdateInstanceErr() { + instance := s.Fixtures.Instances[0] + + s.Fixtures.SQLMock. + ExpectQuery(regexp.QuoteMeta("SELECT * FROM `instances` WHERE id = ? AND `instances`.`deleted_at` IS NULL ORDER BY `instances`.`id` LIMIT 1")). + WithArgs(instance.ID). + WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(instance.ID)) + s.Fixtures.SQLMock. + ExpectQuery(regexp.QuoteMeta("SELECT * FROM `addresses` WHERE `addresses`.`instance_id` = ? AND `addresses`.`deleted_at` IS NULL")). + WithArgs(instance.ID). + WillReturnRows(sqlmock.NewRows([]string{"address", "type", "instance_id"}).AddRow("10.10.1.10", "private", instance.ID)) + s.Fixtures.SQLMock. + ExpectQuery(regexp.QuoteMeta("SELECT * FROM `instance_status_updates` WHERE `instance_status_updates`.`instance_id` = ? AND `instance_status_updates`.`deleted_at` IS NULL")). + WithArgs(instance.ID). + WillReturnRows(sqlmock.NewRows([]string{"message", "instance_id"}).AddRow("instance sample message", instance.ID)) + s.Fixtures.SQLMock.ExpectBegin() + s.Fixtures.SQLMock. + ExpectExec(("UPDATE `instances`")). + WillReturnError(fmt.Errorf("mocked update instance error")) + s.Fixtures.SQLMock.ExpectRollback() + + _, err := s.StoreSQLMocked.UpdateInstance(context.Background(), instance.ID, s.Fixtures.UpdateInstanceParams) + + s.assertSQLMockExpectations() + s.Require().NotNil(err) + s.Require().Equal("updating instance: mocked update instance error", err.Error()) +} + +func (s *InstancesTestSuite) TestUpdateInstanceDBUpdateAddressErr() { + instance := s.Fixtures.Instances[0] + + s.Fixtures.SQLMock. + ExpectQuery(regexp.QuoteMeta("SELECT * FROM `instances` WHERE id = ? AND `instances`.`deleted_at` IS NULL ORDER BY `instances`.`id` LIMIT 1")). + WithArgs(instance.ID). + WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(instance.ID)) + s.Fixtures.SQLMock. + ExpectQuery(regexp.QuoteMeta("SELECT * FROM `addresses` WHERE `addresses`.`instance_id` = ? AND `addresses`.`deleted_at` IS NULL")). + WithArgs(instance.ID). + WillReturnRows(sqlmock.NewRows([]string{"address", "type", "instance_id"}).AddRow("10.10.1.10", "private", instance.ID)) + s.Fixtures.SQLMock. + ExpectQuery(regexp.QuoteMeta("SELECT * FROM `instance_status_updates` WHERE `instance_status_updates`.`instance_id` = ? AND `instance_status_updates`.`deleted_at` IS NULL")). + WithArgs(instance.ID). + WillReturnRows(sqlmock.NewRows([]string{"message", "instance_id"}).AddRow("instance sample message", instance.ID)) + s.Fixtures.SQLMock.ExpectBegin() + s.Fixtures.SQLMock. + ExpectExec(regexp.QuoteMeta("UPDATE `instances` SET")). + WillReturnResult(sqlmock.NewResult(1, 1)) + s.Fixtures.SQLMock. + ExpectExec(regexp.QuoteMeta("INSERT INTO `addresses`")). + WillReturnResult(sqlmock.NewResult(1, 1)) + s.Fixtures.SQLMock. + ExpectExec(regexp.QuoteMeta("INSERT INTO `instance_status_updates`")). + WillReturnResult(sqlmock.NewResult(1, 1)) + s.Fixtures.SQLMock.ExpectCommit() + s.Fixtures.SQLMock.ExpectBegin() + s.Fixtures.SQLMock. + ExpectExec(regexp.QuoteMeta("UPDATE `instances` SET")). + WillReturnResult(sqlmock.NewResult(1, 1)) + s.Fixtures.SQLMock. + ExpectExec(regexp.QuoteMeta("INSERT INTO `addresses`")). + WillReturnError(fmt.Errorf("update addresses mock error")) + s.Fixtures.SQLMock.ExpectRollback() + + _, err := s.StoreSQLMocked.UpdateInstance(context.Background(), instance.ID, s.Fixtures.UpdateInstanceParams) + + s.assertSQLMockExpectations() + s.Require().NotNil(err) + s.Require().Equal("updating addresses: update addresses mock error", err.Error()) +} + func (s *InstancesTestSuite) TestListPoolInstances() { instances, err := s.Store.ListPoolInstances(context.Background(), s.Fixtures.Pool.ID) @@ -275,6 +506,18 @@ func (s *InstancesTestSuite) TestListAllInstances() { s.equalInstancesByName(s.Fixtures.Instances, instances) } +func (s *InstancesTestSuite) TestListAllInstancesDBFetchErr() { + s.Fixtures.SQLMock. + ExpectQuery(regexp.QuoteMeta("SELECT * FROM `instances` WHERE `instances`.`deleted_at` IS NULL")). + WillReturnError(fmt.Errorf("fetch instances mock error")) + + _, err := s.StoreSQLMocked.ListAllInstances(context.Background()) + + s.assertSQLMockExpectations() + s.Require().NotNil(err) + s.Require().Equal("fetching instances: fetch instances mock error", err.Error()) +} + func (s *InstancesTestSuite) TestPoolInstanceCount() { instancesCount, err := s.Store.PoolInstanceCount(context.Background(), s.Fixtures.Pool.ID) @@ -288,6 +531,25 @@ func (s *InstancesTestSuite) TestPoolInstanceCountInvalidPoolID() { s.Require().Equal("fetching pool: parsing id: invalid request", err.Error()) } +func (s *InstancesTestSuite) TestPoolInstanceCountDBCountErr() { + pool := s.Fixtures.Pool + + s.Fixtures.SQLMock. + ExpectQuery(regexp.QuoteMeta("SELECT * FROM `pools` WHERE id = ? AND `pools`.`deleted_at` IS NULL ORDER BY `pools`.`id` LIMIT 1")). + WithArgs(pool.ID). + WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(pool.ID)) + s.Fixtures.SQLMock. + ExpectQuery(regexp.QuoteMeta("SELECT count(*) FROM `instances` WHERE pool_id = ? AND `instances`.`deleted_at` IS NULL")). + WithArgs(pool.ID). + WillReturnError(fmt.Errorf("count mock error")) + + _, err := s.StoreSQLMocked.PoolInstanceCount(context.Background(), pool.ID) + + s.assertSQLMockExpectations() + s.Require().NotNil(err) + s.Require().Equal("fetching instance count: count mock error", err.Error()) +} + func TestInstTestSuite(t *testing.T) { suite.Run(t, new(InstancesTestSuite)) } diff --git a/go.mod b/go.mod index ceba63cf..006c34e9 100644 --- a/go.mod +++ b/go.mod @@ -28,6 +28,7 @@ require ( gorm.io/driver/mysql v1.3.3 gorm.io/driver/sqlite v1.3.2 gorm.io/gorm v1.23.4 + gopkg.in/DATA-DOG/go-sqlmock.v1 v1.3.0 ) require ( diff --git a/go.sum b/go.sum index ba0fb3a3..6b59fc3e 100644 --- a/go.sum +++ b/go.sum @@ -519,6 +519,8 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/DATA-DOG/go-sqlmock.v1 v1.3.0 h1:FVCohIoYO7IJoDDVpV2pdq7SgrMH6wHnuTyrdrxJNoY= +gopkg.in/DATA-DOG/go-sqlmock.v1 v1.3.0/go.mod h1:OdE7CF6DbADk7lN8LIKRzRJTTZXIjtWgA5THM5lhBAw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/vendor/gopkg.in/DATA-DOG/go-sqlmock.v1/.gitignore b/vendor/gopkg.in/DATA-DOG/go-sqlmock.v1/.gitignore new file mode 100644 index 00000000..e4001c08 --- /dev/null +++ b/vendor/gopkg.in/DATA-DOG/go-sqlmock.v1/.gitignore @@ -0,0 +1,3 @@ +/examples/blog/blog +/examples/orders/orders +/examples/basic/basic diff --git a/vendor/gopkg.in/DATA-DOG/go-sqlmock.v1/.travis.yml b/vendor/gopkg.in/DATA-DOG/go-sqlmock.v1/.travis.yml new file mode 100644 index 00000000..8fec0e8a --- /dev/null +++ b/vendor/gopkg.in/DATA-DOG/go-sqlmock.v1/.travis.yml @@ -0,0 +1,19 @@ +language: go + +go: + - 1.2.x + - 1.3.x + - 1.4 # has no cover tool for latest releases + - 1.5.x + - 1.6.x + - 1.7.x + - 1.8.x + # - tip # sadly fails most of the times + +script: + - go vet + - test -z "$(go fmt ./...)" # fail if not formatted properly + - go test -race -coverprofile=coverage.txt -covermode=atomic + +after_success: + - bash <(curl -s https://codecov.io/bash) diff --git a/vendor/gopkg.in/DATA-DOG/go-sqlmock.v1/LICENSE b/vendor/gopkg.in/DATA-DOG/go-sqlmock.v1/LICENSE new file mode 100644 index 00000000..7f8bedf4 --- /dev/null +++ b/vendor/gopkg.in/DATA-DOG/go-sqlmock.v1/LICENSE @@ -0,0 +1,28 @@ +The three clause BSD license (http://en.wikipedia.org/wiki/BSD_licenses) + +Copyright (c) 2013-2017, DATA-DOG team +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* The name DataDog.lt may not be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL MICHAEL BOSTOCK BE LIABLE FOR ANY DIRECT, +INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY +OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/gopkg.in/DATA-DOG/go-sqlmock.v1/README.md b/vendor/gopkg.in/DATA-DOG/go-sqlmock.v1/README.md new file mode 100644 index 00000000..16a825f7 --- /dev/null +++ b/vendor/gopkg.in/DATA-DOG/go-sqlmock.v1/README.md @@ -0,0 +1,229 @@ +[![Build Status](https://travis-ci.org/DATA-DOG/go-sqlmock.svg)](https://travis-ci.org/DATA-DOG/go-sqlmock) +[![GoDoc](https://godoc.org/github.com/DATA-DOG/go-sqlmock?status.svg)](https://godoc.org/github.com/DATA-DOG/go-sqlmock) +[![codecov.io](https://codecov.io/github/DATA-DOG/go-sqlmock/branch/master/graph/badge.svg)](https://codecov.io/github/DATA-DOG/go-sqlmock) + +# Sql driver mock for Golang + +**sqlmock** is a mock library implementing [sql/driver](https://godoc.org/database/sql/driver). Which has one and only +purpose - to simulate any **sql** driver behavior in tests, without needing a real database connection. It helps to +maintain correct **TDD** workflow. + +- this library is now complete and stable. (you may not find new changes for this reason) +- supports concurrency and multiple connections. +- supports **go1.8** Context related feature mocking and Named sql parameters. +- does not require any modifications to your source code. +- the driver allows to mock any sql driver method behavior. +- has strict by default expectation order matching. +- has no third party dependencies. + +**NOTE:** in **v1.2.0** **sqlmock.Rows** has changed to struct from interface, if you were using any type references to that +interface, you will need to switch it to a pointer struct type. Also, **sqlmock.Rows** were used to implement **driver.Rows** +interface, which was not required or useful for mocking and was removed. Hope it will not cause issues. + +## Install + + go get gopkg.in/DATA-DOG/go-sqlmock.v1 + +## Documentation and Examples + +Visit [godoc](http://godoc.org/github.com/DATA-DOG/go-sqlmock) for general examples and public api reference. +See **.travis.yml** for supported **go** versions. +Different use case, is to functionally test with a real database - [go-txdb](https://github.com/DATA-DOG/go-txdb) +all database related actions are isolated within a single transaction so the database can remain in the same state. + +See implementation examples: + +- [blog API server](https://github.com/DATA-DOG/go-sqlmock/tree/master/examples/blog) +- [the same orders example](https://github.com/DATA-DOG/go-sqlmock/tree/master/examples/orders) + +### Something you may want to test + +``` go +package main + +import "database/sql" + +func recordStats(db *sql.DB, userID, productID int64) (err error) { + tx, err := db.Begin() + if err != nil { + return + } + + defer func() { + switch err { + case nil: + err = tx.Commit() + default: + tx.Rollback() + } + }() + + if _, err = tx.Exec("UPDATE products SET views = views + 1"); err != nil { + return + } + if _, err = tx.Exec("INSERT INTO product_viewers (user_id, product_id) VALUES (?, ?)", userID, productID); err != nil { + return + } + return +} + +func main() { + // @NOTE: the real connection is not required for tests + db, err := sql.Open("mysql", "root@/blog") + if err != nil { + panic(err) + } + defer db.Close() + + if err = recordStats(db, 1 /*some user id*/, 5 /*some product id*/); err != nil { + panic(err) + } +} +``` + +### Tests with sqlmock + +``` go +package main + +import ( + "fmt" + "testing" + + "gopkg.in/DATA-DOG/go-sqlmock.v1" +) + +// a successful case +func TestShouldUpdateStats(t *testing.T) { + db, mock, err := sqlmock.New() + if err != nil { + t.Fatalf("an error '%s' was not expected when opening a stub database connection", err) + } + defer db.Close() + + mock.ExpectBegin() + mock.ExpectExec("UPDATE products").WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectExec("INSERT INTO product_viewers").WithArgs(2, 3).WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit() + + // now we execute our method + if err = recordStats(db, 2, 3); err != nil { + t.Errorf("error was not expected while updating stats: %s", err) + } + + // we make sure that all expectations were met + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expections: %s", err) + } +} + +// a failing test case +func TestShouldRollbackStatUpdatesOnFailure(t *testing.T) { + db, mock, err := sqlmock.New() + if err != nil { + t.Fatalf("an error '%s' was not expected when opening a stub database connection", err) + } + defer db.Close() + + mock.ExpectBegin() + mock.ExpectExec("UPDATE products").WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectExec("INSERT INTO product_viewers"). + WithArgs(2, 3). + WillReturnError(fmt.Errorf("some error")) + mock.ExpectRollback() + + // now we execute our method + if err = recordStats(db, 2, 3); err == nil { + t.Errorf("was expecting an error, but there was none") + } + + // we make sure that all expectations were met + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expections: %s", err) + } +} +``` + +## Matching arguments like time.Time + +There may be arguments which are of `struct` type and cannot be compared easily by value like `time.Time`. In this case +**sqlmock** provides an [Argument](https://godoc.org/github.com/DATA-DOG/go-sqlmock#Argument) interface which +can be used in more sophisticated matching. Here is a simple example of time argument matching: + +``` go +type AnyTime struct{} + +// Match satisfies sqlmock.Argument interface +func (a AnyTime) Match(v driver.Value) bool { + _, ok := v.(time.Time) + return ok +} + +func TestAnyTimeArgument(t *testing.T) { + t.Parallel() + db, mock, err := New() + if err != nil { + t.Errorf("an error '%s' was not expected when opening a stub database connection", err) + } + defer db.Close() + + mock.ExpectExec("INSERT INTO users"). + WithArgs("john", AnyTime{}). + WillReturnResult(NewResult(1, 1)) + + _, err = db.Exec("INSERT INTO users(name, created_at) VALUES (?, ?)", "john", time.Now()) + if err != nil { + t.Errorf("error '%s' was not expected, while inserting a row", err) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expections: %s", err) + } +} +``` + +It only asserts that argument is of `time.Time` type. + +## Run tests + + go test -race + +## Change Log + +- **2017-09-01** - it is now possible to expect that prepared statement will be closed, + using **ExpectedPrepare.WillBeClosed**. +- **2017-02-09** - implemented support for **go1.8** features. **Rows** interface was changed to struct + but contains all methods as before and should maintain backwards compatibility. **ExpectedQuery.WillReturnRows** may now + accept multiple row sets. +- **2016-11-02** - `db.Prepare()` was not validating expected prepare SQL + query. It should still be validated even if Exec or Query is not + executed on that prepared statement. +- **2016-02-23** - added **sqlmock.AnyArg()** function to provide any kind + of argument matcher. +- **2016-02-23** - convert expected arguments to driver.Value as natural + driver does, the change may affect time.Time comparison and will be + stricter. See [issue](https://github.com/DATA-DOG/go-sqlmock/issues/31). +- **2015-08-27** - **v1** api change, concurrency support, all known issues fixed. +- **2014-08-16** instead of **panic** during reflect type mismatch when comparing query arguments - now return error +- **2014-08-14** added **sqlmock.NewErrorResult** which gives an option to return driver.Result with errors for +interface methods, see [issue](https://github.com/DATA-DOG/go-sqlmock/issues/5) +- **2014-05-29** allow to match arguments in more sophisticated ways, by providing an **sqlmock.Argument** interface +- **2014-04-21** introduce **sqlmock.New()** to open a mock database connection for tests. This method +calls sql.DB.Ping to ensure that connection is open, see [issue](https://github.com/DATA-DOG/go-sqlmock/issues/4). +This way on Close it will surely assert if all expectations are met, even if database was not triggered at all. +The old way is still available, but it is advisable to call db.Ping manually before asserting with db.Close. +- **2014-02-14** RowsFromCSVString is now a part of Rows interface named as FromCSVString. +It has changed to allow more ways to construct rows and to easily extend this API in future. +See [issue 1](https://github.com/DATA-DOG/go-sqlmock/issues/1) +**RowsFromCSVString** is deprecated and will be removed in future + +## Contributions + +Feel free to open a pull request. Note, if you wish to contribute an extension to public (exported methods or types) - +please open an issue before, to discuss whether these changes can be accepted. All backward incompatible changes are +and will be treated cautiously + +## License + +The [three clause BSD license](http://en.wikipedia.org/wiki/BSD_licenses) + diff --git a/vendor/gopkg.in/DATA-DOG/go-sqlmock.v1/argument.go b/vendor/gopkg.in/DATA-DOG/go-sqlmock.v1/argument.go new file mode 100644 index 00000000..7727481a --- /dev/null +++ b/vendor/gopkg.in/DATA-DOG/go-sqlmock.v1/argument.go @@ -0,0 +1,24 @@ +package sqlmock + +import "database/sql/driver" + +// Argument interface allows to match +// any argument in specific way when used with +// ExpectedQuery and ExpectedExec expectations. +type Argument interface { + Match(driver.Value) bool +} + +// AnyArg will return an Argument which can +// match any kind of arguments. +// +// Useful for time.Time or similar kinds of arguments. +func AnyArg() Argument { + return anyArgument{} +} + +type anyArgument struct{} + +func (a anyArgument) Match(_ driver.Value) bool { + return true +} diff --git a/vendor/gopkg.in/DATA-DOG/go-sqlmock.v1/driver.go b/vendor/gopkg.in/DATA-DOG/go-sqlmock.v1/driver.go new file mode 100644 index 00000000..2a480fee --- /dev/null +++ b/vendor/gopkg.in/DATA-DOG/go-sqlmock.v1/driver.go @@ -0,0 +1,78 @@ +package sqlmock + +import ( + "database/sql" + "database/sql/driver" + "fmt" + "sync" +) + +var pool *mockDriver + +func init() { + pool = &mockDriver{ + conns: make(map[string]*sqlmock), + } + sql.Register("sqlmock", pool) +} + +type mockDriver struct { + sync.Mutex + counter int + conns map[string]*sqlmock +} + +func (d *mockDriver) Open(dsn string) (driver.Conn, error) { + d.Lock() + defer d.Unlock() + + c, ok := d.conns[dsn] + if !ok { + return c, fmt.Errorf("expected a connection to be available, but it is not") + } + + c.opened++ + return c, nil +} + +// New creates sqlmock database connection +// and a mock to manage expectations. +// Pings db so that all expectations could be +// asserted. +func New() (*sql.DB, Sqlmock, error) { + pool.Lock() + dsn := fmt.Sprintf("sqlmock_db_%d", pool.counter) + pool.counter++ + + smock := &sqlmock{dsn: dsn, drv: pool, ordered: true} + pool.conns[dsn] = smock + pool.Unlock() + + return smock.open() +} + +// NewWithDSN creates sqlmock database connection +// with a specific DSN and a mock to manage expectations. +// Pings db so that all expectations could be asserted. +// +// This method is introduced because of sql abstraction +// libraries, which do not provide a way to initialize +// with sql.DB instance. For example GORM library. +// +// Note, it will error if attempted to create with an +// already used dsn +// +// It is not recommended to use this method, unless you +// really need it and there is no other way around. +func NewWithDSN(dsn string) (*sql.DB, Sqlmock, error) { + pool.Lock() + if _, ok := pool.conns[dsn]; ok { + pool.Unlock() + return nil, nil, fmt.Errorf("cannot create a new mock database with the same dsn: %s", dsn) + } + smock := &sqlmock{dsn: dsn, drv: pool, ordered: true} + pool.conns[dsn] = smock + pool.Unlock() + + return smock.open() +} diff --git a/vendor/gopkg.in/DATA-DOG/go-sqlmock.v1/expectations.go b/vendor/gopkg.in/DATA-DOG/go-sqlmock.v1/expectations.go new file mode 100644 index 00000000..6ff9a65c --- /dev/null +++ b/vendor/gopkg.in/DATA-DOG/go-sqlmock.v1/expectations.go @@ -0,0 +1,353 @@ +package sqlmock + +import ( + "database/sql/driver" + "fmt" + "regexp" + "strings" + "sync" + "time" +) + +// an expectation interface +type expectation interface { + fulfilled() bool + Lock() + Unlock() + String() string +} + +// common expectation struct +// satisfies the expectation interface +type commonExpectation struct { + sync.Mutex + triggered bool + err error +} + +func (e *commonExpectation) fulfilled() bool { + return e.triggered +} + +// ExpectedClose is used to manage *sql.DB.Close expectation +// returned by *Sqlmock.ExpectClose. +type ExpectedClose struct { + commonExpectation +} + +// WillReturnError allows to set an error for *sql.DB.Close action +func (e *ExpectedClose) WillReturnError(err error) *ExpectedClose { + e.err = err + return e +} + +// String returns string representation +func (e *ExpectedClose) String() string { + msg := "ExpectedClose => expecting database Close" + if e.err != nil { + msg += fmt.Sprintf(", which should return error: %s", e.err) + } + return msg +} + +// ExpectedBegin is used to manage *sql.DB.Begin expectation +// returned by *Sqlmock.ExpectBegin. +type ExpectedBegin struct { + commonExpectation + delay time.Duration +} + +// WillReturnError allows to set an error for *sql.DB.Begin action +func (e *ExpectedBegin) WillReturnError(err error) *ExpectedBegin { + e.err = err + return e +} + +// String returns string representation +func (e *ExpectedBegin) String() string { + msg := "ExpectedBegin => expecting database transaction Begin" + if e.err != nil { + msg += fmt.Sprintf(", which should return error: %s", e.err) + } + return msg +} + +// WillDelayFor allows to specify duration for which it will delay +// result. May be used together with Context +func (e *ExpectedBegin) WillDelayFor(duration time.Duration) *ExpectedBegin { + e.delay = duration + return e +} + +// ExpectedCommit is used to manage *sql.Tx.Commit expectation +// returned by *Sqlmock.ExpectCommit. +type ExpectedCommit struct { + commonExpectation +} + +// WillReturnError allows to set an error for *sql.Tx.Close action +func (e *ExpectedCommit) WillReturnError(err error) *ExpectedCommit { + e.err = err + return e +} + +// String returns string representation +func (e *ExpectedCommit) String() string { + msg := "ExpectedCommit => expecting transaction Commit" + if e.err != nil { + msg += fmt.Sprintf(", which should return error: %s", e.err) + } + return msg +} + +// ExpectedRollback is used to manage *sql.Tx.Rollback expectation +// returned by *Sqlmock.ExpectRollback. +type ExpectedRollback struct { + commonExpectation +} + +// WillReturnError allows to set an error for *sql.Tx.Rollback action +func (e *ExpectedRollback) WillReturnError(err error) *ExpectedRollback { + e.err = err + return e +} + +// String returns string representation +func (e *ExpectedRollback) String() string { + msg := "ExpectedRollback => expecting transaction Rollback" + if e.err != nil { + msg += fmt.Sprintf(", which should return error: %s", e.err) + } + return msg +} + +// ExpectedQuery is used to manage *sql.DB.Query, *dql.DB.QueryRow, *sql.Tx.Query, +// *sql.Tx.QueryRow, *sql.Stmt.Query or *sql.Stmt.QueryRow expectations. +// Returned by *Sqlmock.ExpectQuery. +type ExpectedQuery struct { + queryBasedExpectation + rows driver.Rows + delay time.Duration +} + +// WithArgs will match given expected args to actual database query arguments. +// if at least one argument does not match, it will return an error. For specific +// arguments an sqlmock.Argument interface can be used to match an argument. +func (e *ExpectedQuery) WithArgs(args ...driver.Value) *ExpectedQuery { + e.args = args + return e +} + +// WillReturnError allows to set an error for expected database query +func (e *ExpectedQuery) WillReturnError(err error) *ExpectedQuery { + e.err = err + return e +} + +// WillDelayFor allows to specify duration for which it will delay +// result. May be used together with Context +func (e *ExpectedQuery) WillDelayFor(duration time.Duration) *ExpectedQuery { + e.delay = duration + return e +} + +// String returns string representation +func (e *ExpectedQuery) String() string { + msg := "ExpectedQuery => expecting Query, QueryContext or QueryRow which:" + msg += "\n - matches sql: '" + e.sqlRegex.String() + "'" + + if len(e.args) == 0 { + msg += "\n - is without arguments" + } else { + msg += "\n - is with arguments:\n" + for i, arg := range e.args { + msg += fmt.Sprintf(" %d - %+v\n", i, arg) + } + msg = strings.TrimSpace(msg) + } + + if e.rows != nil { + msg += fmt.Sprintf("\n - %s", e.rows) + } + + if e.err != nil { + msg += fmt.Sprintf("\n - should return error: %s", e.err) + } + + return msg +} + +// ExpectedExec is used to manage *sql.DB.Exec, *sql.Tx.Exec or *sql.Stmt.Exec expectations. +// Returned by *Sqlmock.ExpectExec. +type ExpectedExec struct { + queryBasedExpectation + result driver.Result + delay time.Duration +} + +// WithArgs will match given expected args to actual database exec operation arguments. +// if at least one argument does not match, it will return an error. For specific +// arguments an sqlmock.Argument interface can be used to match an argument. +func (e *ExpectedExec) WithArgs(args ...driver.Value) *ExpectedExec { + e.args = args + return e +} + +// WillReturnError allows to set an error for expected database exec action +func (e *ExpectedExec) WillReturnError(err error) *ExpectedExec { + e.err = err + return e +} + +// WillDelayFor allows to specify duration for which it will delay +// result. May be used together with Context +func (e *ExpectedExec) WillDelayFor(duration time.Duration) *ExpectedExec { + e.delay = duration + return e +} + +// String returns string representation +func (e *ExpectedExec) String() string { + msg := "ExpectedExec => expecting Exec or ExecContext which:" + msg += "\n - matches sql: '" + e.sqlRegex.String() + "'" + + if len(e.args) == 0 { + msg += "\n - is without arguments" + } else { + msg += "\n - is with arguments:\n" + var margs []string + for i, arg := range e.args { + margs = append(margs, fmt.Sprintf(" %d - %+v", i, arg)) + } + msg += strings.Join(margs, "\n") + } + + if e.result != nil { + res, _ := e.result.(*result) + msg += "\n - should return Result having:" + msg += fmt.Sprintf("\n LastInsertId: %d", res.insertID) + msg += fmt.Sprintf("\n RowsAffected: %d", res.rowsAffected) + if res.err != nil { + msg += fmt.Sprintf("\n Error: %s", res.err) + } + } + + if e.err != nil { + msg += fmt.Sprintf("\n - should return error: %s", e.err) + } + + return msg +} + +// WillReturnResult arranges for an expected Exec() to return a particular +// result, there is sqlmock.NewResult(lastInsertID int64, affectedRows int64) method +// to build a corresponding result. Or if actions needs to be tested against errors +// sqlmock.NewErrorResult(err error) to return a given error. +func (e *ExpectedExec) WillReturnResult(result driver.Result) *ExpectedExec { + e.result = result + return e +} + +// ExpectedPrepare is used to manage *sql.DB.Prepare or *sql.Tx.Prepare expectations. +// Returned by *Sqlmock.ExpectPrepare. +type ExpectedPrepare struct { + commonExpectation + mock *sqlmock + sqlRegex *regexp.Regexp + statement driver.Stmt + closeErr error + mustBeClosed bool + wasClosed bool + delay time.Duration +} + +// WillReturnError allows to set an error for the expected *sql.DB.Prepare or *sql.Tx.Prepare action. +func (e *ExpectedPrepare) WillReturnError(err error) *ExpectedPrepare { + e.err = err + return e +} + +// WillReturnCloseError allows to set an error for this prepared statement Close action +func (e *ExpectedPrepare) WillReturnCloseError(err error) *ExpectedPrepare { + e.closeErr = err + return e +} + +// WillDelayFor allows to specify duration for which it will delay +// result. May be used together with Context +func (e *ExpectedPrepare) WillDelayFor(duration time.Duration) *ExpectedPrepare { + e.delay = duration + return e +} + +// WillBeClosed expects this prepared statement to +// be closed. +func (e *ExpectedPrepare) WillBeClosed() *ExpectedPrepare { + e.mustBeClosed = true + return e +} + +// ExpectQuery allows to expect Query() or QueryRow() on this prepared statement. +// this method is convenient in order to prevent duplicating sql query string matching. +func (e *ExpectedPrepare) ExpectQuery() *ExpectedQuery { + eq := &ExpectedQuery{} + eq.sqlRegex = e.sqlRegex + e.mock.expected = append(e.mock.expected, eq) + return eq +} + +// ExpectExec allows to expect Exec() on this prepared statement. +// this method is convenient in order to prevent duplicating sql query string matching. +func (e *ExpectedPrepare) ExpectExec() *ExpectedExec { + eq := &ExpectedExec{} + eq.sqlRegex = e.sqlRegex + e.mock.expected = append(e.mock.expected, eq) + return eq +} + +// String returns string representation +func (e *ExpectedPrepare) String() string { + msg := "ExpectedPrepare => expecting Prepare statement which:" + msg += "\n - matches sql: '" + e.sqlRegex.String() + "'" + + if e.err != nil { + msg += fmt.Sprintf("\n - should return error: %s", e.err) + } + + if e.closeErr != nil { + msg += fmt.Sprintf("\n - should return error on Close: %s", e.closeErr) + } + + return msg +} + +// query based expectation +// adds a query matching logic +type queryBasedExpectation struct { + commonExpectation + sqlRegex *regexp.Regexp + args []driver.Value +} + +func (e *queryBasedExpectation) attemptMatch(sql string, args []namedValue) (err error) { + if !e.queryMatches(sql) { + return fmt.Errorf(`could not match sql: "%s" with expected regexp "%s"`, sql, e.sqlRegex.String()) + } + + // catch panic + defer func() { + if e := recover(); e != nil { + _, ok := e.(error) + if !ok { + err = fmt.Errorf(e.(string)) + } + } + }() + + err = e.argsMatches(args) + return +} + +func (e *queryBasedExpectation) queryMatches(sql string) bool { + return e.sqlRegex.MatchString(sql) +} diff --git a/vendor/gopkg.in/DATA-DOG/go-sqlmock.v1/expectations_before_go18.go b/vendor/gopkg.in/DATA-DOG/go-sqlmock.v1/expectations_before_go18.go new file mode 100644 index 00000000..146f2404 --- /dev/null +++ b/vendor/gopkg.in/DATA-DOG/go-sqlmock.v1/expectations_before_go18.go @@ -0,0 +1,52 @@ +// +build !go1.8 + +package sqlmock + +import ( + "database/sql/driver" + "fmt" + "reflect" +) + +// WillReturnRows specifies the set of resulting rows that will be returned +// by the triggered query +func (e *ExpectedQuery) WillReturnRows(rows *Rows) *ExpectedQuery { + e.rows = &rowSets{sets: []*Rows{rows}} + return e +} + +func (e *queryBasedExpectation) argsMatches(args []namedValue) error { + if nil == e.args { + return nil + } + if len(args) != len(e.args) { + return fmt.Errorf("expected %d, but got %d arguments", len(e.args), len(args)) + } + for k, v := range args { + // custom argument matcher + matcher, ok := e.args[k].(Argument) + if ok { + // @TODO: does it make sense to pass value instead of named value? + if !matcher.Match(v.Value) { + return fmt.Errorf("matcher %T could not match %d argument %T - %+v", matcher, k, args[k], args[k]) + } + continue + } + + dval := e.args[k] + // convert to driver converter + darg, err := driver.DefaultParameterConverter.ConvertValue(dval) + if err != nil { + return fmt.Errorf("could not convert %d argument %T - %+v to driver value: %s", k, e.args[k], e.args[k], err) + } + + if !driver.IsValue(darg) { + return fmt.Errorf("argument %d: non-subset type %T returned from Value", k, darg) + } + + if !reflect.DeepEqual(darg, v.Value) { + return fmt.Errorf("argument %d expected [%T - %+v] does not match actual [%T - %+v]", k, darg, darg, v.Value, v.Value) + } + } + return nil +} diff --git a/vendor/gopkg.in/DATA-DOG/go-sqlmock.v1/expectations_go18.go b/vendor/gopkg.in/DATA-DOG/go-sqlmock.v1/expectations_go18.go new file mode 100644 index 00000000..2b4b44e9 --- /dev/null +++ b/vendor/gopkg.in/DATA-DOG/go-sqlmock.v1/expectations_go18.go @@ -0,0 +1,66 @@ +// +build go1.8 + +package sqlmock + +import ( + "database/sql" + "database/sql/driver" + "fmt" + "reflect" +) + +// WillReturnRows specifies the set of resulting rows that will be returned +// by the triggered query +func (e *ExpectedQuery) WillReturnRows(rows ...*Rows) *ExpectedQuery { + sets := make([]*Rows, len(rows)) + for i, r := range rows { + sets[i] = r + } + e.rows = &rowSets{sets: sets} + return e +} + +func (e *queryBasedExpectation) argsMatches(args []namedValue) error { + if nil == e.args { + return nil + } + if len(args) != len(e.args) { + return fmt.Errorf("expected %d, but got %d arguments", len(e.args), len(args)) + } + // @TODO should we assert either all args are named or ordinal? + for k, v := range args { + // custom argument matcher + matcher, ok := e.args[k].(Argument) + if ok { + if !matcher.Match(v.Value) { + return fmt.Errorf("matcher %T could not match %d argument %T - %+v", matcher, k, args[k], args[k]) + } + continue + } + + dval := e.args[k] + if named, isNamed := dval.(sql.NamedArg); isNamed { + dval = named.Value + if v.Name != named.Name { + return fmt.Errorf("named argument %d: name: \"%s\" does not match expected: \"%s\"", k, v.Name, named.Name) + } + } else if k+1 != v.Ordinal { + return fmt.Errorf("argument %d: ordinal position: %d does not match expected: %d", k, k+1, v.Ordinal) + } + + // convert to driver converter + darg, err := driver.DefaultParameterConverter.ConvertValue(dval) + if err != nil { + return fmt.Errorf("could not convert %d argument %T - %+v to driver value: %s", k, e.args[k], e.args[k], err) + } + + if !driver.IsValue(darg) { + return fmt.Errorf("argument %d: non-subset type %T returned from Value", k, darg) + } + + if !reflect.DeepEqual(darg, v.Value) { + return fmt.Errorf("argument %d expected [%T - %+v] does not match actual [%T - %+v]", k, darg, darg, v.Value, v.Value) + } + } + return nil +} diff --git a/vendor/gopkg.in/DATA-DOG/go-sqlmock.v1/result.go b/vendor/gopkg.in/DATA-DOG/go-sqlmock.v1/result.go new file mode 100644 index 00000000..a63e72ba --- /dev/null +++ b/vendor/gopkg.in/DATA-DOG/go-sqlmock.v1/result.go @@ -0,0 +1,39 @@ +package sqlmock + +import ( + "database/sql/driver" +) + +// Result satisfies sql driver Result, which +// holds last insert id and rows affected +// by Exec queries +type result struct { + insertID int64 + rowsAffected int64 + err error +} + +// NewResult creates a new sql driver Result +// for Exec based query mocks. +func NewResult(lastInsertID int64, rowsAffected int64) driver.Result { + return &result{ + insertID: lastInsertID, + rowsAffected: rowsAffected, + } +} + +// NewErrorResult creates a new sql driver Result +// which returns an error given for both interface methods +func NewErrorResult(err error) driver.Result { + return &result{ + err: err, + } +} + +func (r *result) LastInsertId() (int64, error) { + return r.insertID, r.err +} + +func (r *result) RowsAffected() (int64, error) { + return r.rowsAffected, r.err +} diff --git a/vendor/gopkg.in/DATA-DOG/go-sqlmock.v1/rows.go b/vendor/gopkg.in/DATA-DOG/go-sqlmock.v1/rows.go new file mode 100644 index 00000000..836f49bd --- /dev/null +++ b/vendor/gopkg.in/DATA-DOG/go-sqlmock.v1/rows.go @@ -0,0 +1,157 @@ +package sqlmock + +import ( + "database/sql/driver" + "encoding/csv" + "fmt" + "io" + "strings" +) + +// CSVColumnParser is a function which converts trimmed csv +// column string to a []byte representation. currently +// transforms NULL to nil +var CSVColumnParser = func(s string) []byte { + switch { + case strings.ToLower(s) == "null": + return nil + } + return []byte(s) +} + +type rowSets struct { + sets []*Rows + pos int +} + +func (rs *rowSets) Columns() []string { + return rs.sets[rs.pos].cols +} + +func (rs *rowSets) Close() error { + return rs.sets[rs.pos].closeErr +} + +// advances to next row +func (rs *rowSets) Next(dest []driver.Value) error { + r := rs.sets[rs.pos] + r.pos++ + if r.pos > len(r.rows) { + return io.EOF // per interface spec + } + + for i, col := range r.rows[r.pos-1] { + dest[i] = col + } + + return r.nextErr[r.pos-1] +} + +// transforms to debuggable printable string +func (rs *rowSets) String() string { + if rs.empty() { + return "with empty rows" + } + + msg := "should return rows:\n" + if len(rs.sets) == 1 { + for n, row := range rs.sets[0].rows { + msg += fmt.Sprintf(" row %d - %+v\n", n, row) + } + return strings.TrimSpace(msg) + } + for i, set := range rs.sets { + msg += fmt.Sprintf(" result set: %d\n", i) + for n, row := range set.rows { + msg += fmt.Sprintf(" row %d - %+v\n", n, row) + } + } + return strings.TrimSpace(msg) +} + +func (rs *rowSets) empty() bool { + for _, set := range rs.sets { + if len(set.rows) > 0 { + return false + } + } + return true +} + +// Rows is a mocked collection of rows to +// return for Query result +type Rows struct { + cols []string + rows [][]driver.Value + pos int + nextErr map[int]error + closeErr error +} + +// NewRows allows Rows to be created from a +// sql driver.Value slice or from the CSV string and +// to be used as sql driver.Rows +func NewRows(columns []string) *Rows { + return &Rows{cols: columns, nextErr: make(map[int]error)} +} + +// CloseError allows to set an error +// which will be returned by rows.Close +// function. +// +// The close error will be triggered only in cases +// when rows.Next() EOF was not yet reached, that is +// a default sql library behavior +func (r *Rows) CloseError(err error) *Rows { + r.closeErr = err + return r +} + +// RowError allows to set an error +// which will be returned when a given +// row number is read +func (r *Rows) RowError(row int, err error) *Rows { + r.nextErr[row] = err + return r +} + +// AddRow composed from database driver.Value slice +// return the same instance to perform subsequent actions. +// Note that the number of values must match the number +// of columns +func (r *Rows) AddRow(values ...driver.Value) *Rows { + if len(values) != len(r.cols) { + panic("Expected number of values to match number of columns") + } + + row := make([]driver.Value, len(r.cols)) + for i, v := range values { + row[i] = v + } + + r.rows = append(r.rows, row) + return r +} + +// FromCSVString build rows from csv string. +// return the same instance to perform subsequent actions. +// Note that the number of values must match the number +// of columns +func (r *Rows) FromCSVString(s string) *Rows { + res := strings.NewReader(strings.TrimSpace(s)) + csvReader := csv.NewReader(res) + + for { + res, err := csvReader.Read() + if err != nil || res == nil { + break + } + + row := make([]driver.Value, len(r.cols)) + for i, v := range res { + row[i] = CSVColumnParser(strings.TrimSpace(v)) + } + r.rows = append(r.rows, row) + } + return r +} diff --git a/vendor/gopkg.in/DATA-DOG/go-sqlmock.v1/rows_go18.go b/vendor/gopkg.in/DATA-DOG/go-sqlmock.v1/rows_go18.go new file mode 100644 index 00000000..4ecf84e7 --- /dev/null +++ b/vendor/gopkg.in/DATA-DOG/go-sqlmock.v1/rows_go18.go @@ -0,0 +1,20 @@ +// +build go1.8 + +package sqlmock + +import "io" + +// Implement the "RowsNextResultSet" interface +func (rs *rowSets) HasNextResultSet() bool { + return rs.pos+1 < len(rs.sets) +} + +// Implement the "RowsNextResultSet" interface +func (rs *rowSets) NextResultSet() error { + if !rs.HasNextResultSet() { + return io.EOF + } + + rs.pos++ + return nil +} diff --git a/vendor/gopkg.in/DATA-DOG/go-sqlmock.v1/sqlmock.go b/vendor/gopkg.in/DATA-DOG/go-sqlmock.v1/sqlmock.go new file mode 100644 index 00000000..fa7f6247 --- /dev/null +++ b/vendor/gopkg.in/DATA-DOG/go-sqlmock.v1/sqlmock.go @@ -0,0 +1,542 @@ +/* +Package sqlmock is a mock library implementing sql driver. Which has one and only +purpose - to simulate any sql driver behavior in tests, without needing a real +database connection. It helps to maintain correct **TDD** workflow. + +It does not require any modifications to your source code in order to test +and mock database operations. Supports concurrency and multiple database mocking. + +The driver allows to mock any sql driver method behavior. +*/ +package sqlmock + +import ( + "database/sql" + "database/sql/driver" + "fmt" + "regexp" + "time" +) + +// Sqlmock interface serves to create expectations +// for any kind of database action in order to mock +// and test real database behavior. +type Sqlmock interface { + + // ExpectClose queues an expectation for this database + // action to be triggered. the *ExpectedClose allows + // to mock database response + ExpectClose() *ExpectedClose + + // ExpectationsWereMet checks whether all queued expectations + // were met in order. If any of them was not met - an error is returned. + ExpectationsWereMet() error + + // ExpectPrepare expects Prepare() to be called with sql query + // which match sqlRegexStr given regexp. + // the *ExpectedPrepare allows to mock database response. + // Note that you may expect Query() or Exec() on the *ExpectedPrepare + // statement to prevent repeating sqlRegexStr + ExpectPrepare(sqlRegexStr string) *ExpectedPrepare + + // ExpectQuery expects Query() or QueryRow() to be called with sql query + // which match sqlRegexStr given regexp. + // the *ExpectedQuery allows to mock database response. + ExpectQuery(sqlRegexStr string) *ExpectedQuery + + // ExpectExec expects Exec() to be called with sql query + // which match sqlRegexStr given regexp. + // the *ExpectedExec allows to mock database response + ExpectExec(sqlRegexStr string) *ExpectedExec + + // ExpectBegin expects *sql.DB.Begin to be called. + // the *ExpectedBegin allows to mock database response + ExpectBegin() *ExpectedBegin + + // ExpectCommit expects *sql.Tx.Commit to be called. + // the *ExpectedCommit allows to mock database response + ExpectCommit() *ExpectedCommit + + // ExpectRollback expects *sql.Tx.Rollback to be called. + // the *ExpectedRollback allows to mock database response + ExpectRollback() *ExpectedRollback + + // MatchExpectationsInOrder gives an option whether to match all + // expectations in the order they were set or not. + // + // By default it is set to - true. But if you use goroutines + // to parallelize your query executation, that option may + // be handy. + // + // This option may be turned on anytime during tests. As soon + // as it is switched to false, expectations will be matched + // in any order. Or otherwise if switched to true, any unmatched + // expectations will be expected in order + MatchExpectationsInOrder(bool) +} + +type sqlmock struct { + ordered bool + dsn string + opened int + drv *mockDriver + + expected []expectation +} + +func (c *sqlmock) open() (*sql.DB, Sqlmock, error) { + db, err := sql.Open("sqlmock", c.dsn) + if err != nil { + return db, c, err + } + return db, c, db.Ping() +} + +func (c *sqlmock) ExpectClose() *ExpectedClose { + e := &ExpectedClose{} + c.expected = append(c.expected, e) + return e +} + +func (c *sqlmock) MatchExpectationsInOrder(b bool) { + c.ordered = b +} + +// Close a mock database driver connection. It may or may not +// be called depending on the sircumstances, but if it is called +// there must be an *ExpectedClose expectation satisfied. +// meets http://golang.org/pkg/database/sql/driver/#Conn interface +func (c *sqlmock) Close() error { + c.drv.Lock() + defer c.drv.Unlock() + + c.opened-- + if c.opened == 0 { + delete(c.drv.conns, c.dsn) + } + + var expected *ExpectedClose + var fulfilled int + var ok bool + for _, next := range c.expected { + next.Lock() + if next.fulfilled() { + next.Unlock() + fulfilled++ + continue + } + + if expected, ok = next.(*ExpectedClose); ok { + break + } + + next.Unlock() + if c.ordered { + return fmt.Errorf("call to database Close, was not expected, next expectation is: %s", next) + } + } + + if expected == nil { + msg := "call to database Close was not expected" + if fulfilled == len(c.expected) { + msg = "all expectations were already fulfilled, " + msg + } + return fmt.Errorf(msg) + } + + expected.triggered = true + expected.Unlock() + return expected.err +} + +func (c *sqlmock) ExpectationsWereMet() error { + for _, e := range c.expected { + if !e.fulfilled() { + return fmt.Errorf("there is a remaining expectation which was not matched: %s", e) + } + + // for expected prepared statement check whether it was closed if expected + if prep, ok := e.(*ExpectedPrepare); ok { + if prep.mustBeClosed && !prep.wasClosed { + return fmt.Errorf("expected prepared statement to be closed, but it was not: %s", prep) + } + } + } + return nil +} + +// Begin meets http://golang.org/pkg/database/sql/driver/#Conn interface +func (c *sqlmock) Begin() (driver.Tx, error) { + ex, err := c.begin() + if err != nil { + return nil, err + } + + time.Sleep(ex.delay) + return c, nil +} + +func (c *sqlmock) begin() (*ExpectedBegin, error) { + var expected *ExpectedBegin + var ok bool + var fulfilled int + for _, next := range c.expected { + next.Lock() + if next.fulfilled() { + next.Unlock() + fulfilled++ + continue + } + + if expected, ok = next.(*ExpectedBegin); ok { + break + } + + next.Unlock() + if c.ordered { + return nil, fmt.Errorf("call to database transaction Begin, was not expected, next expectation is: %s", next) + } + } + if expected == nil { + msg := "call to database transaction Begin was not expected" + if fulfilled == len(c.expected) { + msg = "all expectations were already fulfilled, " + msg + } + return nil, fmt.Errorf(msg) + } + + expected.triggered = true + expected.Unlock() + + return expected, expected.err +} + +func (c *sqlmock) ExpectBegin() *ExpectedBegin { + e := &ExpectedBegin{} + c.expected = append(c.expected, e) + return e +} + +// Exec meets http://golang.org/pkg/database/sql/driver/#Execer +func (c *sqlmock) Exec(query string, args []driver.Value) (driver.Result, error) { + namedArgs := make([]namedValue, len(args)) + for i, v := range args { + namedArgs[i] = namedValue{ + Ordinal: i + 1, + Value: v, + } + } + + ex, err := c.exec(query, namedArgs) + if err != nil { + return nil, err + } + + time.Sleep(ex.delay) + return ex.result, nil +} + +func (c *sqlmock) exec(query string, args []namedValue) (*ExpectedExec, error) { + query = stripQuery(query) + var expected *ExpectedExec + var fulfilled int + var ok bool + for _, next := range c.expected { + next.Lock() + if next.fulfilled() { + next.Unlock() + fulfilled++ + continue + } + + if c.ordered { + if expected, ok = next.(*ExpectedExec); ok { + break + } + next.Unlock() + return nil, fmt.Errorf("call to ExecQuery '%s' with args %+v, was not expected, next expectation is: %s", query, args, next) + } + if exec, ok := next.(*ExpectedExec); ok { + if err := exec.attemptMatch(query, args); err == nil { + expected = exec + break + } + } + next.Unlock() + } + if expected == nil { + msg := "call to ExecQuery '%s' with args %+v was not expected" + if fulfilled == len(c.expected) { + msg = "all expectations were already fulfilled, " + msg + } + return nil, fmt.Errorf(msg, query, args) + } + defer expected.Unlock() + + if !expected.queryMatches(query) { + return nil, fmt.Errorf("ExecQuery '%s', does not match regex '%s'", query, expected.sqlRegex.String()) + } + + if err := expected.argsMatches(args); err != nil { + return nil, fmt.Errorf("ExecQuery '%s', arguments do not match: %s", query, err) + } + + expected.triggered = true + if expected.err != nil { + return nil, expected.err // mocked to return error + } + + if expected.result == nil { + return nil, fmt.Errorf("ExecQuery '%s' with args %+v, must return a database/sql/driver.Result, but it was not set for expectation %T as %+v", query, args, expected, expected) + } + + return expected, nil +} + +func (c *sqlmock) ExpectExec(sqlRegexStr string) *ExpectedExec { + e := &ExpectedExec{} + sqlRegexStr = stripQuery(sqlRegexStr) + e.sqlRegex = regexp.MustCompile(sqlRegexStr) + c.expected = append(c.expected, e) + return e +} + +// Prepare meets http://golang.org/pkg/database/sql/driver/#Conn interface +func (c *sqlmock) Prepare(query string) (driver.Stmt, error) { + ex, err := c.prepare(query) + if err != nil { + return nil, err + } + + time.Sleep(ex.delay) + return &statement{c, ex, query}, nil +} + +func (c *sqlmock) prepare(query string) (*ExpectedPrepare, error) { + var expected *ExpectedPrepare + var fulfilled int + var ok bool + + query = stripQuery(query) + + for _, next := range c.expected { + next.Lock() + if next.fulfilled() { + next.Unlock() + fulfilled++ + continue + } + + if c.ordered { + if expected, ok = next.(*ExpectedPrepare); ok { + break + } + + next.Unlock() + return nil, fmt.Errorf("call to Prepare statement with query '%s', was not expected, next expectation is: %s", query, next) + } + + if pr, ok := next.(*ExpectedPrepare); ok { + if pr.sqlRegex.MatchString(query) { + expected = pr + break + } + } + next.Unlock() + } + + if expected == nil { + msg := "call to Prepare '%s' query was not expected" + if fulfilled == len(c.expected) { + msg = "all expectations were already fulfilled, " + msg + } + return nil, fmt.Errorf(msg, query) + } + defer expected.Unlock() + if !expected.sqlRegex.MatchString(query) { + return nil, fmt.Errorf("Prepare query string '%s', does not match regex [%s]", query, expected.sqlRegex.String()) + } + + expected.triggered = true + return expected, expected.err +} + +func (c *sqlmock) ExpectPrepare(sqlRegexStr string) *ExpectedPrepare { + sqlRegexStr = stripQuery(sqlRegexStr) + e := &ExpectedPrepare{sqlRegex: regexp.MustCompile(sqlRegexStr), mock: c} + c.expected = append(c.expected, e) + return e +} + +type namedValue struct { + Name string + Ordinal int + Value driver.Value +} + +// Query meets http://golang.org/pkg/database/sql/driver/#Queryer +func (c *sqlmock) Query(query string, args []driver.Value) (driver.Rows, error) { + namedArgs := make([]namedValue, len(args)) + for i, v := range args { + namedArgs[i] = namedValue{ + Ordinal: i + 1, + Value: v, + } + } + + ex, err := c.query(query, namedArgs) + if err != nil { + return nil, err + } + + time.Sleep(ex.delay) + return ex.rows, nil +} + +func (c *sqlmock) query(query string, args []namedValue) (*ExpectedQuery, error) { + query = stripQuery(query) + var expected *ExpectedQuery + var fulfilled int + var ok bool + for _, next := range c.expected { + next.Lock() + if next.fulfilled() { + next.Unlock() + fulfilled++ + continue + } + + if c.ordered { + if expected, ok = next.(*ExpectedQuery); ok { + break + } + next.Unlock() + return nil, fmt.Errorf("call to Query '%s' with args %+v, was not expected, next expectation is: %s", query, args, next) + } + if qr, ok := next.(*ExpectedQuery); ok { + if err := qr.attemptMatch(query, args); err == nil { + expected = qr + break + } + } + next.Unlock() + } + + if expected == nil { + msg := "call to Query '%s' with args %+v was not expected" + if fulfilled == len(c.expected) { + msg = "all expectations were already fulfilled, " + msg + } + return nil, fmt.Errorf(msg, query, args) + } + + defer expected.Unlock() + + if !expected.queryMatches(query) { + return nil, fmt.Errorf("Query '%s', does not match regex [%s]", query, expected.sqlRegex.String()) + } + + if err := expected.argsMatches(args); err != nil { + return nil, fmt.Errorf("Query '%s', arguments do not match: %s", query, err) + } + + expected.triggered = true + if expected.err != nil { + return nil, expected.err // mocked to return error + } + + if expected.rows == nil { + return nil, fmt.Errorf("Query '%s' with args %+v, must return a database/sql/driver.Rows, but it was not set for expectation %T as %+v", query, args, expected, expected) + } + return expected, nil +} + +func (c *sqlmock) ExpectQuery(sqlRegexStr string) *ExpectedQuery { + e := &ExpectedQuery{} + sqlRegexStr = stripQuery(sqlRegexStr) + e.sqlRegex = regexp.MustCompile(sqlRegexStr) + c.expected = append(c.expected, e) + return e +} + +func (c *sqlmock) ExpectCommit() *ExpectedCommit { + e := &ExpectedCommit{} + c.expected = append(c.expected, e) + return e +} + +func (c *sqlmock) ExpectRollback() *ExpectedRollback { + e := &ExpectedRollback{} + c.expected = append(c.expected, e) + return e +} + +// Commit meets http://golang.org/pkg/database/sql/driver/#Tx +func (c *sqlmock) Commit() error { + var expected *ExpectedCommit + var fulfilled int + var ok bool + for _, next := range c.expected { + next.Lock() + if next.fulfilled() { + next.Unlock() + fulfilled++ + continue + } + + if expected, ok = next.(*ExpectedCommit); ok { + break + } + + next.Unlock() + if c.ordered { + return fmt.Errorf("call to Commit transaction, was not expected, next expectation is: %s", next) + } + } + if expected == nil { + msg := "call to Commit transaction was not expected" + if fulfilled == len(c.expected) { + msg = "all expectations were already fulfilled, " + msg + } + return fmt.Errorf(msg) + } + + expected.triggered = true + expected.Unlock() + return expected.err +} + +// Rollback meets http://golang.org/pkg/database/sql/driver/#Tx +func (c *sqlmock) Rollback() error { + var expected *ExpectedRollback + var fulfilled int + var ok bool + for _, next := range c.expected { + next.Lock() + if next.fulfilled() { + next.Unlock() + fulfilled++ + continue + } + + if expected, ok = next.(*ExpectedRollback); ok { + break + } + + next.Unlock() + if c.ordered { + return fmt.Errorf("call to Rollback transaction, was not expected, next expectation is: %s", next) + } + } + if expected == nil { + msg := "call to Rollback transaction was not expected" + if fulfilled == len(c.expected) { + msg = "all expectations were already fulfilled, " + msg + } + return fmt.Errorf(msg) + } + + expected.triggered = true + expected.Unlock() + return expected.err +} diff --git a/vendor/gopkg.in/DATA-DOG/go-sqlmock.v1/sqlmock_go18.go b/vendor/gopkg.in/DATA-DOG/go-sqlmock.v1/sqlmock_go18.go new file mode 100644 index 00000000..52b0e0cb --- /dev/null +++ b/vendor/gopkg.in/DATA-DOG/go-sqlmock.v1/sqlmock_go18.go @@ -0,0 +1,101 @@ +// +build go1.8 + +package sqlmock + +import ( + "context" + "database/sql/driver" + "errors" + "time" +) + +var ErrCancelled = errors.New("canceling query due to user request") + +// Implement the "QueryerContext" interface +func (c *sqlmock) QueryContext(ctx context.Context, query string, args []driver.NamedValue) (driver.Rows, error) { + namedArgs := make([]namedValue, len(args)) + for i, nv := range args { + namedArgs[i] = namedValue(nv) + } + + ex, err := c.query(query, namedArgs) + if err != nil { + return nil, err + } + + select { + case <-time.After(ex.delay): + return ex.rows, nil + case <-ctx.Done(): + return nil, ErrCancelled + } +} + +// Implement the "ExecerContext" interface +func (c *sqlmock) ExecContext(ctx context.Context, query string, args []driver.NamedValue) (driver.Result, error) { + namedArgs := make([]namedValue, len(args)) + for i, nv := range args { + namedArgs[i] = namedValue(nv) + } + + ex, err := c.exec(query, namedArgs) + if err != nil { + return nil, err + } + + select { + case <-time.After(ex.delay): + return ex.result, nil + case <-ctx.Done(): + return nil, ErrCancelled + } +} + +// Implement the "ConnBeginTx" interface +func (c *sqlmock) BeginTx(ctx context.Context, opts driver.TxOptions) (driver.Tx, error) { + ex, err := c.begin() + if err != nil { + return nil, err + } + + select { + case <-time.After(ex.delay): + return c, nil + case <-ctx.Done(): + return nil, ErrCancelled + } +} + +// Implement the "ConnPrepareContext" interface +func (c *sqlmock) PrepareContext(ctx context.Context, query string) (driver.Stmt, error) { + ex, err := c.prepare(query) + if err != nil { + return nil, err + } + + select { + case <-time.After(ex.delay): + return &statement{c, ex, query}, nil + case <-ctx.Done(): + return nil, ErrCancelled + } +} + +// Implement the "Pinger" interface +// for now we do not have a Ping expectation +// may be something for the future +func (c *sqlmock) Ping(ctx context.Context) error { + return nil +} + +// Implement the "StmtExecContext" interface +func (stmt *statement) ExecContext(ctx context.Context, args []driver.NamedValue) (driver.Result, error) { + return stmt.conn.ExecContext(ctx, stmt.query, args) +} + +// Implement the "StmtQueryContext" interface +func (stmt *statement) QueryContext(ctx context.Context, args []driver.NamedValue) (driver.Rows, error) { + return stmt.conn.QueryContext(ctx, stmt.query, args) +} + +// @TODO maybe add ExpectedBegin.WithOptions(driver.TxOptions) diff --git a/vendor/gopkg.in/DATA-DOG/go-sqlmock.v1/statement.go b/vendor/gopkg.in/DATA-DOG/go-sqlmock.v1/statement.go new file mode 100644 index 00000000..570efd99 --- /dev/null +++ b/vendor/gopkg.in/DATA-DOG/go-sqlmock.v1/statement.go @@ -0,0 +1,28 @@ +package sqlmock + +import ( + "database/sql/driver" +) + +type statement struct { + conn *sqlmock + ex *ExpectedPrepare + query string +} + +func (stmt *statement) Close() error { + stmt.ex.wasClosed = true + return stmt.ex.closeErr +} + +func (stmt *statement) NumInput() int { + return -1 +} + +func (stmt *statement) Exec(args []driver.Value) (driver.Result, error) { + return stmt.conn.Exec(stmt.query, args) +} + +func (stmt *statement) Query(args []driver.Value) (driver.Rows, error) { + return stmt.conn.Query(stmt.query, args) +} diff --git a/vendor/gopkg.in/DATA-DOG/go-sqlmock.v1/util.go b/vendor/gopkg.in/DATA-DOG/go-sqlmock.v1/util.go new file mode 100644 index 00000000..072e380f --- /dev/null +++ b/vendor/gopkg.in/DATA-DOG/go-sqlmock.v1/util.go @@ -0,0 +1,13 @@ +package sqlmock + +import ( + "regexp" + "strings" +) + +var re = regexp.MustCompile("\\s+") + +// strip out new lines and trim spaces +func stripQuery(q string) (s string) { + return strings.TrimSpace(re.ReplaceAllString(q, " ")) +} diff --git a/vendor/modules.txt b/vendor/modules.txt index a8375b14..a479779f 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -253,6 +253,9 @@ google.golang.org/protobuf/reflect/protoregistry google.golang.org/protobuf/runtime/protoiface google.golang.org/protobuf/runtime/protoimpl google.golang.org/protobuf/types/descriptorpb +# gopkg.in/DATA-DOG/go-sqlmock.v1 v1.3.0 +## explicit +gopkg.in/DATA-DOG/go-sqlmock.v1 # gopkg.in/errgo.v1 v1.0.1 ## explicit gopkg.in/errgo.v1