【Docker x Dredd】 hookファイルをJS以外で書く場合の注意点 / Cognitoのトークンを使ってテストを行う


https://qiita.com/ItsukiN32/items/4a92ab911d0e49c21de5

前回の記事でDockerを使ってDreddを動かすところまで書きましたが、あれから、いくつかやってみたいことが出てきました。

テストの制御はhookファイルで行いますが、前回はJavaScriptで書きました。
管理するバックエンドはJavaScripitよりもPHPやRubyの方が慣れているメンバーの方が多いという状況。
そこで、やってみたいこととして、

- **このhookファイルをPHPで書き変えたい。**

また、Cognitoなどの外部認証サービスを使ってトークンを取得する場合、hookファイルにあらかじめその取得 + セットする処理を記述する必要があり、
AWS SDK が使えるようにする必要があります。
LaravelをAPIとして扱っていて、特にJSのパッケージを利用していない場合、それだけのためにpackage.jsonに追加するのも不自然です。
そこで、次のやってみたいこととして、

- **package.jsonを汚さずにAWS SDKを導入したい。**

以上に触れてみました。

## hookファイルの言語をPHPに変更する

### Dockerを使わない場合

https://dredd.org/en/latest/hooks/php.html

Dockerを使わずにローカルでnpmを使ってdreddを管理/実行する場合、上記のURLの手順で簡単に別言語でhookファイルを扱うことができます。

```
composer require ddelnano/dredd-hooks-php --dev
```


```
dredd apiary.apib http://127.0.0.1:3000 --language=vendor/bin/dredd-hooks-php --hookfiles=./hooks*.php
```

一方で、これがDockerを使った場合、少々面倒になります。

### Dockerを使う場合

https://dredd.org/en/latest/hooks/index.html#hooks-docker

hookファイルを扱う場合、dreddのプロセスとは別プロセスでhookファイルの内容を扱うhookサーバが立ち上がります。

デフォルトではdreddが子プロセスでhookサーバを立ち上げるのですが、

dreddのデフォルトのコンテナにはphpが入っていないので、別コンテナを立ち上げて、そこでhookサーバが立ち上がるようにし、アクセスするように仕向ける必要があります。

例えば docker-compose などで一緒に立ち上げたhookサーバのコンテナの名前がphpだとすると、dredd自体が起動しているコンテナのコマンドは

```
dredd openapi.yml http://host.docker.internal:8000/api/ \ 
--language php \
-d --hooks-worker-handler-host php --hooks-worker-handler-port 61321 \
--hookfiles="openapi/dredd_cmd/hooks/*.php"
```

このようになります。

```
--hooks-worker-handler-host
--hooks-worker-handler-port
```

このオプションを使って、接続先を指定する必要があるのです。

ただ、本来的にはこれで動くはずなのですが、

https://github.com/apiaryio/dredd/issues/748#issuecomment-286710573

にあるように、`--language`オプションがうまく働いておらず、dummyスクリプトを挟む必要があるようです。

公式にあるように 

*However, hooks were not originally designed with this scenario in mind.* 

と、元々こうした実装を想定されてデザインされていなかったようなので、動作が安定しないと困るため、今回は別言語にすることを見送りました。

そもそも、hookファイルに書かれている処理はそこまで難しい処理を含まないため、別言語で書き直すことの恩恵も少ないのかなと思います。

## package.jsonを汚さずにAWS SDKを導入したい。

AWS SDKのためだけに `npm install` の工程が増えるのも煩わしいので、これを使わなくていいようにしました。
処理としては単純なのですが、Dockerファイル内にaws-sdkをinstallする工程を増やしただけです。

先述の通り、hookファイルはDreddの子プロセスとしてhookサーバが立ち上がり、それがhookファイルを読み取って処理します。

そのため、処理としては非常に単純でDreddを起動させるコンテナと同じコンテナに aws-sdk をインストールしてあげるだけです。


```docker:Dockerfile
FROM apiaryio/dredd

RUN npm install aws-sdk

CMD ["dredd"]
```

あとは、このコンテナをbuildして、指定して起動するだけです。


私はめんどくさがり屋なので、上記の処理を全てコマンドにしました。

ファイルの配置は

```
openapi
  ┠ default.yml
  ┗ dredd/
    　　 ┠ Dockerfile
      ┠ dredd.sh
      ┠ params.cfg
      ┠ tmp/
      ┗ hooks/
          ┗ hook.js
```

