diff --git a/kadai4/nguyengiabk/.gitignore b/kadai4/nguyengiabk/.gitignore new file mode 100644 index 0000000..b1174a0 --- /dev/null +++ b/kadai4/nguyengiabk/.gitignore @@ -0,0 +1 @@ +kadai4 diff --git a/kadai4/nguyengiabk/README.md b/kadai4/nguyengiabk/README.md new file mode 100644 index 0000000..16fcc2d --- /dev/null +++ b/kadai4/nguyengiabk/README.md @@ -0,0 +1,25 @@ +# Gopher Dojo 3 - Kadai4 +## Problem + +おみくじAPIを作ってみよう +* [x] JSON形式でおみくじの結果を返す +* [x] 正月(1/1-1/3)だけ大吉にする +* [x] ハンドラのテストを書いてみる + +## Build +``` +$ go build -o kadai4 . +``` + +## Usage + +Start server +``` +$ ./kadai4 +``` + +Client send request +``` +$ curl localhost:8080 +{"result":"凶"} +``` diff --git a/kadai4/nguyengiabk/main.go b/kadai4/nguyengiabk/main.go new file mode 100644 index 0000000..7d4bd0e --- /dev/null +++ b/kadai4/nguyengiabk/main.go @@ -0,0 +1,61 @@ +package main + +import ( + "encoding/json" + "io" + "net/http" + + "github.com/gopherdojo/dojo3/kadai4/nguyengiabk/omikuji" +) + +// Result represents api response structure +type Result struct { + Result omikuji.Fortune `json:"result"` +} + +// JSONEncoder is used to encode JSON +type JSONEncoder interface { + Encode(interface{}, io.Writer) error +} + +// JSONEncodeFunc is function that used to encode JSON +type JSONEncodeFunc func(interface{}, io.Writer) error + +// Encode encode JSON and write result to writer +func (f JSONEncodeFunc) Encode(data interface{}, w io.Writer) error { + return f(data, w) +} + +// OmikujiServer handles request and response as JSON +type OmikujiServer struct { + jsonEncoder JSONEncoder + omikuji omikuji.Omikuji +} + +func (os *OmikujiServer) encode(data interface{}, w io.Writer) error { + if os.jsonEncoder != nil { + return os.jsonEncoder.Encode(data, w) + } + encoder := json.NewEncoder(w) + if err := encoder.Encode(data); err != nil { + return err + } + return nil +} + +// ServerErrorMessage is message that will be returned to user in case of error +const serverErrorMessage = "Server error" + +func (os *OmikujiServer) handler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + res := Result{os.omikuji.GetResult()} + if err := os.encode(res, w); err != nil { + http.Error(w, serverErrorMessage, http.StatusInternalServerError) + } +} + +func main() { + os := OmikujiServer{omikuji: omikuji.Omikuji{}} + http.HandleFunc("/", os.handler) + http.ListenAndServe(":8080", nil) +} diff --git a/kadai4/nguyengiabk/main_test.go b/kadai4/nguyengiabk/main_test.go new file mode 100644 index 0000000..51fc6e3 --- /dev/null +++ b/kadai4/nguyengiabk/main_test.go @@ -0,0 +1,71 @@ +package main + +import ( + "errors" + "io" + "io/ioutil" + "net/http" + "net/http/httptest" + "testing" + + "github.com/gopherdojo/dojo3/kadai4/nguyengiabk/omikuji" +) + +type testCase struct { + os OmikujiServer + statusCode int + response string +} + +var TestHandlerFixtures = map[string]testCase{ + "Test json encode error": { + OmikujiServer{ + JSONEncodeFunc(func(data interface{}, w io.Writer) error { + return errors.New("Encode error") + }), + omikuji.Omikuji{}, + }, + http.StatusInternalServerError, + "Server error\n", + }, + "Test normal case": { + OmikujiServer{ + // want to fixed response + omikuji: omikuji.Omikuji{Randomize: omikuji.RandomizeFunc(func(max int) int { + return 0 + })}, + }, + http.StatusOK, + "{\"result\":\"大吉\"}\n", + }, +} + +func TestHandler(t *testing.T) { + for name, tc := range TestHandlerFixtures { + tc := tc + t.Run(name, func(t *testing.T) { + RunTestCase(t, tc) + }) + } +} + +// split to use defer +func RunTestCase(t *testing.T, tc testCase) { + t.Helper() + w := httptest.NewRecorder() + r := httptest.NewRequest("GET", "/", nil) + tc.os.handler(w, r) + rw := w.Result() + defer rw.Body.Close() + + if rw.StatusCode != tc.statusCode { + t.Errorf("unexpected status code, actual = %v, expected = %v", rw.StatusCode, tc.statusCode) + } + b, err := ioutil.ReadAll(rw.Body) + if err != nil { + t.Errorf("unexpected error when read response body") + } + if s := string(b); s != tc.response { + t.Fatalf("unexpected response: actual = %s, expected = %s", s, tc.response) + } +} diff --git a/kadai4/nguyengiabk/omikuji/omikuji.go b/kadai4/nguyengiabk/omikuji/omikuji.go new file mode 100644 index 0000000..ee04e45 --- /dev/null +++ b/kadai4/nguyengiabk/omikuji/omikuji.go @@ -0,0 +1,92 @@ +package omikuji + +import ( + "math/rand" + "time" +) + +// Fortune represents omikuji result +type Fortune string + +const ( + // Daikichi represents 大吉 result + Daikichi Fortune = "大吉" + + // Chukichi represents 中吉 result + Chukichi Fortune = "中吉" + + // Shokichi represents 小吉 result + Shokichi Fortune = "小吉" + + // Kichi represents 吉 result + Kichi Fortune = "吉" + + // Kyou represents 凶 result + Kyou Fortune = "凶" + + // Shokyou represents 小凶 result + Shokyou Fortune = "小凶" + + // Daikyou represents 大凶 result + Daikyou Fortune = "大凶" +) + +var omikujiValues = []Fortune{Daikichi, Chukichi, Shokichi, Kichi, Kyou, Shokyou, Daikyou} + +// Clock defines types that can return current time +type Clock interface { + Now() time.Time +} + +// ClockFunc returns current time, we use this type for testing +type ClockFunc func() time.Time + +// Now returns current time by calling CockFunc itself +func (f ClockFunc) Now() time.Time { + return f() +} + +// Randomize defines types that can return ramdom integer +type Randomize interface { + Rand(max int) int +} + +// RandomizeFunc returns randome integer, we use this type for testing +type RandomizeFunc func(max int) int + +// Rand returns random integer by calling RandomizeFunc itself +func (f RandomizeFunc) Rand(max int) int { + return f(max) +} + +// Omikuji is used to get omikuji result based on current time +type Omikuji struct { + Clock Clock + Randomize Randomize +} + +func (o *Omikuji) now() time.Time { + if o.Clock == nil { + return time.Now() + } + return o.Clock.Now() +} + +func (o *Omikuji) rand(max int) int { + if o.Randomize == nil { + rand.Seed(time.Now().UnixNano()) + return rand.Intn(max) + } + return o.Randomize.Rand(max) +} + +// GetResult return randomly selected omikuji value +func (o *Omikuji) GetResult() Fortune { + _, m, d := o.now().Date() + switch { + case m == time.January && d <= 3: + return Daikichi + default: + return omikujiValues[o.rand(len(omikujiValues))] + } +} diff --git a/kadai4/nguyengiabk/omikuji/omikuji_test.go b/kadai4/nguyengiabk/omikuji/omikuji_test.go new file mode 100644 index 0000000..b4c1f88 --- /dev/null +++ b/kadai4/nguyengiabk/omikuji/omikuji_test.go @@ -0,0 +1,72 @@ +package omikuji_test + +import ( + "fmt" + "testing" + "time" + + "github.com/gopherdojo/dojo3/kadai4/nguyengiabk/omikuji" +) + +func Example() { + o := omikuji.Omikuji{} + fmt.Println(o.GetResult()) +} + +type randomize struct { + useMock bool + randResult int +} + +var testGetResultFixtures = map[string]struct { + date string + randomize randomize + result omikuji.Fortune +}{ + "Test 1/1": {"2018/01/01", randomize{false, 0}, omikuji.Daikichi}, + "Test 1/2": {"2018/01/02", randomize{false, 0}, omikuji.Daikichi}, + "Test 1/3": {"2018/01/03", randomize{false, 0}, omikuji.Daikichi}, + "Test 1/4 and Kyou": {"2018/01/04", randomize{true, 4}, omikuji.Kyou}, + "Test Daikichi not new year": {"2018/07/01", randomize{true, 0}, omikuji.Daikichi}, + "Test Chukichi": {"2018/08/02", randomize{true, 1}, omikuji.Chukichi}, + "Test Shokichi": {"2018/09/10", randomize{true, 2}, omikuji.Shokichi}, + "Test Shokyou": {"2018/10/11", randomize{true, 5}, omikuji.Shokyou}, + "Test Daikyou": {"2018/03/21", randomize{true, 6}, omikuji.Daikyou}, +} + +func TestGetResult(t *testing.T) { + for name, tc := range testGetResultFixtures { + tc := tc + t.Run(name, func(t *testing.T) { + clock := mockClock(t, tc.date) + var randomize omikuji.Randomize + if tc.randomize.useMock { + randomize = mockRandomize(t, tc.randomize.randResult) + } + o := omikuji.Omikuji{Clock: clock, Randomize: randomize} + result := o.GetResult() + if result != tc.result { + t.Errorf("GetResult() return wrong result, actual = %v, expected = %v", result, tc.result) + } + }) + } +} + +func mockClock(t *testing.T, v string) omikuji.Clock { + t.Helper() + now, err := time.Parse("2006/01/02", v) + if err != nil { + t.Fatal("unexpected error:", err) + } + + return omikuji.ClockFunc(func() time.Time { + return now + }) +} + +func mockRandomize(t *testing.T, v int) omikuji.Randomize { + t.Helper() + return omikuji.RandomizeFunc(func(max int) int { + return v + }) +}