<commit_before>function m(mod) { return __dirname + '/../../../src/' + mod; }

const assert = require('assert').strict;
const common = require('../common');
const io = require(m('node_modules/socket.io-client'));
const padManager = require(m('node/db/PadManager'));
const plugins = require(m('static/js/pluginfw/plugin_defs'));
const setCookieParser = require(m('node_modules/set-cookie-parser'));
const settings = require(m('node/utils/Settings'));

const logger = common.logger;

// Waits for and returns the next named socket.io event. Rejects if there is any error while waiting
// (unless waiting for that error event).
const getSocketEvent = async (socket, event) => {
  const errorEvents = [
    'error',
    'connect_error',
    'connect_timeout',
    'reconnect_error',
    'reconnect_failed',
  ];
  const handlers = {};
  let timeoutId;
  return new Promise((resolve, reject) => {
    timeoutId = setTimeout(() => reject(new Error(`timed out waiting for ${event} event`)), 1000);
    for (const event of errorEvents) {
      handlers[event] = (errorString) => {
        logger.debug(`socket.io ${event} event: ${errorString}`);
        reject(new Error(errorString));
      };
    }
    // This will overwrite one of the above handlers if the user is waiting for an error event.
    handlers[event] = (...args) => {
      logger.debug(`socket.io ${event} event`);
      if (args.length > 1) return resolve(args);
      resolve(args[0]);
    }
    Object.entries(handlers).forEach(([event, handler]) => socket.on(event, handler));
  }).finally(() => {
    clearTimeout(timeoutId);
    Object.entries(handlers).forEach(([event, handler]) => socket.off(event, handler));
  });
};

// Establishes a new socket.io connection. Passes the cookies from the `set-cookie` header(s) in
// `res` (which may be nullish) to the server. Returns a socket.io Socket object.
const connect = async (res) => {
  // Convert the `set-cookie` header(s) into a `cookie` header.
  const resCookies = (res == null) ? {} : setCookieParser.parse(res, {map: true});
  const reqCookieHdr = Object.entries(resCookies).map(([name, cookie]) => {
    return `${name}=${encodeURIComponent(cookie.value)}`;
  }).join('; ');

  logger.debug('socket.io connecting...');
  const socket = io(`${common.baseUrl}/`, {
    forceNew: true, // Different tests will have different query parameters.
    path: '/socket.io',
    // socketio.js-client on node.js doesn't support cookies (see https://git.io/JU8u9), so the
    // express_sid cookie must be passed as a query parameter.
    query: {cookie: reqCookieHdr},
  });
  try {
    await getSocketEvent(socket, 'connect');
  } catch (e) {
    socket.close();
    throw e;
  }
  logger.debug('socket.io connected');

  return socket;
};

// Helper function to exchange CLIENT_READY+CLIENT_VARS messages for the named pad.
// Returns the CLIENT_VARS message from the server.
const handshake = async (socket, padID) => {
  logger.debug('sending CLIENT_READY...');
  socket.send({
    component: 'pad',
    type: 'CLIENT_READY',
    padId: padID,
    sessionID: null,
    token: '<PASSWORD>',
    protocolVersion: 2,
  });
  logger.debug('waiting for CLIENT_VARS response...');
  const msg = await getSocketEvent(socket, 'message');
  logger.debug('received CLIENT_VARS message');
  return msg;
};

