SuparOak でフォームと Cookie を送ってみる

先日、Super なんとかって記事を書いたんですが、その最後に書いた「ページの中からトークンを取得して、それを含めてリクエスト。なんてのを試したい~~」をやってみます。

参考

実装

テスト対象のアプリケーション

テスト対象のアプリケーションは次の通りです。
GET /form にアクセスし、返ってきたフォームを送ると / に飛ばされるという動きです。

server.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
import {
Application,
type Context,
Router,
} from "https://deno.land/x/oak@v10.4.0/mod.ts";
import {
computeAesGcmTokenPair,
computeVerifyAesGcmTokenPair,
} from "https://deno.land/x/deno_csrf@0.0.4/mod.ts";

const CRYPT_KEY = Deno.env.get("CRYPT_KEY") as string;
const router = new Router();

router.post("/form", async ({ request, response, cookies }: Context) => {
const form = await request.body({ type: "form" }).value;
const csrfToken = form.get("csrf_token") || "";

const csrfCookie = (await cookies.get("csrf_cookie")) || "";

const result = computeVerifyAesGcmTokenPair(CRYPT_KEY, csrfToken, csrfCookie);

if (!result) {
return response.redirect("/form");
}

response.redirect("/");
});

router.get("/form", async ({ response, cookies }: Context) => {
const pair = computeAesGcmTokenPair(CRYPT_KEY, 123);
pair.tokenStr;
pair.cookieStr;

await cookies.set("csrf_cookie", pair.cookieStr);
response.body = `
<html>
<body>
<form METHOD="POST">
<input type="text" name="text">
<input type="hidden" name="csrf_token" value="${pair.tokenStr}">
<button type="submit">submit</button>
</form>
</body>
</html>
`;
});

router.get("/", ({ response }: Context) => {
response.body = "success";
});

const app = new Application();
app.use(router.routes());
app.use(router.allowedMethods());

export { app };
app.ts
1
2
3
import { app } from "./server.ts";

app.listen({ port: 8080 });

テストコード

アプリケーションの内容に基づいて、次のシナリオでテストを用意します。

  • Form を取得するための GET リクエストを実行
  • レスポンスの Header から、set-cookie を文字列操作し、cookie の値を取得
  • アプリケーションが返してくる HTML をパースし、トークンの取得
  • Cookie とトークンを含めて POST リクエスト
  • レスポンスの内容が / へのリダイレクトであることを確認

テストコードは、次のようになります。

app_test.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import { superoak } from "https://deno.land/x/superoak@4.7.0/mod.ts";
import * as queryString from "https://deno.land/x/querystring@v1.0.2/mod.js";
import { assertEquals } from "https://deno.land/std@0.148.0/testing/asserts.ts";
import {
Document,
DOMParser,
} from "https://deno.land/x/deno_dom/deno-dom-wasm.ts";
import { app } from "./server.ts";

Deno.test("#1 form, cookie test", async () => {
// フォーム取得
const request1 = await superoak(app);
const getForm = await request1.get("/form");

// cookie を引き抜く
const csrfCookieValue = `${getForm.headers["set-cookie"]}`
.split(";")[0]
.split("csrf_cookie=")[1];

// HTMLをパースし、埋め込んである値を取得
const doc1: Document = new DOMParser().parseFromString(
getForm.text,
"text/html"
)!;
const csrfToken = doc1.querySelector('[name="csrf_token"]');
const csrfTokenValue = csrfToken?.getAttribute("value") || "";

// Form を application/x-www-form-urlencoded 形式に合うように文字列作成
// Cookie を付与してリクエスト
const request2 = await superoak(app);
const response = await request2
.post("/form")
.set("content-type", "application/x-www-form-urlencoded")
.set("cookie", `csrf_cookie=${csrfCookieValue}; `)
.send(`csrf_token=${encodeURIComponent(csrfTokenValue)}&text=test-text`)
// 以下2つでもいい
//.send( queryString.stringify({ csrf_token: csrfTokenValue, text: "test-text" }))
//.send(`csrf_token=${csrfTokenValue.replace(/\+/g, "%2B")}&text=test-text`)
.expect(302);

assertEquals(response.headers["location"], "/");
});

実行

以下の通り実行します。

1
2
3
4
5
6
7
8
9
$ CRYPT_KEY=01234567012345670123456701234568 deno test -A app_test.ts
Check file:///usr/src/app/app_test.ts

# 出力内容省略 使用した外部モジュールのテストも実行されている

Failed token from HMAC verification not pair. ... ok (4ms)
#1 form, cookie test ... ok (72ms)

ok | 9 passed | 0 failed (236ms)

テストが通りました。


というわけで、作りたかったテストを書けました。
cookie 周りの操作は、「なるほど、これでいいのか感」がありました。
ブラウザで実際に動作させた場合と同じ操作を再現、アプリ側に不要な考慮をせずに済んだのが良いところです。
今後 oak で込み入ったもの作るときには、同じ方法でテストを作り込んでいきたいところです。

ではでは。