You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@couchdb.apache.org by ro...@apache.org on 2016/12/13 11:03:09 UTC

fauxton commit: updated refs/heads/master to 25630f4

Repository: couchdb-fauxton
Updated Branches:
  refs/heads/master 258ec71fb -> 25630f4e4


Redux: Permissions - Use Redux for Flux flow

Integrates Redux into CouchDB Fauxton to softmigrate our stores
to Redux. Removes the Backbone models and introduces testing with
Jest.

Additional Highlights:

 - Bluebird for Promises (bye jQuery deferreds)
 - uses the WHATWG fetch API
 - 1 file per component


Project: http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/repo
Commit: http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/commit/25630f4e
Tree: http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/tree/25630f4e
Diff: http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/diff/25630f4e

Branch: refs/heads/master
Commit: 25630f4e48a55e983ea6aff095b034cc2a728251
Parents: 258ec71
Author: Robert Kowalski <ro...@apache.org>
Authored: Thu Nov 3 15:09:51 2016 +0100
Committer: Robert Kowalski <ro...@apache.org>
Committed: Mon Dec 12 17:56:52 2016 +0100

----------------------------------------------------------------------
 .eslintrc                                       |   3 +-
 __mocks__/fileMock.js                           |   1 +
 __mocks__/styleMock.js                          |   1 +
 .../permissions/__tests__/actions-test.js       | 112 +++++++++
 .../permissions/__tests__/container-test.js     |  57 +++++
 .../permissions/__tests__/helpers-test.js       |  84 +++++++
 .../__tests__/permissionsScreen-test.js         |  97 ++++++++
 app/addons/permissions/actions.js               | 132 +++++++----
 app/addons/permissions/actiontypes.js           |   8 +-
 app/addons/permissions/base.js                  |   3 +
 app/addons/permissions/components.react.jsx     | 234 -------------------
 .../permissions/components/Permissions.js       |  48 ++++
 .../permissions/components/PermissionsItem.js   |  36 +++
 .../permissions/components/PermissionsScreen.js |  85 +++++++
 .../components/PermissionsSection.js            | 163 +++++++++++++
 .../container/PermissionsContainer.js           |  40 ++++
 app/addons/permissions/helpers.js               |  39 ++++
 app/addons/permissions/layout.js                |   4 +-
 app/addons/permissions/reducers.js              |  67 ++++++
 app/addons/permissions/resources.js             |  83 -------
 app/addons/permissions/routes.js                |  34 +--
 app/addons/permissions/stores.js                | 104 ---------
 app/addons/permissions/tests/actionsSpec.js     | 122 ----------
 .../permissions/tests/componentsSpec.react.jsx  | 135 -----------
 .../permissions/tests/nightwatch/permissions.js |  40 ++++
 app/addons/permissions/tests/resourceSpec.js    |  69 ------
 app/core/base.js                                |   2 +
 app/helpers.js                                  |   1 +
 app/main.js                                     |  23 +-
 jest-config.json                                |  12 +-
 jest-setup.js                                   |  18 ++
 package.json                                    |  10 +-
 32 files changed, 1042 insertions(+), 825 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/25630f4e/.eslintrc
----------------------------------------------------------------------
diff --git a/.eslintrc b/.eslintrc
index 0b5439d..c642ec6 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -73,7 +73,8 @@
     "after": true,
     "define": true,
     "expect": true,