describe(__filename, function() {
  let agent;
  before(async function() { agent = await common.init(); });

  let authorize;
  let authorizeHooksBackup;
  const cleanUpPads = async () => {
    const padIds = ['pad', 'other-pad', 'päd'];
    await Promise.all(padIds.map(async (padId) => {
      if (await padManager.doesPadExist(padId)) {
        const pad = await padManager.getPad(padId);
        await pad.remove();
      }
    }));
  };
  const settingsBackup = {};
  let socket;

  beforeEach(async function() {
    Object.assign(settingsBackup, settings);
    assert(socket == null);
    settings.editOnly = false;
    settings.requireAuthentication = false;
    settings.requireAuthorization = false;
    settings.users = {
      admin: {password: '<PASSWORD>', is_admin: true},
      user: {password: '<PASSWORD>'},
    };
    authorize = () => true;
    authorizeHooksBackup = plugins.hooks.authorize;
    plugins.hooks.authorize = [{hook_fn: (hookName, {req}, cb) => {
      return cb([authorize(req)]);
    }}];
    await cleanUpPads();
  });
  afterEach(async function() {
    Object.assign(settings, settingsBackup);
    if (socket) socket.close();
    socket = null;
    plugins.hooks.authorize = authorizeHooksBackup;
    await cleanUpPads();
  });

  describe('Normal accesses', function() {
    it('!authn anonymous cookie /p/pad -> 200, ok', async function() {
      const res = await agent.get('/p/pad').expect(200);
      socket = await connect(res);
      const clientVars = await handshake(socket, 'pad');
      assert.equal(clientVars.type, 'CLIENT_VARS');
    });
    it('!authn !cookie -> ok', async function() {
      socket = await connect(null);
      const clientVars = await handshake(socket, 'pad');
      assert.equal(clientVars.type, 'CLIENT_VARS');
    });
    it('!authn user /p/pad -> 200, ok', async function() {
      const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200);
      socket = await connect(res);
      const clientVars = await handshake(socket, 'pad');
      assert.equal(clientVars.type, 'CLIENT_VARS');
    });
    it('authn user /p/pad -> 200, ok', async function() {
      settings.requireAuthentication = true;
      const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200);
      socket = await connect(res);
      const clientVars = await handshake(socket, 'pad');
      assert.equal(clientVars.type, 'CLIENT_VARS');
    });
    it('authz user /p/pad -> 200, ok', async function() {
      settings.requireAuthentication = true;
      settings.requireAuthorization = true;
      const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200);
      socket = await connect(res);
      const clientVars = await handshake(socket, 'pad');
      assert.equal(clientVars.type, 'CLIENT_VARS');
    });
    it('supports pad names with characters that must be percent-encoded', async function() {
      settings.requireAuthentication = true;
      // requireAuthorization is set to true here to guarantee that the user's padAuthorizations
      // object is populated. Technically this isn't necessary because the user's padAuthorizations
      // is currently populated even if requireAuthorization is false, but setting this to true
      // ensures the test remains useful if the implementation ever changes.
      settings.requireAuthorization = true;
      const encodedPadId = encodeURIComponent('päd');
      const res = await agent.get(`/p/${encodedPadId}`).auth('user', 'user-password').expect(200);
      socket = await connect(res);
      const clientVars = await handshake(socket, 'päd');
      assert.equal(clientVars.type, 'CLIENT_VARS');
    });
  });

  describe('Abnormal access attempts', function() {
    it('authn anonymous /p/pad -> 401, error', async function() {
      settings.requireAuthentication = true;
      const res = await agent.get('/p/pad').expect(401);
      // Despite the 401, try to create the pad via a socket.io connection anyway.
      socket = await connect(res);
      const message = await handshake(socket, 'pad');
      assert.equal(message.accessStatus, 'deny');
    });
    it('authn !cookie -> error', async function() {
      settings.requireAuthentication = true;
      socket = await connect(null);
      const message = await handshake(socket, 'pad');
      assert.equal(message.accessStatus, 'deny');
    });
    it('authorization bypass attempt -> error', async function() {
      // Only allowed to access /p/pad.
      authorize = (req) => req.path === '/p/pad';
      settings.requireAuthentication = true;
      settings.requireAuthorization = true;
      // First authenticate and establish a session.
      const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200);
      socket = await connect(res);
      // Accessing /p/other-pad should fail, despite the successful fetch of /p/pad.
      const message = await handshake(socket, 'other-pad');
      assert.equal(message.accessStatus, 'deny');
    });
  });

  describe('Authorization levels via authorize hook', function() {
    beforeEach(async function() {
      settings.requireAuthentication = true;
      settings.requireAuthorization = true;
    });

    it("level='create' -> can create", async function() {
      authorize = () => 'create';
      const res = await agent.get('/p/pad').auth('user', '<PASSWORD>-password').expect(200);
      socket = await connect(res);
      const clientVars = await handshake(socket, 'pad');
      assert.equal(clientVars.type, 'CLIENT_VARS');
      assert.equal(clientVars.data.readonly, false);
    });
    it('level=true -> can create', async function() {
      authorize = () => true;
      const res = await agent.get('/p/pad').auth('user', '<PASSWORD>-password').expect(200);
      socket = await connect(res);
      const clientVars = await handshake(socket, 'pad');
      assert.equal(clientVars.type, 'CLIENT_VARS');
      assert.equal(clientVars.data.readonly, false);
    });
    it("level='modify' -> can modify", async function() {
      await padManager.getPad('pad'); // Create the pad.
      authorize = () => 'modify';
      const res = await agent.get('/p/pad').auth('user', '<PASSWORD>-password').expect(200);
      socket = await connect(res);
      const clientVars = await handshake(socket, 'pad');
      assert.equal(clientVars.type, 'CLIENT_VARS');
      assert.equal(clientVars.data.readonly, false);
    });
    it("level='create' settings.editOnly=true -> unable to create", async function() {
      authorize = () => 'create';
      settings.editOnly = true;
      const res = await agent.get('/p/pad').auth('user', '<PASSWORD>').expect(200);
      socket = await connect(res);
      const message = await handshake(socket, 'pad');
      assert.equal(message.accessStatus, 'deny');
    });
    it("level='modify' settings.editOnly=false -> unable to create", async function() {
      authorize = () => 'modify';
      settings.editOnly = false;
      const res = await agent.get('/p/pad').auth('user', '<PASSWORD>-password').expect(200);
      socket = await connect(res);
      const message = await handshake(socket, 'pad');
      assert.equal(message.accessStatus, 'deny');
    });
    it("level='readOnly' -> unable to create", async function() {
      authorize = () => 'readOnly';
      const res = await agent.get('/p/pad').auth('user', '<PASSWORD>-password').expect(200);
      socket = await connect(res);
      const message = await handshake(socket, 'pad');
      assert.equal(message.accessStatus, 'deny');
    });
    it("level='readOnly' -> unable to modify", async function() {
      await padManager.getPad('pad'); // Create the pad.
      authorize = () => 'readOnly';
      const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200);
      socket = await connect(res);
      const clientVars = await handshake(socket, 'pad');
      assert.equal(clientVars.type, 'CLIENT_VARS');
      assert.equal(clientVars.data.readonly, true);
    });
  });

  describe('Authorization levels via user settings', function() {
    beforeEach(async function() {
      settings.requireAuthentication = true;
    });

    it('user.canCreate = true -> can create and modify', async function() {
      settings.users.user.canCreate = true;
      const res = await agent.get('/p/pad').auth('user', '<PASSWORD>-password').expect(200);
      socket = await connect(res);
      const clientVars = await handshake(socket, 'pad');
      assert.equal(clientVars.type, 'CLIENT_VARS');
      assert.equal(clientVars.data.readonly, false);
    });
    it('user.canCreate = false -> unable to create', async function() {
      settings.users.user.canCreate = false;
      const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200);
      socket = await connect(res);
      const message = await handshake(socket, 'pad');
      assert.equal(message.accessStatus, 'deny');
    });
    it('user.readOnly = true -> unable to create', async function() {
      settings.users.user.readOnly = true;
      const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200);
      socket = await connect(res);
      const message = await handshake(socket, 'pad');
      assert.equal(message.accessStatus, 'deny');
    });
    it('user.readOnly = true -> unable to modify', async function() {
      await padManager.getPad('pad'); // Create the pad.
      settings.users.user.readOnly = true;
      const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200);
      socket = await connect(res);
      const clientVars = await handshake(socket, 'pad');
      assert.equal(clientVars.type, 'CLIENT_VARS');
      assert.equal(clientVars.data.readonly, true);
    });
    it('user.readOnly = false -> can create and modify', async function() {
      settings.users.user.readOnly = false;
      const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200);
      socket = await connect(res);
      const clientVars = await handshake(socket, 'pad');
      assert.equal(clientVars.type, 'CLIENT_VARS');
      assert.equal(clientVars.data.readonly, false);
    });
    it('user.readOnly = true, user.canCreate = true -> unable to create', async function() {
      settings.users.user.canCreate = true;
      settings.users.user.readOnly = true;
      const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200);
      socket = await connect(res);
      const message = await handshake(socket, 'pad');
      assert.equal(message.accessStatus, 'deny');
    });
  });

  describe('Authorization level interaction between authorize hook and user settings', function() {
    beforeEach(async function() {
      settings.requireAuthentication = true;
      settings.requireAuthorization = true;
    });

    it('authorize hook does not elevate level from user settings', async function() {
      settings.users.user.readOnly = true;
      authorize = () => 'create';
      const res = await agent.get('/p/pad').auth('user', '<PASSWORD>-password').expect(200);
      socket = await connect(res);
      const message = await handshake(socket, 'pad');
      assert.equal(message.accessStatus, 'deny');
    });
    it('user settings does not elevate level from authorize hook', async function() {
      settings.users.user.readOnly = false;
      settings.users.user.canCreate = true;
      authorize = () => 'readOnly';
      const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200);
      socket = await connect(res);
      const message = await handshake(socket, 'pad');
      assert.equal(message.accessStatus, 'deny');
    });
  });
});
<commit_msg>tests: Clear auth hooks before running socket.io unit tests<commit_after>/* global __dirname, __filename, afterEach, before, beforeEach, clearTimeout, describe, it, require, setTimeout */

