Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Kadai 4 - nguyengiabk #71

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions kadai4/nguyengiabk/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
kadai4
25 changes: 25 additions & 0 deletions kadai4/nguyengiabk/README.md
Original file line number Diff line number Diff line change
@@ -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":"凶"}
```
61 changes: 61 additions & 0 deletions kadai4/nguyengiabk/main.go
Original file line number Diff line number Diff line change
@@ -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)
}
71 changes: 71 additions & 0 deletions kadai4/nguyengiabk/main_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
92 changes: 92 additions & 0 deletions kadai4/nguyengiabk/omikuji/omikuji.go
Original file line number Diff line number Diff line change
@@ -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))]
}
}
72 changes: 72 additions & 0 deletions kadai4/nguyengiabk/omikuji/omikuji_test.go
Original file line number Diff line number Diff line change
@@ -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
})
}