FirestoreのSecurity RuleをLocalでテストする Step1
これに沿って進める
Firebaseの初期化は完了済み、firestore.rulesがすでにある状態で始める。
ここから -> https://youtu.be/VDulvfBpzZE?t=391
テスト用のフォルダ作成
$ mkdir test
$ cd test
$ npm init
こんな感じで入力
package name: (test)
version: (1.0.0)
description:
entry point: (index.js) test.js
test command: mocha --exit
git repository:
keywords:
author:
license: (ISC)
mochaのインストール
$ npm install mocha --save-dev
動画では、このライブラリを使うが、mogaさんに教えてもらって
新しいライブラリを使うように修正
旧:
@firebase/testingのインストール
$ npm install @firebase/testing --save-dev
新:
$ npm i -D @firebase/rules-unit-testing
$ npm i -D firebase-admin
簡単なテストを書く
const assert = require('assert');
describe("Our test app", ()=> {
it("Understands basic addition", () => {
assert.strictEqual(2+2, 4); // 動画ではequalで書かれてましたが、deprecatedになった模様。
})
})
テスト実行
$ npm test
yay!
Our test app
✓ Understands basic addition
1 passing (4ms)
Firebaseを絡めたテストに修正
const assert = require('assert');
const firebase = require('@firebase/rules-unit-testing');
const MY_PROJECT_ID = "my-project-name" // 適宜変更
describe("Our test app", ()=> {
it("Understands basic addition", () => {
assert.strictEqual(2+2, 4);
})
it("Can read items in the read-only collection", async() => {
const db =firebase.initializeTestApp({projectId: MY_PROJECT_ID}).firestore();
const testDoc = db.collection("readonly").doc("testDoc");
await firebase.assertSucceeds(testDoc.get());
})
})
動画の通り、下記エラーとなる。
FirebaseError: Failed to get document because the client is offline.
エミュレーターの起動
firebase emulators:start
再度テストしてもセキュリティルールによって、Read/Write共に全拒否しているのでエラーになる。
ここで全解放かと思いきや、それはダメと怒られる。(動画の中の話)
最小限のアクセス権を与えるようにしないといけないよと。
ルールを修正
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// lock down the db
match /{document=**} {
allow read: if false;
allow write: if false;
}
+ match /readonly/{docId} {
+ allow read: if true;
+ allow write: if false;
+ }
}
}
test実行
$ npm test
yay!
Our test app
✓ Understands basic addition
✓ Can read items in the read-only collection (220ms)
2 passing (225ms)
書き込みに失敗するテストも書く
it("Can't write to items in the read-only collection", async() => {
const db =firebase.initializeTestApp({projectId: MY_PROJECT_ID}).firestore();
const testDoc = db.collection("readonly").doc("testDoc2");
await firebase.assertFails(testDoc.set({foo: "bar"}));
})
yay!
Our test app
✓ Understands basic addition
✓ Can read items in the read-only collection (116ms)
✓ Can't write to items in the read-only collection (52ms)
3 passing (173ms)
次は、書き込みのテスト
requestにあるuserIdと、DocumentのIdが一致しなければ書き込めないようにする。
ルールはこんな感じ
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// lock down the db
match /{document=**} {
allow read: if false;
allow write: if false;
}
match /readonly/{docId} {
allow read: if true;
allow write: if false;
}
+ match /users/{userId} {
+ allow write: if (request.auth.uid === userId);
+ }
}
}
テスト追加。
認証情報を追加している
it("Can write to a user document with the same ID as our user", async() => {
const myAuth = { uid: 'user_abc', email: 'user_abc@gmail.com'}; // <-認証情報の追加
const db =firebase.initializeTestApp({projectId: MY_PROJECT_ID, auth: myAuth}).firestore();
const testDoc = db.collection("users").doc("user_abc");
await firebase.assertSucceeds(testDoc.set({foo: "bar"}));
})
異なるIDの時に失敗するテストを追加
it("Can't write to a user document with the different ID as our user", async() => {
const myAuth = { uid: 'user_abc', email: 'user_abc@gmail.com'};
const db =firebase.initializeTestApp({projectId: MY_PROJECT_ID, auth: myAuth}).firestore();
const testDoc = db.collection("users").doc("user_xyz"); // <- user_abc != user_xyz
await firebase.assertFails(testDoc.set({foo: "bar"}));
})
test実行
$ npm test
yay!
✓ Understands basic addition
✓ Can read items in the read-only collection (110ms)
✓ Can't write to items in the read-only collection (50ms)
✓ Can write to a user document with the same ID as our user (48ms)
✓ Can't write to a user document with the different ID as our user
5 passing (246ms)
リファクタ
onst assert = require('assert');
const firebase = require('@firebase/rules-unit-testing');
const MY_PROJECT_ID = "project-id";
const myId = "user_abc"; //追加
const theirId ="user_xyz"; //追加
const myAuth = { uid: myId, email: 'user_abc@gmail.com'}; //追加
function getFirestore(auth) { //追加
return firebase.initializeTestApp({projectId: MY_PROJECT_ID, auth: auth}).firestore();
}
describe("Our test app", ()=> {
it("Understands basic addition", () => {
assert.strictEqual(2+2, 4);
});
it("Can read items in the read-only collection", async() => {
const db = getFirestore(null);
const testDoc = db.collection("readonly").doc("testDoc");
await firebase.assertSucceeds(testDoc.get());
});
it("Can't write to items in the read-only collection", async() => {
const db = getFirestore(null);
const testDoc = db.collection("readonly").doc("testDoc2");
await firebase.assertFails(testDoc.set({foo: "bar"}));
});
it("Can write to a user document with the same ID as our user", async() => {
const db = getFirestore(myAuth);
const testDoc = db.collection("users").doc(myId);
await firebase.assertSucceeds(testDoc.set({foo: "bar"}));
});
it("Can't write to a user document with the different ID as our user", async() => {
const db = getFirestore(myAuth);
const testDoc = db.collection("users").doc(theirId);
await firebase.assertFails(testDoc.set({foo: "bar"}));
});
})
問題なし
Our test app
✓ Understands basic addition
✓ Can read items in the read-only collection (217ms)
✓ Can't write to items in the read-only collection (55ms)
✓ Can write to a user document with the same ID as our user (57ms)
✓ Can't write to a user document with the different ID as our user
5 passing (368ms)
投稿に関するルールを追加。
visibilityがpublicもしくは、自分の投稿であればデータ取得できるように。
match /posts/{postId} {
allow read: if (resource.data.visibility =="public") ||
(resource.data.authorId == request.auth.uid);
}
visibilityがpublicなものは取得できるテスト
it("Can read posts marked public", async() => {
const db = getFirestore(null);
const testQuery = db.collection("posts").where("visibility", "==", "public");
await firebase.assertSucceeds(testQuery.get());
});
✓ Can read posts marked public
自分の投稿を見れるかテスト
it("Can query personal posts", async() => {
const db = getFirestore(myAuth);
const testQuery = db.collection("posts").where("authorId", "==", myId);
await firebase.assertSucceeds(testQuery.get());
});
✓ Can query personal posts (129ms)
わざと失敗するテストを書く
認証済ユーザーが全ての投稿を取得できる
it("Can query all posts", async() => {
const db = getFirestore(myAuth);
const testQuery = db.collection("posts");
await firebase.assertSucceeds(testQuery.get());
});
無事失敗
Can query all posts:
FirebaseError:
false for 'list' @ L6, Property visibility is undefined on object. for 'list' @ L20
テスト修正
it("Can't query all posts", async() => {
const db = getFirestore(myAuth);
const testQuery = db.collection("posts");
await firebase.assertFails(testQuery.get());
});
✓ Can't query all posts
単一のpublicなポストを取得できるかのテスト
it("Can query a single public post", async() => {
const db = getFirestore();
const testQuery = db.collection("posts").doc("public_post");
await firebase.assertSucceeds(testQuery.get());
});
これは、もちろんデータがないので失敗する。
エミュレーター (http://localhost:4000/firestore/)にアクセスして
データを作成し、テスト通ることを確認。
✓ Can query a single public post
毎回エミュレーターを使って手動でデータを作るのは非効率なので、
コードで実行できるようにします。
initializeAdminApp
というセキュリティルールを全無視してfirestoreの操作ができるライブラリがあるので、それを使います。
functionを作成
function getAdminFirestore(auth) {
return firebase.initializeAdminApp({projectId: MY_PROJECT_ID}).firestore();
}
documentをセットするコードを追加
it("Can query a single public post", async() => {
const admin = getAdminFirestore();
const postId = "public_post";
const setupDoc = admin.collection("posts").doc(postId);
await setupDoc.set({authorId: theirId, visibility: "public"});
const db = getFirestore();
const readQuery = db.collection("posts").doc(postId);
await firebase.assertSucceeds(readQuery.get());
});
✓ Can query a single public post (60ms)
自分のプライベートの投稿は、自分は取得できるテスト
it("Can read a private post belonging to the user", async() => {
const admin = getAdminFirestore();
const postId = "private_post";
const setupDoc = admin.collection("posts").doc(postId);
await setupDoc.set({authorId: myId, visibility: "private"});
const db = getFirestore(myAuth);
const readQuery = db.collection("posts").doc(postId);
await firebase.assertSucceeds(readQuery.get());
});
✓ Can read a private post belonging to the user
他人のプライベート投稿は、取得できないテスト
it("Can't read a private post belonging to the another user", async() => {
const admin = getAdminFirestore();
const postId = "private_post";
const setupDoc = admin.collection("posts").doc(postId);
await setupDoc.set({authorId: theirId, visibility: "private"});
const db = getFirestore(myAuth);
const readQuery = db.collection("posts").doc(postId);
await firebase.assertFails(readQuery.get());
});
✓ Can't read a private post belonging to the another user
エミュレータの中に、テストデータが残るので終了後に自動で削除する
after (async() => {
await firebase.clearFirestoreData({projectId: MY_PROJECT_ID});
});
テストをクリーンな状態で行うように、各テスト前にも自動で削除する
beforeEach (async() => {
await firebase.clearFirestoreData({projectId: MY_PROJECT_ID});
});
最終的なテストコード
const assert = require('assert');
const firebase = require('@firebase/rules-unit-testing');
const MY_PROJECT_ID = "project-id";
const myId = "user_abc";
const theirId ="user_xyz";
const myAuth = { uid: myId, email: 'user_abc@gmail.com'};
function getFirestore(auth) {
return firebase.initializeTestApp({projectId: MY_PROJECT_ID, auth: auth}).firestore();
}
function getAdminFirestore(auth) {
return firebase.initializeAdminApp({projectId: MY_PROJECT_ID}).firestore();
}
beforeEach (async() => {
await firebase.clearFirestoreData({projectId: MY_PROJECT_ID});
});
describe("Our test app", ()=> {
it("Understands basic addition", () => {
assert.strictEqual(2+2, 4);
});
it("Can read items in the read-only collection", async() => {
const db = getFirestore(null);
const testDoc = db.collection("readonly").doc("testDoc");
await firebase.assertSucceeds(testDoc.get());
});
it("Can't write to items in the read-only collection", async() => {
const db = getFirestore(null);
const testDoc = db.collection("readonly").doc("testDoc2");
await firebase.assertFails(testDoc.set({foo: "bar"}));
});
it("Can write to a user document with the same ID as our user", async() => {
const db = getFirestore(myAuth);
const testDoc = db.collection("users").doc(myId);
await firebase.assertSucceeds(testDoc.set({foo: "bar"}));
});
it("Can't write to a user document with the different ID as our user", async() => {
const db = getFirestore(myAuth);
const testDoc = db.collection("users").doc(theirId);
await firebase.assertFails(testDoc.set({foo: "bar"}));
});
it("Can read posts marked public", async() => {
const db = getFirestore(null);
const testQuery = db.collection("posts").where("visibility", "==", "public");
await firebase.assertSucceeds(testQuery.get());
});
it("Can query personal posts", async() => {
const db = getFirestore(myAuth);
const testQuery = db.collection("posts").where("authorId", "==", myId);
await firebase.assertSucceeds(testQuery.get());
});
it("Can't query all posts", async() => {
const db = getFirestore(myAuth);
const testQuery = db.collection("posts");
await firebase.assertFails(testQuery.get());
});
it("Can query a single public post", async() => {
const admin = getAdminFirestore();
const postId = "public_post";
const setupDoc = admin.collection("posts").doc(postId);
await setupDoc.set({authorId: theirId, visibility: "public"});
const db = getFirestore();
const readQuery = db.collection("posts").doc(postId);
await firebase.assertSucceeds(readQuery.get());
});
it("Can read a private post belonging to the user", async() => {
const admin = getAdminFirestore();
const postId = "private_post";
const setupDoc = admin.collection("posts").doc(postId);
await setupDoc.set({authorId: myId, visibility: "private"});
const db = getFirestore(myAuth);
const readQuery = db.collection("posts").doc(postId);
await firebase.assertSucceeds(readQuery.get());
});
it("Can't read a private post belonging to the another user", async() => {
const admin = getAdminFirestore();
const postId = "private_post";
const setupDoc = admin.collection("posts").doc(postId);
await setupDoc.set({authorId: theirId, visibility: "private"});
const db = getFirestore(myAuth);
const readQuery = db.collection("posts").doc(postId);
await firebase.assertFails(readQuery.get());
});
after (async() => {
await firebase.clearFirestoreData({projectId: MY_PROJECT_ID});
});
})
最終的なルール
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// lock down the db
match /{document=**} {
allow read: if false;
allow write: if false;
}
match /readonly/{docId} {
allow read: if true;
allow write: if false;
}
match /users/{userId} {
allow write: if (request.auth.uid == userId);
}
match /posts/{postId} {
allow read: if (resource.data.visibility =="public") ||
(resource.data.authorId == request.auth.uid);
}
}