Compare commits

..

No commits in common. "1747612b86977de369ec9c9467d48ceb2c21725a" and "cdd29a7ae8439ad3b43df8c5dcc8f42c3b158163" have entirely different histories.

22 changed files with 34 additions and 528 deletions

View file

@ -9,12 +9,11 @@ check-generate:
script: script:
- nix-shell --run "make generate && git diff --quiet" - nix-shell --run "make generate && git diff --quiet"
check-lint: check-fmt:
stage: format stage: format
image: nixos/nix:latest image: nixos/nix:latest
script: script:
- nix-shell --run "make fmt && git diff --quiet" - nix-shell --run "make fmt && git diff --quiet"
- nix-shell --run "make lint"
check-gomod: check-gomod:
stage: format stage: format

View file

@ -11,9 +11,6 @@ test-watch:
fmt: fmt:
go fmt ./... go fmt ./...
lint:
golangci-lint run ./...
run: run:
go run ./cmd/birthdaybot -config example-config.yml go run ./cmd/birthdaybot -config example-config.yml

View file

@ -24,11 +24,6 @@ Template Format, and has the following properties available:
- `.MonthOfBirth`: the month that the person was born, as number. - `.MonthOfBirth`: the month that the person was born, as number.
- `.DayOfBirth`: the day that the person was born, as number. - `.DayOfBirth`: the day that the person was born, as number.
There are as well some functions available to be used:
- `getYearsOld`: receives the year that a user was born, and returns
how old the user is getting.
### Pictures ### Pictures
Alongside the notification for each birthday, the bot can send a Alongside the notification for each birthday, the bot can send a
@ -70,12 +65,6 @@ $ make run
- [X] Reduce logger verbosity (through levels) - [X] Reduce logger verbosity (through levels)
- [X] Add pictures to birthday notifications - [X] Add pictures to birthday notifications
- [X] Create a configurable template to fill with each notification - [X] Create a configurable template to fill with each notification
- [X] Add some endpoints
- [X] Health endpoint
- [X] Next birthdays endpoint
- [ ] Birthday list endpoint
- [X] Allow to use a random port in web tests
- [X] Web server should be optional
- [ ] Create different message systems to use with the bot - [ ] Create different message systems to use with the bot
- [X] Telegram - [X] Telegram
- [ ] Email - [ ] Email

View file

@ -1,3 +1,2 @@
John Doe,john@doe.com,12345,17/04/2192 John Doe,john@doe.com,12345,17/04/2192
John Doe The Second,john@doesecond.com,12543,17/04/2192
Jane Doe,jane@doe.com,54321,10/11/2020 Jane Doe,jane@doe.com,54321,10/11/2020
1 John Doe john@doe.com 12345 17/04/2192
John Doe The Second john@doesecond.com 12543 17/04/2192
2 Jane Doe jane@doe.com 54321 10/11/2020

View file

@ -1,91 +0,0 @@
package client
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
"strings"
"git.ctrlz.es/mgdelacroix/birthdaybot/model"
)
var (
ErrEmptyURL = errors.New("URL cannot be empty")
)
type Client struct {
url string
httpClient *http.Client
headers map[string]string
}
func New(opts ...Option) (*Client, error) {
client := &Client{
httpClient: &http.Client{},
}
for _, opt := range opts {
client = opt(client)
}
if client.url == "" {
return nil, ErrEmptyURL
}
return client, nil
}
func (c *Client) Do(ctx context.Context, method, path, data string, headers map[string]string) (*http.Response, error) {
url, err := url.JoinPath(c.url, path)
if err != nil {
return nil, fmt.Errorf("cannot build request url: %w", err)
}
req, err := http.NewRequestWithContext(ctx, method, url, strings.NewReader(data))
if err != nil {
return nil, fmt.Errorf("cannot create request: %w", err)
}
for k, v := range c.headers {
req.Header.Set(k, v)
}
for k, v := range headers {
req.Header.Set(k, v)
}
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("cannot do request: %w", err)
}
return resp, nil
}
func (c *Client) Health(ctx context.Context) (bool, error) {
resp, err := c.Do(ctx, http.MethodGet, "/health", "", nil)
if err != nil {
return false, err
}
defer resp.Body.Close()
return resp.StatusCode == 200, nil
}
func (c *Client) NextBirthdays(ctx context.Context) ([]*model.Birthday, error) {
resp, err := c.Do(ctx, http.MethodGet, "/next_birthdays", "", nil)
if err != nil {
return nil, err
}
defer resp.Body.Close()
var birthdays []*model.Birthday
if err := json.NewDecoder(resp.Body).Decode(&birthdays); err != nil {
return nil, fmt.Errorf("cannot decode birthdays: %w", err)
}
return birthdays, nil
}

