Skip to content

Commit

Permalink
Merge pull request #150 from a-xin/add-jest-matcher
Browse files Browse the repository at this point in the history
Add jest matcher
  • Loading branch information
dinoboff authored May 11, 2018
2 parents f3281a0 + 396bd0d commit bb579bc
Show file tree
Hide file tree
Showing 11 changed files with 509 additions and 1 deletion.
6 changes: 6 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,9 @@ script:
fi
- npm run coveralls
- npm run lint
- >
if node --version | grep v6\. ; then
npm run test:plugin:jest;
else
echo "skipping jest plugin test";
fi
25 changes: 24 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -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.
Expand Down
57 changes: 57 additions & 0 deletions USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
});

});

});
```
71 changes: 71 additions & 0 deletions docs/jest/README.md
Original file line number Diff line number Diff line change
@@ -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/<firebase path>.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/<firebase path>.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.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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 <[email protected]>",
Expand Down Expand Up @@ -72,9 +73,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"
Expand Down
106 changes: 106 additions & 0 deletions plugins/jest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/**
* targaryen/plugins/jest - Reference implementation of a jest plugin for
* targaryen.
*
*/

'use strict';

const json = require('firebase-json');
const targaryen = require('../');

// Need to disable eslint rule for jest's utils: this.utils.EXPECTED_COLOR('a')
/* eslint-disable new-cap */

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
};
}

function toAllowRead(database, path, options) {
const result = database
.with({debug: true})
.read(path, options);

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
};
}

function toAllowWrite(database, path, value, options) {
const result = database
.with({debug: true})
.write(path, value, options);

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
};
}

function toAllowUpdate(database, path, patch, options) {
const result = database
.with({debug: true})
.update(path, patch, options);

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
};
}

/**
* Expose `targaryen.database()` for conveniently creating a
* database for a jest test.
*
* @return {Database}
*/
const getDatabase = targaryen.database;

/**
* Simple wrapper for `targaryen.database()` that also enables debug mode for
* detailed error messages.
*
* @return {Database}
*/
function getDebugDatabase() {
return targaryen.database.apply(this, arguments).with({debug: true});
}

const jestTargaryen = {
toBeAllowed,
toAllowRead,
toAllowWrite,
toAllowUpdate,

// NOTE: Exported for convenience only
getDatabase,
getDebugDatabase,
json,
users: targaryen.util.users
};

module.exports = jestTargaryen;
10 changes: 10 additions & 0 deletions test/jest/.eslintrc.yml
Original file line number Diff line number Diff line change
@@ -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
31 changes: 31 additions & 0 deletions test/jest/__snapshots__/core.test.js.snap
Original file line number Diff line number Diff line change
@@ -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 rules>
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."
`;
Loading

0 comments on commit bb579bc

Please sign in to comment.