GoでValidationを実装してみる①

# GoでValidationを実装してみる①
こんにちは、株式会社アクシス福岡オフィスのueharaです。
この記事は福岡支部にて定期的に行っている開発合宿での内容を記事にした物です。
開催された日時は「2020/01/25」と約1ヶ月前に行われたものとなり、大分投稿が遅くなってしまいましたが、色々と一旦落ち着いたのでやっとのことで投稿するに至りました。

私がこの開発合宿で記事を上げるのは2回目となり、基本的には共同開発しているSNSアプリのバックエンド回りの実装を記事にしています。
興味のある方は、[前回の記事](https://qiita.com/fumiya-uehara/items/d5e10380299a22995c64)も読んで頂けると嬉しいです。

では、早速始めます！
今回はGoを使ってEmailのValidationを実装したいと思います。

## 要件
ユーザによる新規登録実施前にEmailの重複、及びリクエストされたEmailの文字列が正しい形となっているかをチェックする機能。

### 利用シーン（ユーザーストーリー）

1. SNSアプリに初めてアクセスする
2. 登録制のサイトなので「新規登録」ボタンをクリックする
3. 新規登録画面に遷移する
4. メールアドレスを入力して次へ

上記「手順4」にてユーザーが「次へ」ボタンをクリック後にフロントエンド 側でリクエストデータを詰めてバックエンド側で処理します。この時Emailのバリデーションを実施します。

## 設計
エンドポイント、インタフェース（リクエストデータやレスポンスデータ）を定義していきたいと思います。

### Endpoint

`POST /users/check_email`

### インタフェース

**リクエストデータ**

```json
{
  "email": "xxx@xxx.com"
}
```

**レスポンスデータ**

```json
{
	"status": HTTP STATUS
	"message": Successful Message or Failed Message
}
```

- 成功
	- status: 200
	- message:  successful!
- リクエストメソッドがPOST以外
	- status: 400
	- message: request method type is need post
- Content-Typeが”application/json”以外
	- status: 400
	- message: Content-Type entity header is need “application/json”
- emailキーの値がvalidationに通らない
	- status: 400
	- message:  validation target value format is wrong
- Emailに重複がある
	- status: 422
	- message:  already registered

### 実装方針
「[go-playground/validator](https://github.com/go-playground/validator)」を利用していきます。
そのため、「emailキーの値がvalidationに通らない」はこのパッケージに全て任せちゃいます。

また、DB操作は [jinzhu/gorm](http://github.com/jinzhu/gorm) を利用します。

## 実装
今回は「go-playground/validator」を利用した実装とgormを利用したDatabase参照を必要とするvalidationは実装できていません。

そのため、今回の対象となるのは以下です。

- POSTメソッドでない場合
	- statusが400
	- messageが「request method type is need post」であること
- Content-Typeが”application/json”以外である場合
	- statusが400
	- messageが「Content-Type entity header is need “application/json”」であること

本来はJSONで返ってくる値のアサーションまでを行いたかったのですが、できませんでした。
なので、「意図した真偽値を返すこと」をテストするところまで実装してみました。
JSONをアサーションするにはエラーハンドリングとか考慮しないといけず、お恥ずかしい話勉強不足で上手く実装できませんでした。。。

```go:main.go
package main

import (
	"package/validator"
	"log"
	"net/http"
)

func main() {
	http.HandleFunc("/users/check_email", validator.CheckEmail)
	err := http.ListenAndServe(":9000", nil) //監視するポートを設定する。
	if err != nil {
		log.Fatal("ListenAndServe: ", err)
	}
}
```

**※上記`main.go`について一つ注意事項がございます。コードを載せていますが、`main.go`で読み込んでいる`”package/validator”`は自身のリポジトリでパッケージ化しているものとなるため、コードをそのまま載せても動作しません。その点のみご理解ください。**

```go:user.go
package validator

import (
	"encoding/json"
	"net/http"
)

type HttpResult struct {
	Status int `json:"status"`
	Message string `json:"message"`
}

func newDefaultHttpResult() *HttpResult {
	result := new(HttpResult)
	result.Status = http.StatusOK
	result.Message = "successful"
	return result
}

func CheckEmail(w http.ResponseWriter, r *http.Request) {
	result := newDefaultHttpResult()

	if isPost := isPostMethod(r); !isPost {
		result.Status = http.StatusBadRequest
		result.Message = "request method type is need post"
	}

	if isJson := isApplicationJson(r); !isJson {
		result.Status = http.StatusBadRequest
		result.Message = "Content-Type entity header is need application/json"
	}

	if err := json.NewEncoder(w).Encode(result); err != nil {
		panic(err)
	}
}

func isApplicationJson(r *http.Request) bool {
	if r.Header.Get("Content-Type") != "application/json" {
		return false
	}
	return true
}

func isPostMethod(r *http.Request) bool {
	if r.Method != "POST" {
		return false
	}
	return true
}

```


## TEST
上記でチェックしている「POSTメソッドでない場合」と「Content-Typeが”application/json”以外である場合」のテストを書きたいと思います。対象の関数は「isApplicationJson」と「isPostMethod」ですね。

```go:user_test.go
package validator

import (
	"net/http"
	"testing"
)

// リクエストメソッドがPOSTであるためtrueが返ってくることを期待するテストです。
func TestIsPostMethodOK(t *testing.T) {
	request, _ := http.NewRequest("POST", "", nil)
	if result := isPostMethod(request); !result {
		t.Fatal("failed test")
	}
}

// リクエストメソッドがGETであるためfalseが返ってくることを期待するテストです。
func TestIsPostMethodNG(t *testing.T) {
	request, _ := http.NewRequest("GET", "", nil)
	if result := isPostMethod(request); result {
		t.Fatal("failed test")
	}
}

// Content-Typeがapplication/jsonであるためtrueが返ってくることを期待するテストです。
func TestIsApplicationJsonOK(t *testing.T) {
	request, _ := http.NewRequest("", "", nil)
	request.Header.Set("Content-Type", "application/json")
	if result := isApplicationJson(request); !result {
		t.Fatal("failed test")
	}
}

// Content-Typeがfailedであるためfalseが返ってくることを期待するテストです。
func TestIsApplicationJsonNG(t *testing.T) {
	request, _ := http.NewRequest("", "", nil)
	request.Header.Set("Content-Type", "failed")
	if result := isApplicationJson(request); result {
		t.Fatal("failed test")
	}
}
```


## まとめ
実際に書いてみましたが、勉強不足で思うように実装できなかったです。
本腰入れてGoの勉強をすることにします。
忘れるといけないので現時点で「課題」だと思うものを列挙しとこうと思います。

- そもそもJSONが返ってくるはずなのにそれに対するテストができていない
- エラーハンドリングされていないのでresultが上書きされる
- 真偽値のテストをしているが、最終的なデータをアサーションできないと意味ない

今回実装できなかったところ＋上で挙げた課題を②で対応できたらなぁと考えています。
それでは〜