diff --git a/Makefile b/Makefile index 954864a..c5dc744 100644 --- a/Makefile +++ b/Makefile @@ -16,3 +16,7 @@ generate: mod-tidy build: GOVERSION=$$(go version) \ goreleaser build --clean --debug --single-target --snapshot + +.PHONY: fuzz +fuzz: mod-tidy generate + go test -fuzz='^Fuzz' -fuzztime=10s -v ./internal/server diff --git a/cmd/go-cli-github/main.go b/cmd/go-cli-github/main.go index 2cc9a5c..e98707f 100644 --- a/cmd/go-cli-github/main.go +++ b/cmd/go-cli-github/main.go @@ -1,3 +1,4 @@ +// Package main implements the command-line interface of a server. package main import ( diff --git a/cmd/go-cli-github/serve.go b/cmd/go-cli-github/serve.go index a80e5f0..fe7016c 100644 --- a/cmd/go-cli-github/serve.go +++ b/cmd/go-cli-github/serve.go @@ -2,6 +2,7 @@ package main import ( "fmt" + "time" "github.com/smlx/go-cli-github/internal/server" ) @@ -11,6 +12,6 @@ type ServeCmd struct{} // Run the serve command. func (*ServeCmd) Run() error { - fmt.Println(server.Serve()) + fmt.Println(server.New(time.Now).Serve()) return nil } diff --git a/go.mod b/go.mod index c90281c..cc14af5 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,12 @@ module github.com/smlx/go-cli-github go 1.21 -require github.com/alecthomas/kong v0.8.1 +require ( + github.com/alecthomas/assert/v2 v2.1.0 + github.com/alecthomas/kong v0.8.1 +) + +require ( + github.com/alecthomas/repr v0.1.0 // indirect + github.com/hexops/gotextdiff v1.0.3 // indirect +) diff --git a/internal/server/serve.go b/internal/server/serve.go index d89e008..1113065 100644 --- a/internal/server/serve.go +++ b/internal/server/serve.go @@ -1,6 +1,55 @@ +// Package server implements an example server. package server +import ( + "fmt" + "time" +) + +// Server is an example server. +type Server struct { + // now is a function returning a time that the server will consider the + // current instant. + now func() time.Time +} + +// New constructs a new Server, which greets people based on the time returned +// by the given nowFunc. If nowFunc is nil, the Server will default to time.Now. +func New(nowFunc func() time.Time) *Server { + return &Server{ + now: nowFunc, + } +} + // Serve is an example function. -func Serve() string { +func (*Server) Serve() string { return "example serve command" } + +// Greet is an example fuzzable function. +// name is free form, but birthDate must be in YYYY-MM-DD format. +func (s *Server) Greet(name, birthDate string) (string, error) { + b, err := time.ParseInLocation("2006-01-02", birthDate, time.Local) + if err != nil { + return "", fmt.Errorf("couldn't parse birthDate: %v", err) + } + now := s.now() + if now.Before(b) { + return "", fmt.Errorf("time travel detected") + } + // calculate time since birthday this year + d := now.Sub( + time.Date(now.Year(), b.Month(), b.Day(), 0, 0, 0, 0, time.Local)) + switch { + case d < 0: + // calculate time since birthday last year + d = now.Sub( + time.Date(now.Year()-1, b.Month(), b.Day(), 0, 0, 0, 0, time.Local)) + fallthrough + case d > 0: + return fmt.Sprintf("Hello %s, happy belated birthday for %d days ago.", + name, int(d.Hours()/24)), nil + default: + return fmt.Sprintf("Hello %s, happy birthday!", name), nil + } +} diff --git a/internal/server/serve_test.go b/internal/server/serve_test.go index 68cedc3..7de175f 100644 --- a/internal/server/serve_test.go +++ b/internal/server/serve_test.go @@ -2,10 +2,20 @@ package server_test import ( "testing" + "time" + "github.com/alecthomas/assert/v2" "github.com/smlx/go-cli-github/internal/server" ) +func nowTestFunc() time.Time { + t, err := time.ParseInLocation("2006-01-02", "2023-12-12", time.Local) + if err != nil { + panic(err) + } + return t +} + func TestServe(t *testing.T) { var testCases = map[string]struct { input string @@ -15,10 +25,50 @@ func TestServe(t *testing.T) { } for name, tc := range testCases { t.Run(name, func(tt *testing.T) { - result := server.Serve() - if result != tc.expect { - tt.Fatalf("expected %s, got %s", tc.expect, result) + s := server.New(nowTestFunc) + result := s.Serve() + assert.Equal(tt, tc.expect, result, name) + }) + } +} + +func TestGreet(t *testing.T) { + var testCases = map[string]struct { + input []string + expect string + expectError bool + }{ + "boomer": { + input: []string{"Jim", "1963-02-03"}, + expect: "Hello Jim, happy belated birthday for 312 days ago.", + }, + "the doctor": { + input: []string{"Who", "2963-02-03"}, + expect: "time travel detected", + expectError: true, + }, + } + for name, tc := range testCases { + t.Run(name, func(tt *testing.T) { + s := server.New(nowTestFunc) + result, err := s.Greet(tc.input[0], tc.input[1]) + if tc.expectError { + assert.EqualError(tt, err, tc.expect, name) + } else { + assert.NoError(tt, err, name) + assert.Equal(tt, tc.expect, result, name) } }) } } + +func FuzzGreet(f *testing.F) { + f.Add("Joe", "2020-04-02") + f.Fuzz(func(t *testing.T, name, birthDate string) { + s := server.New(nowTestFunc) + out, err := s.Greet(name, birthDate) + if err != nil && out != "" { + t.Errorf("%q, %v", out, err) + } + }) +}