function m(mod) { return __dirname + '/../../../src/' + mod; }

const assert = require('assert').strict;
const common = require('../common');
const io = require(m('node_modules/socket.io-client'));
const padManager = require(m('node/db/PadManager'));
const plugins = require(m('static/js/pluginfw/plugin_defs'));
const setCookieParser = require(m('node_modules/set-cookie-parser'));
const settings = require(m('node/utils/Settings'));

const logger = common.logger;

// Waits for and returns the next named socket.io event. Rejects if there is any error while waiting
// (unless waiting for that error event).
const getSocketEvent = async (socket, event) => {
  const errorEvents = [
    'error',
    'connect_error',
    'connect_timeout',
    'reconnect_error',
    'reconnect_failed',
  ];
  const handlers = {};
  let timeoutId;
  return new Promise((resolve, reject) => {
    timeoutId = setTimeout(() => reject(new Error(`timed out waiting for ${event} event`)), 1000);
    for (const event of errorEvents) {
      handlers[event] = (errorString) => {
        logger.debug(`socket.io ${event} event: ${errorString}`);
        reject(new Error(errorString));
      };
    }
    // This will overwrite one of the above handlers if the user is waiting for an error event.
    handlers[event] = (...args) => {
      logger.debug(`socket.io ${event} event`);
      if (args.length > 1) return resolve(args);
      resolve(args[0]);
    };
    Object.entries(handlers).forEach(([event, handler]) => socket.on(event, handler));
  }).finally(() => {
    clearTimeout(timeoutId);
    Object.entries(handlers).forEach(([event, handler]) => socket.off(event, handler));
  });
};