View file

@ -1,17 +0,0 @@
package client
type Option func(*Client) *Client
func WithURL(url string) Option {
return func(client *Client) *Client {
client.url = url
return client
}
}
func WithHeaders(headers map[string]string) Option {
return func(client *Client) *Client {
client.headers = headers
return client
}
}

View file

@ -47,16 +47,8 @@ func main() {
c := make(chan os.Signal, 1) c := make(chan os.Signal, 1)
signal.Notify(c, syscall.SIGINT, syscall.SIGTERM) signal.Notify(c, syscall.SIGINT, syscall.SIGTERM)
if err := srv.Start(); err != nil { srv.Start()
fmt.Fprintf(os.Stderr, "ERROR: cannot start server: %s\n", err)
os.Exit(1)
}
s := <-c s := <-c
srv.Logger.Info("received signal, stopping", "signal", s) srv.Logger.Info("received signal, stopping", "signal", s)
srv.Stop()
if err := srv.Stop(); err != nil {
fmt.Fprintf(os.Stderr, "ERROR: cannot stop server: %s\n", err)
os.Exit(1)
}
} }

View file

@ -1,8 +1,4 @@
--- ---
web:
enabled: true
port: 8080
birthdays: birthdays:
file: birthdays.csv file: birthdays.csv
template: ./birthday_message.tmpl template: ./birthday_message.tmpl

View file

@ -4,7 +4,6 @@ import (
"fmt" "fmt"
"strconv" "strconv"
"strings" "strings"
"time"
) )
type Birthday struct { type Birthday struct {
@ -20,8 +19,15 @@ func (b *Birthday) Filename() string {
return fmt.Sprintf("%d_%d_%d_%s.png", b.YearOfBirth, b.MonthOfBirth, b.DayOfBirth, b.Phone) return fmt.Sprintf("%d_%d_%d_%s.png", b.YearOfBirth, b.MonthOfBirth, b.DayOfBirth, b.Phone)
} }
func (b *Birthday) Time() time.Time { func (b *Birthday) ToMap() map[string]any {
return time.Date(b.YearOfBirth, time.Month(b.MonthOfBirth), b.DayOfBirth, 0, 0, 0, 0, time.Now().Location()) return map[string]any{
"Name": b.Name,
"Email": b.Email,
"Phone": b.Phone,
"YearOfBirth": b.YearOfBirth,
"MonthOfBirth": b.MonthOfBirth,
"DayOfBirth": b.DayOfBirth,
}
} }
func NewBirthdayFromRecord(record []string) (*Birthday, error) { func NewBirthdayFromRecord(record []string) (*Birthday, error) {
@ -102,36 +108,3 @@ func FilterByDate(birthdays []*Birthday, day, month, year int) []*Birthday {
} }
return filteredBirthdays return filteredBirthdays
} }
func NextBirthdayDate(birthdays []*Birthday, now time.Time) (int, int, int) {
nowRounded := now.Round(24 * time.Hour)
var nextBirthday *Birthday
for _, birthday := range birthdays {
if nextBirthday == nil {
nextBirthday = birthday
continue
}
birthdayTime := birthday.Time()
nextBirthdayTime := nextBirthday.Time()
if nextBirthdayTime.Before(nowRounded) && birthdayTime.After(nowRounded) {
nextBirthday = birthday
continue
}
if birthdayTime.Before(nextBirthdayTime) {
if birthdayTime.After(nowRounded) || nextBirthdayTime.Before(nowRounded) {
nextBirthday = birthday
}
}
}
return nextBirthday.DayOfBirth, nextBirthday.MonthOfBirth, nextBirthday.YearOfBirth
}
func NextBirthdays(birthdays []*Birthday, now time.Time) []*Birthday {
day, month, year := NextBirthdayDate(birthdays, now)
return FilterByDate(birthdays, day, month, year)
}

View file

