diff --git a/kadai4/shimastripe/README.md b/kadai4/shimastripe/README.md new file mode 100644 index 0000000..4ecbf49 --- /dev/null +++ b/kadai4/shimastripe/README.md @@ -0,0 +1,41 @@ +# おみくじAPIを作ってみよう + +## 仕様 + +- JSON形式でおみくじ􏰁結果を返す +- 正月(1/1-1/3)だけ大吉にする +- ハンドラ􏰁テストを書いてみる + +## 実行方法 + + ```bash + $ go run cmd/kadai4/main.go + $ curl 'http://localhost:8080' + + # => {"fortune":"大吉"} + ``` + +## Test + + ```bash + $ go test -v + + === RUN TestHandlerRandomly + --- PASS: TestHandlerRandomly (0.00s) + === RUN TestHandlerWhenNewYear + === RUN TestHandlerWhenNewYear/NewYearCase + === RUN TestHandlerWhenNewYear/NewYearCase#01 + === RUN TestHandlerWhenNewYear/NewYearCase#02 + --- PASS: TestHandlerWhenNewYear (0.00s) + --- PASS: TestHandlerWhenNewYear/NewYearCase (0.00s) + --- PASS: TestHandlerWhenNewYear/NewYearCase#01 (0.00s) + --- PASS: TestHandlerWhenNewYear/NewYearCase#02 (0.00s) + PASS + ok github.com/gopherdojo/dojo3/kadai4/shimastripe 0.018s +``` + +## Detail + +- 時間は講義で受けたClock interfaceを用意して抽象化 +- ハンドラ側のテストを用意 + - ランダムで決定する要素と決定しない要素を分けて書いた。ランダム要素側は不安定なので実質フレークテスト \ No newline at end of file diff --git a/kadai4/shimastripe/cli.go b/kadai4/shimastripe/cli.go new file mode 100644 index 0000000..fb3d22c --- /dev/null +++ b/kadai4/shimastripe/cli.go @@ -0,0 +1,53 @@ +package shimastripe + +import ( + "encoding/json" + "log" + "math/rand" + "net/http" +) + +const success = iota + +type CLI struct { + Clock Clock +} + +// Generate a seed only once +func (c *CLI) generateSeed() { + rand.Seed(c.Clock.Now().Unix()) +} + +func (c *CLI) IsNewYear() bool { + _, m, d := c.Clock.Now().Date() + if m == 1 && d <= 3 { + return true + } + return false +} + +// handler +func (c *CLI) handler(w http.ResponseWriter, req *http.Request) { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.Header().Set("X-Content-Type-Options", "nosniff") + var respBody *FortuneRepository + + if c.IsNewYear() { + respBody = &FortuneRepository{Fortune: daikichi} + } else { + respBody = &FortuneRepository{Fortune: DrawRandomly()} + } + + if err := json.NewEncoder(w).Encode(respBody); err != nil { + log.Printf("Encode error: %v\n", err) + http.Error(w, "{\"result\":\"Internal server error\"}\n", http.StatusInternalServerError) + } +} + +// Run a server +func (c *CLI) Run(args []string) int { + c.generateSeed() + http.HandleFunc("/", c.handler) + http.ListenAndServe(":8080", nil) + return success +} diff --git a/kadai4/shimastripe/cli_test.go b/kadai4/shimastripe/cli_test.go new file mode 100644 index 0000000..8c7bdbc --- /dev/null +++ b/kadai4/shimastripe/cli_test.go @@ -0,0 +1,112 @@ +package shimastripe + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "os" + "testing" + "time" +) + +var fortuneList []string + +func TestMain(m *testing.M) { + setup() + ret := m.Run() + os.Exit(ret) +} + +func setup() { + for index := 0; index < int(threshold); index++ { + fortuneList = append(fortuneList, FortuneElement(index).String()) + } +} + +// Test handler. This test is flaky. (choose fortune element randomly) +func TestHandlerRandomly(t *testing.T) { + cli := &CLI{Clock: ClockFunc(func() time.Time { + return time.Now() + })} + w := httptest.NewRecorder() + r := httptest.NewRequest("GET", "/", nil) + cli.handler(w, r) + rw := w.Result() + defer rw.Body.Close() + + if rw.StatusCode != http.StatusOK { + t.Error("unexpected status code\n") + } + + m := struct { + Fortune string `json:"fortune"` + }{} + + if err := json.NewDecoder(rw.Body).Decode(&m); err != nil { + t.Error("Decode error\n") + } + + if !contain(fortuneList, m.Fortune) { + t.Errorf("unexpected fortune element: %v", m.Fortune) + } +} + +func TestHandlerWhenNewYear(t *testing.T) { + cases := []struct { + clock Clock + answer string + }{ + {clock: mockClock(t, "2018/01/01"), answer: "大吉"}, + {clock: mockClock(t, "2018/01/02"), answer: "大吉"}, + {clock: mockClock(t, "2018/01/03"), answer: "大吉"}, + } + + for _, c := range cases { + t.Run("NewYearCase", func(t *testing.T) { + t.Helper() + cli := &CLI{Clock: c.clock} + w := httptest.NewRecorder() + r := httptest.NewRequest("GET", "/", nil) + cli.handler(w, r) + rw := w.Result() + defer rw.Body.Close() + + if rw.StatusCode != http.StatusOK { + t.Error("unexpected status code\n") + } + + m := struct { + Fortune string `json:"fortune"` + }{} + + if err := json.NewDecoder(rw.Body).Decode(&m); err != nil { + t.Error("Decode error\n") + } + + if m.Fortune != c.answer { + t.Errorf("unexpected fortune element: %v, expected: %v", m.Fortune, c.answer) + } + }) + } +} + +func contain(list []string, elm string) bool { + for _, l := range list { + if l == elm { + return true + } + } + return false +} + +func mockClock(t *testing.T, v string) Clock { + t.Helper() + now, err := time.Parse("2006/01/02", v) + if err != nil { + t.Fatal("unexpected error:", err) + } + + return ClockFunc(func() time.Time { + return now + }) +} diff --git a/kadai4/shimastripe/clock.go b/kadai4/shimastripe/clock.go new file mode 100644 index 0000000..5fecdc2 --- /dev/null +++ b/kadai4/shimastripe/clock.go @@ -0,0 +1,15 @@ +package shimastripe + +import ( + "time" +) + +type Clock interface { + Now() time.Time +} + +type ClockFunc func() time.Time + +func (f ClockFunc) Now() time.Time { + return f() +} diff --git a/kadai4/shimastripe/cmd/kadai4/main.go b/kadai4/shimastripe/cmd/kadai4/main.go new file mode 100644 index 0000000..85e5493 --- /dev/null +++ b/kadai4/shimastripe/cmd/kadai4/main.go @@ -0,0 +1,15 @@ +package main + +import ( + "os" + "time" + + "github.com/gopherdojo/dojo3/kadai4/shimastripe" +) + +func main() { + cli := &shimastripe.CLI{Clock: shimastripe.ClockFunc(func() time.Time { + return time.Now() + })} + os.Exit(cli.Run(os.Args)) +} diff --git a/kadai4/shimastripe/fortune.go b/kadai4/shimastripe/fortune.go new file mode 100644 index 0000000..c8892d6 --- /dev/null +++ b/kadai4/shimastripe/fortune.go @@ -0,0 +1,50 @@ +package shimastripe + +import "math/rand" + +type FortuneElement int +type FortuneRepository struct { + Fortune FortuneElement `json:"fortune"` +} + +const ( + daikichi FortuneElement = iota + chukichi + shokichi + suekichi + kichi + kyo + daikyo + threshold // use FortuneElement length +) + +// Behave Stringer interface +func (oe FortuneElement) String() string { + switch oe { + case daikichi: + return "大吉" + case chukichi: + return "中吉" + case shokichi: + return "小吉" + case suekichi: + return "末吉" + case kichi: + return "吉" + case kyo: + return "凶" + case daikyo: + return "大凶" + default: + return "強運" + } +} + +func (oe FortuneElement) MarshalJSON() ([]byte, error) { + return []byte(`"` + oe.String() + `"`), nil +} + +// Draw a fortune randomly +func DrawRandomly() FortuneElement { + return FortuneElement(rand.Intn(int(threshold))) +}