Callable HTTPS Function(Cloud Function for Firebase)のデバッグとテスト
2018/4頃にCloud Functions for Firebase SDK v1.0 がリリースされました。追加された機能の1つに「Callable HTTPS Function」というものがあります。このFunctionのデバッグとテストの方法を記述します。
目次:
まとめ
Callable HTTPS Function の中身は HTTP 関数。関数内でユーザーを特定したい場合は、Callable HTTPS Functions を使った方が良さそう。
- とりあえず実行したい場合は、ID Token を HTTP Header に格納して HTTP リクエストを送信する
- テストは、メソッドを切り出した方が楽。あるいはこの Issue の経過を見る。
Callable HTTPS Function とは
- ドキュメント:アプリから関数を呼び出す | Firebase
Callable HTTPS Function とは、ドキュメントにも記載されていますが、Cloud Function に従来ある HTTP(S) 関数を、クライアントから容易に呼び出せるようにしたものです。容易に呼び出せるというのは、今までは HTTP 関数を呼び出すには、クライアント側で HTTP リクエストを組み立てて送信しますが、それを Firebase SDK がやってくれるということになります。例えば、iOS では Swift で以下のように記述すると Callable HTTPS Functions を呼び出せます。
functions.httpsCallable("addMessage").call(["text": inputField.text]) { (result, error) in //... }
HTTP 関数との違いの1つは、リクエスト送信時に Firebase Auth・FCMトークンの認証情報が自動的に追加され、サーバー側で検証してくれることです。これは次のようなケースの場合に便利です——「iOS からの呼び出しを想定し、 Cloud Functions 内で送信元の Firebase Auth で認証されたユーザーを特定して処理を行う」といったものです。このケースではHTTP 関数ではなく Callable HTTPS Function を選択した方が良さそうです。(HTTP 関数で行う場合は、このサンプルコードのように記述します。)
中身は HTTP 関数なので、SDK を経由せずとも 純粋にHTTP 送信で呼び出すことができます。ただし、一定のルールに合わせる必要があります。そのルールは以下の通りです。
- HTTP Method は POST、Content-Type は application/json
- IDトークンは HTTP Header に、キー:Authorization、値: Bearer {idtoken} で埋め込む
- この Authorization は無くても Cloud Function はエラーを発生せずに処理します(その代わりサーバー側ではユーザーを特定できません)。
- HTTP Request の Body には、"data" キーに送信したいオブジェクトを格納する
詳細は以下にあります。
Callable HTTPS Function の準備
ここでは、TypeScript で Function を書きます。以下のドキュメントを参考にし、Callable HTTPS Function を定義します。
次のように、ユーザーのIDとメッセージを返す Function を定義しました。2つのファイルに分けた理由は、後述のテストのためです。
/src/index.ts:
import * as functions from 'firebase-functions'; import * as httpFunc from './httpfunc'; export const myFunc = functions.https.onCall(httpFunc.myFunc);
/src/httpfunc.ts:
import * as functions from 'firebase-functions'; export function myFunc(data: any, context: functions.https.CallableContext) { console.log(data); let uid = ""; if (context.auth != null) { uid = context.auth.uid; } return { uid: uid, message: "yah!" }; };
デバッグ・実行
firebase serve
を実行し、ローカルで実行した後、Postman などの HTTP ツールにて、次のように HTTP リクエストを送信します。
送信する HTTP リクエスト:
POST /***firebase の project id***/us-central1/myFunc HTTP/1.1 Host: localhost:5000 Content-Type: application/json Authorization: Bearer ***IDトークン*** { "data": { "a": "b" } }
Body { "a": "b" }
は適当な値です。***
で囲んだ部分は自分の環境に合わせて置換します。IDトークンの取得方法ですが、例として次のように iOS 側で実行し、手動で取得するという方法があります。(あらかじめ Firebase Authentication にユーザーが登録されていることが前提です)
import Firebase //... if let user = Auth.auth().currentUser { user.getIDToken { (token, _) in if let token = token { logger.debug("idToken: \(token)") } else { logger.debug("no idToken") } } }
送信すると、次のようにレスポンスが返ってきます。
返ってきた HTTP レスポンス:
{ "result": { "uid": "1hVPnUmHVoQdFWKT64amzGTqN013", "message": "yah!" } }
テスト
HTTP 越しのテストは実用的ではなさそう
先の実行では、手動でIDトークンを取得しているのでやや面倒です。テストも行えるようにコードから実行するには、Cloud Functions の単体テスト | Firebaseを参照して HTTP 関数のテストと同じように記述したいところです。 ただし、Callable HTTPS Function を HTTP 関数と同じようにテストするには、以下の難点があったので見送りました。
- IDトークンを HTTP Header に設定する必要がある。(Firebase Admin SDK で特定ユーザーのIDトークンを取得する方法がほぼ無い。Client SDK と同じように認証してからID トークンを取得する必要がある。)
これは Function 内でユーザーを特定する場合の話です。ユーザーを特定しない場合は、ID トークンが不要なので HTTP 関数と同じようにテストを記述できます。しかし、HTTP Request オブジェクトを先述のルールに合わせたり、いろいろモックのメソッドを用意する必要があるため、実用的ではありません。また、テストコード上でユーザーを認証させてIDトークンを取得する方法もありますが(参照:node.js - Testing callable cloud functions with the Firebase CLI shell - Stack Overflow、後述の方法と比べるとこれも実用的ではありません。
また、firebase-functions-test
ライブラリでは、Callable HTTPS Function のテストをサポートする予定なので、それを待つのも手です。(参照:Unit testing HTTP Callable functions · Issue #9 · firebase/firebase-functions-test · GitHub)
ということで、メソッドを切り出してテストする
ということで、メソッドを切り出してテストします。先のコード(/src/httpfunc.ts)のように、Callable HTTPS Function の記述を、HTTP レイヤーから切り出して (data: any, context: CallableContext) => any | Promise<any>
の形で定義しました。テストコードは以下の通りです。
import * as functions from 'firebase-functions'; import * as admin from 'firebase-admin'; import * as fftest from 'firebase-functions-test'; import * as chai from 'chai'; const assert = chai.assert; const test = fftest({ databaseURL: 'https://***.firebaseio.com', storageBucket: '***appspot.com', projectId: '***' }, `${__dirname}/../../../***.json`); import * as myFunctions from '../index' import * as myHttpFunctions from '../httpfunc' describe(`myFunc`, () => { it("返ってくるobjectの検証", () => { const data: any = { }; const context: any = { auth: { uid: "0123" } }; const response = myHttpFunctions.myFunc(data, context); assert.equal(response.uid, "0123"); assert.equal(response.message, "yah!"); }); });
これで、対象のユーザーのIDを指定することでテストが可能になります。切り出しのレベルは、テストの目的に合わせて変わるでしょう。