@ -2,7 +2,6 @@ package model
import ( import (
"testing" "testing"
"time"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@ -56,68 +55,3 @@ func TestFilename(t *testing.T) {
require.Equal(t, "2022_4_6_123456789.png", birthday.Filename()) require.Equal(t, "2022_4_6_123456789.png", birthday.Filename())
} }
func TestNextBirthdayDate(t *testing.T) {
firstBirthday := &Birthday{
YearOfBirth: 1900,
MonthOfBirth: 2,
DayOfBirth: 1,
}
secondBirthday := &Birthday{
YearOfBirth: 1900,
MonthOfBirth: 8,
DayOfBirth: 1,
}
birthdays := []*Birthday{firstBirthday, secondBirthday}
birthdaysReversed := []*Birthday{secondBirthday, firstBirthday}
testCases := []struct {
Name string
Now time.Time
Birthdays []*Birthday
ExpectedDay int
ExpectedMonth int
ExpectedYear int
}{
{
Name: "should find first birthday",
Now: time.Date(1900, time.Month(1), 1, 0, 0, 0, 0, time.Now().Location()),
ExpectedDay: 1,
ExpectedMonth: 2,
ExpectedYear: 1900,
},
{
Name: "should find second birthday",
Now: time.Date(1900, time.Month(4), 1, 0, 0, 0, 0, time.Now().Location()),
ExpectedDay: 1,
ExpectedMonth: 8,
ExpectedYear: 1900,
},
{
Name: "should find first birthday for next year",
Now: time.Date(1900, time.Month(10), 1, 0, 0, 0, 0, time.Now().Location()),
ExpectedDay: 1,
ExpectedMonth: 2,
ExpectedYear: 1900,
},
}
for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) {
t.Run("with birthdays sorted", func(t *testing.T) {
day, month, year := NextBirthdayDate(birthdays, tc.Now)
require.Equal(t, tc.ExpectedDay, day)
require.Equal(t, tc.ExpectedMonth, month)
require.Equal(t, tc.ExpectedYear, year)
})
t.Run("with birthdays reversed", func(t *testing.T) {
day, month, year := NextBirthdayDate(birthdaysReversed, tc.Now)
require.Equal(t, tc.ExpectedDay, day)
require.Equal(t, tc.ExpectedMonth, month)
require.Equal(t, tc.ExpectedYear, year)
})
})
}
}

View file

