バックエンドのテスト
https://www.udemy.com/course/introduction-to-testing-in-go-golang/
を参考に実装しつつまとめていく
ハンドラのテスト
流れ
- requestにデータを詰める
- ハンドラを実行する
- responseからデータを取り出してチェックする
テンプレ
func Test_Example(t *testing.T) { //テストケースを定義する tests := []struct { name string data string status int }{ {"test 1", "data1", http.StatusBadRequest}, {"test 2", "data2", http.StatusOK}, } for _, test := range tests { //テスト実行 t.Run(test.name, func(t *testing.T) { //requestの作成 r := httptest.NewRequest(http.MethodPost, "/test", nil) //responseの作成 w := httptest.NewRecorder() //handlerの作成 handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) //実行 handler.ServeHTTP(w, r) //responseがwに書き込まれるのでテストケースと比較 want_status := test.status got_status := w.Result().StatusCode if got_status != want_status { t.Errorf("statusCode: got %d, want %d", got_status, want_status) } if want_status == http.StatusOK { //responseのチェック } else { //エラーのチェック } }) } }
requestにデータを詰める
//json //Marshalなどで文字列化して、NewRequestの引数(body)に入れる r := httptest.NewRequest(http.MethodPost, "/test", strings.NewReader(jsonString)) //context //WithContextで渡す ctx := context.WithValue(context.Background(),ContextKey, "value") handler.ServeHTTP(w, r.WithContext(ctx))
ミドルウェアのテスト
func myMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { next.ServeHTTP(w, r) }) } //適当なハンドラを作ってmiddlewareに渡す emptyHandler := http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {}) handler := myMiddleware(emptyHandler) handler.ServeHTTP(w, r) //middleware内でcontextにセットした値はハンドラ内から取り出してチェック emptyHandler := http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { value := r.Context().Value(Key) if value ==nil{ t.Error("") } handler := myMiddleware(emptyHandler) })
APIリクエストのテスト
書き途中
//サーバーを立てる ts = httptest.NewTLSServer(app.registerHandlers()) defer ts.Close() //request作成 r := httptest.NewRequest(http.MethodGet, ts.URL+"/_chk", nil) //エラー防止 r.RequestURI = "" //client作成, request実行 res, err := ts.Client().Do(r) defer res.Body.Close() if res.StatusCode != http.StatusOK{ t.Error("...") }
ハンドラのテストと同様にhttptest.NewRequest
からrequestを作成できますが、エラーが発生するためRequestURIの上書きが必要でした。
http.Request RequestURI field when making request in go - Stack Overflow
DB絡みをどうするか
正解はわかりませんが、現在はテスト専用のDBコンテナに接続してテスト毎にDBをクリア(DROP TABLEしてマイグレーションし直す)しています。
既に感じているデメリットとして
- テスト全体で一つのDBを共有しているため、
-p 1
で並列実行を止める必要がある - 複数パッケージでDBを使おうとすると、importの都合でDBをクリアする関数がアプリ側に漏れてしまう
リクエストからレスポンスまで一貫して挙動を確認できるのは良いのですが、将来的にはDBコンテナを使ったテストはモデル内で完結させて、その他のパッケージからモデルを利用する時はモックを返すようにすると良いのかもしれません。
そうすることで並列実行が可能になり、クリア関数をテストファイル内に留めておくことができます。(一長一短な感じしますが・・・)
書いた後に知りましたがDBのクリアに関してはテスト終了後にロールバックするという裏技があるようなので、まず地獄みたいな関数を無くそうと思います。
テスト毎にロールバックする方式にしたところスピードも上がり大体解決しました。
トランザクション | GORM - The fantastic ORM library for Golang, aims to be developer friendly.
auto_incrementの値がロールバックされないようなので、IDの返り値を決め打ちでチェックしてる箇所は修正が必要になります。
ログイン、リフレッシュ処理の流れ
地獄みたいなコードになってたので整理したものをまとめておきます。
正式なフローとかではないのでご了承ください。
SPAを想定しています。
データ
アクセストークン
- JWT
- このトークンをparseすることでuser_idを取り出し、そのユーザーからのアクセスとみなす
- アクセスの度に取り出したuser_idからUsersテーブルを引くようなことは基本しない(はず)
- 有効期間は短くする(1時間程度)
- バックエンドには保存しない
- フロントエンドにはメモリ(javascriptの変数)上に保存する
リフレッシュトークン
- JWT
- アクセストークンを更新するためのトークン
- 有効期間を長くする(〜数ヶ月)
- バックエンドではセッションIDとともにDBに保存する
- JWT単体で無効化できない、payloadの中身が読める、サイズが大きいなどの理由でフロントには返さない
- DBのレコードを削除することで無効化する
セッションID
- UUID
- DBに保存したリフレッシュトークンにアクセスするための文字列
- バックエンドではリフレッシュトークンと一緒にDBに保存する
- フロント側ではリフレッシュトークンの代わりにクッキー内に保存する
- クッキーをhttpOnly属性にすることで、javascriptから読み取られない状態でフロントに保存することができる
まとめ
アクセストークン | リフレッシュトークン | セッションID | |
---|---|---|---|
役割 | そのユーザーになれる | アクセストークンを再発行できる | リフレッシュトークンの取得と削除 |
有効期間 | 短時間(~1時間) | 長時間(~数ヶ月) | 永続 |
形式 | JWT | JWT | UUID |
中身 | user_idなど | user_idなど | - |
保存場所(バックエンド) | 保存しない | DB | DB |
保存場所(フロントエンド) | メモリ内 | 保存しない | cookie(httpOnly) |
流れ
サインイン
サインアップ(ID、パスワード登録)済みとします。
- (フロント)ID、パスワードを送信
- (バックエンド)ID、パスワードのチェック
- (バックエンド)OKならリフレッシュトークンとセッションIDを生成し、保存
- (バックエンド)セッションIDを乗せたクッキーをSet-Cookieでフロントに返す
- (アクセストークンも同時に返してセットさせるか、またはブラウザ更新して次のアクセストークン取得処理を実行させる)
アクセストークンの取得、リフレッシュ
アクセストークンはメモリ上に保存しているため、ページを離れると削除されてしまう。
ブラウザ更新時や再訪問時にはセッションIDを用いてアクセストークンを取得し直す必要がある。
- (フロント)リクエストを送信
- (バックエンド)クッキーからセッションIDを取り出し、リフレッシュトークンを検索
- (バックエンド)リフレッシュトークンをチェック
- (バックエンド)有効ならセッションIDとリフレッシュトークンを更新して再保存、アクセストークン生成
- (バックエンド)セッションIDをクッキーにセットし、レスポンスでアクセストークンを返す
- (フロント)アクセストークンをメモリに保存
アクセストークンの自動更新
ユーザーが長時間操作を行うと、途中でアクセストークンが切れて通信できなくなってしまう。
アクセストークンが切れるより前のタイミングでアクセストークンの取得、リフレッシュを再実行し、アクセストークンを更新する必要がある。
setIntervalなどを使い、定期的に実行させる。
アクセストークンによるAPI通信
- (フロント)バックエンドへの各リクエストに対して、アクセストークンをAuthorizationヘッダにつけて実行
- (バックエンド)Authorizationヘッダからアクセストークンを取り出し、検証
- (バックエンド)parseして得られたuser_idからのアクセスとして取り扱う
サインアウト
フロントエンドからhttpOnlyクッキーが削除できないので、バックエンド経由で削除する必要がある
その他
reactの場合
アクセストークンがセットされる前に認証が必要なリクエストが飛んで失敗するので、成否に関わらずアクセストークン取得処理が終わってから各ページがrenderされるようにする。
{authFinished ? <RouterProvider router={router} /> : null}
参考URL
セキュアなトークン管理方法 - Carpe Diem
より良い記事
%sとか%dとかの一覧
これらをそもそもなんと呼ぶのかわからず検索に詰まりました。
pkg.go.dev
日本語だと書式指定子と呼ぶそうです。
リンク先から察するに英語だとformat verbsでしょうか。
cannot find package ~~ in GOROOT or GOPATH
golang関連の教材のコードをリファレンスとしてすぐに動かせる状態で一つのコンテナに押し込めようとしたところ、パッケージがインストールされているにもかかわらずvscodeのgoplsが上記のエラーを吐いてしまいました。
下記の通りそれぞれのgo.mod
のパスをworkspaceに追加することで解決しました。
stackoverflow.com
github.com
「Create React App Sampleをインストールします」を消す
(react)tailwindcss導入->vscodeで自動補完するまで
tailwindcssのインストール
vscode拡張のインストール
補完や色の確認ができるようになり便利です tailwindcss.com
prettierとの連携
導入すると自動フォーマット時にtailwindcssのclass記述がいい感じの順番にソートされるようになりスッキリします
prettier本体, 拡張プラグイン、vscode拡張のインストール
npm install -D prettier prettier-plugin-tailwindcss vscode拡張でprettier検索してインストール
prettier設定ファイル作成
prettierとtailwindcssの連携
prettier設定ファイルにpluginsを追記
plugins: ['prettier-plugin-tailwindcss'],
自動フォーマットの有効化
vscode->preferences->settings Workspaceに切り替え formatterで検索->Default Formatterでprettier選択、Format On Saveにチェック
動作確認
以下のコンポーネントをコピペ->保存していい感じにフォーマットされればOK
const TEST=()=> { const value=1 return ( <h1 className="font-bold underline text-3xl"> Hello world! </h1> ) }
追加作業
特定の変数名に対して補完を適用させたい
共通化などの都合でclassNameに指定する文字列を外部で宣言したくなると思います。
現状のままだと素のstringに対してtailwindcssの拡張が機能しないので、使いにくくなってしまいます。
const style="text-3xl font-bold underline" // <- ここに補完やフォーマットを適用させたい <div className={style}>...
解決策を調べたところ、変数名のパターンを指定して補完を適用させる方法があるようです。 stackoverflow.com 自分はTAILWINDという名前のオブジェクトとtailwind_から始まるstringに補完を適用させたかったので以下を指定してみました。
.vscode/settings.json === "tailwindCSS.classAttributes": [ "class", "className", "ngClass", "TAILWIND", "tailwind_.*" ]
const TAILWIND = { BG_COLOR: 'bg-slate-100', TEXT_COLOR: 'text-zinc-400', TEXT_COLOR_STRONG:'text-orange-600' } const tailwind_style = 'text-3xl font-bold underline'
またprettierによる自動ソートも同じような設定ができるようですが、変数名に対しては適用できませんでした。
GitHub - tailwindlabs/prettier-plugin-tailwindcss: A Prettier plugin for Tailwind CSS that automatically sorts classes based on our recommended class order.
とりあえず補完が効くようになったので満足してます。