-    "prettyPrint": true
+    "prettyPrint": true,
+    "jest": true
   }
 
 }

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/25630f4e/__mocks__/fileMock.js
----------------------------------------------------------------------
diff --git a/__mocks__/fileMock.js b/__mocks__/fileMock.js
new file mode 100644
index 0000000..86059f3
--- /dev/null
+++ b/__mocks__/fileMock.js
@@ -0,0 +1 @@
+module.exports = 'test-file-stub';

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/25630f4e/__mocks__/styleMock.js
----------------------------------------------------------------------
diff --git a/__mocks__/styleMock.js b/__mocks__/styleMock.js
new file mode 100644
index 0000000..f053ebf
--- /dev/null
+++ b/__mocks__/styleMock.js
@@ -0,0 +1 @@
+module.exports = {};

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/25630f4e/app/addons/permissions/__tests__/actions-test.js
----------------------------------------------------------------------
diff --git a/app/addons/permissions/__tests__/actions-test.js b/app/addons/permissions/__tests__/actions-test.js
new file mode 100644
index 0000000..5f74223
--- /dev/null
+++ b/app/addons/permissions/__tests__/actions-test.js
@@ -0,0 +1,112 @@
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+import {
+  setPermissionOnObject,
+  deletePermissionFromObject
+} from '../actions';
+
+
+describe('Permissions Actions', () => {
+
+  describe('deleting roles', () => {
+    it('throws if a role is not in permissions', () => {
+      const p = {
+        admins: {
+          names: ['abc'],
+          roles: []
+        },
+        members: {
+          names: [],
+          roles: []
+        }
+      };
+
+      expect(() => {
+        deletePermissionFromObject(p, 'admins', 'names', 'pizza');
+      }).toThrow();
+    });
+
+    it('deletes roles', () => {
+      const p = {
+        admins: {
+          names: ['abc', 'furbie'],
+          roles: []
+        },
+        members: {
+          names: [],
+          roles: []
+        }
+      };
+
+      const res = deletePermissionFromObject(p, 'admins', 'names', 'abc');
+
+      expect(res).toEqual({
+        admins: {
+          names: ['furbie'],
+          roles: []
+        },
+        members: {
+          names: [],
+          roles: []
+        }
+      });
+    });
+
+  });
+
+  describe('adding roles', () => {
+    it('throws if a role is already in permissions', () => {
+      const p = {
+        admins: {
+          names: ['abc'],
+          roles: []
+        },
+        members: {
+          names: [],
+          roles: []
+        }
+      };
+
+      expect(() => {
+        setPermissionOnObject(p, 'admins', 'names', 'abc');
+      }).toThrow();
+    });
+
+    it('adds if not already present', () => {
+      const p = {
+        admins: {
+          names: ['abc'],
+          roles: []
+        },
+        members: {
+          names: [],
+          roles: []
+        }
+      };
+
+      const res = setPermissionOnObject(p, 'admins', 'names', 'test123');
+
+      expect(res).toEqual({
+        admins: {
+          names: ['abc', 'test123'],
+          roles: []
+        },
+        members: {
+          names: [],
+          roles: []
+        }
+      });
+    });
+
+  });
+});

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/25630f4e/app/addons/permissions/__tests__/container-test.js
----------------------------------------------------------------------
diff --git a/app/addons/permissions/__tests__/container-test.js b/app/addons/permissions/__tests__/container-test.js
new file mode 100644
index 0000000..82ae677
--- /dev/null
+++ b/app/addons/permissions/__tests__/container-test.js
@@ -0,0 +1,57 @@
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+import { receivedPermissions } from '../actions';
+
+import React from 'react';
+import { mount } from 'enzyme';
+
+import { createStore, applyMiddleware } from 'redux';
+
+import thunk from 'redux-thunk';
+import { Provider } from 'react-redux';
+
+import reducer from '../reducers';
+import PermissionsContainer from '../container/PermissionsContainer';
+
+describe('Permissions Container', () => {
+
+  it('renders with new results', () => {
+
+    fetch.mockResponse(
+      JSON.stringify({})
+    );
+
+    const middlewares = [thunk];
+    const store = createStore(
+      reducer,
+      applyMiddleware(...middlewares)
+    );
+
+    const wrapper = mount(
+      <Provider store={store}>
+        <PermissionsContainer url="http://example.com/abc" />
+      </Provider>
+    );
+
+    store.dispatch(
+      receivedPermissions({
+        admins:  { names: ['banana'], roles: [] }
+      })
+    );
+
+    const item = wrapper
+      .find('.permissions__admins .permissions__entry');
+
+    expect(item.text()).toContain('banana');
+  });
+});

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/25630f4e/app/addons/permissions/__tests__/helpers-test.js
----------------------------------------------------------------------
diff --git a/app/addons/permissions/__tests__/helpers-test.js b/app/addons/permissions/__tests__/helpers-test.js
new file mode 100644
index 0000000..a3fd54b
--- /dev/null
+++ b/app/addons/permissions/__tests__/helpers-test.js
@@ -0,0 +1,84 @@
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+import {
+  isValueAlreadySet,
+  addValueToPermissions
+} from '../helpers';
+
+describe('Permissions - Helpers', () => {
+
+  describe('isValueAlreadySet', () => {
+
+    it('returns false if object does not have properties', () => {
+
+      let permissions = {};
+      expect(
+        isValueAlreadySet(permissions, 'admins', 'names', 'rocko')
+      ).toBe(false);
+
+      permissions = { admins: {} };
+      expect(
+        isValueAlreadySet(permissions, 'admins', 'names', 'rocko')
+      ).toBe(false);
+
+      permissions = { admins: { names: [] } };
+      expect(
+        isValueAlreadySet(permissions, 'admins', 'names', 'rocko')
+      ).toBe(false);
+    });
+
+    it('confirms existing properties', () => {
+
+      const permissions = { admins: { names: ['michelle', 'rocko', 'garren'] } };
+
+      expect(
+        isValueAlreadySet(permissions, 'admins', 'names', 'rocko')
+      ).toBe(true);
+    });
+
+  });
+
+  describe('addValueToPermissions', () => {
+
+    it('adds values, even if properties not set', () => {
+
+      const permissions = {};
+      expect(
+        addValueToPermissions(permissions, 'admins', 'names', 'rocko')
+      ).toEqual({
+        admins: {
+          names: ['rocko']
+        }
+      });
+    });
+
+    it('adds values', () => {
+
+      const permissions = {
+        admins: {
+          names: ['rocko']
+        }
+      };
+
+      expect(
+        addValueToPermissions(permissions, 'admins', 'names', 'garren')
+      ).toEqual({
+        admins: {
+          names: ['rocko', 'garren']
+        }
+      });
+    });
+
+  });
+
+});

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/25630f4e/app/addons/permissions/__tests__/permissionsScreen-test.js
----------------------------------------------------------------------
diff --git a/app/addons/permissions/__tests__/permissionsScreen-test.js b/app/addons/permissions/__tests__/permissionsScreen-test.js
new file mode 100644
index 0000000..2283a47
--- /dev/null
+++ b/app/addons/permissions/__tests__/permissionsScreen-test.js
@@ -0,0 +1,97 @@
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+import React from 'react';
+import { mount } from 'enzyme';
+
+import PermissionsScreen from '../components/PermissionsScreen';
+
+
+describe('PermissionsScreen', () => {
+
+  it('add permississon: does not dispatch if value already exists', () => {
+
+    const security = {
+      admins:  { names: ['abc'], roles: [] },
+      members: { names: [], roles: [] }
+    };
+    const stub = jest.fn();
+
+    const wrapper = mount(
+      <PermissionsScreen
+        adminRoles={security.admins.roles}
+        adminNames={security.admins.names}
+        memberRoles={security.members.roles}
+        memberNames={security.members.names}
+        security={security}
+        dispatch={stub} />
+    );
+
+    wrapper
+      .find('.permissions__admins .permissions-add-user input')
+      .simulate('change', {target: {value: 'abc'}});
+
+    wrapper
+      .find('.permissions__admins .permissions-add-user')
+      .simulate('submit');
+
+    expect(stub).not.toHaveBeenCalled();
+  });
+
+  it('add permississon: dispatches if values does not exist', () => {
+
+    const security = {
+      admins:  { names: ['pineapple'], roles: [] },
+      members: { names: [], roles: [] }
+    };
+    const stub = jest.fn();
+
+    const wrapper = mount(
+      <PermissionsScreen security={security} dispatch={stub} />
+    );
+
+    wrapper
+      .find('.permissions__admins .permissions-add-user input')
+      .simulate('change', {target: {value: 'mango'}});
+
+    wrapper
+      .find('.permissions__admins .permissions-add-user')
+      .simulate('submit');
+
+    expect(stub).toHaveBeenCalled();
+  });
+
+  it('remove permississon: dispatches', () => {
+
+    const security = {
+      admins:  { names: ['pineapple'], roles: [] },
+      members: { names: [], roles: [] }
+    };
+    const stub = jest.fn();
+
+    const wrapper = mount(
+      <PermissionsScreen
+        adminRoles={security.admins.roles}
+        adminNames={security.admins.names}
+        memberRoles={security.members.roles}
+        memberNames={security.members.names}
+        security={security}
+        dispatch={stub} />
+    );
+
+    wrapper
+      .find('.permissions__admins .permissions__entry button')
+      .simulate('click');
+
+    expect(stub).toHaveBeenCalled();
+  });
+});

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/25630f4e/app/addons/permissions/actions.js
----------------------------------------------------------------------
diff --git a/app/addons/permissions/actions.js b/app/addons/permissions/actions.js
index f11ff09..0a49cc0 100644
--- a/app/addons/permissions/actions.js
+++ b/app/addons/permissions/actions.js
@@ -12,70 +12,104 @@
 
 import FauxtonAPI from "../../core/api";
 import ActionTypes from "./actiontypes";
-import Stores from "./stores";
-var permissionsStore = Stores.permissionsStore;
+import Promise from 'bluebird';
+import 'whatwg-fetch';
+import { isValueAlreadySet, addValueToPermissions } from './helpers';
 
