diff --git a/server/helpers_test.go b/server/helpers_test.go new file mode 100644 index 0000000..bdbcc42 --- /dev/null +++ b/server/helpers_test.go @@ -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() +} diff --git a/server/options.go b/server/options.go index 99a33cd..1d5b618 100644 --- a/server/options.go +++ b/server/options.go @@ -2,6 +2,7 @@ package server import ( "git.ctrlz.es/mgdelacroix/birthdaybot/model" + "git.ctrlz.es/mgdelacroix/birthdaybot/notification" "github.com/charmbracelet/log" ) @@ -20,3 +21,24 @@ func WithLogger(logger *log.Logger) Option { 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 + } +} diff --git a/server/server.go b/server/server.go index 0921de7..955953e 100644 --- a/server/server.go +++ b/server/server.go @@ -10,12 +10,16 @@ import ( "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 { Logger *log.Logger config *model.Config - workers []*Worker + workers []Worker birthdays []*model.Birthday notificationServices []notification.NotificationService } @@ -44,33 +48,43 @@ func New(options ...Option) (*Server, error) { srv = option(srv) } - srv.Logger.Debug("parsing CSV file", "birthdayFile", srv.config.BirthdayFile) - - 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) + if srv.Logger == nil { + return nil, ErrNoLogger } - srv.Logger.Debug("creating notification services from config") - - notificationServices, err := createNotificationServices(srv.Logger, srv.config) - if err != nil { - srv.Logger.Error("error creating notification services", "error", err) - return nil, err + if srv.config == nil { + return nil, ErrNoConfig } - srv.Logger.Info("creating server") + if len(srv.birthdays) == 0 { + srv.Logger.Debug("parsing CSV file", "birthdayFile", srv.config.BirthdayFile) - server := &Server{ - Logger: srv.Logger, - config: srv.config, - birthdays: birthdays, - notificationServices: notificationServices, + var err error + srv.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) + } } - 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() { diff --git a/server/server_test.go b/server/server_test.go new file mode 100644 index 0000000..c54a82b --- /dev/null +++ b/server/server_test.go @@ -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) + }) +} diff --git a/server/worker.go b/server/worker.go index 5b66c54..d9f03a8 100644 --- a/server/worker.go +++ b/server/worker.go @@ -1,3 +1,4 @@ +//go:generate mockgen -source=worker.go -destination=worker_mock.go -package=server package server import ( @@ -7,15 +8,20 @@ import ( "github.com/charmbracelet/log" ) -type Worker struct { +type Worker interface { + Start() + Stop() +} + +type SimpleWorker struct { server *Server logger *log.Logger stop chan bool stopped chan bool } -func NewWorker(server *Server) *Worker { - return &Worker{ +func NewSimpleWorker(server *Server) *SimpleWorker { + return &SimpleWorker{ server: server, logger: server.Logger, 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") go w.run() w.logger.Info("worker started") } -func (w *Worker) Stop() { +func (w *SimpleWorker) Stop() { w.logger.Info("stopping worker") w.stop <- true <-w.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) 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 now := time.Now() eleven := time.Date(now.Year(), now.Month(), now.Day(), 23, 0, 0, 0, now.Location()) diff --git a/server/worker_mock.go b/server/worker_mock.go new file mode 100644 index 0000000..b86ba02 --- /dev/null +++ b/server/worker_mock.go @@ -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)) +}