Supabase Edge Functionsのテストを書く
公式ドキュメント
FunctionsでのSupabase clientは、ブラウザ実行(SPA)のときと同じように、Anon Keyを利用してRow Level Securityするか、Service Roke Keyを使ってルート権限で実行させる(別途認証・認可ロジックを自分で書く)パターンに分かれる。
両方のパターンに対応したヘルパーはこんな感じ。(アンダースコアから始まるディレクトリはルーティングから無視されるのでその中に共通ファイルを入れる。)
supabase/functions/_shared/client.ts
import { SupabaseClientOptions, createClient } from 'https://esm.sh/@supabase/supabase-js@2'
// import { createClient, SupabaseClient } from 'jsr:@supabase/supabase-js'
import { Database } from "../types/supabase.ts"
type Params = {
auth?: string
root?: boolean
}
const defaultParams: Params = {
root: false
}
export const getSupabaseClient = <
SchemaName extends string & keyof Database = 'public' extends keyof Database
? 'public'
: string & keyof Database,
>({ auth, root }: Params = defaultParams) => {
const supabaseUrl = Deno.env.get('SUPABASE_URL')!
const supabaseKey = root ? Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")! : Deno.env.get('SUPABASE_ANON_KEY')!
const headers: Record<string, string> = {}
if (auth) {
headers.Authorization = auth
}
const options: SupabaseClientOptions<SchemaName> = {
global: {
headers
},
auth: {
autoRefreshToken: false,
persistSession: false,
detectSessionInUrl: false,
}
}
return createClient<Database, SchemaName>(supabaseUrl, supabaseKey, options)
}
ここで、Databaseという型は
supabase gen types typescript --local
の結果である。フロントエンドとFunctionsが同じディレクトリにあるプロジェクトではpackage.jsonに以下を追加すると、両方で同じ型を使える。
"scripts": {
...
"types": "supabase gen types typescript --local | tee src/types/supabase.ts supabase/functions/types/supabase.ts"
},
autoRefreshTokenをfalseにしておかないと、テストケースで以下のようなエラーが出てハマった。
error: Leaks detected:
- An interval was started in this test, but never completed. This is often caused by not calling `clearInterval`. The operation was started here:
at Object.queueUserTimer (ext:core/01_core.js:738:9)
at setInterval (ext:deno_web/02_timers.js:83:15)
at p._startAutoRefresh (https://esm.sh/v135/@supabase/auth-js@2.64.2/denonext/auth-js.mjs:2:41246)
at async p.startAutoRefresh (https://esm.sh/v135/@supabase/auth-js@2.64.2/denonext/auth-js.mjs:2:41742)
また、このSupabase Client作成に関するテストファイルも一応作ってみた。
client.test.ts
import { assertEquals, assertInstanceOf } from "jsr:@std/assert"
import { getSupabaseClient } from "../_shared/client.ts";
import { AuthError } from 'https://esm.sh/@supabase/supabase-js@2'
Deno.test('Admin can create user', async (t) => {
const client = getSupabaseClient({ root: true })
let userId: string | undefined
await t.step("Create user", async () => {
const { data, error } = await client.auth.admin.createUser({
email: "hoge@gmail.com",
password: "password",
})
assertEquals(error, null)
assertEquals(data.user?.email, "hoge@gmail.com")
userId = data.user?.id
})
await t.step("Delete user", async () => {
const { error } = await client.auth.admin.deleteUser(userId!);
assertEquals(error, null);
});
})
Deno.test("Anon can call edge function", async () => {
const client = getSupabaseClient()
const { data, error } = await client.functions.invoke("ping")
assertEquals(data, "pong")
assertEquals(error, null)
})
Deno.test("Anon do not have access to admin functions", async () => {
const client = getSupabaseClient()
const { data, error } = await client.auth.admin.createUser({
email: "hoge@gmail.com",
password: "password",
})
assertEquals(data.user, null)
assertInstanceOf(error, AuthError)
assertEquals(error?.code, "not_admin")
})
コレをfunctions/test/client.test.tsとなるように置きまして、
Functionsのping用のエントリポイントを追加する:functions/ping/index.ts
import "https://esm.sh/@supabase/functions-js/src/edge-runtime.d.ts"
Deno.serve((req) => {
const headersObject = Object.fromEntries(req.headers)
const headersString = JSON.stringify(headersObject, null, 2)
console.log(`Request headers: ${headersString}`)
return new Response("pong")
})
テストファイルは
supabase/functions/tests
にhoge.test.ts
という形式でおく
テストの実行には、別途環境変数ファイルを用意して指定する必要がある。
Functionsのローカル実行では自動的にSUPABASE_
から始まるもろもろの環境変数がセットされていて、追加でカスタム環境変数が必要なら
supabase/functions/.env
とかを作って
supabase functions serve --env-file ./supabase/functions/.env
こうやって指定すればいい。
ただ、テスト実行時は普通のdeno testをホストデバイスで実行する為、そのような自動環境変数はセットされていない。
だからsupabase/functions/.env.test
を作って
SUPABASE_URL="http://localhost:54321"
SUPABASE_ANON_KEY=""
SUPABASE_SERVICE_ROLE_KEY=""
MY_CUSTOM_KEY=""
として
deno test --allow-all --env=./supabase/functions/.env.test supabase/functions/tests/*.test.ts
すればいい。.env.testをgitignoreすることを忘れずに。
学校の授業でディレクトリ(パス)トラバーサル攻撃の概念を知ったから、functionsフォルダ以下のファイルに対する直接的なアクセスを全部ブロックできるかテストを書いてみた。
....あれ、やばくね?