@ -3,7 +3,7 @@ package model
import ( import (
"errors" "errors"
"fmt" "fmt"
"os" "io/ioutil"
"strings" "strings"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
@ -19,7 +19,6 @@ var (
type Config struct { type Config struct {
Birthdays *BirthdaysConfig `yaml:"birthdays"` Birthdays *BirthdaysConfig `yaml:"birthdays"`
Logger *LoggerConfig `yaml:"logger"` Logger *LoggerConfig `yaml:"logger"`
Web *WebConfig `yaml:"web"`
TelegramNotifications *TelegramNotificationsConfig `yaml:"telegram_notifications"` TelegramNotifications *TelegramNotificationsConfig `yaml:"telegram_notifications"`
} }
@ -32,10 +31,6 @@ func (c *Config) IsValid() error {
return fmt.Errorf("invalid logger config: %w", err) return fmt.Errorf("invalid logger config: %w", err)
} }
if err := c.Web.IsValid(); err != nil {
return fmt.Errorf("invalid web config: %w", err)
}
if c.TelegramNotifications != nil { if c.TelegramNotifications != nil {
if err := c.TelegramNotifications.IsValid(); err != nil { if err := c.TelegramNotifications.IsValid(); err != nil {
return fmt.Errorf("invalid telegram notifications config: %w", err) return fmt.Errorf("invalid telegram notifications config: %w", err)
@ -58,12 +53,6 @@ func (c *Config) SetDefaults() {
c.Logger.SetDefaults() c.Logger.SetDefaults()
if c.Web == nil {
c.Web = &WebConfig{}
}
c.Web.SetDefaults()
if c.TelegramNotifications != nil { if c.TelegramNotifications != nil {
c.TelegramNotifications.SetDefaults() c.TelegramNotifications.SetDefaults()
} }
@ -110,17 +99,6 @@ func (lc *LoggerConfig) IsValid() error {
return nil return nil
} }
type WebConfig struct {
Enabled bool `yaml:"enabled"`
Port int `yaml:"port"`
}
func (wc *WebConfig) SetDefaults() {}
func (wc *WebConfig) IsValid() error {
return nil
}
type TelegramNotificationsConfig struct { type TelegramNotificationsConfig struct {
BotToken string `yaml:"bot_token"` BotToken string `yaml:"bot_token"`
ChannelID string `yaml:"channel_id"` ChannelID string `yaml:"channel_id"`
@ -141,7 +119,7 @@ func (tnc *TelegramNotificationsConfig) IsValid() error {
} }
func ReadConfig(path string) (*Config, error) { func ReadConfig(path string) (*Config, error) {
fileBytes, err := os.ReadFile(path) fileBytes, err := ioutil.ReadFile(path)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View file

@ -2,6 +2,7 @@ package model
import ( import (
"io" "io"
"io/ioutil"
"os" "os"
"testing" "testing"
@ -10,7 +11,7 @@ import (
func TestReadConfig(t *testing.T) { func TestReadConfig(t *testing.T) {
t.Run("should correctly read a configuration file", func(t *testing.T) { t.Run("should correctly read a configuration file", func(t *testing.T) {
f, err := os.CreateTemp("", "birthdaybot-") f, err := ioutil.TempFile("", "birthdaybot-")
require.NoError(t, err) require.NoError(t, err)
defer os.Remove(f.Name()) defer os.Remove(f.Name())
@ -24,7 +25,7 @@ func TestReadConfig(t *testing.T) {
}) })
t.Run("should fail if the file doesn't exist", func(t *testing.T) { t.Run("should fail if the file doesn't exist", func(t *testing.T) {
f, err := os.CreateTemp("", "birthdaybot-") f, err := ioutil.TempFile("", "birthdaybot-")
require.NoError(t, err) require.NoError(t, err)
f.Close() f.Close()
os.Remove(f.Name()) os.Remove(f.Name())

View file

@ -43,7 +43,7 @@ func (tns *TelegramNotificationService) Notify(birthday *model.Birthday, templat
var msgText string var msgText string
if template != nil { if template != nil {
var stringBuffer bytes.Buffer var stringBuffer bytes.Buffer
if err := template.Execute(&stringBuffer, birthday); err != nil { if err := template.Execute(&stringBuffer, birthday.ToMap()); err != nil {
return fmt.Errorf("cannot execute template for birthday: %w", err) return fmt.Errorf("cannot execute template for birthday: %w", err)
} }

View file

@ -2,7 +2,7 @@ package parser
import ( import (
"io" "io"
"os" "io/ioutil"
"testing" "testing"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@ -10,7 +10,7 @@ import (
func TestParseCsv(t *testing.T) { func TestParseCsv(t *testing.T) {
t.Run("should correctly parse a valid CSV file", func(t *testing.T) { t.Run("should correctly parse a valid CSV file", func(t *testing.T) {
f, err := os.CreateTemp("", "birthdaybot-") f, err := ioutil.TempFile("", "birthdaybot-")
require.NoError(t, err) require.NoError(t, err)
_, werr := io.WriteString(f, "John Doe , john@doe.com, 1234, 17/04/2192\nJane Doe,jane@doe.com,4321,15/01/2020\n") _, werr := io.WriteString(f, "John Doe , john@doe.com, 1234, 17/04/2192\nJane Doe,jane@doe.com,4321,15/01/2020\n")

View file

@ -1 +1 @@
¡Mañana es el cumpleaños de {{.Name}}! Cumple {{getYearsOld .YearOfBirth}} años, puedes felicitarle o bien escribiendo a {{.Email}} o bien llamando al número {{.Phone}} ¡Mañana es el cumpleaños de {{.Name}}! Puedes felicitarle o bien escribiendo a {{.Email}} o bien llamando al número {{.Phone}}

View file

@ -1,11 +1,10 @@
package server package server
import ( import (
"fmt" "io/ioutil"
"os" "os"
"testing" "testing"
"git.ctrlz.es/mgdelacroix/birthdaybot/client"
"git.ctrlz.es/mgdelacroix/birthdaybot/model" "git.ctrlz.es/mgdelacroix/birthdaybot/model"
"git.ctrlz.es/mgdelacroix/birthdaybot/notification" "git.ctrlz.es/mgdelacroix/birthdaybot/notification"
notification_mocks "git.ctrlz.es/mgdelacroix/birthdaybot/notification/mocks" notification_mocks "git.ctrlz.es/mgdelacroix/birthdaybot/notification/mocks"
@ -21,22 +20,18 @@ type TestHelper struct {
mockNotificationService *notification_mocks.MockNotificationService mockNotificationService *notification_mocks.MockNotificationService
mockWorker *server_mocks.MockWorker mockWorker *server_mocks.MockWorker
srv *Server srv *Server
client *client.Client
} }
func testConfig(t *testing.T) *model.Config { func testConfig(t *testing.T) *model.Config {
f, err := os.CreateTemp("", "birthdaybot-") f, err := ioutil.TempFile("", "birthdaybot-")
require.NoError(t, err) require.NoError(t, err)
require.NoError(t, f.Close()) require.NoError(t, f.Close())
require.NoError(t, os.Remove(f.Name())) require.NoError(t, os.Remove(f.Name()))
return &model.Config{ return &model.Config{Birthdays: &model.BirthdaysConfig{File: f.Name()}}
Web: &model.WebConfig{Enabled: true, Port: 0},
Birthdays: &model.BirthdaysConfig{File: f.Name()},
}
} }
func SetupTestHelper(t *testing.T, opts ...Option) *TestHelper { func SetupTestHelper(t *testing.T) *TestHelper {
th := &TestHelper{t: t} th := &TestHelper{t: t}
th.ctrl = gomock.NewController(t) th.ctrl = gomock.NewController(t)
@ -59,28 +54,22 @@ func SetupTestHelper(t *testing.T, opts ...Option) *TestHelper {
}, },
} }
serverOpts := append([]Option{ var err error
th.srv, err = New(
WithConfig(testConfig(t)), WithConfig(testConfig(t)),
WithLogger(log.New(os.Stderr)), WithLogger(log.New(os.Stderr)),
WithBirthdays(birthdays), WithBirthdays(birthdays),
WithNotificationServices(notificationServices), WithNotificationServices(notificationServices),
WithWorkers(workers), WithWorkers(workers),
}, opts...) )
var err error
th.srv, err = New(serverOpts...)
require.NoError(t, err) require.NoError(t, err)
require.NoError(t, th.srv.Start()) th.srv.Start()
client, err := client.New(client.WithURL(fmt.Sprintf("http://127.0.0.1:%d", th.srv.WebServer.Port())))
require.NoError(t, err)
th.client = client
return th return th
} }
func (th *TestHelper) TearDown() { func (th *TestHelper) TearDown() {
require.NoError(th.t, th.srv.Stop()) th.srv.Stop()
th.ctrl.Finish() th.ctrl.Finish()
} }

View file

@ -3,9 +3,7 @@ package server
import ( import (
"errors" "errors"
"fmt" "fmt"
"path"
"text/template" "text/template"
"time"
"git.ctrlz.es/mgdelacroix/birthdaybot/model" "git.ctrlz.es/mgdelacroix/birthdaybot/model"
"git.ctrlz.es/mgdelacroix/birthdaybot/notification" "git.ctrlz.es/mgdelacroix/birthdaybot/notification"
@ -22,7 +20,6 @@ var (
type Server struct { type Server struct {
Logger *log.Logger Logger *log.Logger
Config *model.Config Config *model.Config
WebServer *WebServer
workers []Worker workers []Worker
birthdays []*model.Birthday birthdays []*model.Birthday
notificationServices []notification.NotificationService notificationServices []notification.NotificationService
@ -92,70 +89,30 @@ func New(options ...Option) (*Server, error) {
if srv.Config.Birthdays.Template != "" { if srv.Config.Birthdays.Template != "" {
srv.Logger.Debug("parsing birthday template", "file", srv.Config.Birthdays.Template) srv.Logger.Debug("parsing birthday template", "file", srv.Config.Birthdays.Template)
funcs := template.FuncMap{
"getYearsOld": func(yearOfBirth int) int {
return time.Now().Year() - yearOfBirth
},
}
var err error var err error
srv.tmpl, err = template. srv.tmpl, err = template.ParseFiles(srv.Config.Birthdays.Template)
New(path.Base(srv.Config.Birthdays.Template)).
Funcs(funcs).
ParseFiles(srv.Config.Birthdays.Template)
if err != nil { if err != nil {
return nil, fmt.Errorf("cannot parse template file %q: %w", srv.Config.Birthdays.Template, err) return nil, fmt.Errorf("cannot parse template file %q: %w", srv.Config.Birthdays.Template, err)
} }
} }
if srv.WebServer == nil && srv.Config.Web.Enabled {
srv.Logger.Debug("creating web server")
ws, err := NewWebServer(srv)
if err != nil {
return nil, fmt.Errorf("cannot create web server: %w", err)
}
srv.WebServer = ws
}
return srv, nil return srv, nil
} }
func (s *Server) Start() error { func (s *Server) Start() {
s.Logger.Info("starting server") s.Logger.Info("starting server")
if s.WebServer != nil {
if err := s.WebServer.Start(); err != nil {
return fmt.Errorf("cannot start web server: %w", err)
}
}
for _, worker := range s.workers { for _, worker := range s.workers {
worker.Start() worker.Start()
} }
s.Logger.Debug("server started", "workers", len(s.workers)) s.Logger.Debug("server started", "workers", len(s.workers))
return nil
} }
func (s *Server) Stop() error { func (s *Server) Stop() {
s.Logger.Info("stopping server") s.Logger.Info("stopping server")
if s.WebServer != nil {
if err := s.WebServer.Stop(); err != nil {
return fmt.Errorf("cannot stop web server: %w", err)
}
}
for _, worker := range s.workers { for _, worker := range s.workers {
worker.Stop() worker.Stop()
} }
s.Logger.Debug("server stopped", "workers", len(s.workers)) s.Logger.Debug("server stopped", "workers", len(s.workers))
return nil
} }
func (s *Server) Notify(birthday *model.Birthday) error { func (s *Server) Notify(birthday *model.Birthday) error {
@ -176,7 +133,3 @@ func (s *Server) Notify(birthday *model.Birthday) error {
func (s *Server) Birthdays() []*model.Birthday { func (s *Server) Birthdays() []*model.Birthday {
return s.birthdays return s.birthdays
} }
func (s *Server) NextBirthdays() []*model.Birthday {
return model.NextBirthdays(s.birthdays, time.Now())
}

View file

@ -1,21 +1,15 @@
package server package server
import ( import (
"bytes"
"errors" "errors"
"fmt"
"os"
"testing" "testing"
"time"
"git.ctrlz.es/mgdelacroix/birthdaybot/model"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
func TestNotify(t *testing.T) { func TestNotify(t *testing.T) {
th := SetupTestHelper(t) th := SetupTestHelper(t)
defer th.TearDown() defer th.TearDown()
t.Run("should correctly use the notification services to notify", func(t *testing.T) { t.Run("should correctly use the notification services to notify", func(t *testing.T) {
birthday := th.srv.birthdays[0] birthday := th.srv.birthdays[0]
th.mockNotificationService. th.mockNotificationService.
@ -41,53 +35,3 @@ func TestNotify(t *testing.T) {
require.ErrorIs(t, err, mockErr) require.ErrorIs(t, err, mockErr)
}) })
} }
func TestTemplate(t *testing.T) {
t.Run("template should work with birthday data", func(t *testing.T) {
// create a template file and populate it
f, err := os.CreateTemp("", "birthdaybot-config-")
require.NoError(t, err)
_, werr := fmt.Fprint(f, "My name is {{.Name}}")
require.NoError(t, werr)
require.NoError(t, f.Close())
// create a test config and set the template
config := testConfig(t)
config.Birthdays.Template = f.Name()
// create the test helper with the custom config
th := SetupTestHelper(t, WithConfig(config))
defer th.TearDown()
birthday := &model.Birthday{Name: "Jane Doe"}
expectedString := "My name is Jane Doe"
var stringBuffer bytes.Buffer
require.NoError(t, th.srv.tmpl.Execute(&stringBuffer, birthday))
require.Equal(t, expectedString, stringBuffer.String())
})
t.Run("template should work with custom functions", func(t *testing.T) {
// create a template file and populate it
f, err := os.CreateTemp("", "birthdaybot-config-")
require.NoError(t, err)
_, werr := fmt.Fprint(f, "I'm getting {{getYearsOld .YearOfBirth}} years old")
require.NoError(t, werr)
require.NoError(t, f.Close())
// create a test config and set the template
config := testConfig(t)
config.Birthdays.Template = f.Name()
// create the test helper with the custom config
th := SetupTestHelper(t, WithConfig(config))
defer th.TearDown()
birthday := &model.Birthday{YearOfBirth: 1980}
expectedString := fmt.Sprintf("I'm getting %d years old", time.Now().Year()-birthday.YearOfBirth)
var stringBuffer bytes.Buffer
require.NoError(t, th.srv.tmpl.Execute(&stringBuffer, birthday))
require.Equal(t, expectedString, stringBuffer.String())
})
}

View file

@ -1,88 +0,0 @@
package server
import (
"encoding/json"
"errors"
"fmt"
"net"
"net/http"
"github.com/charmbracelet/log"
)
type WebServer struct {
server *Server
listener net.Listener
logger *log.Logger
httpServer *http.Server
}
func NewWebServer(server *Server) (*WebServer, error) {
listener, err := net.Listen("tcp", fmt.Sprintf(":%d", server.Config.Web.Port))
if err != nil {
return nil, fmt.Errorf("cannot create listener: %w", err)
}
ws := &WebServer{
server: server,
listener: listener,
logger: server.Logger,
}
mux := http.NewServeMux()
mux.HandleFunc("/health", ws.healthHandler)
mux.HandleFunc("/next_birthdays", ws.nextBirthdayHandler)
ws.httpServer = &http.Server{Handler: mux}
return ws, nil
}
func (ws *WebServer) Start() error {
ws.logger.Debug("starting web server")
go func() {
if err := ws.httpServer.Serve(ws.listener); err != nil && !errors.Is(err, http.ErrServerClosed) {
ws.logger.Fatal("cannot start web server", "error", err)
}
}()
return nil
}
func (ws *WebServer) Stop() error {
ws.logger.Debug("stopping web server")
if err := ws.httpServer.Close(); err != nil {
return fmt.Errorf("cannot stop web server: %w", err)
}
return nil
}
func (ws *WebServer) Port() int {
return ws.listener.Addr().(*net.TCPAddr).Port
}
func (ws *WebServer) healthHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "OK")
}
func (ws *WebServer) nextBirthdayHandler(w http.ResponseWriter, r *http.Request) {
ws.JSON(w, http.StatusOK, ws.server.NextBirthdays())
}
func (ws *WebServer) JSON(w http.ResponseWriter, statusCode int, data any) {
b, err := json.Marshal(data)
if err != nil {
ws.logger.Error("cannot marshal data", "error", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
w.WriteHeader(statusCode)
if _, err := w.Write(b); err != nil {
ws.logger.Error("cannot write to response writer", "error", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
}
}

View file

@ -1,39 +0,0 @@
package server
import (
"context"
"testing"
"github.com/stretchr/testify/require"
)
func TestPort(t *testing.T) {
th := SetupTestHelper(t)
defer th.TearDown()
port := th.srv.WebServer.Port()
require.NotEmpty(t, port)
}
func TestHealthHandler(t *testing.T) {
th := SetupTestHelper(t)
defer th.TearDown()
t.Run("should return ok if the server is up and running", func(t *testing.T) {
health, err := th.client.Health(context.Background())
require.NoError(t, err)
require.True(t, health)
})
}
func TestNextBirthdaysHandler(t *testing.T) {
th := SetupTestHelper(t)
defer th.TearDown()
t.Run("should return a list if the server is up and running", func(t *testing.T) {
birthdays, err := th.client.NextBirthdays(context.Background())
require.NoError(t, err)
require.Len(t, birthdays, 1)
require.Equal(t, "john@doe.com", birthdays[0].Email)
})
}

View file

@ -48,9 +48,7 @@ func (w *SimpleWorker) notifyDay(year, month, day int) {
for _, b := range birthdays { for _, b := range birthdays {
w.logger.Info("notifying for birthday", "name", b.Name) w.logger.Info("notifying for birthday", "name", b.Name)
if err := w.server.Notify(b); err != nil { w.server.Notify(b)
w.logger.Error("error notifying for birthday", "name", b.Name, "error", err)
}
} }
} }

View file

@ -15,7 +15,6 @@ pkgs.mkShell {
gnumake gnumake
modd modd
mockgen mockgen
golangci-lint
]; ];
shellHook = '' shellHook = ''