// Establishes a new socket.io connection. Passes the cookies from the `set-cookie` header(s) in
// `res` (which may be nullish) to the server. Returns a socket.io Socket object.
const connect = async (res) => {
  // Convert the `set-cookie` header(s) into a `cookie` header.
  const resCookies = (res == null) ? {} : setCookieParser.parse(res, {map: true});
  const reqCookieHdr = Object.entries(resCookies).map(([name, cookie]) => {
    return `${name}=${encodeURIComponent(cookie.value)}`;
  }).join('; ');

  logger.debug('socket.io connecting...');
  const socket = io(`${common.baseUrl}/`, {
    forceNew: true, // Different tests will have different query parameters.
    path: '/socket.io',
    // socketio.js-client on node.js doesn't support cookies (see https://git.io/JU8u9), so the
    // express_sid cookie must be passed as a query parameter.
    query: {cookie: reqCookieHdr},
  });
  try {
    await getSocketEvent(socket, 'connect');
  } catch (e) {
    socket.close();
    throw e;
  }
  logger.debug('socket.io connected');

  return socket;
};

// Helper function to exchange CLIENT_READY+CLIENT_VARS messages for the named pad.
// Returns the CLIENT_VARS message from the server.
const handshake = async (socket, padID) => {
  logger.debug('sending CLIENT_READY...');
  socket.send({
    component: 'pad',
    type: 'CLIENT_READY',
    padId: padID,
    sessionID: null,
    token: '<PASSWORD>',
    protocolVersion: 2,
  });
  logger.debug('waiting for CLIENT_VARS response...');
  const msg = await getSocketEvent(socket, 'message');
  logger.debug('received CLIENT_VARS message');
  return msg;
};