このようにし、 `sh dredd.sh` でdreddが起動できるようにしました。

この記事は前回の続きなので、細かい点は前回の記事を参照いただけると幸いです。

以下、実装したコード

#### 実行部分のシェルスクリプト

```sh:params.cfg
# openapi(OAS)のファイル名 
FILE='default.yml'

# hookファイル
# *.jsとすることで複数のファイルを指定可能
HOOKFILES='*.js'

# dredd.sh があるディレクトリの名前
DREDD_DIR='dredd'

# OpenAPIのURLではなく、APIのURLを書く。 
# Dockerからの通信なので、ホストマシンのlocalhostにアクセスする場合以下のように書く
# 例： API_URL='http://host.docker.internal:8000/api/'
API_URL='http://host.docker.internal:8000/api/'

IMAGE_NAME='dredd-with-aws-sdk'
```

```sh:dredd.sh
SCRIPT_DIR=$(cd $(dirname $0); pwd)
source ${SCRIPT_DIR}/params.cfg

HOOKFILES="/openapi/$DREDD_DIR/hooks/$HOOKFILES"

# OASが分割されている場合 dredd は利用できないので
# OASが分割されている場合mergeする。

docker run -it --rm -v $SCRIPT_DIR/..:/app \
properdom/swagger-merger swagger-merger \
-i app/$FILE \
-o app/$DREDD_DIR/tmp/merged.yml

# Imageがビルドされていなければビルドする

image=$(docker images | grep $IMAGE_NAME | cut -d' ' -f1 | grep -x $IMAGE_NAME)

if [ "$image" != "$IMAGE_NAME" ]; then
  docker build ${SCRIPT_DIR}/. -t $IMAGE_NAME
fi

# APIがOAS通りのレスポンスを返しているか確認する。
# 出力が長いので、詳細は ./dredd/dredd_result.txt に出力する。

docker run -it --rm -v $SCRIPT_DIR/..:/openapi \
--add-host=host.docker.internal:host-gateway \
$IMAGE_NAME sh -c \
"dredd openapi/$DREDD_DIR/tmp/merged.yml $API_URL \
-d --hookfiles=$HOOKFILES --no-color | \
tee openapi/$DREDD_DIR/tmp/dredd_result.txt | \
grep -e 'pass\:' -e 'fail\:' -e 'complete\:'"

echo "result in"
echo openapi/$DREDD_DIR/tmp/dredd_result.txt
```

#### Cognitoのトークンを使う場合のhookファイルの書き方：例

例えばCognitoのトークンを取ってきて、それを元に認証をしてテストをしたい場合以下のように書きます。

```javascript:hook.js
var hooks = require('hooks');
var stash = {};

const AWS = require('aws-sdk');
async function getCognitoJWT() {
	AWS.config.update({
		region: 'ap-northeast-1',
	});    
	var cognitoidentityserviceprovider = new AWS.CognitoIdentityServiceProvider({apiVersion: '2016-04-18'});
	return await cognitoidentityserviceprovider.initiateAuth({
		ClientId: 'クライアントID',
		AuthFlow: 'USER_PASSWORD_AUTH',
		AuthParameters: {
			USERNAME: 'ユーザ名',
			PASSWORD: 'パスワード'
		}
	}).promise()
};

// テストの一番初めに起動する処理
// cognito のトークンをstashに保存
hooks.beforeAll(function (transaction) {
	getCognitoJWT().then(function(data){
	  stash['token'] = data.AuthenticationResult.IdToken;
		console.log(stash['token']);
	}, function(err){
		console.log(err);
	});
});

hooks.beforeEach(function (transaction) {
	// skipするリクエストを指定
	if (
		transaction.name.match(/auth\/(login|refresh)/) != null || 
		transaction.expected.statusCode == "429"
	) {
		transaction.skip = true;
	};

	// 401以外はトークンをつける
	if (stash['token'] != undefined) {
		if (transaction.expected.statusCode != "401") {
			transaction.request.headers['Authorization'] = "Bearer " + stash['token'];
		};
	};
});

// 各リクエスト固有の処理

hooks.before("/hc > Health-Check Endpoint. > 200 > text/html", function (transaction) {
	transaction.skip = true;
});
```

※固有のリクエストに処理を記述する場合 `hooks.before(リクエスト名, 処理)` という文法なのですが、リクエスト名は
https://github.com/itsuki-n22/dredd_cmd/blob/main/dredd-names.sh
を参考に取得してもらえると嬉しいです。