Create a test to notify from the server, and all necessary structure

This commit is contained in:
Miguel de la Cruz 2023-07-10 13:39:25 +02:00
parent 977e2d993e
commit 15f90c1e19
6 changed files with 239 additions and 29 deletions

73
server/helpers_test.go Normal file
View file

@ -0,0 +1,73 @@
package server
import (
"io/ioutil"
"os"
"testing"
"git.ctrlz.es/mgdelacroix/birthdaybot/model"
"git.ctrlz.es/mgdelacroix/birthdaybot/notification"
"github.com/charmbracelet/log"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/require"
)
type TestHelper struct {
t *testing.T
ctrl *gomock.Controller
mockNotificationService *notification.MockNotificationService
mockWorker *MockWorker
srv *Server
}
func testConfig(t *testing.T) *model.Config {
f, err := ioutil.TempFile("", "birthdaybot-")
require.NoError(t, err)
require.NoError(t, f.Close())
require.NoError(t, os.Remove(f.Name()))
return &model.Config{BirthdayFile: f.Name()}
}
func SetupTestHelper(t *testing.T) *TestHelper {
th := &TestHelper{t: t}
th.ctrl = gomock.NewController(t)
th.mockNotificationService = notification.NewMockNotificationService(th.ctrl)
notificationServices := []notification.NotificationService{th.mockNotificationService}
th.mockWorker = NewMockWorker(th.ctrl)
workers := []Worker{th.mockWorker}
th.mockWorker.EXPECT().Start().Times(1)
th.mockWorker.EXPECT().Stop().Times(1)
birthdays := []*model.Birthday{
{
Name: "John",
Email: "john@doe.com",
Phone: "1234",
YearOfBirth: 2022,
MonthOfBirth: 1,
DayOfBirth: 1,
},
}
var err error
th.srv, err = New(
WithConfig(testConfig(t)),
WithLogger(log.New(os.Stderr)),
WithBirthdays(birthdays),
WithNotificationServices(notificationServices),
WithWorkers(workers),
)
require.NoError(t, err)
th.srv.Start()
return th
}
func (th *TestHelper) TearDown() {
th.srv.Stop()
th.ctrl.Finish()
}

View file

@ -2,6 +2,7 @@ package server
import ( import (
"git.ctrlz.es/mgdelacroix/birthdaybot/model" "git.ctrlz.es/mgdelacroix/birthdaybot/model"
"git.ctrlz.es/mgdelacroix/birthdaybot/notification"
"github.com/charmbracelet/log" "github.com/charmbracelet/log"
) )
@ -20,3 +21,24 @@ func WithLogger(logger *log.Logger) Option {
return server return server
} }
} }
func WithBirthdays(birthdays []*model.Birthday) Option {
return func(server *Server) *Server {
server.birthdays = birthdays
return server
}
}
func WithNotificationServices(notificationServices []notification.NotificationService) Option {
return func(server *Server) *Server {
server.notificationServices = notificationServices
return server
}
}
func WithWorkers(workers []Worker) Option {
return func(server *Server) *Server {
server.workers = workers
return server
}
}

View file