describe(__filename, function() {
  let agent;
  let authorize;
  const backups = {};
  const cleanUpPads = async () => {
    const padIds = ['pad', 'other-pad', 'päd'];
    await Promise.all(padIds.map(async (padId) => {
      if (await padManager.doesPadExist(padId)) {
        const pad = await padManager.getPad(padId);
        await pad.remove();
      }
    }));
  };
  let socket;

  before(async function() { agent = await common.init(); });
  beforeEach(async function() {
    backups.hooks = {};
    for (const hookName of ['preAuthorize', 'authenticate', 'authorize']) {
      backups.hooks[hookName] = plugins.hooks[hookName];
      plugins.hooks[hookName] = [];
    }
    backups.settings = {};
    for (const setting of ['editOnly', 'requireAuthentication', 'requireAuthorization', 'users']) {
      backups.settings[setting] = settings[setting];
    }
    settings.editOnly = false;
    settings.requireAuthentication = false;
    settings.requireAuthorization = false;
    settings.users = {
      admin: {password: '<PASSWORD>', is_admin: true},
      user: {password: '<PASSWORD>'},
    };
    assert(socket == null);
    authorize = () => true;
    plugins.hooks.authorize = [{hook_fn: (hookName, {req}, cb) => {
      return cb([authorize(req)]);
    }}];
    await cleanUpPads();
  });
  afterEach(async function() {
    if (socket) socket.close();
    socket = null;
    await cleanUpPads();
    Object.assign(plugins.hooks, backups.hooks);
    Object.assign(settings, backups.settings);
  });

  describe('Normal accesses', function() {
    it('!authn anonymous cookie /p/pad -> 200, ok', async function() {
      const res = await agent.get('/p/pad').expect(200);
      socket = await connect(res);
      const clientVars = await handshake(socket, 'pad');
      assert.equal(clientVars.type, 'CLIENT_VARS');
    });
    it('!authn !cookie -> ok', async function() {
      socket = await connect(null);
      const clientVars = await handshake(socket, 'pad');
      assert.equal(clientVars.type, 'CLIENT_VARS');
    });
    it('!authn user /p/pad -> 200, ok', async function() {
      const res = await agent.get('/p/pad').auth('user', '<PASSWORD>').expect(200);
      socket = await connect(res);
      const clientVars = await handshake(socket, 'pad');
      assert.equal(clientVars.type, 'CLIENT_VARS');
    });
    it('authn user /p/pad -> 200, ok', async function() {
      settings.requireAuthentication = true;
      const res = await agent.get('/p/pad').auth('user', '<PASSWORD>').expect(200);
      socket = await connect(res);
      const clientVars = await handshake(socket, 'pad');
      assert.equal(clientVars.type, 'CLIENT_VARS');
    });
    it('authz user /p/pad -> 200, ok', async function() {
      settings.requireAuthentication = true;
      settings.requireAuthorization = true;
      const res = await agent.get('/p/pad').auth('user', '<PASSWORD>-password').expect(200);
      socket = await connect(res);
      const clientVars = await handshake(socket, 'pad');
      assert.equal(clientVars.type, 'CLIENT_VARS');
    });
    it('supports pad names with characters that must be percent-encoded', async function() {
      settings.requireAuthentication = true;
      // requireAuthorization is set to true here to guarantee that the user's padAuthorizations
      // object is populated. Technically this isn't necessary because the user's padAuthorizations
      // is currently populated even if requireAuthorization is false, but setting this to true
      // ensures the test remains useful if the implementation ever changes.
      settings.requireAuthorization = true;
      const encodedPadId = encodeURIComponent('päd');
      const res = await agent.get(`/p/${encodedPadId}`).auth('user', 'user-password').expect(200);
      socket = await connect(res);
      const clientVars = await handshake(socket, 'päd');
      assert.equal(clientVars.type, 'CLIENT_VARS');
    });
  });

  describe('Abnormal access attempts', function() {
    it('authn anonymous /p/pad -> 401, error', async function() {
      settings.requireAuthentication = true;
      const res = await agent.get('/p/pad').expect(401);
      // Despite the 401, try to create the pad via a socket.io connection anyway.
      socket = await connect(res);
      const message = await handshake(socket, 'pad');
      assert.equal(message.accessStatus, 'deny');
    });
    it('authn !cookie -> error', async function() {
      settings.requireAuthentication = true;
      socket = await connect(null);
      const message = await handshake(socket, 'pad');
      assert.equal(message.accessStatus, 'deny');
    });
    it('authorization bypass attempt -> error', async function() {
      // Only allowed to access /p/pad.
      authorize = (req) => req.path === '/p/pad';
      settings.requireAuthentication = true;
      settings.requireAuthorization = true;
      // First authenticate and establish a session.
      const res = await agent.get('/p/pad').auth('user', '<PASSWORD>-password').expect(200);
      socket = await connect(res);
      // Accessing /p/other-pad should fail, despite the successful fetch of /p/pad.
      const message = await handshake(socket, 'other-pad');
      assert.equal(message.accessStatus, 'deny');
    });
  });

  describe('Authorization levels via authorize hook', function() {
    beforeEach(async function() {
      settings.requireAuthentication = true;
      settings.requireAuthorization = true;
    });

    it("level='create' -> can create", async function() {
      authorize = () => 'create';
      const res = await agent.get('/p/pad').auth('user', '<PASSWORD>').expect(200);
      socket = await connect(res);
      const clientVars = await handshake(socket, 'pad');
      assert.equal(clientVars.type, 'CLIENT_VARS');
      assert.equal(clientVars.data.readonly, false);
    });
    it('level=true -> can create', async function() {
      authorize = () => true;
      const res = await agent.get('/p/pad').auth('user', '<PASSWORD>').expect(200);
      socket = await connect(res);
      const clientVars = await handshake(socket, 'pad');
      assert.equal(clientVars.type, 'CLIENT_VARS');
      assert.equal(clientVars.data.readonly, false);
    });
    it("level='modify' -> can modify", async function() {
      await padManager.getPad('pad'); // Create the pad.
      authorize = () => 'modify';
      const res = await agent.get('/p/pad').auth('user', '<PASSWORD>-password').expect(200);
      socket = await connect(res);
      const clientVars = await handshake(socket, 'pad');
      assert.equal(clientVars.type, 'CLIENT_VARS');
      assert.equal(clientVars.data.readonly, false);
    });
    it("level='create' settings.editOnly=true -> unable to create", async function() {
      authorize = () => 'create';
      settings.editOnly = true;
      const res = await agent.get('/p/pad').auth('user', '<PASSWORD>-password').expect(200);
      socket = await connect(res);
      const message = await handshake(socket, 'pad');
      assert.equal(message.accessStatus, 'deny');
    });
    it("level='modify' settings.editOnly=false -> unable to create", async function() {
      authorize = () => 'modify';
      settings.editOnly = false;
      const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200);
      socket = await connect(res);
      const message = await handshake(socket, 'pad');
      assert.equal(message.accessStatus, 'deny');
    });
    it("level='readOnly' -> unable to create", async function() {
      authorize = () => 'readOnly';
      const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200);
      socket = await connect(res);
      const message = await handshake(socket, 'pad');
      assert.equal(message.accessStatus, 'deny');
    });
    it("level='readOnly' -> unable to modify", async function() {
      await padManager.getPad('pad'); // Create the pad.
      authorize = () => 'readOnly';
      const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200);
      socket = await connect(res);
      const clientVars = await handshake(socket, 'pad');
      assert.equal(clientVars.type, 'CLIENT_VARS');
      assert.equal(clientVars.data.readonly, true);
    });
  });

  describe('Authorization levels via user settings', function() {
    beforeEach(async function() {
      settings.requireAuthentication = true;
    });

    it('user.canCreate = true -> can create and modify', async function() {
      settings.users.user.canCreate = true;
      const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200);
      socket = await connect(res);
      const clientVars = await handshake(socket, 'pad');
      assert.equal(clientVars.type, 'CLIENT_VARS');
      assert.equal(clientVars.data.readonly, false);
    });
    it('user.canCreate = false -> unable to create', async function() {
      settings.users.user.canCreate = false;
      const res = await agent.get('/p/pad').auth('user', '<PASSWORD>-password').expect(200);
      socket = await connect(res);
      const message = await handshake(socket, 'pad');
      assert.equal(message.accessStatus, 'deny');
    });
    it('user.readOnly = true -> unable to create', async function() {
      settings.users.user.readOnly = true;
      const res = await agent.get('/p/pad').auth('user', '<PASSWORD>-password').expect(200);
      socket = await connect(res);
      const message = await handshake(socket, 'pad');
      assert.equal(message.accessStatus, 'deny');
    });
    it('user.readOnly = true -> unable to modify', async function() {
      await padManager.getPad('pad'); // Create the pad.
      settings.users.user.readOnly = true;
      const res = await agent.get('/p/pad').auth('user', '<PASSWORD>-password').expect(200);
      socket = await connect(res);
      const clientVars = await handshake(socket, 'pad');
      assert.equal(clientVars.type, 'CLIENT_VARS');
      assert.equal(clientVars.data.readonly, true);
    });
    it('user.readOnly = false -> can create and modify', async function() {
      settings.users.user.readOnly = false;
      const res = await agent.get('/p/pad').auth('user', '<PASSWORD>-password').expect(200);
      socket = await connect(res);
      const clientVars = await handshake(socket, 'pad');
      assert.equal(clientVars.type, 'CLIENT_VARS');
      assert.equal(clientVars.data.readonly, false);
    });
    it('user.readOnly = true, user.canCreate = true -> unable to create', async function() {
      settings.users.user.canCreate = true;
      settings.users.user.readOnly = true;
      const res = await agent.get('/p/pad').auth('user', '<PASSWORD>-password').expect(200);
      socket = await connect(res);
      const message = await handshake(socket, 'pad');
      assert.equal(message.accessStatus, 'deny');
    });
  });

  describe('Authorization level interaction between authorize hook and user settings', function() {
    beforeEach(async function() {
      settings.requireAuthentication = true;
      settings.requireAuthorization = true;
    });

    it('authorize hook does not elevate level from user settings', async function() {
      settings.users.user.readOnly = true;
      authorize = () => 'create';
      const res = await agent.get('/p/pad').auth('user', '<PASSWORD>').expect(200);
      socket = await connect(res);
      const message = await handshake(socket, 'pad');
      assert.equal(message.accessStatus, 'deny');
    });
    it('user settings does not elevate level from authorize hook', async function() {
      settings.users.user.readOnly = false;
      settings.users.user.canCreate = true;
      authorize = () => 'readOnly';
      const res = await agent.get('/p/pad').auth('user', '<PASSWORD>').expect(200);
      socket = await connect(res);
      const message = await handshake(socket, 'pad');
      assert.equal(message.accessStatus, 'deny');
    });
  });
});

{'fraction_non_alphanumeric': 0.1576954732510288, 'fraction_numerical': 0.0061234567901234565, 'mean_word_length': 3.3649949705417446, 'pattern_counts': {'":': 0, '<': 32, '<?xml version=': 0, '>': 126, 'https://': 2, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '498955', 'n_tokens_mistral': 8912, 'n_tokens_neox': 8847, 'n_words': 2262}