goldibex / targaryen

Test Firebase security rules without connecting to Firebase.
ISC License
242 stars 36 forks source link

Jest support #129

Closed fernandopasik closed 6 years ago

fernandopasik commented 6 years ago

Since there are jasmine matchers do you know if there is a way to use them with jest as well?

dinoboff commented 6 years ago

I am not familiar with Jest test suite; I am not sure what kind of api would work with it. Not all test suite require their own plugin, the default API might be fine; e.g. tap or ava can use the default API directly (something like the chai plugin would be an anti-pattern), although #124 needs a fix.

What would a Jest test for firebase security rules look like?

dinoboff commented 6 years ago

Also, are tests run concurrently?

fernandopasik commented 6 years ago

Yes, tests run concurrently.

I've tried to extend jest matchers with this code:

import targaryen from 'targaryen/plugins/jasmine';

expect.extend(targaryen.matchers);

but now I'm receiving this error:

Unexpected return from a matcher function.
    Matcher functions should return an object in the following format:
      {message?: string | function, pass: boolean}
    '{"compare": [Function compare]}' was returned
fernandopasik commented 6 years ago

Looks like your structure is returning a compare function that later returns what's needed by jest

canRead() {

    return {compare(auth, path, now) {

      const data = targaryen.util.getFirebaseData().as(auth);

      const result = data.read(path, now);

      return {
        pass: result.allowed === true,
        message: targaryen.util.unreadableError(result)
      };

    }};

  },
dinoboff commented 6 years ago

I meant try without a matcher, using targaryen API directly, to see what's missing, what a matcher could improve:

const targaryen = require('targaryen');

const rules = {
  rules: {
    foo: {
      '.write': 'true'
    }
  }
};
const initialData = {foo: {bar: 1, baz: 2}};
const database = targaryen.database(rules, initialData).with({debug: true});

test('Anyone can update foo', t => {
  // Note that a database, its internal rules and its data layer are immutable;
  // concurrent tests can use the same database
  const {info, allowed, newDatabase} = database.update('/foo', {bar: 2});

  // a Jest matcher could conditionally print the debug info.
  console.log(info);
  expect(allowed).toBe(true);

  // Unlike chai matcher, a new matcher could also allow to test the new data
  expect(newDatabase.snapshot('/foo').val()). toEqual({bar: 2, baz: 2});
});

ps: jest matcher api.

dinoboff commented 6 years ago

@fernandopasik I suggest you create a matcher on one of your project. I would suggest also to use targaryen.util.*Error helpers but to avoid the helpers using global values, targaryen.util.set* or targaryen.util.set*; targaryen has only synchronous methods so they can be used safely with concurrent test, but you need to reset rules and data between each test.

A set of matcher like:

const rules = {
  rules: {
    foo: {
      '.write': 'auth != null'
    }
  }
};
const initialData = {foo: {bar: 1, baz: 2}};
const database = targaryen.database(rules, initialData).with({debug: true});
const alice = {uid: 'alice'};

test('Authenticated user can read foo', t => {
  expect(database).not.toAllowRead('/foo');
  expect(database.as(alice)).toAllowRead('/foo');

  // or if jest api allows that
  expect(database).can.not.read('/foo');
  expect(database.as(alice)).can.read('/foo');
});

Once you like the matchers, create a package. I am happy to link it in the doc, but I am not sure it's something I can maintain in core, especially since I am not using jest myself.

dedan commented 6 years ago

@fernandopasik You can simply use the Chai matchers within your Jest test suites. I found a blog post that describes this setup in detail.

And here is an example of a simple test of mine, using targaryen matchers with Chai within Jest.

const targaryen = require('targaryen/plugins/chai')
const chai = require('chai')
global.chaiExpect = chai.expect

const rules = require('../database.rules.json')

chai.use(targaryen)

beforeEach(() => {
  targaryen.setFirebaseData({})
  targaryen.setFirebaseRules(rules)
})

it('should allow everybody to write to betaEmails', () => {
  chaiExpect(null).cannot.read.path('/betaEmails')
})
fernandopasik commented 6 years ago

Thanks @dedan. I'd like to continue looking into adapting the jasmine matchers to jest. Usually with jest there is no need to include chai for assertions.