@ -10,12 +10,16 @@ import (
"github.com/charmbracelet/log" "github.com/charmbracelet/log"
) )
var ErrNoNotificationServices = errors.New("there are no notification services configured") var (
ErrNoNotificationServices = errors.New("there are no notification services configured")
ErrNoLogger = errors.New("there is no logger configured")
ErrNoConfig = errors.New("configuration is required to create a server")
)
type Server struct { type Server struct {
Logger *log.Logger Logger *log.Logger
config *model.Config config *model.Config
workers []*Worker workers []Worker
birthdays []*model.Birthday birthdays []*model.Birthday
notificationServices []notification.NotificationService notificationServices []notification.NotificationService
} }
@ -44,33 +48,43 @@ func New(options ...Option) (*Server, error) {
srv = option(srv) srv = option(srv)
} }
srv.Logger.Debug("parsing CSV file", "birthdayFile", srv.config.BirthdayFile) if srv.Logger == nil {
return nil, ErrNoLogger
birthdays, err := parser.ParseCSV(srv.config.BirthdayFile)
if err != nil {
srv.Logger.Error("cannot parse CSV file", "birthdayFile", srv.config.BirthdayFile, "error", err)
return nil, fmt.Errorf("cannot parse CSV file %s: %w", srv.config.BirthdayFile, err)
} }
srv.Logger.Debug("creating notification services from config") if srv.config == nil {
return nil, ErrNoConfig
notificationServices, err := createNotificationServices(srv.Logger, srv.config)
if err != nil {
srv.Logger.Error("error creating notification services", "error", err)
return nil, err
} }
srv.Logger.Info("creating server") if len(srv.birthdays) == 0 {
srv.Logger.Debug("parsing CSV file", "birthdayFile", srv.config.BirthdayFile)
server := &Server{ var err error
Logger: srv.Logger, srv.birthdays, err = parser.ParseCSV(srv.config.BirthdayFile)
config: srv.config, if err != nil {
birthdays: birthdays, srv.Logger.Error("cannot parse CSV file", "birthdayFile", srv.config.BirthdayFile, "error", err)
notificationServices: notificationServices, return nil, fmt.Errorf("cannot parse CSV file %s: %w", srv.config.BirthdayFile, err)
}
} }
server.workers = []*Worker{NewWorker(server)}
return server, nil if len(srv.notificationServices) == 0 {
srv.Logger.Debug("creating notification services from config")
var err error
srv.notificationServices, err = createNotificationServices(srv.Logger, srv.config)
if err != nil {
srv.Logger.Error("error creating notification services", "error", err)
return nil, err
}
}
if len(srv.workers) == 0 {
srv.Logger.Info("creating server workers")
srv.workers = []Worker{NewSimpleWorker(srv)}
}
return srv, nil
} }
func (s *Server) Start() { func (s *Server) Start() {

37
server/server_test.go Normal file
View file

@ -0,0 +1,37 @@
package server
import (
"errors"
"testing"
"github.com/stretchr/testify/require"
)
func TestNotify(t *testing.T) {
th := SetupTestHelper(t)
defer th.TearDown()
t.Run("should correctly use the notification services to notify", func(t *testing.T) {
birthday := th.srv.birthdays[0]
th.mockNotificationService.
EXPECT().
Notify(birthday).
Return(nil).
Times(1)
err := th.srv.Notify(birthday)
require.NoError(t, err)
})
t.Run("should return an error if a service fails", func(t *testing.T) {
mockErr := errors.New("failed to notify")
birthday := th.srv.birthdays[0]
th.mockNotificationService.
EXPECT().
Notify(birthday).
Return(mockErr).
Times(1)
err := th.srv.Notify(birthday)
require.ErrorIs(t, err, mockErr)
})
}

View file

@ -1,3 +1,4 @@
//go:generate mockgen -source=worker.go -destination=worker_mock.go -package=server
package server package server
import ( import (
@ -7,15 +8,20 @@ import (
"github.com/charmbracelet/log" "github.com/charmbracelet/log"
) )
type Worker struct { type Worker interface {
Start()
Stop()
}
type SimpleWorker struct {
server *Server server *Server
logger *log.Logger logger *log.Logger
stop chan bool stop chan bool
stopped chan bool stopped chan bool
} }
func NewWorker(server *Server) *Worker { func NewSimpleWorker(server *Server) *SimpleWorker {
return &Worker{ return &SimpleWorker{
server: server, server: server,
logger: server.Logger, logger: server.Logger,
stop: make(chan bool, 1), stop: make(chan bool, 1),
@ -23,20 +29,20 @@ func NewWorker(server *Server) *Worker {
} }
} }
func (w *Worker) Start() { func (w *SimpleWorker) Start() {
w.logger.Info("starting worker") w.logger.Info("starting worker")
go w.run() go w.run()
w.logger.Info("worker started") w.logger.Info("worker started")
} }
func (w *Worker) Stop() { func (w *SimpleWorker) Stop() {
w.logger.Info("stopping worker") w.logger.Info("stopping worker")
w.stop <- true w.stop <- true
<-w.stopped <-w.stopped
w.logger.Info("worker stopped") w.logger.Info("worker stopped")
} }
func (w *Worker) notifyDay(year, month, day int) { func (w *SimpleWorker) notifyDay(year, month, day int) {
birthdays := model.FilterByDate(w.server.Birthdays(), year, month, day) birthdays := model.FilterByDate(w.server.Birthdays(), year, month, day)
w.logger.Info("notifying for date", "birthdays", len(birthdays), "year", year, "month", month, "day", day) w.logger.Info("notifying for date", "birthdays", len(birthdays), "year", year, "month", month, "day", day)
@ -46,7 +52,7 @@ func (w *Worker) notifyDay(year, month, day int) {
} }
} }
func (w *Worker) run() { func (w *SimpleWorker) run() {
// first we calculate the delta with 23:00 // first we calculate the delta with 23:00
now := time.Now() now := time.Now()
eleven := time.Date(now.Year(), now.Month(), now.Day(), 23, 0, 0, 0, now.Location()) eleven := time.Date(now.Year(), now.Month(), now.Day(), 23, 0, 0, 0, now.Location())

58
server/worker_mock.go Normal file
View file

@ -0,0 +1,58 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: worker.go
// Package server is a generated GoMock package.
package server
import (
reflect "reflect"
gomock "github.com/golang/mock/gomock"
)
// MockWorker is a mock of Worker interface.
type MockWorker struct {
ctrl *gomock.Controller
recorder *MockWorkerMockRecorder
}
// MockWorkerMockRecorder is the mock recorder for MockWorker.
type MockWorkerMockRecorder struct {
mock *MockWorker
}
// NewMockWorker creates a new mock instance.
func NewMockWorker(ctrl *gomock.Controller) *MockWorker {
mock := &MockWorker{ctrl: ctrl}
mock.recorder = &MockWorkerMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockWorker) EXPECT() *MockWorkerMockRecorder {
return m.recorder
}
// Start mocks base method.
func (m *MockWorker) Start() {
m.ctrl.T.Helper()
m.ctrl.Call(m, "Start")
}
// Start indicates an expected call of Start.
func (mr *MockWorkerMockRecorder) Start() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Start", reflect.TypeOf((*MockWorker)(nil).Start))
}
// Stop mocks base method.
func (m *MockWorker) Stop() {
m.ctrl.T.Helper()
m.ctrl.Call(m, "Stop")
}
// Stop indicates an expected call of Stop.
func (mr *MockWorkerMockRecorder) Stop() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Stop", reflect.TypeOf((*MockWorker)(nil).Stop))
}