Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add jest matcher #150

Merged
merged 7 commits into from
May 11, 2018
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 | cut -c 2-) < 6 ]]; then
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you please change the version test. Either use grep or change the previous test to use cut. cut is probably better.

echo "skipping jest plugin test";
else
npm run test:plugin:jest;
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