From 4f3fc3bc4e54914e137c68267eff7cad64c9d8f7 Mon Sep 17 00:00:00 2001 From: Achim Weimert Date: Wed, 9 May 2018 11:09:43 +0100 Subject: [PATCH 1/7] Add jest matcher Adding jest matchers to conveniently use targaryen with jest. Usage: ``` const { getDatabase, toAllowRead, toAllowUpdate, toAllowWrite, json } = require('targaryen/plugins/jest'); expect.extend({ toAllowRead, toAllowUpdate, toAllowWrite, }); const RULES_PATH = 'database.rules.json'; const rules = json.loadSync(RULES_PATH); const initialData = {}; const database = getDatabase(rules, initialData); test('basic', () => { expect(database.as(null)).not.toAllowRead('/user'); expect(database.as(null)).toAllowRead('/public'); expect(database.as({ uid: '1234'})).toAllowWrite('/user/1234', { name: 'Anna', }); }); ``` or using the generic matcher: ``` const { getDebugDatabase, toBeAllowed, json } = require('targaryen/plugins/jest'); expect.extend({ toBeAllowed, }); const RULES_PATH = 'database.rules.json'; const rules = json.loadSync(RULES_PATH); const initialData = {}; // NOTE: Create a database with debug set to true for detailed errors const database = getDebugDatabase(rules, initialData); test('generic', () => { expect(database.as(null).read('/user')).not.toBeAllowed(); expect(database.as({ uid: '1234' }).write('/user/1234', { name: 'Anna', })).toBeAllowed(); }); ``` --- plugins/jest.js | 100 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 plugins/jest.js diff --git a/plugins/jest.js b/plugins/jest.js new file mode 100644 index 0000000..01557ca --- /dev/null +++ b/plugins/jest.js @@ -0,0 +1,100 @@ +/** + * targaryen/plugins/jest - Reference implementation of a jest plugin for + * targaryen. + * + */ + +'use strict'; + +const targaryen = require('../'); + +function toBeAllowed({ info, allowed }) { + const pass = allowed === true; + const message = pass + ? () => `Expected operation to be ${this.utils.EXPECTED_COLOR('denied')} but it was ${this.utils.RECEIVED_COLOR('allowed')}\n\n${info}` + : () => `Expected operation to be ${this.utils.EXPECTED_COLOR('allowed')} but it was ${this.utils.RECEIVED_COLOR('denied')}\n\n${info}`; + + return { + message, + pass, + }; +} + +function toAllowRead(database, path, options) { + const { info, allowed } = database + .with({ debug: true }) + .read(path, options); + + const pass = allowed === true; + const message = pass + ? () => `Expected ${this.utils.EXPECTED_COLOR('read')} to be ${this.utils.EXPECTED_COLOR('denied')} but it was ${this.utils.RECEIVED_COLOR('allowed')}\n\n${info}` + : () => `Expected ${this.utils.EXPECTED_COLOR('read')} to be ${this.utils.EXPECTED_COLOR('allowed')} but it was ${this.utils.RECEIVED_COLOR('denied')}\n\n${info}`; + + return { + message, + pass, + }; +} + +function toAllowWrite(database, path, value, options) { + const { info, allowed } = database + .with({ debug: true }) + .write(path, value, options); + + const pass = allowed === true; + const message = pass + ? () => `Expected ${this.utils.EXPECTED_COLOR('write')} to be ${this.utils.EXPECTED_COLOR('denied')} but it was ${this.utils.RECEIVED_COLOR('allowed')}\n\n${info}` + : () => `Expected ${this.utils.EXPECTED_COLOR('write')} to be ${this.utils.EXPECTED_COLOR('allowed')} but it was ${this.utils.RECEIVED_COLOR('denied')}\n\n${info}`; + + return { + message, + pass, + }; +} + +function toAllowUpdate(database, path, patch, options) { + const { info, allowed } = database + .with({ debug: true }) + .update(path, patch, options); + + const pass = allowed === true; + const message = pass + ? () => `Expected ${this.utils.EXPECTED_COLOR('update')} to be ${this.utils.EXPECTED_COLOR('denied')} but it was ${this.utils.RECEIVED_COLOR('allowed')}\n\n${info}` + : () => `Expected ${this.utils.EXPECTED_COLOR('update')} to be ${this.utils.EXPECTED_COLOR('allowed')} but it was ${this.utils.RECEIVED_COLOR('denied')}\n\n${info}`; + + return { + message, + pass, + }; +} + +/** + * Simple wrapper for `targaryen.database()` for conveniently creating a + * database for a jest test. + */ +function getDatabase(...args) { + return targaryen.database(...args); +} + +/** + * Simple wrapper for `targaryen.database()` that also enables debug mode for + * detailed error messages. + */ +function getDebugDatabase(...args) { + return targaryen.database(...args).with({ debug: true }); +} + +const jestTargaryen = { + toBeAllowed, + toAllowRead, + toAllowWrite, + toAllowUpdate, + + // NOTE: Exported for convenience only + getDatabase, + getDebugDatabase, + json: require('firebase-json'), + users: targaryen.util.users, +}; + +module.exports = jestTargaryen; From 4ba2c43f18508fe49bbfb1f62816d2fd68382787 Mon Sep 17 00:00:00 2001 From: Achim Weimert Date: Thu, 10 May 2018 17:37:12 +0100 Subject: [PATCH 2/7] Clean-up jest plugin --- plugins/jest.js | 78 ++++++++++++++++++++++++++----------------------- 1 file changed, 42 insertions(+), 36 deletions(-) diff --git a/plugins/jest.js b/plugins/jest.js index 01557ca..43079f6 100644 --- a/plugins/jest.js +++ b/plugins/jest.js @@ -6,82 +6,88 @@ 'use strict'; +const json = require('firebase-json'); const targaryen = require('../'); -function toBeAllowed({ info, allowed }) { - const pass = allowed === true; - const message = pass - ? () => `Expected operation to be ${this.utils.EXPECTED_COLOR('denied')} but it was ${this.utils.RECEIVED_COLOR('allowed')}\n\n${info}` - : () => `Expected operation to be ${this.utils.EXPECTED_COLOR('allowed')} but it was ${this.utils.RECEIVED_COLOR('denied')}\n\n${info}`; +// Need to disable eslint rule for jest's utils: this.utils.EXPECTED_COLOR('a') +/* eslint-disable new-cap */ - return { +function toBeAllowed(result) { + const pass = result.allowed === true; + const message = pass ? + () => `Expected operation to be ${this.utils.EXPECTED_COLOR('denied')} but it was ${this.utils.RECEIVED_COLOR('allowed')}\n\n${result.info}` : + () => `Expected operation to be ${this.utils.EXPECTED_COLOR('allowed')} but it was ${this.utils.RECEIVED_COLOR('denied')}\n\n${result.info}`; + + return { message, - pass, + pass }; } function toAllowRead(database, path, options) { - const { info, allowed } = database - .with({ debug: true }) + const result = database + .with({debug: true}) .read(path, options); - const pass = allowed === true; - const message = pass - ? () => `Expected ${this.utils.EXPECTED_COLOR('read')} to be ${this.utils.EXPECTED_COLOR('denied')} but it was ${this.utils.RECEIVED_COLOR('allowed')}\n\n${info}` - : () => `Expected ${this.utils.EXPECTED_COLOR('read')} to be ${this.utils.EXPECTED_COLOR('allowed')} but it was ${this.utils.RECEIVED_COLOR('denied')}\n\n${info}`; + const pass = result.allowed === true; + const message = pass ? + () => `Expected ${this.utils.EXPECTED_COLOR('read')} to be ${this.utils.EXPECTED_COLOR('denied')} but it was ${this.utils.RECEIVED_COLOR('allowed')}\n\n${result.info}` : + () => `Expected ${this.utils.EXPECTED_COLOR('read')} to be ${this.utils.EXPECTED_COLOR('allowed')} but it was ${this.utils.RECEIVED_COLOR('denied')}\n\n${result.info}`; return { message, - pass, + pass }; } function toAllowWrite(database, path, value, options) { - const { info, allowed } = database - .with({ debug: true }) + const result = database + .with({debug: true}) .write(path, value, options); - const pass = allowed === true; - const message = pass - ? () => `Expected ${this.utils.EXPECTED_COLOR('write')} to be ${this.utils.EXPECTED_COLOR('denied')} but it was ${this.utils.RECEIVED_COLOR('allowed')}\n\n${info}` - : () => `Expected ${this.utils.EXPECTED_COLOR('write')} to be ${this.utils.EXPECTED_COLOR('allowed')} but it was ${this.utils.RECEIVED_COLOR('denied')}\n\n${info}`; + const pass = result.allowed === true; + const message = pass ? + () => `Expected ${this.utils.EXPECTED_COLOR('write')} to be ${this.utils.EXPECTED_COLOR('denied')} but it was ${this.utils.RECEIVED_COLOR('allowed')}\n\n${result.info}` : + () => `Expected ${this.utils.EXPECTED_COLOR('write')} to be ${this.utils.EXPECTED_COLOR('allowed')} but it was ${this.utils.RECEIVED_COLOR('denied')}\n\n${result.info}`; return { message, - pass, + pass }; } function toAllowUpdate(database, path, patch, options) { - const { info, allowed } = database - .with({ debug: true }) + const result = database + .with({debug: true}) .update(path, patch, options); - const pass = allowed === true; - const message = pass - ? () => `Expected ${this.utils.EXPECTED_COLOR('update')} to be ${this.utils.EXPECTED_COLOR('denied')} but it was ${this.utils.RECEIVED_COLOR('allowed')}\n\n${info}` - : () => `Expected ${this.utils.EXPECTED_COLOR('update')} to be ${this.utils.EXPECTED_COLOR('allowed')} but it was ${this.utils.RECEIVED_COLOR('denied')}\n\n${info}`; + const pass = result.allowed === true; + const message = pass ? + () => `Expected ${this.utils.EXPECTED_COLOR('update')} to be ${this.utils.EXPECTED_COLOR('denied')} but it was ${this.utils.RECEIVED_COLOR('allowed')}\n\n${result.info}` : + () => `Expected ${this.utils.EXPECTED_COLOR('update')} to be ${this.utils.EXPECTED_COLOR('allowed')} but it was ${this.utils.RECEIVED_COLOR('denied')}\n\n${result.info}`; return { message, - pass, + pass }; } /** - * Simple wrapper for `targaryen.database()` for conveniently creating a + * Expose `targaryen.database()` for conveniently creating a * database for a jest test. + * + * @return {Database} */ -function getDatabase(...args) { - return targaryen.database(...args); -} +const getDatabase = targaryen.database; /** * Simple wrapper for `targaryen.database()` that also enables debug mode for * detailed error messages. + * + * @return {Database} */ -function getDebugDatabase(...args) { - return targaryen.database(...args).with({ debug: true }); +function getDebugDatabase() { + return targaryen.database.apply(this, arguments).with({debug: true}); } const jestTargaryen = { @@ -93,8 +99,8 @@ const jestTargaryen = { // NOTE: Exported for convenience only getDatabase, getDebugDatabase, - json: require('firebase-json'), - users: targaryen.util.users, + json, + users: targaryen.util.users }; module.exports = jestTargaryen; From 47a92c52a2ae96f03d7aab8912b95039f24ef2ca Mon Sep 17 00:00:00 2001 From: Achim Weimert Date: Thu, 10 May 2018 17:38:29 +0100 Subject: [PATCH 3/7] Add tests for jest plugin --- package.json | 2 + test/jest/.eslintrc.yml | 10 +++ test/jest/__snapshots__/core.test.js.snap | 31 ++++++++ test/jest/__snapshots__/matchers.test.js.snap | 61 +++++++++++++++ test/jest/core.test.js | 65 ++++++++++++++++ test/jest/matchers.test.js | 75 +++++++++++++++++++ 6 files changed, 244 insertions(+) create mode 100644 test/jest/.eslintrc.yml create mode 100644 test/jest/__snapshots__/core.test.js.snap create mode 100644 test/jest/__snapshots__/matchers.test.js.snap create mode 100644 test/jest/core.test.js create mode 100644 test/jest/matchers.test.js diff --git a/package.json b/package.json index 574e922..0718fb3 100644 --- a/package.json +++ b/package.json @@ -72,9 +72,11 @@ "coveralls": "^2.11.15", "eslint": "^3.9.1", "eslint-config-xo": "^0.17.0", + "eslint-plugin-jest": "^21.15.1", "eslint-plugin-node": "^2.1.3", "istanbul": "^0.4.5", "jasmine": "^2.1.1", + "jest": "^22.4.3", "mocha": "^2.1.0", "sinon": "^1.17.6", "sinon-chai": "^2.8.0" diff --git a/test/jest/.eslintrc.yml b/test/jest/.eslintrc.yml new file mode 100644 index 0000000..f0a2075 --- /dev/null +++ b/test/jest/.eslintrc.yml @@ -0,0 +1,10 @@ +extends: '../../.eslintrc.yml' +plugins: + - jest +env: + jest/globals: true +rules: + jest/no-disabled-tests: warn + jest/no-focused-tests: error + jest/no-identical-title: error + jest/valid-expect: error \ No newline at end of file diff --git a/test/jest/__snapshots__/core.test.js.snap b/test/jest/__snapshots__/core.test.js.snap new file mode 100644 index 0000000..c156c22 --- /dev/null +++ b/test/jest/__snapshots__/core.test.js.snap @@ -0,0 +1,31 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`generic matchers toBeAllowed 1`] = ` +"Expected operation to be allowed but it was denied + +Attempt to read /user as null. + +/user: read + +No .read rule allowed the operation. +read was denied." +`; + +exports[`generic matchers toBeAllowed 2`] = ` +"Expected operation to be denied but it was allowed + +Attempt to write /user/1234 as {\\"uid\\":\\"1234\\"}. +New Value: \\"{ + \\"name\\": \\"Anna\\" +}\\". + +/user/1234: write \\"auth.uid === $uid\\" => true + auth.uid === $uid [=> true] + using [ + $uid = \\"1234\\" + auth = {\\"uid\\":\\"1234\\"} + auth.uid = \\"1234\\" + ] + +write was allowed." +`; diff --git a/test/jest/__snapshots__/matchers.test.js.snap b/test/jest/__snapshots__/matchers.test.js.snap new file mode 100644 index 0000000..112bce7 --- /dev/null +++ b/test/jest/__snapshots__/matchers.test.js.snap @@ -0,0 +1,61 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`matchers toAllowRead 1`] = ` +"Expected read to be allowed but it was denied + +Attempt to read /user as null. + +/user: read + +No .read rule allowed the operation. +read was denied." +`; + +exports[`matchers toAllowRead 2`] = ` +"Expected read to be denied but it was allowed + +Attempt to read /public as null. + +/public: read \\"true\\" => true + true [=> true] + +read was allowed." +`; + +exports[`matchers toAllowUpdate 1`] = ` +"Expected update to be denied but it was allowed + +Attempt to patch /user/1234 as {\\"uid\\":\\"1234\\"}. +Patch: \\"{ + \\"name\\": \\"Anna\\" +}\\". + +/user/1234: write \\"auth.uid === $uid\\" => true + auth.uid === $uid [=> true] + using [ + $uid = \\"1234\\" + auth = {\\"uid\\":\\"1234\\"} + auth.uid = \\"1234\\" + ] + +patch was allowed." +`; + +exports[`matchers toAllowWrite 1`] = ` +"Expected write to be denied but it was allowed + +Attempt to write /user/1234 as {\\"uid\\":\\"1234\\"}. +New Value: \\"{ + \\"name\\": \\"Anna\\" +}\\". + +/user/1234: write \\"auth.uid === $uid\\" => true + auth.uid === $uid [=> true] + using [ + $uid = \\"1234\\" + auth = {\\"uid\\":\\"1234\\"} + auth.uid = \\"1234\\" + ] + +write was allowed." +`; diff --git a/test/jest/core.test.js b/test/jest/core.test.js new file mode 100644 index 0000000..f3362f5 --- /dev/null +++ b/test/jest/core.test.js @@ -0,0 +1,65 @@ +/** + * Jest test definition to test targaryen Jest integration. + */ + +'use strict'; + +const targaryen = require('../../plugins/jest'); + +expect.extend({ + toBeAllowed: targaryen.toBeAllowed +}); + +test('getDebugDatabase()', () => { + const emptyRules = {rules: {}}; + const database = targaryen.getDebugDatabase(emptyRules, {}); + + expect(database.debug).toBe(true); +}); + +test('getDatabase()', () => { + const emptyRules = {rules: {}}; + const database = targaryen.getDatabase(emptyRules, {}); + + expect(database.debug).toBe(false); +}); + +describe('generic matchers', () => { + test('toBeAllowed', () => { + const rules = { + rules: { + user: { + $uid: { + '.read': 'auth.uid !== null', + '.write': 'auth.uid === $uid' + } + } + } + }; + const initialData = {}; + + // NOTE: Create a database with debug set to true for detailed errors + const database = targaryen.getDebugDatabase(rules, initialData); + + expect(() => { + expect(database.as(null).read('/user')).not.toBeAllowed(); + }).not.toThrow(); + + expect(() => { + expect(database.as(null).read('/user')).toBeAllowed(); + }).toThrowErrorMatchingSnapshot(); + + expect(() => { + expect(database.as({uid: '1234'}).write('/user/1234', { + name: 'Anna' + })).toBeAllowed(); + }).not.toThrow(); + + expect(() => { + expect(database.as({uid: '1234'}).write('/user/1234', { + name: 'Anna' + })).not.toBeAllowed(); + }).toThrowErrorMatchingSnapshot(); + }); +}); + diff --git a/test/jest/matchers.test.js b/test/jest/matchers.test.js new file mode 100644 index 0000000..8c2f155 --- /dev/null +++ b/test/jest/matchers.test.js @@ -0,0 +1,75 @@ +/** + * Jest test definition to test targaryen Jest integration. + */ + +'use strict'; + +const targaryen = require('../../plugins/jest'); + +expect.extend({ + toAllowRead: targaryen.toAllowRead, + toAllowUpdate: targaryen.toAllowUpdate, + toAllowWrite: targaryen.toAllowWrite +}); + +describe('matchers', () => { + const rules = {rules: { + user: { + $uid: { + '.read': 'auth.uid !== null', + '.write': 'auth.uid === $uid' + } + }, + public: { + '.read': true + } + }}; + const initialData = {}; + const database = targaryen.getDatabase(rules, initialData); + + test('toAllowRead', () => { + expect(() => { + expect(database).not.toAllowRead('/user'); + }).not.toThrow(); + + expect(() => { + expect(database).toAllowRead('/user'); + }).toThrowErrorMatchingSnapshot(); + + expect(() => { + expect(database).toAllowRead('/public'); + }).not.toThrow(); + + expect(() => { + expect(database).not.toAllowRead('/public'); + }).toThrowErrorMatchingSnapshot(); + }); + + test('toAllowWrite', () => { + expect(() => { + expect(database.as({uid: '1234'})).toAllowWrite('/user/1234', { + name: 'Anna' + }); + }).not.toThrow(); + + expect(() => { + expect(database.as({uid: '1234'})).not.toAllowWrite('/user/1234', { + name: 'Anna' + }); + }).toThrowErrorMatchingSnapshot(); + }); + + test('toAllowUpdate', () => { + expect(() => { + expect(database.as({uid: '1234'})).toAllowUpdate('/user/1234', { + name: 'Anna' + }); + }).not.toThrow(); + + expect(() => { + expect(database.as({uid: '1234'})).not.toAllowUpdate('/user/1234', { + name: 'Anna' + }); + }).toThrowErrorMatchingSnapshot(); + }); +}); From d417e7c21b5875a628ce4e9a435d06595853978f Mon Sep 17 00:00:00 2001 From: Achim Weimert Date: Thu, 10 May 2018 17:38:46 +0100 Subject: [PATCH 4/7] Add docs for jest plugin --- README.md | 25 +++++++++++++++- USAGE.md | 57 ++++++++++++++++++++++++++++++++++++ docs/jest/README.md | 71 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 152 insertions(+), 1 deletion(-) create mode 100644 docs/jest/README.md diff --git a/README.md b/README.md index 8442386..3c19cff 100644 --- a/README.md +++ b/README.md @@ -65,7 +65,7 @@ Targaryen provides three convenient ways to run tests: }); ``` -- or as a plugin for [Chai](http://chaijs.com). +- as a plugin for [Chai](http://chaijs.com). ```js const chai = require('chai'); @@ -90,6 +90,29 @@ Targaryen provides three convenient ways to run tests: }); ``` +- or as a set of custom matchers for [Jest](https://facebook.github.io/jest/): + + ```js + const targaryen = require('targaryen/plugins/jest'); + const rules = targaryen.json.loadSync(RULES_PATH); + + expect.extend({ + toAllowRead: targaryen.toAllowRead, + toAllowUpdate: targaryen.toAllowUpdate, + toAllowWrite: targaryen.toAllowWrite + }); + + describe('my security rules', function() { + const database = targaryen.getDatabase(rules, require(DATA_PATH)); + + it('should allow authenticated user to read all data', function() { + expect(database.as({uid: 'foo'})).toAllowRead('/'); + expect(database.as(null)).not.toAllowRead('/'); + }) + + }); + ``` + When a test fails, you get detailed debug information that explains why the read/write operation succeeded/failed. See [USAGE.md](https://github.com/goldibex/targaryen/blob/master/USAGE.md) for more information. diff --git a/USAGE.md b/USAGE.md index 33eeb5b..189a860 100644 --- a/USAGE.md +++ b/USAGE.md @@ -166,3 +166,60 @@ describe('A set of rules and data', function() { }); ``` + +### Jest + +Docs are at [docs/jest](https://github.com/goldibex/targaryen/blob/master/docs/jest). A quick example: + +```js + +const targaryen = require('targaryen/plugins/jest'); + +// see Chai example above for format +const rules = targaryen.json.loadSync(RULES_PATH); +const data = require(DATA_PATH); + +expect.extend({ + toAllowRead: targaryen.toAllowRead, + toAllowUpdate: targaryen.toAllowUpdate, + toAllowWrite: targaryen.toAllowWrite +}); + +describe('A set of rules and data', function() { + const database = targaryen.getDatabase(rules, data); + + it('should allow authenticated user to read all data', function() { + expect(database.as({uid: 'foo'})).toAllowRead('/'); + expect(database.as(null)).not.toAllowRead('/'); + }) + + it('can be tested', function() { + + expect(database.as(targaryen.users.unauthenticated)) + .not.toAllowRead('users/password:500f6e96-92c6-4f60-ad5d-207253aee4d3'); + + expect(database.as(targaryen.users.password)) + .toAllowRead('users/password:500f6e96-92c6-4f60-ad5d-207253aee4d3'); + + expect(database.as(targaryen.users.password)) + .not.toAllowWrite('users/password:500f6e96-92c6-4f60-ad5d-207253aee4d3/innocent', true); + + expect(database.as({ uid: 'password:3403291b-fdc9-4995-9a54-9656241c835d'})) + .toAllowWrite('users/password:500f6e96-92c6-4f60-ad5d-207253aee4d3/onFire', true); + + expect(database.as({ uid: 'password:3403291b-fdc9-4995-9a54-9656241c835d'})) + .toAllowUpdate('/', { + 'users/password:500f6e96-92c6-4f60-ad5d-207253aee4d3/onFire': true, + 'users/password:500f6e96-92c6-4f60-ad5d-207253aee4d3/innocent': null + }); + + expect(database.as({ uid: 'password:3403291b-fdc9-4995-9a54-9656241c835d'})) + .not.toAllowUpdate('/', { + 'users/password:500f6e96-92c6-4f60-ad5d-207253aee4d3/onFire': null, + 'users/password:500f6e96-92c6-4f60-ad5d-207253aee4d3/innocent': true + }); + + }); + +}); +``` diff --git a/docs/jest/README.md b/docs/jest/README.md new file mode 100644 index 0000000..daad72c --- /dev/null +++ b/docs/jest/README.md @@ -0,0 +1,71 @@ + +## Using Targaryen with Jest + +1. Run `npm install -g jest` and `npm install --save-dev targaryen`. + +2. Create a new directory for your security tests (**NOTE**: Jest defaults to look for tests inside of `__tests__` folders, or in files that end in `.spec.js` or `.test.js`). + +3. Add a new *fixture JSON* for the state of your Firebase. Call this `spec/security/.json`. This file will describe the state of the Firebase data store for your tests, that is, what you can get via the `root` and `data` variables in the security rules. + +4. Create a new file for your first set of tests, like `spec/security/.spec.js`. + +5. Add the following content to the top of the new file: + +```js +// user-rules.spec.js +const targaryen = require('targaryen/plugins/jest'); + +expect.extend({ + toAllowRead: targaryen.toAllowRead, + toAllowUpdate: targaryen.toAllowUpdate, + toAllowWrite: targaryen.toAllowWrite, +}); + +const RULES_PATH = 'database.rules.json'; +const rules = targaryen.json.loadSync(RULES_PATH); +const initialData = require(path.join(__dirname, path.basename(__filename, '.spec.js') + '.json')); + +test('basic', () => { + const database = targaryen.getDatabase(rules, initialData); + + expect(database.as(targaryen.users.unauthenticated)).not.toAllowRead('/user'); + expect(database.as(targaryen.users.unauthenticated)).toAllowRead('/public'); + expect(database.as(targaryen.users.facebook)).toAllowRead('/user'); + expect(database.as({ uid: '1234'})).toAllowWrite('/user/1234', { + name: 'Anna', + }); +}); +``` + +where `RULES_PATH` is the path to your security rules JSON file. If your security rules are broken, Targaryen will throw an exception at this point with detailed information about what specifically is broken. + +6. Write your security tests. + +The subject of every assertion will be the authentication state (i.e., `auth`) of the user trying the operation, so for instance, `null` would be an unauthenticated user, or a Firebase Password Login user would look like `{ uid: 'password:500f6e96-92c6-4f60-ad5d-207253aee4d3', id: 1, provider: 'password' }`. There are symbolic versions of these in `targaryen.users`. + +See the API section below for details, or take a look at the example files here. + +7. Run the tests with `jest`. + +## API + +- import with `require('targaryen/plugins/jest')`. +- `jestTargaryen.toAllowRead`, `jestTargaryen.toAllowWrite`, `jestTargaryen.toAllowUpdate`, `jestTargaryen.toBeAllowed`: The jest matchers. Load them using `expect.extend({toAllowRead: targaryen.toAllowRead, toAllowWrite: targaryen.toAllowWrite, toAllowUpdate: targaryen.toAllowUpdate, toBeAllowed: targaryen.toBeAllowed});` before running any tests. +- `jestTargaryen.getDatabase(rules: object|Ruleset, data: object|DataNode, now: null|number): Database`: Wrapper for `targaryen.database()`. +- `jestTargaryen.getDebugDatabase(rules: object|Ruleset, data: object|DataNode, now: null|number): Database`: Wrapper for `targaryen.database()` that also enables debug mode. Use this if you write your tests using the generic matcher `toBeAllowed()`. +- `jestTargaryen.json`: Export of `firebase-json` to allow parsing of firebase rule files. +- `jestTargaryen.users`: A set of authentication objects you can use as the subject of the assertions. Has the following keys: + - `unauthenticated`: an unauthenticated user, i.e., `auth === null`. + - `anonymous`: a user authenticated using Firebase anonymous sessions. + - `password`: a user authenticated using Firebase Password Login. + - `facebook`: a user authenticated by their Facebook account. + - `twitter`: a user authenticated by their Twitter account. + - `google`: a user authenticated by their Google account. + - `github`: a user authenticated by their Github account. +- `expect(auth).canRead(path: string [, options: {now?: number, query?: object} ])`: asserts that the given path is readable by a user with the given authentication data. +- `expect(auth).cannotRead(path: string[, options: {now?: number, query?: object} ])`: asserts that the given path is not readable by a user with the given authentication data. +- `expect(auth).canWrite(path: string [, data: any [, options: {now: number, priority: any} ]])`: asserts that the given path is writable by a user with the given authentication data. Optionally takes a Javascript object containing `newData`, otherwise this will be set to `null`. +- `expect(auth).cannotWrite(path: string [, data: any [, options: {now: number, priority: any} ]])`: asserts that the given path is not writable by a user with the given authentication data. Optionally takes a Javascript object containing `newData`, otherwise this will be set to `null`. +- `expect(auth).canPatch(path: string, patch: {[path: string]: any} [, options: {now: number} ])`: asserts that the given patch (or multi-location update) operation is writable by a user with the given authentication data. +- `expect(auth).cannotPatch(path: string, patch: {[path: string]: any} [, options: {now: number} ])`: asserts that the given patch (or multi-location update) operation is writable by a user with the given authentication data. + From 170e1a7062057f1cb60a6efeb7cd4379f1a335c9 Mon Sep 17 00:00:00 2001 From: Achim Weimert Date: Thu, 10 May 2018 17:57:18 +0100 Subject: [PATCH 5/7] Add run script for jest plugin tests --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 0718fb3..8329ce2 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "prepare": "npm run build", "test": "npm run build && mocha -r test/setup.js test/spec/ --recursive && jasmine && node ./bin/targaryen --verbose test/integration/rules.json test/integration/tests.json", "test:unit": "mocha -r test/setup.js test/spec/ --recursive", + "test:plugin:jest": "jest test/jest", "test:inspect": "node --inspect --debug-brk node_modules/.bin/_mocha -r test/setup.js test/spec/ --recursive" }, "author": "Harry Schmidt ", From e9db4568091f7e1f62a1881e6912c6f409c92632 Mon Sep 17 00:00:00 2001 From: Achim Weimert Date: Thu, 10 May 2018 18:16:19 +0100 Subject: [PATCH 6/7] Run jest plugin tests on travis As jest requires node v6+ don't run tests if a version lower than 6 is detected. --- .travis.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.travis.yml b/.travis.yml index e67f3f0..1f8bbf7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,3 +11,9 @@ script: fi - npm run coveralls - npm run lint + - > + if [[ $(node --version | cut -c 2-) < 6 ]]; then + echo "skipping jest plugin test"; + else + npm run test:plugin:jest; + fi From 396bd0d3bdb90c95b7223760c92082ed2572e117 Mon Sep 17 00:00:00 2001 From: Achim Weimert Date: Fri, 11 May 2018 11:53:05 +0100 Subject: [PATCH 7/7] Use grep to check node version Using grep is less future-proof but the better option, as comparing using `<` can be incorrect. --- .travis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 1f8bbf7..333fabe 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,8 +12,8 @@ script: - npm run coveralls - npm run lint - > - if [[ $(node --version | cut -c 2-) < 6 ]]; then - echo "skipping jest plugin test"; - else + if node --version | grep v6\. ; then npm run test:plugin:jest; + else + echo "skipping jest plugin test"; fi