<|start_filename|>frontend/js/helpers/cleanupXss.js<|end_filename|>
/**
*  vim:set sw=2 ts=2 sts=2 ft=javascript expandtab:
*
*  # CleanupXss helpers functions
*
*  ## License
*
*  Licensed to the Apache Software Foundation (ASF) under one
*  or more contributor license agreements.  See the NOTICE file
*  distributed with this work for additional information
*  regarding copyright ownership.  The ASF licenses this file
*  to you 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.
*
*  ## Description
*
*  This module contains function to check if the user cache is populated
*/

module.exports = (function () {
  var cleanupXss = {};

  /**
  * `cleanup` removes `<` and `>` to replace them by HTML entities
  */

  var rgx1 = new RegExp('<', 'g');
  var rgx2 = new RegExp('>', 'g');

  cleanupXss.cleanup = function(text) {
    return text.replace(rgx1, '&lt;').replace(rgx2, '&gt;');
  };

  return cleanupXss;

}).call(this);


<|start_filename|>frontend/js/widgets/notification.js<|end_filename|>
/**
*  vim:set sw=2 ts=2 sts=2 ft=javascript expandtab:
*
*  # Notification module
*
*  ## License
*
*  Licensed to the Apache Software Foundation (ASF) under one
*  or more contributor license agreements.  See the NOTICE file
*  distributed with this work for additional information
*  regarding copyright ownership.  The ASF licenses this file
*  to you 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.
*
*  ## Description
*
*  This module contains a generic widgets handling several types of visual
*  notifications.
*/

module.exports = (function () {
  'use strict';
  // Global dependencies
  var m = require('mithril');
  // Local dependencies
  var conf = require('../configuration.js');

  var notif = {};

  /**
  * ## Model
  *
  * `model` contains a list of items id -> item and an autoincrement id.
  */

  notif.model = {
    items: {},
    counter: 0
  };

  /**
  * ## Controller
  *
  * `controller` relies on `model` and gives functions to close directly or
  * after a delay a notification.
  */
  notif.controller = function () {
    var c     = {};
    c.toClose = {};
    c.close   = function (id, from) {
      // with from false, no clear because notimeout
      if ((from) && (from !== 'timeout')) {
        window.clearTimeout(c.toClose[id]);
      }
      delete c.toClose[id];
      delete notif.model.items[id];
    };

    c.delayClose = function (id, timeout) {
      if ((timeout) && !(c.toClose[id])) {
        c.toClose[id] = window.setTimeout(function () {
          m.startComputation();
          c.close(id, 'timeout');
          m.endComputation();
        }, timeout * 1000);
      }
    };
    return c;
  };

  /**
  * ## View
  *
  * Self contained view for notifications.
  */

  notif.view = function (ctrl) {
    var keys = Object.keys(notif.model.items).sort();
    return m('div', keys.map(function (id) {
      var n = notif.model.items[id];
      ctrl.delayClose(id, n.timeout);
      var closeFn = ctrl.close.bind(ctrl, id, n.timeout);
      return m('div', {
        class: n.cls ? n.cls : '',
        onclick: (n.click ? n.click : closeFn)
      }, [
        m('header', [
          (n.icon ? m('i', { class: 'glyphicon glyphicon-' + n.icon }) : ''),
          m('span', ' '+n.title)
        ]),
        m('i', {
          class: 'close glyphicon glyphicon-remove-circle',
          onclick: closeFn
        }),
        m('p', m.trust(n.body))
      ]);
    }));
  };

  /**
  * ## Public functions
  *
  * `send` is the generic way of sending a new notification.
  * They all takes an `options` object and a `callback` function.
  */

  notif.send = function (options, callback) {
    if (options.timeout === undefined) { options.timeout = 10; }
    notif.model.counter += 1;
    notif.model.items[notif.model.counter] = options;
    m.redraw();
    if (callback) { callback(); }
  };

  notif.success = function (options, callback) {
    options.title = options.title || conf.LANG.NOTIFICATION.SUCCESS;
    options.cls   = 'alert alert-success';
    options.icon  = 'ok';
    notif.send(options, callback);
  };

  notif.info = function (options, callback) {
    options.title = options.title || conf.LANG.NOTIFICATION.INFO;
    options.cls   = 'alert alert-info';
    options.icon  = 'info-sign';
    notif.send(options, callback);
  };

  notif.warning = function (options, callback) {
    options.title   = conf.LANG.NOTIFICATION.WARNING;
    options.cls     = 'alert alert-warning';
    options.icon    = 'warning-sign';
    options.timeout = 15;
    notif.send(options, callback);
  };

  notif.error = function (options, callback) {
    options.title   = conf.LANG.NOTIFICATION.ERROR;
    options.cls     = 'alert alert-danger';
    options.icon    = 'exclamation-sign';
    options.timeout = 15;
    notif.send(options, callback);
  };

  notif.errorUnexpected = function (options, callback) {
    options.body = '<em>' + options.body + '</em><br>' +
      conf.LANG.NOTIFICATION.ERROR_UNEXPECTED;
    notif.error(options, callback);
  };

  return notif;

}).call(this);


<|start_filename|>model/common-group-pad.js<|end_filename|>
/**
*  vim:set sw=2 ts=2 sts=2 ft=javascript expandtab:
*
*  # Group Model
*
*  ## License
*
*  Licensed to the Apache Software Foundation (ASF) under one
*  or more contributor license agreements.  See the NOTICE file
*  distributed with this work for additional information
*  regarding copyright ownership.  The ASF licenses this file
*  to you 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 = (function () {
  'use strict';

  // Dependencies
  var ld           = require('lodash');
  var hashPassword = require('./common.js').hashPassword;

  /**
  * ## Description
  *
  * This module regroups shared functions for group and pad models.
  */

  var commonGroupPad = {};

  /**
  * ### handlePassword
  *
  * `handlePassword` is a function that ensures if `visibility` is *private*, a
  * password has been filled. Also, it encrypts the given password with a salt,
  * using `common.hashPassword`. It takes :
  *
  * - `params` group object
  * - `callback` function, called with *null* if `visibility` isn't *private* or
  *   if the password is an object, or with an error or *null* and the *password*
  *   object
  */

  commonGroupPad.handlePassword = function (params, callback) {
    if ((params.visibility !== 'private') || ld.isObject(params.password)) {
      return callback(null);
    }
    if (!ld.isString(params.password) || ld.isEmpty(params.password)) {
      var err = new Error('BACKEND.ERROR.AUTHENTICATION.PASSWORD_INCORRECT');
      return callback(err);
    }
    hashPassword(undefined, params.password, function (err, res) {
      if (err) { return callback(err); }
      callback(null, res);
    });
  };

  return commonGroupPad;


}).call(this);


<|start_filename|>spec/frontend/admin-users.spec.js<|end_filename|>
/**
*  vim:set sw=2 ts=2 sts=2 ft=javascript expandtab:
*
*  # Admin Users Management functional testing
*
*  ## License
*
*  Licensed to the Apache Software Foundation (ASF) under one
*  or more contributor license agreements.  See the NOTICE file
*  distributed with this work for additional information
*  regarding copyright ownership.  The ASF licenses this file
*  to you 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 = (function () {
  'use strict';
  // Dependencies
  var ld = require('lodash');
  var common = require('./common.js');
  var fill = common.helper.fill;

  var admin = {};

  admin.run = function (app) {
    // Shared variables
    var $el;
    var qfirst = function (sel) { return app.document.querySelector(sel); };
    var qall = function (sel) { return app.document.querySelectorAll(sel); };

    describe('admin users management module testing', function () {

      beforeAll(function (done) {
        qfirst('ul.lang li:first-child').click();
        window.setTimeout(function () {
          // Go to admin users page
          qfirst('footer a[href$=admin]').click();
          fill(qfirst('input[name=login]'), 'grace');
          fill(qfirst('input[name=password]'), '<PASSWORD>');
          qfirst('input[type=submit]').click();
          window.setTimeout(function () {
            qfirst('body > section p').click();
            window.setTimeout(function () {
              qfirst('header nav a[href$="admin/users"]').click();
              window.setTimeout(done, 200);
            });
          }, 200);
        }, 100);
      });

      afterAll(function (done) {
        qfirst('header nav a[href$="admin/logout"]').click();
        window.setTimeout(function () {
          qfirst('body > section p').click();
          window.setTimeout(done, 100);
        }, 200);
      });

      describe('admin user research', function () {

        beforeAll(function (done) {
          $el = {
            login: qfirst('input[name=login]'),
            box: qfirst('.admin-users'),
            submit: qfirst('input[type=submit]'),
            form: qfirst('form#users-form')
          };
          window.setTimeout(done, 100);
        });

        it('should display default properly', function () {
          expect(ld.isEmpty($el.login.value)).toBeTruthy();
          expect($el.box.textContent).toMatch('No user');
        });

        it('should require login to search', function (done) {
          $el.submit.click();
          window.setTimeout(function () {
            expect($el.login.checkValidity()).toBeFalsy();
            expect($el.form.checkValidity()).toBeFalsy();
            done();
          }, 100);
        });

        it('should not found inexistent user', function (done) {
          fill($el.login, 'inexistent');
          $el.submit.click();
          window.setTimeout(function () {
            $el.box = qfirst('.admin-users');
            expect($el.box.textContent).toMatch('No user');
            var $err = qfirst('body > section p');
            expect($err.textContent).toMatch('User not found');
            $err.click();
            done();
          }, 200);
        });

        it('should found existent user and propose edit and remove',
          function (done) {
            fill($el.login, 'frank');
            $el.submit.click();
            window.setTimeout(function () {
              var box = qfirst('.admin-users li');
              expect(box.textContent).toMatch('frank');
              var actions = box.querySelectorAll('span.actions a');
              expect(actions[0].getAttribute('title')).toBe('Edit');
              expect(actions[1].getAttribute('title')).toBe('Remove');
              var $info = qfirst('body > section p');
              expect($info.textContent).toMatch('User found');
              $info.click();
              done();
            }, 200);
          }
        );

      });

      describe('admin user edit', function () {

        beforeAll(function (done) {
          qfirst('.admin-users span.actions a').click();
          window.setTimeout(function () {
            $el = {
              password: qfirst('input[name=password]'),
              passwordConfirm: qfirst('input[name=passwordConfirm]'),
              email: qfirst('input[name=email]'),
              lang: qfirst('select[name=lang]'),
              firstname: qfirst('input[name=firstname]'),
              lastname: qfirst('input[name=lastname]'),
              organization: qfirst('input[name=organization]'),
              color: qfirst('input[name=color]'),
              submit: qfirst('input[type=submit]'),
              form: qfirst('form#subscribe-form')
            };
            window.setTimeout(done, 100);
          }, 200);
        });

        it('should display user properties correctly', function () {
          expect(ld.isEmpty($el.password.value)).toBeTruthy();
          expect(ld.isEmpty($el.passwordConfirm.value)).toBeTruthy();
          expect($el.email.value).toBe('<EMAIL>');
          expect($el.lang.value).toBe('en');
          var options = $el.lang.querySelectorAll('option');
          expect(options[0].value).toBe('en');
          expect(options[1].value).toBe('fr');
          expect(ld.isEmpty($el.firstname.value)).toBeTruthy();
          expect(ld.isEmpty($el.lastname.value)).toBeTruthy();
          expect(ld.isEmpty($el.organization.value)).toBeTruthy();
          expect($el.color.value).toBe('#000000');
        });

        it('should require required fields', function (done) {
          fill($el.email, '');
          $el.lang.value = '';
          $el.lang.onchange($el.lang);
          window.setTimeout(function () {
            expect($el.email.checkValidity()).toBeFalsy();
            expect($el.lang.required).toBeTruthy();
            fill($el.email, '<EMAIL>');
            $el.lang.value = 'fr';
            $el.lang.onchange($el.lang);
            window.setTimeout(function () {
              expect($el.email.checkValidity()).toBeTruthy();
              expect($el.lang.checkValidity()).toBeTruthy();
              done();
            }, 100);
          }, 100);
        });

        it('should handle password correctly', function (done) {
          fill($el.password, '');
          window.setTimeout(function () {
            expect($el.password.checkValidity()).toBeTruthy();
            expect($el.form.checkValidity()).toBeTruthy();
            fill($el.password, '<PASSWORD>');
            fill($el.passwordConfirm, '<PASSWORD>');
            window.setTimeout(function () {
              $el.submit.click();
              window.setTimeout(function () {
                var $warn = qfirst('body > section p');
                expect($warn.textContent).toMatch('passwords do not match');
                $warn.click();
                window.setTimeout(done, 100);
              }, 100);
            }, 100);
          }, 100);
        });

        it('should update all properties', function (done) {
          fill($el.passwordConfirm, '<PASSWORD>');
          fill($el.firstname, 'Frank');
          fill($el.lastname, 'Lemmer');
          window.setTimeout(function () {
            $el.submit.click();
            window.setTimeout(function () {
              var $success = qfirst('body > section p');
              expect($success.textContent)
                .toMatch('Profile successfully updated');
              $success.click();
              window.setTimeout(done, 100);
            }, 200);
          }, 100);
        });

      });

      describe('admin user remove', function () {

        beforeAll(function (done) {
          qfirst('header nav a[href$="admin/users"]').click();
          window.setTimeout(function () {
            fill(qfirst('input[name=login]'), 'frank');
            window.setTimeout(function () {
              qfirst('input[type=submit]').click();
              window.setTimeout(function () {
                qfirst('body > section p').click();
                window.setTimeout(done, 100);
              }, 200);
            }, 100);
          }, 200);
        });

        it('should remove user', function (done) {
          var remove = qall('.admin-users span.actions a')[1];
          expect(remove.getAttribute('title')).toBe('Remove');
          app.window.confirm = function () { return true; };
          remove.click();
          window.setTimeout(function () {
            var $success = qfirst('body > section p');
            expect($success.textContent)
              .toMatch('Account removed with success');
            $success.click();
            window.setTimeout(done, 100);
          }, 200);
        });

      });

    });

  };

  return admin;

}).call(this);


<|start_filename|>frontend/js/modules/userlist.js<|end_filename|>
/**
*  vim:set sw=2 ts=2 sts=2 ft=javascript expandtab:
*
*  # Userlists module
*
*  ## License
*
*  Licensed to the Apache Software Foundation (ASF) under one
*  or more contributor license agreements.  See the NOTICE file
*  distributed with this work for additional information
*  regarding copyright ownership.  The ASF licenses this file
*  to you 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.
*
*  ## Description
*
*  This module displays all userlists from a authenticated user and allow him
*  to create, rename and remove each of them.
*/

module.exports = (function () {
  'use strict';
  // Global dependencies
  var m  = require('mithril');
  var ld = require('lodash');

  // Local dependencies
  var conf   = require('../configuration.js');
  var auth   = require('../auth.js');
  var layout = require('./layout.js');
  var model  = require('../model/group.js');

  var userlist = {};

  /**
  * ## Controller
  *
  * Used for module state and actions.
  */

  userlist.controller = function () {
    if (!auth.isAuthenticated()) {
      conf.unauthUrl(true);
      return m.route('/login');
    }
    document.title = conf.LANG.MENU.USERLIST + ' - ' + conf.SERVER.title;

    var c = {};

    var init = function () {
      c.userlists = auth.userInfo().userlists;
    };

    if (ld.isEmpty(model.groups())) { model.fetch(init); } else { init(); }
    return c;
  };

  /**
  * ## Views
  */

  var view = {};

  view.userlist = function (c, ul, key) {
    var ulistRoute = '/myuserlists/' + key;
    var actions    = [
      m('a.btn.btn-default.btn-xs', {
        href: ulistRoute + '/edit',
        config: m.route,
        title: conf.LANG.GROUP.EDIT
      }, [ m('i.glyphicon.glyphicon-pencil') ]),
      m('a.btn.btn-default.btn-xs', {
        href: ulistRoute + '/remove',
        config: m.route,
        title: conf.LANG.GROUP.REMOVE
      }, [ m('i.glyphicon.glyphicon-trash.text-danger') ])
    ];
    return m('tr', [
        m('th',
          m('a', {
            href: ulistRoute + '/edit',
            config: m.route,
            title: conf.LANG.GROUP.EDIT
          }, ul.name)
        ),
        m('td', ld.size(ul.uids)),
        m('td.text-right', actions)
      ]);
  };

  view.main = function (c) {
    var ulists = ld.reduce(c.userlists, function (memo, ul, key) {
      memo.push(view.userlist(c, ul, key));
      return memo;
    }, []);
    return m('section', [
      m('h2', [
        m('span', conf.LANG.MENU.USERLIST),
        m('a.btn.btn-warning.pull-right', {
          href: '/myuserlists/add',
          config: m.route
        }, [
          m('span', conf.LANG.USERLIST.ADD)
        ]),
      ]),
      m('section.panel.panel-default',
        m('table.table.table-stripped', [
          m('thead',
            m('tr', [
              m('th', {scope: 'col'}),  // Name
              m('th', {scope: 'col'}, conf.LANG.USERLIST.FIELD.USERS),
              m('th', {scope: 'col'})   // Actions
            ])
          ),
          m('tbody', ulists)
        ])
      )
    ]);
  };

  view.aside = function () {
    return m('section.user-aside', [
      m('h2', conf.LANG.ACTIONS.HELP),
      m('article.well', m.trust(conf.LANG.USERLIST.HELP))
    ]);
  };

  userlist.view = function (c) {
    return layout.view(view.main(c), view.aside(c));
  };

  return userlist;

}).call(this);


<|start_filename|>frontend/js/widgets/tag.js<|end_filename|>
/**
*  vim:set sw=2 ts=2 sts=2 ft=javascript expandtab:
*
*  # Tag field module
*
*  ## License
*
*  Licensed to the Apache Software Foundation (ASF) under one
*  or more contributor license agreements.  See the NOTICE file
*  distributed with this work for additional information
*  regarding copyright ownership.  The ASF licenses this file
*  to you 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.
*
*  ## Description
*
*  This module contains a generic widget handling tag like widget, using HTML5
*  datalist element.
*/

module.exports = (function () {
  'use strict';
  // Global dependencies
  var m  = require('mithril');
  var ld = require('lodash');

  // Local dependencies
  var conf = require('../configuration.js');

  var tag = {};

  /**
  * ## Controller
  *
  * Used for state. Takes a `config` JS Object with :
  *
  * - `content` ??
  * - `tags` array, containing available choices
  * - `current` array, for current attached values
  * - optional `label` text for input field, default to *TAG.TAGS*
  * - optional `placeholder` text for input field, default to *TAG.PLACEHOLDER*
  * - optional `name` attribute, for the name of the input, default to *tags*
  */

  tag.controller = function (config) {
    var c         = config;
    c.label       = c.label || conf.LANG.TAG.TAGS;
    c.placeholder = c.placeholder || conf.LANG.TAG.PLACEHOLDER;
    c.name        = c.name || 'tags';

    c.tags = ld.difference(c.tags, c.current);

    c.add = function (input) {
      var v = input.value;
      if (v.length !== 0) {
        if (!ld.includes(c.current, v)) { c.current.push(v); }
        ld.pull(c.tags, v);
        input.value = '';
      }
    };

    c.addMultiple = function (textarea) {
      var v = textarea.value;
      if (v.length !== 0) {
        v              = ld.compact(v.split('\n'));
        c.current      = ld.union(c.current, v);
        c.tags         = ld.difference(c.tags, v);
        textarea.value = '';
      }
    };

    return c;
  };

  /**
  * ## Views
  *
  * Views for the widget.
  */

  tag.views = {};

  tag.views.icon = function () {
    return m('i', {
      class: 'mp-tooltip glyphicon glyphicon-info-sign tag',
      'data-msg': conf.LANG.TAG.HELP
    });
  };

  tag.views.input = function (c) {
    return m('input.form-control', {
      id: c.name + '-input',
      name: c.name,
      type: 'text',
      list: c.name,
      placeholder: c.placeholder,
      onkeydown: function (e) {
        if (e.keyCode === 13) { // ENTER
          e.preventDefault();
          c.add(e.target);
        }
      },
      onchange: function (e) { c.add(e.target); }
    });
  };

  tag.views.datalist = function (c) {
    return m('datalist', { id: c.name }, ld.map(c.tags, function (tag) {
      return m('option', { value: tag });
    }));
  };

  tag.views.tagslist = function (c) {
    return m('ul.list-inline help-block', ld.map(c.current, function (tag) {
      return m('li', [
        m('span.label.label-default', tag),
        m('i', {
          role: 'button',
          class: 'small glyphicon glyphicon-remove text-danger',
          onclick: function (e) {
            var value = e.target.parentElement.textContent;
            ld.pull(c.current, value);
            c.tags.push(value);
          }
        })
        ]);
      }));
  };

  tag.view = function (c) {
    return m('div.form-group', [
      m('label.col-sm-4', { for: c.name }, [ c.label, tag.views.icon() ]),
      m('.col-sm-7', [
        m('.input-group.input-group-sm', [
          tag.views.input(c),
          m('span.input-group-btn',
            m('button.btn.btn-default', {
              type: 'button',
              onclick: function () {
                c.add(document.getElementById(c.name + '-input'));
              },
            }, conf.LANG.USER.OK)
          )
        ]),
        tag.views.datalist(c),
        tag.views.tagslist(c)
      ])
    ]);
  };

  return tag;
}).call(this);


<|start_filename|>spec/frontend/subscribe.spec.js<|end_filename|>
/**
*  vim:set sw=2 ts=2 sts=2 ft=javascript expandtab:
*
*  # Subcription functional testing
*
*  ## License
*
*  Licensed to the Apache Software Foundation (ASF) under one
*  or more contributor license agreements.  See the NOTICE file
*  distributed with this work for additional information
*  regarding copyright ownership.  The ASF licenses this file
*  to you 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 = (function () {
  'use strict';
  // Dependencies
  var ld = require('lodash');
  var common = require('./common.js');
  var fill = common.helper.fill;

  var subscribe = {};

  // Shared variables
  var first;
  var $el;
  var hash;

  subscribe.beforeAll = function (app, done) {
    // Go to subscription page
    common.user.beforeAll(app, 'header nav > ul > li:last-child > a',
      function (res) {
        $el = res.$el; first = res.first; hash = res.hash;
        $el.color.type = 'text';
        ld.assign($el, { login: first('input[name=login]') });
        done();
      }
    );
  };

  subscribe.run = function (app) {

    describe('subscription module testing', function () {
      beforeAll(function (done) { subscribe.beforeAll(app, done); });

      it('should forbid subscription with no fill', function (done) {
        $el.submit.click();
        expect($el.form.checkValidity()).toBeFalsy();
        expect($el.login.checkValidity()).toBeFalsy();
        expect($el.password.checkValidity()).toBeFalsy();
        expect($el.passwordConfirm.checkValidity()).toBeFalsy();
        expect($el.email.checkValidity()).toBeFalsy();
        expect($el.lang.checkValidity()).toBeTruthy();
        expect($el.lang.value).toBe('en');
        expect($el.firstname.checkValidity()).toBeTruthy();
        expect($el.lastname.checkValidity()).toBeTruthy();
        expect($el.organization.checkValidity()).toBeTruthy();
        expect($el.color.checkValidity()).toBeTruthy();
        done();
      });

      it('should forbid subscription with incorrect fill', function (done) {
        fill($el.login, 'mikey');
        fill($el.password, '<PASSWORD>');
        fill($el.email, 'notamail');
        fill($el.color, 'badHexCode');
        expect($el.form.checkValidity()).toBeFalsy();
        expect($el.login.checkValidity()).toBeTruthy();
        expect($el.password.checkValidity()).toBeFalsy();
        expect($el.email.checkValidity()).toBeFalsy();
        expect($el.color.checkValidity()).toBeFalsy();
        done();
      });

      it('should forbid subscription with unmatched passwords',
        function (done) {
          fill($el.login, 'mikey');
          fill($el.password, '<PASSWORD>');
          fill($el.passwordConfirm, '<PASSWORD>');
          fill($el.email, '<EMAIL>');
          fill($el.color, '');
          expect($el.form.checkValidity()).toBeTruthy();
          expect($el.login.checkValidity()).toBeTruthy();
          expect($el.password.checkValidity()).toBeTruthy();
          expect($el.email.checkValidity()).toBeTruthy();
          $el.submit.click();
          window.setTimeout(function () {
            expect(app.location.search).toBe(hash);
            var $ialert = first('.glyphicon-exclamation-sign');
            expect($ialert.getAttribute('data-msg'))
              .toMatch('For verification');
            var $warning = app.document.querySelectorAll('body > section p');
            $warning = $warning[$warning.length - 1];
            expect($warning.textContent)
              .toMatch('The entered passwords do not match');
            $warning.click();
            done();
          }, 100);
        }
      );

      it('should disallow subscription for existing user', function (done) {
        fill($el.login, 'parker');
        fill($el.password, '<PASSWORD>');
        fill($el.passwordConfirm, '<PASSWORD>');
        fill($el.email, '<EMAIL>');
        expect($el.form.checkValidity()).toBeTruthy();
        expect($el.login.checkValidity()).toBeTruthy();
        expect($el.password.checkValidity()).toBeTruthy();
        expect($el.email.checkValidity()).toBeTruthy();
        $el.submit.click();
        window.setTimeout(function () {
          expect(app.location.search).toBe(hash);
          var $error = app.document.querySelectorAll('body > section p');
          $error = $error[$error.length - 1];
          expect($error.textContent).toMatch('User already exists');
          $error.click();
          done();
        }, 100);
      });

      it('should disallow subscription for existing email', function (done) {
        fill($el.login, 'betterParker');
        fill($el.password, '<PASSWORD>');
        fill($el.passwordConfirm, '<PASSWORD>');
        fill($el.email, '<EMAIL>');
        expect($el.form.checkValidity()).toBeTruthy();
        expect($el.login.checkValidity()).toBeTruthy();
        expect($el.password.checkValidity()).toBeTruthy();
        expect($el.email.checkValidity()).toBeTruthy();
        $el.submit.click();
        window.setTimeout(function () {
          expect(app.location.search).toBe(hash);
          var $error = app.document.querySelectorAll('body > section p');
          $error = $error[$error.length - 1];
          expect($error.textContent)
            .toMatch('This user email has already been used');
          $error.click();
          done();
        }, 100);
      });

      it('should allow subscription with good filling', function (done) {
        var login = 'mikey-' + new Date().getTime();
        fill($el.login, login);
        fill($el.password, '<PASSWORD>');
        fill($el.passwordConfirm, '<PASSWORD>');
        fill($el.email, '<EMAIL>');
        fill($el.color, '#00ff00');
        $el.lang.value = 'fr';
        $el.lang.onchange($el.lang);
        expect($el.form.checkValidity()).toBeTruthy();
        expect($el.login.checkValidity()).toBeTruthy();
        expect($el.password.checkValidity()).toBeTruthy();
        expect($el.email.checkValidity()).toBeTruthy();
        expect($el.color.checkValidity()).toBeTruthy();
        $el.submit.click();
        window.setTimeout(function () {
          expect(app.location.search).not.toBe(hash);
          var $success = app.document.querySelectorAll('body > section p');
          $success = $success[$success.length - 1];
          expect($success.textContent).toMatch('Successful subscription');
          $success.click();
          window.setTimeout(function () {
            expect(first('main h2').textContent).toMatch('Mes dossiers');
            first('header ul li').click(); // to english
            first('.glyphicon-off').parentNode.click();
            window.setTimeout(function () {
              expect(first('.glyphicon-lock')).toBeTruthy();
              first('body > section p').click();
              done();
            }, 200);
          }, 100);
        }, 300);
      });
    });
  };

  return subscribe;

}).call(this);


<|start_filename|>frontend/js/modules/passrecover.js<|end_filename|>
/**
*  vim:set sw=2 ts=2 sts=2 ft=javascript expandtab:
*
*  # Password recovery module
*
*  ## License
*
*  Licensed to the Apache Software Foundation (ASF) under one
*  or more contributor license agreements.  See the NOTICE file
*  distributed with this work for additional information
*  regarding copyright ownership.  The ASF licenses this file
*  to you 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.
*
*  ## Description
*
*  This module contains the password recovery process
*/

module.exports = (function () {
  // Global dependencies
  var m  = require('mithril');
  var ld = require('lodash');

  // Local dependencies
  var conf   = require('../configuration.js');
  var auth   = require('../auth.js');
  var form   = require('../helpers/form.js');
  var notif  = require('../widgets/notification.js');
  var layout = require('./layout.js');
  var user   = require('./user.js');

  var passrec = {};

  /**
  * ## Controller
  *
  * Used for module state and actions.
  */

  passrec.controller = function () {
    if (auth.isAuthenticated()) {
      m.route('/logout');
    }
    document.title = conf.LANG.USER.PASSRECOVER + ' - ' + conf.SERVER.title;

    var c = {
      token: m.route.param('token')
    };
    var fields = (c.token ? ['password', 'passwordConfirm'] : ['email']);
    form.initFields(c, fields);

    var errFn = function (err) {
      notif.error({ body: ld.result(conf.LANG, err.error) });
    };

    c.submit = function (e) {
      e.preventDefault();
      m.request({
        method: 'POST',
        url: conf.URLS.PASSRECOVER,
        data: c.data
      }).then(function () {
        notif.success({ body: conf.LANG.USER.INFO.PASSRECOVER_SUCCESS });
      }, errFn);
    };

    c.changePass = function (e) {
      e.preventDefault();
      m.request({
        method: 'PUT',
        url: conf.URLS.PASSRECOVER + '/' + c.token,
        data: c.data
      }).then(function () {
        m.route('/login');
        notif.success({ body: conf.LANG.USER.INFO.CHANGEPASS_SUCCESS });
      }, errFn);
    };

    return c;
  };

  /**
  * ## Views
  */

  var view = {};

  view.form = function (c) {
    var email = user.view.field.email(c);
    return m('form', {
      id: 'passrec-form',
      onsubmit: c.submit
      }, [
        m('fieldset', [
          m('.form-group', [
            email.label,
            email.input
          ])
        ]),
        m('input.btn.btn-success.pull-right.col-sm-12', {
          form: 'passrec-form',
          type: 'submit',
          value: conf.LANG.USER.OK
        })
      ]
    );
  };

  view.formChange = function (c) {
    var pass                = user.view.field.password(c);
    var passC               = user.view.field.passwordConfirm(c);
    pass.label.attrs.class  = 'col-sm-4';
    passC.label.attrs.class = 'col-sm-4';
    return m('form.form-horizontal', {
      id: 'passchange-form',
      onsubmit: c.changePass
      }, [
        m('fieldset', [
          m('.form-group', [
            pass.label,
            m('.col-sm-7', pass.input)
          ]),
          m('.form-group', [
            passC.label,
            m('.col-sm-7', passC.input)
          ])
        ]),
        m('input.btn.btn-success.pull-right', {
          form: 'passchange-form',
          type: 'submit',
          value: conf.LANG.USER.OK
        })
      ]
    );
  };

  view.main = function (c) {
    return m('section', { class: 'user' }, [
      m('h2', conf.LANG.USER.PASSRECOVER),
      (c.token ? view.formChange(c) : view.form(c))
    ]);
  };

  passrec.view = function (c) {
    return layout.view(view.main(c), user.view.aside.common(c));
  };


  return passrec;

}).call(this);


<|start_filename|>mockupserver.js<|end_filename|>
/**
*  vim:set sw=2 ts=2 sts=2 ft=javascript expandtab:
*
*  licensed to the apache software foundation (asf) under one
*  or more contributor license agreements.  see the notice file
*  distributed with this work for additional information
*  regarding copyright ownership.  the asf licenses this file
*  to you 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.
*
*  ## Description
*
*  Express server mockup for development purposes. It initializes all needed
*  stuff for MyPads, except Etherpad itself and creates a first user.
*/

(function () {
  'use strict';

  var hooks      = require('./hooks.js');
  var storage    = require('./storage.js');
  var api        = require('./api.js');
  var mail       = require('./mail.js');
  var user       = require('./model/user.js');
  var group      = require('./model/group.js');
  var pad        = require('./model/pad.js');
  var specCommon = require('./spec/backend/common.js');

  specCommon.mockupExpressServer();
  specCommon.reInitDatabase(function () {
    hooks.init(null, null, function () {
      storage.init(function () {
        user.set({
          login: 'frank',
          password: '<PASSWORD>',
          email: '<EMAIL>'
        }, function (err, frank) {
          if (err) { console.log(err); }
          user.set({
            login: 'parker',
            password: '<PASSWORD>',
            firstname: 'Parker',
            lastname: 'Lewis',
            email: '<EMAIL>'
          }, function (err, parker) {
            if (err) { console.log(err); }
            user.set({
              login: 'shelly',
              password: '<PASSWORD>',
              email: '<EMAIL>'
            }, function (err, shelly) {
              if (err) { console.log(err); }
              var opts = {
                crud: 'add',
                login: 'parker',
                name: 'enemies',
                uids: [ frank._id, shelly._id ]
              };
              user.userlist(opts, function (err) {
                if (err) { console.log(err); }
                var g = {
                  name: '<NAME>',
                  admin: parker._id,
                  tags: ['cool', 'weird']
                };
                group.set(g, function () {
                  g.name       = 'memories';
                  g.visibility = 'public';
                  g.tags       = ['cool', 'funky'];
                  group.set(g, function (err, g) {
                    if (err) { console.log(err); }
                    pad.set({ name: 'Loving Annie', group: g._id }, function() {
                      pad.set({ name: 'Watch sync', group: g._id }, function() {
                        g = {
                          name: 'shared notes',
                          admin: parker._id,
                          admins: [ frank._id ],
                          visibility: 'public'
                        };
                        group.set(g, function () {
                          api.init(specCommon.express.app, function () {
                            mail.init();
                            console.log('MockupServer runs on port 8042');
                          });
                        });
                      });
                    });
                  });
                });
              });
            });
          });
        });
      });
    });
  });

}).call(this);


<|start_filename|>spec/backend/configuration.spec.ldap.js<|end_filename|>
/**
*  vim:set sw=2 ts=2 sts=2 ft=javascript expandtab:
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements.  See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership.  The ASF licenses this file
* to you 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.
*/

(function () {
  'use strict';

  var ld = require('lodash');
  var specCommon = require('./common.js');
  var conf = require('../../configuration.js');

  describe('configuration', function () {
    beforeAll(specCommon.reInitDatabase);
    afterAll(specCommon.reInitDatabase);

    describe('init', function () {

      it('takes an optional callback as argument that must be a function',
        function () {
          expect(ld.partial(conf.init, 'string')).toThrow();
          expect(conf.init).not.toThrow();
        }
      );

      it('will call the callback, with an error or null when succeeded',
        function () {
          conf.init(function (err) {
            expect(err).toBeNull();
            var res = conf.get('passwordMax');
            expect(res).toBe(30);
          });
        }
      );

      it('will set the authMethod to \'ldap\' because of Etherpad\'s settings.json (ep_mypads.ldap)',
        function () {
          var res = conf.get('authMethod');
          expect(res).toBe('ldap');
        }
      );

      it('will fetch and set authLdapSettings from Etherpad\'s settings.json (ep_mypads.ldap)',
        function (done) {
          var res = conf.get('authLdapSettings');
          expect(res).toEqual({
            url:             'ldap://rroemhild-test-openldap',
            bindDN:          'cn=admin,dc=planetexpress,dc=com',
            bindCredentials: '<PASSWORD>',
            searchBase:      'ou=people,dc=planetexpress,dc=com',
            searchFilter:    '(uid={{username}})',
            properties: {
              login:     'uid',
              email:     'mail',
              firstname: 'givenName',
              lastname:  'sn'
            },
            defaultLang: 'fr'
          });
          done();
        }
      );
    });

    describe('get', function () {

      it('throws an error if key isn\'t a string', function () {
        expect(conf.get).toThrow();
        expect(ld.partial(conf.get, 1)).toThrow();
      });

      it('returns undefined if the field isn\'t defined', function () {
        var res = conf.get('inexistent');
        expect(res).toBeUndefined();
      });

      it('returns the value of the field otherwise', function () {
        var res = conf.get('passwordMin');
        expect(res).toBe(8);
      });
    });

    describe('set', function () {

      it('throws an error if key isn\'t a string, value is undefined, ' +
        'callback is not a function', function (done) {
          expect(conf.set).toThrow();
          expect(ld.partial(conf.set, 'key')).toThrow();
          expect(ld.partial(conf.set, 'key', 'value')).toThrow();
          expect(ld.partial(conf.set, 12, ld.noop)).toThrow();
          expect(ld.partial(conf.set, [], 12, ld.noop)).toThrow();
          expect(ld.partial(conf.set, 'key', 'notAFn')).toThrow();
          done();
      });

      it('sets a key for the conf with the given value', function (done) {
        conf.set('key', 'value', function () {
          var val = conf.get('key');
          expect(val).toBe('value');
          conf.set('@rray', [1, 2, 3], function () {
            val = conf.get('@rray');
            expect(val.length).toBe(3);
            done();
          });
        });
      });
    });

    describe('del', function () {

      it('throws an error if key isn\'t a string and callback not a function',
        function () {
          expect(conf.del).toThrow();
          expect(ld.partial(conf.del, 1)).toThrow();
          expect(ld.partial(conf.del, 1, 1)).toThrow();
          expect(ld.partial(conf.del, 1, ld.noop)).toThrow();
          expect(ld.partial(conf.del, 'key')).toThrow();
          expect(ld.partial(conf.del, 'key', 2)).toThrow();
      });

      it('removes the item otherwise', function (done) {
        conf.set('forremove', 10, function () {
          var res = conf.get('forremove');
          expect(res).toBe(10);
          conf.del('forremove', function (err) {
            expect(err).toBeUndefined();
            res = conf.get('forremove');
            expect(res).toBeUndefined();
            done();
          });
        });
      });
    });

    describe('all', function () {

      it('returns the configuration object', function (done) {
        conf.set('key', 10, function () {
          conf.set('power', 'max', function () {
            var settings = conf.all();
            expect(settings.key).toBe(10);
            expect(settings.power).toBe('max');
            done();
          });
        });
      });
    });

    describe('public', function () {

      it('returns the filtered configuration object', function (done) {
        conf.set('power', 'max', function () {
          var settings = conf.public();
          expect(settings.power).toBeUndefined();
          expect(settings.title).toBeDefined();
          expect(settings.passwordMin).toBeDefined();
          expect(settings.passwordMax).toBeDefined();
          expect(settings.languages).toBeDefined();
          expect(settings.HTMLExtraHead).toBeDefined();
          expect(settings.openRegistration).toBeDefined();
          expect(settings.hideHelpBlocks).toBeDefined();
          expect(settings.useFirstLastNameInPads).toBeDefined();
          done();
        });
      });
    });
  });

}).call(this);


<|start_filename|>spec/frontend/pad-move.spec.js<|end_filename|>
/**
*  vim:set sw=2 ts=2 sts=2 ft=javascript expandtab:
*
*  # Pad Moving functional testing
*
*  ## License
*
*  Licensed to the Apache Software Foundation (ASF) under one
*  or more contributor license agreements.  See the NOTICE file
*  distributed with this work for additional information
*  regarding copyright ownership.  The ASF licenses this file
*  to you 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 = (function () {
  'use strict';
  // Dependencies
  var common = require('./common.js');
  var fill = common.helper.fill;

  var move = {};

  // Shared variables
  var $el;
  var qfirst;
  var qall;

  move.run = function (app) {

    qfirst = function (sel) { return app.document.querySelector(sel); };
    qall = function (sel) { return app.document.querySelectorAll(sel); };

    describe('group view module testing', function () {

      beforeAll(function (done) {
        // Login and go to group view page
        qfirst('header nav a:first-child').click();
        window.setTimeout(function () {
          fill(qfirst('input[name=login]'), 'parker');
          fill(qfirst('input[name=password]'),
            '<PASSWORD>');
          qfirst('input[type=submit]').click();
          window.setTimeout(function () {
            qall('a[href$=view]')[4].click();
            window.setTimeout(function () {
              qfirst('a[href$="pad/move"]').click();
              window.setTimeout(function () {
                $el = {
                  form: qfirst('section.padmove form'),
                  select: qfirst('section.padmove select'),
                  submit: qfirst('section.padmove input[type=submit]')
                };
                qfirst('body > section > div p').click();
                window.setTimeout(done, 100);
              }, 200);
            }, 200);
          }, 200);
        }, 200);
      });

      afterAll(function (done) {
        qfirst('.icon-logout').parentNode.click();
        window.setTimeout(function () {
          qfirst('body > section p').click();
          done();
        }, 100);
      });

      it('should require selection', function () {
        $el.submit.click();
        expect($el.form.checkValidity()).toBeFalsy();
        expect($el.select.checkValidity()).toBeFalsy();
      });

      it('should move all pads to destination', function (done) {
        $el.select.value = qall('option')[1].value;
        $el.select.onchange($el.select);
        window.setTimeout(function () {
          $el.submit.click();
          window.setTimeout(function () {
            var $notif = qfirst('body > section p');
            expect($notif.innerHTML).toMatch('successfully moved');
            $notif.click();
            window.setTimeout(done, 100);
          }, 200);
        }, 200);
      });

    });

  };

  return move;

}).call(this);


<|start_filename|>spec/backend/model/common.spec.js<|end_filename|>
/**
*  Licensed to the Apache Software Foundation (ASF) under one
*  or more contributor license agreements.  See the NOTICE file
*  distributed with this work for additional information
*  regarding copyright ownership.  The ASF licenses this file
*  to you 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.
*/

(function () {
  'use strict';
  var ld = require('lodash');
  var storage = require('../../../storage');
  var specCommon = require('../common.js');
  var common = require('../../../model/common.js');

  describe('common', function () {
    beforeAll(specCommon.reInitDatabase);
    afterAll(specCommon.reInitDatabase);

    describe('addSetInit', function () {
      var addSetI = common.addSetInit;

      it('should throw TypeError if params is not an object or if callback ' +
        'is not a function', function () {
          expect(addSetI).toThrow();
          expect(ld.partial(addSetI, 'notAnObject')).toThrow();
          expect(ld.partial(addSetI, 123, ld.noop)).toThrow();
          expect(ld.partial(addSetI, { empty: false })).toThrow();
          expect(ld.partial(addSetI, { empty: false }, 'notAFunction'))
            .toThrow();
      });

      it('should throw TypeError if strFields is given and these fields are' +
        ' anything else than strings not empty', function () {
          var params = { a: false, obj: 123 };
          expect(ld.partial (addSetI, params, ld.noop, ['a', 'obj'])).toThrow();
          expect(ld.partial (addSetI, params, ld.noop, ['obj'])).toThrow();
          expect(ld.partial (addSetI, params, ld.noop, ['inexist'])).toThrow();
      });

      it('should retuns nothing otherwise', function () {
        expect(ld.partial(addSetI, { empty: false }, ld.noop)).not.toThrow();
        expect(addSetI({ empty: false }, ld.noop)).toBeUndefined();
      });
    });

    describe('hashPassword', function () {

      it('should hash any given string password', function (done) {
        common.hashPassword(null, 'pass', function (err, pass) {
          expect(err).toBeNull();
          expect(ld.isObject(pass)).toBeTruthy();
          expect(ld.isString(pass.hash)).toBeTruthy();
          expect(ld.isEmpty(pass.hash)).toBeFalsy();
          expect(ld.isString(pass.salt)).toBeTruthy();
          expect(ld.isEmpty(pass.salt)).toBeFalsy();
          done();
        });
      });
      it('should hash any given string password with given salt',
        function (done) {
          common.hashPassword('salt', 'pass', function (err, pass) {
            expect(err).toBeNull();
            expect(ld.isObject(pass)).toBeTruthy();
            expect(ld.isString(pass.hash)).toBeTruthy();
            expect(ld.isEmpty(pass.hash)).toBeFalsy();
            expect(ld.isString(pass.salt)).toBeTruthy();
            expect(ld.isEmpty(pass.salt)).toBeFalsy();
            var _pass = pass;
            common.hashPassword('salt', 'pass', function (err, pass) {
              expect(err).toBeNull();
              expect(ld.isObject(pass)).toBeTruthy();
              expect(pass.salt).toBe(_pass.salt);
              expect(pass.hash).toBe(_pass.hash);
              done();
            });
          });
        }
      );
    });

    describe('checkExistence', function () {
      var key = 'test:john';

      beforeAll(function (done) { storage.db.set(key, 'exists', done); });
      afterAll(function (done) { storage.db.remove(key, done); });

      it('should return null and true if the key exists', function (done) {
        common.checkExistence(key, function (err, res) {
          expect(err).toBeNull();
          expect(res).toBeTruthy();
          done();
        });
      });

      it('should return null and false if the key do not exist',
        function (done) {
          common.checkExistence('test:bob', function (err, res) {
            expect(err).toBeNull();
            expect(res).toBeFalsy();
            done();
          }
        );
      });
    });

    describe('checkMultiExist', function () {
      var kv = {
        'test:parker': 123,
        'test:jerry': ['a','c', 'e'],
        'test:mikey': 'a string'
      };

      beforeAll(function (done) { storage.fn.setKeys(kv, done); });
      afterAll(specCommon.reInitDatabase);

      it('should return null and true if all keys are found', function (done) {
        common.checkMultiExist(ld.keys(kv), function (err, res) {
          expect(err).toBeNull();
          expect(res).toBeTruthy();
          done();
        });
      });

      it('should return null and false if one or more keys are not found',
        function (done) {
          common.checkMultiExist(['test:parker', 'test:indexistent'],
            function (err, res) {
              expect(err).toBeNull();
              expect(res).toBeFalsy();
              done();
            }
          );
        }
      );
    });

    describe('getDel', function () {
      var key = 'test:john';

      beforeAll(function (done) { storage.db.set(key, 'exists', done); });
      afterAll(function (done) { storage.db.remove(key, done); });

      it('throws error if arguments are not given correctly', function () {
        expect(common.getDel).toThrow();
        expect(ld.partial(common.getDel, 123)).toThrow();
        expect(ld.partial(common.getDel, false, 'prefix')).toThrow();
        expect(ld.partial(common.getDel, false, 'prefix')).toThrow();
        expect(ld.partial(common.getDel, true, 'prefix', {})).toThrow();
        expect(ld.partial(common.getDel, true, 'prefix', 'key', {})).toThrow();
        expect(ld.partial(common.getDel, true, 'prefix', false, ld.noop))
          .toThrow();
      });

      it('should return an error through the callback if the key is not found',
        function (done) {
          common.getDel(false, 'prefix:', 'key', function (err, res) {
            expect(ld.isError(err)).toBeTruthy();
            expect(res).toBeUndefined();
            done();
          });
        }
      );

      it('should return null and the object otherwise, as in edit case',
        function (done) {
          common.getDel(false, 'test:', 'john', function (err, res) {
            expect(err).toBeNull();
            expect(res).toBe('exists');
            done();
          }
        );
      });

      it('should return null and the object otherwise, as in del case',
        function (done) {
          common.getDel(true, 'test:', 'john', function (err, res) {
            expect(err).toBeNull();
            expect(res).toBe('exists');
            done();
          }
        );
      });

    });

  });
}).call(this);


<|start_filename|>spec/frontend/pad-form.spec.js<|end_filename|>
/**
*  vim:set sw=2 ts=2 sts=2 ft=javascript expandtab:
*
*  # Pad form functional testing
*
*  ## License
*
*  Licensed to the Apache Software Foundation (ASF) under one
*  or more contributor license agreements.  See the NOTICE file
*  distributed with this work for additional information
*  regarding copyright ownership.  The ASF licenses this file
*  to you 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 = (function () {
  'use strict';
  // Dependencies
  var common = require('./common.js');
  var fill = common.helper.fill;

  var form = {};

  form.run = function (app) {
    // Shared variables
    var $el;
    var qfirst = function (sel) { return app.document.querySelector(sel); };
    var qall = function (sel) { return app.document.querySelectorAll(sel); };
    var populate = function () {
      return {
        form: qfirst('form#pad-form'),
        submit: qfirst('input.send[type=submit]'),
        name: qfirst('input[name=name]'),
        groupParams: qfirst('input[name=groupParams]')
      };
    };

    describe('pad form module testing', function () {

      beforeAll(function (done) {
        // Login and go to pad creation page
        app.document.querySelector('header nav a:first-child').click();
        window.setTimeout(function () {
          fill(qfirst('input[name=login]'), 'parker');
          fill(qfirst('input[name=password]'), '<PASSWORD>');
          qfirst('input[type=submit]').click();
          window.setTimeout(function () {
            qall('a[href$=view]')[4].click();
            window.setTimeout(function () {
              qfirst('section.pad a[href$=add]').click();
              window.setTimeout(function () {
                $el = populate();
                qfirst('body > section > div p').click();
                window.setTimeout(done, 100);
              }, 200);
            }, 200);
          }, 200);
        }, 200);
      });

      afterAll(function (done) {
        qfirst('.icon-logout').parentNode.click();
        window.setTimeout(function () {
          qfirst('body > section p').click();
          done();
        }, 100);
      });

      describe('pad creation', function () {

        it('should display properties as expected', function () {
          expect($el.name.value.length).toBe(0);
          expect($el.name.checkValidity()).toBeFalsy();
          expect($el.form.checkValidity()).toBeFalsy();
          expect($el.groupParams.checked).toBeTruthy();
        });

        it('should allow simple creation otherwise', function (done) {
          fill($el.name, 'Another one');
          expect($el.name.checkValidity()).toBeTruthy();
          expect($el.form.checkValidity()).toBeTruthy();
          $el.submit.click();
          window.setTimeout(function () {
            expect(qfirst('dl.group dd').textContent).toBe('3');
            expect(qall('section.pad ul li span.name')[2].textContent)
              .toBe('Another one');
            var $success = qfirst('body > section p');
            expect($success.parentNode.className).toMatch('success');
            $success.click();
            window.setTimeout(done, 100);
          }, 100);
        });

        it('should allow more complex creation', function (done) {
          qfirst('section.pad a[href$=add]').click();
          window.setTimeout(function () {
            $el = populate();
            $el.groupParams.checked = false;
            $el.groupParams.onchange($el.groupParams);
            window.setTimeout(function () {
              $el.visibility = qfirst('select[name=visibility]');
              $el.options = qall('select[name=visibility] option');
              $el.readonly = qfirst('input[name=readonly]');
              expect($el.visibility.value.length).toBe(0);
              expect($el.options.length).toBe(3);
              expect($el.readonly.checked).toBeFalsy();
              $el.visibility.value = 'private';
              $el.visibility.onchange($el.visibility);
              window.setTimeout(function () {
                $el.password = qfirst('input[type=password]');
                fill($el.name, 'A private one');
                fill($el.password, '<PASSWORD>');
                window.setTimeout(function () {
                  $el.submit.click();
                  window.setTimeout(function () {
                    expect(qfirst('dl.group dd').textContent).toBe('4');
                    expect(qall('section.pad ul li span.name')[3].textContent)
                      .toBe('A private one (Private)');
                    var $success = qfirst('body > section p');
                    expect($success.parentNode.className).toMatch('success');
                    $success.click();
                    window.setTimeout(done, 100);
                  }, 100);
                }, 100);
              }, 100);
            }, 100);
          }, 200);
        });

      });

      describe('pad edition', function () {

        beforeAll(function (done) {
          // Go to last pad edit
          qall('section.pad ul li span.actions a[title=Configuration]')[3]
            .click();
          window.setTimeout(function () {
            $el = populate();
            window.setTimeout(done, 100);
          }, 200);
        });

        it('should allow edition of existing pad', function (done) {
          expect($el.name.value).toBe('A private one');
          expect($el.groupParams.checked).toBeFalsy();
          fill($el.name, 'Enhanced one');
          $el.groupParams.checked = true;
          $el.groupParams.onchange($el.groupParams);
          window.setTimeout(function () {
            $el.submit.click();
            window.setTimeout(function () {
              expect(qfirst('dl.group dd').textContent).toBe('4');
              expect(qall('section.pad ul li span.name')[3].textContent)
              .toBe('Enhanced one');
              var $success = qfirst('body > section p');
              expect($success.parentNode.className).toMatch('success');
              $success.click();
              window.setTimeout(done, 100);
            }, 200);
          }, 100);
        });
      });
    });
  };

  return form;
}).call(this);


<|start_filename|>model/user-cache.js<|end_filename|>
/**
*  vim:set sw=2 ts=2 sts=2 ft=javascript expandtab:
*
*  # User Model
*
*  ## License
*
*  Licensed to the Apache Software Foundation (ASF) under one
*  or more contributor license agreements.  See the NOTICE file
*  distributed with this work for additional information
*  regarding copyright ownership.  The ASF licenses this file
*  to you 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 = (function () {
  'use strict';

  // Dependencies
  require('../helpers.js'); // Helpers auto-init
  var ld      = require('lodash');
  var conf    = require('../configuration.js');
  var storage = require('../storage.js');
  var UPREFIX = storage.DBPREFIX.USER;

  /**
  *  ## Description
  *
  * The `userCache` is the masterpiece of the MyPads plugin.
  *
  * It initially contains :
  *
  * - `logins`, an in memory object to map `_id` to `login` field and ensures
  *   uniqueness of logins
  * - `emails`, which have the same purpose for emails
  * - `firstname`, to ease search with firstname
  * - `lastname`, to ease search with lastname
  */

  var userCache = { logins: {}, emails: {}, firstname: {}, lastname: {}, userCacheReady: false };

  userCache.fn  = {};

  /**
  * ## Public Functions
  *
  * ### init
  *
  * `init` is a function that is called once at the initialization of mypads
  * and loops over all users to map their *login* to their *_id* and then
  * ensures uniqueness.
  *
  * It takes a callback function which is returned with *null* when finished.
  */

  userCache.init = function (callback) {
    storage.db.findKeys(UPREFIX + '*', null, function (err, keys) {
      if (err) { return callback(err); }
      // If you want to delay the user cache readyness (to test #141 for example),
      // put storage.fn.getKeys function below in
      // setTimeout(function() { }, 30000);
      // (NB: won't work with mockupserver since it waits for cache to be
      // ready before providing the web interface)
      storage.fn.getKeysUncached(keys, function (err, results) {
        if (results) {
          var memo = ld.reduce(results, function (memo, val, key) {
            if (val) {
              var k                  = key.replace(UPREFIX, '');
              memo.logins[val.login] = k;
              var email              = (conf.get('insensitiveMailMatch')) ? val.email.toLowerCase() : val.email;
              memo.emails[email]     = k;
              memo.firstname[k]      = val.firstname;
              memo.lastname[k]       = val.lastname;
            }
            return memo;
          }, { logins: {}, emails: {}, firstname: {}, lastname: {} });
          memo.userCacheReady = true;
          ld.assign(userCache, memo);
        }
        callback(null);
      });
    });
  };

  /**
  * ### getIdsFromLoginsOrEmails
  *
  * `getIdsFromLoginsOrEmails` is a private synchronous function that checks
  * if given data, users or admins logins or emails, are correct and transforms
  * it to expected values : unique identifiers, before saving it to database.
  *
  * It takes an array of users `logins` and `emails`.
  * It returns an object with :
  *
  * - `uids` for found users
  * - `present` users logins or emails
  * - `absent` users logins or emails
  */

  userCache.fn.getIdsFromLoginsOrEmails = function (loginsMails) {
    if (!ld.isArray(loginsMails)) {
      throw new TypeError('BACKEND.ERROR.TYPE.LOGINS_ARR');
    }
    return ld.reduce(loginsMails, function (memo, lm) {
      var email = (conf.get('insensitiveMailMatch')) ? lm.toLowerCase() : lm;
      var uid   = userCache.logins[lm] || userCache.emails[email];
      if (uid) {
        memo.uids.push(uid);
        memo.present.push(lm);
      } else {
        memo.absent.push(lm);
      }
      return memo;
    }, { uids: [], present: [], absent: [] });
  };

  /**
  * ### searchUserInfos
  *
  * `searchUserInfos` is a private synchronous function that search a user
  * in the cache.
  *
  * The search string can be a login, an email, a first or a last name.
  *
  * It returns a hash table with the login of the user as a key, and the value
  * is a hash table containing the email, first name and last name.
  *
  * exemple: {
  *   foo: {
  *     email: <EMAIL>,
  *     firstname: Foo,
  *     lastname: Bar
  *   }
  * }
  */
  userCache.fn.searchUserInfos = function (search) {
    search = search.toLowerCase();

    var emails  = ld.reduce(userCache.emails, function (result, n, key) {
      result[n] = key;
      return result;
    }, {});
    var users = ld.reduce(userCache.logins, function (result, n, key) {
      if (key.toLowerCase()                    === search  ||
         (typeof(emails[n]) !== 'undefined' &&
          emails[n].toLowerCase()              === search) ||
          userCache.lastname[n].toLowerCase()  === search  ||
          userCache.firstname[n].toLowerCase() === search) {
        result[key] = {
          email: emails[n],
          firstname: userCache.firstname[n],
          lastname: userCache.lastname[n]
        };
      }
      return result;
    }, {});
    return users;
  };

  return userCache;

}).call(this);


<|start_filename|>spec/frontend/profile.spec.js<|end_filename|>
/**
*  vim:set sw=2 ts=2 sts=2 ft=javascript expandtab:
*
*  # Profile functional testing
*
*  ## License
*
*  Licensed to the Apache Software Foundation (ASF) under one
*  or more contributor license agreements.  See the NOTICE file
*  distributed with this work for additional information
*  regarding copyright ownership.  The ASF licenses this file
*  to you 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 = (function () {
  'use strict';
  // Dependencies
  var ld = require('lodash');
  var common = require('./common.js');
  var fill = common.helper.fill;

  var profile = {};

  // Shared variables
  var first;
  var $el;
  var hash;

  profile.run = function (app) {

    describe('profile module testing', function () {

      beforeAll(function (done) {
        // Go to login page
        app.document.querySelector('header nav a:first-child').click();
        window.setTimeout(function () {
          fill(app.document.querySelector('input[name=login]'), 'parker');
          fill(app.document.querySelector('input[name=password]'),
            '<PASSWORD>');
          app.document.querySelector('input[type=submit]').click();
          window.setTimeout(function () {
            common.user.beforeAll(app, 'a[href$=myprofile]',
              function (res) {
                $el = res.$el; first = res.first; hash = res.hash;
                $el.color.type = 'text';
                var uLCP = first('input[name=useLoginAndColorInPads]');
                ld.assign($el, {
                    passwordCurrent: first('input[name=passwordCurrent]'),
                    useLoginAndColorInPads: uLCP,
                    removeAccount: first('button.btn-danger')
                });
                app.document.querySelector('body > section p').click();
                window.setTimeout(done, 100);
              }
            );
          }, 200);
        }, 200);
      });

      afterAll(function (done) {
        first('a[href$=logout]').click();
        window.setTimeout(function () {
          first('body > section p').click();
          done();
        }, 100);
      });

      it('should displays login name into the page title', function () {
        expect(first('section.user h2').textContent).toMatch('parker');
      });

      it('should forbid profile update with empty current password',
        function (done) {
          $el.submit.click();
          expect($el.email.checkValidity()).toBeTruthy();
          expect($el.form.checkValidity()).toBeFalsy();
          expect($el.passwordCurrent.checkValidity()).toBeFalsy();
          done();
        }
      );

      it('should return an error if password is bad', function (done) {
        fill($el.passwordCurrent, '<PASSWORD>');
        expect($el.form.checkValidity()).toBeTruthy();
        $el.submit.click();
        window.setTimeout(function () {
          var $err = first('body > section p');
          expect($err.innerHTML).toMatch('Password is not correct');
          $err.click();
          done();
        }, 100);
      });

      it('should accept updates otherwise, for password only if two new match',
        function (done) {
          fill($el.passwordCurrent, '<PASSWORD>');
          fill($el.password, '<PASSWORD>');
          fill($el.passwordConfirm, '<PASSWORD>!!!!');
          fill($el.organization, 'Santo Domingo High');
          fill($el.color, '#ff0000');
          $el.useLoginAndColorInPads.checked = false;
          $el.useLoginAndColorInPads.onchange($el.useLoginAndColorInPads);
          expect($el.form.checkValidity()).toBeTruthy();
          $el.submit.click();
          window.setTimeout(function () {
            expect($el.organization.value).toBe('Santo Domingo High');
            var $notif = first('body > section p');
            expect($notif.innerHTML).toMatch('Profile successfully updated');
            $notif.click();
            window.setTimeout(done, 100);
          }, 200);
        }
      );

      it('should cancel an account removal if canceled or bad password',
        function (done) {
          app.window.prompt = function () { return 'badPassword'; };
          $el.removeAccount.click();
          window.setTimeout(function () {
            var $err = first('body > section p');
            expect($err.innerHTML).toMatch('Password is not correct');
            $err.click();
            done();
          }, 100);
        }
      );

    });

  };

  return profile;
}).call(this);


<|start_filename|>templates/mail_fr.json<|end_filename|>
{
   "ACCOUNT_CONFIRMATION": "<%= login %>, vous vous êtes inscrit sur <%= title %>. Cette instance etherpad nécessite une confirmation de votre adresse mail afin d’activer votre compte. Pour ce faire, merci de vous rendre sur l’adresse Web suivante : <%= url %>. Cette adresse ne sera valide que pendant <%= duration %> minutes. Après cela, vous devrez contacter un administrateur.",
   "ACCOUNT_CONFIRMATION_SUBJECT": "Confirmation de votre compte <%= title %>",
   "PASSRECOVER": "<%= login %>, une demande de changement de mot de passe a été faite sur <%= title %> concernant votre identifiant. Si vous souhaitez confirmer ce changement de mot de passe, merci de vous rendre à l’adresse suivante : <%= url %> . Si cette requête n’émane pas de vous, ignorez simplement ce message. L’adresse deviendra invalide dans <%= duration %> minutes.",
   "PASSRECOVER_SUBJECT": "Changement de mot de passe sur <%= title %>"
}


<|start_filename|>spec/backend/common.js<|end_filename|>
/**
*  vim:set sw=2 ts=2 sts=2 ft=javascript expandtab:
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements.  See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership.  The ASF licenses this file
* to you 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 = (function () {
  'use strict';
  var ld = require('lodash');
  var storage = require('../../storage.js');
  var common = {};

  /**
  * `reInitDatabase` is a private function aiming to remove the test database
  * file and lets it virgin comme previsous testing before and after all module
  * tests. It also initializes secondary in-memory index.
  */

  common.reInitDatabase = function (done) {
    var unlink = require('fs').unlink;
    var db = require('../../storage.js').db;
    db.close(function () {
      unlink('./test.db', function () {
        db.init(ld.partial(storage.init, done));
      });
    });
  };

  /**
  * `mockupExpressServer` is a private function that launch an Express Server,
  * for local testing. It uses an intern namespace : `common.express`.
  */

  common.mockupExpressServer = function () {
    var express = require('express');
    common.express = {};
    common.express.app = express();
    common.express.server = common.express.app.listen(8042);
  };

  /**
  * `unmockExpressServer` closes the server launched with `mockupExpressServer`.
  */

  common.unmockExpressServer = function() { common.express.server.close(); };

  return common;
}).call(this);


<|start_filename|>spec/frontend/admin.spec.js<|end_filename|>
/**
*  vim:set sw=2 ts=2 sts=2 ft=javascript expandtab:
*
*  # Admin functional testing
*
*  ## License
*
*  Licensed to the Apache Software Foundation (ASF) under one
*  or more contributor license agreements.  See the NOTICE file
*  distributed with this work for additional information
*  regarding copyright ownership.  The ASF licenses this file
*  to you 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 = (function () {
  'use strict';
  // Dependencies
  var common = require('./common.js');
  var fill = common.helper.fill;

  var admin = {};

  admin.run = function (app) {
    // Shared variables
    var $el;
    var qfirst = function (sel) { return app.document.querySelector(sel); };

    describe('admin module testing', function () {

      beforeAll(function (done) {
        // Go to admin page
        app.document.querySelector('footer a[href$=admin]').click();
        window.setTimeout(done, 200);
      });

      describe('login testing', function () {

        beforeEach(function (done) {
          $el = {
            form: qfirst('form'),
            login: qfirst('input[name=login]'),
            password: qfirst('input[name=password]'),
            submit: qfirst('input[type=submit]')
          };
          window.setTimeout(done, 100);
        });

        it('should forbid submision whith no login/pass fill', function (done) {
          $el.submit.click();
          expect($el.login.checkValidity()).toBeFalsy();
          expect($el.password.checkValidity()).toBeFalsy();
          expect($el.form.checkValidity()).toBeFalsy();
          fill($el.login,'a login');
          expect($el.login.checkValidity()).toBeTruthy();
          expect($el.form.checkValidity()).toBeFalsy();
          $el.submit.click();
          done();
        });


        it('should allow submission with conform login and password',
          function (done) {
            fill($el.login, 'mikey');
            fill($el.password, '<PASSWORD>');
            expect($el.form.checkValidity()).toBeTruthy();
            done();
          }
        );

        it('should login if user and pass are ok', function (done) {
          fill($el.login, 'grace');
          fill($el.password, '<PASSWORD>');
          expect($el.form.checkValidity()).toBeTruthy();
          $el.submit.click();
          window.setTimeout(function () {
            var $success = qfirst('body > section p');
            expect($success.parentNode.className).toMatch('success');
            $success.click();
            window.setTimeout(done, 100);
          }, 200);
        });

      });

      describe('settings testing', function () {

        beforeAll(function (done) {
          $el = {
            form: qfirst('form'),
            title: qfirst('input[name=title]'),
            rootUrl: qfirst('input[name=rootUrl]'),
            lang: qfirst('select[name=defaultLanguage]'),
            passwordMin: qfirst('input[name=passwordMin]'),
            passwordMax: qfirst('input[name=passwordMax]'),
            allowEtherPads: qfirst('input[name=allowEtherPads]'),
            checkMails: qfirst('input[name=checkMails]'),
            tokenDuration: qfirst('input[name=tokenDuration]'),
            SMTPHost: qfirst('input[name=SMTPHost]'),
            SMTPPort: qfirst('input[name=SMTPPort]'),
            SMTPSSL: qfirst('input[name=SMTPSSL]'),
            SMTPTLS: qfirst('input[name=SMTPTLS]'),
            SMTPUser: qfirst('input[name=SMTPUser]'),
            SMTPPass: qfirst('input[name=SMTPPass]'),
            SMTPEmailFrom: qfirst('input[name=SMTPEmailFrom]'),
            submit: qfirst('input[type=submit]')
          };
          window.setTimeout(done, 100);
        });

        it('should display default values and be valid', function () {
          expect($el.title.value).toBe('MyPads');
          expect($el.title.checkValidity()).toBeTruthy();
          expect($el.rootUrl.value).toBe('http://localhost:8042');
          expect($el.title.checkValidity()).toBeTruthy();
          expect($el.lang.value).toBe('en');
          expect($el.lang.checkValidity()).toBeTruthy();
          expect($el.passwordMin.value).toBe('8');
          expect($el.passwordMin.checkValidity()).toBeTruthy();
          expect($el.passwordMax.value).toBe('30');
          expect($el.passwordMax.checkValidity()).toBeTruthy();
          expect($el.allowEtherPads.value).toBe('true');
          expect($el.checkMails.value.length).toBe(0);
          expect($el.tokenDuration.value).toBe('60');
          expect($el.SMTPHost.value.length).toBe(0);
          expect($el.SMTPPort.value.length).toBe(0);
          expect($el.SMTPSSL.value.length).toBe(0);
          expect($el.SMTPTLS.value).toBe('true');
          expect($el.SMTPUser.value.length).toBe(0);
          expect($el.SMTPPass.value.length).toBe(0);
          expect($el.SMTPEmailFrom.value.length).toBe(0);
          expect($el.form.checkValidity()).toBeTruthy();
        });

        it('should forbid submission whith bad input', function (done) {
          fill($el.title, '');
          expect($el.title.checkValidity()).toBeFalsy();
          fill($el.passwordMin, '');
          expect($el.passwordMin.checkValidity()).toBeFalsy();
          fill($el.passwordMax, '0');
          expect($el.passwordMax.checkValidity()).toBeFalsy();
          fill($el.SMTPEmailFrom, 'notanemail');
          expect($el.SMTPEmailFrom.checkValidity()).toBeFalsy();
          fill($el.SMTPEmailFrom, '');
          $el.submit.click();
          expect($el.form.checkValidity()).toBeFalsy();
          window.setTimeout(done, 100);
        });

        it('should warn if password sizes are incorrect', function (done) {
          fill($el.title, 'Title');
          expect($el.title.checkValidity()).toBeTruthy();
          $el.lang.value = 'fr';
          $el.lang.onchange($el.lang);
          expect($el.lang.checkValidity()).toBeTruthy();
          fill($el.passwordMin, '12');
          expect($el.passwordMin.checkValidity()).toBeTruthy();
          fill($el.passwordMax, '8');
          expect($el.passwordMax.checkValidity()).toBeTruthy();
          $el.submit.click();
          expect($el.form.checkValidity()).toBeTruthy();
          window.setTimeout(function () {
            var $warn = qfirst('body > section p');
            expect($warn.parentNode.className).toMatch('warning');
            $warn.click();
            window.setTimeout(done, 100);
          }, 200);
        });

        it('should update configuration otherwise', function (done) {
          fill($el.passwordMin, '6');
          fill($el.passwordMax, '32');
          $el.submit.click();
          window.setTimeout(function () {
            var $success = qfirst('body > section p');
            expect($success.parentNode.className).toMatch('success');
            $success.click();
            expect(qfirst('header div h1').textContent).toBe($el.title.value);
            window.setTimeout(done, 100);
          }, 300);
        });

      });

      describe('admin logout testing', function () {

        it('should logout if already admin', function (done) {
          qfirst('header nav a[href$="admin/logout"]').click();
          window.setTimeout(function () {
            var $success = qfirst('body > section p');
            expect($success.parentNode.className).toMatch('success');
            $success.click();
            window.setTimeout(done, 100);
          }, 200);
        });

      });

    });

  };

  return admin;

}).call(this);


<|start_filename|>templates/mail_it.json<|end_filename|>
{
    "ACCOUNT_CONFIRMATION": "<%= login %>, ti sei registrato su <%= title %>. Questa istanza etherpad richiede una conferma email per attivare il tuo account. Per procedere, vai a questo indirizzo web: <%= url %>. Questa url sarà valida solo per <%= duration %> minuti. Dopo, dovrai contattare un amministratore.",
    "ACCOUNT_CONFIRMATION_SUBJECT": "Conferma dell'account su <%= title %>",
    "PASSRECOVER": "<%= login %>, una richiesta di cambio password su <%= title %> è stata iniziata per il tuo login. Se vuoi confermare la modifica della tua password, vai a questo indirizzo web : <%= url %> . Se questa richiesta non proviene da te, ignora semplicemente questa mail. L'indirizzo non sarà più valido tra <%= duration %> minuti.",
    "PASSRECOVER_SUBJECT": "Recupero password su <%= title %>"
}


<|start_filename|>frontend/js/modules/pad-share.js<|end_filename|>
/**
*  vim:set sw=2 ts=2 sts=2 ft=javascript expandtab:
*
*  # Pad and group sharing module
*
*  ## License
*
*  Licensed to the Apache Software Foundation (ASF) under one
*  or more contributor license agreements.  See the NOTICE file
*  distributed with this work for additional information
*  regarding copyright ownership.  The ASF licenses this file
*  to you 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.
*
*  ## Description
*
*  Short module for pad and group sharing
*/

module.exports = (function () {
  'use strict';
  // Dependencies
  var conf = require('../configuration.js');

  /**
  * ## Main function
  *
  * Used to give the MyPads URL
  */
  return function (gid, pid) {
    var l      = window.location;
    var url    = l.protocol + '//' + l.host + l.pathname + '?/mypads/group/' + gid;
    var suffix = (pid ? '/pad/view/' + pid : '/view');
    window.prompt(conf.LANG.GROUP.SHARE_LINK, url + suffix);
  };
}).call(this);


<|start_filename|>scripts/mypads-jobqueue-minion.js<|end_filename|>
#! /usr/bin/env node
// vim:set sw=2 ts=2 sts=2 ft=javascript expandtab:
'use strict';

var program = require('commander'),
    fs      = require('fs'),
    path    = require('path');

var ueberDB      = require('ueberdb2'),
    jsonminify   = require('jsonminify'),
    forever      = require('async/forever'),
    eachSeries   = require('async/eachSeries'),
    eachOfSeries = require('async/eachOfSeries');

var JOBQ_PREFIX = require('../storage').DBPREFIX.JOBQ;

// Arguments handling
program
  .version('0.1.0')
  .option('-s, --settings <file>', '[MANDATORY] the path to your Etherpad\'s settings.json file')
  .option('-q, --quiet', '[OPTIONAL] low verbosity')
  .option('-o, --oneshot', '[OPTIONAL] don\'t start a loop, process the jobs and exit')
  .parse(process.argv);

// Check that we have the mandatory arguments
if (!program.settings) {
  console.log('');
  console.log('=====================================================================');
  console.log('  You must provide the path to your Etherpad\'s settings.json file!');
  console.log('=====================================================================');
  program.help();
}

// Try to parse the settings
var settingsFilename = path.resolve(program.settings);
var settingsStr      = fs.readFileSync(settingsFilename).toString();
var settings;
try {
  if(settingsStr) {
    settingsStr = jsonminify(settingsStr).replace(',]',']').replace(',}','}');
    settings    = JSON.parse(settingsStr);
  }
} catch(e) {
  console.error('There was an error processing your settings.json file: '+e.message);
  process.exit(1);
}

// Open the database
var db = new ueberDB.database(settings.dbType, settings.dbSettings, {cache: 0, writeInterval: 0});

/*
 * Functions
 */
function exitIfErr(err) {
  console.log('=====================');
  console.error(err);
  return db.doShutdown(function() { process.exit(1); });
}

// Remove record from DB
function removeRecord(key, callback) {
  db.remove(key, function(err) {
    if (err) { return exitIfErr(err); }
    callback();
  });
}

function deletePads(nextLoop) {
  if (!program.quiet) { console.log('Entering loop'); }

  db.findKeys(JOBQ_PREFIX+'deletePad:*', null, function(err, pads) {
    if (err) { return exitIfErr(err); }

    if (pads !== null && pads.length > 0) {
      if (!program.quiet) { console.log('Pads to delete: '+pads); }

      eachOfSeries(pads, function(padId, index, next) {
        padId = padId.substring(26); // remove "mypads:jobqueue:deletePad:"

        if (!program.quiet) { console.log('Starting deletion of '+padId); }

        // Cascade deletion process
        // 0. Get the readonly name of the pad
        db.get('pad2readonly:'+padId, function(err, readonly2pad) {
          if (err) { return exitIfErr(err); }

          // 1. Delete the readonly2pad record
          db.remove('readonly2pad:'+readonly2pad, function(err) {
            if (err) { return exitIfErr(err); }

            // 2. Delete the pad2readonly:padId record
            db.remove('pad2readonly:'+padId, function(err) {
              if (err) { return exitIfErr(err); }

              // 3. The pad:padId record has already been deleted through PadManager.removePad
              // called from MyPads

              // 4. Check for revs records
              db.findKeys('pad:'+padId+':revs:*', null, function(err, keys) {
                if (err) { return exitIfErr(err); }

                // 5. Delete the revs records
                eachSeries(keys, removeRecord, function(err) {
                  if (err) { return exitIfErr(err); }

                  // 6. Check for chat records
                  db.findKeys('pad:'+padId+':chat:*', null, function(err, keys) {
                    if (err) { return exitIfErr(err); }

                    // 7. Delete the chat records
                    eachSeries(keys, removeRecord, function(err) {
                      if (err) { return exitIfErr(err); }

                      // 8. Remove the job
                      db.remove('mypads:jobqueue:deletePad:'+padId, function(err) {
                        if (err) { return exitIfErr(err); }

                        if (!program.quiet) { console.log('End deletion of '+padId); }
                        return next();
                      });
                    });
                  });
                });
              });
            });
          });
        });
      }, function(err) {
        if (err) { return exitIfErr(err); }

        if (program.oneshot) {
          return db.doShutdown(function() { process.exit(0); });
        } else {
          nextLoop();
        }
      });
    } else {
      if (!program.quiet) { console.log('No pad to process'); }

      if (program.oneshot) {
        return db.doShutdown(function() { process.exit(0); });
      } else {
        setTimeout(nextLoop, 1000);
      }
    }
  });
}

/*
 * Start loop
 */
db.init(function(err) {
  if (err) { return exitIfErr(err); }
  if (program.oneshot) {
    deletePads();
  } else {
    forever(deletePads, exitIfErr);
  }
});


<|start_filename|>frontend.js<|end_filename|>
/**
*  vim:set sw=2 ts=2 sts=2 ft=javascript expandtab:
*
*  # Frontend Entry Point
*
*  ## License
*
*  Licensed to the Apache Software Foundation (ASF) under one
*  or more contributor license agreements.  See the NOTICE file
*  distributed with this work for additional information
*  regarding copyright ownership.  The ASF licenses this file
*  to you 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.
*
*  ## Description
*
*  This module is the entry point for front JavaScript programming. It requires
*  global dependencies and main client-side file.
*/

(function () {
  var conf  = require('./frontend/js/configuration.js');
  var route = require('./frontend/js/route.js');
  conf.init(route.init);
}).call(this);


<|start_filename|>static/css/mypads-hide-help-blocks.css<|end_filename|>
body.hidehelpblocks aside section.user-aside {
  display: none;
}
body.hidehelpblocks main.ombre > section.col-md-9:not(.groupList) {
  width: 100% !important;
}


<|start_filename|>Makefile<|end_filename|>
build:
	npm run frontend:build

watch:
	npm run frontend:watch

mockup:
	npm run mockupserver

lint:
	npm run lint

test:
	npm run test
	npm run test-ldap


<|start_filename|>frontend/js/helpers/ready.js<|end_filename|>
/**
*  vim:set sw=2 ts=2 sts=2 ft=javascript expandtab:
*
*  # Ready helpers functions
*
*  ## License
*
*  Licensed to the Apache Software Foundation (ASF) under one
*  or more contributor license agreements.  See the NOTICE file
*  distributed with this work for additional information
*  regarding copyright ownership.  The ASF licenses this file
*  to you 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.
*
*  ## Description
*
*  This module contains function to check if the user cache is populated
*/

module.exports = (function () {
  // Dependencies
  var m    = require('mithril');
  var conf = require('../configuration.js');

  var ready = {};

  /**
  * `checkLoop` will poll the server to know if the user cache is fully loaded
  * It will call ready.inFrame too.
  */

  ready.checkLoop = function(element, isInitialized) {
    if (isInitialized) { return; }

    // Are we in an iFrame?
    ready.inFrame(element, isInitialized);

    // Is the user cache ready?
    m.request({
      method: 'GET',
      url: conf.URLS.CACHECHECK
    }).then(function (resp) {
      if (resp.userCacheReady) {
        var toHide = document.getElementById('hide-when-ready').classList;
        var toShow = document.querySelectorAll('.show-when-ready');
        toHide.add('hidden');
        // Hack due to browsers that does not support forEach on nodeList
        [].forEach.call(toShow, function(element) {
          element.classList.remove('hidden');
        });
        return 'user cache ready';
      } else {
        setTimeout(function() {
          return ready.checkLoop();
        }, 500);
      }
    }, function (err) {
      console.log('user cache check loop:' + err);
      var emsg = conf.LANG.BACKEND.CONFIGURATION.LANG_PROBLEM +
        ' (' + err + ')';
      var notifErr = require('../widgets/notification.js').error;
      notifErr({ body: emsg });
      if (err) {
        setTimeout(function() {
          return ready.checkLoop();
        }, 500);
      }
    });
  };

  /**
  * `inFrame` checks if we are in a MyPads iFrame and reload the parent window
  * if it's the case.
  *
  * Example: you're using a pad through a MyPads iFrame, the server crashes, you
  * need to relog, but this prevents the login page to show inside the iFrame.
  */
  var rgx = new RegExp('/mypads');
  ready.inFrame = function(element, isInitialized) {
    if (isInitialized) { return; }

    var windowMode     = location.href;
    window.displaymode = windowMode;
    //detect if we are in a iframe
    if (window.parent.location.href.match(rgx) &&
        (typeof(window.parent.displaymode) === 'undefined' ||
         window.parent.displaymode !== windowMode)
    ) {
      window.parent.location.reload();
    }
  };

  return ready;

}).call(this);


<|start_filename|>static/css/mypads.css<|end_filename|>
/* External Dependencies */
@import url('vendor/bootstrap.min.css');
/* Internal Dependencies */
@import url('layout.css');
/* Widgets */
@import url('widgets/notification.css');
@import url('widgets/tooltip.css');
@import url('widgets/dropdown.css');


<|start_filename|>spec/backend/model/group.spec.js<|end_filename|>
/**
*  Licensed to the Apache Software Foundation (ASF) under one
*  or more contributor license agreements.  See the NOTICE file
*  distributed with this work for additional information
*  regarding copyright ownership.  The ASF licenses this file
*  to you 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.
*/

(function () {
  'use strict';
  var ld = require('lodash');
  var specCommon = require('../common.js');
  var user = require('../../../model/user.js');
  var pad = require('../../../model/pad.js');
  var group = require('../../../model/group.js');
  var storage = require('../../../storage.js');

  var gparams;
  var gadm;
  var gusers;
  var gpads;


  var initGroupUsersAndPads = function (done) {
    gadm = {
      login: 'parker',
      password: '<PASSWORD>',
      firstname: 'Parker',
      lastname: 'Lewis',
      email: '<EMAIL>',
      groups: []
    };
    gusers = [];
    gpads = [];
    var users = ld.map(['frank', 'grace','shelly', 'mikey', 'jerry'],
      function (val) { return {
        login: val,
        password: '<PASSWORD>',
        email: val + '@santodomingo.edu'
      }; });
    specCommon.reInitDatabase(function () {
      user.set(gadm, function (err, u) {
        if (err) { console.log(err); }
        gadm = u;
        var setUsers = function () {
          if (users.length) {
            user.set(users.pop(), function (err, u) {
              if (err) { console.log(err); }
              gusers.push(u._id);
              setUsers();
            });
          } else {
            gusers.push(gadm._id);
            gparams = {
              name: 'college',
              admin: gadm._id,
              visibility: 'private',
              password: '<PASSWORD>',
              readonly: true
            };
            group.set(gparams, function (err, g) {
              if (err) { console.log(err); }
              gparams = g;
              gparams.admin = gadm._id;
              gparams.admins = ld.takeRight(gusers, 2);
              gparams.users = ld.take(gusers, 3);
              gparams.password = '<PASSWORD>';
              var pads = ld.map(['pad1', 'pad2', 'pad3'], function (val) {
                return { name: val, group: gparams._id };
              });
              var setPads = function () {
                if (pads.length) {
                  pad.set(pads.pop(), function (err, p) {
                    if (err) { console.log(err); }
                    gpads.push(p._id);
                    setPads();
                  });
                } else {
                  group.set(gparams, function (err, g) {
                    if (err) { console.log(err); }
                    gparams = g;
                    done();
                  });
                }
              };
              setPads();
            });
          }
        };
        setUsers();
      });
    });
  };

  describe('group', function () {
    beforeAll(specCommon.reInitDatabase);
    afterAll(specCommon.reInitDatabase);

    describe('addition using set', function () {
      beforeEach(initGroupUsersAndPads);
      afterEach(specCommon.reInitDatabase);

      it('should throws errors if params.name or params.admin, callback or ' +
        'edit aren`t correct', function () {
          expect(group.set).toThrow();
          expect(ld.partial(group.set, [])).toThrow();
          var params = { name: 123, noadmin: true };
          expect(ld.partial(group.set, params, ld.noop)).toThrow();
          params = { name: 'ok', admin: false };
          expect(ld.partial(group.set, params, ld.noop)).toThrow();
          params.name = [];
          expect(ld.partial(group.set, params, ld.noop)).toThrow();
          params.name = 'ok';
          params.admin = 'ok';
          expect(ld.partial(group.set, params, false)).toThrow();
        }
      );

      it('should return an error if admin user is not found', function (done) {
        group.set({ name: 'g', admin: 'inexistent' },
          function (err, g) {
            expect(ld.isError(err)).toBeTruthy();
            expect(err).toMatch('ITEMS_NOT_FOUND');
            expect(g).toBeUndefined();
            done();
          }
        );
      });

      it('should assign defaults if other params are not properly typed nor' +
        'defined', function (done) {
          var params = { name: 'group', admin: gadm._id };
          group.set(params, function (err, g) {
            expect(err).toBeNull();
            expect(g.name).toBe('group');
            expect(ld.isString(g.description) && ld.isEmpty(g.description))
              .toBeTruthy();
            expect(ld.isArray(g.admins)).toBeTruthy();
            expect(ld.isArray(g.users) && ld.isEmpty(g.users)).toBeTruthy();
            expect(ld.isArray(g.pads) && ld.isEmpty(g.pads)).toBeTruthy();
            expect(ld.first(g.admins)).toBe(gadm._id);
            expect(g.visibility).toBe('restricted');
            expect(g.password).toBeNull();
            expect(g.readonly).toBeFalsy();
            expect(ld.isArray(g.tags)).toBeTruthy();

            params = {
              name: 'college',
              admin: gadm._id,
              admins: [123],
              users: {},
              pads: false,
              visibility: 'inexistentOption',
              password: [],
              readonly: 'needABoolean',
              tags: 123
            };
            group.set(params, function (err, g) {
              expect(err).toBeNull();
              expect(ld.isString(g._id)).toBeTruthy();
              expect(ld.isNumber(g.ctime)).toBeTruthy();
              expect(ld.startsWith(g._id, 'college-')).toBeTruthy();
              expect(g._id).not.toBe('will not be given');
              expect(g.name).toBe('college');
              expect(ld.isArray(g.admins)).toBeTruthy();
              expect(ld.first(g.admins)).toBe(gadm._id);
              expect(ld.size(g.admins)).toBe(1);
              expect(ld.isArray(g.users) && ld.isEmpty(g.users)).toBeTruthy();
              expect(ld.isArray(g.pads) && ld.isEmpty(g.pads)).toBeTruthy();
              expect(g.visibility).toBe('restricted');
              expect(ld.isEmpty(g.password)).toBeTruthy();
              expect(g.readonly).toBeFalsy();
              expect(ld.isArray(g.tags)).toBeTruthy();
              done();
            });
          });
        }
      );

      it('should otherwise accept well defined parameters', function (done) {
        var params = {
          name: 'college2',
          description: 'A strange place',
          admin: gadm._id,
          admins: ld.take(gusers, 2),
          users: ld.takeRight(gusers, 3),
          visibility: 'private',
          password: '<PASSWORD>',
          readonly: true,
          tags: ['80s', 'coolness']
        };
        group.set(params, function (err, g) {
          expect(err).toBeNull();
          expect(ld.isString(g._id)).toBeTruthy();
          expect(ld.startsWith(g._id, 'college2-')).toBeTruthy();
          expect(g.name).toBe('college2');
          expect(g.description).toBe('A strange place');
          expect(ld.isArray(g.admins)).toBeTruthy();
          expect(ld.first(g.admins)).toBe(gadm._id);
          var contained = (ld.size(ld.intersection(g.admins, gusers)) ===
            ld.size(g.admins));
          expect(contained).toBeTruthy();
          expect(ld.isEmpty(ld.xor(g.users, params.users))).toBeTruthy();
          expect(g.visibility).toBe('private');
          expect(g.password).toBeDefined();
          expect(ld.isEmpty(g.password)).toBeFalsy();
          expect(g.readonly).toBeTruthy();
          expect(ld.isArray(g.tags)).toBeTruthy();
          expect(ld.first(g.tags)).toBe('80s');
          expect(g.tags[1]).toBe('coolness');
          user.get(gadm.login, function (err, u) {
            expect(err).toBeNull();
            expect(u.login).toBe(gadm.login);
            expect(ld.isArray(u.groups)).toBeTruthy();
            expect(ld.size(u.groups)).toBe(2);
            expect(u.groups[1]).toBe(g._id);
            done();
          });
        });
      });
    });

    describe('set', function () {
      beforeAll(initGroupUsersAndPads);
      afterAll(specCommon.reInitDatabase);

      it('should throws errors if params._id|name|admin, callback or edit ' +
        'aren`t correct', function () {
          expect(group.set).toThrow();
          expect(ld.partial(group.set, [])).toThrow();
          var params = { name: 123, noadmin: true };
          expect(ld.partial(group.set, params, ld.noop)).toThrow();
          params = { name: 'ok', admin: false };
          expect(ld.partial(group.set, params, ld.noop)).toThrow();
          params.name = 'ok';
          params.admin = 'ok';
          expect(ld.partial(group.set, params, false)).toThrow();
          params._id = 123;
          expect(ld.partial(group.set, params, ld.noop)).toThrow();
        }
      );

      it('should return an error if admin user is not found', function (done) {
        group.set({ _id: gparams._id, name: 'college', admin: 'inexist' },
          function (err, g) {
            expect(ld.isError(err)).toBeTruthy();
            expect(err).toMatch('ITEMS_NOT_FOUND');
            expect(g).toBeUndefined();
            done();
          }
        );
      });

      it('should return an error if group _id is not found', function (done) {
        group.set({ _id: 'i', name: 'g', admin: gadm._id }, function (err, g) {
          expect(ld.isError(err)).toBeTruthy();
          expect(err).toMatch('GROUP.INEXISTENT');
          expect(g).toBeUndefined();
          done();
        });
      });

      it('should return an error if visibility is private with invalid ' +
        'password', function (done) {
          var params = {
          name: 'college3',
          admin: gadm._id,
          visibility: 'private'
        };
        group.set(params, function (err, g) {
          expect(ld.isError(err)).toBeTruthy();
          expect(err).toMatch('PASSWORD_INCORRECT');
          expect(g).toBeUndefined();
          params.password = <PASSWORD>;
          group.set(params, function (err, g) {
            expect(ld.isError(err)).toBeTruthy();
            expect(err).toMatch('PASSWORD_INCORRECT');
            expect(g).toBeUndefined();
            done();
          });
        });
      });

      it('should otherwise accept well defined parameters', function (done) {
        var params = {
          _id: gparams._id,
          name: 'college2',
          description: 'A very strange place',
          admin: gadm._id,
          admins: ld.takeRight(gusers, 2),
          users: ld.take(gusers, 3),
          visibility: 'private',
          password: '<PASSWORD>',
          readonly: true
        };
        group.set(params, function (err, g) {
          expect(err).toBeNull();
          expect(ld.isString(g._id)).toBeTruthy();
          expect(ld.isNumber(g.ctime)).toBeTruthy();
          expect(ld.startsWith(g._id, 'college2-')).toBeFalsy();
          expect(g.name).toBe('college2');
          expect(g.description).toBe('A very strange place');
          expect(ld.isArray(g.admins)).toBeTruthy();
          expect(ld.first(g.admins)).toBe(gadm._id);
          var contained = (ld.size(ld.intersection(g.admins, gusers)) ===
            ld.size(g.admins));
          expect(contained).toBeTruthy();
          expect(ld.isEmpty(ld.xor(g.users, params.users))).toBeTruthy();
          expect(ld.includes(gpads, ld.first(g.pads))).toBeTruthy();
          expect(g.visibility).toBe('private');
          expect(ld.isObject(g.password)).toBeTruthy();
          expect(g.password.salt).toBeDefined();
          expect(g.password.hash).toBeDefined();
          expect(ld.isEmpty(g.password)).toBeFalsy();
          expect(g.readonly).toBeTruthy();
          group.get(g._id, function (err, g) {
            expect(err).toBeNull();
            expect(ld.isString(g._id)).toBeTruthy();
            expect(g.name).toBe('college2');
            expect(ld.isArray(g.admins)).toBeTruthy();
            expect(ld.first(g.admins)).toBe(gadm._id);
            contained = (ld.size(ld.intersection(g.admins, gusers)) ===
              ld.size(g.admins));
            expect(contained).toBeTruthy();
            expect(ld.isEmpty(ld.xor(g.users, params.users))).toBeTruthy();
            expect(ld.includes(gpads, ld.first(g.pads))).toBeTruthy();
            expect(g.visibility).toBe('private');
            expect(g.password).toBeDefined();
            expect(ld.isEmpty(g.password)).toBeFalsy();
            expect(g.readonly).toBeTruthy();
            user.get(gadm.login, function (err, u) {
              expect(err).toBeNull();
              expect(u.login).toBe(gadm.login);
              expect(ld.isArray(u.groups)).toBeTruthy();
              expect(ld.size(u.groups)).toBe(1);
              expect(u.groups[0]).toBe(g._id);
              done();
            });
          });
        });
      });

      it('should accept empty password for an already private group',
        function (done) {
          var params = ld.clone(gparams);
          delete params.password;
          params.name = 'collegeRenamed';
          params.admin = gadm._id;
          group.set(params, function (err, g) {
            expect(err).toBeNull();
            expect(ld.isString(g._id)).toBeTruthy();
            expect(ld.startsWith(g._id, 'collegerenamed-')).toBeFalsy();
            expect(g.name).toBe('collegeRenamed');
            expect(ld.isObject(g.password)).toBeTruthy();
            expect(g.password.salt).toBeDefined();
            expect(g.password.hash).toBeDefined();
            done();
          });
        }
      );
    });

    describe('group get', function () {

      beforeAll(initGroupUsersAndPads);
      afterAll(specCommon.reInitDatabase);

      it('should throw errors if arguments are not provided as expected',
        function () {
          expect(group.get).toThrow();
          expect(ld.partial(group.get, 123)).toThrow();
          expect(ld.partial(group.get, 'key')).toThrow();
          expect(ld.partial(group.get, 'key', 'notAFunc')).toThrow();
        }
      );

      it('should return an Error if the key is not found', function (done) {
        group.get('inexistent', function (err, g) {
          expect(ld.isError(err)).toBeTruthy();
          expect(g).toBeUndefined();
          done();
        });
      });

      it('should return the group otherwise', function (done) {
        group.get(gparams._id, function (err, g) {
          expect(err).toBeNull();
          expect(ld.isString(g._id)).toBeTruthy();
          expect(ld.startsWith(g._id, 'college-')).toBeTruthy();
          expect(g.name).toBe('college');
          expect(ld.isArray(g.admins)).toBeTruthy();
          expect(ld.first(g.admins)).toBe(gadm._id);
          var contained = (ld.size(ld.intersection(g.admins, gusers)) ===
            ld.size(g.admins));
          expect(contained).toBeTruthy();
          expect(ld.isEmpty(ld.xor(g.users, gparams.users))).toBeTruthy();
          expect(ld.includes(gpads, ld.first(g.pads))).toBeTruthy();
          expect(g.visibility).toBe('private');
          expect(g.password).toBeDefined();
          expect(ld.isEmpty(g.password)).toBeFalsy();
          expect(g.readonly).toBeTruthy();
          done();
        });
      });

    });

    describe('group getWithPads', function () {

      beforeAll(initGroupUsersAndPads);
      afterAll(specCommon.reInitDatabase);

      it('should throw errors if arguments are not as expected',
        function () {
          expect(group.getWithPads).toThrow();
          expect(ld.partial(group.getWithPads, 123)).toThrow();
          expect(ld.partial(group.getWithPads, 'key')).toThrow();
          expect(ld.partial(group.getWithPads, 'key', 'notAFunc')).toThrow();
        }
      );

      it('should return an Error if the key is not found', function (done) {
        group.getWithPads('inexistent', function (err, g) {
          expect(ld.isError(err)).toBeTruthy();
          expect(g).toBeUndefined();
          done();
        });
      });

      it('should return the group and its pads otherwise', function (done) {
        group.getWithPads(gparams._id, function (err, g, pads) {
          expect(err).toBeNull();
          expect(ld.isObject(g)).toBeTruthy();
          expect(ld.isObject(pads)).toBeTruthy();
          expect(ld.isString(g._id)).toBeTruthy();
          expect(ld.startsWith(g._id, 'college-')).toBeTruthy();
          expect(g.name).toBe('college');
          pads = ld.values(pads);
          expect(ld.size(pads)).toBe(ld.size(g.pads));
          expect(pads[0].name).toBe('pad1');
          group.set({ name: 'EmptyGroup', admin: gadm._id }, function (err, g) {
            expect(err).toBeNull();
            group.getWithPads(g._id, function (err, g, pads) {
              expect(err).toBeNull();
              expect(g.name).toBe('EmptyGroup');
              expect(ld.size(pads)).toBe(0);
              done();
            });
          });
        });
      });

    });

    describe('group getByUser', function () {
      beforeAll(initGroupUsersAndPads);
      afterAll(specCommon.reInitDatabase);

      it('should throw errors if arguments are incorrect', function () {
        expect(group.getByUser).toThrow();
        expect(ld.partial(group.getByUser, 123)).toThrow();
        expect(ld.partial(group.getByUser, gadm)).toThrow();
        expect(ld.partial(group.getByUser, gadm, 'notABool', function () {}))
          .toThrow();
        expect(ld.partial(group.getByUser, gadm, false, 'notAFunc')).toThrow();
        expect(ld.partial(group.getByUser, { login: 'inexistant' }, false,
          function () {})).toThrowError(/USER_INVALID/);
      });

      it('should return groups otherwise', function (done) {
        user.get(gadm.login, function (err, user) {
          if (err) { console.log(err); }
          gadm = user;
          group.getByUser(gadm, false, function (err, groups) {
            expect(err).toBeNull();
            expect(ld.isObject(groups)).toBeTruthy();
            var key = gadm.groups[0];
            expect(groups[key].name).toBe('college');
            expect(groups[key].visibility).toBe('private');
            expect(groups[key].readonly).toBeTruthy();
            done();
          });
        });
      });

      it('should return groups, pads, admins and users if withExtra is set ' +
        'to true', function (done) {
          user.get(gadm.login, function (err, user) {
            if (err) { console.log(err); }
            gadm = user;
            group.getByUser(gadm, true, function (err, groupsFull) {
              expect(err).toBeNull();
              expect(ld.isObject(groupsFull)).toBeTruthy();
              expect(ld.isObject(groupsFull.groups)).toBeTruthy();
              expect(ld.isObject(groupsFull.pads)).toBeTruthy();
              expect(ld.isObject(groupsFull.users)).toBeTruthy();
              var groups = groupsFull.groups;
              var key = gadm.groups[0];
              expect(groups[key].name).toBe('college');
              expect(groups[key].visibility).toBe('private');
              expect(groups[key].readonly).toBeTruthy();
              var pad = ld.first(ld.values(groupsFull.pads));
              expect(pad.name).toBe('pad1');
              var admin = ld.first(ld.values(groupsFull.users));
              expect(admin.login).toBe('frank');
              done();
            }
          );
        });
      });

    });

    describe('group del', function () {

      beforeAll(initGroupUsersAndPads);
      afterAll(specCommon.reInitDatabase);

      it('should throw errors if arguments are not provided as expected',
        function () {
          expect(group.del).toThrow();
          expect(ld.partial(group.del, 123)).toThrow();
          expect(ld.partial(group.del, 'key')).toThrow();
          expect(ld.partial(group.del, 'key', 'notAFunc')).toThrow();
        }
      );

      it('should return an Error if the key is not found', function (done) {
        group.del('inexistent', function (err, g) {
          expect(ld.isError(err)).toBeTruthy();
          expect(g).toBeUndefined();
          done();
        });
      });

      it('should removes the group otherwise, and all the linked pads',
        function (done) {
          group.del(gparams._id, function (err) {
            expect(err).toBeNull();
            user.get(gadm.login, function (err, u) {
              expect(err).toBeNull();
              expect(u.login).toBe(gadm.login);
              expect(ld.isArray(u.groups)).toBeTruthy();
              expect(ld.includes(u.groups, gparams._id)).toBeFalsy();
              var PFX = storage.DBPREFIX.PAD;
              var pads = ld.map(gpads, function (p) { return PFX + p; });
              storage.fn.getKeys(pads, function (err, res) {
                expect(err).toBeNull();
                expect(ld.isObject(res)).toBeTruthy();
                expect(ld.every(res, ld.isUndefined)).toBeTruthy();
                done();
              });
            });
          });
        }
      );

    });

    describe('group inviteOrShare', function () {

      beforeAll(initGroupUsersAndPads);
      afterAll(specCommon.reInitDatabase);

      it('should throw errors if arguments are not provided as expected',
        function () {
          expect(group.inviteOrShare).toThrow();
          expect(ld.partial(group.inviteOrShare, 'noBool')).toThrow();
          expect(ld.partial(group.inviteOrShare, false, false)).toThrow();
          expect(ld.partial(group.inviteOrShare, false, 'str', 'noArr'))
            .toThrow();
          expect(ld.partial(group.inviteOrShare, false, 'str', [], 'notFn'))
            .toThrow();
          expect(ld.partial(group.inviteOrShare, false, 123, [],
            function () {})).toThrow();
        }
      );

      it('should return an error if there is no admin anymore',
        function (done) {
          group.inviteOrShare(false, gparams._id, [], function (err) {
            expect(ld.isError(err)).toBeTruthy();
            expect(err).toMatch('GROUP.RESIGN_UNIQUE_ADMIN');
            done();
          });
        }
      );

      it('should return the updated group and an object containing present ' +
        ' and absent logins or emails otherwise', function (done) {
          var users = ['<EMAIL>', 'mikey', 'inexist'];
          group.inviteOrShare(true, gparams._id, users,
            function (err, res, uids) {
              expect(err).toBeNull();
              expect(ld.isObject(res)).toBeTruthy();
              expect(ld.size(res.users)).toBe(ld.size(users) - 1);
              expect(ld.size(uids.present)).toBe(2);
              expect(ld.size(uids.absent)).toBe(1);
              expect(uids.absent[0]).toBe('inexist');
              var nbAdmins = res.admins.length;
              var admins = ['guest', 'grace', 'inexist'];
              group.inviteOrShare(false, gparams._id, admins,
                function (err, res, uids) {
                  expect(err).toBeNull();
                  expect(ld.isObject(res)).toBeTruthy();
                  expect(ld.size(res.admins)).toBe(nbAdmins - 1);
                  expect(ld.size(uids.present)).toBe(1);
                  expect(uids.present[0]).toBe('grace');
                  expect(ld.size(uids.absent)).toBe(2);
                  done();
                }
              );
            }
          );
        }
      );

      it('should handle secondary indexed correctly', function (done) {
        group.get(gparams._id, function (err, g) {
          expect(err).toBeNull();
          expect(ld.size(g.users)).toBe(2);
          var uid = g.users[0];
          var UPREFIX = storage.DBPREFIX.USER;
          storage.db.get(UPREFIX + uid, function (err, u) {
            expect(err).toBeNull();
            expect(ld.includes(u.groups, g._id)).toBeTruthy();
            ld.pull(g.users, u._id);
            group.inviteOrShare(true, g._id, ['mikey'], function (err, g) {
              expect(err).toBeNull();
              expect(ld.includes(g.users, u._id)).toBeFalsy();
              expect(ld.size(g.users)).toBe(1);
              done();
            });
          });
        });
      });

    });

    describe('group resign', function () {

      beforeAll(initGroupUsersAndPads);
      afterAll(specCommon.reInitDatabase);

      it('should throw errors if arguments are not as expected', function () {
        expect(group.resign).toThrow();
        expect(ld.partial(group.resign, false)).toThrow();
        expect(ld.partial(group.resign, false, false, ld.noop)).toThrow();
        expect(ld.partial(group.resign, 'gid')).toThrow();
        expect(ld.partial(group.resign, 'gid', 'uid')).toThrow();
        expect(ld.partial(group.resign, 'gid', false)).toThrow();
        expect(ld.partial(group.resign, 'gid', 'uid', false)).toThrow();
      });

      it('should return an error if group does not exist', function (done) {
        group.resign('fakeGid', gadm._id, function (err) {
          expect(ld.isError(err)).toBeTruthy();
          expect(err).toMatch('KEY_NOT_FOUND');
          done();
        });
      });

      it('should forbid resignation from non user or non admin',
        function (done) {
          group.resign(gparams._id, 'fakeUid', function (err) {
            expect(ld.isError(err)).toBeTruthy();
            expect(err).toMatch('GROUP.NOT_USER');
            done();
          }
        );
      });

      it('should forbid resignation from unique group admin', function (done) {
        group.set({
          name: 'new Group',
          admin: gadm._id
        }, function (err, g) {
          expect(err).toBeNull();
          group.resign(g._id, gadm._id, function (err) {
            expect(ld.isError(err)).toBeTruthy();
            expect(err).toMatch('RESIGN_UNIQUE_ADMIN');
            done();
          });
        });
      });

      it('should allow resignation for user and admin otherwise',
        function (done) {
          var uid = ld.first(gusers);
          var UPREFIX = storage.DBPREFIX.USER;
          storage.db.get(UPREFIX + uid, function (err, u) {
            expect(err).toBeNull();
            expect(ld.includes(u.groups, gparams._id)).toBeTruthy();
            group.resign(gparams._id, uid, function (err, g) {
              expect(err).toBeNull();
              var users = ld.union(g.admins, g.users);
              expect(ld.includes(users, uid)).toBeFalsy();
              storage.db.get(UPREFIX + uid, function (err, u) {
                expect(err).toBeNull();
                expect(ld.includes(u.groups, gparams._id)).toBeFalsy();
                uid = ld.last(gusers);
                group.resign(gparams._id, uid, function (err, g) {
                  expect(err).toBeNull();
                  users = ld.union(g.admins, g.users);
                  expect(ld.includes(users, uid)).toBeFalsy();
                  done();
                });
              });
            });
          });
        }
      );

    });

  });

}).call(this);


<|start_filename|>scripts/remove-orphan-db-records.js<|end_filename|>
#! /usr/bin/env node
// vim:set sw=2 ts=2 sts=2 ft=javascript expandtab:
'use strict';

// Dependencies
var program = require('commander'),
      fs    = require('fs'),
      path  = require('path');

var _cliProgress = require('cli-progress');

var ueberDB      = require('ueberdb2'),
    jsonminify   = require('jsonminify'),
    eachSeries   = require('async/eachSeries'),
    eachOfSeries = require('async/eachOfSeries');

// Arguments handling
program
  .version('0.1.0')
  .option('-s, --settings <file>', '[MANDATORY] the path to your Etherpad\'s settings.json file')
  .option('-n, --dryrun',          '(optional) just count the number of pads that would have been normally deleted')
  .parse(process.argv);

// Check that we have the mandatory arguments
if (!program.settings) {
  console.log('');
  console.log('=====================================================================');
  console.log('  You must provide the path to your Etherpad\'s settings.json file!');
  console.log('=====================================================================');
  program.help();
}

// Try to parse the settings
var settingsFilename = path.resolve(program.settings);
var settingsStr      = fs.readFileSync(settingsFilename).toString();
var settings;
try {
  if(settingsStr) {
    settingsStr = jsonminify(settingsStr).replace(',]',']').replace(',}','}');
    settings    = JSON.parse(settingsStr);
  }
} catch(e) {
  console.error('There was an error processing your settings.json file: '+e.message);
  process.exit(1);
}

// Open the database
var db = new ueberDB.database(settings.dbType, settings.dbSettings, {cache: 0, writeInterval: 0});

/*
 * Functions
 */
function exitIfErr(err) {
  console.log('=====================');
  console.error(err);
  return db.doShutdown(function() { process.exit(1); });
}

// Remove record from DB
function removeRecord(key, callback) {
  db.remove(key, function(err) {
    if (err) { return exitIfErr(err); }
    callback();
  });
}

/*
 * Start searching
 */
db.init(function(err) {
  if (err) { return exitIfErr(err); }

  db.get('mypads:conf:allowEtherPads', function(err, val) {
    if (err) { return exitIfErr(err); }

    // Yeah, we don't want to remove anonymous pads
    if (val !== false) {
      console.log(' _______________');
      console.log('|===============|');
      console.log('|: .---------. :|');
      console.log('|: | HAL-9000| :|');
      console.log('|: \'---------\' :|');
      console.log('|:             :|    ________________________________________');
      console.log('|:             :|   / I\'m sorry, Dave, I\'m afraid I can\'t do \\');
      console.log('|:             :|   \\ that.                                  /');
      console.log('|:             :|    ----------------------------------------');
      console.log('|:      _      :|    /');
      console.log('|:   ,`   `.   :| __/');
      console.log('|:   : (o) :   :|');
      console.log('|:   `. _ ,`   :|');
      console.log('|:             :|');
      console.log('|:_____________:|');
      console.log('|:=============:|');
      console.log('|:*%*%*%*%*%*%*:|');
      console.log('|:%*%*%*%*%*%*%:|');
      console.log('|:*%*%*%*%*%*%*:|');
      console.log('|:%*%*%*%*%*%*%:|');
      console.log('\'===============\'');
      console.log('');
      console.log('================================================');
      console.log('It seems that you allow anonymous pads.');
      console.log('This script would delete all the anonymous pads.');
      console.log('Exiting');
      console.log('================================================');
      return db.doShutdown(function() { process.exit(1); });
    }

    // readonly2pad:* records are leftovers of deleted pads before #197 was solved
    db.findKeys('readonly2pad:*', null, function(err, keys) {
      if (err) { return exitIfErr(err); }

      var candidatesForDeletion = 0;

      if (keys.length === 0) {
        console.log('No pads to check.');
        return db.doShutdown(function() { process.exit(0); });
      }

      console.log(keys.length+' pad(s) to check.');

      // I love progress bars, it's cool
      var bar = new _cliProgress.Bar({
        format: '[{bar}] {percentage}% | ETA: {eta}s | {val}/{tot}',
        stopOnComplete: true,
        hideCursor: true,
        barsize: 60,
        fps: 60
      }, _cliProgress.Presets.shades_grey);
      bar.start(2 * keys.length, 0, { val: 0, tot: keys.length });

      // For each readonly2pad record, do pad existence check
      eachOfSeries(keys, function(readonly2pad, index, next) {
        bar.increment(1, { val: index + 1 });

        // First, get the padId
        db.get(readonly2pad, function(err, padId) {
          if (err) { return exitIfErr(err); }

          // Then, check if the pad exists in MyPads
          db.get('mypads:pad:'+padId, function(err, val) {
            if (err) { return exitIfErr(err); }

            // Launch a delete process if the pad doesn't exist
            if (val === null) {
              if (program.dryrun) {
                bar.increment();
                candidatesForDeletion++;
                return next();
              }
              // Cascade deletion process
              // 1. Delete the readonly2pad record
              db.remove(readonly2pad, function(err) {
                if (err) { return exitIfErr(err); }

                // 2. Delete the pad2readonly:padId record
                db.remove('pad2readonly:'+padId, function(err) {
                  if (err) { return exitIfErr(err); }

                  // 3. Delete the pad:padId record
                  db.remove('pad:'+padId, function(err) {
                    if (err) { return exitIfErr(err); }

                    // 4. Check for revs records
                    db.findKeys('pad:'+padId+':revs:*', null, function(err, keys) {
                      if (err) { return exitIfErr(err); }

                      // 5. Delete the revs records
                      eachSeries(keys, removeRecord, function(err) {
                        if (err) { return exitIfErr(err); }
                        // 6. Check for chat records
                        db.findKeys('pad:'+padId+':chat:*', null, function(err, keys) {
                          if (err) { return exitIfErr(err); }

                          // 7. Delete the chat records
                          eachSeries(keys, removeRecord, function(err){
                            if (err) { return exitIfErr(err); }
                            bar.increment();
                            return next();
                          });
                        });
                      });
                    });
                  });
                });
              });
            } else {
              // No deletion needed
              bar.increment();
              return next();
            }
          });
        });
      }, function(err) {
        if (err) { return exitIfErr(err); }

        // Give time for progress bar to update before exiting
        setTimeout(function() {
          console.log(keys.length+' pad(s) checked.');

          if (program.dryrun) {
            console.log(candidatesForDeletion+' pad(s) would have been deleted.');
          }

          return db.doShutdown(function() { process.exit(0); });
        }, 100);
      });
    });
  });
});


<|start_filename|>utils.js<|end_filename|>
function wrapPromise (p, cb) {
  return p.then(function (result) { cb(null, result); })
    .catch(function(err) { cb(err); });
}

exports.callbackify1 = function (fun) {
  return function (arg1, cb) {
    return wrapPromise(fun(arg1), cb);
  };
};

exports.callbackify2 = function (fun) {
  return function (arg1, arg2, cb) {
    return wrapPromise(fun(arg1, arg2), cb);
  };
};


<|start_filename|>templates/mail_de.json<|end_filename|>
{
    "ACCOUNT_CONFIRMATION": "<%= login %>, Sie haben sich bei <%= title %> angemeldet. Diese Etherpad-Instanz benötigt eine E-Mail-Bestätigung, um Ihr Konto zu aktivieren. Um fortzufahren, gehen Sie zu dieser Webadresse: <%= url %>. Diese URL ist nur für <%= duration %> Minuten gültig. Danach müssen Sie einen Administrator kontaktieren.",
    "ACCOUNT_CONFIRMATION_SUBJECT": "Kontobestätigung auf <%= title %>",
    "PASSRECOVER": "<%= login %>, eine Anfrage zur Änderung ihres Passwortes bei <%= title %> wurde für ihren Zugang ausgelöst. Wenn Sie Ihre Passwortänderung bestätigen möchten, gehen Sie bitte zu dieser Webadresse: <%= url %>. Wenn diese Anfrage nicht von Ihnen kommt, ignorieren Sie einfach diese E-Mail. Die Adresse wird in <%= duration %> Minuten ungültig.",
    "PASSRECOVER_SUBJECT": "Wiederherstellung des Passwortes für <%= title %>"
}


<|start_filename|>spec/frontend/userlist.spec.js<|end_filename|>
/**
*  vim:set sw=2 ts=2 sts=2 ft=javascript expandtab:
*
*  # Userlists list, form and remove functional testing
*
*  ## License
*
*x  Licensed to the Apache Software Foundation (ASF) under one
*  or more contributor license agreements.  See the NOTICE file
*  distributed with this work for additional information
*  regarding copyright ownership.  The ASF licenses this file
*  to you 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 = (function () {
  'use strict';
  // Dependencies
  var common = require('./common.js');
  var fill = common.helper.fill;

  var list = {};

  list.run = function (app) {
    // Shared variables
    var $el;
    var qfirst = function (sel) { return app.document.querySelector(sel); };
    var qall = function (sel) { return app.document.querySelectorAll(sel); };

    describe('userlists module testing', function () {

      describe('list', function () {

        beforeAll(function (done) {
          // Login and go to userlists list page
          app.document.querySelector('header nav a:first-child').click();
          window.setTimeout(function () {
            fill(qfirst('input[name=login]'), 'parker');
            fill(qfirst('input[name=password]'), '<PASSWORD>');
            qfirst('input[type=submit]').click();
            window.setTimeout(function () {
              qfirst('nav.menu-main a[href$=myuserlists]').click();
              window.setTimeout(function () {
                $el = qall('ul.group li');
                qfirst('body > section > div p').click();
                window.setTimeout(done, 100);
              }, 200);
            }, 200);
          }, 200);
        });

        afterAll(function (done) {
          qfirst('.icon-logout').parentNode.click();
          window.setTimeout(function () {
            qfirst('body > section p').click();
            done();
          }, 100);
        });

        it('should display properties properly', function (done) {
          $el = qall('ul.group li');
          expect($el.length).toBe(1);
          expect($el[0].querySelector('header h4').textContent).toBe('enemies');
          expect($el[0].querySelector('dl dt').textContent).toBe('Users');
          expect($el[0].querySelector('dl dd').textContent).toBe('2');
          window.setTimeout(done, 100);
        });

      });

      describe('userlists form module testing', function () {

        beforeAll(function (done) {
          // Login and go to userlists add page
          app.document.querySelector('header nav a:first-child').click();
          window.setTimeout(function () {
            fill(qfirst('input[name=login]'), 'parker');
            fill(qfirst('input[name=password]'), '<PASSWORD>');
            qfirst('input[type=submit]').click();
            window.setTimeout(function () {
              qfirst('nav.menu-main a[href$=myuserlists]').click();
              window.setTimeout(function () {
                qfirst('section.group h2 a[href$=add]').click();
                window.setTimeout(function () {
                  $el = {
                    form: qfirst('form#ulist-form'),
                    submit: qfirst('input.send[type=submit]'),
                    name: qfirst('input[name=name]'),
                    users: qfirst('input[name=users]'),
                    ok: qfirst('button.ok'),
                    ulist: qfirst('section.group-form ul')
                  };
                  qfirst('body > section > div p').click();
                  window.setTimeout(done, 100);
                }, 200);
              }, 200);
            }, 200);
          }, 200);
        });

        afterAll(function (done) {
          qfirst('.icon-logout').parentNode.click();
          window.setTimeout(function () {
            qfirst('body > section p').click();
            done();
          }, 100);
        });

        describe('userlist creation', function () {

          it('should forbid creation with no name', function () {
            $el.submit.click();
            expect($el.form.checkValidity()).toBeFalsy();
            expect($el.name.checkValidity()).toBeFalsy();
          });

          it('should create otherwise', function (done) {
            fill($el.name, 'secretlist');
            fill($el.users, 'frank');
            $el.ok.click();
            window.setTimeout(function () {
              fill($el.users, 'inexistent');
              $el.ok.click();
              window.setTimeout(function () {
                var $opts = $el.ulist.querySelectorAll('li');
                expect($opts.length).toBe(2);
                expect($opts[0].textContent).toBe('frank');
                expect($opts[1].textContent).toBe('inexistent');
                $el.submit.click();
                window.setTimeout(function () {
                  var $notif = qfirst('body > section.notification');
                  expect($notif.innerHTML)
                    .toMatch('Userlist has been successfully created');
                  expect($notif.innerHTML)
                    .toMatch('no account in this instance : inexistent');
                  expect($notif.innerHTML).toMatch('successfull for frank');
                  var notifs = $notif.querySelectorAll('p');
                  notifs[0].click();
                  notifs[1].click();
                  notifs[2].click();
                  var $ulist = qall('section.group ul.group li')[1];
                  expect($ulist.querySelector('h4').textContent)
                    .toBe('secretlist');
                  expect($ulist.querySelector('dd').textContent).toBe('1');
                  window.setTimeout(function () {
                    qfirst('body > section.notification p').click();
                    window.setTimeout(done, 100);
                  }, 100);
                }, 200);
              }, 200);
            }, 200);
          });

        });

        describe('userlist edition', function () {

          beforeAll(function (done) {
            qall('section.group a[href$=edit]')[1].click();
            window.setTimeout(function () {
              $el = {
                form: qfirst('form#ulist-form'),
                submit: qfirst('input.send[type=submit]'),
                name: qfirst('input[name=name]'),
                users: qfirst('input[name=users]'),
                ok: qfirst('button.ok'),
                ulist: qall('section.group-form ul li')
              };
              window.setTimeout(done, 100);
            }, 200);
          });

          it('should allow renaming and user removal', function (done) {
            expect($el.name.value).toBe('secretlist');
            fill($el.name, 'secret');
            expect($el.ulist.length).toBe(1);
            expect($el.ulist[0].textContent).toBe('frank');
            $el.ulist[0].querySelector('i.icon-cancel').click();
            window.setTimeout(function () {
              $el.submit.click();
              window.setTimeout(function () {
                var $notif = qfirst('body > section.notification p');
                expect($notif.textContent)
                  .toMatch('Userlist has been successfully updated');
                $notif.click();
                var $ulist = qall('section.group ul.group li')[1];
                expect($ulist.querySelector('h4').textContent).toBe('secret');
                expect($ulist.querySelector('dd').textContent).toBe('0');
                window.setTimeout(done, 100);
              }, 200);
            }, 100);
          });

        });

      });

      describe('userlists remove module testing', function () {
        beforeAll(function (done) {
          // Login and go to userlists list page
          app.document.querySelector('header nav a:first-child').click();
          window.setTimeout(function () {
            fill(qfirst('input[name=login]'), 'parker');
            fill(qfirst('input[name=password]'), '<PASSWORD>');
            qfirst('input[type=submit]').click();
            window.setTimeout(function () {
              qfirst('nav.menu-main a[href$=myuserlists]').click();
              window.setTimeout(function () {
                qfirst('body > section > div p').click();
                window.setTimeout(done, 100);
              }, 200);
            }, 200);
          }, 200);
        });

        afterAll(function (done) {
          qfirst('.icon-logout').parentNode.click();
          window.setTimeout(function () {
            qfirst('body > section p').click();
            done();
          }, 100);
        });

        it('should not remove if canceled', function (done) {
          app.window.confirm = function () { return false; };
          var $ulists = qall('section.group ul.group li');
          expect($ulists.length).toBe(2);
          $ulists[0].querySelector('a[href$=remove]').click();
          window.setTimeout(function () {
            $ulists = qall('section.group ul.group li');
            expect($ulists.length).toBe(2);
            done();
          }, 100);
        });

        it('should remove otherwise', function (done) {
          app.window.confirm = function () { return true; };
          var $ulists = qall('section.group ul.group li');
          expect($ulists.length).toBe(2);
          $ulists[1].querySelector('a[href$=remove]').click();
          window.setTimeout(function () {
            var $n = qfirst('body > section p');
            expect($n.innerHTML)
              .toMatch('Userlist has been successfully removed');
            $n.click();
            $ulists = qall('section.group ul.group li');
            expect($ulists.length).toBe(1);
            done();
          }, 100);
        });

      });

    });
  };

  return list;

}).call(this);


<|start_filename|>frontend/js/helpers/filterPads.js<|end_filename|>
/**
*  vim:set sw=2 ts=2 sts=2 ft=javascript expandtab:
*
*  # FilterPads helpers functions
*
*  ## License
*
*  Licensed to the Apache Software Foundation (ASF) under one
*  or more contributor license agreements.  See the NOTICE file
*  distributed with this work for additional information
*  regarding copyright ownership.  The ASF licenses this file
*  to you 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.
*
*  ## Description
*
*  This module contains a function to filter pads in a folder
*/

module.exports = (function () {
  // Dependencies

  var filterPads = {};

  /**
  * `filter` will hide or show pads which name matches the filter
  */

  filterPads.filterKeyword = function(keyword) {
    var pads;
    if (typeof keyword === 'undefined' || keyword === '') {
      pads = document.querySelectorAll('li.list-group-item.group-pad-item');
      [].forEach.call(pads, function(element) {
        element.classList.remove('hidden');
      });
    } else {
      var rgx = new RegExp(keyword, 'i');
      pads    = document.querySelectorAll('li.list-group-item.group-pad-item');
      [].forEach.call(pads, function(element) {
        if (element.getAttribute('data-padname').match(rgx)) {
          element.classList.remove('hidden');
        } else {
          element.classList.add('hidden');
        }
      });
    }
  };

  return filterPads;

}).call(this);


<|start_filename|>spec/frontend/group-remove.spec.js<|end_filename|>
/**
*  vim:set sw=2 ts=2 sts=2 ft=javascript expandtab:
*
*  # Group removal functional testing
*
*  ## License
*
*  Licensed to the Apache Software Foundation (ASF) under one
*  or more contributor license agreements.  See the NOTICE file
*  distributed with this work for additional information
*  regarding copyright ownership.  The ASF licenses this file
*  to you 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 = (function () {
  'use strict';
  // Dependencies
  var common = require('./common.js');
  var fill = common.helper.fill;

  var remove = {};

  remove.run = function (app) {

    describe('group remove module testing', function () {
      var qfirst = function (sel) { return app.document.querySelector(sel); };

      beforeAll(function (done) {
        // Login
        app.document.querySelector('header nav a:first-child').click();
        window.setTimeout(function () {
          fill(app.document.querySelector('input[name=login]'), 'parker');
          fill(app.document.querySelector('input[name=password]'),
            '<PASSWORD>');
          app.document.querySelector('input[type=submit]').click();
          window.setTimeout(function () {
            app.document.querySelectorAll('a[href$=view]')[9].click();
            app.document.querySelector('body > section p').click();
            done();
          }, 200);
        }, 100);
      });

      afterAll(function (done) {
        app.document.querySelector('.icon-logout').parentNode.click();
        window.setTimeout(function () {
          app.document.querySelector('body > section p').click();
          done();
        }, 100);
      });

      it('should not remove if not confirmed', function (done) {
        app.window.confirm = function () { return false; };
        qfirst('section.group a[title=Remove]').click();
        window.setTimeout(function () {
          var title = qfirst('section.group h2 span').textContent;
          expect(title).toBe('Group High School Memories, again');
          done();
        }, 100);
      });

      it('should remove if confirmed', function (done) {
        app.window.confirm = function () { return true; };
        qfirst('section.group a[title=Remove]').click();
        window.setTimeout(function () {
          var $li = app.document.querySelector('h3.group').parentNode;
          $li = $li.querySelector('ul li');
          expect($li.querySelector('h4').textContent).toBe('Santa Fe');
          var $n = app.document.querySelector('body > section p');
          expect($n.innerHTML).toMatch('Group has been successfully removed');
          $n.click();
          done();
        }, 100);
      });

    });

  };

  return remove;
}).call(this);


<|start_filename|>static/css/widgets/notification.css<|end_filename|>
/* Notification Widget */

/* Global styling */

section.notification {
  z-index: 9999;
  cursor: pointer;
  width: 80%;
  position: fixed;
  right: 1%;
  bottom: 2%;
}

/* Tablet */
@media (min-width: 40em) {
  section.notification { width: 40%; }
}
/* Desktop */
@media (min-width: 60em) {
  section.notification { width: 25%; }
}

section.notification > div > div {
  animation: 1s fadein;
  -moz-animation: 1s fadein;
  -webkit-animation: 1s fadein;
}


section.notification header {
  font-weight: bold;
}

section.notification i.close { 
  position: absolute;
  right: 3px;
  top: 5px;
}

/* Animation */

@keyframes fadein { from { opacity: 0 } to { opacity: 1 } }
@-moz-keyframes fadein { from { opacity: 0 } to { opacity: 1 } }
@-webkit-keyframes fadein { from { opacity: 0 } to { opacity: 1 } }


<|start_filename|>frontend/js/helpers/form.js<|end_filename|>
/**
*  vim:set sw=2 ts=2 sts=2 ft=javascript expandtab:
*
*  # Form helpers functions
*
*  ## License
*
*  Licensed to the Apache Software Foundation (ASF) under one
*  or more contributor license agreements.  See the NOTICE file
*  distributed with this work for additional information
*  regarding copyright ownership.  The ASF licenses this file
*  to you 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.
*
*  ## Description
*
*  This module contains common functions used with forms.
*/

module.exports = (function () {
  // Dependencies
  var m = require('mithril');

  var form = {};

  /**
  * ## icon
  *
  * `icon` is an helper that makes an classic icon by its :
  *
  * - `c` controller,
  * - `name` of the field,
  * - `info` message
  * - `err` message
  *
  * It returns a vdom node.
  */

  form.icon = function (c, name, info, err) {
    var icls = 'glyphicon mp-tooltip glyphicon-';
    icls    += (c.valid[name]() ? 'info-sign' : 'exclamation-sign');
    var msg  = c.valid[name]() ? info : err;
    return m('i', {
      class: icls,
      'data-msg': msg
    });
  };

  /**
  * ## field
  *
  * `field` is an helper that returns the triplet of vdoms, by taking
  *
  * - the `c` controller,
  * - the `name` of the field,
  * - the `label` locale
  *   the `icon` ready vdom
  */

  form.field = function (c, name, label, icon) {
    return {
      label: m('label', { for: name }, [ label, icon ]),
      input:
        m('input.form-control', {
          name: name,
          value: c.data[name]() || '',
          oninput: form.handleField.bind(null, c)
        }),
      icon: icon
    };
  };


  /**
  * `initFields` takes
  *
  * - a controller instance
  * - an array of field `names`
  *
  * It creates local data states for values of these fields and booleans values
  * for validitation purpose.
  */

  form.initFields = function (c, names) {
    c.data  = {};
    c.valid = {};
    names.forEach(function (n) {
      c.data[n]  = m.prop();
      c.valid[n] = m.prop(true);
    });
  };

  /**
  * `handleField` function takes
  *
  * - a `c` controller instance, used to set validity and fix value
  * - an `extra` optional function that can be used to add another test to HTML5
  *   validity, that takes itself `c` controller and `e` event
  * - a DOM Event, which use HTML5 Validation API to ensure input is valid or
  *   not.
  */

  form.handleField = function (c, extra, e) {
    if (!e) {
      e     = extra;
      extra = undefined;
    }
    var field   = e.target.getAttribute('name');
    var isValid = function () {
      if (extra) {
        return (e.target.checkValidity() && extra(c, e));
      } else {
        return e.target.checkValidity();
      }
    };
    c.data[field](e.target.value);
    c.valid[field](isValid());
  };

  /**
  * `focusOnInit` is a mithril.config attribute that focus on a given `element`
  * at first initialization.
  */

  form.focusOnInit = function (element, isInit) {
    if (!isInit) { element.focus(); }
  };

  return form;

}).call(this);


<|start_filename|>spec/frontend/pad-view.spec.js<|end_filename|>
/**
*  vim:set sw=2 ts=2 sts=2 ft=javascript expandtab:
*
*  # Pad view functional testing
*
*  ## License
*
*  Licensed to the Apache Software Foundation (ASF) under one
*  or more contributor license agreements.  See the NOTICE file
*  distributed with this work for additional information
*  regarding copyright ownership.  The ASF licenses this file
*  to you 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 = (function () {
  'use strict';
  // Dependencies
  var common = require('./common.js');
  var fill = common.helper.fill;


  var view = {};


  view.run = function (app) {
    // Shared variables
    var qfirst = function (sel) { return app.document.querySelector(sel); };
    var qall = function (sel) { return app.document.querySelectorAll(sel); };

    var login = function (done) {
      // Login and go to group view page
      app.document.querySelector('header nav a:first-child').click();
      window.setTimeout(function () {
        fill(app.document.querySelector('input[name=login]'), 'parker');
        fill(app.document.querySelector('input[name=password]'), '<PASSWORD>');
        app.document.querySelector('input[type=submit]').click();
        window.setTimeout(function () {
          app.document.querySelectorAll('a[href$=view]')[4].click();
          window.setTimeout(function () {
            qfirst('body > section > div p').click();
            window.setTimeout(done, 100);
          }, 200);
        }, 200);
      }, 200);
    };

    var logout = function (done) {
      app.document.querySelector('.icon-logout').parentNode.click();
      window.setTimeout(function () {
        app.document.querySelector('body > section p').click();
        done();
      }, 100);
    };

    describe('pad view module testing', function () {

      beforeAll(login);
      afterAll(logout);

      describe('public pad view and properties', function () {

        beforeAll(function (done) {
          qfirst('section.pad li span a').click();
          window.setTimeout(done, 200);
        });

        it('should display properties as expected', function (done) {
          var title = qfirst('section.group h2 span');
          var group = qfirst('span.subtitle');
          var iframe = qfirst('iframe');
          window.setTimeout(function () {
            expect(title.textContent).toBe('Pad Loving Annie');
            expect(group.textContent).toMatch('memories');
            expect(iframe.getAttribute('src')).not.toMatch('mypadspassword');
            done();
          }, 100);
        });

        it('should have links for several things', function (done) {
          var actions = {
            btns: qall('p.actions button'),
            links: qall('p.actions a'),
            tab: qfirst('section.pad a')
          };
          var iframe = qfirst('iframe');
          window.setTimeout(function () {
            expect(actions.btns[0].getAttribute('title')).toBe('Bookmark');
            expect(actions.btns[1].getAttribute('title')).toBe('Share');
            expect(actions.links[0].getAttribute('title'))
              .toBe('Configuration');
            expect(actions.links[0].getAttribute('href')).toMatch('/pad/edit');
            expect(actions.links[1].getAttribute('title')).toBe('Remove');
            expect(actions.links[1].getAttribute('href'))
              .toMatch('/pad/remove');
            expect(actions.tab.getAttribute('href'))
              .toBe(iframe.getAttribute('src'));
            done();
          }, 200);
        });

      });

      describe('public pad view for guest', function () {
        var url;

        beforeAll(function (done) {
          url = app.window.location.href;
          logout(done);
        });
        afterAll(login);

        it('should allow access to this public pad without login',
          function (done) {
            app.window.location.href = url;
            window.setTimeout(function () {
              app.document.querySelector('ul.lang li').click();
              window.setTimeout(function () {
                var title = qfirst('section.group h2 span');
                var group = qfirst('span.subtitle');
                expect(title.textContent).toBe('Pad Loving Annie');
                expect(group.textContent).toMatch('memories');
                done();
              }, 100);
            }, 400);
          }
        );

      });


    });

  };

  return view;

}).call(this);


<|start_filename|>frontend/js/helpers/expandPad.js<|end_filename|>
/**
*  vim:set sw=2 ts=2 sts=2 ft=javascript expandtab:
*
*  # expandPad helpers functions
*
*  ## License
*
*  Licensed to the Apache Software Foundation (ASF) under one
*  or more contributor license agreements.  See the NOTICE file
*  distributed with this work for additional information
*  regarding copyright ownership.  The ASF licenses this file
*  to you 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.
*
*  ## Description
*
*  This module contains function to check if the user cache is populated
*/

module.exports = (function () {
  // Dependencies
  var cookie = require('js-cookie');
  var conf   = require('../configuration.js');

  var expandPad = {};

  /*
   * Expand iframe when the cookie says to
   */
  expandPad.autoExpand = function(element, isInitialized) {
    if (isInitialized) { return; }

    if (expandPad.wantExpandedPad()) {
      expandPad.expandIframe();
    }
  };

  /*
   * Read the wantExpandedPad cookie and if it says that the user wants to expand iframe
   */
  expandPad.wantExpandedPad = function() {
    var wantExpandedPad = cookie.get('wantExpandedPad');
    return (typeof wantExpandedPad === 'string' && wantExpandedPad === 'true');
  };

  /*
   * Check if iframe is expanded
   */
  expandPad.isExpanded = function() {
      return ((document.querySelector('a.expand-toggle').className).indexOf('expanded') > -1);
  };

  /*
   * Expand the iframe
   */
  expandPad.expandIframe = function() {
    var iframe   = document.querySelector('section.pad iframe');
    var aExpandI = document.querySelector('a.expand-toggle i');
    var aExpand  = document.querySelector('a.expand-toggle');

    if (iframe) {
      iframe.style.height = '80vh';
    }

    if (aExpandI) {
      aExpandI.classList.remove('glyphicon-resize-full');
      aExpandI.classList.add('glyphicon-resize-small');
    }

    if (aExpand) {
      aExpand.classList.add('expanded');
      aExpand.title = conf.LANG.GROUP.PAD.REDUCE;
    }

    cookie.set('wantExpandedPad', 'true', { expires: 365, path: conf.URLS.RAWBASE });
  };

  /*
   * Reduce the iframe
   */
  expandPad.reduceIframe = function(remember) {
    if (typeof(remember) === 'undefined') {
      remember = true;
    }
    var iframe   = document.querySelector('section.pad iframe');
    var aExpandI = document.querySelector('a.expand-toggle i');
    var aExpand  = document.querySelector('a.expand-toggle');

    if (iframe) {
      iframe.style.height = null;
    }

    if (aExpandI) {
      aExpandI.classList.remove('glyphicon-resize-small');
      aExpandI.classList.add('glyphicon-resize-full');
    }

    if (aExpand) {
      aExpand.classList.remove('expanded');
      aExpand.title = conf.LANG.GROUP.PAD.EXPAND;
    }

    if (remember) {
      cookie.set('wantExpandedPad', 'false', { expires: 365, path: conf.URLS.RAWBASE });
    }
  };

  return expandPad;

}).call(this);


<|start_filename|>spec/frontend/login.spec.js<|end_filename|>
/**
*  vim:set sw=2 ts=2 sts=2 ft=javascript expandtab:
*
*  # Login functional testing
*
*  ## License
*
*  Licensed to the Apache Software Foundation (ASF) under one
*  or more contributor license agreements.  See the NOTICE file
*  distributed with this work for additional information
*  regarding copyright ownership.  The ASF licenses this file
*  to you 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 = (function () {
  'use strict';
  // Dependencies
  var common = require('./common.js');
  var fill = common.helper.fill;
  var login = {};

  var first;
  var $el;
  login.beforeAll = function (app) {
    // Go to login page
    app.document.querySelector('header nav a:first-child').click();
    first = function (sel) { return app.document.querySelector(sel); };
    $el = {
      form: first('form'),
      login: first('input[name=login]'),
      password: first('input[name=password]'),
      submit: first('input[type=submit]')
    };
  };

  login.run = function (app) {

    describe('login module testing', function () {

      beforeAll(login.beforeAll.bind(null, app));

      afterEach(function (done) {
        $el.login.value = '';
        $el.password.value = '';
        done();
      });

      it('should have a default title without username', function () {
        expect(app.document.title).toBe('MyPads');
      });

      it('should allow language change from homepage', function (done) {
        expect(first('h2').textContent).toMatch('Connexion');
        first('header ul li').click();
        window.setTimeout(function () {
          expect(first('h2').textContent).toMatch('Login');
          first('header ul li:last-child').click();
          window.setTimeout(done, 200);
        }, 200);
      });

      it('should forbid submision whith no login/pass fill', function (done) {
        $el.submit.click();
        expect($el.form.checkValidity()).toBeFalsy();
        done();
      });

      it('should forbid submission whith incorrect log/pass', function (done) {
        fill($el.login,'a login');
        expect($el.form.checkValidity()).toBeFalsy();
        expect($el.login.checkValidity()).toBeTruthy();
        expect($el.password.checkValidity()).toBeFalsy();
        fill($el.password, '<PASSWORD>');
        expect($el.form.checkValidity()).toBeFalsy();
        expect($el.login.checkValidity()).toBeTruthy();
        expect($el.password.checkValidity()).toBeFalsy();
        done();
      });

      it('should allow submission with conform login and password',
        function (done) {
          $el.login.value = 'mikey';
          $el.password.value = '<PASSWORD>';
          expect($el.form.checkValidity()).toBeTruthy();
          done();
        }
      );

      it('should return an error if user does not exist or password is bad',
        function (done) {
          fill($el.login, 'fakeMikey');
          fill($el.password, '<PASSWORD>');
          expect($el.form.checkValidity()).toBeTruthy();
          $el.submit.click();
          window.setTimeout(function () {
            var $err = first('body > section.notification div div');
            expect($err.className).toMatch('alert-danger');
            $err.click();
            done();
          }, 200);
        }
      );

      it('should login if user and pass are ok', function (done) {
        fill($el.login, 'parker');
        fill($el.password, '<PASSWORD>');
        expect($el.form.checkValidity()).toBeTruthy();
        $el.submit.click();
        window.setTimeout(function () {
          var $success = first('body > section p');
          expect($success.parentNode.className).toMatch('success');
          $success.click();
          window.setTimeout(function () {
            expect(app.location.search).toBe('?/mypads');
            first('.glyphicon-off').parentNode.click();
            window.setTimeout(function () {
              first('body > section > div p').click();
              window.setTimeout(done, 100);
            }, 100);
          }, 100);
        }, 100); // Too low ?
      });

    describe('logout module testing', function () {

      beforeAll(login.beforeAll.bind(null, app));

      it('should logout after login', function (done) {
        fill($el.login, 'parker');
        fill($el.password, '<PASSWORD>');
        $el.submit.click();
        window.setTimeout(function () {
          first('body > section p').click();
          first('.glyphicon-off').parentNode.click();
          window.setTimeout(function () {
            expect(first('.glyphicon-lock')).toBeTruthy();
            expect(app.document.title).toBe('MyPads');
            first('body > section p').click();
            done();
          }, 100);
        }, 100);
      });
    });

    });
  };
  return login;
}).call(this);


<|start_filename|>spec/frontend/bookmark.spec.js<|end_filename|>
/**
*  vim:set sw=2 ts=2 sts=2 ft=javascript expandtab:
*
*  # Bookmark list functional testing
*
*  ## License
*
*  Licensed to the Apache Software Foundation (ASF) under one
*  or more contributor license agreements.  See the NOTICE file
*  distributed with this work for additional information
*  regarding copyright ownership.  The ASF licenses this file
*  to you 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 = (function () {
  'use strict';
  // Dependencies
  var common = require('./common.js');
  var fill = common.helper.fill;

  var bookmark = {};

  // Shared variables
  var qfirst;
  var qall;
  var $el;

  bookmark.run = function (app) {
    qfirst = function (sel) { return app.document.querySelector(sel); };
    qall = function (sel) { return app.document.querySelectorAll(sel); };

    describe('bookmark module testing', function () {

      beforeAll(function (done) {
        // Login and go to bookmarks page
        window.setTimeout(function () {
          fill(app.document.querySelector('input[name=login]'), 'parker');
          fill(app.document.querySelector('input[name=password]'),
            '<PASSWORD>');
          app.document.querySelector('input[type=submit]').click();
          window.setTimeout(function () {
            qfirst('body > section > div p').click();
            window.setTimeout(function () {
              qfirst('table tr th a[title$=mark]').click();
              window.setTimeout(function () {
                qfirst('body > section > div p').click();
                window.setTimeout(function () {
                  qall('table tr th a[href$=view]')[1].click();
                  window.setTimeout(function () {
                    qfirst('section.pads button[title$=mark]').click();
                    window.setTimeout(function () {
                      qfirst('body > section > div p').click();
                      window.setTimeout(function () {
                        qfirst('a[href$=mybookmarks]').click();
                        window.setTimeout(function () {
                          $el = {
                            groups: qall('.panel-primary div.panel-body ul li'),
                            pads: qall('.panel-info div.panel-body ul li')
                          };
                          done();
                        }, 100);
                      }, 100);
                    }, 100);
                  }, 100);
                }, 100);
              }, 100);
            }, 100);
          }, 200);
        }, 200);
      });

      afterAll(function (done) {
        qfirst('body > section p').click();
        window.setTimeout(function () {
          qfirst('.glyphicon-off').parentNode.click();
          window.setTimeout(function () {
            qfirst('body > section p').click();
            done();
          }, 100);
        }, 100);
      });

      it('should have elements as expected', function () {
        expect($el.groups.length).toBe(1);
        expect($el.groups[0].textContent).toBe('Santa Fe');
        expect($el.pads.length).toBe(1);
        expect($el.pads[0].textContent).toBe('Loving Annie');
      });

      it('should allow unmarking', function (done) {
        $el.groups[0].querySelector('button').click();
        $el.pads[0].querySelector('button').click();
        window.setTimeout(function () {
          qfirst('body > section > div p').click();
          var groups = qfirst('.panel-primary p');
          var pads = qfirst('.panel-info p');
          expect(groups.textContent).toMatch('No folder');
          expect(pads.textContent).toMatch('No pad');
          done();
        }, 200);
      });
    });
  };

  return bookmark;

}).call(this);


<|start_filename|>spec/backend/perm.spec.js<|end_filename|>
/**
*  vim:set sw=2 ts=2 sts=2 ft=javascript expandtab:
*
*  Licensed to the Apache Software Foundation (ASF) under one
*  or more contributor license agreements.  See the NOTICE file
*  distributed with this work for additional information
*  regarding copyright ownership.  The ASF licenses this file
*  to you 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.
*/

(function () {
  'use strict';

  var ld = require('lodash');
  var encode = require('js-base64').Base64.encode;
  var jwt = require('jsonwebtoken');

  var specCommon = require('./common.js');
  var perm = require('../../perm.js');
  var auth = require('../../auth.js');
  var user = require('../../model/user.js');
  var group = require('../../model/group.js');
  var pad = require('../../model/pad.js');

  describe('perm', function () {
    var users = {
      parker: {
        login: 'parker',
        password: '<PASSWORD>',
        email: '<EMAIL>'
      },
      jerry: {
        login: 'jerry',
        password: '<PASSWORD>',
        email: '<EMAIL>'
      }
    };
    var groups = {};
    var pads = {};
    var getJwt = function (login) {
      return jwt.sign({
        login: login,
        key: auth.tokens[login].key
      }, auth.secret);
    };

    beforeAll(function (done) {
      specCommon.reInitDatabase(function () {
        user.set(users.parker, function (err, u) {
          if (!err) { users.parker = u; }
          user.set(users.jerry, function (err, u) {
            if (!err) { users.jerry = u; }
            groups.college = {
              name: 'College',
              visibility: 'public',
              admin: users.parker._id,
              users: [users.jerry._id]
            };
            groups.memories = {
              name: 'Memories',
              visibility: 'restricted',
              admin: users.parker._id
            };
            groups.annie = {
              name: 'Annie',
              visibility: 'private',
              password: '<PASSWORD>',
              admin: users.parker._id
            };
            group.set(groups.college, function (err, g) {
              if (!err) { groups.college = g; }
              group.set(groups.memories, function (err, g) {
                if (!err) { groups.memories = g; }
                group.set(groups.annie, function (err, g) {
                  if (!err) { groups.annie = g; }
                  pads.college = {
                    name: 'Lesson 1',
                    group: groups.college._id
                  };
                  pads.collegePrivate = {
                    name: 'Private lesson',
                    group: groups.college._id,
                    visibility: 'private',
                    password: '<PASSWORD>'
                  };
                  pads.memories = {
                    name: 'Our first meet',
                    group: groups.memories._id
                  };
                  pads.memoriesPublic = {
                    name: 'Public meeting',
                    group: groups.memories._id,
                    visibility: 'public',
                    readonly: true
                  };
                  pads.memoriesPrivate = {
                    name: 'Private meeting',
                    group: groups.memories._id,
                    visibility: 'private',
                    password: '<PASSWORD>'
                  };
                  pads.annie = {
                    name: 'Falling in love',
                    group: groups.annie._id
                  };
                  pads.annieOwnPass = {
                    name: 'Falling in love',
                    group: groups.annie._id,
                    visibility: 'private',
                    password: '<PASSWORD>'
                  };
                  pad.set(pads.college, function (err, p) {
                    if (!err) { pads.college = p; }
                    pad.set(pads.collegePrivate, function (err, p) {
                      if (!err) { pads.collegePrivate = p; }
                      pad.set(pads.memories, function (err, p) {
                        if (!err) { pads.memories = p; }
                        pad.set(pads.memoriesPublic, function (err, p) {
                          if (!err) { pads.memoriesPublic = p; }
                          pad.set(pads.memoriesPrivate, function (err, p) {
                            if (!err) { pads.memoriesPrivate = p; }
                            pad.set(pads.annie, function (err, p) {
                              if (!err) { pads.annie = p; }
                              pad.set(pads.annieOwnPass, function (err, p) {
                                if (!err) { pads.annieOwnPass = p; }
                                auth.tokens.parker = users.parker;
                                auth.tokens.jerry = users.jerry;
                                done();
                              });
                            });
                          });
                        });
                      });
                    });
                  });
                });
              });
            });
          });
        });
      });
    });

    afterAll(specCommon.reInitDatabase);

    describe('local function getVarFromReferer', function () {
      var req = { headers: { referer: undefined } };

      it('should return false if no referer', function () {
        expect(perm.fn.getVarFromReferer('fake', req)).toBeFalsy();
      });

      it('should return false if the variable is not found', function () {
        req.headers.referer = 'http://localhost:8042/p/apad-xxxx?somevar=123';
        expect(perm.fn.getVarFromReferer('fake', req)).toBeFalsy();
      });

      it('should return the variable otherwise', function () {
        req.headers.referer = 'http://localhost:8042/p/apad-xxxx?somevar=123';
        expect(perm.fn.getVarFromReferer('somevar', req)).toBe('123');
        req.headers.referer += '&var2=42';
        expect(perm.fn.getVarFromReferer('somevar', req)).toBe('123');
        expect(perm.fn.getVarFromReferer('var2', req)).toBe('42');
      });

    });

    describe('local function check', function () {
      var next = function () { return true; };
      var req = { params: {}, query: {}, headers: { referer: false } };
      var res = {
        status: function (code) { this.code = code; return this; },
        send: function (msg) { this.msg = msg; return this; },
        redirect: function (route) { this.route = route; return this; }
      };

      it('should pass if pad has not been created by mypads', function (done) {
        req.params.pid = 'inexistentOne';
        perm.check(req, res, function () {
          expect(true).toBeTruthy();
          done();
        });
      });

      it('should allow access for admin user', function (done) {
        req.query.auth_token = getJwt('parker');
        req.params.pid = pads.college._id;
        perm.check(req, res, function () {
          expect(res.code).toBeUndefined();
          expect(res.msg).toBeUndefined();
          req.params.pid = pads.memories._id;
          perm.check(req, res, function () {
            expect(res.code).toBeUndefined();
            expect(res.msg).toBeUndefined();
            req.params.pid = pads.annie._id;
            perm.check(req, res, function () {
              expect(res.code).toBeUndefined();
              expect(res.msg).toBeUndefined();
              done();
            });
          });
        });
      });

      it('should allow access to all for public group', function (done) {
        delete req.query.auth_token;
        req.params.pid = pads.college._id;
        perm.check(req, res, function () {
          expect(res.code).toBeUndefined();
          expect(res.msg).toBeUndefined();
          done();
        });
      });

      it('should forbid access for user not invited', function (done) {
        req.params.pid = pads.memories._id;
        perm.check(req, res, next);
        setTimeout(function () {
          expect(res.route).toMatch(req.params.pid);
          delete res.route;
          req.query.auth_token = getJwt('<PASSWORD>');
          perm.check(req, res, next);
          setTimeout(function () {
            expect(res.route).toMatch(req.params.pid);
            done();
          }, 100);
        }, 100);

      });

      it('should allow access for user invited', function (done) {
        delete res.code;
        delete res.msg;
        groups.memories.admin = users.parker._id;
        groups.memories.users = [users.jerry._id];

        group.set(groups.memories, function (err, g) {
          expect(err).toBeNull();
          groups.memories = g;
          req.query.auth_token = getJwt('<PASSWORD>');
          perm.check(req, res, function () {
            expect(res.code).toBeUndefined();
            expect(res.msg).toBeUndefined();
            done();
          });
        });
      });

      it('should forbid access for private without password', function (done) {
        delete res.route;
        req.query.auth_token = getJwt('<PASSWORD>');
        req.params.pid = pads.annie._id;

        perm.check(req, res, next);
        setTimeout(function () {
          expect(res.route).toMatch(req.params.pid);
          done();
        }, 100);
      });

      it('should forbid access for private with bad password', function (done) {
        delete res.route;
        req.query.auth_token = getJwt('<PASSWORD>');
        req.params.pid = pads.annie._id;
        req.query.mypadspassword = '<PASSWORD>';

        perm.check(req, res, next);
        setTimeout(function () {
          expect(res.route).toMatch(req.params.pid);
          done();
        }, 100);
      });

      it('should allow access for private with good password', function (done) {
        delete res.route;
        req.query.auth_token = getJwt('<PASSWORD>');
        req.params.pid = pads.annie._id;
        req.query.mypadspassword = encode('<PASSWORD>');

        perm.check(req, res, function () {
          expect(res.route).toBeUndefined();
          done();
        });
      });

      it('should forbid access to private pad in public group',
        function (done) {
          delete res.route;
          delete req.query.auth_token;
          delete req.query.mypadspassword;
          req.params.pid = pads.collegePrivate._id;

          perm.check(req, res, next);
          setTimeout(function () {
            expect(res.route).toMatch(req.params.pid);
            done();
          }, 100);
        }
      );

      it('should allow access to private pad in public group with password',
        function (done) {
          delete res.route;
          delete req.query.auth_token;
          req.params.pid = pads.collegePrivate._id;
          req.query.mypadspassword = encode('<PASSWORD>');

          perm.check(req, res, function () {
            expect(res.route).toBeUndefined();
            done();
          });
        }
      );

      it('should allow access to private pad with its own password in ' +
        'private group', function (done) {
          delete res.route;
          delete req.query.auth_token;
          req.params.pid = pads.annieOwnPass._id;
          req.query.mypadspassword = encode('<PASSWORD>');
          perm.check(req, res, next);
          setTimeout(function () {
            expect(res.route).toMatch(req.params.pid);
            delete res.route;
            req.query.mypadspassword = encode('<PASSWORD>');
            perm.check(req, res, next);
            setTimeout(function () {
              expect(res.route).toMatch(req.params.pid);
              delete res.route;
              req.query.mypadspassword = encode('<PASSWORD>');
              perm.check(req, res, function () {
                expect(res.route).toBeUndefined();
                done();
              });
            }, 100);
          }, 100);

        }
      );

      it('should allow access to public pad in restricted group',
        function (done) {
          delete res.code;
          delete res.msg;
          delete req.query.auth_token;
          req.params.pid = pads.memoriesPublic._id;
          perm.check(req, res, next);
          setTimeout(function () {
            expect(res.code).toBe(200);
            expect(res.msg).toMatch('Testing only');
            done();
          }, 100);
        }
      );

      it('should allow access to private pad in restricted group with password',
        function (done) {
          delete res.route;
          req.query.auth_token = getJwt('<PASSWORD>');
          req.params.pid = pads.memoriesPrivate._id;
          req.query.mypadspassword = encode('<PASSWORD>');

          perm.check(req, res, function () {
            expect(res.route).toBeUndefined();
            done();
          });
        }
      );

    });

    describe('local function readonly', function () {
      var next = function () { return true; };
      var req = { params: {}, query: {}, headers: { referer: false } };
      var res = {
        status: function (code) { this.code = code; return this; },
        send: function (msg) { this.msg = msg; return this; }
      };

      it('should pass if pad has not been created by mypads', function (done) {
        req.params.pid = 'inexistentOne';
        perm.check(req, res, function () {
          expect(true).toBeTruthy();
          done();
        });
      });

      it('should pass if pad is not readonly', function (done) {
        req.params.pid = pads.college._id;
        perm.check(req, res, function () {
          expect(true).toBeTruthy();
          done();
        });
      });

      it('should return HTML if pad is readonly', function (done) {
        groups.college.readonly = true;
        groups.college.admin = users.parker._id;
        group.set(groups.college, function (err, g) {
          expect(err).toBeNull();
          expect(g.readonly).toBeTruthy();
          groups.college = g;

          req.params.pid = pads.college._id;
          perm.check(req, res, next);
          setTimeout(function () {
            expect(res.code).toBe(200);
            expect(res.msg).toMatch('Testing only');
            delete res.code;
            delete res.msg;
            req.params.pid = pads.memoriesPublic._id;
            perm.check(req, res, next);
            setTimeout(function () {
              expect(res.code).toBe(200);
              expect(res.msg).toMatch('Testing only');
              done();
            }, 100);
          }, 100);
        });
      });

    });

    describe('local function set name and color', function () {
      var req = { params: {}, query: {}, headers: { referer: false } };
      var res = {};

      beforeEach(function () {
        perm.padAndAuthor = {};
      });

      it('should do nothing if user does not want to use its params',
        function (done) {
          expect(ld.isObject(perm.padAndAuthor)).toBeTruthy();
          expect(ld.isEmpty(perm.padAndAuthor)).toBeTruthy();
          auth.tokens.jerry.useLoginAndColorInPads = false;
          perm.setNameAndColor(req, res, function () {
            expect(ld.isObject(perm.padAndAuthor)).toBeTruthy();
            expect(ld.isEmpty(perm.padAndAuthor)).toBeTruthy();
            done();
          });
        }
      );

      it('should use login but not color if no color is defined',
        function (done) {
          expect(ld.isObject(perm.padAndAuthor)).toBeTruthy();
          expect(ld.isEmpty(perm.padAndAuthor)).toBeTruthy();
          req.params.pid = 'azerty';
          req.query.auth_token = getJwt('parker');
          ld.assign(auth.tokens.parker, {
            useLoginAndColorInPads: true,
            color: null
          });
          perm.setNameAndColor(req, res, function () {
            expect(ld.size(ld.keys(perm.padAndAuthor))).toBe(1);
            expect(ld.first(ld.keys(perm.padAndAuthor))).toBe('azerty');
            var opts = perm.padAndAuthor.azerty;
            expect(opts.userName).toBe('parker');
            expect(opts.userColor).toBeUndefined();
            done();
          });
        }
      );

      it('should use login and color if all are defined', function (done) {
        expect(ld.isObject(perm.padAndAuthor)).toBeTruthy();
        expect(ld.isEmpty(perm.padAndAuthor)).toBeTruthy();
        req.params.pid = 'azerty';
        req.query.auth_token = getJwt('parker');
        ld.assign(auth.tokens.parker, {
          useLoginAndColorInPads: true,
          color: '#00ff00'
        });
        perm.setNameAndColor(req, res, function () {
          expect(ld.size(ld.keys(perm.padAndAuthor))).toBe(1);
          expect(ld.first(ld.keys(perm.padAndAuthor))).toBe('azerty');
          var opts = perm.padAndAuthor.azerty;
          expect(opts.userName).toBe('parker');
          expect(opts.userColor).toBe('#00ff00');
          done();
        });
      });

      it('should remember only the last options for a given pad',
        function (done) {
          req.params.pid = 'azerty';
          req.query.auth_token = getJwt('parker');
          ld.assign(auth.tokens.parker, {
            useLoginAndColorInPads: true,
            color: '#00ff00'
          });
          perm.setNameAndColor(req, res, function () {
            expect(ld.size(ld.keys(perm.padAndAuthor))).toBe(1);
            expect(ld.first(ld.keys(perm.padAndAuthor))).toBe('azerty');
            var opts = perm.padAndAuthor.azerty;
            expect(opts.userName).toBe('parker');
            expect(opts.userColor).toBe('#00ff00');
            req.query.auth_token = getJwt('jerry');
            perm.setNameAndColor(req, res, function () {
              expect(ld.isObject(perm.padAndAuthor)).toBeTruthy();
              expect(ld.isEmpty(perm.padAndAuthor)).toBeTruthy();
              done();
            });
          });
        }
      );

    });

  });

}).call(this);


<|start_filename|>spec/frontend/passrecover.spec.js<|end_filename|>
/**
*  vim:set sw=2 ts=2 sts=2 ft=javascript expandtab:
*
*  # Password recovery functional testing
*
*  ## License
*
*x  Licensed to the Apache Software Foundation (ASF) under one
*  or more contributor license agreements.  See the NOTICE file
*  distributed with this work for additional information
*  regarding copyright ownership.  The ASF licenses this file
*  to you 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 = (function () {
  'use strict';
  // Dependencies
  var common = require('./common.js');
  var fill = common.helper.fill;

  var passrecover = {};

  passrecover.run = function (app) {
    // Shared variables
    var $el;
    var qfirst = function (sel) { return app.document.querySelector(sel); };

    describe('password recovery module testing', function () {

      beforeAll(function (done) {
        qfirst('ul.lang li:first-child').click();
        // Go to password recovery
        qfirst('a[href$=passrecover]').click();
        window.setTimeout(function () {
          $el = {
            form: qfirst('form#passrec-form'),
            email: qfirst('input[name=email]'),
            submit: qfirst('input[type=submit]')
          };
          window.setTimeout(done, 100);
        }, 200);
      });

      afterAll(function (done) {
        qfirst('header nav a:first-child').click();
        window.setTimeout(done, 200);
      });

      describe('password recovery from email', function () {

        it('should forbid submision whith no email fill', function () {
          $el.submit.click();
          expect($el.email.checkValidity()).toBeFalsy();
          expect($el.form.checkValidity()).toBeFalsy();
        });

        it('should return an error if inexistent email is filled',
          function (done) {
            fill($el.email, '<EMAIL>');
            $el.submit.click();
            window.setTimeout(function () {
              var $err = qfirst('section.notification div.alert-danger p');
              expect($err.innerHTML).toMatch('User not found');
              $err.click();
              window.setTimeout(done, 100);
            }, 100);
          }
        );

        it('should return an error if mail management has not been configured',
          function (done) {
            fill($el.email, '<EMAIL>');
            $el.submit.click();
            window.setTimeout(function () {
              var $err = qfirst('section.notification div.alert-danger p');
              expect($err.innerHTML).toMatch('Root Url setting has not');
              $err.click();
              window.setTimeout(done, 100);
            }, 100);
          }
        );
      });

      describe('password change', function () {

        beforeAll(function (done) {
          app.window.location.search = '?/passrecover/invalidtoken';
          window.setTimeout(function () {
            qfirst('ul.lang li:first-child').click();
            $el = {
              password: qfirst('input[name=password]'),
              passwordConfirm: qfirst('input[name=passwordConfirm]'),
              submit: qfirst('input[type=submit]')
            };
            window.setTimeout(done, 100);
          }, 400);
        });

        it('should return an error if the token is invalid', function (done) {
            fill($el.password, '<PASSWORD>');
            fill($el.passwordConfirm, '<PASSWORD>');
            $el.submit.click();
            window.setTimeout(function () {
              var $err = qfirst('section.notification div.alert-danger p');
              expect($err.innerHTML).toMatch('Used token contains incorrect');
              $err.click();
              window.setTimeout(done, 100);
            }, 200);
        });
      });

    });
  };

  return passrecover;

}).call(this);


<|start_filename|>helpers.js<|end_filename|>
/**
*  vim:set sw=2 ts=2 sts=2 ft=javascript expandtab:
*
*  # Helpers Module
*
*  This module contains all transversal helpers functions.
*/

module.exports = (function () {
  'use strict';

  // Dependencies
  var ld = require('lodash');

  /**
  * ## lodash mixins
  *
  * Here are lodash user extensions for MyPads.
  *
  * ### isEmail
  *
  * `isEmail` checks if given string is an email or not. It takes a value and
  * returns a boolean.
  *
  * For reference, used regular expression is :
  * /[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?/;
  */

  ld.mixin({ isEmail: function (val) {
    var rg = new RegExp(['[a-z0-9!#$%&\'*+/=?^_`{|}~-]+',
      '(?:\\.[a-z0-9!#$%&\'*+/=?^_`{|}~-]+)*@(?:[a-z0-9]',
      '(?:[a-z0-9-]*[a-z0-9])?\\.)+[a-z0-9]',
      '(?:[a-z0-9-]*[a-z0-9])?'].join(''), 'i');
    return (ld.isString(val) && rg.test(val));
  }});

}).call(this);


<|start_filename|>spec/frontend/group-view.spec.js<|end_filename|>
/**
*  vim:set sw=2 ts=2 sts=2 ft=javascript expandtab:
*
*  # Group view functional testing
*
*  ## License
*
*  Licensed to the Apache Software Foundation (ASF) under one
*  or more contributor license agreements.  See the NOTICE file
*  distributed with this work for additional information
*  regarding copyright ownership.  The ASF licenses this file
*  to you 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 = (function () {
  'use strict';
  // Dependencies
  var common = require('./common.js');
  var fill = common.helper.fill;

  var view = {};

  // Shared variables
  var $el;
  var first;
  var qall;

  view.run = function (app) {

    var login = function (login, password, done) {
      // Login and go to group view page
      app.document.querySelector('header nav a:first-child').click();
      window.setTimeout(function () {
        fill(app.document.querySelector('input[name=login]'), login);
        fill(app.document.querySelector('input[name=password]'), password);
        app.document.querySelector('input[type=submit]').click();
        window.setTimeout(function () {
          app.document.querySelectorAll('a[href$=view]')[4].click();
          window.setTimeout(function () {
            $el = {
              group: first('dl.group'),
              groupQuit: first('button.cancel'),
              padAdd: first('section.pad a'),
              pads: first('section.pad ul'),
              users: first('section.users ul')
            };
            first('body > section > div p').click();
            window.setTimeout(done, 100);
          }, 200);
        }, 200);
      }, 200);
    };

    var logout = function (done) {
      app.document.querySelector('.icon-logout').parentNode.click();
      window.setTimeout(function () {
        app.document.querySelector('body > section p').click();
        done();
      }, 100);
    };

    first = function (sel) { return app.document.querySelector(sel); };
    qall = function (sel) { return app.document.querySelectorAll(sel); };

    describe('group view module testing', function () {

      beforeAll(login.bind(null, 'parker', 'lovesKubiak'));
      afterAll(logout);

      describe('group properties', function () {

        it('should have correct properties', function () {
          var defs = $el.group.querySelectorAll('dt');
          var values = $el.group.querySelectorAll('dd');
          expect(defs[0].textContent).toBe('Pads');
          expect(values[0].textContent).toBe('4');
          expect(defs[1].textContent).toBe('Admins');
          expect(values[1].textContent).toBe('1');
          expect(defs[2].textContent).toBe('Users');
          expect(values[2].textContent).toBe('0');
          expect(defs[3].textContent).toBe('Visibility');
          expect(values[3].textContent).toBe('Public');
          expect(defs[4].textContent).toBe('Readonly');
          expect(values[4].textContent).toBe('no');
          expect(defs[5].textContent).toBe('Tags');
          expect(values[5].textContent).toBe('cool, funky');
        });

      });

      describe('group public guest navigation', function () {
        var url;

        beforeAll(function (done) {
          url = app.window.location.href;
          logout(done);
        });
        afterAll(login.bind(null, 'parker', 'lovesKubiak'));

        it('should allow access to this public group without login',
          function (done) {
            app.window.location.href = url;
            window.setTimeout(function () {
              app.document.querySelector('ul.lang li').click();
              window.setTimeout(function () {
                $el = {
                  group: first('dl.group'),
                  pads: first('section.pad ul'),
                  users: first('section.users p')
                };
                var defs = $el.group.querySelectorAll('dt');
                var values = $el.group.querySelectorAll('dd');
                expect(defs[0].textContent).toBe('Pads');
                expect(values[0].textContent).toBe('4');
                expect(defs[1].textContent).toBe('Admins');
                expect(values[1].textContent).toBe('1');
                expect(defs[2].textContent).toBe('Users');
                expect(values[2].textContent).toBe('0');
                expect(defs[3].textContent).toBe('Visibility');
                expect(values[3].textContent).toBe('Public');
                expect(defs[4].textContent).toBe('Readonly');
                expect(values[4].textContent).toBe('no');
                expect(defs[5].textContent).toBe('Tags');
                expect(values[5].textContent).toBe('cool, funky');
                expect($el.users.textContent).toMatch('you are not allowed');
                done();
              }, 100);
            }, 400);
          }
        );

      });

      describe('group admins and users', function () {

        it('should display expected values', function () {
          var admins = $el.users.querySelectorAll('li');
          expect(admins.length).toBe(1);
          expect(admins[0].textContent).toMatch(/Parker/);
          expect(admins[0].textContent).toMatch(/@lewis.me/);
        });

      });

      describe('group pad management', function () {

        it('should display expected values', function () {
          var pads = $el.pads.querySelectorAll('li span.name');
          expect(pads.length).toBe(4);
          expect(pads[0].textContent).toBe('Loving Annie');
          expect(pads[1].textContent).toBe('Watch sync');
          expect(pads[2].textContent).toBe('Another one');
          expect(pads[3].textContent).toBe('Enhanced one');
        });

        it('should allow pad sorting by creation and name', function (done) {
          var padSortBtns = qall('section.pad p.sort button');
          var creationSort = padSortBtns[0];
          var nameSort = padSortBtns[1];
          window.setTimeout(function () {
            var names = qall('section.pads ul li span.name');
            expect(names[0].textContent).toBe('<NAME>');
            expect(names[1].textContent).toBe('Watch sync');
            expect(names[2].textContent).toBe('Another one');
            expect(names[3].textContent).toBe('Enhanced one');
            creationSort.click();
            window.setTimeout(function () {
              names = qall('section.pads ul li span.name');
              expect(names[0].textContent).toBe('Enhanced one');
              expect(names[1].textContent).toBe('Another one');
              expect(names[2].textContent).toBe('Watch sync');
              expect(names[3].textContent).toBe('Loving Annie');
              nameSort.click();
              window.setTimeout(function () {
                names = qall('section.pads ul li span.name');
                expect(names[0].textContent).toBe('Watch sync');
                expect(names[1].textContent).toBe('Loving Annie');
                expect(names[2].textContent).toBe('Enhanced one');
                expect(names[3].textContent).toBe('Another one');
                nameSort.click();
                window.setTimeout(function () {
                  names = qall('section.pads ul li span.name');
                  expect(names[0].textContent).toBe('Another one');
                  expect(names[1].textContent).toBe('Enhanced one');
                  expect(names[2].textContent).toBe('<NAME>');
                  expect(names[3].textContent).toBe('Watch sync');
                  creationSort.click();
                  window.setTimeout(done, 100);
                }, 100);
              }, 100);
            }, 100);
          }, 100);
        });

        it('should propose pad sharing', function (done) {
          var link;
          app.window.prompt = function (title, val) { link = val; };
          var padShare = app.document
            .querySelectorAll('button[title=Share]');
          padShare[1].click();
          window.setTimeout(function () {
            expect(link).toMatch('/pad/view/loving-annie-[a-z0-9]+$');
            done();
          }, 100);
        });

        it('should allow pad removal', function (done) {
          app.window.confirm = function () { return false; };
          var padRemove = app.document
            .querySelectorAll('a[href*=\'/pad/remove\']');
          padRemove[3].click();
          window.setTimeout(function () {
            var pads = app.document
              .querySelectorAll('section.pad ul li span.name');
            expect(pads.length).toBe(4);
            expect(pads[3].textContent).toBe('Enhanced one');
            app.window.confirm = function () { return true; };
            padRemove = app.document
              .querySelectorAll('a[href*=\'/pad/remove\']');
            padRemove[3].click();
            window.setTimeout(function () {
              pads = app.document
                .querySelectorAll('section.pad ul li span.name');
              expect(pads.length).toBe(3);
              var groupVals = app.document.querySelectorAll('dl.group dd');
              expect(groupVals[0].textContent).toBe('3');
              first('body > section > div p').click();
              window.setTimeout(done, 100);
            }, 100);
          }, 100);
        });

        it('should allow pad marking', function (done) {
          var padMark = first('button[title$=mark]');
          expect(padMark.getAttribute('title')).toBe('Bookmark');
          expect(padMark.querySelector('i').className).toBe('icon-star-empty');
          padMark.click();
          window.setTimeout(function () {
            first('body > section > div p').click();
            padMark = first('button[title$=mark]');
            expect(padMark.getAttribute('title')).toBe('Unmark');
            expect(padMark.querySelector('i').className).toBe('icon-star');
            padMark.click();
            window.setTimeout(function () {
              first('body > section > div p').click();
              padMark = first('button[title$=mark]');
              expect(padMark.getAttribute('title')).toBe('Bookmark');
              expect(padMark.querySelector('i').className)
                .toBe('icon-star-empty');
              window.setTimeout(done, 100);
            }, 100);
          }, 100);
        });

      });

      describe('group sharing', function () {

        it('should allow to share public group MyPads URL', function () {
          var $share = first('section.group h2 button[title=Share]');
          var link;
          app.window.prompt = function (title, val) { link = val; };
          $share.click();
          expect(link).toBeDefined();
          expect(link).toMatch('mypads/group/');
          expect(link).toMatch(/\/view$/);
        });

      });

      describe('group leaving', function () {

        beforeAll(function (done) {
          app.document.querySelector('a[href$=mypads]').click();
          window.setTimeout(function () {
            app.document.querySelectorAll('a[href$=view]')[7].click();
            window.setTimeout(function () {
              $el = { groupQuit: first('button.cancel') };
              window.setTimeout(done, 100);
            }, 200);
          }, 100);
        });

        it('should allow pad quitting', function (done) {
          app.window.confirm = function () { return true; };
          $el.groupQuit.click();
          window.setTimeout(function () {
            app.document.querySelector('body > section p').click();
            var g = app.document.querySelectorAll('ul.group > li');
            expect(g.length).toBe(2);
            done();
          }, 200);
        });

      });

    });

  };

  return view;

}).call(this);


<|start_filename|>model/common.js<|end_filename|>
/**
*  vim:set sw=2 ts=2 sts=2 ft=javascript expandtab:
*
*  # Common for Models
*
*  ## License
*
*  Licensed to the Apache Software Foundation (ASF) under one
*  or more contributor license agreements.  See the NOTICE file
*  distributed with this work for additional information
*  regarding copyright ownership.  The ASF licenses this file
*  to you 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 = (function() {
  'use strict';

  // Dependencies
  var ld      = require('lodash');
  var crypto  = require('crypto');
  var storage = require('../storage.js');

  /**
  * ## Description
  *
  * This module regroups shared functions for several models.
  */

  var common = {};

  /**
  * ### hashPassword
  *
  * `hashPassword` is an asynchronous function that use `crypto.randomBytes` to
  * generate a strong `salt` if needed and return a `sha512` `hash` composed of
  * the `salt` and the given `password`. It takes
  *
  * - an optional `salt` string
  * - the mandatory `password` string
  * - a `callback` function which returns an object with `hash`ed password and
  *   the `salt`.
  */

  common.hashPassword = function (salt, password, callback) {
    crypto.randomBytes(40, function (ex, buf) {
      if (ex) { return callback(ex); }
      salt       = salt || buf.toString('hex');
      var sha512 = crypto.createHash('sha512');
      sha512.update(salt);
      callback(null, {
        salt: salt,
        hash: sha512.update(password).digest('hex')
      });
    });
  };

  /**
  * ### addSetInit
  *
  * This function throws errors for common parameters missing or mistyped.
  * It takes three arguments :
  *
  * - a `params` JS object
  * - a `callback` function
  * - a n optional `strFields` array, with fields of the `params` objects that
  *   must be not empty strings with at most 100 characters (ueberdb limit for
  *   SQL backends)
  */

  common.addSetInit = function (params, callback, strFields) {
    if (!ld.isObject(params)) {
      throw new TypeError('BACKEND.ERROR.TYPE.PARAMS_REQUIRED');
    }
    if (!ld.isFunction(callback)) {
      throw new TypeError('BACKEND.ERROR.TYPE.CALLBACK_FN');
    }
    if (!ld.isUndefined(params._id)) {
      if (!ld.isString(params._id) || (ld.isEmpty(params._id))) {
        throw new TypeError('BACKEND.ERROR.TYPE.ID_STR');
      }
    }
    if (strFields) {
      var isFS = function (s) { return (ld.isString(s) && !ld.isEmpty(s)); };
      ld.forEach(strFields, function (s) {
        if (!isFS(params[s])) {
          throw new TypeError('BACKEND.ERROR.TYPE.PARAM_STR');
        }
        if (s.length > 100) {
          throw new TypeError('BACKEND.ERROR.TYPE.STR_100');
        }
      });
    }
  };

  /**
  * ### checkExistence
  *
  * `checkExistence` is an asynchronous function that takes
  *
  * - a database `key`
  * - a `callback` function, returnning an *Error* or a boolean for existence
  */

  common.checkExistence = function (key, callback) {
    storage.db.get(key, function(err, res) {
      if (err) { return callback(err); }
      return callback(null, !!res);
    });
  };

  /**
  * ### checkMultiExist
  *
  * `checkMultiExist` is an asynchronous function that uses
  * `common.checkExistence` to check multiple keys.
  * It takes :
  *
  * - an array of `keys`
  * - a `callback` function, returning an Error or *null* and a boolean for
  *   existence.
  *
  * At the first not found record, callback will be called.
  * FIXME: TCO ?
  */

  common.checkMultiExist = function (keys, callback) {
    var done = function (err, res) {
      if (keys.length) {
        if (err) { return callback(err); }
        if (!res) { return callback(null, res); }
        common.checkExistence(keys.pop(), done);
      } else {
        return callback(null, res);
      }
    };
    done(null, true);
  };

  /**
  *  ### getDel
  *
  *  Model common reading
  *
  *  This function takes mandatory arguments
  *
  *  - a `del` boolean, to add a second step, removal, in the case of *true*
  *  - a `PREFIX`, used to compute real key
  *  - a `key`, the unique identifier of the object
  *  - a `callback` function, that returns an error if there is a problem or if
  *  the key is not found. In the other case, it returns *null* if `del` or
  *  *null* plus the model object.
  */

  common.getDel = function (del, PREFIX, key, callback) {
    if (!ld.isString(key)) {
      throw new TypeError('BACKEND.ERROR.TYPE.KEY_STR');
    }
    if (!ld.isFunction(callback)) {
      throw new TypeError('BACKEND.ERROR.TYPE.CALLBACK_FN');
    }
    key = PREFIX + key;
    storage.db.get(key, function (err, obj) {
      if (err) { return callback(err); }
      if (ld.isUndefined(obj) || ld.isNull(obj)) {
        return callback(new Error('BACKEND.ERROR.CONFIGURATION.KEY_NOT_FOUND'));
      }
      if (!del) {
        return callback(null, obj);
      } else {
        storage.db.remove(key, function (err) {
          if (err) { return callback(err); }
          return callback(null, obj);
        });
      }
    });
  };

  return common;

}).call(this);


<|start_filename|>templates/mail_es.json<|end_filename|>
{
    "ACCOUNT_CONFIRMATION": "<%= login %>, te has suscrito a <%= title %>. Esta instancia de etherpad requiere una confirmación por correo electrónico para activar su cuenta. Para continuar, por favor, ve a esta dirección web: : <%= url %>.Esta URL solo será válida durante<%= duration %>minutos. Pasado ese tiempo, debes ponerte en contacto con el administrador.",
    "ACCOUNT_CONFIRMATION_SUBJECT": "Confirmación de la cuenta en <%= title %>",
    "PASSRECOVER": "<%= login %>, una solicitud de cambio de contraseña en <%= title %> se ha iniciado para su inicio de sesión. Si desea confirmar su cambio de contraseña, vaya a esta URL : <%= url %> . Si esta solicitud no proviene de usted, simplemente ignore este correo electrónico. La dirección dejará de ser válida en <%= duration %> minutos.",
    "PASSRECOVER_SUBJECT": "Recuperación de la contraseña en <%= title %>"
}


<|start_filename|>frontend/js/modules/home.js<|end_filename|>
/**
*  vim:set sw=2 ts=2 sts=2 ft=javascript expandtab:
*
*  # Home module
*
*  ## License
*
*  Licensed to the Apache Software Foundation (ASF) under one
*  or more contributor license agreements.  See the NOTICE file
*  distributed with this work for additional information
*  regarding copyright ownership.  The ASF licenses this file
*  to you 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.
*
*  ## Description
*
*  This module contains the home of MyPads.
*/

module.exports = (function () {
  // Global dependencies
  var m = require('mithril');

  // Local dependencies
  var auth   = require('../auth.js');
  var layout = require('./layout.js');

  var home = {
    controller: function () {
      if (auth.isAdmin()) {
        return m.route('/admin');
      }
      if (auth.isAuthenticated()) {
        return m.route('/mypads');
      }
      m.route('/login');
    },
    view: function () {
      return layout.view(m('p', 'empty home'), m('p', 'empty aside'));
    }
  };

  return home;
}).call(this);


<|start_filename|>spec/backend/model/user.spec.js<|end_filename|>
/**
*  Licensed to the Apache Software Foundation (ASF) under one
*  or more contributor license agreements.  See the NOTICE file
*  distributed with this work for additional information
*  regarding copyright ownership.  The ASF licenses this file
*  to you 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.
*/

(function () {
  'use strict';
  var ld = require('lodash');
  var cuid = require('cuid');
  var specCommon = require('../common.js');
  var storage = require('../../../storage.js');
  var conf = require('../../../configuration.js');
  var common = require('../../../model/common.js');
  var group = require('../../../model/group.js');
  var user = require('../../../model/user.js');
  var userCache = require('../../../model/user-cache.js');
  var UPREFIX = storage.DBPREFIX.USER;
  var CPREFIX = storage.DBPREFIX.CONF;

  describe('userCache', function () {
    beforeAll(specCommon.reInitDatabase);
    afterAll(specCommon.reInitDatabase);

    describe('init', function () {

      beforeAll(function (done) {
        specCommon.reInitDatabase(function () {
          var genId = function () { return UPREFIX + cuid(); };
          var kv = {};
          kv[genId()] = { login: 'parker', email: '<EMAIL>' };
          kv[genId()] = { login: 'kubiak', email: '<EMAIL>' };
          storage.fn.setKeys(kv, done);
        });
      });
      afterAll(specCommon.reInitDatabase);

      it('should populate the userCache.logins and userCache.emails fields',
        function (done) {
          userCache.init(function (err) {
            expect(err).toBeNull();
            expect(ld.isObject(userCache.logins)).toBeTruthy();
            expect(ld.size(userCache.logins)).toBe(2);
            expect(ld.includes(ld.keys(userCache.logins), 'parker')).toBeTruthy();
            expect(ld.includes(ld.keys(userCache.logins), 'kubiak')).toBeTruthy();
            expect(ld.isObject(userCache.emails)).toBeTruthy();
            expect(ld.size(userCache.emails)).toBe(2);
            var ldimails = ld.partial(ld.includes, ld.keys(userCache.emails));
            expect(ldimails('<EMAIL>')).toBeTruthy();
            expect(ldimails('<EMAIL>')).toBeTruthy();
            done();
          });
        }
      );

    });
  });

  describe('user', function () {
    beforeAll(specCommon.reInitDatabase);
    afterAll(specCommon.reInitDatabase);

    describe('creation', function () {
      beforeAll(function (done) {
        specCommon.reInitDatabase(function () {
          conf.init(done);
        });
      });
      afterAll(specCommon.reInitDatabase);

      it('should return a TypeError and a message if either login, password' +
        ' or email aren\'t given; nor callback function', function () {
        expect(user.set).toThrow();
        expect(ld.partial(user.set, { another: 'object' })).toThrow();
        expect(ld.partial(user.set, { login: 'Johnny' })).toThrow();
        expect(ld.partial(user.set, { password: '<PASSWORD>' })).toThrow();
        expect(ld.partial(user.set, { login: 'john', password: '<PASSWORD>' }))
          .toThrow();
        var p = { login: 'john', password: '<PASSWORD>', email: '<EMAIL>' };
        expect(ld.partial(user.set, p)).toThrow();
        expect(ld.partial(user.set, { login: 'john', password: '<PASSWORD>' },
          ld.noop)).toThrow();
        p.email = 'invalidMail';
        expect(ld.partial(user.set, p, ld.noop)).toThrow();
      });

      it('should return an Error to the callback if password size is not' +
        ' appropriate', function (done) {
        user.set({ login: 'bob', password: '1', email: '<EMAIL>'},
          function (err) {
            expect(ld.isError(err)).toBeTruthy();
            done();
          }
        );
      });

      it('should accept any creation if login & password & email are fixed',
        function (done) {
          user.set({
            login: 'parker',
            password: '<PASSWORD>',
            firstname: 'Parker',
            lastname: 'Lewis',
            email: '<EMAIL>'
          }, function (err, u) {
            expect(err).toBeNull();
            expect(u._id).toBeDefined();
            expect(ld.startsWith(u._id, 'parker-')).toBeTruthy();
            expect(ld.isNumber(u.ctime)).toBeTruthy();
            expect(u.active).toBeTruthy();
            expect(u.active).not.toBe(conf.get('checkMails'));
            expect(u.login).toBe('parker');
            expect(ld.isObject(u.password)).toBeTruthy();
            expect(u.firstname).toBe('Parker');
            expect(u.lastname).toBe('Lewis');
            expect(ld.isString(u.organization)).toBeTruthy();
            expect((ld.isArray(u.groups) && ld.isEmpty(u.groups))).toBeTruthy();
            expect(ld.isObject(u.bookmarks)).toBeTruthy();
            expect(ld.isArray(u.bookmarks.groups)).toBeTruthy();
            expect(ld.isArray(u.bookmarks.pads)).toBeTruthy();
            var okUls = (ld.isObject(u.userlists) && ld.isEmpty(u.userlists));
            expect(okUls).toBeTruthy();
            expect(ld.includes(ld.values(userCache.logins), u._id)).toBeTruthy();
            expect((userCache.logins[u.login])).toBe(u._id);
            expect(ld.includes(ld.values(userCache.emails), u._id)).toBeTruthy();
            expect((userCache.emails[u.email])).toBe(u._id);
            done();
          });
        }
      );

      it('should ignore fixing groups and userlists for a valid creation',
        function (done) {
          user.set({
            login: 'grace',
            password: '<PASSWORD>',
            email: '<EMAIL>',
            groups: ['one', 'two'],
            userlists: { uidxxxx: { name: 'u1', users: [] } }
          }, function (err, u) {
            expect(err).toBeNull();
            expect(u._id).toBeDefined();
            expect(u.login).toBe('grace');
            expect(ld.isObject(u.password)).toBeTruthy();
            expect((ld.isArray(u.groups) && ld.isEmpty(u.groups))).toBeTruthy();
            var okUls = (ld.isObject(u.userlists) && ld.isEmpty(u.userlists));
            expect(okUls).toBeTruthy();
            expect(ld.includes(ld.values(userCache.logins), u._id)).toBeTruthy();
            expect((userCache.logins[u.login])).toBe(u._id);
            expect((userCache.emails[u.email])).toBe(u._id);
            done();
          });
        }
      );

      it('should deny usage of an existing login', function (done) {
        var p = {
          login: 'parker',
          password: '<PASSWORD>',
          email: '<EMAIL>'
        };
        user.set(p, function (err, u) {
          expect(ld.isError(err)).toBeTruthy();
          expect(u).toBeUndefined();
          done();
        });
      });

      it('should deny usage of an existing email', function (done) {
        user.set({
          login: 'kubiak',
          password: '<PASSWORD>',
          email: '<EMAIL>' }, function (err, u) {
            expect(ld.isError(err)).toBeTruthy();
            expect(u).toBeUndefined();
            done();
          }
        );
      });
    });

    describe('edition', function () {
      var mikey;
      beforeAll(function (done) {
        conf.init(function () {
          mikey = {
            login: 'mikey',
            password: '<PASSWORD>',
            email: '<EMAIL>'
          };
          user.set(mikey, function (err, u) {
            if (err) { console.log(err); }
            mikey = u;
            done();
          });
        });
      });

      it('should return a TypeError and a message if _id is not given, ' +
       'nor callback function', function () {
        expect(user.set).toThrow();
        expect(ld.partial(user.set, { another: 'object' })).toThrow();
        expect(ld.partial(user.set, { _id: 'Johnny' })).toThrow();
        expect(ld.partial(user.set, { another: 'object' }, ld.noop)).toThrow();
      });

      it('should return an Error if the user does not already exist',
        function (done) {
          var p = { _id: 'inex', login: 'i', password: 'p', email: '<EMAIL>' };
          user.set(p,
            function (err, u) {
              expect(ld.isError(err)).toBeTruthy();
              expect(u).toBeUndefined();
              done();
            }
          );
        }
      );

      it('should return an Error to the callback if password size is not' +
        ' appropriate', function (done) {
          var p = {
            login: 'bob',
            password: '1',
            email: '<EMAIL>'
          };
          user.set(p, function (err, res) {
            expect(ld.isError(err)).toBeTruthy();
            expect(res).toBeUndefined();
            done();
          }
        );
      });

      it('should allow setting of an existing user, and gets back its groups,' +
        ' bookmarks, userlists, active and ctime', function (done) {
          group.set({ name: 'g1', admin: mikey._id }, function (err) {
            expect(err).toBeNull();
            var opts = { crud: 'add', login: 'mikey', name: 'u1' };
            user.userlist(opts, function (err) {
              expect(err).toBeNull();
              user.set({
                _id: mikey._id,
                login: 'mikey',
                password: '<PASSWORD>',
                email: '<EMAIL>',
                firstname: 'Michael',
                lastname: 'Randall'
              },
                function (err, u) {
                  expect(err).toBeNull();
                  expect(ld.startsWith(u._id, 'mikey-')).toBeTruthy();
                  expect(ld.isNumber(u.ctime)).toBeTruthy();
                  expect(u.login).toBe('mikey');
                  expect(u.email).toBe('<EMAIL>');
                  expect(u.firstname).toBe('Michael');
                  expect(u.lastname).toBe('Randall');
                  expect(ld.isArray(u.groups)).toBeTruthy();
                  expect(u.groups.length).toBe(1);
                  expect(ld.isObject(u.bookmarks)).toBeTruthy();
                  expect(ld.isObject(u.userlists)).toBeTruthy();
                  expect(ld.size(u.userlists)).toBe(1);
                  expect(u.active).toBeTruthy();
                  expect(u.active).not.toBe(conf.get('checkMails'));
                  done();
                }
              );
            });
          });
        }
      );

    });
  });

  describe('user get', function () {
    beforeAll(function (done) {
      specCommon.reInitDatabase(function () {
        user.set({
          login: 'parker',
          password: '<PASSWORD>',
          firstname: 'Parker',
          lastname: 'Lewis',
          email: '<EMAIL>'
        }, done);
      });
    });
    afterAll(specCommon.reInitDatabase);

    it('should throw errors if arguments are not provided as expected',
      function () {
        expect(user.get).toThrow();
        expect(ld.partial(user.get, 123)).toThrow();
      }
    );

    it('should return an Error if the user is not found', function (done) {
      user.get('inexistent', function (err, u) {
        expect(ld.isError(err)).toBeTruthy();
        expect(u).toBeUndefined();
        done();
      });
    });

    it('should return the user otherwise from login as email', function (done) {
      user.get('parker', function (err, u) {
        expect(err).toBeNull();
        var parkerId = u._id;
        expect(parkerId).toBeDefined();
        expect(ld.startsWith(parkerId, 'parker-')).toBeTruthy();
        expect(u.login).toBe('parker');
        expect(ld.isObject(u.password)).toBeTruthy();
        expect(u.firstname).toBe('Parker');
        expect(u.lastname).toBe('Lewis');
        expect(u.email).toBe('<EMAIL>');
        expect(ld.isString(u.email)).toBeTruthy();
        user.get('<EMAIL>', function (err, u) {
          expect(err).toBeNull();
          expect(u._id).toBe(parkerId);
          expect(u.login).toBe('parker');
          done();
        });
      });
    });
  });

  describe('user del', function () {
    beforeAll(function (done) {
      specCommon.reInitDatabase(function () {
        user.set({
          login: 'parker',
          password: '<PASSWORD>',
          firstname: 'Parker',
          lastname: 'Lewis',
          email: '<EMAIL>'
        }, function (err, parker) {
          if (err) { console.log(err); }
          user.set({
            login: 'frank',
            password: '<PASSWORD>',
            email: '<EMAIL>'
          }, function (err, frank) {
            if (err) { console.log(err); }
            group.set({
              name: 'withFrank',
              admin: parker._id,
              admins: [frank._id]
            }, function (err) {
                if (err) { console.log(err); }
                group.set({ name: 'college', admin: parker._id }, done);
              }
            );
          });
        });
      });
    });
    afterAll(specCommon.reInitDatabase);

    it('should throw errors if arguments are not provided as expected',
      function () {
        expect(user.del).toThrow();
        expect(ld.partial(user.del, 123)).toThrow();
        expect(ld.partial(user.del, 'key')).toThrow();
        expect(ld.partial(user.del, 'key', 'notAFunc')).toThrow();
      }
    );

    it('should return an Error if the user is not found', function (done) {
      user.del('inexistent', function (err, u) {
        expect(ld.isError(err)).toBeTruthy();
        expect(u).toBeUndefined();
        done();
      });
    });

    it('should delete the user otherwise and pops it from shared groups, ' +
      'and removes from unique owner groups',
      function (done) {
        user.del('parker', function (err, _u) {
          expect(err).toBeNull();
          expect(_u).toBeDefined();
          expect(_u.login).toBe('parker');
          expect(userCache.logins.parker).toBeUndefined();
          expect(userCache.emails['<EMAIL>']).toBeUndefined();
          user.get('parker', function (err, u) {
            expect(ld.isError(err)).toBeTruthy();
            expect(u).toBeUndefined();
            group.get(_u.groups[0], function (err, g) {
              expect(err).toBeNull();
              expect(ld.includes(g.admins, _u._id)).toBeFalsy();
              group.get(_u.groups[1], function (err, g) {
                expect(err).toMatch('KEY_NOT_FOUND');
                expect(g).toBeUndefined();
                done();
              });
            });
          });
        });
      }
    );
  });

  describe('user mark', function () {

    var parker;

    beforeAll(function (done) {
      specCommon.reInitDatabase(function () {
        user.set({
          login: 'parker',
          password: '<PASSWORD>',
          firstname: 'Parker',
          lastname: 'Lewis',
          email: '<EMAIL>'
        }, function (err, u) {
          if (err) { console.log(err); }
          parker = u;
          done();
        });
      });
    });

    afterAll(specCommon.reInitDatabase);

    it('should throw errors if arguments are not provided as expected',
      function () {
        expect(user.mark).toThrow();
        expect(ld.partial(user.mark, 123)).toThrow();
        expect(ld.partial(user.mark, 'key')).toThrow();
        expect(ld.partial(user.mark, 'key', 'badType')).toThrow();
        expect(ld.partial(user.mark, 'login', 'pads')).toThrow();
        expect(ld.partial(user.mark, 'login', 'pads', 'id')).toThrow();
        expect(ld.partial(user.mark, 'login', 'pads', 'id', 'notAFn'))
          .toThrow();
      }
    );

    it('should return an Error if the user is not found', function (done) {
      user.mark('inexistent', 'pads', 'pid', function (err) {
        expect(ld.isError(err)).toBeTruthy();
        expect(err).toMatch('USER.NOT_FOUND');
        done();
      });
    });

    it('should return an Error if the bookmark id is not found',
      function (done) {
        user.mark('parker', 'pads', 'pid', function (err) {
          expect(ld.isError(err)).toBeTruthy();
          expect(err).toMatch('BOOKMARK_NOT_FOUND');
          done();
        });
      }
    );

    it('should accept bookmarking and unmarking otherwise', function (done) {
      group.set({ name: 'g1', admin: parker._id }, function (err, g) {
        user.mark('parker', 'groups', g._id, function (err) {
          expect(err).toBeNull();
          user.get('parker', function (err, u) {
            expect(err).toBeNull();
            expect(ld.size(u.bookmarks.groups)).toBe(1);
            expect(ld.first(u.bookmarks.groups)).toBe(g._id);
            expect(ld.isEmpty(u.bookmarks.pads)).toBeTruthy();
            user.mark('parker', 'groups', g._id, function (err) {
              expect(err).toBeNull();
              user.get('parker', function (err, u) {
                expect(err).toBeNull();
                expect(ld.isEmpty(u.bookmarks.groups)).toBeTruthy();
                done();
              });
            });
          });
        });
      });
    });

  });

  describe('user userlists', function () {

    var parker;

    beforeAll(function (done) {
      specCommon.reInitDatabase(function () {
        user.set({
          login: 'parker',
          password: '<PASSWORD>',
          firstname: 'Parker',
          lastname: 'Lewis',
          email: '<EMAIL>'
        }, function (err, u) {
          if (err) { console.log(err); }
          parker = u;
          var jerry = {
            login: 'jerry',
            password: '<PASSWORD>',
            email: '<EMAIL>'
          };
          user.set(jerry, function (err) {
            if (err) { console.log(err); }
            var mikey = {
              login: 'mikey',
              password: '<PASSWORD>',
              email: '<EMAIL>'
            };
            user.set(mikey, function (err) {
              if (err) { console.log(err); }
              done();
            });
          });
        });
      });
    });

    afterAll(specCommon.reInitDatabase);

    it('should throw errors if params are incorrect', function () {
      expect(user.userlist).toThrow();
      expect(ld.partial(user.userlist, {})).toThrowError(/CALLBACK/);
      expect(ld.partial(user.userlist, { crud: 'aloa' }, ld.noop))
        .toThrowError(/USERLIST_CRUD/);
      expect(ld.partial(user.userlist, { crud: 'set' }, ld.noop))
        .toThrowError(/USERLIST_ID/);
    });

    it('should throw an error for add if the name is not given', function () {
      var opts = { crud: 'add', login: 'parker', name: undefined };
      expect(ld.partial(user.userlist, opts, ld.noop))
        .toThrowError(/USERLIST_NAME/);
    });

    it('should return an error if the login is not found', function (done) {
      var opts = { crud: 'add', name: 'friends', login: 'inexistent' };
      user.userlist(opts, function (err) {
        expect(ld.isError(err)).toBeTruthy();
        expect(err).toMatch('NOT_FOUND');
        done();
      });
    });

    it('should return an error for set and del if the ulistid doesnt exist',
      function (done) {
        var opts = { crud: 'del', login: 'parker', ulistid: 'inexistent' };
        user.userlist(opts, function (err) {
          expect(ld.isError(err)).toBeTruthy();
          expect(err).toMatch('NOT_FOUND');
          done();
        });
      }
    );

    it('should return an error for set if no uids or no name are given',
      function (done) {
        var opts = {
          crud: 'set',
          login: 'parker',
          ulistid: 'shouldBeRealUid',
          uids: undefined,
          name: undefined
        };
        user.userlist(opts, function (err) {
          expect(ld.isError(err)).toBeTruthy();
          expect(err).toMatch('USERLIST_SET_PARAMS');
          done();
        });
      }
    );

    it('should create a new userlist', function (done) {
      var opts = { crud: 'add', login: 'parker', name: 'friends' };
      user.userlist(opts, function (err, u) {
        expect(err).toBeNull();
        expect(ld.startsWith(ld.keys(u.userlists)[0], 'friends-')).toBeTruthy();
        var ulists = ld.values(u.userlists);
        expect(ld.size(ulists)).toBe(1);
        var ul = ld.first(ulists);
        expect(ul.name).toBe('friends');
        expect(ld.isArray(ul.uids)).toBeTruthy();
        expect(ld.size(ul.uids)).toBe(0);
        parker = u;
        done();
      });
    });

    it('should update a list name but not the uid', function (done) {
      var ulistid = ld.first(ld.keys(parker.userlists));
      var opts = {
        crud: 'set',
        login: 'parker',
        ulistid: ulistid,
        name: 'Good friends'
      };
      user.userlist(opts, function (err, u) {
        expect(err).toBeNull();
        var ulists = ld.values(u.userlists);
        expect(ld.startsWith(ld.keys(u.userlists)[0], 'friends-')).toBeTruthy();
        expect(ld.size(ulists)).toBe(1);
        var ul = ld.first(ulists);
        expect(ul.name).toBe('Good friends');
        expect(ld.isArray(ul.uids)).toBeTruthy();
        expect(ld.size(ul.uids)).toBe(0);
        expect(ld.isArray(ul.users)).toBeTruthy();
        expect(ld.size(ul.users)).toBe(0);
        parker = u;
        done();
      });
    });

    it('should update a list with filtered uids', function (done) {
      var ulistid = ld.first(ld.keys(parker.userlists));
      var opts = {
        crud: 'set',
        login: 'parker',
        ulistid: ulistid,
        uids: [ userCache.logins.mikey, userCache.logins.jerry, 'fakeOne' ]
      };
      user.userlist(opts, function (err, u) {
        expect(err).toBeNull();
        var ulists = ld.values(u.userlists);
        expect(ld.size(ulists)).toBe(1);
        var ul = ld.first(ulists);
        expect(ul.name).toBe('Good friends');
        expect(ld.isArray(ul.uids)).toBeTruthy();
        expect(ld.size(ul.uids)).toBe(2);
        expect(ul.uids[0]).toBe(userCache.logins.mikey);
        expect(ul.uids[1]).toBe(userCache.logins.jerry);
        expect(ld.size(ul.users)).toBe(2);
        expect(ul.users[0].login).toBe('mikey');
        parker = u;
        done();
      });
    });

    it('should return detailed lists for a given login', function (done) {
      user.userlist({ crud: 'get', login: 'jerry' }, function (err, u) {
        expect(err).toBeNull();
        expect(ld.size(u.userlists)).toBe(0);
        user.userlist({ crud: 'get', login: 'parker' }, function (err, u) {
          expect(ld.size(u.userlists)).toBe(1);
          var ul = ld.first(ld.values(u.userlists));
          expect(ul.name).toBe('Good friends');
          expect(ld.size(ul.uids)).toBe(2);
          expect(ld.isArray(ul.users)).toBeTruthy();
          expect(ld.size(ul.users)).toBe(2);
          expect(ul.users[0].login).toBe('mikey');
          expect(ul.users[0].email).toBeDefined();
          expect(ul.users[0].firstname).toBeDefined();
          expect(ul.users[0].lastname).toBeDefined();
          expect(ul.users[0].password).toBeUndefined();
          expect(ul.users[1].login).toBe('jerry');
          done();
        });
      });
    });

    it('should delete a list', function (done) {
      var ulistid = ld.first(ld.keys(parker.userlists));
      var opts = { crud: 'del', login: 'parker', ulistid: ulistid };
      user.userlist(opts, function (err, u) {
        expect(err).toBeNull();
        expect(ld.size(u.userlists)).toBe(0);
        parker = u;
        done();
      });
    });

  });

  describe('user functions', function() {

    describe('getPasswordConf', function () {

      it('should retrieve min and max length for password', function (done) {
        user.fn.getPasswordConf(function (err, results) {
          expect(err).toBeNull();
          var rkeys = ld.keys(results);
          expect(ld.contains(rkeys, CPREFIX + 'passwordMin'))
            .toBeTruthy();
          expect(ld.contains(rkeys, CPREFIX + 'passwordMax'))
            .toBeTruthy();
          done();
        });
      });

    });

    describe('checkPasswordLength', function () {
      var params = {};

      beforeAll(function () {
        params[CPREFIX + 'passwordMin'] = 4;
        params[CPREFIX + 'passwordMax'] = 8;
      });

      it('should return an Error if password size is not appropriate',
        function () {
          var cPL = user.fn.checkPasswordLength;
          expect(ld.isError(cPL('a', params))).toBeTruthy();
      });

      it('should return nothing if password size is good', function () {
        expect(user.fn.checkPasswordLength('123456', params)).toBeUndefined();
      });
    });

    describe('genPassword', function () {

      it('should check and returns an updated user object for user creation',
        function (done) {
          var params = {
            login: 'brian',
            password: '<PASSWORD>',
            organization: 'etherInc'
          };
          user.fn.genPassword(null, params, function (err, u) {
            expect(err).toBeNull();
            expect(ld.isObject(u)).toBeTruthy();
            expect(u.login).toBe('brian');
            expect(ld.isObject(u.password)).toBeTruthy();
            expect(ld.isString(u.password.hash)).toBeTruthy();
            expect(ld.isEmpty(u.password.hash)).toBeFalsy();
            expect(ld.isString(u.password.salt)).toBeTruthy();
            expect(ld.isEmpty(u.password.salt)).toBeFalsy();
            done();
          });
        }
      );

      it('should keep the password object for update with the same pass',
        function (done) {
          common.hashPassword(null, 'verySecret', function (err, pass) {
            var old = {
              login: 'brian',
              password: <PASSWORD>,
              organization: 'etherInc'
            };
            var params = ld.clone(old);
            params.password = '<PASSWORD>';
            user.fn.genPassword(old, params, function (err, u) {
              expect(err).toBeNull();
              expect(ld.isObject(u)).toBeTruthy();
              expect(u.login).toBe('brian');
              expect(ld.isObject(u.password)).toBeTruthy();
              expect(ld.isString(u.password.hash)).toBeTruthy();
              expect(u.password.hash).toBe(pass.hash);
              expect(ld.isString(u.password.salt)).toBeTruthy();
              expect(u.password.salt).toBe(pass.salt);
              done();
            });
          });
        }
      );

      it('should check and update the password object for update with new pass',
        function (done) {
          common.hashPassword(null, 'verySecret', function (err, pass) {
            var old = {
              login: 'brian',
              password: <PASSWORD>,
              organization: 'etherInc'
            };
            var params = ld.clone(old);
            params.password = '<PASSWORD>';
            user.fn.genPassword(old, params, function (err, u) {
              expect(err).toBeNull();
              expect(ld.isObject(u)).toBeTruthy();
              expect(u.login).toBe('brian');
              expect(ld.isObject(u.password)).toBeTruthy();
              expect(ld.isString(u.password.hash)).toBeTruthy();
              expect(u.password.hash).not.toBe(pass.hash);
              expect(ld.isString(u.password.salt)).toBeTruthy();
              expect(u.password.salt).not.toBe(pass.salt);
              done();
            });
          });
        }
      );
    });

    describe('assignProps', function () {

      it ('should respect given properties if strings and relevant',
        function () {
          var params = {
            _id: 'aMadeID',
            login: 'brian',
            password: '<PASSWORD>',
            organization: 'etherInc',
            firstname: true,
            irrelevant: 123,
            email: '<EMAIL>',
            lang: 'fr',
            useLoginAndColorInPads: false
          };
          var u = user.fn.assignProps(params);
          expect(u._id).toBe('aMadeID');
          expect(u.login).toBe('brian');
          expect(u.password).toBe('<PASSWORD>');
          expect(u.organization).toBe('etherInc');
          expect(u.email).toBe('<EMAIL>');
          expect(u.lang).toBe('fr');
          var uf = u.firstname;
          var ul = u.lastname;
          expect(ld.isString(uf) && ld.isEmpty(uf)).toBeTruthy();
          expect(ld.isString(ul) && ld.isEmpty(ul)).toBeTruthy();
          expect(ld.isArray(u.groups)).toBeTruthy();
          expect(ld.isEmpty(u.groups)).toBeTruthy();
          expect(ld.isEmpty(u.color)).toBeTruthy();
          expect(u.useLoginAndColorInPads).toBeFalsy();
          expect(u.irrelevant).toBeUndefined();
          params.email = 'notenamail@@@@';
          params.lang = 'notAValidOne';
          u = user.fn.assignProps(params);
          expect(u.lang).toBe('en');
        }
      );
    });

    describe('checkLogin', function () {
      beforeAll(function () {
        userCache.logins = {
          'parker': '087654321',
          'jerry': '<PASSWORD>'
        };
      });
      afterAll(function (done) { userCache.init(done); });

      it('should return an error if add and existing login or id',
        function (done) {
          var u = { login: 'l', _id: '087654321' };
          user.fn.checkLogin(undefined, u, function (err) {
            expect(ld.isError(err)).toBeTruthy();
            expect(err).toMatch('USER.ALREADY_EXISTS');
            u = { login: 'jerry', _id: 'anotherone' };
            user.fn.checkLogin(undefined, u, function (err) {
              expect(ld.isError(err)).toBeTruthy();
              expect(err).toMatch('USER.ALREADY_EXISTS');
              done();
            });
          });
        }
      );

      it('should pay attention to login when edit, returns null',
        function (done) {
          var u = { login: 'parker', _id: '087654321', email: '<EMAIL>' };
          user.fn.checkLogin('087654321', u, function (err) {
            expect(err).toBeNull();
            u = { login: 'park', _id: '087654321' };
            user.fn.checkLogin('087654321', u, function (err) {
              expect(err).toBeNull();
              expect(userCache.logins.parker).toBeUndefined();
              done();
            });
          });
        }
      );

    });

    describe('checkEmail', function () {
      beforeAll(function () {
        userCache.emails = {
          '<EMAIL>': '087654321',
          '<EMAIL>': 'azertyuiop'
        };
      });
      afterAll(function (done) { userCache.init(done); });

      it('should return an error if add and existing email', function (done) {
        var u = { _id: 'azertyuiop', email: '<EMAIL>' };
        user.fn.checkEmail(undefined, u, function (err) {
          expect(ld.isError(err)).toBeTruthy();
          expect(err).toMatch('USER.EMAIL_ALREADY_EXISTS');
          u = { _id: 'anotherone', email: '<EMAIL>' };
          user.fn.checkEmail(undefined, u, function (err) {
            expect(ld.isError(err)).toBeTruthy();
            expect(err).toMatch('USER.EMAIL_ALREADY_EXISTS');
            done();
          });
        });
      });

      it('should pay attention to email when edit, returns null',
        function (done) {
          var u = { _id: '087654321', email: '<EMAIL>' };
          user.fn.checkEmail('087654321', u, function (err) {
            expect(err).toBeNull();
            expect(userCache.emails['<EMAIL>']).toBeDefined();
            u.email = '<EMAIL>';
            user.fn.checkEmail('087654321', u, function (err) {
              expect(err).toBeNull();
              expect(userCache.emails['<EMAIL>']).toBeUndefined();
              done();
            });
          });
        }
      );

    });

    describe('userCache getIdsFromLoginsOrEmails', function () {

      it('should throw errors if arguments are not provided as expected',
        function () {
          var getIds = userCache.fn.getIdsFromLoginsOrEmails;
          expect(getIds).toThrow();
          expect(ld.partial(getIds, 'notArray')).toThrow();
        }
      );

      it('should return an object with uid, fixing not found users to false',
        function (done) {
          var u = {
            login: 'shelly',
            password: '<PASSWORD>',
            email: '<EMAIL>'
          };
          user.set(u, function (err, u) {
            expect(err).toBeNull();
            var users = ['shelly', 'inexistent'];
            var res = userCache.fn.getIdsFromLoginsOrEmails(users);
            expect(ld.isObject(res)).toBeTruthy();
            expect(ld.size(res.uids)).toBe(1);
            expect(ld.first(res.uids)).toBe(u._id);
            expect(ld.size(res.present)).toBe(1);
            expect(ld.first(res.present)).toBe('shelly');
            expect(ld.size(res.absent)).toBe(1);
            expect(ld.first(res.absent)).toBe('inexistent');
            users[0] = '<EMAIL>';
            res = userCache.fn.getIdsFromLoginsOrEmails(users);
            expect(ld.size(res.present)).toBe(1);
            expect(ld.first(res.present)).toBe('<EMAIL>');
            expect(ld.size(res.absent)).toBe(1);
            done();
          });
        }
      );

    });

  });


  describe('lodash mixins', function () {

    describe('isEmail', function () {

      it ('should returns if the value is an email or not', function () {
        expect(ld.isEmail(1)).toBeFalsy();
        expect(ld.isEmail([])).toBeFalsy();
        expect(ld.isEmail({})).toBeFalsy();
        expect(ld.isEmail('aaa')).toBeFalsy();
        expect(ld.isEmail('aaa@')).toBeFalsy();
        expect(ld.isEmail('aaa@bbb')).toBeFalsy();
        expect(ld.isEmail('aaabbb.com')).toBeFalsy();
        expect(ld.isEmail('@example.com')).toBeFalsy();
        expect(ld.isEmail('<EMAIL>')).toBeTruthy();
        expect(ld.isEmail('<EMAIL>')).toBeTruthy();
      });
    });

  });
}).call(this);


<|start_filename|>spec/backend/model/pad.spec.js<|end_filename|>
/**
*  Licensed to the Apache Software Foundation (ASF) under one
*  or more contributor license agreements.  See the NOTICE file
*  distributed with this work for additional information
*  regarding copyright ownership.  The ASF licenses this file
*  to you 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.
*/

(function () {
  'use strict';

  // Dependencies
  var ld = require('lodash');
  var specCommon = require('../common.js');
  var pad = require('../../../model/pad.js');
  var user = require('../../../model/user.js');
  var group = require('../../../model/group.js');

  // Pre-created user, group and pad
  var guser;
  var ggroup;
  var gpad;

  var initAll = function (done) {
    specCommon.reInitDatabase(function () {
      user.set(
        {
          login: 'parker',
          password: '<PASSWORD>',
          email: '<EMAIL>'
        }, function (err, u) {
          if (!err) { guser = u; }
          group.set({ name: 'college', admin: guser._id }, function (err, g) {
            if (!err) { ggroup = g; }
            pad.set({ name: 'exam1', group: ggroup._id }, function (err, p) {
              if (!err) { gpad = p; }
              done();
            });
          });
        }
      );
    });
  };

  describe('Pad', function () {});
    beforeAll(specCommon.reInitDatabase);
    afterAll(specCommon.reInitDatabase);

    describe('pad set (and add)', function () {
      beforeAll(initAll);
      afterAll(specCommon.reInitDatabase);

      it('should return errors if arguments are not as expected', function () {
          expect(pad.set).toThrow();
          expect(ld.partial(pad.set, {})).toThrow();
          expect(ld.partial(pad.set, 'str', ld.noop)).toThrow();
          expect(ld.partial(pad.set, {}, ld.noop)).toThrow();
          var props = { name: 'name', group: 'gId'};
          expect(ld.partial(pad.set, props, false)).toThrow();
          props.group = 123;
          expect(ld.partial(pad.set, props, ld.noop)).toThrow();
        }
      );

      it('should return an Error if pad is not found', function (done) {
        var params = { name: 'name', group: 'group1', _id: 'inexistent' };
        pad.set(params, function (err, p) {
          expect(ld.isError(err)).toBeTruthy();
          expect(err).toMatch('PAD.INEXISTENT');
          expect(p).toBeUndefined();
          done();
        });
      });

      it('should return an Error if group is not found', function (done) {
        var params = { name: 'name', group: 'inexistent' };
        pad.set(params, function (err, p) {
          expect(ld.isError(err)).toBeTruthy();
          expect(err).toMatch('PAD.ITEMS_NOT_FOUND');
          expect(p).toBeUndefined();
          done();
        });
      });

      it('should return an Error if there are users and they are not found',
        function (done) {
          var params = {
            name: 'name',
            group: ggroup._id,
            visibility: 'restricted',
            users: ['inexistent']
          };
          pad.set(params, function (err, p) {
            expect(ld.isError(err)).toBeTruthy();
            expect(err).toMatch('PAD.ITEMS_NOT_FOUND');
            expect(p).toBeUndefined();
            done();
          });
        }
      );

      it('should return an Error if the _id field is fixed and the pad is' +
        ' not found', function (done) {
          var params = {
            name: 'name',
            group: ggroup._id,
            _id: 'notAPad'
          };
          pad.set(params, function (err, p) {
            expect(ld.isError(err)).toBeTruthy();
            expect(err).toMatch('PAD.INEXISTENT');
            expect(p).toBeUndefined();
            done();
          });
        }
      );

      it('should assign defaults if other params are not properly typed nor' +
        'defined and updates group.pads array accordingly', function (done) {
          var params = {
            name: 'conqueringTheWorld',
            group: ggroup._id
          };
          pad.set(params, function (err, p) {
            expect(err).toBeNull();
            expect(ld.isObject(p)).toBeTruthy();
            expect(p._id).toBeDefined();
            expect(ld.startsWith(p._id, 'conqueringtheworld-')).toBeTruthy();
            expect(p.ctime).toBeDefined();
            expect(ld.isNumber(p.ctime)).toBeTruthy();
            expect(p.name).toBe('conqueringTheWorld');
            expect(p.group).toBe(ggroup._id);
            expect(p.visibility).toBeNull();
            expect(p.password).toBeNull();
            expect(p.readonly).toBeNull();
            expect(ld.isArray(p.users)).toBeTruthy();
            expect(ld.isEmpty(p.users)).toBeTruthy();
            params.users = 'notAnArray';
            params.visibility = 121;
            params.password = <PASSWORD>;
            params.readonly = 'shouldBeABoolean';
            pad.set(params, function (err, p) {
              expect(err).toBeNull();
              expect(ld.isObject(p)).toBeTruthy();
              expect(p._id).toBeDefined();
              expect(p.name).toBe('conqueringTheWorld');
              expect(p.group).toBe(ggroup._id);
              expect(p.visibility).toBeNull();
              expect(p.password).toBeNull();
              expect(p.readonly).toBeNull();
              expect(ld.isArray(p.users)).toBeTruthy();
              expect(ld.isEmpty(p.users)).toBeTruthy();
              group.get(p.group, function (err, g) {
                expect(err).toBeNull();
                expect(ld.includes(g.pads, p._id)).toBeTruthy();
                done();
              });
            });
          });
        }
      );

      it('should return an error if visibility is private with invalid ' +
        'password', function (done) {
          var params = {
          name: 'trapFrank',
          group: ggroup._id,
          visibility: 'private'
        };
        pad.set(params, function (err, p) {
          expect(ld.isError(err)).toBeTruthy();
          expect(err).toMatch('PASSWORD_INCORRECT');
          expect(p).toBeUndefined();
          params.password = <PASSWORD>;
          pad.set(params, function (err, p) {
            expect(ld.isError(err)).toBeTruthy();
            expect(err).toMatch('PASSWORD_INCORRECT');
            expect(p).toBeUndefined();
            done();
          });
        });
      });

      it('should otherwise accept well defined parameters and keeps password ' +
        'after first definition', function (done) {
          var params = {
            name: 'trapFrank',
            group: ggroup._id,
            users: [guser._id],
            visibility: 'restricted',
            readonly: false
          };
          pad.set(params, function (err, p) {
            expect(err).toBeNull();
            expect(ld.isObject(p)).toBeTruthy();
            expect(p._id).toBeDefined();
            expect(ld.startsWith(p._id, 'trapfrank-')).toBeTruthy();
            expect(p.name).toBe('trapFrank');
            expect(p.group).toBe(ggroup._id);
            expect(p.visibility).toBe('restricted');
            expect(p.password).toBeNull();
            expect(p.readonly).toBeFalsy();
            expect(ld.isArray(p.users)).toBeTruthy();
            expect(ld.first(p.users)).toBe(guser._id);
            p.visibility = 'private';
            p.password = '<PASSWORD>';
            pad.set(p, function (err, p) {
              expect(err).toBeNull();
              expect(p._id).toBeDefined();
              expect(p.name).toBe('trapFrank');
              expect(p.visibility).toBe('private');
              expect(ld.isObject(p.password)).toBeTruthy();
              var pass = ld.clone(p.password);
              expect(p.password.salt).toBeDefined();
              expect(p.password.hash).toBeDefined();
              p.name = 'TRAPFrank';
              delete p.password;
              pad.set(p, function (err, p) {
                expect(err).toBeNull();
                expect(p._id).toBeDefined();
                expect(ld.startsWith(p._id, 'trapfrank-')).toBeTruthy();
                expect(p.name).toBe('TRAPFrank');
                expect(p.visibility).toBe('private');
                expect(p.password.salt).toBe(pass.salt);
                expect(p.password.hash).toBe(pass.hash);
                done();
              });
            });
          });
        }
      );

      it('should also allow updating existing pad', function (done) {
        gpad.name = 'shellyNator';
        gpad.visibility = 'restricted';
        gpad.users.push(guser._id);
        pad.set(gpad, function (err, p) {
          expect(err).toBeNull();
          expect(ld.isObject(p)).toBeTruthy();
          expect(p._id).toBe(gpad._id);
          expect(p.ctime).toBe(gpad.ctime);
          expect(ld.startsWith(p._id, 'shellynator-')).toBeFalsy();
          expect(p.name).toBe('shellyNator');
          expect(p.group).toBe(gpad.group);
          expect(p.visibility).toBe('restricted');
          expect(p.password).toBeNull();
          expect(p.readonly).toBeNull();
          expect(ld.isArray(p.users)).toBeTruthy();
          expect(ld.first(p.users)).toBe(guser._id);
          group.get(p.group, function (err, g) {
            expect(err).toBeNull();
            expect(ld.includes(g.pads, p._id)).toBeTruthy();
            done();
          });
        });
      });

      it('should handle gracefully group changing', function (done) {
        group.set({ name: 'memories', admin: guser._id }, function (err, g) {
          expect(err).toBeNull();
          var oldGroup = gpad.group;
          gpad.group = g._id;
          pad.set(gpad, function (err, p) {
            expect(err).toBeNull();
            expect(p._id).toBe(gpad._id);
            expect(p.name).toBe('shellyNator');
            group.get(p.group, function (err, g) {
              expect(err).toBeNull();
              expect(ld.includes(g.pads, p._id)).toBeTruthy();
              group.get(oldGroup, function (err, g) {
                expect(err).toBeNull();
                expect(ld.includes(g.pads, p._id)).toBeFalsy();
                done();
              });
            });
          });
        });
      });

    });

    describe('pad get', function () {

      beforeAll(initAll);
      afterAll(specCommon.reInitDatabase);

      it('should throw errors if arguments are not provided as expected',
        function () {
          expect(pad.get).toThrow();
          expect(ld.partial(pad.get, 123)).toThrow();
          expect(ld.partial(pad.get, 'key')).toThrow();
          expect(ld.partial(pad.get, 'key', 'notAFunc')).toThrow();
        }
      );

      it('should return an Error if the key is not found', function (done) {
        pad.get('inexistent', function (err, p) {
          expect(ld.isError(err)).toBeTruthy();
          expect(p).toBeUndefined();
          done();
        });
      });

      it('should return the pad otherwise', function (done) {
        pad.get(gpad._id, function (err, p) {
          expect(err).toBeNull();
          expect(ld.isObject(p)).toBeTruthy();
          expect(p._id).toBeDefined();
          expect(p.name).toBe('exam1');
          expect(p.group).toBe(ggroup._id);
          expect(p.visibility).toBeNull();
          expect(p.password).toBeNull();
          expect(p.readonly).toBeNull();
          expect(ld.isArray(p.users)).toBeTruthy();
          expect(ld.isEmpty(p.users)).toBeTruthy();
          done();
        });
      });

    });

    describe('pad del', function () {

      beforeAll(initAll);
      afterAll(specCommon.reInitDatabase);

      it('should throw errors if arguments are not provided as expected',
        function () {
          expect(pad.del).toThrow();
          expect(ld.partial(pad.del, 123)).toThrow();
          expect(ld.partial(pad.del, 'key')).toThrow();
          expect(ld.partial(pad.del, 'key', 'notAFunc')).toThrow();
        }
      );

      it('should return an Error if the key is not found', function (done) {
        pad.del('inexistent', function (err, p) {
          expect(ld.isError(err)).toBeTruthy();
          expect(p).toBeUndefined();
          done();
        });
      });

      it('should removes and returns null otherwise, removes indexes,' +
        ' including bookmarks', function (done) {
          user.mark('parker', 'pads', gpad._id, function (err) {
            expect(err).toBeNull();
            user.get('parker', function (err, u) {
              expect(err).toBeNull();
              expect(ld.includes(u.bookmarks.pads, gpad._id)).toBeTruthy();
              pad.del(gpad._id, function (err, p) {
                expect(err).toBeNull();
                expect(p).toBeUndefined();
                pad.get(gpad._id, function (err, p) {
                  expect(ld.isError(err)).toBeTruthy();
                  expect(p).toBeUndefined();
                  group.get(gpad.group, function (err, g) {
                    expect(err).toBeNull();
                    expect(ld.includes(g.pads, gpad._id)).toBeFalsy();
                    user.get('parker', function (err, u) {
                      expect(err).toBeNull();
                      var isMarked = ld.includes(u.bookmarks.pads, gpad._id);
                      expect(isMarked).toBeFalsy();
                      done();
                    });
                  });
                });
              });
            });
          });
        }
      );

    });

}).call(this);


<|start_filename|>spec/backend/mail.spec.js<|end_filename|>
/**
*  vim:set sw=2 ts=2 sts=2 ft=javascript expandtab:
*
*  Licensed to the Apache Software Foundation (ASF) under one
*  or more contributor license agreements.  See the NOTICE file
*  distributed with this work for additional information
*  regarding copyright ownership.  The ASF licenses this file
*  to you 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.
*/

(function () {
  'use strict';

  var ld = require('lodash');
  require('es6-shim');
  var SMTPServer = require('smtp-server').SMTPServer;
  var specCommon = require('./common.js');
  var mail = require('../../mail.js');
  var conf = require('../../configuration.js');

  describe('mail module', function () {

    beforeAll(specCommon.reInitDatabase);
    afterAll(specCommon.reInitDatabase);

    it('should have no in-memory tokens nor connection by default',
      function () {
        expect(ld.isEmpty(mail.tokens)).toBeTruthy();
        expect(ld.isEmpty(mail.ends)).toBeTruthy();
        expect(mail.connection).toBeUndefined();
      }
    );

    describe('genToken', function () {

      beforeAll(function () { conf.cache.tokenDuration = 1; });

      it('should throw error if value is not given', function () {
        expect(mail.genToken).toThrowError(/PARAMS_REQUIRED/);
      });

      it('should fixes token and end date', function () {
        var token = mail.genToken('a value');
        expect(ld.size(mail.tokens)).toBe(1);
        expect(ld.size(mail.ends)).toBe(1);
        expect(ld.isString(token)).toBeTruthy();
        expect(mail.tokens[token]).toBe('a value');
        var end = mail.ends[token];
        expect(ld.isNumber(end)).toBeTruthy();
        var now = Date.now();
        var nowEnd = conf.get('tokenDuration') * 60 * 1000;
        expect(now <= end).toBeTruthy();
        expect((now + nowEnd) >= end).toBeTruthy();
      });

    });

    describe('isValidToken', function () {

      // 120ms
      beforeAll(function () { conf.cache.tokenDuration = 0.002; });

      it('should return a boolean result according to request',
        function (done) {
          expect(mail.isValidToken('inexistent')).toBeFalsy();
          var token = mail.genToken('<PASSWORD>');
          expect(mail.isValidToken(token)).toBeTruthy();
          setTimeout(function () {
            expect(mail.isValidToken(token)).toBeFalsy();
            done();
          }, 200);
        }
      );

    });

    describe('with SMTP Server tests', function () {
      var server;
      var mailInfo = {};

      beforeAll(function (done) {
        server = new SMTPServer({
          host: 'localhost',
          secure: false,
          logger: false,
          disabledCommands: ['AUTH'],
          onMailFrom: function (address, session, next) {
            mailInfo.address = address;
            mailInfo.session = session;
            mailInfo.stream = '';
            next(new Error('not null to prevent real sending'));
          }
        });
        server.listen(2525, done);
      });

      afterAll(function (done) {
        mail.server.smtp.quit();
        server.close(done);
      });


      describe('connect', function () {
        var clientOpts = {
          SMTPPort: 2525,
          SMTPHost: 'localhost',
          SMTPSSL: false,
          SMTPTLS: false
        };

        afterAll(function() {
          conf.cache = ld.clone(conf.DEFAULTS);
        });

        it('should throw an error if no options have been fixed', function () {
          expect(ld.partial(mail.connect))
            .toThrowError(/ERROR.TYPE.SMTP_CONFIG/);
        });

        it('should return an error if options are incorrect', function (done) {
          conf.cache.SMTPPort = 25;
          conf.cache.SMTPHost = 'localhost';
          conf.cache.SMTPSSL = false;
          conf.cache.SMTPTLS = false;
          conf.cache.emailFrom = '<EMAIL>';
          mail.connect();
          mail.send('<EMAIL>', 'subject', 'message', function (err) {
            expect(err).toMatch('MAIL_NOT_CONFIGURED');
            expect(mail.server.smtp.state()).toBe(0);
            done();
          });
        });

        describe('mail.connect real connection', function () {

          beforeAll(function () {
            conf.cache.SMTPEmailFrom = '<EMAIL>';
          });
          afterAll(function () { conf.cache.SMTPEmailFrom = undefined; });

          it('should not login with invalid credentials', function (done) {
            ld.assign(conf.cache, clientOpts);
            conf.cache.SMTPUser = 'parker';
            conf.cache.SMTPPass = '<PASSWORD>';
            mail.connect();
            mail.send('<EMAIL>', 'subject', 'message', function (err) {
              expect(err).toMatch('no form of authorization supported');
              expect(mail.server.smtp.authorized()).toBeFalsy();
              expect(mail.server.smtp.state()).toBe(2);
              done();
            });
          });

          it('should connect to a valid basic SMTP Server otherwise',
            function (done) {
              ld.assign(conf.cache, clientOpts);
              delete conf.cache.SMTPUser;
              delete conf.cache.SMTPPass;
              mail.connect();
              mail.send('<EMAIL>', 'subject', 'message',
                function (err) {
                  expect(err).toMatch('bad response');
                  expect(err.smtp).toMatch('550 not null to prevent');
                  expect(mail.server.smtp.authorized()).toBeTruthy();
                  expect(mail.server.smtp.state()).toBe(2);
                  done();
                }
              );
            }
          );

        });

      });

      describe('send', function () {

        it('should throw errors if arguments are not as expected', function () {
          expect(mail.send).toThrowError(/ERROR.TYPE.TO_MAIL/);
          expect(ld.partial(mail.send, 12)).toThrowError(/ERROR.TYPE.TO_MAIL/);
          expect(ld.partial(mail.send, 'notAnEmail'))
            .toThrowError(/ERROR.TYPE.TO_MAIL/);
          expect(ld.partial(mail.send, '<EMAIL>', false))
            .toThrowError(/ERROR.TYPE.SUBJECT_STR/);
          expect(ld.partial(mail.send, '<EMAIL>', 'subject', false))
            .toThrowError(/ERROR.TYPE.MSG_STR/);
          expect(ld.partial(mail.send, '<EMAIL>', 'subj', 'msg', 12))
            .toThrowError(/ERROR.TYPE.CALLBACK_FN/);
        });

        it('should return an email if configuration is not fully done',
          function (done) {
            mail.send('<EMAIL>', 'subject', 'message', function (err) {
              expect(err).toMatch('ERROR.CONFIGURATION.MAIL_NOT_CONFIGURED');
              done();
            });
          }
        );

        it('should otherwise send an email', function (done) {
          conf.cache.SMTPEmailFrom = '<EMAIL>';
          mail.send('<EMAIL>', 'subject', 'message',
            function (err, info) {
              expect(err).toMatch('bad response');
              expect(err.smtp).toMatch('550 not null to prevent');
              expect(info.header.from).toBe('<EMAIL>');
              expect(info.header.to).toBe('<EMAIL>');
              expect(info.text).toBe('message');
              expect(mail.server.smtp.state()).toBe(2);
              done();
            }
          );
        });

      });

    });

  });

}).call(this);


<|start_filename|>storage.js<|end_filename|>
/**
*  vim:set sw=2 ts=2 sts=2 ft=javascript expandtab:
*
*  # Database Module
*
*  ## License
*
*  Licensed to the Apache Software Foundation (ASF) under one
*  or more contributor license agreements.  See the NOTICE file
*  distributed with this work for additional information
*  regarding copyright ownership.  The ASF licenses this file
*  to you 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.
*
*  ## Description
*
*  This module consists only on a wrapper around etherpad database.
*/

module.exports = (function () {
  'use strict';
  // Dependencies
  var ld = require('lodash');

  var storage = {};

  // Database PREFIX CONSTANTS
  storage.DBPREFIX       = { GLOBAL: 'mypads:' };
  var DBPG               = storage.DBPREFIX.GLOBAL;
  storage.DBPREFIX.CONF  = DBPG + 'conf:';
  storage.DBPREFIX.USER  = DBPG + 'user:';
  storage.DBPREFIX.GROUP = DBPG + 'group:';
  storage.DBPREFIX.PAD   = DBPG + 'pad:';
  storage.DBPREFIX.JOBQ  = DBPG + 'jobqueue:';

  try {
    // Normal case : when installed as a plugin
    storage.db = require('ep_etherpad-lite/node/db/DB').db;
  }
  catch (e) {
    // Testing case : we need to mock the database connection, using ueberDB and
    // coherent default configuration with eptherpad-lite one.
    var ueberDB = require('ueberdb2');
    storage.db  = new ueberDB.database('dirty', { filename: './test.db' });
    storage.db.init(function (err) { if (err) { console.error(err); } });
  }

  /**
  * `init` function for initial configuration cache init and in memory
  * secondary indexes. At the moment only user / logins / firstname / lastname.
  */

  storage.init = function (callback) {
    var configuration = require('./configuration.js');
    configuration.init(function (err) {
      if (err) { return callback(err); }
      var userCache = require('./model/user-cache.js');
      userCache.init(callback);
    });
  };

  /**
  * ## Internal functions `fn`
  *
  * These functions are not private like with closures, for testing purposes,
  * but they are expected be used only internally by other MyPads functions.
  */

  storage.fn = {};

  /** ### getDelKeys
  *
  * `getDelKeys` is a function for multiple asynchronous gets and removes,
  * taking :
  *
  * - a `del` boolean, for removals to *true*
  * - a `keys` array, wich contains a list of keys to retrieve
  * - a `callback` function, called if error or when finished with *null* and
  *   the `results` object, which is composed of keys and values for gets,
  *   *true* for removals
  * FIXME: TCO ?
  */

  storage.fn.getDelKeys = function (del, keys, callback) {
    var done;
    var results = del ? true : {};
    var action  = del ? 'remove' : 'get';
    var getDel  = function (k) {
      storage.db[action](k, function (err, res) {
        if (err) { return callback(err); }
        if (!del && !ld.isNull(res)) { results[k] = res; }
        done();
      });
    };
    done = function () {
      if (keys.length) {
        getDel(keys.pop());
      } else {
        return callback(null, results);
      }
    };
    done();
  };

  /**
   * ### getKeysUncached
   *
   * Same than `getKeys` but tries to get values in bulk, bypassing UeberDB cache
   */
  storage.fn.getKeysUncached = function (keys, callback) {
    if (storage.db && (storage.db.type === 'postgres' || storage.db.type === 'postgrespool')) {
      var query = 'SELECT key, value FROM store WHERE key = ANY ($1)';
      return storage.db.db.wrappedDB.db.query(query, [keys], function (err, queryResult) {
        if (err) { return callback(err); }

        var rows = queryResult.rows;

        var results = {};
        rows.forEach(function (row) {
          try {
            if (!ld.isNull(row)) { results[row.key] = JSON.parse(row.value); }
          } catch (e) {
            console.error('JSON-PROBLEM:' + row.value);
          }
        });

        return callback(null, results);
      });
    }

    return storage.fn.getKeys(keys, callback);
  };

  /**
   * ### getKeys
   *
   * `getKeys` is an helper around `storage.fn.getDelKeys` with `del` argument
   * to *false*.
   */
  storage.fn.getKeys = ld.partial(storage.fn.getDelKeys, false);

  /**
  * ### delKeys
  *
  * `delKeys` is an helper around `storage.fn.getDelKeys` with `del` argument
  * to *true*.
  */

  storage.fn.delKeys = ld.partial(storage.fn.getDelKeys, true);

  /**
  * `setKeys` is a function for multiple asynchronous sets, taking :
  s
  * - a `kv` object, wich contains a list a keys and values to set
  * - a `callback` function, called if error or when finished with null
  * FIXME: TCO ?
  */

  storage.fn.setKeys = function (kv, callback) {
    var done;
    var pairs = ld.pairs(kv);
    var setK  = function (k, v) { storage.db.set(k, v, done); };
    done = function (err) {
      if (err) { return callback(err); }
      if (pairs.length) {
        var pair = pairs.pop();
        setK(pair[0], pair[1]);
      } else {
        return callback(null);
      }
    };
    done();
  };

  /**
  * `setKeysIfNotExists` is a function for multiple asynchronous sets if the
  *  key does not exists in database, taking :
  s
  * - a `kv` object, wich contains a list a keys and values to set
  * - a `callback` function, called if error or when finished with null
  * FIXME: TCO ?
  */

  storage.fn.setKeysIfNotExists = function (kv, callback) {
    var done;
    var pairs = ld.pairs(kv);
    var setK = function (k, v) { storage.db.set(k, v, done); };
    done = function (err) {
      if (err) { return callback(err); }
      if (pairs.length) {
        var pair = pairs.pop();
        storage.db.get(pair[0], function(err, res) {
          if (err) { return callback(err); }
          if (ld.isUndefined(res) || ld.isNull(res)) {
            setK(pair[0], pair[1]);
          } else {
            done();
          }
        });
      } else {
        return callback(null);
      }
    };
    done();
  };

  return storage;
}).call(this);


<|start_filename|>configuration.js<|end_filename|>
/**
*  vim:set sw=2 ts=2 sts=2 ft=javascript expandtab:
*
*  # Configuration Module
*
*  ## License
*
*  Licensed to the Apache Software Foundation (ASF) under one
*  or more contributor license agreements.  See the NOTICE file
*  distributed with this work for additional information
*  regarding copyright ownership.  The ASF licenses this file
*  to you 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.
*
*  ## Description
*
*  This is the module for MyPads configuration.
*/

var settings;
try {
  settings = require('ep_etherpad-lite/node/utils/Settings');
}
catch (e) {
  if (process.env.TEST_LDAP) {
    settings = {
      'ep_mypads': {
        'ldap': {
          'url': 'ldap://rroemhild-test-openldap',
          'bindDN': 'cn=admin,dc=planetexpress,dc=com',
          'bindCredentials': 'Good<PASSWORD>',
          'searchBase': 'ou=people,dc=planetexpress,dc=com',
          'searchFilter': '(uid={{username}})',
          'properties': {
            'login': 'uid',
            'email': 'mail',
            'firstname': 'givenName',
            'lastname': 'sn'
          },
          'defaultLang': 'fr'
        }
      }
    };
  } else {
    settings = {};
  }
}
module.exports = (function() {
  'use strict';

  // Dependencies
  var ld      = require('lodash');
  var ldap    = require('ldapjs');
  var storage = require('./storage.js');
  var db      = storage.db;

  var DBPREFIX = storage.DBPREFIX.CONF;

  /**
  * `configuration` object is a closure to interact with the whole
  * config. It will be exported.
  */

  var configuration = {

    /**
    * The object contains a private `DEFAULTS` field, holding defaults
    * settings. Configuration data is taken from the database, applying
    * defaults when necessary, for example at the plugin initialization.
    */

    DEFAULTS: {
      title: 'MyPads',
      rootUrl: '',
      allowEtherPads: true,
      openRegistration: true,
      hideHelpBlocks: false,
      passwordMin: 8,
      passwordMax: 30,
      languages: { en: 'English', fr: 'Français', de: 'Deutsch', es: 'Español', it: 'Italiano'},
      loginMsg: { en: '', fr: '', de: '', es: '', it: '' },
      defaultLanguage: 'en',
      HTMLExtraHead: '',
      checkMails: false,
      SMTPPort: undefined,
      SMTPHost: undefined,
      SMTPUser: undefined,
      SMTPPass: undefined,
      SMTPEmailFrom: undefined,
      SMTPSSL: false,
      SMTPTLS: true,
      tokenDuration: 60, // in minutes
      useFirstLastNameInPads: false,
      insensitiveMailMatch: false,
      authMethod: 'internal',
      availableAuthMethods: [ 'internal', 'ldap', 'cas' ],
      authLdapSettings: {
        url:             'ldaps://ldap.example.org',
        bindDN:          'uid=ldap,ou=users,dc=example,dc=org',
        bindCredentials: 'S3cr3t',
        searchBase:      'ou=users,dc=example,dc=org',
        searchFilter:    '(uid={{username}})',
        tlsOptions: {
          rejectUnauthorized: true
        },
        properties: {
          login:     'uid',
          email:     'mail',
          firstname: 'givenName',
          lastname:  'sn'
        },
        defaultLang: 'en'
      },
      authCasSettings: {
        serverUrl:      'https://cas.example.org/cas',
        protocolVersion: 3.0,
        properties: {
          login:     'login',
          email:     'email',
          firstname: 'firstname',
          lastname:  'lastname'
        },
        defaultLang: 'en'
      },
      allPadsPublicsAuthentifiedOnly: false,
      deleteJobQueue: false
    },

    /**
    * cache object` stored current configuration for faster access than
    * database
    */

    cache: {},

    /**
    * `init` is called when mypads plugin is initialized. It fixes the default
    * data for the configuration into the database and populate the configuration
    * cache.
    * It takes an optional `callback` function used after `db.set` abstraction
    * to return an eventual *error*.
    */

    init: function (callback) {
      callback = callback || function () {};
      if (!ld.isFunction(callback)) {
        throw new TypeError('BACKEND.ERROR.TYPE.CALLBACK_FN');
      }

      var confDefaults = ld.transform(configuration.DEFAULTS,
        function (memo, val, key) { memo[DBPREFIX + key] = val; });

      var initFromDatabase = function () {
        storage.fn.getKeys(ld.keys(confDefaults), function (err, res) {
          if (err) { return callback(err); }

          var pushLdapSettingsToDB = false;
          configuration.cache      = ld.transform(res, function (memo, val, key) {
            key = key.replace(DBPREFIX, '');

            /* get ldap settings from settings.json if exists and database
             * informations are empty */
            if (key === 'authLdapSettings' && ld.isEqual(val, configuration.DEFAULTS.authLdapSettings) &&
                settings.ep_mypads && settings.ep_mypads.ldap) {
              /* json parsing of the settings from settings.json is made by Etherpad,
               * no need to check */
              val                  = settings.ep_mypads.ldap;
              pushLdapSettingsToDB = true;
            }

            if (ld.isUndefined(val)) {
              memo[key] = configuration.DEFAULTS[key];
            } else {
              memo[key] = val;
            }
          });

          if (!pushLdapSettingsToDB) {
            return callback(null);
          }

          /* If DB's LDAP settings are the defaults and settings.json contains
           * LDAP settings, use LDAP auth and put the settings in the database */
          configuration.cache.authMethod = 'ldap';

          var kv   = {
            authLdapSettings: configuration.cache.authLdapSettings,
            authMethod: 'ldap'
          };
          var dbKv = ld.transform(kv,
            function (memo, val, key) { memo[DBPREFIX + key] = val; });

          storage.fn.setKeys(dbKv, callback);
        });
      };

      // Those settings will evolve with MyPads, thus those from MyPads should
      // always be used
      var force   = {
        availableAuthMethods: configuration.DEFAULTS.availableAuthMethods,
        languages: configuration.DEFAULTS.languages
      };
      var dbForce = ld.transform(force,
        function (memo, val, key) { memo[DBPREFIX + key] = val; });

      storage.fn.setKeys(dbForce, function(err) {
        if (err) { return callback(err); }
        // Set the default values in database if they are not already in it
        storage.fn.setKeysIfNotExists(confDefaults, function (err) {
          if (err) { return callback(err); }
          initFromDatabase();
        });
      });
    },

    /**
    * `get` is an synchronous function taking : a mandatory `key` string
    * argument.  It throws *Error* if error, returns *undefined* if key does
    * not exist and the result otherwise.
    */

    get: function (key) {
      if (!ld.isString(key)) {
        throw new TypeError('BACKEND.ERROR.TYPE.KEY_STR');
      }
      return configuration.cache[key];
    },

    /**
    * `set` is an asynchronous function taking two mandatory arguments:
    *
    * - `key` string;
    * - `value`.
    * - `callback` function argument returning *Error* if error, *null*
    *   otherwise
    *
    * `set` sets the `value` for the configuration `key` and takes care of
    * `cache`.
    */

    set: function (key, value, callback) {
      if (!ld.isString(key)) {
        throw new TypeError('BACKEND.ERROR.TYPE.KEY_STR');
      }
      if (ld.isUndefined(value)) {
        throw new TypeError('BACKEND.ERROR.TYPE.VALUE_REQUIRED');
      }
      if (!ld.isFunction(callback)) {
        throw new TypeError('BACKEND.ERROR.TYPE.CALLBACK_FN');
      }
      var dbSet = function(key, value) {
        db.set(DBPREFIX + key, value, function (err) {
          if (err) { return callback(err); }
          configuration.cache[key] = value;
          callback();
        });
      };
      if (key === 'authLdapSettings' || key === 'authCasSettings') {
        delete value.attrs;
      }

      if (key === 'authLdapSettings') {
        /* Test LDAP settings before registering them */
        var ldapErr      = new Error('BACKEND.ERROR.CONFIGURATION.UNABLE_TO_BIND_TO_LDAP');
        var ldapSettings = ld.cloneDeep(value);

        /* Not passed to ldapjs, we don't want to autobind
         * https://github.com/mcavage/node-ldapjs/blob/v1.0.1/lib/client/client.js#L343-L356 */
        delete ldapSettings.bindDN;
        delete ldapSettings.bindCredentials;

        var client = ldap.createClient(ldapSettings);
        client.on('error', function (err) {
          console.error('LDAP settings change: error. See below for details.');
          console.error(err);
          return callback(ldapErr);
        });
        client.bind(value.bindDN, value.bindCredentials, function(err) {
          if (err) {
            console.error(err);
            return callback(ldapErr);
          }
          client.unbind(function(err) {
            client.destroy();
            if (err) {
              console.error(err);
            } else {
              /* Now, we can register new LDAP settings */
              dbSet(key, value);
            }
          });
        });
      } else {
        dbSet(key, value);
      }
    },

    /**
    * `del` is an asynchronous function that removes a configuration option.
    * It takes two mandatory arguments :
    *
    * - a `key` string,
    * - a `callback` function argument returning *Error* if error
    *
    * It takes care of config `cache`.
    */

    del: function (key, callback) {
      if (!ld.isString(key)) {
        throw new TypeError('BACKEND.ERROR.TYPE.KEY_STR');
      }
      if (!ld.isFunction(callback)) {
        throw new TypeError('BACKEND.ERROR.TYPE.CALLBACK_FN');
      }
      db.remove(DBPREFIX + key, function (err) {
        if (err) { return callback(err); }
        delete configuration.cache[key];
        callback();
      });
    },

    /**
    * `all` is a synchronous function that returns the whole configuration
    * from `cache`. Fields / keys are unprefixed.
    */

    all: function () { return configuration.cache; },

    /**
    * `public` is a synchronous function that returns the whole publicly
    * available configuration from `cache`. Fields / keys are unprefixed.
    */

    public: function () {
      var all = configuration.all();
      return ld.pick(all, 'title', 'passwordMin', 'passwordMax', 'languages',
        'HTMLExtraHead', 'openRegistration', 'hideHelpBlocks', 'useFirstLastNameInPads',
        'authMethod', 'authCasSettings', 'allPadsPublicsAuthentifiedOnly', 'defaultLanguage',
        'loginMsg'
      );
    },

    /**
     * `isNotInternalAuth` is a synchronous function that returns true if the
     * authentication method is not `internal`
     */

    isNotInternalAuth: function() {
      return (configuration.get('authMethod') !== 'internal');
    }
  };

  return configuration;

}).call(this);




<|start_filename|>spec/frontend/tag-widget.spec.js<|end_filename|>
/**
*  vim:set sw=2 ts=2 sts=2 ft=javascript expandtab:
*
*  # Tag widget functional testing
*
*  ## License
*
*  Licensed to the Apache Software Foundation (ASF) under one
*  or more contributor license agreements.  See the NOTICE file
*  distributed with this work for additional information
*  regarding copyright ownership.  The ASF licenses this file
*  to you 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.
*
* ## Description
*
* From group form
*/

module.exports = (function () {
  'use strict';
  // Dependencies
  var common = require('./common.js');
  var fill = common.helper.fill;

  var tag = {};

  // Shared variables
  var $el;
  var first;

  tag.run = function (app) {

    first = function (sel) { return app.document.querySelector(sel); };

    describe('tag widget module testing', function () {

      beforeAll(function (done) {
        // Login and go to form add page
        app.document.querySelector('header nav a:first-child').click();
        window.setTimeout(function () {
          fill(app.document.querySelector('input[name=login]'), 'parker');
          fill(app.document.querySelector('input[name=password]'),
            'lovesKubiak');
          app.document.querySelector('input[type=submit]').click();
          window.setTimeout(function () {
            app.document.querySelector('a[href$=\'mypads/group/add\']').click();
            window.setTimeout(function () {
              $el = { 
                tags: first('input[name=tags]'),
                ok: first('button.ok'),
                datalist: first('datalist'),
                ul: first('div.tag ul')
              };
              first('body > section > div p').click();
              window.setTimeout(done, 100);
            }, 200);
          }, 200);
        }, 200);
      });

      afterAll(function (done) {
        first('.icon-logout').parentNode.click();
        window.setTimeout(function () {
          first('body > section p').click();
          done();
        }, 100);
      });

      it('should add new tags by several maners', function (done) {
        fill($el.tags, 'a tag #1');
        $el.ok.click();
        window.setTimeout(function () {
          expect($el.tags.value).toBe('');
          expect($el.ul.textContent).toBe('a tag #1');
          fill($el.tags, 'a tag #2');
          $el.ok.click();
          window.setTimeout(function () {
            expect($el.tags.value).toBe('');
            $el.liFirst = $el.ul.querySelector('li');
            expect($el.liFirst.textContent).toBe('a tag #1');
            $el.liLast = $el.ul.querySelector('li:last-child');
            expect($el.liLast.textContent).toBe('a tag #2');
            done();
          }, 200);
        }, 200);
      });

      it('should remove tags with close icons and ensure datalist coherence',
        function (done) {
          $el.liFirst.querySelector('i').click();
          window.setTimeout(function () {
            var opt = $el.datalist.querySelector('option:last-child');
            expect(opt.value).toBe('a tag #1');
            expect($el.ul.textContent).toBe('a tag #2');
            done();
          }, 200);
      }
    );

    });
  };

  return tag;
}).call(this);


<|start_filename|>spec/frontend/group.spec.js<|end_filename|>
/**
*  vim:set sw=2 ts=2 sts=2 ft=javascript expandtab:
*
*  # Group list functional testing
*
*  ## License
*
*x  Licensed to the Apache Software Foundation (ASF) under one
*  or more contributor license agreements.  See the NOTICE file
*  distributed with this work for additional information
*  regarding copyright ownership.  The ASF licenses this file
*  to you 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.
*
* TODO: migrate to Bootstrap (much work here)
*/

module.exports = (function () {
  'use strict';
  // Dependencies
  var common = require('./common.js');
  var fill = common.helper.fill;

  var list = {};

  // Shared variables
  var $el;
  var first;
  var groupsCount;

  list.run = function (app) {
    first = function (sel) { return app.document.querySelector(sel); };
    describe('group list module testing', function () {

      beforeAll(function (done) {
        // Login and go to group list page
        app.document.querySelector('header nav a:first-child').click();
        window.setTimeout(function () {
          fill(app.document.querySelector('input[name=login]'), 'parker');
          fill(app.document.querySelector('input[name=password]'),
            '<PASSWORD>');
          app.document.querySelector('input[type=submit]').click();
          window.setTimeout(function () {
            var gSel = 'section.panel-primary table tbody tr';
            var groups = app.document.querySelectorAll(gSel);
            var gSelArchived = 'section.panel-default table tbody tr';
            var archived = app.document.querySelectorAll(gSelArchived);
            $el = {
              groups: groups,
              archived: archived,
              searchInput: first('input[type=search]'),
              filter: first('section.filter ul'),
              tagsAside: first('section.tag > ul')
            };
            $el.searchOk = $el.searchInput.parentNode.children[1].firstChild;
            groupsCount = function () {
              return $el.groups.length + $el.archived.length;
            };
            first('body > section > div p').click();
            window.setTimeout(done, 100);
          }, 200);
        }, 200);
      });

      afterAll(function (done) {
        $el.bookmarked.querySelector('header.group a[title$=mark]').click();
        window.setTimeout(function () {
          first('body > section p').click();
          window.setTimeout(function () {
            first('.glyphicon-off').parentNode.click();
            window.setTimeout(function () {
              first('body > section p').click();
              done();
            }, 100);
          }, 100);
        }, 100);
      });

      describe('group marking', function () {

        it('should allow marking of existing unmarked groups',
          function (done) {
            expect($el.bookmarked.children.length).toBe(0);
            expect($el.archived.children.length).toBe(0);
            expect($el.common.children.length).toBe(3);
            var g = $el.common.children[0];
            var name = g.querySelector('header h4').textContent;
            var btn = g.querySelector('header.group a[title$=mark]');
            expect(btn.getAttribute('title')).toBe('Bookmark');
            expect(name).toBe('Santa Fe');
            g.querySelector('header.group a[title$=mark]').click();
            window.setTimeout(function () {
              first('body > section > div p').click();
              expect($el.bookmarked.children.length).toBe(1);
              expect($el.common.children.length).toBe(2);
              g = $el.bookmarked.children[0];
              name = g.querySelector('header h4').textContent;
              expect(name).toBe('Santa Fe');
              btn = g.querySelector('header.group a[title$=mark]');
              expect(btn.getAttribute('title')).toBe('Unmark');
              g = $el.common.children[0];
              name = g.querySelector('header h4').textContent;
              expect(name).toBe('memories');
              g.querySelector('header.group a[title$=mark]').click();
              window.setTimeout(function () {
                expect($el.bookmarked.children.length).toBe(2);
                expect($el.common.children.length).toBe(1);
                first('body > section > div p').click();
                window.setTimeout(done, 100); 
              }, 200);
            }, 200);
          }
        );

        it('should allow unmarking of marked groups', function (done) {
          expect($el.bookmarked.children.length).toBe(2);
          expect($el.common.children.length).toBe(1);
          var g = $el.bookmarked.children[1];
          var name = g.querySelector('header h4').textContent;
          expect(name).toBe('memories');
          g.querySelector('header.group a[title$=mark]').click();
          window.setTimeout(function () {
            expect($el.bookmarked.children.length).toBe(1);
            expect($el.common.children.length).toBe(2);
            first('body > section > div p').click();
            window.setTimeout(done, 100); 
          }, 200);
        });

      });

      describe('group sharing', function () {

        it('should allow to share public group MyPads URL', function () {
          var sel = 'header.group button[title=Share]';
          var $share = $el.common.querySelector(sel);
          var link;
          app.window.prompt = function (title, val) { link = val; };
          $share.click();
          expect(link).toBeDefined();
          expect(link).toMatch('mypads/group/');
          expect(link).toMatch(/\/view$/);
        });

      });


      describe('group tag filtering', function () {
        var g;
        var tags;
        var btnActive;

        it('should filter by clicked tag all groups', function (done) {
          expect(groupsCount()).toBe(3);
          g = $el.bookmarked.children[0];
          tags = g.querySelector('footer ul').children;
          var cool = tags[0];
          var weird = tags[1];
          expect(cool.textContent).toBe('cool');
          expect(weird.textContent).toBe('weird');
          expect(cool.className).toBe('');
          expect(weird.className).toBe('');
          weird.click();
          window.setTimeout(function () {
            expect(cool.className).toBe('');
            expect(weird.className).toBe('active');
            expect(groupsCount()).toBe(1);
            btnActive = $el.tagsAside.querySelector('li button.active');
            expect(btnActive.textContent).toBe('weird');
            window.setTimeout(done, 100);
          }, 200);
        });

        it('should toggle off current filtering', function (done) {
          expect(groupsCount()).toBe(1);
          var weird = tags[1];
          expect(weird.className).toBe('active');
          weird.click();
          window.setTimeout(function () {
            expect(weird.className).toBe('');
            expect(groupsCount()).toBe(3);
            expect(btnActive.className).toBe('');
            window.setTimeout(done, 100);
          }, 200);
        });

      });

      describe('group side filters', function () {

        describe('sort by button', function () {

          it('should have default order by creation date ASC', function () {
            var titles = $el.common.querySelectorAll('li header h4');
            expect(titles.length).toBe(2);
            expect(titles[0].textContent).toBe('memories');
            expect(titles[1].textContent).toBe('shared notes');
          });

          it('should allow ordering by creation and name', function (done) {
            var sel = 'section.sort ul li button';
            var $btns = app.document.querySelectorAll(sel);
            var $sortBy = { creation: $btns[0], name: $btns[1] };
            window.setTimeout(function () {
              var titles = $el.common.querySelectorAll('li header h4');
              expect(titles[0].textContent).toBe('memories');
              expect(titles[1].textContent).toBe('shared notes');
              $sortBy.creation.click();
              window.setTimeout(function () {
                titles = $el.common.querySelectorAll('li header h4');
                expect(titles[0].textContent).toBe('shared notes');
                expect(titles[1].textContent).toBe('memories');
                $sortBy.name.click();
                window.setTimeout(function () {
                  // DESC first
                  titles = $el.common.querySelectorAll('li header h4');
                  expect(titles[0].textContent).toBe('shared notes');
                  expect(titles[1].textContent).toBe('memories');
                  $sortBy.name.click();
                  window.setTimeout(function () {
                    titles = $el.common.querySelectorAll('li header h4');
                    expect(titles[0].textContent).toBe('memories');
                    expect(titles[1].textContent).toBe('shared notes');
                    $sortBy.creation.click();
                    window.setTimeout(done, 100);
                  }, 100);
                }, 100);
              }, 100);
            }, 100);
          });

        });

        describe('filter by search', function () {

          it('should forbid text search under 3 characters', function (done) {
            fill($el.searchInput, '10');
            $el.searchOk.click();
            window.setTimeout(function () {
              expect($el.searchInput.checkValidity()).toBeFalsy();
              done();
            }, 100);
          });

          it('should allow text search otherwise, here without case impact',
            function (done) {
              expect(groupsCount()).toBe(3);
              fill($el.searchInput, 'MORI');
              $el.searchOk.click();
              window.setTimeout(function () {
                expect(groupsCount()).toBe(1);
                done();
              }, 200);
            }
          );

          it('should remove filter with empty search', function (done) {
            expect(groupsCount()).toBe(1);
            fill($el.searchInput, '');
            $el.searchOk.click();
            window.setTimeout(function () {
              expect(groupsCount()).toBe(3);
              done();
            }, 200);
          });

        });

        describe('filter by predefined', function () {
          var f;
          beforeAll(function (done) {
            f = $el.filter.querySelectorAll('button');
            f = {
              admin: f[0],
              user: f[1],
              restricted: f[2], 
              private: f[3], 
              public: f[4], 
            };
            window.setTimeout(done, 100);
          });

          it('should filter when admin', function (done) {
            expect(groupsCount()).toBe(3);
            expect(f.admin.className).toBe('admin');
            f.admin.click();
            window.setTimeout(function () {
              expect(groupsCount()).toBe(3);
              expect(f.admin.className).toBe('admin active');
              f.admin.click();
              window.setTimeout(function () {
                expect(groupsCount()).toBe(3);
                expect(f.admin.className).toBe('admin');
                done();
              }, 200);
            }, 200);
          });

          it('should filter when user', function (done) {
            expect(groupsCount()).toBe(3);
            expect(f.user.className).toBe('user');
            f.user.click();
            window.setTimeout(function () {
              expect(groupsCount()).toBe(0);
              expect(f.user.className).toBe('user active');
              f.user.click();
              window.setTimeout(function () {
                expect(groupsCount()).toBe(3);
                expect(f.user.className).toBe('user');
                done();
              }, 200);
            }, 200);
          });

          it('should filter when visibility is restricted', function (done) {
            expect(groupsCount()).toBe(3);
            expect(f.restricted.className).toBe('user');
            f.restricted.click();
            window.setTimeout(function () {
              expect(groupsCount()).toBe(1);
              expect(f.restricted.className).toBe('user active');
              f.restricted.click();
              window.setTimeout(function () {
                expect(groupsCount()).toBe(3);
                expect(f.restricted.className).toBe('user');
                done();
              }, 200);
            }, 200);
          });

          it('should filter when visibility is private', function (done) {
            expect(groupsCount()).toBe(3);
            expect(f.private.className).toBe('user');
            f.private.click();
            window.setTimeout(function () {
              expect(groupsCount()).toBe(0);
              expect(f.private.className).toBe('user active');
              f.private.click();
              window.setTimeout(function () {
                expect(groupsCount()).toBe(3);
                expect(f.private.className).toBe('user');
                done();
              }, 200);
            }, 200);
          });

          it('should filter when visibility is public', function (done) {
            expect(groupsCount()).toBe(3);
            expect(f.public.className).toBe('user');
            f.public.click();
            window.setTimeout(function () {
              expect(groupsCount()).toBe(2);
              expect(f.public.className).toBe('user active');
              f.public.click();
              window.setTimeout(function () {
                expect(groupsCount()).toBe(3);
                expect(f.public.className).toBe('user');
                done();
              }, 200);
            }, 200);
          });

        });

        describe('filter by tags', function () {
          var t;
          beforeAll(function (done) {
            var tags = $el.tagsAside.querySelectorAll('li button');
            t = {
              cool: tags[0],
              funky: tags[1],
              weird: tags[2]
            };
            window.setTimeout(done, 100);
          });

          it('should filter with one tag', function (done) {
            expect(groupsCount()).toBe(3);
            expect(t.funky.className).toBe('');
            t.funky.click();
            window.setTimeout(function () {
              expect(groupsCount()).toBe(1);
              expect(t.funky.className).toBe('active');
              t.funky.click();
              window.setTimeout(function () {
                expect(groupsCount()).toBe(3);
                expect(t.funky.className).toBe('');
                done();
              }, 200);
            }, 200);
          });

          it('should filter with multiple tags', function (done) {
            expect(groupsCount()).toBe(3);
            expect(t.cool.className).toBe('');
            expect(t.weird.className).toBe('');
            t.cool.click();
            t.weird.click();
            window.setTimeout(function () {
              expect(groupsCount()).toBe(1);
              expect(t.cool.className).toBe('active');
              expect(t.weird.className).toBe('active');
              t.cool.click();
              t.weird.click();
              window.setTimeout(function () {
                expect(groupsCount()).toBe(3);
                expect(t.cool.className).toBe('');
                expect(t.weird.className).toBe('');
                done();
              }, 200);
            }, 200);
          });

        });

        describe('filters combined', function () {

          it('should filter with combined filters, here search and admin', 
            function (done) {
              expect(groupsCount()).toBe(3);
              fill($el.searchInput, 'MORI');
              $el.searchOk.click();
              var filterAdmin = $el.filter.querySelector('button');
              expect(filterAdmin.className).toBe('admin');
              filterAdmin.click();
              window.setTimeout(function () {
                expect(groupsCount()).toBe(1);
                expect(filterAdmin.className).toBe('admin active');
                fill($el.searchInput, '');
                $el.searchOk.click();
                filterAdmin.click();
                window.setTimeout(function () {
                  expect(filterAdmin.className).toBe('admin');
                  expect(groupsCount()).toBe(3);
                  done();
                }, 200);
              }, 200);
            }
          );

        });
      });
    });
  };

  return list;
}).call(this);


<|start_filename|>spec/frontend/entry.js<|end_filename|>
/**
*  vim:set sw=2 ts=2 sts=2 ft=javascript expandtab:
*
*  # Frontend functional testing Entry Point
  *
*  ## License
*
*  Licensed to the Apache Software Foundation (ASF) under one
*  or more contributor license agreements.  See the NOTICE file
*  distributed with this work for additional information
*  regarding copyright ownership.  The ASF licenses this file
*  to you 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.
*
*  ## Description
*
*  This module is the entry point for front JavaScript programming. It requires
*  global dependencies and main client-side file.
*/

(function () {
  var login = require('./login.spec.js');
  var subscribe = require('./subscribe.spec.js');
  var profile = require('./profile.spec.js');
  var passRecover = require('./passrecover.spec.js');
  var bookmark = require('./bookmark.spec.js');
  //var groupList = require('./group.spec.js');
  //var groupForm = require('./group-form.spec.js');
  //var groupRemove = require('./group-remove.spec.js');
  //var groupView = require('./group-view.spec.js');
  //var padForm = require('./pad-form.spec.js');
  //var userInvite = require('./user-invitation.spec.js');
  //var padView = require('./pad-view.spec.js');
  //var padMove = require('./pad-move.spec.js');
  //var userlist = require('./userlist.spec.js');
  //var tagWidget = require('./tag-widget.spec.js');
  //var admin = require('./admin.spec.js');
  //var adminUsers = require('./admin-users.spec.js');

  describe('MyPads testing', function () {
    var app = frames[0];
    beforeAll(function(done) {
      // FIXME: 1sec for simplicity but should listen search/hash changes...
      window.setTimeout(done, 1000);
    });
    login.run(app);
    subscribe.run(app);
    profile.run(app);
    passRecover.run(app);
    bookmark.run(app);
    //groupList.run(app);
    //groupForm.run(app);
    //groupRemove.run(app);
    //padForm.run(app);
    //groupView.run(app);
    //userInvite.run(app);
    //padView.run(app);
    //padMove.run(app);
    //userlist.run(app);
    //tagWidget.run(app);
    //admin.run(app);
    //adminUsers.run(app);
  });
}).call(this);


<|start_filename|>spec/backend/auth.spec.js<|end_filename|>
/**
*  vim:set sw=2 ts=2 sts=2 ft=javascript expandtab:
*
*  Licensed to the Apache Software Foundation (ASF) under one
*  or more contributor license agreements.  See the NOTICE file
*  distributed with this work for additional information
*  regarding copyright ownership.  The ASF licenses this file
*  to you 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.
*/

(function () {
  'use strict';
  var ld = require('lodash');
  var specCommon = require('./common.js');
  var common = require('../../model/common.js');
  var user = require('../../model/user.js');
  var auth = require('../../auth.js');

  describe('auth', function () {
    beforeAll(specCommon.reInitDatabase);
    afterAll(specCommon.reInitDatabase);

    describe('isPasswordValid', function () {
      var params;
      beforeAll(function (done) {
        common.hashPassword(null, 'password', function (err, res) {
          if (err) { console.log(err); }
          params = { password: <PASSWORD> };
          done();
        });
      });

      it('should return an Error if password is not given', function (done) {
        expect(auth.fn.isPasswordValid(params, undefined, function (err) {
          expect(ld.isError(err));
          expect(err).toMatch('PASSWORD_MISSING');
          done();
        }));
      });

      it('should return null and false if password does not match',
        function (done) {
          auth.fn.isPasswordValid(params, 'anotherOne', function (err, res) {
            expect(err).toBeNull();
            expect(res).toBeFalsy();
            done();
          });
        }
      );

      it('should return null and true if password does match', function (done) {
        auth.fn.isPasswordValid(params, 'password', function (err, res) {
          expect(err).toBeNull();
          expect(res).toBeTruthy();
          done();
        });
      });
    });

    describe('localFn and checkMyPadsUser', function () {
    var params;
    beforeAll(function (done) {
      specCommon.reInitDatabase(function () {
        params = {
          login: 'parker',
          password: '<PASSWORD>',
          firstname: 'Parker',
          lastname: 'Lewis',
          email: '<EMAIL>'
        };
        user.set(params, done);
      });
    });

      it('should not auth if user does not exist', function (done) {
        auth.fn.localFn('inexistent', 'none', function (err) {
          expect(ld.isError(err)).toBeTruthy();
          expect(err).toMatch('USER.NOT_FOUND');
          done();
        });
      });

      it('should not auth if password does not match', function (done) {
        auth.fn.localFn(params.login, 'anotherOne', function (err, res) {
          expect(ld.isError(err)).toBeTruthy();
          expect(err).toMatch('PASSWORD_INCORRECT');
          expect(res).toBeFalsy();
          done();
        });
      });

      it('should auth if login and password match', function (done) {
        auth.fn.localFn(params.login, params.password,
          function (err, res) {
            expect(err).toBeNull();
            expect(ld.isObject(res)).toBeTruthy();
            expect(res.login).toBe(params.login);
            expect(res.firstname).toBe(params.firstname);
            expect(res.lastname).toBe(params.lastname);
            expect(ld.isString(res.password.hash)).toBeTruthy();
            done();
          }
        );
      });
    });

    describe('checkAdminUser', function () {

      it('should not auth if admin does not exist', function (done) {
        auth.fn.checkAdminUser('inexistent', 'passw0rd', function (err, u) {
          expect(err).toMatch('USER.NOT_FOUND');
          expect(u).toBeNull();
          done();
        });
      });

      it('should not auth if password does not match', function (done) {
        auth.fn.checkAdminUser('admin', '<PASSWORD>', function (err, u) {
          expect(err).toMatch('AUTHENTICATION.PASSWORD_INCORRECT');
          expect(u).toBeFalsy();
          done();
        });
      });

      it('should not auth if user is not admin', function (done) {
        auth.fn.checkAdminUser('parker', 'lovesKubiak', function (err, u) {
          expect(err).toMatch('AUTHENTICATION.ADMIN');
          expect(u).toBeFalsy();
          done();
        });
      });

      it('should auth if login and password match', function (done) {
        auth.fn.checkAdminUser('admin', 'admin', function (err, u) {
          expect(err).toBeNull();
          expect(u).toBeDefined();
          done();
        });
      });

    });
  });


}).call(this);


<|start_filename|>spec/frontend/user-invitation.spec.js<|end_filename|>
/**
*  vim:set sw=2 ts=2 sts=2 ft=javascript expandtab:
*
*  # Group user invitation and admin sharing functional testing
*
*  ## License
*
*  Licensed to the Apache Software Foundation (ASF) under one
*  or more contributor license agreements.  See the NOTICE file
*  distributed with this work for additional information
*  regarding copyright ownership.  The ASF licenses this file
*  to you 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 = (function () {
  'use strict';
  // Dependencies
  var common = require('./common.js');
  var fill = common.helper.fill;

  var invite = {};

  // Shared variables
  var qfirst;
  var qall;

  invite.run = function (app) {

    qfirst = function (sel) { return app.document.querySelector(sel); };
    qall = function (sel) { return app.document.querySelectorAll(sel); };

    describe('group user invitation and admin sharing testing', function () {

      beforeAll(function (done) {
        // Login and go to group view page of a restricted group
        app.document.querySelector('header nav a:first-child').click();
        window.setTimeout(function () {
          fill(app.document.querySelector('input[name=login]'), 'parker');
          fill(app.document.querySelector('input[name=password]'),
            '<PASSWORD>');
          app.document.querySelector('input[type=submit]').click();
          window.setTimeout(function () {
            app.document.querySelectorAll('a[href$=view]')[0].click();
            window.setTimeout(function () {
              qfirst('body > section > div p').click();
              window.setTimeout(done, 100);
            }, 200);
          }, 200);
        }, 200);
      });

      afterAll(function (done) {
        app.document.querySelector('.icon-logout').parentNode.click();
        window.setTimeout(function () {
          app.document.querySelector('body > section p').click();
          done();
        }, 100);
      });

      describe('group properties', function () {

        it('should list users properties correctly', function (done) {
          var admins = qall('section.users ul li');
          window.setTimeout(function () {
            expect(admins.length).toBe(1);
            expect(admins[0].textContent).toMatch('Parker');
            done();
          }, 100);
        });

      });

      describe('userlist invitation', function () {

        beforeAll(function (done) {
          qfirst('a[href$=invite]').click();
            window.setTimeout(done, 100);
        });

        afterAll(function (done) {
          app.window.history.go(-1);
          window.setTimeout(done, 100);
        });

        it('should have no userlist selected per default', function () {
          expect(qfirst('select').value).toBe('');
          expect(qall('select option').length).toBe(2);
          expect(qfirst('fieldset ul').children.length).toBe(0);
        });

        it('should allow userlist choice and adds its users on selection',
          function (done) {
            var $ulist = qfirst('select');
            $ulist.value = qfirst('select option').value;
            $ulist.onchange($ulist);
            window.setTimeout(function () {
              expect(qall('fieldset ul li').length).toBe(2);
              window.setTimeout(done, 100);
            }, 100);
          }
        );

      });

      describe('user invitation', function () {
        var $el;
        var fillElements = function () {
          return {
            ipt: qfirst('input'),
            ok: qfirst('button.ok'),
            datalist: qfirst('datalist'),
            list: qfirst('fieldset ul'),
            submit: qfirst('input[type=submit]')
          };
        };

        beforeAll(function (done) {
          qfirst('a[href$=invite]').click();
          window.setTimeout(function () {
            $el = fillElements();
            window.setTimeout(done, 100);
          });
        });

        it('should display properties correcly', function (done) {
          var title = qfirst('section h2');
          var users = $el.list.querySelectorAll('li');
          window.setTimeout(function () {
            expect(title.textContent).toBe('Group Santa Fe');
            expect($el.datalist.children.length).toBe(0);
            expect(users.length).toBe(0);
            window.setTimeout(done, 100);
          }, 100);
        });

        it('should allow entering new users by login or email and ' +
          'removing some of them', function (done) {
            fill($el.ipt, 'jerry');
            $el.ok.click();
            fill($el.ipt, '<EMAIL>');
            $el.ok.click();
            fill($el.ipt, 'johnny');
            $el.ok.click();
            fill($el.ipt, 'parker');
            $el.ok.click();
            window.setTimeout(function () {
              var users = $el.list.querySelectorAll('li');
              window.setTimeout(function () {
                expect(users.length).toBe(4);
                expect(users[0].textContent).toBe('jerry');
                expect(users[1].textContent).toBe('<EMAIL>');
                expect(users[2].textContent).toBe('johnny');
                expect(users[3].textContent).toBe('parker');
                users[2].querySelector('i.icon-cancel').click();
                window.setTimeout(function () {
                  users = $el.list.querySelectorAll('li');
                  window.setTimeout(function () {
                    expect(users.length).toBe(3);
                    expect(users[0].textContent).toBe('jerry');
                    expect(users[1].textContent).toBe('<EMAIL>');
                    expect(users[2].textContent).toBe('parker');
                    expect($el.datalist.children.length).toBe(1);
                    expect($el.datalist.children[0].value).toBe('johnny');
                    window.setTimeout(done, 100);
                  }, 100);
                }, 100);
              }, 100);
            }, 200);
          }
        );

        it('should update groups with existing users only', function (done) {
          $el.submit.click();
          window.setTimeout(function () {
            var $notif = qfirst('body > section.notification');
            expect($notif.textContent)
              .toMatch('achieved for <EMAIL>, parker');
            expect($notif.textContent)
              .toMatch('no account in this instance : jerry');
            var notif = $notif.querySelectorAll('p');
            notif[0].click();
            notif[1].click();
            var users = qall('section.users ul')[1].querySelectorAll('li');
            window.setTimeout(function () {
              expect(users.length).toBe(1);
              expect(users[0].textContent).toMatch('frank');
              qfirst('a[href$=invite]').click();
              window.setTimeout(function () {
                $el = fillElements();
                window.setTimeout(function () {
                  users = $el.list.querySelectorAll('li');
                  window.setTimeout(function () {
                    expect(users.length).toBe(1);
                    expect(users[0].textContent).toBe('frank');
                    done();
                  }, 100);
                }, 100);
              }, 200);
            }, 200);
          }, 200);
        });

        it('should allow removing all users', function (done) {
          var submit = qfirst('input[type=submit]');
          qfirst('fieldset ul li i.icon-cancel').click();
          window.setTimeout(function () {
            submit.click();
            window.setTimeout(function () {
              var $notif = qfirst('body > section.notification p');
              expect($notif.textContent).toMatch('successfully achieved');
              $notif.click();
              var users = qfirst('section.users p');
              window.setTimeout(function () {
                expect(users.textContent).toMatch('No user');
                qfirst('a[href$=invite]').click();
                window.setTimeout(function () {
                  $el = fillElements();
                  window.setTimeout(function () {
                    expect($el.list.children.length).toBe(0);
                    done();
                  }, 100);
                }, 100);
              }, 100);
            }, 200);
          }, 200);
        });

      });

    });

  };

  return invite;

}).call(this);


<|start_filename|>spec/frontend/common.js<|end_filename|>
/**
*  vim:set sw=2 ts=2 sts=2 ft=javascript expandtab:
*
*  # Common for functional testing
*
*  ## License
*
*  Licensed to the Apache Software Foundation (ASF) under one
*  or more contributor license agreements.  See the NOTICE file
*  distributed with this work for additional information
*  regarding copyright ownership.  The ASF licenses this file
*  to you 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.
*
*  ## Description
*
*  Here are shared functions and helpers for client functional testing.
*/

module.exports = (function () {
  var common = {};
  /*
  * ## Helpers
  *
  * Functions helpers for functional testing.
  */

  common.helper = {};

  /**
  * ### fill
  *
  * `fill` is a function, taking an HTML input `el` *Element* and a string
  * `val`ue, that fixes the `val` to the *Element* and dispatches an *input*
  * *KeyboardEvent*. Then mithril *oninput* usage is filled.
  */

  common.helper.fill = function (el, val) {
    el.value = val;
    el.dispatchEvent(new window.KeyboardEvent('input'));
  };

  /**
  * ## user
  *
  * Helpers for `user` testing : profile, login, subscription...
  */

  common.user = {};

  /**
  * ### beforeAll
  *
  * `beforeAll` takes the iframe `app`, `nSel` navigation selector and a
  * `callback` function.
  * It simulates a click on the navigation element and returns an object with
  *
  * - local query `first` function,
  * - `hash` shortcut,
  * - generated `$el` elements as a JS Object via the callback function.
  */

  common.user.beforeAll = function (app, nSel, callback) {
    app.document.querySelector(nSel).click();
    window.setTimeout(function () {
      var first = function (sel) { return app.document.querySelector(sel); };
      callback({
        hash: app.location.search,
        first: first,
        $el: {
          form: first('form'),
          password: first('input[name=password]'),
          passwordConfirm: first('input[name=passwordConfirm]'),
          email: first('input[name=email]'),
          lang: first('select[name=lang]'),
          firstname: first('input[name=firstname]'),
          lastname: first('input[name=lastname]'),
          organization: first('input[name=organization]'),
          color: first('input[name=color]'),
          submit: first('input[type=submit]')
        }
      });
    }, 200);
  };

  return common;
}).call(this);


<|start_filename|>spec/frontend/group-form.spec.js<|end_filename|>
/**
*  vim:set sw=2 ts=2 sts=2 ft=javascript expandtab:
*
*  # Group form functional testing
*
*  ## License
*
*  Licensed to the Apache Software Foundation (ASF) under one
*  or more contributor license agreements.  See the NOTICE file
*  distributed with this work for additional information
*  regarding copyright ownership.  The ASF licenses this file
*  to you 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 = (function () {
  'use strict';
  // Dependencies
  var common = require('./common.js');
  var fill = common.helper.fill;

  var form = {};

  // Shared variables
  var $el;
  var first;

  form.run = function (app) {

    first = function (sel) { return app.document.querySelector(sel); };

    describe('group form module testing', function () {

      beforeAll(function (done) {
        // Login and go to form add page
        app.document.querySelector('header nav a:first-child').click();
        window.setTimeout(function () {
          fill(app.document.querySelector('input[name=login]'), 'parker');
          fill(app.document.querySelector('input[name=password]'),
            '<PASSWORD>');
          app.document.querySelector('input[type=submit]').click();
          window.setTimeout(function () {
            app.document.querySelector('a[href$=\'mypads/group/add\']').click();
            window.setTimeout(function () {
              $el = { 
                form: first('form'),
                name: first('input[name=name]'),
                description: first('textarea[name=description]'),
                visibility: first('select[name=visibility]'),
                readonly: first('input[name=readonly]'),
                submit: first('input[type=submit]')
              };
              first('body > section > div p').click();
              window.setTimeout(done, 100);
            }, 200);
          }, 200);
        }, 200);
      });

      afterAll(function (done) {
        first('.icon-logout').parentNode.click();
        window.setTimeout(function () {
          first('body > section p').click();
          done();
        }, 100);
      });

      describe('group creation', function () {

        it('should forbid creation of a group with no fill', function (done) {
          $el.submit.click();
          expect($el.form.checkValidity()).toBeFalsy();
          expect($el.name.checkValidity()).toBeFalsy();
          expect($el.description.checkValidity()).toBeTruthy();
          expect($el.visibility.checkValidity()).toBeTruthy();
          expect($el.visibility.value).toBe('restricted');
          expect($el.readonly.checkValidity()).toBeTruthy();
          done();
        });

        it('should forbid creation of a private group with no password',
          function (done) {
            fill($el.name, 'High School Memories');
            $el.visibility.value = 'private';
            $el.visibility.onchange($el.visibility);
            window.setTimeout(function () {
              expect($el.name.checkValidity()).toBeTruthy();
              expect($el.visibility.value).toBe('private');
              $el.password = first('input[type=password]');
              $el.password.value = '';
              $el.submit.click();
              expect($el.form.checkValidity()).toBeFalsy();
              expect($el.password.checkValidity()).toBeFalsy();
              done();
            }, 100);
          }
        );

        it('should create otherwise', function (done) {
          fill($el.password, '<PASSWORD>');
          $el.readonly.checked = false;
          $el.readonly.onchange($el.readonly);
          window.setTimeout(function () {
            $el.submit.click();
            window.setTimeout(function () {
              var $notif = first('body > section p');
              expect($notif.innerHTML)
                .toMatch('Group has been successfully created');
              $notif.click();
              window.setTimeout(done, 100);
            }, 200);
          }, 200);
        });

      });

      describe('group edition', function () {

        beforeEach(function (done) {
          app.document.querySelector('a[title=Configuration]').click();
          window.setTimeout(function () {
            $el = { 
              form: first('form'),
              name: first('input[name=name]'),
              description: first('textarea[name=description]'),
              visibility: first('select[name=visibility]'),
              password: first('input[type=password]'),
              readonly: first('input[name=readonly]'),
              submit: first('input[type=submit]')
            };
            window.setTimeout(done, 200);
          }, 200);
        });

        it('should accept changes and letting password empty', function (done) {
          expect($el.name.value).toBe('High School Memories');
          expect($el.visibility.value).toBe('private');
          expect($el.readonly.checked).toBeFalsy();
          expect($el.password.value).toBe('');
          fill($el.name, 'High School Memories, again');
          fill($el.description, 'Something quite private.');
          $el.description.onchange($el.description);
          window.setTimeout(function () {
            $el.submit.click();
            window.setTimeout(function () {
              var $notif = first('body > section p');
              expect($notif.innerHTML)
                .toMatch('Group has been successfully updated');
              $notif.click();
              window.setTimeout(done, 100);
            }, 200);
          }, 200);
        });

        it('should accept changes and setting a new password', function (done) {
          expect($el.name.value).toBe('High School Memories, again');
          expect($el.description.value).toBe('Something quite private.');
          $el.password.value = '<PASSWORD>';
          $el.submit.click();
          window.setTimeout(function () {
            var $notif = first('body > section p');
            expect($notif.innerHTML)
              .toMatch('Group has been successfully updated');
            $notif.click();
            window.setTimeout(done, 100);
          }, 200);
        });

      });

    });
  };

  return form;
}).call(this);


<|start_filename|>spec/backend/api.spec.js<|end_filename|>
/**
*  vim:set sw=2 ts=2 sts=2 ft=javascript expandtab:
*
*  Licensed to the Apache Software Foundation (ASF) under one
*  or more contributor license agreements.  See the NOTICE file
*  distributed with this work for additional information
*  regarding copyright ownership.  The ASF licenses this file
*  to you 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.
*/

(function () {
  'use strict';

  var ld = require('lodash');
  var request = require('request');
  var jwt = require('jsonwebtoken');
  var encode = require('js-base64').Base64.encode;
  var api = require('../../api.js');
  var mail = require('../../mail.js');
  var auth = require('../../auth.js');
  var user = require('../../model/user.js');
  var group = require('../../model/group.js');
  var pad = require('../../model/pad.js');
  var specCommon = require('./common.js');

  describe('MyPads API', function () {
    /**
    * For standalone backend testing : mocking a fresh Express app and
    * initializate API routes.
    */
    var route = 'http://127.0.0.1:8042' + api.initialRoute;
    var admToken;
    var rq;
    var conf = require('../../configuration.js');

    /* Global local variables */
    var guest = {
      login: 'guest',
      password: '<PASSWORD>',
      email: '<EMAIL>'
    };
    var etherAdmin = {
      login: 'admin',
      password: '<PASSWORD>',
      is_admin: 'true'
    };

    beforeAll(function (done) {
      specCommon.mockupExpressServer();
      specCommon.reInitDatabase(function () {
        conf.init(function () {
          api.init(specCommon.express.app, function () {
            auth.adminTokens[etherAdmin.login] = etherAdmin;
            admToken = jwt.sign(etherAdmin, auth.secret);
            rq = request.defaults({ json: true });
            setTimeout(done, 500);
          });
        });
      });
    });

    afterAll(function (done) {
      auth.adminTokens = {};
      specCommon.unmockExpressServer();
      specCommon.reInitDatabase(done);
    });

    describe('index.html template', function () {
      beforeAll(function () {
        conf.cache.HTMLExtraHead = '<style>/* custom HTML for head */</style>';
      });
      afterAll(function () {
        conf.cache.HTMLExtraHead = '';
        conf.cache.hideHelpBlocks = false;
      });

      it('should include custom HTML for head', function (done) {
        rq.get('http://127.0.0.1:8042/mypads/index.html',
          function (err, resp, body) {
            expect(resp.statusCode).toBe(200);
            expect(body).toMatch('custom HTML for head');
            done();
          }
        );
      });
      it('should not have body with hidehelpblocks class if hideHelpBlocks is false', function (done) {
        rq.get('http://127.0.0.1:8042/mypads/index.html',
          function (err, resp, body) {
            expect(resp.statusCode).toBe(200);
            expect(body).not.toMatch('body class="hidehelpblocks"');
            done();
          }
        );
      });
      it('should have body with hidehelpblocks class if hideHelpBlocks is true', function (done) {
        conf.cache.hideHelpBlocks = true;
        rq.get('http://127.0.0.1:8042/mypads/index.html',
          function (err, resp, body) {
            expect(resp.statusCode).toBe(200);
            expect(body).toMatch('body class="hidehelpblocks"');
            done();
          }
        );
      });
    });

    describe('authentification API', function () {
      var authRoute = route + 'auth';

      beforeAll(function (done) {
        specCommon.reInitDatabase(function () {
          user.set(guest, done);
        });
      });
      afterAll(specCommon.reInitDatabase);

      describe('auth.check POST', function () {

        var token;

        beforeAll(function (done) {
          var params = { body: guest };
          rq.post(authRoute + '/login', params, function (err, resp, body) {
            if (!err && resp.statusCode === 200) {
              token = body.token;
              done();
            }
          });
        });
        afterAll(function (done) {
          var b = { body: { auth_token: token } };
          rq.get(authRoute + '/logout', b, done);
        });

        it('should return an error if params are inexistent', function (done) {
          rq.post(authRoute + '/check', {}, function (err, resp) {
            expect(resp.statusCode).toBe(401);
            done();
          });
        });

        it('should return an error if params are bad', function(done) {
          var params = {
            body: {
              login: 'guest',
              password: '<PASSWORD>',
              email: '<EMAIL>',
              auth_token: jwt.sign({}, 'bad')
            } 
          };
          rq.post(authRoute + '/check', params, function (err, resp) {
            expect(resp.statusCode).toBe(401);
            done();
          });
        });

        it('should return success otherwise', function (done) {
          var params = {
            body: {
              login: 'guest',
              password: '<PASSWORD>',
              email: '<EMAIL>',
              auth_token: token
            } 
          };
          rq.post(authRoute + '/check', params, function (err, resp, body) {
            expect(resp.statusCode).toBe(200);
            expect(body.success).toBeTruthy();
            done();
          });
        });

      });

      describe('auth.login POST', function () {

        it('should not auth if params are inexistent', function (done) {
          rq.post(authRoute + '/login', {}, function (err, resp, body) {
            expect(resp.statusCode).toBe(400);
            expect(body.error).toMatch('PASSWORD_STR');
            done();
          });
        });

        it('should not auth if params are incorrect', function (done) {
          var params = { body: { login: 'inexistent', password: <PASSWORD> } };
          rq.post(authRoute + '/login', params, function (err, resp, body) {
            expect(resp.statusCode).toBe(400);
            expect(body.error).toMatch('PASSWORD_STR');
            done();
          });
        });

        it('should not auth if user does not exist', function (done) {
          var params = { body: { login: 'inexistent', password: '<PASSWORD>' } };
          rq.post(authRoute + '/login', params, function (err, resp, body) {
            expect(resp.statusCode).toBe(400);
            expect(body.error).toMatch('USER.NOT_FOUND');
            done();
          });
        });

        it('should not auth if user exists but pasword does not match',
          function (done) {
            var params = { body: ld.clone(guest) };
            params.body.password = '<PASSWORD>';
            rq.post(authRoute + '/login', params, function (err, resp, body) {
              expect(resp.statusCode).toBe(400);
              expect(body.error).toMatch('PASSWORD_INCORRECT');
              done();
            });
          }
        );

        it('should auth otherwise', function (done) {
          var params = { body: guest };
          rq.post(authRoute + '/login', params, function (err, resp, body) {
            expect(resp.statusCode).toBe(200);
            expect(body.success).toBeTruthy();
            expect(body.user).toBeDefined();
            expect(body.user._id).toBeDefined();
            expect(body.user.login).toBe('guest');
            expect(body.user.password).toBeUndefined();
            done();
          });
        });
      });

      describe('auth.logout GET', function () {

        it('should not logout if not already authenticated', function (done) {
          rq.get(authRoute + '/logout', function (err, resp) {
            expect(err).toBeNull();
            expect(resp.statusCode).toBe(401);
            done();
          });
        });

        it('should logout if authenticated', function (done) {
          var params = { body: guest };
          rq.post(authRoute + '/login', params, function (err, resp, body) {
            expect(resp.statusCode).toBe(200);
            expect(body.success).toBeTruthy();
            var params = { body: { auth_token: body.token } };
            rq.get(authRoute + '/logout', params, function (err, resp, body) {
              expect(err).toBeNull();
              expect(resp.statusCode).toBe(200);
              expect(body.success).toBeTruthy();
              rq.get(route + 'pad/inexistent',
                function (err, resp, body) {
                  expect(err).toBeNull();
                  expect(body.error).toMatch('KEY_NOT_FOUND');
                  done();
                }
              );
            });
          });
        });

      });

      describe('auth.adminlogin POST', function () {

        it('should not auth if params are inexistent', function (done) {
          rq.post(authRoute + '/admin/login', {}, function (err, resp, body) {
            expect(resp.statusCode).toBe(400);
            expect(body.error).toMatch('PASSWORD_STR');
            done();
          });
        });

        it('should not auth if params are incorrect', function (done) {
          var params = { body: { login: 'inexistent', password: <PASSWORD> } };
          rq.post(authRoute + '/admin/login', params,
            function (err, resp, body) {
              expect(resp.statusCode).toBe(400);
              expect(body.error).toMatch('PASSWORD_STR');
              done();
            }
          );
        });

        it('should not auth if user does not exist', function (done) {
          var params = { body: { login: 'inexistent', password: '<PASSWORD>' } };
          rq.post(authRoute + '/admin/login', params,
            function (err, resp, body) {
              expect(resp.statusCode).toBe(400);
              expect(body.error).toMatch('USER.NOT_FOUND');
              done();
            }
          );
        });

        it('should not auth if user exists but password does not match',
          function (done) {
            var params = { body: ld.clone(etherAdmin) };
            params.body.password = '<PASSWORD>';
            rq.post(authRoute + '/admin/login', params,
              function (err, resp, body) {
                expect(resp.statusCode).toBe(400);
                expect(body.error).toMatch('PASSWORD_INCORRECT');
                done();
              }
            );
          }
        );

        it('should auth otherwise', function (done) {
          var params = { body: etherAdmin };
          rq.post(authRoute + '/admin/login', params,
            function (err, resp, body) {
              expect(err).toBeNull();
              expect(resp.statusCode).toBe(200);
              expect(body.success).toBeTruthy();
              expect(body.token).toBeDefined();
              admToken = body.token;
              done();
            }
          );
        });

      });

      describe('auth.adminlogout GET', function () {});

    });

    describe('unAuth legitimate tests', function () {

      describe('configuration.public GET', function () {
        var confRoute = route + 'configuration';

        beforeAll(function (done) {
          conf.set('title', 'Amigo', function () {
            conf.set('field', 3, done);
          });
        });

        afterAll(specCommon.reInitDatabase);

        it('should reply with all public setting with GET method',
          function (done) {
            rq.get(confRoute, function (err, resp, body) {
              expect(err).toBeNull();
              expect(resp.statusCode).toBe(200);
              expect(ld.isObject(body.value)).toBeTruthy();
              expect(body.value.title).toBe('Amigo');
              expect(body.value.field).toBeUndefined();
              done();
            });
          }
        );
      });
    });

    describe('configuration API', function () {
      var confRoute = route + 'configuration';
      var token;

      beforeAll(function (done) {
        var kv = { field1: 8, field2: 3, field3: ['a', 'b'] };
        ld.assign(conf.cache, kv);
        user.set(guest, function () {
          rq.post(route + 'auth/login', { body: guest },
            function (err, resp, b) {
              if (!err && resp.statusCode === 200) {
                token = b.token;
                done();
              }
            }
          );
        });
      });

      afterAll(function (done) {
        rq.get(route + 'auth/logout', { body: { auth_token: token } },  done);
      });

      describe('configuration.all GET', function () {
        it('should not reply to DELETE method here', function (done) {
          rq.del(confRoute, function (err, resp, body) {
            expect(resp.statusCode).toBe(404);
            expect(body).toMatch('Cannot DELETE');
            done();
          });
        });

        it('should reply with filtered settings with GET and no admin role',
          function (done) {
            var params = { body: { auth_token: token } };
            rq.get(confRoute, params, function (err, resp, body) {
              expect(err).toBeNull();
              expect(resp.statusCode).toBe(200);
              expect(ld.isObject(body.value)).toBeTruthy();
              expect(body.value.title).toBeDefined();
              expect(body.value.field1).toBeUndefined();
              expect(body.value.field2).toBeUndefined();
              done();
            });
          }
        );

        it('should reply with all settings with GET method if admin',
          function (done) {
            var b = { body: { auth_token: admToken } };
            rq.get(confRoute, b, function (err, resp, body) {
              expect(err).toBeNull();
              expect(resp.statusCode).toBe(200);
              expect(ld.isObject(body.value)).toBeTruthy();
              expect(body.value.field1).toBe(8);
              expect(body.value.field2).toBe(3);
              expect(ld.size(body.value.field3)).toBe(2);
              expect(body.auth).toBeTruthy();
              done();
            });
          }
        );
      });

      describe('configuration.get GET key', function () {

        it('should return an error if the user if not an etherpad admin',
          function (done) {
            rq.get(confRoute + '/whatever', function (err, resp, body) {
              expect(resp.statusCode).toBe(401);
              expect(body.error).toMatch('AUTHENTICATION.ADMIN');
              done();
            });
          }
        );

        it('should return an error if the field does not exist',
          function (done) {
            var b = { body: { auth_token: <PASSWORD>Token } };
            rq.get(confRoute + '/inexistent', b, function (err, resp, body) {
              expect(resp.statusCode).toBe(404);
              expect(body.error).toMatch('CONFIGURATION.KEY_NOT_FOUND');
              expect(body.key).toBe('inexistent');
              done();
            });
          }
        );

        it('should give the key and the value otherwise', function (done) {
          var b = { body: { auth_token: <PASSWORD>Token } };
          rq.get(confRoute + '/field1', b, function (err, resp, body) {
            expect(resp.statusCode).toBe(200);
            expect(body.key).toBe('field1');
            expect(body.value).toBe(8);
            done();
          });
        });
      });

      describe('configuration.set POST/PUT key value', function () {

        it('post : should return an error if key and/or value are not provided',
          function (done) {
            var b = { body: { auth_token: <PASSWORD>Token } };
            rq.post(confRoute, b, function (err, resp, body) {
              expect(resp.statusCode).toBe(400);
              expect(body.error).toMatch('KEY_STR');
              b.body.key = 'field1';
              rq.post(confRoute, b, function (err, resp, body) {
                expect(resp.statusCode).toBe(400);
                expect(body.error).toMatch('VALUE_REQUIRED');
                done();
              });
            });
          }
        );

        it('put : should return an error if key is not in URL and if no value',
          function (done) {
            var b = { body: { auth_token: <PASSWORD>Token } };
            rq.put(confRoute, b, function (err, resp, body) {
              expect(resp.statusCode).toBe(404);
              expect(body).toMatch('Cannot PUT');
              rq.put(confRoute + '/field1', b, function (err, resp, body) {
                expect(resp.statusCode).toBe(400);
                expect(body.error).toMatch('VALUE_REQUIRED');
                done();
              });
            });
          }
        );

        it('post : should save good request like expected', function (done) {
          var b = { body: {
            key: 'field1',
            value: 'éèà',
            auth_token: <PASSWORD>Token
          } };
          rq.post(confRoute, b, function (err, resp, body) {
            expect(err).toBeNull();
            expect(resp.statusCode).toBe(200);
            expect(body.success).toBeTruthy();
            expect(body.key).toBe(b.body.key);
            expect(body.value).toBe(b.body.value);
            rq.get(confRoute + '/' + b.body.key, b, function (err, resp, body) {
              expect(resp.statusCode).toBe(200);
              expect(body.key).toBe(b.body.key);
              expect(body.value).toBe(b.body.value);
              done();
            });
          });
        });

        it('put : should save good request like expected', function (done) {
          var b = { body: { value: 42, auth_token: <PASSWORD>Token } };
          rq.put(confRoute + '/field1', b, function (err, resp, body) {
            expect(err).toBeNull();
            expect(resp.statusCode).toBe(200);
            expect(body.success).toBeTruthy();
            expect(body.key).toBe('field1');
            expect(body.value).toBe(b.body.value);
            rq.get(confRoute + '/field1', b, function (err, resp, body) {
              expect(resp.statusCode).toBe(200);
              expect(body.key).toBe('field1');
              expect(body.value).toBe(b.body.value);
              done();
            });
          });
        });


      });

      describe('configuration.del DELETE key', function () {

        it('will not return an error if the field does not exist',
          function (done) {
            var b = { body: { auth_token: admToken } };
            rq.del(confRoute + '/inexistent', b, function (err, resp, body) {
              expect(resp.statusCode).toBe(200);
              expect(body.success).toBeTruthy();
              expect(body.key).toBe('inexistent');
              done();
            });
          }
        );

        it('should deletes the record and returns the key, value and success' +
          ' otherwise', function (done) {
            var b = { body: { auth_token: admToken } };
            rq.del(confRoute + '/field1', b, function (err, resp, body) {
              expect(resp.statusCode).toBe(200);
              expect(body.success).toBeTruthy();
              expect(body.key).toBe('field1');
              rq.get(confRoute + '/field1', b, function (err, resp, body) {
                expect(resp.statusCode).toBe(404);
                expect(body.error).toMatch('CONFIGURATION.KEY_NOT_FOUND');
                expect(body.key).toBe('field1');
                done();
              });
            });
          }
        );

      });

      describe('configuration.public GET usefirstlastname', function () {

        it('should return the value of useFirstLastNameInPads',
          function (done) {
            rq.get(confRoute + '/public/usefirstlastname', function (err, resp, body) {
              expect(resp.statusCode).toBe(200);
              expect(body.success).toBeTruthy();
              expect(body.usefirstlastname).toBeDefined();
              done();
            });
          }
        );
      });

    });

    describe('user inactive account limitations', function () {

      beforeAll(function (done) {
        conf.cache.checkMails = true;
        var params = {
          login: 'shelly',
          password: '<PASSWORD>',
          email: '<EMAIL>',
          firstname: 'Shelly',
          lastname: 'Lewis'
        };
        user.set(params, done);
      });

      afterAll(function (done) {
        conf.cache.checkMails = false;
        rq.get(route + 'auth/logout', done);
      });

      it('should create in inactive account according to the configuration',
        function (done) {
          var b =  { login: 'shelly', password: '<PASSWORD>' };
          rq.post(route + 'auth/login', { body: b },
            function (err, resp, body) {
              expect(err).toBeNull();
              expect(body.error).toMatch('ACTIVATION_NEEDED');
              done();
            }
          );
        }
      );

    });

    describe('user API', function () {
      var userRoute = route + 'user';
      var allUsersRoute = route + 'all-users';
      var userlistRoute = route + 'userlist';
      var token;

      beforeAll(function (done) {
        conf.init(function () {
          var set = require('../../model/user.js').set;
          set(guest, function () {
            var p = {
              login: 'jerry',
              password: '<PASSWORD>',
              email: '<EMAIL>'
            };
            set(p, function () {
              p.login = 'mikey';
              p.email = '<EMAIL>';
              set(p, function () {
                rq.post(route + 'auth/login', { body: guest },
                  function (err, resp, b) {
                    if (!err && resp.statusCode === 200) {
                      token = b.token;
                      done();
                    }
                  }
                );
              });
            });
          });
        });
      });

      describe('user.set/add POST and value as params', function () {

        it('should return error when arguments are not as expected',
          function (done) {
            rq.post(userRoute, function (err, resp, body) {
              expect(resp.statusCode).toBe(400);
              expect(body.error).toMatch('PARAM_STR');
              var b = { body: { login: 'parker', password: '' } };
              rq.post(userRoute, b, function (err, resp, body) {
                expect(resp.statusCode).toBe(400);
                expect(body.error).toMatch('PARAM_STR');
                b = { body: {
                  login: 'parker',
                  password: '<PASSWORD>',
                  email: '<EMAIL>'
                } };
                rq.post(userRoute, b, function (err, resp, body) {
                  expect(resp.statusCode).toBe(400);
                  expect(body.error).toMatch('PASSWORD_SIZE');
                  done();
                });
              });
            });
          }
        );

        it('should return an error if password size is not correct',
          function (done) {
            var b = { body: {
              login: 'parker',
              password: '1',
              email: '<EMAIL>'
            } };
            rq.post(userRoute, b, function (err, resp, body) {
              expect(resp.statusCode).toBe(400);
              expect(body.error).toMatch('PASSWORD_SIZE');
              done();
            });
          }
        );

        it('should create a new user otherwise', function (done) {
          var b = {
            body: {
              login: 'parker',
              password: '<PASSWORD>',
              firstname: 'Parker',
              lastname: 'Lewis',
              email: '<EMAIL>'
            }
          };
          rq.post(userRoute, b, function (err, resp, body) {
            expect(err).toBeNull();
            expect(resp.statusCode).toBe(200);
            expect(body.success).toBeTruthy();
            expect(body.key).toBe('parker');
            expect(body.value.login).toBe('parker');
            expect(body.value.lastname).toBe('Lewis');
            b = { body: { auth_token: admToken } };
            rq.get(userRoute + '/parker', b, function (err, resp, body) {
              expect(resp.statusCode).toBe(200);
              expect(body.value.login).toBe('parker');
              expect(body.value.firstname).toBe('Parker');
              expect(ld.isArray(body.value.groups)).toBeTruthy();
              done();
            });
          });
        });

        it('should return an error if the login/key already exists',
          function (done) {
            var b = { body: {
              login: 'mikey',
              password: '<PASSWORD>',
              email: '<EMAIL>'
            } };
            rq.post(userRoute, b, function () {
              rq.post(userRoute, b, function (err, resp, body) {
                expect(resp.statusCode).toBe(400);
                expect(body.error).toMatch('USER.ALREADY_EXISTS');
                done();
              });
            });
          }
        );

        it('should return an error if the email already exists',
          function (done) {
            var b = { body: {
              login: 'martin',
              password: '<PASSWORD>',
              email: '<EMAIL>'
            } };
            rq.post(userRoute, b, function () {
              rq.post(userRoute, b, function (err, resp, body) {
                expect(resp.statusCode).toBe(400);
                expect(body.error).toMatch('USER.EMAIL_ALREADY_EXISTS');
                done();
              });
            });
          }
        );

        it('should not create a new user if registration is disabled', function (done) {
          var b = {
            body: {
              login: 'bender',
              password: '<PASSWORD>',
              firstname: 'Bender',
              lastname: 'Rodriguez',
              email: '<EMAIL>'
            }
          };
          conf.cache.openRegistration = false;
          rq.post(userRoute, b, function (err, resp, body) {
            expect(err).toBeNull();
            expect(resp.statusCode).toBe(400);
            expect(body.error).toMatch('AUTHENTICATION.NO_REGISTRATION');
            b = { body: { auth_token: admToken } };
            rq.get(userRoute + '/bender', b, function (err, resp, body) {
              expect(resp.statusCode).toBe(404);
              expect(body.error).toMatch('USER.NOT_FOUND');
              done();
            });
          });
        });

      });

      describe('user.set PUT key in URL and value as params', function () {

        it('should return error when arguments are not as expected',
          function (done) {
            var b = { body: { auth_token: token } };
            rq.put(userRoute + '/guest', b, function (err, resp, body) {
              expect(resp.statusCode).toBe(400);
              expect(body.error).toMatch('PARAM_STR');
              b = { body: { login: 'guest', password: '' } };
              b.body.auth_token = token;
              rq.put(userRoute + '/guest', b, function (err, resp, body) {
                expect(resp.statusCode).toBe(400);
                expect(body.error).toMatch('PARAM_STR');
                b = { body: { login: 'guest', password: '<PASSWORD>' } };
                b.body.auth_token = token;
                rq.put(userRoute + '/guest', b, function (err, resp, body) {
                  expect(resp.statusCode).toBe(400);
                  expect(body.error).toMatch('MAIL');
                  b.body.email = '<EMAIL>';
                  rq.put(userRoute + '/guest', b, function (err, resp, body) {
                    expect(resp.statusCode).toBe(400);
                    expect(body.error).toMatch('PASSWORD_SIZE');
                    done();
                  });
                });
              });
            });
          }
        );

        it('should return an error if password size is not correct',
          function (done) {
            var b = { body: {
              login: 'guest',
              password: '1',
              email: '<EMAIL>'
            } };
            b.body.auth_token = token;
            rq.put(userRoute + '/guest', b, function (err, resp, body) {
              expect(resp.statusCode).toBe(400);
              expect(body.error).toMatch('PASSWORD_SIZE');
              done();
            });
          }
        );

        it('should not be allowed to create a user', function (done) {
          var b = {
            body: {
              password: '<PASSWORD>',
              firstname: 'Parker',
              lastname: 'Lewis',
              email: '<EMAIL>',
              auth_token: token
            }
          };
          rq.put(userRoute + '/parker', b, function (err, resp, body) {
            expect(err).toBeNull();
            expect(resp.statusCode).toBe(401);
            expect(body.error).toMatch('DENIED');
            done();
          });
        });

        it('should create a user, if admin, otherwise', function (done) {
          var b = {
            body: {
              password: '<PASSWORD>ak',
              firstname: 'Parker',
              lastname: 'Lewis',
              email: '<EMAIL>',
              auth_token: admToken
            }
          };
          rq.put(userRoute + '/parker', b, function (err, resp, body) {
            expect(err).toBeNull();
            expect(resp.statusCode).toBe(200);
            expect(body.success).toBeTruthy();
            expect(body.key).toBe('parker');
            expect(body.value.login).toBe('parker');
            expect(body.value.lastname).toBe('Lewis');
            rq.get(userRoute + '/parker', b, function (err, resp, body) {
              expect(resp.statusCode).toBe(200);
              expect(body.value.login).toBe('parker');
              expect(body.value.firstname).toBe('Parker');
              expect(ld.isArray(body.value.groups)).toBeTruthy();
              done();
            });
          });
        });

        it('should accept changes with no password if admin', function (done) {
          var b = { body: { organization: 'secret', auth_token: admToken } };
          rq.put(userRoute + '/parker', b, function (err, resp, body) {
            expect(err).toBeNull();
            expect(resp.statusCode).toBe(200);
            expect(body.success).toBeTruthy();
            expect(body.key).toBe('parker');
            expect(body.value.login).toBe('parker');
            expect(body.value.organization).toBe('secret');
            done();
          });
        });

        it('should accept updates on an existing user, if he is logged himself',
          function (done) {
            var b = { body: guest };
            b.body.email =  '<EMAIL>';
            b.body.auth_token = token;
            rq.put(userRoute + '/guest', b, function (err, resp, body) {
              expect(resp.statusCode).toBe(200);
              expect(body.success).toBeTruthy();
              expect(body.key).toBe('guest');
              expect(body.value.email).toBe('<EMAIL>');
              done();
            });
          }
        );

        xit('should accept login change',
          function (done) {
            var b = { body: { password: '<PASSWORD>', } };
            rq.put(userRoute + '/mikey', b, function () {
              b.body.login = 'mike';
              rq.put(userRoute + '/mikey', b, function (err, resp, body) {
                expect(resp.statusCode).toBe(200);
                expect(body.success).toBeTruthy();
                expect(body.key).toBe('mikey');
                expect(body.value.login).toBe('mike');
                done();
              });
            });
          }
        );

      });

      describe('user.get GET and key/login', function () {

        it('should return an error if the login does not exist',
          function (done) {
            var b = { body: { auth_token: admToken } };
            rq.get(userRoute + '/inexistent', b, function (err, resp, body) {
              expect(resp.statusCode).toBe(404);
              expect(body.error).toMatch('USER.NOT_FOUND');
              expect(body.key).toBe('inexistent');
              done();
            });
          }
        );

        it('should give the login/key and the user attributes, password' +
          ' excepted otherwise',
          function (done) {
            var b = { body: { auth_token: admToken } };
            rq.get(userRoute + '/parker', b, function (err, resp, body) {
              expect(resp.statusCode).toBe(200);
              expect(body.value.login).toBe('parker');
              expect(body.value.firstname).toBe('Parker');
              expect(ld.isArray(body.value.groups)).toBeTruthy();
              done();
            });
          }
        );

      });

      describe('list all users (GET)', function () {
        it('should NOT return the list of all users if not admin',
          function (done) {
            rq.get(allUsersRoute, {}, function (err, resp, body) {
              expect(resp.statusCode).toBe(401);
              expect(body.error).toMatch('AUTHENTICATION.ADMIN');
              done();
            });
          }
        );

        it('should return the list of all users if admin',
          function (done) {
            var b = { body: { auth_token: admToken } };

            rq.get(allUsersRoute, b, function (err, resp, body) {
              expect(resp.statusCode).toBe(200);
              expect(body.usersCount).toBe(5);
              expect(body.users.parker.email).toBe('<EMAIL>');
              expect(body.users.parker.firstname).toBe('Parker');
              done();
            });
          }
        );
      });

      describe('userlist testing', function () {
        var ulists;

        describe('userlist POST', function () {

          it('should return an error if no name is sent', function (done) {
            var b = { body: { auth_token: token } };
            rq.post(userlistRoute, b, function (err, resp, body) {
              expect(resp.statusCode).toBe(400);
              expect(body.error).toMatch('USERLIST_NAME');
              done();
            });
          });

          it('should allow creation otherwise', function (done) {
            var b = { body: { name: 'friends' } };
            b.body.auth_token = token;
            rq.post(userlistRoute, b, function (err, resp, body) {
              expect(resp.statusCode).toBe(200);
              expect(body.success).toBeTruthy();
              ulists = body.value;
              expect(body.value).toBeDefined();
              expect(ld.size(body.value)).toBe(1);
              expect(ld.values(ulists)[0].name).toBe('friends');
              done();
            });
          });

        });

        describe('userlist PUT', function () {

          it('should return an error if the userlist is not found',
            function (done) {
              var b = { body: { name: 'Useless' } };
              b.body.auth_token = token;
              rq.put(userlistRoute + '/inexistent', b,
                function (err, resp, body) {
                  expect(resp.statusCode).toBe(400);
                  expect(body.error).toMatch('NOT_FOUND');
                  done();
                }
              );
            }
          );

          it('should return an error for set if no uids or no name are given',
            function (done) {
              var b = { body: { auth_token: token } };
              rq.put(userlistRoute + '/' + ld.keys(ulists)[0], b,
                function (err, resp, body) {
                  expect(resp.statusCode).toBe(400);
                  expect(body.error).toMatch('USERLIST_SET_PARAMS');
                  done();
                }
              );
            }
          );

          it('should update a list name', function (done) {
            var ulkey = ld.keys(ulists)[0];
            var b = { body: { name: 'Good friends' } };
            b.body.auth_token = token;
            rq.put(userlistRoute + '/' + ulkey, b, function (err, resp, body) {
              expect(resp.statusCode).toBe(200);
              expect(body.success).toBeTruthy();
              ulists = body.value;
              expect(ld.isObject(ulists)).toBeTruthy();
              var ul = ld.values(ulists)[0];
              expect(ul.name).toBe('Good friends');
              done();
            });
          });

          it('should update a list with filtered logins or emails',
            function (done) {
              var ulk = ld.keys(ulists)[0];
              var b = { body: {
                loginsOrEmails: ['inexistent', '<EMAIL>', 'jerry']
              } };
              b.body.auth_token = token;
              rq.put(userlistRoute + '/' + ulk, b, function (err, resp, body) {
                expect(resp.statusCode).toBe(200);
                expect(body.success).toBeTruthy();
                ulists = body.value;
                expect(ld.isObject(ulists)).toBeTruthy();
                var ul = ld.values(ulists)[0];
                expect(ul.name).toBe('Good friends');
                expect(ld.isArray(ul.uids)).toBeTruthy();
                expect(ld.size(ul.uids)).toBe(2);
                expect(ld.isArray(ul.users)).toBeTruthy();
                expect(ld.size(ul.users)).toBe(2);
                b = { body: { loginsOrEmails: [] } };
                b.body.auth_token = token;
                rq.put(userlistRoute + '/' + ulk, b,
                  function (err, resp, body) {
                    expect(resp.statusCode).toBe(200);
                    expect(body.success).toBeTruthy();
                    ulists = body.value;
                    var ul = ld.values(ulists)[0];
                    expect(ul.name).toBe('Good friends');
                    expect(ld.isArray(ul.uids)).toBeTruthy();
                    expect(ld.isEmpty(ul.uids)).toBeTruthy();
                    expect(ld.isArray(ul.users)).toBeTruthy();
                    expect(ld.isEmpty(ul.users)).toBeTruthy();
                    done();
                  }
                );
              });
            }
          );

        });

        describe('userlist GET', function () {

          it('should return userlists of the current logged user',
            function (done) {
              var b = { body: { auth_token: token } };
              rq.get(userlistRoute, b, function (err, resp, body) {
                expect(resp.statusCode).toBe(200);
                ulists = body.value;
                expect(ld.isObject(ulists)).toBeTruthy();
                expect(ld.size(ulists)).toBe(1);
                expect(ld.values(ulists)[0].name).toBe('Good friends');
                done();
              });
            }
          );

        });

        describe('userlist DELETE', function () {

          it('should return an error if the userlist is not found',
            function (done) {
              var b = { body: { auth_token: token } };
              rq.del(userlistRoute + '/inexistent', b,
                function (err, resp, body) {
                  expect(resp.statusCode).toBe(400);
                  expect(body.error).toMatch('NOT_FOUND');
                  done();
                }
              );
            }
          );

          it('should otherwise delete an userlist', function (done) {
            var b = { body: { auth_token: token } };
            var ulkey = ld.keys(ulists)[0];
            rq.del(userlistRoute + '/' + ulkey, b, function (err, resp, body) {
              expect(resp.statusCode).toBe(200);
              expect(body.success).toBeTruthy();
              ulists = body.value;
              expect(ld.isObject(ulists)).toBeTruthy();
              expect(ld.isEmpty(ulists)).toBeTruthy();
              done();
            });
          });


        });

      });

      describe('user.mark POST', function () {

        it('should return error when arguments are not as expected',
          function (done) {
            var b = { body: { auth_token: token } };
            rq.post(userRoute + 'mark', b, function (err, resp, body) {
              expect(resp.statusCode).toBe(400);
              expect(body.error).toMatch('TYPE_PADSORGROUPS');
              done();
            });
          }
        );

        it('will return an error if the bookmark id does not exist',
          function (done) {
            var b = { body: { type: 'pads', key:'xxx' } };
            b.body.auth_token = token;
            rq.post(userRoute + 'mark', b, function (err, resp, body) {
              expect(resp.statusCode).toBe(404);
              expect(body.error).toMatch('BOOKMARK_NOT_FOUND');
              done();
            });
          }
        );

        it('should mark or unmark successfully otherwise',
          function (done) {
            var b = { body: { auth_token: token } };
            rq.get(userRoute + '/guest', b, function (err, resp, body) {
              b.body.name = 'group1';
              b.body.admin = body.value._id;
              b.body.visibility = 'restricted';
              rq.post(route + 'group', b, function (err, resp, body) {
                b = { body: { type: 'groups', key: body.value._id } };
                b.body.auth_token = token;
                rq.post(userRoute + 'mark', b, function (err, resp, body) {
                  expect(resp.statusCode).toBe(200);
                  expect(body.success).toBeTruthy();
                  done();
                });
              });
            });
          }
        );

      });

      describe('user password recovery POST', function () {

        it('should complain about missing email', function (done) {
          rq.post(route + 'passrecover', function (err, resp, body) {
            expect(err).toBeNull();
            expect(resp.statusCode).toBe(400);
            expect(body.error).toMatch('TYPE');
            done();
          });
        });

        it('should complain about invalid email', function (done) {
          var b = { body: { email: 'inexistent' } };
          rq.post(route + 'passrecover', b, function (err, resp, body) {
            expect(err).toBeNull();
            expect(resp.statusCode).toBe(400);
            expect(body.error).toMatch('TYPE');
            done();
          });
        });

        it('should complain about not found user', function (done) {
          var b = { body: { email: '<EMAIL>' } };
          rq.post(route + 'passrecover', b, function (err, resp, body) {
            expect(err).toBeNull();
            expect(resp.statusCode).toBe(404);
            expect(body.error).toMatch('USER.NOT_FOUND');
            done();
          });
        });

        it('should complain about not configured rootUrl setting',
          function (done) {
            var b = { body: { email: '<EMAIL>' } };
            rq.post(route + 'passrecover', b, function (err, resp, body) {
              expect(err).toBeNull();
              expect(resp.statusCode).toBe(501);
              expect(body.error).toMatch('ROOTURL_NOT_CONFIGURED');
              done();
            });
          }
        );

        it('should complain about not configured mail settings',
          function (done) {
            conf.cache.rootUrl = 'http://localhost:8042';
            var b = { body: { email: '<EMAIL>' } };
            rq.post(route + 'passrecover', b, function (err, resp, body) {
              expect(err).toBeNull();
              expect(resp.statusCode).toBe(501);
              expect(body.error).toMatch('MAIL_NOT_CONFIGURED');
              done();
            });
          }
        );

      });

      describe('user password recovery PUT', function () {

        afterAll(function () {
          mail.tokens = {};
          mail.ends = {};
        });

        it('should return an error if the token value is incorrect',
          function (done) {
            rq.put(route + 'passrecover/invalid', function (err, resp, body) {
              expect(err).toBeNull();
              expect(resp.statusCode).toBe(400);
              expect(body.error).toMatch('TOKEN.INCORRECT');
              var mtk = mail.genToken({ login: 'guest', action: 'another' });
              rq.put(route + 'passrecover/' + mtk,
                function (err, resp, body) {
                  expect(err).toBeNull();
                  expect(resp.statusCode).toBe(400);
                  expect(body.error).toMatch('TOKEN.INCORRECT');
                  mtk = mail.genToken({ action: 'passrecover' });
                  rq.put(route + 'passrecover/' + mtk,
                    function (err, resp, body) {
                      expect(err).toBeNull();
                      expect(resp.statusCode).toBe(400);
                      expect(body.error).toMatch('TOKEN.INCORRECT');
                      done();
                    }
                  );
                }
              );
            });
          }
        );

        it('should return an error if the token is no more valid',
          function (done) {
            conf.cache.tokenDuration = -1;
            var tk = mail.genToken({ login: 'guest', action: 'passrecover' });
            rq.put(route + 'passrecover/' + tk, function (err, resp, body) {
              expect(err).toBeNull();
              expect(resp.statusCode).toBe(400);
              expect(body.error).toMatch('TOKEN.EXPIRED');
              conf.cache.tokenDuration = 60;
              done();
            });
          }
        );

        it('should return an error if the passwords are not given or mismatch',
          function (done) {
            var tk = mail.genToken({ login: 'guest', action: 'passrecover' });
            rq.put(route + 'passrecover/' + tk, function (err, resp, body) {
              expect(err).toBeNull();
              expect(resp.statusCode).toBe(400);
              expect(body.error).toMatch('PASSWORD_MISMATCH');
              var b = { body: { password: '<PASSWORD>', passwordConfirm: '2' } };
              rq.put(route + 'passrecover/' + tk, b,
                function (err, resp, body) {
                  expect(err).toBeNull();
                  expect(resp.statusCode).toBe(400);
                  expect(body.error).toMatch('PASSWORD_MISMATCH');
                  conf.cache.tokenDuration = 60;
                  done();
                }
              );
            });
          }
        );

        it('should forbid bad password', function (done) {
          var tk = mail.genToken({ login: 'guest', action: 'passrecover' });
          var b = { body: { password: '<PASSWORD>', passwordConfirm: '<PASSWORD>' } };
          rq.put(route + 'passrecover/' + tk, b, function (err, resp, body) {
            expect(err).toBeNull();
            expect(resp.statusCode).toBe(400);
            expect(body.error).toMatch('TYPE.PASSWORD_SIZE');
            done();
          });
        });

        it('should change password otherwise', function (done) {
          var tk = mail.genToken({ login: 'guest', action: 'passrecover' });
          var b = { body: {
            password: '<PASSWORD>',
            passwordConfirm: '<PASSWORD>'
          } };
          rq.put(route + 'passrecover/' + tk, b, function (err, resp, body) {
            expect(err).toBeNull();
            expect(resp.statusCode).toBe(200);
            expect(body.login).toBe('guest');
            expect(body.success).toBeTruthy();
            done();
          });
        });

      });

      describe('user account confirmation POST', function () {

        afterAll(function () {
          mail.tokens = {};
          mail.ends = {};
        });

        it('should return an error if the token value is incorrect',
          function (done) {
            rq.post(route + 'accountconfirm', function (err, resp, body) {
              expect(err).toBeNull();
              expect(resp.statusCode).toBe(400);
              expect(body.error).toMatch('TOKEN.INCORRECT');
              var tk = mail.genToken({ login: 'guest', action: 'another' });
              var b = { body: { token: tk } };
              rq.post(route + 'accountconfirm', b, function (err, resp, body) {
                expect(err).toBeNull();
                expect(resp.statusCode).toBe(400);
                expect(body.error).toMatch('TOKEN.INCORRECT');
                done();
              });
            });
          }
        );

        it('should return an error if the token is no more valid',
          function (done) {
            conf.cache.tokenDuration = -1;
            var tk = mail.genToken({ login: 'xxx', action: 'accountconfirm' });
            var b = { body: { token: tk } };
            rq.post(route + 'accountconfirm/', b, function (err, resp, body) {
              expect(err).toBeNull();
              expect(resp.statusCode).toBe(400);
              expect(body.error).toMatch('TOKEN.EXPIRED');
              conf.cache.tokenDuration = 60;
              done();
            });
          }
        );

        it('should activate account otherwise', function (done) {
          var tk = mail.genToken({ login: 'guest', action: 'accountconfirm' });
          var b = { body: { token: tk } };
          rq.post(route + 'accountconfirm', b, function (err, resp, body) {
            expect(err).toBeNull();
            expect(resp.statusCode).toBe(200);
            expect(body.login).toBe('guest');
            expect(body.success).toBeTruthy();
            done();
          });
        });

      });


      describe('user.del DELETE and key/login', function () {

        it('will return an error if the user does not exist',
          function (done) {
            var b = { body: { auth_token: admToken } };
            rq.del(userRoute + '/inexistent', b, function (err, resp, body) {
              expect(resp.statusCode).toBe(404);
              expect(body.error).toMatch('USER.NOT_FOUND');
              done();
            });
          }
        );

        it('should delete the record and returns the key and success' +
         ' otherwise', function (done) {
            var b = { body: { auth_token: token } };
            rq.del(userRoute + '/guest', b, function (err, resp, body) {
              expect(resp.statusCode).toBe(200);
              expect(body.success).toBeTruthy();
              expect(body.key).toBe('guest');
              b.body.auth_token = admToken;
              rq.get(userRoute + '/guest', b, function (err, resp, body) {
                expect(resp.statusCode).toBe(404);
                expect(body.error).toMatch('USER.NOT_FOUND');
                done();
              });
            });
          }
        );

      });

    });

    describe('group API', function () {
      var groupRoute = route + 'group';
      var uid;
      var gid;
      var uotherid;
      var gotherid;
      var gprivateid;
      var token;

      beforeAll(function (done) {
        specCommon.reInitDatabase(function () {
          var params = guest;
          var oparams = {
            login: 'other',
            password: '<PASSWORD>',
            email: '<EMAIL>'
          };
          user.set(params, function (err, u) {
            if (err) { console.log(err); }
            uid = u._id;
            user.set(oparams, function (err, u) {
              if (err) { console.log(err); }
              uotherid = u._id;
              group.set({ name: 'g1', admin: uid, visibility: 'public' },
                function (err, res) {
                  if (err) { console.log(err); }
                  gid = res._id;
                  group.set({ name: 'gother1', admin: uotherid },
                    function (err, res) {
                    if (err) { console.log(err); }
                    gotherid = res._id;
                    var gparams = {
                      name: 'gprivate1',
                      admin: uotherid,
                      visibility: 'private',
                      password: '<PASSWORD>'
                    };
                    group.set(gparams, function (err, res) {
                      if (err) { console.log(err); }
                      gprivateid = res._id;
                      rq.post(route + 'auth/login', { body: params },
                        function (err, resp, body) {
                          if (!err && resp.statusCode === 200) {
                            token = body.token;
                            done();
                          }
                        }
                      );
                    });
                  });
                }
              );
            });
          });
        });
      });

      afterAll(function (done) {
        var b = { body: { auth_token: token } };
        rq.get(route + 'auth/logout', b, function () {
          specCommon.reInitDatabase(done);
        });
      });

      describe('group.inviteOrShare POST', function () {

        it('should return error when arguments are not as expected',
          function (done) {
            rq.post(groupRoute + '/invite', function (err, resp, body) {
              expect(resp.statusCode).toBe(400);
              expect(body.error).toMatch('PARAMS_REQUIRED');
              done();
            });
          }
        );

        it('will return an error if the group id does not exist',
          function (done) {
            var b = {
              body: {
                invite: true,
                gid:'xxx',
                loginsOrEmails: ['one', 'two'] 
              }
            };
            rq.post(groupRoute + '/invite', b, function (err, resp) {
              expect(resp.statusCode).toBe(401);
              done();
            });
          }
        );

        it('forbid invitation if the user is not group admin', function (done) {
          var b = {
            body: {
              invite: true,
              gid: gotherid,
              loginsOrEmails: ['one', 'two']
            }
          };
          rq.post(groupRoute + '/invite', b, function (err, resp) {
            expect(resp.statusCode).toBe(401);
            done();
          });
        });

        it('allow invitation if the user is global admin', function (done) {
          var b = {
            body: {
              invite: true,
              gid: gotherid,
              loginsOrEmails: ['one', 'two'],
              auth_token: admToken
            }
          };
          rq.post(groupRoute + '/invite', b, function (err, resp, body) {
            expect(resp.statusCode).toBe(200);
            expect(body.success).toBeTruthy();
            expect(ld.isObject(body.value)).toBeTruthy();
            expect(body.value._id).toBe(gotherid);
            done();
          });
        });

        it('should invite successfully otherwise', function (done) {
          var params = {
            login: 'franky',
            password: '<PASSWORD>',
            email: '<EMAIL>'
          };
          user.set(params, function (err, u) {
            expect(err).toBeNull();
            var b = {
              body: {
                invite: true,
                gid: gid,
                loginsOrEmails: [u.login, 'inexistent'],
                auth_token: token
              }
            };
              rq.post(groupRoute + '/invite', b, function (err, resp, body) {
                expect(err).toBeNull();
                expect(resp.statusCode).toBe(200);
                expect(body.success).toBeTruthy();
                expect(ld.isObject(body.value)).toBeTruthy();
                expect(ld.first(body.value.users)).toBe(u._id);
                expect(ld.isObject(body.present)).toBeTruthy();
                expect(ld.isObject(body.absent)).toBeTruthy();
                expect(ld.size(body.present)).toBe(1);
                expect(ld.first(body.present)).toBe(u.login);
                expect(ld.size(body.absent)).toBe(1);
                expect(ld.first(body.absent)).toBe('inexistent');
                done();
              });
            });
          }
        );

      });

      describe('group.resign POST', function () {

        it('should return error when arguments are not as expected',
          function (done) {
            rq.post(groupRoute + '/resign', function (err, resp) {
              expect(resp.statusCode).toBe(401);
              done();
            });
          }
        );

        it('will return an error if the group or user id does not exist',
          function (done) {
            var b = { body: { gid:'xxx', uid: 'xxx' } };
            rq.post(groupRoute + '/resign', b, function (err, resp) {
              expect(resp.statusCode).toBe(401);
              done();
            });
          }
        );

        it('should forbid resignation if the user is not part of the group',
          function (done) {
            var b = { body: { gid: gotherid } };
            rq.post(groupRoute + '/resign', b, function (err, resp) {
              expect(resp.statusCode).toBe(401);
              done();
            });
          }
        );

        it('should forbid resignation if the user is the unique admin',
          function (done) {
            var b = { body: { gid: gid } };
            rq.post(groupRoute + '/resign', b, function (err, resp) {
              expect(resp.statusCode).toBe(401);
              done();
            });
          }
        );

        it('should allow resignation otherwise', function (done) {
          group.set({ name: 'newg', admin: uid, admins: [ uotherid ] },
            function (err, g) {
              expect(err).toBeNull();
              var b = { body: { gid: g._id } };
              b.body.auth_token = token;
              rq.post(groupRoute + '/resign', b, function (err, resp, body) {
                expect(err).toBeNull();
                expect(resp.statusCode).toBe(200);
                expect(body.success).toBeTruthy();
                expect(ld.isObject(body.value)).toBeTruthy();
                var users = ld.union(body.value.users, body.value.admins);
                expect(ld.includes(users, uid)).toBeFalsy();
                done();
                group.del(g._id, function (err) {
                  expect(err).toBeNull();
                  done();
                });
              });
            }
          );
        });

      });

      describe('group.getByUser GET', function () {

        it('should return groups, pads and users', function (done) {
          var b = { body: { auth_token: token } };
          rq.get(groupRoute, b, function (err, resp, body) {
            expect(err).toBeNull();
            expect(resp.statusCode).toBe(200);
            expect(ld.isObject(body.value)).toBeTruthy();
            expect(ld.isObject(body.value.groups)).toBeTruthy();
            expect(ld.isObject(body.value.pads)).toBeTruthy();
            expect(ld.isObject(body.value.users)).toBeTruthy();
            var groups = body.value.groups;
            var key = ld.first(ld.keys(groups));
            expect(groups[key].name).toBe('g1');
            expect(groups[key].visibility).toBe('public');
            expect(groups[key].password).toBeUndefined();
            var admin = ld.first(ld.values(body.value.users));
            expect(admin.login).toBe('guest');
            expect(admin._id).toBeDefined();
            expect(admin.firstname).toBeDefined();
            expect(admin.lastname).toBeDefined();
            expect(admin.email).toBeDefined();
            expect(admin.password).toBeUndefined();
            done(); 
          });
        });
      });

      describe('group.get GET and id', function () {

        it('should return an error if the id does not exist',
          function (done) {
            rq.get(groupRoute + '/ginexistent', function (err, resp, body) {
              expect(resp.statusCode).toBe(404);
              expect(body.error).toMatch('KEY_NOT_FOUND');
              expect(body.key).toBe('ginexistent');
              done();
            });
          }
        );

        it('should forbid access to others groups for regular users',
          function (done) {
            rq.get(groupRoute + '/' + gotherid, function (err, resp, body) {
              expect(err).toBeNull();
              expect(resp.statusCode).toBe(401);
              expect(body.error).toMatch('DENIED_RECORD');
              done();
            });
          }
        );

        it('should allow access to public groups for unauth users',
          function (done) {
            var b = {
              body: {
                name: 'groupPublic',
                admin: uid,
                visibility: 'public',
                auth_token: token
              }
            };
            rq.post(groupRoute, b, function (err, resp, body) {
              expect(err).toBeNull();
              expect(resp.statusCode).toBe(200);
              expect(body.success).toBeTruthy();
              var key = body.key;
              rq.get(route + 'auth/logout', b, function () {
                rq.get(groupRoute + '/' + key, function (err, resp, body) {
                  expect(err).toBeNull();
                  expect(resp.statusCode).toBe(200);
                  expect(body.key).toBeDefined();
                  var g = body.value;
                  var pads = body.pads;
                  expect(g.name).toBe('groupPublic');
                  expect(ld.size(pads)).toBe(0);
                  var params = { login: 'guest', password: '<PASSWORD>' };
                  rq.post(route + 'auth/login', { body: params },
                    function (err, resp, body) {
                      if (!err && resp.statusCode === 200) {
                        token = body.token;
                        done();
                      }
                    }
                  );
                });
              });
            });
          }
        );

        it('should return filtered pads for public access groups',
          function (done) {
            var b = { body: { name: 'public', group: gid } };
            b.body.auth_token = token;
            rq.post(route + 'pad', b, function (err, resp, body) {
              expect(err).toBeNull();
              expect(resp.statusCode).toBe(200);
              expect(body.success).toBeTruthy();
              b.body.visibility = 'private';
              b.body.password = 'password';
              rq.post(route + 'pad', b, function (err, resp, body) {
                expect(err).toBeNull();
                expect(resp.statusCode).toBe(200);
                expect(body.success).toBeTruthy();
                b = { body: { auth_token: token } };
                rq.get(route + 'auth/logout', b, function () {
                  rq.get(groupRoute + '/' + gid, function (err, resp, body) {
                    expect(err).toBeNull();
                    expect(resp.statusCode).toBe(200);
                    expect(body.key).toBe(gid);
                    var pads = body.pads;
                    expect(ld.size(pads)).toBe(1);
                    expect(ld.values(pads)[0].name).toBe('public');
                    var pms = { login: 'guest', password: '<PASSWORD>' };
                    rq.post(route + 'auth/login', { body: pms },
                      function (err, resp, body) {
                        if (!err && resp.statusCode === 200) {
                          token = body.token;
                          done();
                        }
                      }
                    );
                  });
                });
              });
            });
          }
        );

        it('should return a subset for unauth or non-admin for private group',
          function (done) {
            rq.get(groupRoute + '/' + gprivateid, function (err, resp, body) {
              expect(err).toBeNull();
              expect(resp.statusCode).toBe(200);
              expect(body.key).toBe(gprivateid);
              expect(body.pads).toBeUndefined();
              var diff = ld.difference(ld.keys(body.value),
                ['name', 'visibility']);
              expect(diff.length).toBe(0);
              expect(body.value.name).toBe('gprivate1');
              expect(body.value.visibility).toBe('private');
              done();
            });
          }
        );

        it('should return an error in case of private unauth or non-admin ' +
          'if password is bad', function (done) {
            var route = groupRoute + '/' + gprivateid + '?password=<PASSWORD>';
            rq.get(route, function (err, resp, body) {
              expect(err).toBeNull();
              expect(resp.statusCode).toBe(401);
              expect(body.key).toBe(gprivateid);
              expect(body.error).toMatch('PERMISSION.UNAUTHORIZED');
              done();
            });
          }
        );

        it('should return the group in case of private unauth or non-admin ' +
          'if password is correct', function (done) {
            var route = groupRoute + '/' + gprivateid + '?password=' + encode('<PASSWORD>');
            rq.get(route, function (err, resp, body) {
              expect(err).toBeNull();
              expect(resp.statusCode).toBe(200);
              expect(body.key).toBe(gprivateid);
              expect(body.value.name).toBe('gprivate1');
              expect(body.value.visibility).toBe('private');
              expect(body.value.readonly).toBeFalsy();
              expect(ld.isArray(body.value.tags)).toBeTruthy();
              expect(ld.isObject(body.pads)).toBeTruthy();
              expect(ld.isEmpty(body.pads)).toBeTruthy();
              done();
            });
          }
        );

        it('should allow access to regular user if group is public',
          function (done) {
            rq.get(groupRoute + '/' + gid, function (err, resp, body) {
              expect(err).toBeNull();
              expect(resp.statusCode).toBe(200);
              expect(body.key).toBe(gid);
              expect(body.value.name).toBe('g1');
              expect(ld.size(body.pads)).toBe(1);
              done();
            });
          }
        );

        it('should allow access to all groups for admin', function (done) {
          var b = { body: { auth_token: admToken } };
          rq.get(groupRoute + '/' + gotherid, b, function (err, resp, body) {
            expect(resp.statusCode).toBe(200);
            expect(body.key).toBe(gotherid);
            expect(body.value.name).toBe('gother1');
            done();
          });
        });

        it('should give the key and the group attributes otherwise',
          function (done) {
            rq.get(groupRoute + '/' + gid, function (err, resp, body) {
              expect(resp.statusCode).toBe(200);
              expect(body.key).toBe(gid);
              expect(body.value._id).toBe(gid);
              expect(body.value.name).toBe('g1');
              expect(body.value.visibility).toBe('public');
              expect(ld.isArray(body.value.users)).toBeTruthy();
              expect(ld.isArray(body.value.pads)).toBeTruthy();
              expect(body.value.password).toBeNull();
              expect(body.value.readonly).toBeFalsy();
              expect(ld.size(body.value.admins)).toBe(1);
              expect(body.value.admins[0]).toBe(uid);
              done();
            });
          }
        );

      });

      describe('group.set/add POST and value as params', function () {

        it('should return error when arguments are not as expected',
          function (done) {
            rq.post(groupRoute, function (err, resp) {
              expect(resp.statusCode).toBe(401);
              var b = { body: { visibility: 'public' } };
              rq.post(groupRoute, b, function (err, resp) {
                expect(resp.statusCode).toBe(401);
                done();
              });
            });
          }
        );

        it('should return an error if admin user does not exist',
          function (done) {
            var b = {
              body: {
                name: 'group1',
                admin: 'inexistentId',
                auth_token: admToken
              }
            };
            rq.post(groupRoute, b, function (err, resp, body) {
              expect(resp.statusCode).toBe(400);
              expect(body.error).toMatch('GROUP.ITEMS_NOT_FOUND');
              done();
            });
          }
        );

        it('should return an error if visibility is private with invalid ' +
          'password', function (done) {
            var b = {
              body: {
                name: 'groupOk',
                admin: uid,
                visibility: 'private'
              }
            };
            rq.post(groupRoute, b, function (err, resp) {
              expect(resp.statusCode).toBe(401);
              done();
            });
          }
        );

        it('should force admin id to be the current user, unless ether admin',
          function (done) {
            var b = { body: { name: 'groupOk', admin: uotherid } };
            b.body.auth_token = token;
            rq.post(groupRoute, b, function (err, resp, body) {
              expect(err).toBeNull();
              expect(resp.statusCode).toBe(200);
              expect(body.success).toBeTruthy();
              expect(body.key).toBeDefined();
              expect(body.value.name).toBe('groupOk');
              expect(body.value.admins[0]).not.toBe(uotherid);
              expect(body.value.admins[0]).toBe(uid);
              b.body.auth_token = admToken;
              rq.post(groupRoute, b, function (err, resp, body) {
                expect(err).toBeNull();
                expect(resp.statusCode).toBe(200);
                expect(body.value.admins[0]).toBe(uotherid);
                done();
              });
            });
          }
        );

        it('should create a new group otherwise', function (done) {
          var b = {
            body: {
              name: 'groupOk',
              description: 'a cool new group',
              admin: uid,
              visibility: 'private',
              password: '<PASSWORD>',
              auth_token: token
            }
          };
          rq.post(groupRoute, b, function (err, resp, body) {
            expect(err).toBeNull();
            expect(resp.statusCode).toBe(200);
            expect(body.success).toBeTruthy();
            expect(body.key).toBeDefined();
            var key = body.key;
            expect(body.value.name).toBe('groupOk');
            expect(body.value.description).toBe('a cool new group');
            rq.get(groupRoute + '/' + key + '?auth_token=' + token,
              function (err, resp, body) {
                expect(err).toBeNull();
                expect(resp.statusCode).toBe(200);
                expect(body.key).toBe(key);
                expect(body.value._id).toBe(key);
                expect(body.value.name).toBe('groupOk');
                expect(body.value.visibility).toBe('private');
                expect(body.value.password).toBeDefined();
                expect(ld.isArray(body.value.users)).toBeTruthy();
                expect(ld.isArray(body.value.pads)).toBeTruthy();
                expect(body.value.readonly).toBeFalsy();
                expect(ld.size(body.value.admins)).toBe(1);
                expect(body.value.admins[0]).toBe(uid);
                done();
              }
            );
          });
        });

      });

      describe('group.set PUT key in URL and value as params', function () {

        it('should return error when arguments are not as expected',
          function (done) {
            rq.put(groupRoute + '/' + gid, function (err, resp) {
              expect(resp.statusCode).toBe(401);
              var b = { body: { name: 'group1' } };
              rq.put(groupRoute + '/' + gid, b, function (err, resp) {
                expect(resp.statusCode).toBe(401);
                b = { body: { name: 'group1', admin: 'inexistentId' } };
                rq.put(groupRoute + '/' + gid, b, function (err, resp) {
                  expect(resp.statusCode).toBe(401);
                  done();
                });
              });
            });
          }
        );

        it('should update an existing group otherwise', function (done) {
          var b = {
            body: {
              _id: gid,
              name: 'gUpdated',
              description: 'an updated one',
              admin: uid,
              visibility: 'public',
              readonly: true,
              auth_token: token
            }
          };
          rq.put(groupRoute + '/' + gid, b, function (err, resp, body) {
            expect(err).toBeNull();
            expect(resp.statusCode).toBe(200);
            expect(body.success).toBeTruthy();
            expect(body.key).toBe(gid);
            expect(body.value.name).toBe('gUpdated');
            expect(body.value.description).toBe('an updated one');
            rq.get(groupRoute + '/' + gid, b,
              function (err, resp, body) {
                expect(resp.statusCode).toBe(200);
                expect(body.key).toBe(gid);
                expect(body.value._id).toBe(gid);
                expect(body.value.name).toBe('gUpdated');
                expect(body.value.visibility).toBe('public');
                expect(ld.isArray(body.value.users)).toBeTruthy();
                expect(ld.isArray(body.value.pads)).toBeTruthy();
                expect(body.value.readonly).toBeTruthy();
                expect(ld.size(body.value.admins)).toBe(1);
                expect(body.value.admins[0]).toBe(uid);
                done();
              }
            );
          });
        });

        it('should not create a non existent group', function (done) {
          var b = {
            body: {
              name: 'gCreated',
              admin: uid,
              visibility: 'public',
              readonly: true,
              auth_token: token
            }
          };
          rq.put(groupRoute + '/newgid', b, function (err, resp, body) {
            expect(err).toBeNull();
            expect(resp.statusCode).toBe(400);
            expect(body.error).toMatch('NOT_FOUND');
            done();
          });
        });

        it('shouldn\'t allow update of a group when the user is not admin',
          function (done) {
            var b = {
              body: {
                _id: gotherid,
                name: 'gOtherUpdated',
                admin: uotherid,
                visibility: 'public',
                auth_token: token
              }
            };
            rq.put(groupRoute + '/' + gotherid, b, function (err, resp, body) {
              expect(err).toBeNull();
              expect(resp.statusCode).toBe(401);
              expect(body.error).toMatch('DENIED_RECORD_EDIT');
              done();
            });
          }
        );

        it('should allow update all groups when the user is global admin',
          function (done) {
            var goid = gotherid;
            var b = {
              body: {
                _id: goid,
                name: 'gOtherUpdated',
                admin: uotherid,
                visibility: 'public',
                auth_token: admToken
              }
            };
            rq.put(groupRoute + '/' + goid, b, function (err, resp, body) {
              expect(err).toBeNull();
              expect(resp.statusCode).toBe(200);
              expect(body.key).toBe(goid);
              expect(body.value._id).toBe(goid);
              expect(body.value.name).toBe('gOtherUpdated');
              expect(body.value.visibility).toBe('public');
              done();
            });
          }
        );

      });

      describe('group.del DELETE and id', function () {
        it('will return an error if the group does not exist',
          function (done) {
            var b = { body: { auth_token: token } };
            rq.del(groupRoute + '/inexistentId', b, function (err, resp, body) {
              expect(resp.statusCode).toBe(400);
              expect(body.error).toMatch('KEY_NOT_FOUND');
              done();
            });
          }
        );

        it('should deletes the record and returns the key and success' +
         ' otherwise', function (done) {
            var b = { body: { auth_token: token } };
            rq.del(groupRoute + '/' + gid, b, function (err, resp, body) {
              expect(resp.statusCode).toBe(200);
              expect(body.success).toBeTruthy();
              expect(body.key).toBe(gid);
              rq.get(groupRoute + '/' + gid, b, function (err, resp, body) {
                expect(resp.statusCode).toBe(404);
                expect(body.error).toMatch('KEY_NOT_FOUND');
                done();
              });
            });
          }
        );

        it('should forbid deleting a record when the user is not an admin of' +
          ' the group', function (done) {
            var b = { body: { auth_token: token } };
            rq.del(groupRoute + '/' + gotherid, b, function (err, resp, body) {
              expect(err).toBeNull();
              expect(resp.statusCode).toBe(401);
              expect(body.error).toMatch('DENIED_RECORD_EDIT');
              done();
            });
          }
        );

        it('should allow deleting a record if the user is a global admin',
          function (done) {
            var b = { body: { auth_token: admToken } };
            rq.del(groupRoute + '/' + gotherid, b, function (err, resp, body) {
              expect(err).toBeNull();
              expect(resp.statusCode).toBe(200);
              expect(body.success).toBeTruthy();
              expect(body.key).toBe(gotherid);
              done();
            });
          }
        );

      });

    });

    describe('pad API', function () {
      var padRoute = route + 'pad';
      var uid;
      var uotherid;
      var gid;
      var gotherid;
      var gsharedid;
      var gsharedallowcreationid;
      var pid;
      var potherid;
      var ppublicid;
      var token;

      beforeAll(function (done) {
        specCommon.reInitDatabase(function () {
          var params = guest;
          user.set(params, function (err, u) {
            if (err) { console.log(err); }
            uid = u._id;
            group.set({ name: 'g1', admin: u._id }, function (err, g) {
              if (err) { console.log(err); }
              gid = g._id;
              pad.set({ name: 'p1', group: g._id }, function (err, p) {
                if (err) { console.log(err); }
                pid = p._id;
                var oparams = {
                  login: 'other',
                  password: '<PASSWORD>',
                  email: '<EMAIL>'
                };
                user.set(oparams, function (err, u) {
                  if (err) { console.log(err); }
                  uotherid = u._id;
                  group.set({ name: 'gother1', admin: u._id },
                    function (err, g) {
                      if (err) { console.log(err); }
                      gotherid = g._id;
                      pad.set({ name: 'pother1', group: g._id },
                        function (err, p) {
                          if (err) { console.log(err); }
                          potherid = p._id;
                          pad.set({
                            name: 'ppublic1',
                            group: g._id,
                            visibility: 'public'
                          }, function (err, p) {
                            ppublicid = p._id;
                            rq.post(route + 'auth/login', { body: params },
                              function (err, resp, b) {
                                if (!err && resp.statusCode === 200) {
                                  token = b.token;
                                  group.set({ name: 'sharedgroup', admin: uotherid, users: [uid] }, function (err, g) {
                                    if (err) { console.log(err); }
                                    gsharedid = g._id;
                                    group.set(
                                      {
                                        name: 'sharedgroup',
                                        admin: uotherid, users: [uid],
                                        allowUsersToCreatePads: true
                                      }, function (err, g) {
                                        if (err) { console.log(err); }
                                        gsharedallowcreationid = g._id;
                                        done();
                                      }
                                    );
                                  });
                                }
                              }
                            );
                          });
                        }
                      );
                    }
                  );
                });
              });
            });
          });
        });
      });

      afterAll(function (done) {
        var b = { body: { auth_token: token } };
        rq.get(route + 'auth/logout', b, function () {
          specCommon.reInitDatabase(done);
        });
      });

      describe('pad.get GET and id', function () {

        it('should return an error if the id does not exist',
          function (done) {
            var b = { body: { auth_token: token } };
            rq.get(padRoute + '/pinexistent', b, function (err, resp, body) {
              expect(resp.statusCode).toBe(404);
              expect(body.error).toMatch('KEY_NOT_FOUND');
              done();
            });
          }
        );

        it('should forbid access to other pads', function (done) {
          var b = { body: { auth_token: <PASSWORD> } };
          rq.get(padRoute + '/' + potherid, b, function (err, resp, body) {
            expect(resp.statusCode).toBe(401);
            expect(body.error).toMatch('DENIED_RECORD');
            done();
          });
        });

        it('should allow access to other pads if admin', function (done) {
          var b = { body: { auth_token: admToken } };
          rq.get(padRoute + '/' + potherid, b, function (err, resp, body) {
            expect(resp.statusCode).toBe(200);
            expect(body.key).toBe(potherid);
            expect(body.value._id).toBe(potherid);
            expect(body.value.name).toBe('pother1');
            done();
          });
        });

        it('should allow access to public pads', function (done) {
          rq.get(padRoute + '/' + ppublicid, function (err, resp, body) {
            expect(resp.statusCode).toBe(200);
            expect(body.key).toBe(ppublicid);
            expect(body.value._id).toBe(ppublicid);
            expect(body.value.name).toBe('ppublic1');
            done();
          });
        });

        it('should give the key and the pad attributes otherwise',
          function (done) {
            var b = { body: { auth_token: token } };
            rq.get(padRoute + '/' + pid, b, function (err, resp, body) {
              expect(resp.statusCode).toBe(200);
              expect(body.key).toBe(pid);
              expect(body.value._id).toBe(pid);
              expect(body.value.group).toBe(gid);
              expect(body.value.name).toBe('p1');
              expect(body.value.visibility).toBeNull();
              expect(body.value.password).toBeNull();
              expect(body.value.readonly).toBeNull();
              expect(ld.isArray(body.value.users)).toBeTruthy();
              done();
            });
          }
        );
      });

      describe('pad.set/add POST and value as params', function () {

        it('should return an error if no user is given',
          function (done) {
            var b = { body: { } };
            rq.post(padRoute, b, function (err, resp, body) {
              expect(resp.statusCode).toBe(401);
              expect(body.error).toMatch('AUTHENTICATION.NOT_AUTH');
              done();
            });
          }
        );

        it('should return an error if no group is given',
          function (done) {
            var b = { body: { auth_token: token } };
            rq.post(padRoute, b, function (err, resp, body) {
              expect(resp.statusCode).toBe(400);
              expect(body.error).toMatch('PARAM_STR');
              done();
            });
          }
        );

        it('should return error when arguments are not as expected',
          function (done) {
            var b = { body: { auth_token: token, group: gid } };
            rq.post(padRoute, b, function (err, resp, body) {
              expect(resp.statusCode).toBe(400);
              expect(body.error).toMatch('PARAM_STR');
              var b = { body: { name: 'pad1' } };
              b.body.auth_token = token;
              rq.post(padRoute, b, function (err, resp, body) {
                expect(resp.statusCode).toBe(400);
                expect(body.error).toMatch('PARAM_STR');
                done();
              });
            });
          }
        );

        it('should return an error if pad does not exist',
          function (done) {
            var b = { body: { name: 'pad1', group: gid, _id: 'inexistent' } };
            b.body.auth_token = token;
            rq.post(padRoute, b, function (err, resp, body) {
              expect(resp.statusCode).toBe(400);
              expect(body.error).toMatch('PAD.INEXISTENT');
              done();
            });
          }
        );

        it('should return an error if group does not exist',
          function (done) {
            var b = { body: { name: 'pad1', group: 'inexistentId' } };
            b.body.auth_token = token;
            rq.post(padRoute, b, function (err, resp, body) {
              expect(resp.statusCode).toBe(400);
              expect(body.error).toMatch('PAD.ITEMS_NOT_FOUND');
              done();
            });
          }
        );

        it('should return an error if users are not found',
          function (done) {
            var b = { body:
              {
                name: 'pad1',
                group: gid,
                visibility: 'restricted',
                users: ['inexistentId'],
                auth_token: token
              }
            };
            rq.post(padRoute, b, function (err, resp, body) {
              expect(resp.statusCode).toBe(400);
              expect(body.error).toMatch('PAD.ITEMS_NOT_FOUND');
              done();
            });
          }
        );

        it('should return an error if creator is not in group admins',
          function (done) {
            var b = { body:
              {
                name: 'pad1',
                group: gsharedid,
                visibility: 'restricted',
                auth_token: token
              }
            };
            rq.post(padRoute, b, function (err, resp, body) {
              expect(resp.statusCode).toBe(401);
              expect(body.error).toMatch('AUTHENTICATION.DENIED');
              done();
            });
          }
        );

        it('should create a new pad if creator is in group admins', function (done) {
          var b = {
            body: {
              name: 'padOk',
              group: gid,
              visibility: 'private',
              password: '<PASSWORD>',
              auth_token: token
            }
          };
          rq.post(padRoute, b, function (err, resp, body) {
            expect(err).toBeNull();
            expect(resp.statusCode).toBe(200);
            expect(body.success).toBeTruthy();
            expect(body.key).toBeDefined();
            var key = body.key;
            expect(body.value.name).toBe('padOk');
            rq.get(padRoute + '/' + key + '?password=' + encode('secret'),
              function (err, resp, body) {
                expect(err).toBeNull();
                expect(resp.statusCode).toBe(200);
                expect(body.key).toBe(key);
                expect(body.value._id).toBe(key);
                expect(body.value.name).toBe('padOk');
                expect(body.value.visibility).toBe('private');
                expect(body.value.password).toBeDefined();
                expect(ld.isArray(body.value.users)).toBeTruthy();
                expect(body.value.readonly).toBeNull();
                done();
              }
            );
          });
        });

        it('should create a new pad if creator is in group users and allowUsersToCreatePads is true',
          function (done) {
            var b = { body:
              {
                name: 'padOk2',
                group: gsharedallowcreationid,
                visibility: 'restricted',
                auth_token: token
              }
            };
            rq.post(padRoute, b, function (err, resp, body) {
              expect(err).toBeNull();
              expect(resp.statusCode).toBe(200);
              expect(body.success).toBeTruthy();
              expect(body.key).toBeDefined();
              var key = body.key;
              expect(body.value.name).toBe('padOk2');
              b = { body: { auth_token: token } };
              rq.get(padRoute + '/' + key, b,
                function (err, resp, body) {
                  expect(err).toBeNull();
                  expect(resp.statusCode).toBe(200);
                  expect(body.key).toBe(key);
                  expect(body.value._id).toBe(key);
                  expect(body.value.name).toBe('padOk2');
                  expect(body.value.visibility).toBe('restricted');
                  expect(ld.isArray(body.value.users)).toBeTruthy();
                  expect(body.value.password).toBeNull();
                  expect(body.value.readonly).toBeNull();
                  done();
                }
              );
            });
          }
        );

      });

      describe('pad.set PUT key in URL and value as params', function () {

        it('should return error when arguments are not as expected',
          function (done) {
            var b = { body: { auth_token: token } };
            rq.put(padRoute, b, function (err, resp) {
              expect(resp.statusCode).toBe(404);
              var b = { body: { name: 'pad1' } };
              b.body.auth_token = token;
              rq.put(padRoute + '/' + pid, b, function (err, resp, body) {
                expect(resp.statusCode).toBe(400);
                expect(body.error).toMatch('PARAM_STR');
                done();
              });
            });
          }
        );

        it('should forbid update of existing other pad', function (done) {
          var b = {
            body: {
              name: 'pother1',
              group: gotherid,
              visibility: 'public',
              readonly: true,
              auth_token: <PASSWORD>
            }
          };
          rq.put(padRoute + '/' + potherid, b, function (err, resp, body) {
            expect(err).toBeNull();
            expect(resp.statusCode).toBe(401);
            expect(body.error).toMatch('DENIED_RECORD_EDIT');
            done();
          });
        });

        it('should allow update of all pads for global admin', function (done) {
          var b = {
            body: {
              _id: potherid,
              name: 'pother1',
              group: gotherid,
              visibility: 'public',
              readonly: true,
              auth_token: <PASSWORD>
            }
          };
          rq.put(padRoute + '/' + potherid, b, function (err, resp, body) {
            expect(err).toBeNull();
            expect(resp.statusCode).toBe(200);
            expect(body.success).toBeTruthy();
            expect(body.key).toBe(potherid);
            expect(body.value._id).toBe(potherid);
            expect(body.value.visibility).toBe('public');
            expect(body.value.readonly).toBeTruthy();
            done();
          });
        });

        it('should also not create a non existent pad', function (done) {
          var b = {
            body: {
              name: 'pCreated',
              group: gid,
              visibility: 'public',
              readonly: true,
              auth_token: <PASSWORD>
            }
          };
          rq.put(padRoute + '/newpid', b, function (err, resp, body) {
            expect(err).toBeNull();
            expect(resp.statusCode).toBe(404);
            expect(body.error).toMatch('KEY_NOT_FOUND');
            done();
          });
        });

        it('should update an existing pad otherwise', function (done) {
          var b = {
            body: {
              _id: pid,
              name: 'pUpdated',
              group: gid,
              visibility: 'public',
              readonly: true,
              auth_token: <PASSWORD>
            }
          };
          rq.put(padRoute + '/' + pid, b, function (err, resp, body) {
            expect(err).toBeNull();
            expect(resp.statusCode).toBe(200);
            expect(body.success).toBeTruthy();
            expect(body.key).toBe(pid);
            expect(body.value.name).toBe('pUpdated');
            b = { body: { auth_token: token } };
            rq.get(padRoute + '/' + pid, b,
              function (err, resp, body) {
                expect(resp.statusCode).toBe(200);
                expect(body.key).toBe(pid);
                expect(body.value._id).toBe(pid);
                expect(body.value.group).toBe(gid);
                expect(body.value.name).toBe('pUpdated');
                expect(body.value.visibility).toBe('public');
                expect(ld.isArray(body.value.users)).toBeTruthy();
                done();
              }
            );
          });
        });

      });

      describe('pad.del DELETE and id', function () {
        it('will return an error if the pad does not exist',
          function (done) {
            var b = { body: { auth_token: token } };
            rq.del(padRoute + '/inexistentId', b, function (err, resp, body) {
              expect(resp.statusCode).toBe(404);
              expect(body.error).toMatch('KEY_NOT_FOUND');
              done();
            });
          }
        );

        it('should forbid removal for other pads', function (done) {
          var b = { body: { auth_token: token } };
          rq.del(padRoute + '/' + potherid, b, function (err, resp, body) {
            expect(err).toBeNull();
            expect(resp.statusCode).toBe(401);
            expect(body.error).toMatch('DENIED_RECORD_EDIT');
            done();
          });
        });

        it('should allow removal of all pads if the user is global admin',
          function (done) {
            var b = { body: { auth_token: admToken } };
            rq.del(padRoute + '/' + potherid, b, function (err, resp, body) {
              expect(err).toBeNull();
              expect(resp.statusCode).toBe(200);
              expect(body.success).toBeTruthy();
              expect(body.key).toBe(potherid);
              done();
            });
          }
        );

        it('should deletes the record and returns the key and success' +
         ' otherwise', function (done) {
            var b = { body: { auth_token: token } };
            rq.del(padRoute + '/' + pid, b, function (err, resp, body) {
              expect(resp.statusCode).toBe(200);
              expect(body.success).toBeTruthy();
              expect(body.key).toBe(pid);
              rq.get(padRoute + '/' + pid, function (err, resp, body) {
                expect(resp.statusCode).toBe(404);
                expect(body.error).toMatch('KEY_NOT_FOUND');
                done();
              });
            });
          }
        );
      });
    });

    describe('cacheAPI GET', function () {
      var cacheRoute = route + 'cache';

      it('should return {"userCacheReady":true} (with mockupserver, the cache is ready before the server)',
        function (done) {
          rq.get(cacheRoute + '/check', {}, function (err, resp, body) {
            expect(resp.statusCode).toBe(200);
            expect(body.userCacheReady).toBeTruthy();
            done();
          });
        }
      );
    });
  });

}).call(this);


<|start_filename|>spec/backend/storage.spec.js<|end_filename|>
/**
*  vim:set sw=2 ts=2 sts=2 ft=javascript expandtab:
*
*  ## License
*
*  Licensed to the Apache Software Foundation (ASF) under one
*  or more contributor license agreements.  See the NOTICE file
*  distributed with this work for additional information
*  regarding copyright ownership.  The ASF licenses this file
*  to you 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.
*
*  ## Description
*
*  This module consists only on a wrapper around etherpad database.
*/

(function () {
  'use strict';

  var ld = require('lodash');
  var storage = require('../../storage.js');
  var specCommon = require('./common.js');

  describe('storage', function () {
    beforeAll(specCommon.reInitDatabase);
    afterAll(specCommon.reInitDatabase);

    describe('Internal functions fn', function () {

      describe('getKeys', function () {

        beforeAll(function (done) {
          var db = storage.db;
          db.set('key1', 'value1', function () {
            db.set('key2', 'value2', function () {
              db.set('convictedKey', 'tmp', function () { done(); });
            });
          });
        });

        it('should return an undefined value for non existent keys',
          function (done) {
            storage.fn.getKeys(['key1', 'inexistent'], function (err, res) {
              expect(res.inexistent).toBeUndefined();
              done();
            });
          }
        );

        it('should return an undefined value for removed keys',
           function (done) {
             storage.db.remove('convictedKey', function () {
               storage.fn.getKeys(
                 ['key1', 'convictedKey'],
                 function (err, res) {
                   expect(res.convictedKey).toBeUndefined();
                   done();
                 }
               );
             });
           }
          );

        it('should return the result for one key or more', function (done) {
          storage.fn.getKeys(['key1'], function (err, results) {
            expect(err).toBeNull();
            expect(results.key1).toBe('value1');
            storage.fn.getKeys(['key2', 'key1'], function (err, results) {
              expect(err).toBeNull();
              expect(ld.isObject(results)).toBeTruthy();
              expect(results.key1).toBe('value1');
              expect(results.key2).toBe('value2');
              done();
            });
          });
        });
      });

      describe('delKeys', function () {

        beforeAll(function (done) {
          var db = storage.db;
          db.set('key1', 'value1', function () {
            db.set('key2', 'value2', function () {
              db.set('key3', 'value3', function () {
                done();
              });
            });
          });
        });

        it('should removes every given keys, even if thet don`t exist',
          function (done) {
            storage.fn.delKeys(['key1'], function (err, res) {
              expect(err).toBeNull();
              expect(res).toBeTruthy();
              storage.fn.delKeys(['key2', 'key3', 'key4'],
                function (err, res) {
                  expect(err).toBeNull();
                  expect(res).toBeTruthy();
                  done();
                }
              );
            });
          }
        );
      });

      describe('setKeys', function () {

        beforeAll(function (done) {
          var db = storage.db;
          db.set('existent', [1, 2, 3], function () { done(); });
        });

        it('should put the keys and their values into the database',
          function (done) {
            var kv = { 'JS': 'JavaScript', 'PL': 'Perl', 'existent': [2, 4] };
            storage.fn.setKeys(kv, function () {
              storage.fn.getKeys(ld.keys(kv), function (err, results) {
                expect(results.JS).toBe('JavaScript');
                expect(results.PL).toBe('Perl');
                expect(ld.isArray(results.existent)).toBeTruthy();
                expect(results.existent[1]).toBe(4);
                done();
              });
            });
          }
        );
      });
    });
  });
}).call(this);
