diff --git a/kadai4/nas/fortune/Makefile b/kadai4/nas/fortune/Makefile new file mode 100644 index 0000000..5f7906c --- /dev/null +++ b/kadai4/nas/fortune/Makefile @@ -0,0 +1,38 @@ +# CONST +BINARYNAME=fortune + +export GO111MODULE=on + +# command +.PHONY: help +help: + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' + +.PHONY: build +build: ## go build + go build -v -o ${BINARYNAME} + +#.PHONY: test +#test: ## go test +# go test -v -cover ./pkg/${BINARYNAME} + +.PHONY: clean +clean: ## go clean + go clean -cache -testcache + +.PHONY: analyze +analyze: ## do static code analysis + goimports -l -w . + go vet ./... + golint ./... + +.PHONY: test +test: ## go test ... + go test -v -cover ./... + +.PHONY: remove +remove: ## remove binary and test output data + rm -f ./${BINARYNAME} + +.PHONY: all +all: remove clean test analyze build ## run 'build' with 'remove', 'clean', 'test' and 'analyze' diff --git a/kadai4/nas/fortune/go.mod b/kadai4/nas/fortune/go.mod new file mode 100644 index 0000000..2acac98 --- /dev/null +++ b/kadai4/nas/fortune/go.mod @@ -0,0 +1,3 @@ +module github.com/gopherdojo/dojo7/kadai4/nas/fortune + +go 1.13 diff --git a/kadai4/nas/fortune/main.go b/kadai4/nas/fortune/main.go new file mode 100644 index 0000000..c1a70f1 --- /dev/null +++ b/kadai4/nas/fortune/main.go @@ -0,0 +1,40 @@ +package main + +import ( + "encoding/json" + "net/http" + + "github.com/gopherdojo/dojo7/kadai4/nas/fortune/pkg/fortune" +) + +var ( + // MockDate is Date + MockDate fortune.Date + // MockParameter is Parameter + MockParameter fortune.Parameter +) + +// FortuneHandler response fortune result json +func FortuneHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "applicaiton/json; charset=utf-8") + + f := &fortune.Fortune{ + Date: MockDate, + Parameter: MockParameter, + } + + l, err := f.Draw() + if err != nil { + http.Error(w, "Server error", http.StatusInternalServerError) + return + } + + enc := json.NewEncoder(w) + if err := enc.Encode(l); err != nil { + http.Error(w, "Server error", http.StatusInternalServerError) + } +} +func main() { + http.HandleFunc("/", FortuneHandler) + http.ListenAndServe(":8080", nil) +} diff --git a/kadai4/nas/fortune/main_test.go b/kadai4/nas/fortune/main_test.go new file mode 100644 index 0000000..9eff0df --- /dev/null +++ b/kadai4/nas/fortune/main_test.go @@ -0,0 +1,70 @@ +package main + +import ( + "io/ioutil" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/gopherdojo/dojo7/kadai4/nas/fortune/pkg/fortune" +) + +func TestFortuneHandler(t *testing.T) { + cases := []struct { + name string + date time.Time + parameter float64 + want string + }{ + {"OKGreat", time.Date(2019, 9, 25, 0, 0, 0, 0, time.FixedZone("Asia/Tokyo", 60*60*9)), 1, "{\"lack\":\"大吉\"}\n"}, + {"OKHigh", time.Date(2019, 9, 25, 0, 0, 0, 0, time.FixedZone("Asia/Tokyo", 60*60*9)), 0.83, "{\"lack\":\"中吉\"}\n"}, + {"OKMiddle", time.Date(2019, 9, 25, 0, 0, 0, 0, time.FixedZone("Asia/Tokyo", 60*60*9)), 0.5, "{\"lack\":\"吉\"}\n"}, + {"OKLow", time.Date(2019, 9, 25, 0, 0, 0, 0, time.FixedZone("Asia/Tokyo", 60*60*9)), 0.16, "{\"lack\":\"凶\"}\n"}, + {"OKNewYear", time.Date(2019, 1, 1, 0, 0, 0, 0, time.FixedZone("Asia/Tokyo", 60*60*9)), 0.00, "{\"lack\":\"大吉\"}\n"}, + } + for _, tt := range cases { + t.Run(tt.name, func(t *testing.T) { + teardown := setupTest(t, fortune.DateFunc(func() time.Time { return tt.date }), fortune.ParameterFunc(func() float64 { return tt.parameter })) + defer teardown() + w := httptest.NewRecorder() + r := httptest.NewRequest("GET", "/", nil) + FortuneHandler(w, r) + rw := w.Result() + defer rw.Body.Close() + if rw.StatusCode != http.StatusOK { + t.Fatal("unexpected status code") + } + b, err := ioutil.ReadAll(rw.Body) + if err != nil { + t.Fatal("unexpected error") + } + if got := string(b); got != tt.want { + t.Errorf("unexpected response: %s, but want %s", got, tt.want) + } + }) + } +} + +func TestFortuneHandlerNG(t *testing.T) { + teardown := setupTest(t, fortune.DateFunc(func() time.Time { return time.Date(2019, 9, 25, 0, 0, 0, 0, time.FixedZone("Asia/Tokyo", 60*60*9)) }), fortune.ParameterFunc(func() float64 { return 1.01 })) + defer teardown() + w := httptest.NewRecorder() + r := httptest.NewRequest("GET", "/", nil) + FortuneHandler(w, r) + rw := w.Result() + defer rw.Body.Close() + if rw.StatusCode != http.StatusInternalServerError { + t.Errorf("unexpected status code : %d", rw.StatusCode) + } +} + +func setupTest(t *testing.T, d fortune.Date, p fortune.Parameter) func() { + t.Helper() + MockDate = d + MockParameter = p + return func() { + MockDate = nil + MockParameter = nil + } +} diff --git a/kadai4/nas/fortune/pkg/fortune/fortune.go b/kadai4/nas/fortune/pkg/fortune/fortune.go new file mode 100644 index 0000000..ad2e398 --- /dev/null +++ b/kadai4/nas/fortune/pkg/fortune/fortune.go @@ -0,0 +1,110 @@ +package fortune + +import ( + "fmt" + "math/rand" + "time" +) + +const ( + // Great lack type + Great = "大吉" + // High lack type + High = "中吉" + // Middle lack type + Middle = "吉" + // Low lack type + Low = "凶" +) + +// Date behave today +type Date interface { + Now() time.Time +} + +// DateFunc return time func +type DateFunc func() time.Time + +// Now return time +func (f DateFunc) Now() time.Time { + return f() +} + +// Parameter behave random +type Parameter interface { + Float64() float64 +} + +// ParameterFunc return float64 func +type ParameterFunc func() float64 + +// Float64 return float64 +func (f ParameterFunc) Float64() float64 { + return f() +} + +// Fortune has several config +type Fortune struct { + Date + Parameter +} + +// Lack is drew results +type Lack struct { + Lack string `json:"lack"` +} + +// Draw return random Fortune +func (f *Fortune) Draw() (*Lack, error) { + d := today(f.Date) + if isNewYear(d) { + return &Lack{Great}, nil + } + + p := random(f.Parameter) + l, err := check(p) + if err != nil { + return nil, err + } + return &Lack{l}, nil +} + +func today(d Date) time.Time { + if d == nil { + return time.Now() + } + return d.Now() +} + +func isNewYear(d time.Time) bool { + _, month, day := d.Date() + if month != time.January { + return false + } + return map[int]bool{ + 1: true, + 2: true, + 3: true, + }[day] +} + +func random(p Parameter) float64 { + if p == nil { + return rand.Float64() + } + return p.Float64() +} + +func check(p float64) (string, error) { + switch { + case 6.0/6.0 >= p && p > 5.0/6.0: + return Great, nil + case 5.0/6.0 >= p && p > 3.0/6.0: + return High, nil + case 3.0/6.0 >= p && p > 1.0/6.0: + return Middle, nil + case 1.0/6.0 >= p && p >= 0.0/6.0: + return Low, nil + } + return "", fmt.Errorf("Can't draw, please redraw") +} diff --git a/kadai4/nas/fortune/pkg/fortune/fortune_test.go b/kadai4/nas/fortune/pkg/fortune/fortune_test.go new file mode 100644 index 0000000..0160408 --- /dev/null +++ b/kadai4/nas/fortune/pkg/fortune/fortune_test.go @@ -0,0 +1,115 @@ +package fortune + +import ( + "reflect" + "testing" + "time" +) + +func TestFortune_Draw(t *testing.T) { + cases := []struct { + name string + date time.Time + parameter float64 + want *Lack + }{ + {"Great", time.Date(2019, 9, 25, 0, 0, 0, 0, time.FixedZone("Asia/Tokyo", 60*60*9)), 1, &Lack{Great}}, + {"NewYear", time.Date(2019, 1, 1, 0, 0, 0, 0, time.FixedZone("Asia/Tokyo", 60*60*9)), 0, &Lack{Great}}, + } + + for _, tt := range cases { + t.Run(tt.name, func(t *testing.T) { + f := &Fortune{ + Date: DateFunc(func() time.Time { return tt.date }), + Parameter: ParameterFunc(func() float64 { return tt.parameter }), + } + got, err := f.Draw() + if err != nil { + t.Errorf("Unexpected Error : %v", err) + } + if !reflect.DeepEqual(tt.want, got) { + t.Errorf("Draw() => want %s, but got %s", tt.want, got) + } + }) + } +} + +func TestFortune_DrawInValid(t *testing.T) { + test := struct { + date time.Time + parameter float64 + want *Lack + }{time.Date(2019, 9, 25, 0, 0, 0, 0, time.FixedZone("Asia/Tokyo", 60*60*9)), 1.01, nil} + + f := &Fortune{ + Date: DateFunc(func() time.Time { return test.date }), + Parameter: ParameterFunc(func() float64 { return test.parameter }), + } + got, err := f.Draw() + if err == nil { + t.Error("Expected Error but nil") + } + if test.want != got { + t.Errorf("Draw() => want%s, but got %s", test.want, got) + } +} + +func TestIsNewYear(t *testing.T) { + cases := []struct { + name string + date time.Time + want bool + }{ + {"True", time.Date(2019, 1, 1, 0, 0, 0, 0, time.FixedZone("Asia/Tokyo", 60*60*9)), true}, + {"FalseMonth", time.Date(2019, 2, 1, 0, 0, 0, 0, time.FixedZone("Asia/Tokyo", 60*60*9)), false}, + {"FalseDay", time.Date(2019, 1, 4, 0, 0, 0, 0, time.FixedZone("Asia/Tokyo", 60*60*9)), false}, + } + + for _, tt := range cases { + t.Run(tt.name, func(t *testing.T) { + if got := isNewYear(tt.date); tt.want != got { + t.Errorf("isNewYear(%v) => want %t, but got %t", tt.date, tt.want, got) + } + }) + } +} + +func TestCheck(t *testing.T) { + cases := []struct { + name string + parameter float64 + want string + }{ + {"Low", 0.16, Low}, + {"Middle", 0.5, Middle}, + {"High", 0.83, High}, + {"Great", 1, Great}, + } + + for _, tt := range cases { + t.Run(tt.name, func(t *testing.T) { + got, err := check(tt.parameter) + if err != nil { + t.Errorf("Unexpected Error : %v", err) + } + if tt.want != got { + t.Errorf("Draw() => want %s, but got %s", tt.want, got) + } + }) + } +} + +func TestCheckInValid(t *testing.T) { + test := struct { + parameter float64 + want string + }{1.01, ""} + + got, err := check(test.parameter) + if err == nil { + t.Error("Expected Error but nil") + } + if test.want != got { + t.Errorf("check() => want %s, but got %s", test.want, got) + } +}