Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions .github/workflows/go-test.yml
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -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=
Expand Down Expand Up @@ -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=
Expand Down
21 changes: 15 additions & 6 deletions internal/company/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -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}
}

Expand All @@ -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
}

Expand All @@ -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
}

Expand All @@ -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
}
258 changes: 258 additions & 0 deletions internal/company/service_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
Loading