diff --git a/.github/workflows/go-test.yml b/.github/workflows/go-test.yml new file mode 100644 index 0000000..ebf37d2 --- /dev/null +++ b/.github/workflows/go-test.yml @@ -0,0 +1,23 @@ +name: Go Tests + +on: + push: + branches: + - main + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version-file: go.mod + + - name: Run tests + run: go test ./... -v -race -count=1 diff --git a/go.mod b/go.mod index 0a9f9f6..4ee6d4c 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( ) require ( + github.com/DATA-DOG/go-sqlmock v1.5.2 // indirect github.com/bytedance/sonic v1.14.0 // indirect github.com/bytedance/sonic/loader v0.3.0 // indirect github.com/cloudwego/base64x v0.1.6 // indirect diff --git a/go.sum b/go.sum index 52c204d..f3e89c0 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU= +github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ= github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA= github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA= @@ -32,6 +34,7 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= diff --git a/internal/company/service.go b/internal/company/service.go index 421b539..eefb768 100644 --- a/internal/company/service.go +++ b/internal/company/service.go @@ -2,16 +2,19 @@ package company import ( "context" - "customer-service/internal/event" "strconv" ) +type Publisher interface { + Publish(routingKey string, payload interface{}) +} + type Service struct { Repo *Repository - Publisher *event.Publisher + Publisher Publisher } -func NewService(repo *Repository, pub *event.Publisher) *Service { +func NewService(repo *Repository, pub Publisher) *Service { return &Service{Repo: repo, Publisher: pub} } @@ -20,7 +23,9 @@ func (s *Service) CreateCompany(ctx context.Context, c *Company) error { return err } - s.Publisher.Publish("CompanyCreated", c) + if s.Publisher != nil { + s.Publisher.Publish("CompanyCreated", c) + } return nil } @@ -37,7 +42,9 @@ func (s *Service) UpdateCompany(ctx context.Context, c *Company) error { return err } - s.Publisher.Publish("CompanyUpdated", c) + if s.Publisher != nil { + s.Publisher.Publish("CompanyUpdated", c) + } return nil } @@ -46,6 +53,8 @@ func (s *Service) DeleteCompany(ctx context.Context, id int) error { return err } - s.Publisher.Publish("CompanyDeleted", map[string]string{"id": strconv.Itoa(id)}) + if s.Publisher != nil { + s.Publisher.Publish("CompanyDeleted", map[string]string{"id": strconv.Itoa(id)}) + } return nil } diff --git a/internal/company/service_test.go b/internal/company/service_test.go new file mode 100644 index 0000000..4b9ee0b --- /dev/null +++ b/internal/company/service_test.go @@ -0,0 +1,258 @@ +package company + +import ( + "context" + "errors" + "reflect" + "testing" + "time" + + "github.com/DATA-DOG/go-sqlmock" +) + +type publishedEvent struct { + routingKey string + payload interface{} +} + +type spyPublisher struct { + events []publishedEvent +} + +func (s *spyPublisher) Publish(routingKey string, payload interface{}) { + s.events = append(s.events, publishedEvent{routingKey: routingKey, payload: payload}) +} + +func newTestService(t *testing.T) (*Service, sqlmock.Sqlmock, *spyPublisher, func()) { + t.Helper() + + db, mock, err := sqlmock.New() + if err != nil { + t.Fatalf("failed to create sqlmock: %v", err) + } + + spy := &spyPublisher{} + svc := NewService(NewRepository(db), spy) + + cleanup := func() { + _ = db.Close() + } + + return svc, mock, spy, cleanup +} + +func TestService_CreateCompany_Success(t *testing.T) { + svc, mock, spy, cleanup := newTestService(t) + defer cleanup() + + c := &Company{ + Name: "Acme Corp", + Email: "contact@acme.com", + Phone: "0601020304", + Address: "42 rue de Paris", + } + + mock.ExpectQuery(`INSERT INTO companies \(name, email, phone, address, created_at, updated_at\)\s+VALUES \(\$1, \$2, \$3, \$4, NOW\(\), NOW\(\)\)\s+RETURNING id`). + WithArgs(c.Name, c.Email, c.Phone, c.Address). + WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow("3")) + + err := svc.CreateCompany(context.Background(), c) + if err != nil { + t.Fatalf("CreateCompany returned error: %v", err) + } + if c.ID != "3" { + t.Fatalf("expected company ID to be set to 3, got %s", c.ID) + } + if len(spy.events) != 1 { + t.Fatalf("expected 1 published event, got %d", len(spy.events)) + } + if spy.events[0].routingKey != "CompanyCreated" { + t.Fatalf("expected CompanyCreated event, got %s", spy.events[0].routingKey) + } + if spy.events[0].payload != c { + t.Fatal("expected published payload to be the created company pointer") + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Fatalf("unmet sql expectations: %v", err) + } +} + +func TestService_CreateCompany_Error(t *testing.T) { + svc, mock, spy, cleanup := newTestService(t) + defer cleanup() + + c := &Company{Name: "Acme Corp"} + + mock.ExpectQuery(`INSERT INTO companies \(name, email, phone, address, created_at, updated_at\)\s+VALUES \(\$1, \$2, \$3, \$4, NOW\(\), NOW\(\)\)\s+RETURNING id`). + WithArgs(c.Name, c.Email, c.Phone, c.Address). + WillReturnError(errors.New("insert failed")) + + err := svc.CreateCompany(context.Background(), c) + if err == nil { + t.Fatal("expected error from CreateCompany, got nil") + } + if len(spy.events) != 0 { + t.Fatalf("expected no published events, got %d", len(spy.events)) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Fatalf("unmet sql expectations: %v", err) + } +} + +func TestService_GetCompanyByID(t *testing.T) { + svc, mock, _, cleanup := newTestService(t) + defer cleanup() + + now := time.Now() + mock.ExpectQuery(`SELECT id, name, email, phone, address, created_at, updated_at\s+FROM companies\s+WHERE id = \$1`). + WithArgs(5). + WillReturnRows(sqlmock.NewRows([]string{ + "id", "name", "email", "phone", "address", "created_at", "updated_at", + }).AddRow("5", "Acme Corp", "contact@acme.com", "0601020304", "42 rue de Paris", now, now)) + + c, err := svc.GetCompanyByID(context.Background(), 5) + if err != nil { + t.Fatalf("GetCompanyByID returned error: %v", err) + } + if c == nil || c.ID != "5" { + t.Fatalf("unexpected company returned: %#v", c) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Fatalf("unmet sql expectations: %v", err) + } +} + +func TestService_GetAllCompanies(t *testing.T) { + svc, mock, _, cleanup := newTestService(t) + defer cleanup() + + now := time.Now() + mock.ExpectQuery(`SELECT id, name, email, phone, address, created_at, updated_at\s+FROM companies\s+ORDER BY created_at DESC`). + WillReturnRows(sqlmock.NewRows([]string{ + "id", "name", "email", "phone", "address", "created_at", "updated_at", + }). + AddRow("1", "A", "a@acme.com", "0600000001", "Addr A", now, now). + AddRow("2", "B", "b@acme.com", "0600000002", "Addr B", now, now)) + + companies, err := svc.GetAllCompanies(context.Background()) + if err != nil { + t.Fatalf("GetAllCompanies returned error: %v", err) + } + if len(companies) != 2 { + t.Fatalf("expected 2 companies, got %d", len(companies)) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Fatalf("unmet sql expectations: %v", err) + } +} + +func TestService_UpdateCompany_Success(t *testing.T) { + svc, mock, spy, cleanup := newTestService(t) + defer cleanup() + + c := &Company{ + ID: "3", + Name: "Acme Corporation Updated", + Email: "support@acme.com", + Phone: "0707070707", + Address: "100 avenue de Lyon", + } + + mock.ExpectExec(`UPDATE companies\s+SET name=\$1, email=\$2, phone=\$3, address=\$4, updated_at=NOW\(\)\s+WHERE id=\$5`). + WithArgs(c.Name, c.Email, c.Phone, c.Address, c.ID). + WillReturnResult(sqlmock.NewResult(0, 1)) + + err := svc.UpdateCompany(context.Background(), c) + if err != nil { + t.Fatalf("UpdateCompany returned error: %v", err) + } + if len(spy.events) != 1 { + t.Fatalf("expected 1 published event, got %d", len(spy.events)) + } + if spy.events[0].routingKey != "CompanyUpdated" { + t.Fatalf("expected CompanyUpdated event, got %s", spy.events[0].routingKey) + } + if spy.events[0].payload != c { + t.Fatal("expected published payload to be the updated company pointer") + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Fatalf("unmet sql expectations: %v", err) + } +} + +func TestService_UpdateCompany_NotFound(t *testing.T) { + svc, mock, spy, cleanup := newTestService(t) + defer cleanup() + + c := &Company{ID: "999"} + + mock.ExpectExec(`UPDATE companies\s+SET name=\$1, email=\$2, phone=\$3, address=\$4, updated_at=NOW\(\)\s+WHERE id=\$5`). + WithArgs(c.Name, c.Email, c.Phone, c.Address, c.ID). + WillReturnResult(sqlmock.NewResult(0, 0)) + + err := svc.UpdateCompany(context.Background(), c) + if err == nil || err.Error() != "company not found" { + t.Fatalf("expected company not found error, got %v", err) + } + if len(spy.events) != 0 { + t.Fatalf("expected no published events, got %d", len(spy.events)) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Fatalf("unmet sql expectations: %v", err) + } +} + +func TestService_DeleteCompany_Success(t *testing.T) { + svc, mock, spy, cleanup := newTestService(t) + defer cleanup() + + mock.ExpectExec(`DELETE FROM companies WHERE id=\$1`). + WithArgs(77). + WillReturnResult(sqlmock.NewResult(0, 1)) + + err := svc.DeleteCompany(context.Background(), 77) + if err != nil { + t.Fatalf("DeleteCompany returned error: %v", err) + } + if len(spy.events) != 1 { + t.Fatalf("expected 1 published event, got %d", len(spy.events)) + } + if spy.events[0].routingKey != "CompanyDeleted" { + t.Fatalf("expected CompanyDeleted event, got %s", spy.events[0].routingKey) + } + expectedPayload := map[string]string{"id": "77"} + if !reflect.DeepEqual(spy.events[0].payload, expectedPayload) { + t.Fatalf("unexpected delete payload: %#v", spy.events[0].payload) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Fatalf("unmet sql expectations: %v", err) + } +} + +func TestService_DeleteCompany_NotFound(t *testing.T) { + svc, mock, spy, cleanup := newTestService(t) + defer cleanup() + + mock.ExpectExec(`DELETE FROM companies WHERE id=\$1`). + WithArgs(77). + WillReturnResult(sqlmock.NewResult(0, 0)) + + err := svc.DeleteCompany(context.Background(), 77) + if err == nil || err.Error() != "company not found" { + t.Fatalf("expected company not found error, got %v", err) + } + if len(spy.events) != 0 { + t.Fatalf("expected no published events, got %d", len(spy.events)) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Fatalf("unmet sql expectations: %v", err) + } +} diff --git a/internal/event/publisher.go b/internal/event/publisher.go index c172441..05b9298 100644 --- a/internal/event/publisher.go +++ b/internal/event/publisher.go @@ -1,32 +1,36 @@ package event import ( - "encoding/json" - "github.com/streadway/amqp" - "log" + "encoding/json" + "github.com/streadway/amqp" + "log" ) type Publisher struct { - Channel *amqp.Channel - Exchange string + Channel *amqp.Channel + Exchange string } func NewPublisher(ch *amqp.Channel, exchange string) *Publisher { - return &Publisher{Channel: ch, Exchange: exchange} + return &Publisher{Channel: ch, Exchange: exchange} } func (p *Publisher) Publish(routingKey string, payload interface{}) { - body, _ := json.Marshal(payload) - if err := p.Channel.Publish( - p.Exchange, - routingKey, - false, - false, - amqp.Publishing{ - ContentType: "application/json", - Body: body, - }, - ); err != nil { - log.Println("Failed to publish event:", err) - } + if p == nil || p.Channel == nil { + return + } + + body, _ := json.Marshal(payload) + if err := p.Channel.Publish( + p.Exchange, + routingKey, + false, + false, + amqp.Publishing{ + ContentType: "application/json", + Body: body, + }, + ); err != nil { + log.Println("Failed to publish event:", err) + } } diff --git a/internal/user/service.go b/internal/user/service.go index 314800b..c33bbb5 100644 --- a/internal/user/service.go +++ b/internal/user/service.go @@ -3,17 +3,20 @@ package user import ( "context" company "customer-service/internal/company" - "customer-service/internal/event" "strconv" ) +type Publisher interface { + Publish(routingKey string, payload interface{}) +} + type Service struct { Repo *Repository CompanyRepo *company.Repository - Publisher *event.Publisher + Publisher Publisher } -func NewService(repo *Repository, companyRepo *company.Repository, pub *event.Publisher) *Service { +func NewService(repo *Repository, companyRepo *company.Repository, pub Publisher) *Service { return &Service{Repo: repo, CompanyRepo: companyRepo, Publisher: pub} } @@ -26,7 +29,9 @@ func (s *Service) CreateUser(ctx context.Context, u *User) error { return err } - s.Publisher.Publish("UserCreated", u) + if s.Publisher != nil { + s.Publisher.Publish("UserCreated", u) + } return nil } @@ -43,7 +48,9 @@ func (s *Service) UpdateUser(ctx context.Context, u *User) error { return err } - s.Publisher.Publish("UserUpdated", u) + if s.Publisher != nil { + s.Publisher.Publish("UserUpdated", u) + } return nil } @@ -52,6 +59,8 @@ func (s *Service) DeleteUser(ctx context.Context, id int) error { return err } - s.Publisher.Publish("UserDeleted", map[string]string{"id": strconv.Itoa(id)}) + if s.Publisher != nil { + s.Publisher.Publish("UserDeleted", map[string]string{"id": strconv.Itoa(id)}) + } return nil } diff --git a/internal/user/service_test.go b/internal/user/service_test.go new file mode 100644 index 0000000..3a6f1ee --- /dev/null +++ b/internal/user/service_test.go @@ -0,0 +1,274 @@ +package user + +import ( + "context" + "database/sql" + "errors" + "reflect" + "testing" + "time" + + company "customer-service/internal/company" + + "github.com/DATA-DOG/go-sqlmock" +) + +type publishedEvent struct { + routingKey string + payload interface{} +} + +type spyPublisher struct { + events []publishedEvent +} + +func (s *spyPublisher) Publish(routingKey string, payload interface{}) { + s.events = append(s.events, publishedEvent{routingKey: routingKey, payload: payload}) +} + +func newTestService(t *testing.T) (*Service, sqlmock.Sqlmock, *spyPublisher, func()) { + t.Helper() + + db, mock, err := sqlmock.New() + if err != nil { + t.Fatalf("failed to create sqlmock: %v", err) + } + + spy := &spyPublisher{} + svc := NewService( + NewRepository(db), + company.NewRepository(db), + spy, + ) + + cleanup := func() { + _ = db.Close() + } + + return svc, mock, spy, cleanup +} + +func TestService_CreateUser_Success(t *testing.T) { + svc, mock, spy, cleanup := newTestService(t) + defer cleanup() + + now := time.Now() + u := &User{ + CompanyID: 7, + FirstName: "John", + LastName: "Doe", + Email: "john@acme.com", + Phone: "0611223344", + } + + mock.ExpectQuery(`SELECT id, name, email, phone, address, created_at, updated_at\s+FROM companies\s+WHERE id = \$1`). + WithArgs(7). + WillReturnRows(sqlmock.NewRows([]string{ + "id", "name", "email", "phone", "address", "created_at", "updated_at", + }).AddRow("7", "Acme", "info@acme.com", "0600000000", "Main St", now, now)) + + mock.ExpectQuery(`INSERT INTO users \(company_id, first_name, last_name, email, phone, created_at, updated_at\)\s+VALUES \(\$1,\$2,\$3,\$4,\$5,NOW\(\),NOW\(\)\)\s+RETURNING id`). + WithArgs(u.CompanyID, u.FirstName, u.LastName, u.Email, u.Phone). + WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(12)) + + err := svc.CreateUser(context.Background(), u) + if err != nil { + t.Fatalf("CreateUser returned error: %v", err) + } + if u.ID != 12 { + t.Fatalf("expected user ID to be set to 12, got %d", u.ID) + } + if len(spy.events) != 1 { + t.Fatalf("expected 1 published event, got %d", len(spy.events)) + } + if spy.events[0].routingKey != "UserCreated" { + t.Fatalf("expected UserCreated event, got %s", spy.events[0].routingKey) + } + if spy.events[0].payload != u { + t.Fatal("expected published payload to be the created user pointer") + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Fatalf("unmet sql expectations: %v", err) + } +} + +func TestService_CreateUser_CompanyLookupError(t *testing.T) { + svc, mock, spy, cleanup := newTestService(t) + defer cleanup() + + u := &User{CompanyID: 99} + + mock.ExpectQuery(`SELECT id, name, email, phone, address, created_at, updated_at\s+FROM companies\s+WHERE id = \$1`). + WithArgs(99). + WillReturnError(errors.New("db down")) + + err := svc.CreateUser(context.Background(), u) + if err == nil { + t.Fatal("expected error from CreateUser, got nil") + } + if len(spy.events) != 0 { + t.Fatalf("expected no published events, got %d", len(spy.events)) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Fatalf("unmet sql expectations: %v", err) + } +} + +func TestService_GetUserByID(t *testing.T) { + svc, mock, _, cleanup := newTestService(t) + defer cleanup() + + now := time.Now() + mock.ExpectQuery(`SELECT id, company_id, first_name, last_name, email, phone, created_at, updated_at\s+FROM users WHERE id=\$1`). + WithArgs(5). + WillReturnRows(sqlmock.NewRows([]string{ + "id", "company_id", "first_name", "last_name", "email", "phone", "created_at", "updated_at", + }).AddRow(5, 7, "Jane", "Doe", "jane@acme.com", "0611223344", now, now)) + + u, err := svc.GetUserByID(context.Background(), 5) + if err != nil { + t.Fatalf("GetUserByID returned error: %v", err) + } + if u.ID != 5 || u.CompanyID != 7 { + t.Fatalf("unexpected user returned: %#v", u) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Fatalf("unmet sql expectations: %v", err) + } +} + +func TestService_GetUsersByCompany(t *testing.T) { + svc, mock, _, cleanup := newTestService(t) + defer cleanup() + + now := time.Now() + mock.ExpectQuery(`SELECT id, company_id, first_name, last_name, email, phone, created_at, updated_at\s+FROM users WHERE company_id=\$1`). + WithArgs(7). + WillReturnRows(sqlmock.NewRows([]string{ + "id", "company_id", "first_name", "last_name", "email", "phone", "created_at", "updated_at", + }). + AddRow(1, 7, "A", "One", "a@acme.com", "0600000001", now, now). + AddRow(2, 7, "B", "Two", "b@acme.com", "0600000002", now, now)) + + users, err := svc.GetUsersByCompany(context.Background(), 7) + if err != nil { + t.Fatalf("GetUsersByCompany returned error: %v", err) + } + if len(users) != 2 { + t.Fatalf("expected 2 users, got %d", len(users)) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Fatalf("unmet sql expectations: %v", err) + } +} + +func TestService_UpdateUser_Success(t *testing.T) { + svc, mock, spy, cleanup := newTestService(t) + defer cleanup() + + u := &User{ + ID: 10, + FirstName: "Updated", + LastName: "Name", + Email: "updated@acme.com", + Phone: "0699887766", + } + + mock.ExpectExec(`UPDATE users SET first_name=\$2, last_name=\$3, email=\$4, phone=\$5,\s+updated_at=NOW\(\) WHERE id=\$1`). + WithArgs(u.ID, u.FirstName, u.LastName, u.Email, u.Phone). + WillReturnResult(sqlmock.NewResult(0, 1)) + + err := svc.UpdateUser(context.Background(), u) + if err != nil { + t.Fatalf("UpdateUser returned error: %v", err) + } + if len(spy.events) != 1 { + t.Fatalf("expected 1 published event, got %d", len(spy.events)) + } + if spy.events[0].routingKey != "UserUpdated" { + t.Fatalf("expected UserUpdated event, got %s", spy.events[0].routingKey) + } + if spy.events[0].payload != u { + t.Fatal("expected published payload to be the updated user pointer") + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Fatalf("unmet sql expectations: %v", err) + } +} + +func TestService_UpdateUser_NotFound(t *testing.T) { + svc, mock, spy, cleanup := newTestService(t) + defer cleanup() + + u := &User{ID: 123} + + mock.ExpectExec(`UPDATE users SET first_name=\$2, last_name=\$3, email=\$4, phone=\$5,\s+updated_at=NOW\(\) WHERE id=\$1`). + WithArgs(u.ID, u.FirstName, u.LastName, u.Email, u.Phone). + WillReturnResult(sqlmock.NewResult(0, 0)) + + err := svc.UpdateUser(context.Background(), u) + if !errors.Is(err, sql.ErrNoRows) { + t.Fatalf("expected sql.ErrNoRows, got %v", err) + } + if len(spy.events) != 0 { + t.Fatalf("expected no published events, got %d", len(spy.events)) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Fatalf("unmet sql expectations: %v", err) + } +} + +func TestService_DeleteUser(t *testing.T) { + svc, mock, spy, cleanup := newTestService(t) + defer cleanup() + + mock.ExpectExec(`DELETE FROM users WHERE id=\$1`). + WithArgs(77). + WillReturnResult(sqlmock.NewResult(0, 1)) + + err := svc.DeleteUser(context.Background(), 77) + if err != nil { + t.Fatalf("DeleteUser returned error: %v", err) + } + if len(spy.events) != 1 { + t.Fatalf("expected 1 published event, got %d", len(spy.events)) + } + if spy.events[0].routingKey != "UserDeleted" { + t.Fatalf("expected UserDeleted event, got %s", spy.events[0].routingKey) + } + expectedPayload := map[string]string{"id": "77"} + if !reflect.DeepEqual(spy.events[0].payload, expectedPayload) { + t.Fatalf("unexpected delete payload: %#v", spy.events[0].payload) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Fatalf("unmet sql expectations: %v", err) + } +} + +func TestService_DeleteUser_Error(t *testing.T) { + svc, mock, spy, cleanup := newTestService(t) + defer cleanup() + + mock.ExpectExec(`DELETE FROM users WHERE id=\$1`). + WithArgs(77). + WillReturnError(errors.New("delete failed")) + + err := svc.DeleteUser(context.Background(), 77) + if err == nil { + t.Fatal("expected error from DeleteUser, got nil") + } + if len(spy.events) != 0 { + t.Fatalf("expected no published events, got %d", len(spy.events)) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Fatalf("unmet sql expectations: %v", err) + } +}