-export default {
 
-  fetchPermissions: function (database, security) {
-    FauxtonAPI.dispatch({
-      type: ActionTypes.PERMISSIONS_FETCHING,
-      database: database,
-      security: security
-    });
+import {
+  PERMISSIONS_UPDATE
+} from './actiontypes';
 
-    FauxtonAPI.when([database.fetch(), security.fetch()]).then(function () {
-      this.editPermissions(database, security);
-    }.bind(this));
-  },
 
-  editPermissions: function (database, security) {
-    FauxtonAPI.dispatch({
-      type: ActionTypes.PERMISSIONS_EDIT,
-      database: database,
-      security: security
-    });
-  },
-  addItem: function (options) {
-    var check = permissionsStore.getSecurity().canAddItem(options.value, options.type, options.section);
+export const receivedPermissions = json => {
+  return {
+    type: PERMISSIONS_UPDATE,
+    permissions: json
+  };
+};
 
-    if (check.error) {
-      FauxtonAPI.addNotification({
-        msg: check.msg,
-        type: 'error'
-      });
 
-      return;
-    }
+export const fetchPermissions = url => dispatch => {
+  return fetch(url, { headers: { 'Accept': 'application/json' }})
+    .then(res => res.json())
+    .then(json => dispatch(receivedPermissions(json)));
+};
 
-    FauxtonAPI.dispatch({
-      type: ActionTypes.PERMISSIONS_ADD_ITEM,
-      options: options
-    });
+export const setPermissionOnObject = (p, section, type, value) => {
+  if (isValueAlreadySet(p, section, type, value)) {
+    throw new Error('Role/Name has already been added');
+  }
 
-    this.savePermissions();
+  const res = addValueToPermissions(p, section, type, value);
 
-  },
-  removeItem: function (options) {
-    FauxtonAPI.dispatch({
-      type: ActionTypes.PERMISSIONS_REMOVE_ITEM,
-      options: options
-    });
-    this.savePermissions();
-  },
+  return res;
+};
+
+export const deletePermissionFromObject = (p, section, type, value) => {
+  if (!isValueAlreadySet(p, section, type, value)) {
+    throw new Error('Role/Name does not exist');
+  }
+
+  p[section][type] = p[section][type].filter((el) => {
+    return el !== value;
+  });
 
-  savePermissions: function () {
-    permissionsStore.getSecurity().save().then(function () {
+  return p;
+};
+
+export const updatePermission = (url, permissions, section, type, value) => dispatch => {
+  const res = setPermissionOnObject(permissions, section, type, value);
+
+  updatePermissionUnsafe(url, permissions, dispatch)
+    .catch((err) => {
       FauxtonAPI.addNotification({
-        msg: 'Database permissions has been updated.'
+        msg: err.message,
+        type: 'error'
       });
-    }, function (xhr) {
-      if (!xhr && !xhr.responseJSON) { return;}
+    });
+};
+
+export const deletePermission = (url, permissions, section, type, value) => dispatch => {
+  const res = deletePermissionFromObject(permissions, section, type, value);
 
+  updatePermissionUnsafe(url, permissions, dispatch)
+    .catch((err) => {
       FauxtonAPI.addNotification({
-        msg: 'Could not update permissions - reason: ' + xhr.responseJSON.reason,
+        msg: err.message,
         type: 'error'
       });
     });
-  }
+};
+
+export const updatePermissionUnsafe = (url, p, dispatch) => {
+  return fetch(url, {
+    headers: {
+      'Accept': 'application/json',
+      'Content-Type': 'application/json'
+    },
+    credentials: 'include',
+    method: 'PUT',
+    body: JSON.stringify(p)
+  })
+  .then((res) => res.json())
+  .then((json) => {
+    if (!json.ok) {
+      throw new Error(json.reason);
+    }
+    return json;
+  })
+  .then((json) => {
+    FauxtonAPI.addNotification({
+      msg: 'Database permissions has been updated.'
+    });
+
+    return dispatch(receivedPermissions(p));
+  })
+  .catch((error) => {
+    FauxtonAPI.addNotification({
+      msg: 'Could not update permissions - reason: ' + error,
+      type: 'error'
+    });
+  });
 };

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/25630f4e/app/addons/permissions/actiontypes.js
----------------------------------------------------------------------
diff --git a/app/addons/permissions/actiontypes.js b/app/addons/permissions/actiontypes.js
index 132da81..f474fbd 100644
--- a/app/addons/permissions/actiontypes.js
+++ b/app/addons/permissions/actiontypes.js
@@ -10,9 +10,5 @@
 // License for the specific language governing permissions and limitations under
 // the License.
 
-export default {
-  PERMISSIONS_EDIT: 'PERMISSIONS_EDIT',
-  PERMISSIONS_FETCHING: 'PERMISSIONS_FETCHING',
-  PERMISSIONS_ADD_ITEM: 'PERMISSIONS_ADD_ITEM',
-  PERMISSIONS_REMOVE_ITEM: 'PERMISSIONS_REMOVE_ITEM'
-};
+
+export const PERMISSIONS_UPDATE = 'PERMISSIONS_UPDATE';

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/25630f4e/app/addons/permissions/base.js
----------------------------------------------------------------------
diff --git a/app/addons/permissions/base.js b/app/addons/permissions/base.js
index d6ddf5d..5123112 100644
--- a/app/addons/permissions/base.js
+++ b/app/addons/permissions/base.js
@@ -13,8 +13,11 @@
 import app from "../../app";
 import FauxtonAPI from "../../core/api";
 import Permissions from "./routes";
+import reducer from './reducers';
 import "./assets/less/permissions.less";
 
 Permissions.initialize = function () {};
 
+FauxtonAPI.reducers.push(reducer);
+
 export default Permissions;

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/25630f4e/app/addons/permissions/components.react.jsx
----------------------------------------------------------------------
diff --git a/app/addons/permissions/components.react.jsx b/app/addons/permissions/components.react.jsx
deleted file mode 100644
index 4b5d307..0000000
--- a/app/addons/permissions/components.react.jsx
+++ /dev/null
@@ -1,234 +0,0 @@
-// Licensed under the Apache License, Version 2.0 (the "License"); you may not
-// use this file except in compliance with the License. You may obtain a copy of
-// the License at
-//
-//   http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-// License for the specific language governing permissions and limitations under
-// the License.
-
-import app from "../../app";
-import FauxtonAPI from "../../core/api";
-import React from "react";
-import Components from "../components/react-components.react";
-import Stores from "./stores";
-import Actions from "./actions";
-var LoadLines = Components.LoadLines;
-var permissionsStore = Stores.permissionsStore;
-var getDocUrl = app.helpers.getDocUrl;
-
-var PermissionsItem = React.createClass({
-
-  removeItem: function (e) {
-    this.props.removeItem({
-      value: this.props.item,
-      type: this.props.type,
-      section: this.props.section
-    });
-  },
-
-  render: function () {
-    return (
-      <li>
-        <span>{this.props.item}</span>
-        <button onClick={this.removeItem} type="button" className="pull-right close">�</button>
-      </li>
-    );
-  }
-
-});
-
-var PermissionsSection = React.createClass({
-  getInitialState: function () {
-    return {
-      newRole: '',
-      newName: ''
-    };
-  },
-
-  getHelp: function () {
-    if (this.props.section === 'admins') {
-      return 'Database members can access the database. If no members are defined, the database is public. ';
-    }
-
-    return 'Database members can access the database. If no members are defined, the database is public. ';
-  },
-
-  isEmptyValue: function (value, type) {
-    if (!_.isEmpty(value)) {
-      return false;
-    }
-    FauxtonAPI.addNotification({
-      msg: 'Cannot add an empty value for ' + type + '.',
-      type: 'warning'
-    });
-
-    return true;
-  },
-
-  addNames: function (e) {
-    e.preventDefault();
-    if (this.isEmptyValue(this.state.newName, 'names')) {
-      return;
-    }
-    this.props.addItem({
-      type: 'names',
-      section: this.props.section,
-      value: this.state.newName
-    });
-
-    this.setState({newName: ''});
-  },
-
-  addRoles: function (e) {
-    e.preventDefault();
-    if (this.isEmptyValue(this.state.newRole, 'roles')) {
-      return;
-    }
-    this.props.addItem({
-      type: 'roles',
-      section: this.props.section,
-      value: this.state.newRole
-    });
-
-    this.setState({newRole: ''});
-  },
-
-  getItems: function (items, type) {
-    return _.map(items, function (item, i) {
-      return <PermissionsItem key={i} item={item} section={this.props.section} type={type} removeItem={this.props.removeItem} />;
-    }, this);
-  },
-
-  getNames: function () {
-    return this.getItems(this.props.names, 'names');
-  },
-
-  getRoles: function () {
-    return this.getItems(this.props.roles, 'roles');
-  },
-
-  nameChange: function (e) {
-    this.setState({newName: e.target.value});
-  },
-
-  roleChange: function (e) {
-    this.setState({newRole: e.target.value});
-  },
-
-  render: function () {
-    return (
-    <div>
-      <header className="page-header">
-        <h3>{this.props.section}</h3>
-        <p className="help">
-          {this.getHelp()}
-          <a className="help-link" data-bypass="true" href={getDocUrl('DB_PERMISSION')} target="_blank">
-            <i className="icon-question-sign"></i>
-          </a>
-        </p>
-      </header>
-      <div className="row-fluid">
-        <div className="span6">
-          <header>
-            <h4>Users</h4>
-            <p>Specify users who will have {this.props.section} access to this database.</p>
-          </header>
-          <form onSubmit={this.addNames} className="permission-item-form permissions-add-user form-inline">
-            <input onChange={this.nameChange} value={this.state.newName} type="text" className="item input-small" placeholder="Add User" />
-            <button type="submit" className="btn btn-success"><i className="icon fonticon-plus-circled" /> Add User</button>
-          </form>
-          <ul className="clearfix unstyled permission-items span10">
-            {this.getNames()}
-          </ul>
-        </div>
-        <div className="span6">
-          <header>
-            <h4>Roles</h4>
-            <p>Users with any of the following role(s) will have {this.props.section} access.</p>
-          </header>
-          <form onSubmit={this.addRoles} className="permission-item-form permissions-add-role form-inline">
-            <input onChange={this.roleChange} value={this.state.newRole} type="text" className="item input-small" placeholder="Add Role" />
-            <button type="submit" className="btn btn-success"><i className="icon fonticon-plus-circled" /> Add Role</button>
-          </form>
-          <ul className="unstyled permission-items span10">
-            {this.getRoles()}
-          </ul>
-        </div>
-      </div>
-    </div>
-    );
-  }
-
-});
-
-var PermissionsController = React.createClass({
-
-  getStoreState: function () {
-    return {
-      isLoading: permissionsStore.isLoading(),
-      adminRoles: permissionsStore.getAdminRoles(),
-      adminNames: permissionsStore.getAdminNames(),
-      memberRoles: permissionsStore.getMemberRoles(),
-      memberNames: permissionsStore.getMemberNames(),
-    };
-  },
-
-  getInitialState: function () {
-    return this.getStoreState();
-  },
-
-  componentDidMount: function () {
-    permissionsStore.on('change', this.onChange, this);
-  },
-
-  componentWillUnmount: function () {
-    permissionsStore.off('change', this.onChange);
-  },
-
-  onChange: function () {
-    this.setState(this.getStoreState());
-  },
-
-  addItem: function (options) {
-    Actions.addItem(options);
-  },
-
-  removeItem: function (options) {
-    Actions.removeItem(options);
-  },
-
-  render: function () {
-    if (this.state.isLoading) {
-      return <LoadLines />;
-    }
-
-    return (
-      <div className="permissions-page flex-body">
-        <div id="sections">
-          <PermissionsSection roles={this.state.adminRoles}
-            names={this.state.adminNames}
-            addItem={this.addItem}
-            removeItem={this.removeItem}
-            section={'admins'} />
-          <PermissionsSection
-            roles={this.state.memberRoles}
-            names={this.state.memberNames}
-            addItem={this.addItem}
-            removeItem={this.removeItem}
-            section={'members'} />
-        </div>
-      </div>
-    );
-  }
-
-});
-
-export default {
-  PermissionsController: PermissionsController,
-  PermissionsSection: PermissionsSection,
-  PermissionsItem: PermissionsItem
-};

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/25630f4e/app/addons/permissions/components/Permissions.js
----------------------------------------------------------------------
diff --git a/app/addons/permissions/components/Permissions.js b/app/addons/permissions/components/Permissions.js
new file mode 100644
index 0000000..49bc9c8
--- /dev/null
+++ b/app/addons/permissions/components/Permissions.js
@@ -0,0 +1,48 @@
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+import React, { Component, PropTypes } from 'react';
+
+import PermissionsScreen  from './PermissionsScreen';
+
+import { fetchPermissions } from '../actions';
+import { LoadLines } from '../../components/components/loadlines';
+
+
+export default class Permissions extends Component {
+
+  constructor (props) {
+    super(props);
+  }
+
+  componentDidMount() {
+    const { dispatch, url } = this.props;
+    dispatch(fetchPermissions(url));
+  }
+
+  render () {
+    const { isLoading } = this.props;
+
+    return (
+      isLoading ? <LoadLines /> : <PermissionsScreen {...this.props} />
+    );
+  }
+};
+
+Permissions.propTypes = {
+  isLoading: PropTypes.bool.isRequired,
+  adminRoles: React.PropTypes.array.isRequired,
+  adminNames: React.PropTypes.array.isRequired,
+  memberNames: React.PropTypes.array.isRequired,
+  memberRoles: React.PropTypes.array.isRequired,
+  security: React.PropTypes.object.isRequired
+};

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/25630f4e/app/addons/permissions/components/PermissionsItem.js
----------------------------------------------------------------------
diff --git a/app/addons/permissions/components/PermissionsItem.js b/app/addons/permissions/components/PermissionsItem.js
new file mode 100644
index 0000000..6c03456
--- /dev/null
+++ b/app/addons/permissions/components/PermissionsItem.js
@@ -0,0 +1,36 @@
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+import React, { Component, PropTypes } from 'react';
+
+const PermissionsItem = ({removeItem, section, type, value}) =>�{
+
+  return (
+    <li className="permissions__entry">
+      <span>{value}</span>
+      <button
+        onClick={() => removeItem(section, type, value)}
+        type="button"
+        className="pull-right close"
+      >
+        �
+      </button>
+    </li>
+  );
+};
+
+PermissionsItem.propTypes = {
+  value: React.PropTypes.string.isRequired,
+  removeItem: PropTypes.func.isRequired,
+};
+
+export default PermissionsItem;

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/25630f4e/app/addons/permissions/components/PermissionsScreen.js
----------------------------------------------------------------------
diff --git a/app/addons/permissions/components/PermissionsScreen.js b/app/addons/permissions/components/PermissionsScreen.js
new file mode 100644
index 0000000..12bf0ad
--- /dev/null
+++ b/app/addons/permissions/components/PermissionsScreen.js
@@ -0,0 +1,85 @@
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+import FauxtonAPI from '../../../core/api';
+import React, { Component, PropTypes } from 'react';
+
+import PermissionsSection from './PermissionsSection';
+import { updatePermission, deletePermission } from '../actions';
+import { isValueAlreadySet } from '../helpers';
+
+export default class PermissionsScreen extends Component {
+
+  constructor (props) {
+    super(props);
+
+    this.addItem = this.addItem.bind(this);
+    this.removeItem = this.removeItem.bind(this);
+  }
+
+  addItem ({ section, type, value }) {
+
+    if (isValueAlreadySet(this.props.security, section, type, value)) {
+      FauxtonAPI.addNotification({
+        msg: 'Role/Name has already been added',
+        type: 'error'
+      });
+
+      return null;
+    }
+
+    this.props.dispatch(
+      updatePermission(this.props.url, this.props.security, section, type, value)
+    );
+  }
+
+  removeItem (section, type, value) {
+
+    this.props.dispatch(
+      deletePermission(this.props.url, this.props.security, section, type, value)
+    );
+  }
+
+  render () {
+    const {
+      adminRoles,
+      adminNames,
+      memberRoles,
+      memberNames
+    } = this.props;
+
+    return (
+      <div className="permissions-page flex-body">
+        <div>
+          <PermissionsSection
+            roles={adminRoles}
+            names={adminNames}
+            addItem={this.addItem}
+            removeItem={this.removeItem}
+            section="admins" />
+
+          <PermissionsSection
+            roles={memberRoles}
+            names={memberNames}
+            addItem={this.addItem}
+            removeItem={this.removeItem}
+            section="members" />
+        </div>
+      </div>
+    );
+  }
+
+};
+
+PermissionsScreen.propTypes = {
+  security: React.PropTypes.object.isRequired
+};

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/25630f4e/app/addons/permissions/components/PermissionsSection.js
----------------------------------------------------------------------
diff --git a/app/addons/permissions/components/PermissionsSection.js b/app/addons/permissions/components/PermissionsSection.js
new file mode 100644
index 0000000..95eb9bf
--- /dev/null
+++ b/app/addons/permissions/components/PermissionsSection.js
@@ -0,0 +1,163 @@
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+import React, { Component, PropTypes } from 'react';
+
+import FauxtonAPI from '../../../core/api';
+import app from '../../../app';
+import _ from 'lodash';
+
+import PermissionsItem from './PermissionsItem';
+
+
+const getDocUrl = app.helpers.getDocUrl;
+
+const PermissionsSection = React.createClass({
+  getInitialState: function () {
+    return {
+      newRole: '',
+      newName: ''
+    };
+  },
+
+  getDefaultProps: function () {
+    return {
+      names: [],
+      roles: []
+    };
+  },
+
+  getHelp: function () {
+    if (this.props.section === 'admins') {
+      return 'Database members can access the database. If no members are defined, the database is public. ';
+    }
+
+    return 'Database members can access the database. If no members are defined, the database is public. ';
+  },
+
+  isEmptyValue: function (value, type) {
+    if (!_.isEmpty(value)) {
+      return false;
+    }
+    FauxtonAPI.addNotification({
+      msg: 'Cannot add an empty value for ' + type + '.',
+      type: 'error'
+    });
+
+    return true;
+  },
+
+  addNames: function (e) {
+    e.preventDefault();
+    if (this.isEmptyValue(this.state.newName, 'names')) {
+      return;
+    }
+    this.props.addItem({
+      type: 'names',
+      section: this.props.section,
+      value: this.state.newName
+    });
+
+    this.setState({newName: ''});
+  },
+
+  addRoles: function (e) {
+    e.preventDefault();
+    if (this.isEmptyValue(this.state.newRole, 'roles')) {
+      return;
+    }
+    this.props.addItem({
+      type: 'roles',
+      section: this.props.section,
+      value: this.state.newRole
+    });
+
+    this.setState({newRole: ''});
+  },
+
+  getItems: function (items, type) {
+    return items.map((item, i) => {
+      return <PermissionsItem
+        key={i}
+        value={item}
+        section={this.props.section}
+        type={type}
+        removeItem={this.props.removeItem} />;
+    });
+  },
+
+  getNames: function () {
+    return this.getItems(this.props.names, 'names');
+  },
+
+  getRoles: function () {
+    return this.getItems(this.props.roles, 'roles');
+  },
+
+  nameChange: function (e) {
+    this.setState({newName: e.target.value});
+  },
+
+  roleChange: function (e) {
+    this.setState({newRole: e.target.value});
+  },
+
+  render: function () {
+
+    const { section } = this.props;
+
+    return (
+    <div className={"permissions__" + section}>
+      <header className="page-header">
+        <h3>{section}</h3>
+        <p className="help">
+          {this.getHelp()}
+          <a className="help-link" data-bypass="true" href={getDocUrl('DB_PERMISSION')} target="_blank">
+            <i className="icon-question-sign"></i>
+          </a>
+        </p>
+      </header>
+      <div className="row-fluid">
+        <div className="span6">
+          <header>
+            <h4>Users</h4>
+            <p>Specify users who will have {this.props.section} access to this database.</p>
+          </header>
+          <form onSubmit={this.addNames} className="permission-item-form permissions-add-user form-inline">
+            <input onChange={this.nameChange} value={this.state.newName} type="text" className="item input-small" placeholder="Add User" />
+            <button type="submit" className="btn btn-success"><i className="icon fonticon-plus-circled" /> Add User</button>
+          </form>
+          <ul className="unstyled permission-items span10">
+            {this.getNames()}
+          </ul>
+        </div>
+        <div className="span6">
+          <header>
+            <h4>Roles</h4>
+            <p>Users with any of the following role(s) will have {this.props.section} access.</p>
+          </header>
+          <form onSubmit={this.addRoles} className="permission-item-form permissions-add-role form-inline">
+            <input onChange={this.roleChange} value={this.state.newRole} type="text" className="item input-small" placeholder="Add Role" />
+            <button type="submit" className="btn btn-success"><i className="icon fonticon-plus-circled" /> Add Role</button>
+          </form>
+          <ul className="unstyled permission-items span10">
+            {this.getRoles()}
+          </ul>
+        </div>
+      </div>
+    </div>
+    );
+  }
+
+});
+
+export default PermissionsSection;

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/25630f4e/app/addons/permissions/container/PermissionsContainer.js
----------------------------------------------------------------------
diff --git a/app/addons/permissions/container/PermissionsContainer.js b/app/addons/permissions/container/PermissionsContainer.js
new file mode 100644
index 0000000..0360ee7
--- /dev/null
+++ b/app/addons/permissions/container/PermissionsContainer.js
@@ -0,0 +1,40 @@
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+import { connect } from 'react-redux';
+
+import Permissions from '../components/Permissions';
+
+import {
+  getIsLoading,
+  getSecurity,
+  getAdminRoles,
+  getAdminNames,
+  getMemberNames,
+  getMemberRoles
+} from '../reducers';
+
+
+const mapStateToProps = (state) => {
+  return {
+    isLoading: getIsLoading(state),
+    adminRoles: getAdminRoles(state),
+    adminNames: getAdminNames(state),
+    memberNames: getMemberNames(state),
+    memberRoles: getMemberRoles(state),
+    security: getSecurity(state)
+  };
+};
+
+export default connect(
+  mapStateToProps
+)(Permissions);

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/25630f4e/app/addons/permissions/helpers.js
----------------------------------------------------------------------
diff --git a/app/addons/permissions/helpers.js b/app/addons/permissions/helpers.js
new file mode 100644
index 0000000..6658f8c
--- /dev/null
+++ b/app/addons/permissions/helpers.js
@@ -0,0 +1,39 @@
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+export function isValueAlreadySet (p, section, type, value) {
+
+  if (!p[section]) {
+    return false;
+  }
+
+  if (!p[section][type]) {
+    return false;
+  }
+
+  return p[section][type].indexOf(value) !== -1;
+}
+
+export function addValueToPermissions (p, section, type, value) {
+
+  if (!p[section]) {
+    p[section] = {};
+  }
+
+  if (!p[section][type]) {
+    p[section][type] = [];
+  }
+
+  p[section][type].push(value);
+
+  return p;
+}

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/25630f4e/app/addons/permissions/layout.js
----------------------------------------------------------------------
diff --git a/app/addons/permissions/layout.js b/app/addons/permissions/layout.js
index 0ddeee0..84bd27c 100644
--- a/app/addons/permissions/layout.js
+++ b/app/addons/permissions/layout.js
@@ -13,7 +13,7 @@
 import React from 'react';
 import FauxtonAPI from "../../core/api";
 import {TabsSidebarHeader} from '../documents/layouts';
-import Permissions from "./components.react";
+import PermissionsContainer from './container/PermissionsContainer';
 import SidebarComponents from "../documents/sidebar/sidebar.react";
 
 export const PermissionsLayout = ({docURL, database, endpoint, dbName, dropDownLinks}) => {
@@ -32,7 +32,7 @@ export const PermissionsLayout = ({docURL, database, endpoint, dbName, dropDownL
           <SidebarComponents.SidebarController />
         </aside>
         <section id="dashboard-content" className="flex-layout flex-col">
-          <Permissions.PermissionsController />
+          <PermissionsContainer url={endpoint} />
         </section>
       </div>
     </div>

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/25630f4e/app/addons/permissions/reducers.js
----------------------------------------------------------------------
diff --git a/app/addons/permissions/reducers.js b/app/addons/permissions/reducers.js
new file mode 100644
index 0000000..3bf70b7
--- /dev/null
+++ b/app/addons/permissions/reducers.js
@@ -0,0 +1,67 @@
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+import {
+  PERMISSIONS_UPDATE
+} from './actiontypes';
+
+const initialState = {
+  isLoading: true,
+  security: {},
+  adminRoles: [],
+  adminNames: [],
+  memberNames: [],
+  memberRoles: []
+};
+
+export default function permissions (state = initialState, action) {
+  switch (action.type) {
+
+    case PERMISSIONS_UPDATE:
+      const { permissions } = action;
+      return Object.assign({}, state, {
+        isLoading: false,
+
+        security: permissions,
+        adminRoles: getRoles('admins', permissions),
+        adminNames: getNames('admins', permissions),
+        memberRoles: getRoles('members', permissions),
+        memberNames: getNames('members', permissions)
+      });
+
+    default:
+      return state;
+  }
+};
+
+function getRoles (type, permissions) {
+  if (!permissions[type]) {
+    return [];
+  }
+
+  return permissions[type].roles ? permissions[type].roles : [];
+}
+
+function getNames (type, permissions) {
+  if (!permissions[type]) {
+    return [];
+  }
+
+  return permissions[type].names ? permissions[type].names : [];
+}
+
+export const getIsLoading = state => state.isLoading;
+export const getSecurity = state => state.security;
+export const getAdminRoles = state => state.adminRoles;
+export const getAdminNames = state => state.adminNames;
+export const getMemberNames = state => state.memberNames;
+export const getMemberRoles = state => state.memberRoles;

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/25630f4e/app/addons/permissions/resources.js
----------------------------------------------------------------------
diff --git a/app/addons/permissions/resources.js b/app/addons/permissions/resources.js
deleted file mode 100644
index d4a4b58..0000000
--- a/app/addons/permissions/resources.js
+++ /dev/null
@@ -1,83 +0,0 @@
-// Licensed under the Apache License, Version 2.0 (the "License"); you may not
-// use this file except in compliance with the License. You may obtain a copy of
-// the License at
-//
-//   http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-// License for the specific language governing permissions and limitations under
-// the License.
-
-import app from "../../app";
-import FauxtonAPI from "../../core/api";
-var Permissions = FauxtonAPI.addon();
-
-Permissions.Security = Backbone.Model.extend({
-  defaults: {
-    admins:  { names: [], roles: [] },
-    members: { names: [], roles: [] }
-  },
-
-  isNew: function () {
-    return false;
-  },
-
-  initialize: function (attrs, options) {
-    this.database = options.database;
-  },
-
-  url: function () {
-    return window.location.origin + '/' + this.database.safeID() + '/_security';
-  },
-
-  documentation: FauxtonAPI.constants.DOC_URLS.DB_PERMISSION,
-
-  addItem: function (value, type, section) {
-    var sectionValues = this.get(section);
-
-    var check = this.canAddItem(value, type, section);
-    if (check.error) { return check;}
-
-    sectionValues[type].push(value);
-    return this.set(section, sectionValues);
-  },
-
-  canAddItem: function (value, type, section) {
-    var sectionValues = this.get(section);
-
-    if (!sectionValues || !sectionValues[type]) {
-      return {
-        error: true,
-        msg: 'Section ' + section + ' does not exist'
-      };
-    }
-
-    if (sectionValues[type].indexOf(value) > -1) {
-      return {
-        error: true,
-        msg: 'Role/Name has already been added'
-      };
-    }
-
-    return {
-      error: false
-    };
-  },
-
-  removeItem: function (value, type, section) {
-    var sectionValues = this.get(section);
-    var types = sectionValues[type];
-    var indexOf = _.indexOf(types, value);
-
-    if (indexOf  === -1) { return;}
-
-    types.splice(indexOf, 1);
-    sectionValues[type] = types;
-    return this.set(section, sectionValues);
-  }
-
-});
-
-export default Permissions;

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/25630f4e/app/addons/permissions/routes.js
----------------------------------------------------------------------
diff --git a/app/addons/permissions/routes.js b/app/addons/permissions/routes.js
index bd87d59..5025daa 100644
--- a/app/addons/permissions/routes.js
+++ b/app/addons/permissions/routes.js
@@ -13,48 +13,48 @@
 import app from "../../app";
 import FauxtonAPI from "../../core/api";
 import Databases from "../databases/base";
-import Resources from "./resources";
+
 import Actions from "./actions";
 import BaseRoute from "../documents/shared-routes";
 import Layout from './layout';
 import React from 'react';
 
 const PermissionsRouteObject = BaseRoute.extend({
+
   roles: ['fx_loggedIn'],
   routes: {
     'database/:database/permissions': 'permissions'
   },
 
   initialize: function (route, options) {
-    var docOptions = app.getParams();
-    docOptions.include_docs = true;
+    const docOptions = app.getParams();
 
-    this.initViews(options[0]);
+    docOptions.include_docs = true;
   },
 
-  initViews: function (databaseName) {
-    this.database = new Databases.Model({ id: databaseName });
-    this.security = new Resources.Security(null, {
-      database: this.database
-    });
+  permissions: function (databaseId) {
+
+    // XXX magic inheritance props we need to maintain for BaseRoute
+    this.database = new Databases.Model({ id: databaseId });
 
+    // XXX magic methods we have to call - originating from BaseRoute.extend
     this.createDesignDocsCollection();
     this.addSidebar('permissions');
-  },
 
-  permissions: function () {
-    Actions.fetchPermissions(this.database, this.security);
     const crumbs = [
-      { name: this.database.id, link: Databases.databaseUrl(this.database)},
+      { name: this.database.id, link: Databases.databaseUrl(databaseId)},
       { name: 'Permissions' }
     ];
+
+    const url = FauxtonAPI.urls('permissions', 'server', databaseId);
+
     return <Layout
-      docURL={this.security.documentation}
-      endpoint={this.security.url('apiurl')}
+      docURL={FauxtonAPI.constants.DOC_URLS.DB_PERMISSION}
+      endpoint={url}
       dbName={this.database.id}
       dropDownLinks={crumbs}
-      database={this.database}
-    />;
+      database={this.database} />;
+
   }
 });
 

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/25630f4e/app/addons/permissions/stores.js
----------------------------------------------------------------------
diff --git a/app/addons/permissions/stores.js b/app/addons/permissions/stores.js
deleted file mode 100644
index 9737cd0..0000000
--- a/app/addons/permissions/stores.js
+++ /dev/null
@@ -1,104 +0,0 @@
-// Licensed under the Apache License, Version 2.0 (the "License"); you may not
-// use this file except in compliance with the License. You may obtain a copy of
-// the License at
-//
-//   http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-// License for the specific language governing permissions and limitations under
-// the License.
-
-import FauxtonAPI from "../../core/api";
-import ActionTypes from "./actiontypes";
-var Stores = {};
-
-Stores.PermissionsStore = FauxtonAPI.Store.extend({
-  initialize: function () {
-    this._isLoading = true;
-  },
-
-  isLoading: function () {
-    return this._isLoading;
-  },
-
-  editPermissions: function (database, security) {
-    this._database = database;
-    this._security = security;
-    this._isLoading = false;
-  },
-
-  getItem: function (section, type) {
-    if (this._isLoading) {return [];}
-
-    return this._security.get(section)[type];
-  },
-
-  getDatabase: function () {
-    return this._database;
-  },
-
-  getSecurity: function () {
-    return this._security;
-  },
-
-  getAdminRoles: function () {
-    return this.getItem('admins', 'roles');
-  },
-
-  getAdminNames: function () {
-    return this.getItem('admins', 'names');
-  },
-
-  getMemberNames: function () {
-    return this.getItem('members', 'names');
-  },
-
-  getMemberRoles: function () {
-    return this.getItem('members', 'roles');
-  },
-
-  addItem: function (options) {
-    this._security.addItem(options.value, options.type, options.section);
-  },
-
-  removeItem: function (options) {
-    this._security.removeItem(options.value, options.type, options.section);
-  },
-
-  dispatch: function (action) {
-    switch (action.type) {
-      case ActionTypes.PERMISSIONS_FETCHING:
-        this._isLoading = true;
-        this.triggerChange();
-      break;
-
-      case ActionTypes.PERMISSIONS_EDIT:
-        this.editPermissions(action.database, action.security);
-        this.triggerChange();
-      break;
-
-      case ActionTypes.PERMISSIONS_ADD_ITEM:
-        this.addItem(action.options);
-        this.triggerChange();
-      break;
-
-      case ActionTypes.PERMISSIONS_REMOVE_ITEM:
-        this.removeItem(action.options);
-        this.triggerChange();
-      break;
-
-      default:
-      return;
-      // do nothing
-    }
-  }
-
-});
-
-Stores.permissionsStore = new Stores.PermissionsStore();
-
-Stores.permissionsStore.dispatchToken = FauxtonAPI.dispatcher.register(Stores.permissionsStore.dispatch);
-
-export default Stores;

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/25630f4e/app/addons/permissions/tests/actionsSpec.js
----------------------------------------------------------------------
diff --git a/app/addons/permissions/tests/actionsSpec.js b/app/addons/permissions/tests/actionsSpec.js
deleted file mode 100644
index 4424e04..0000000
--- a/app/addons/permissions/tests/actionsSpec.js
+++ /dev/null
@@ -1,122 +0,0 @@
-// Licensed under the Apache License, Version 2.0 (the "License"); you may not
-// use this file except in compliance with the License. You may obtain a copy of
-// the License at
-//
-//   http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-// License for the specific language governing permissions and limitations under
-// the License.
-
-import FauxtonAPI from "../../../core/api";
-import Databases from "../../databases/base";
-import Stores from "../stores";
-import Permissions from "../resources";
-import Actions from "../actions";
-import testUtils from "../../../../test/mocha/testUtils";
-import sinon from "sinon";
-var assert = testUtils.assert;
-var restore = testUtils.restore;
-var store = Stores.permissionsStore;
-
-describe('Permissions Actions', function () {
-  var getSecuritystub;
-
-  beforeEach(function () {
-    var databaseName = 'permissions-test';
-    var database = new Databases.Model({ id: databaseName });
-    Actions.editPermissions(
-      database,
-      new Permissions.Security(null, {
-        database: database
-      })
-    );
-
-
-    var promise = FauxtonAPI.Deferred();
-    getSecuritystub = sinon.stub(store, 'getSecurity');
-    getSecuritystub.returns({
-      canAddItem: function () { return {error: true};},
-      save: function () {
-        return promise;
-      }
-    });
-  });
-
-  afterEach(function () {
-    restore(store.getSecurity);
-  });
-
-  describe('add Item', function () {
-
-    afterEach(function () {
-      restore(FauxtonAPI.addNotification);
-      restore(Actions.savePermissions);
-      restore(store.getSecurity);
-    });
-
-    it('does not save item if cannot add it', function () {
-      var spy = sinon.spy(FauxtonAPI, 'addNotification');
-      var spy2 = sinon.spy(Actions, 'savePermissions');
-
-      Actions.addItem({
-        value: 'boom',
-        type: 'names',
-        section: 'members'
-      });
-
-      assert.ok(spy.calledOnce);
-      assert.notOk(spy2.calledOnce);
-    });
-
-    it('save items', function () {
-      var spy = sinon.spy(FauxtonAPI, 'addNotification');
-      var spy2 = sinon.spy(Actions, 'savePermissions');
-
-      var promise = FauxtonAPI.Deferred();
-      getSecuritystub.returns({
-        canAddItem: function () { return {error: false};},
-        save: function () {
-          return promise;
-        }
-      });
-
-      Actions.addItem({
-        value: 'boom',
-        type: 'names',
-        section: 'members'
-      });
-
-      assert.ok(spy2.calledOnce);
-      assert.notOk(spy.calledOnce);
-    });
-  });
-
-  describe('remove item', function () {
-
-    afterEach(function () {
-      restore(Actions.savePermissions);
-    });
-
-    it('saves item', function () {
-      Actions.addItem({
-        value: 'boom',
-        type: 'names',
-        section: 'members'
-      });
-
-      var spy = sinon.spy(Actions, 'savePermissions');
-
-      Actions.removeItem({
-        value: 'boom',
-        type: 'names',
-        section: 'members'
-      });
-
-      assert.ok(spy.calledOnce);
-    });
-
-  });
-});

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/25630f4e/app/addons/permissions/tests/componentsSpec.react.jsx
----------------------------------------------------------------------
diff --git a/app/addons/permissions/tests/componentsSpec.react.jsx b/app/addons/permissions/tests/componentsSpec.react.jsx
deleted file mode 100644
index 1b90088..0000000
--- a/app/addons/permissions/tests/componentsSpec.react.jsx
+++ /dev/null
@@ -1,135 +0,0 @@
-// Licensed under the Apache License, Version 2.0 (the "License"); you may not
-// use this file except in compliance with the License. You may obtain a copy of
-// the License at
-//
-//   http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-// License for the specific language governing permissions and limitations under
-// the License.
-import FauxtonAPI from "../../../core/api";
-import Databases from "../../databases/base";
-import Permissions from "../resources";
-import Views from "../components.react";
-import Actions from "../actions";
-import utils from "../../../../test/mocha/testUtils";
-import React from "react";
-import ReactDOM from "react-dom";
-import sinon from "sinon";
-import { mount } from 'enzyme';
-var assert = utils.assert;
-var restore = utils.restore;
-
-FauxtonAPI.router = new FauxtonAPI.Router([]);
-
-describe('Permissions Components', () => {
-
-  beforeEach(() => {
-    var savePermissionsStub = sinon.stub(Actions, 'savePermissions');
-  });
-
-  afterEach(() => {
-    restore(Actions.savePermissions);
-  });
-
-  describe('Permissions Controller', () => {
-    afterEach(() => {
-      restore(Actions.addItem);
-      restore(Actions.removeItem);
-    });
-
-    it('on Add triggers add action', () => {
-      var spy = sinon.spy(Actions, 'addItem');
-      const el = mount(<Views.PermissionsController />);
-      el.instance().addItem({});
-      assert.ok(spy.calledOnce);
-    });
-
-    it('on Remove triggers remove action', () => {
-      var spy = sinon.spy(Actions, 'removeItem');
-      const el = mount(<Views.PermissionsController />);
-      el.instance().removeItem({
-        value: 'boom',
-        type: 'names',
-        section: 'members'
-      });
-      assert.ok(spy.calledOnce);
-    });
-
-  });
-
-  describe('PermissionsSection', () => {
-
-    it('adds user on submit', () => {
-      const addSpy = sinon.spy();
-      const el = mount(<Views.PermissionsSection section={'members'} roles={[]} names={[]} addItem={addSpy} />);
-      el.find('.permissions-add-user input').simulate('change', {
-        target: {
-          value: 'newusername'
-        }
-      });
-
-      el.find('.permissions-add-user').simulate('submit');
-
-      var options = addSpy.args[0][0];
-      assert.ok(addSpy.calledOnce);
-      assert.equal(options.type, "names");
-      assert.equal(options.section, "members");
-    });
-
-    it('adds role on submit', () => {
-      const addSpy = sinon.spy();
-      const el = mount(<Views.PermissionsSection section={'members'} roles={[]} names={[]} addItem={addSpy} />);
-
-      el.find('.permissions-add-role input').simulate('change', {
-        target: {
-          value: 'newrole'
-        }
-      });
-
-      el.find('.permissions-add-role').simulate('submit');
-
-      var options = addSpy.args[0][0];
-      assert.ok(addSpy.calledOnce);
-      assert.equal(options.type, "roles");
-      assert.equal(options.section, "members");
-    });
-
-    it('stores new name on change', () => {
-      const addSpy = sinon.spy();
-      var newName = 'newName';
-      const el = mount(<Views.PermissionsSection section={'members'} roles={[]} names={[]} addItem={addSpy} />);
-      el.find('.permissions-add-user .item').simulate('change', {
-        target: {
-          value: newName
-        }
-      });
-
-      assert.equal(el.state().newName, newName);
-    });
-
-    it('stores new role on change', () => {
-      var newRole = 'newRole';
-      const addSpy = sinon.spy();
-      const el = mount(<Views.PermissionsSection section={'members'} roles={[]} names={[]} addItem={addSpy} />);
-      el.find('.permissions-add-role .item').simulate('change', {
-        target: {
-          value: newRole
-        }
-      });
-      assert.equal(el.state().newRole, newRole);
-    });
-  });
-
-  describe('PermissionsItem', () => {
-
-    it('triggers remove on click', () => {
-      const removeSpy = sinon.spy();
-      const el = mount(<Views.PermissionsItem section={'members'} item={'test-item'} removeItem={removeSpy} />);
-      el.find('.close').simulate('click');
-      assert.ok(removeSpy.calledOnce);
-    });
-  });
-});

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/25630f4e/app/addons/permissions/tests/nightwatch/permissions.js
----------------------------------------------------------------------
diff --git a/app/addons/permissions/tests/nightwatch/permissions.js b/app/addons/permissions/tests/nightwatch/permissions.js
new file mode 100644
index 0000000..3092bec
--- /dev/null
+++ b/app/addons/permissions/tests/nightwatch/permissions.js
@@ -0,0 +1,40 @@
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+
+module.exports = {
+
+  'CouchDB Database Permissions Test' : (client) => {
+
+    const waitTime = client.globals.maxWaitTime;
+    const newDatabaseName = client.globals.testDatabaseName;
+    const baseUrl = client.globals.test_settings.launch_url;
+
+    client
+      .loginToGUI()
+      .url(baseUrl + '/#/database/' + newDatabaseName + '/permissions')
+
+      .waitForElementVisible('.permissions__admins', waitTime, false)
+
+      .setValue('.permissions__admins [placeholder="Add User"]', 'blergie')
+      .clickWhenVisible('.permissions__admins .permissions-add-user button')
+
+      .waitForElementVisible('.permissions__admins .permissions__entry', waitTime, false)
+      .assert.containsText('.permissions__entry span', 'blergie')
+
+      .url(baseUrl + '/#/database/' + newDatabaseName + '/permissions')
+      .waitForElementVisible('.permissions__admins .permissions__entry', waitTime, false)
+      .assert.containsText('.permissions__entry span', 'blergie')
+
+      .end();
+  }
+};

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/25630f4e/app/addons/permissions/tests/resourceSpec.js
----------------------------------------------------------------------
diff --git a/app/addons/permissions/tests/resourceSpec.js b/app/addons/permissions/tests/resourceSpec.js
deleted file mode 100644
index 7a77710..0000000
--- a/app/addons/permissions/tests/resourceSpec.js
+++ /dev/null
@@ -1,69 +0,0 @@
-// Licensed under the Apache License, Version 2.0 (the "License"); you may not
-// use this file except in compliance with the License. You may obtain a copy of
-// the License at
-//
-//   http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-// License for the specific language governing permissions and limitations under
-// the License.
-import FauxtonAPI from "../../../core/api";
-import Models from "../resources";
-import testUtils from "../../../../test/mocha/testUtils";
-var assert = testUtils.assert;
-
-describe('Permissions', function () {
-
-  describe('#addItem', function () {
-    var security;
-
-    beforeEach(function () {
-      security = new Models.Security(null, {database: 'fakedb'});
-    });
-
-    it('Should add value to section', function () {
-
-      security.addItem('_user', 'names', 'admins');
-      assert.equal(security.get('admins').names[0], '_user');
-    });
-
-    it('Should handle incorrect type', function () {
-      security.addItem('_user', 'asdasd', 'admins');
-    });
-
-    it('Should handle incorrect section', function () {
-      security.addItem('_user', 'names', 'Asdasd');
-    });
-
-    it('Should reject duplicates', function () {
-      security.addItem('_user', 'names', 'admins');
-      security.addItem('_user', 'names', 'admins');
-      assert.equal(security.get('admins').names.length, 1);
-    });
-  });
-
-  describe('#removeItem', function () {
-    var security;
-
-    beforeEach(function () {
-      security = new Models.Security(null, {database: 'fakedb'});
-    });
-
-    it('removes value from section', function () {
-      security.addItem('_user', 'names', 'admins');
-      security.removeItem('_user', 'names', 'admins');
-
-      assert.equal(security.get('admins').names.length, 0);
-    });
-
-    it('ignores non-existing value', function () {
-      security.addItem('_user', 'names', 'admins');
-      security.removeItem('wrong_user', 'names', 'admins');
-      assert.equal(security.get('admins').names.length, 1);
-    });
-
-  });
-
-});

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/25630f4e/app/core/base.js
----------------------------------------------------------------------
diff --git a/app/core/base.js b/app/core/base.js
index ef3d909..00e4f19 100644
--- a/app/core/base.js
+++ b/app/core/base.js
@@ -157,4 +157,6 @@ FauxtonAPI.setSession = function (newSession) {
   return FauxtonAPI.session.fetchUser();
 };
 
+FauxtonAPI.reducers = [];
+
 export default FauxtonAPI;

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/25630f4e/app/helpers.js
----------------------------------------------------------------------
diff --git a/app/helpers.js b/app/helpers.js
index f80a3f0..9310ffc 100644
--- a/app/helpers.js
+++ b/app/helpers.js
@@ -21,6 +21,7 @@ import constants from "./constants";
 import utils from "./core/utils";
 import d3 from "d3";
 import moment from "moment";
+import _ from 'lodash';
 
 var Helpers = {};
 

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/25630f4e/app/main.js
----------------------------------------------------------------------
diff --git a/app/main.js b/app/main.js
index 0a1c7e0..7f092da 100644
--- a/app/main.js
+++ b/app/main.js
@@ -19,6 +19,9 @@ import Backbone from 'backbone';
 import $ from 'jquery';
 import AppWrapper from './addons/fauxton/appwrapper';
 
+import { createStore, applyMiddleware } from 'redux';
+import thunk from 'redux-thunk';
+import { Provider } from 'react-redux';
 
 app.addons = LoadAddons;
 FauxtonAPI.router = app.router = new FauxtonAPI.Router(app.addons);
@@ -54,4 +57,22 @@ $(document).on("click", "a:not([data-bypass])", function (evt) {
   }
 });
 
-ReactDOM.render(<AppWrapper router={app.router}/>, document.getElementById('app'));
+
+const reducer = FauxtonAPI.reducers.reduce((el, acc) => {
+  acc[el] = el;
+  return acc;
+}, {});
+
+const middlewares = [thunk];
+
+const store = createStore(
+  reducer,
+  applyMiddleware(...middlewares)
+);
+
+ReactDOM.render(
+  <Provider store={store}>
+    <AppWrapper router={app.router}/>
+  </Provider>,
+  document.getElementById('app')
+);

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/25630f4e/jest-config.json
----------------------------------------------------------------------
diff --git a/jest-config.json b/jest-config.json
index 3321768..7f817ce 100644
--- a/jest-config.json
+++ b/jest-config.json
@@ -1,3 +1,13 @@
 {
-  "testPathDirs": ["app"]
+  "testPathDirs": ["app"],
+
+  "setupTestFrameworkScriptFile": "jest-setup.js",
+
+  "moduleNameMapper": {
+    "bootstrap": "<rootDir>/assets/js/libs/bootstrap",
+    "underscore": "lodash",
+
+    "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|swf|wav|mp3|m4a|aac|oga)$": "<rootDir>/__mocks__/fileMock.js",
+    "\\.(css|less)$": "<rootDir>/__mocks__/styleMock.js"
+  }
 }

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/25630f4e/jest-setup.js
----------------------------------------------------------------------
diff --git a/jest-setup.js b/jest-setup.js
new file mode 100644
index 0000000..35502e6
--- /dev/null
+++ b/jest-setup.js
@@ -0,0 +1,18 @@
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+const jest = require('jest');
+
+window.$ = window.jQuery = require('jquery');
+jest.mock('zeroclipboard', () => {});
+
+global.fetch = require('jest-fetch-mock');

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/25630f4e/package.json
----------------------------------------------------------------------
diff --git a/package.json b/package.json
index 2f141a6..bcd4fcb 100644
--- a/package.json
+++ b/package.json
@@ -20,12 +20,15 @@
     "enzyme": "^2.4.1",
     "es5-shim": "4.5.4",
     "jest": "^17.0.3",
+    "jest-fetch-mock": "^1.0.6",
     "mocha": "~3.1.2",
     "mocha-loader": "^1.0.0",
     "mocha-phantomjs": "git+https://github.com/garrensmith/mocha-phantomjs.git",
     "nightwatch": "~0.9.0",
     "phantomjs-prebuilt": "^2.1.7",
     "react-addons-test-utils": "~15.4.1",
+    "redux-devtools": "^3.3.1",
+    "redux-mock-store": "^1.2.1",
     "sinon": "git+https://github.com/sinonjs/sinon.git",
     "url-polyfill": "github/url-polyfill"
   },
@@ -41,6 +44,7 @@
     "babel-register": "^6.4.3",
     "backbone": "^1.1.0",
     "base-64": "^0.1.0",
+    "bluebird": "^3.4.6",
     "brace": "^0.7.0",
     "chai": "^3.5.0",
     "clean-css": "^3.4.9",
@@ -82,7 +86,10 @@
     "react-addons-css-transition-group": "~15.4.1",
     "react-bootstrap": "^0.30.7",
     "react-dom": "~15.4.1",
+    "react-redux": "^4.4.5",
     "react-select": "1.0.0-rc.2",
+    "redux": "^3.6.0",
+    "redux-thunk": "^2.1.0",
     "request": "^2.54.0",
     "semver": "^5.1.0",
     "send": "^0.13.1",
@@ -96,6 +103,7 @@
     "visualizeRevTree": "git+https://github.com/neojski/visualizeRevTree.git#gh-pages",
     "webpack": "^1.12.12",
     "webpack-dev-server": "^1.14.1",
+    "whatwg-fetch": "^2.0.1",
     "zeroclipboard": "^2.2.0"
   },
   "scripts": {
@@ -104,7 +112,7 @@
     "webpack:test": "webpack --debug --progress --colors --config ./webpack.config.test.js",
     "webpack:release": "webpack --debug --progress --colors --config ./webpack.config.release.js",
     "jest": "jest --config ./jest-config.json",
-    "test": "npm run jest && grunt test",
+    "test": "grunt test && npm run jest",
     "phantomjs": "./node_modules/.bin/mocha-phantomjs --debug=false --ssl-protocol=sslv2 --web-security=false --ignore-ssl-errors=true ./test/runner.html",
     "couchdebug": "grunt couchdebug",
     "couchdb": "grunt couchdb",