mirror of
https://github.com/ether/etherpad-lite.git
synced 2025-04-25 01:46:14 -04:00
Move all files to esm
This commit is contained in:
parent
237f7242ec
commit
76a6f665a4
87 changed files with 23693 additions and 30732 deletions
|
@ -1,67 +1,59 @@
|
|||
import assert$0 from "assert";
|
||||
import * as common from "../common.js";
|
||||
import * as exportEtherpad from "../../../node/utils/ExportEtherpad.js";
|
||||
import * as padManager from "../../../node/db/PadManager.js";
|
||||
import * as plugins from "../../../static/js/pluginfw/plugin_defs.js";
|
||||
import * as readOnlyManager from "../../../node/db/ReadOnlyManager.js";
|
||||
'use strict';
|
||||
|
||||
const assert = require('assert').strict;
|
||||
const common = require('../common');
|
||||
const exportEtherpad = require('../../../node/utils/ExportEtherpad');
|
||||
const padManager = require('../../../node/db/PadManager');
|
||||
const plugins = require('../../../static/js/pluginfw/plugin_defs');
|
||||
const readOnlyManager = require('../../../node/db/ReadOnlyManager');
|
||||
|
||||
const assert = assert$0.strict;
|
||||
describe(__filename, function () {
|
||||
let padId;
|
||||
|
||||
beforeEach(async function () {
|
||||
padId = common.randomString();
|
||||
assert(!await padManager.doesPadExist(padId));
|
||||
});
|
||||
|
||||
describe('exportEtherpadAdditionalContent', function () {
|
||||
let hookBackup;
|
||||
|
||||
before(async function () {
|
||||
hookBackup = plugins.hooks.exportEtherpadAdditionalContent || [];
|
||||
plugins.hooks.exportEtherpadAdditionalContent = [{hook_fn: () => ['custom']}];
|
||||
let padId;
|
||||
beforeEach(async function () {
|
||||
padId = common.randomString();
|
||||
assert(!await padManager.doesPadExist(padId));
|
||||
});
|
||||
|
||||
after(async function () {
|
||||
plugins.hooks.exportEtherpadAdditionalContent = hookBackup;
|
||||
describe('exportEtherpadAdditionalContent', function () {
|
||||
let hookBackup;
|
||||
before(async function () {
|
||||
hookBackup = plugins.hooks.exportEtherpadAdditionalContent || [];
|
||||
plugins.hooks.exportEtherpadAdditionalContent = [{ hook_fn: () => ['custom'] }];
|
||||
});
|
||||
after(async function () {
|
||||
plugins.hooks.exportEtherpadAdditionalContent = hookBackup;
|
||||
});
|
||||
it('exports custom records', async function () {
|
||||
const pad = await padManager.getPad(padId);
|
||||
await pad.db.set(`custom:${padId}`, 'a');
|
||||
await pad.db.set(`custom:${padId}:`, 'b');
|
||||
await pad.db.set(`custom:${padId}:foo`, 'c');
|
||||
const data = await exportEtherpad.getPadRaw(pad.id, null);
|
||||
assert.equal(data[`custom:${padId}`], 'a');
|
||||
assert.equal(data[`custom:${padId}:`], 'b');
|
||||
assert.equal(data[`custom:${padId}:foo`], 'c');
|
||||
});
|
||||
it('export from read-only pad uses read-only ID', async function () {
|
||||
const pad = await padManager.getPad(padId);
|
||||
const readOnlyId = await readOnlyManager.getReadOnlyId(padId);
|
||||
await pad.db.set(`custom:${padId}`, 'a');
|
||||
await pad.db.set(`custom:${padId}:`, 'b');
|
||||
await pad.db.set(`custom:${padId}:foo`, 'c');
|
||||
const data = await exportEtherpad.getPadRaw(padId, readOnlyId);
|
||||
assert.equal(data[`custom:${readOnlyId}`], 'a');
|
||||
assert.equal(data[`custom:${readOnlyId}:`], 'b');
|
||||
assert.equal(data[`custom:${readOnlyId}:foo`], 'c');
|
||||
assert(!(`custom:${padId}` in data));
|
||||
assert(!(`custom:${padId}:` in data));
|
||||
assert(!(`custom:${padId}:foo` in data));
|
||||
});
|
||||
it('does not export records from pad with similar ID', async function () {
|
||||
const pad = await padManager.getPad(padId);
|
||||
await pad.db.set(`custom:${padId}x`, 'a');
|
||||
await pad.db.set(`custom:${padId}x:`, 'b');
|
||||
await pad.db.set(`custom:${padId}x:foo`, 'c');
|
||||
const data = await exportEtherpad.getPadRaw(pad.id, null);
|
||||
assert(!(`custom:${padId}x` in data));
|
||||
assert(!(`custom:${padId}x:` in data));
|
||||
assert(!(`custom:${padId}x:foo` in data));
|
||||
});
|
||||
});
|
||||
|
||||
it('exports custom records', async function () {
|
||||
const pad = await padManager.getPad(padId);
|
||||
await pad.db.set(`custom:${padId}`, 'a');
|
||||
await pad.db.set(`custom:${padId}:`, 'b');
|
||||
await pad.db.set(`custom:${padId}:foo`, 'c');
|
||||
const data = await exportEtherpad.getPadRaw(pad.id, null);
|
||||
assert.equal(data[`custom:${padId}`], 'a');
|
||||
assert.equal(data[`custom:${padId}:`], 'b');
|
||||
assert.equal(data[`custom:${padId}:foo`], 'c');
|
||||
});
|
||||
|
||||
it('export from read-only pad uses read-only ID', async function () {
|
||||
const pad = await padManager.getPad(padId);
|
||||
const readOnlyId = await readOnlyManager.getReadOnlyId(padId);
|
||||
await pad.db.set(`custom:${padId}`, 'a');
|
||||
await pad.db.set(`custom:${padId}:`, 'b');
|
||||
await pad.db.set(`custom:${padId}:foo`, 'c');
|
||||
const data = await exportEtherpad.getPadRaw(padId, readOnlyId);
|
||||
assert.equal(data[`custom:${readOnlyId}`], 'a');
|
||||
assert.equal(data[`custom:${readOnlyId}:`], 'b');
|
||||
assert.equal(data[`custom:${readOnlyId}:foo`], 'c');
|
||||
assert(!(`custom:${padId}` in data));
|
||||
assert(!(`custom:${padId}:` in data));
|
||||
assert(!(`custom:${padId}:foo` in data));
|
||||
});
|
||||
|
||||
it('does not export records from pad with similar ID', async function () {
|
||||
const pad = await padManager.getPad(padId);
|
||||
await pad.db.set(`custom:${padId}x`, 'a');
|
||||
await pad.db.set(`custom:${padId}x:`, 'b');
|
||||
await pad.db.set(`custom:${padId}x:foo`, 'c');
|
||||
const data = await exportEtherpad.getPadRaw(pad.id, null);
|
||||
assert(!(`custom:${padId}x` in data));
|
||||
assert(!(`custom:${padId}x:` in data));
|
||||
assert(!(`custom:${padId}x:foo` in data));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,208 +1,185 @@
|
|||
import assert$0 from "assert";
|
||||
import * as authorManager from "../../../node/db/AuthorManager.js";
|
||||
import * as db from "../../../node/db/DB.js";
|
||||
import * as importEtherpad from "../../../node/utils/ImportEtherpad.js";
|
||||
import * as padManager from "../../../node/db/PadManager.js";
|
||||
import * as plugins from "../../../static/js/pluginfw/plugin_defs.js";
|
||||
import * as padUtils from "../../../static/js/pad_utils.js";
|
||||
'use strict';
|
||||
|
||||
const assert = require('assert').strict;
|
||||
const authorManager = require('../../../node/db/AuthorManager');
|
||||
const db = require('../../../node/db/DB');
|
||||
const importEtherpad = require('../../../node/utils/ImportEtherpad');
|
||||
const padManager = require('../../../node/db/PadManager');
|
||||
const plugins = require('../../../static/js/pluginfw/plugin_defs');
|
||||
const {randomString} = require('../../../static/js/pad_utils');
|
||||
|
||||
const assert = assert$0.strict;
|
||||
const { randomString } = padUtils;
|
||||
describe(__filename, function () {
|
||||
let padId;
|
||||
|
||||
const makeAuthorId = () => `a.${randomString(16)}`;
|
||||
|
||||
const makeExport = (authorId) => ({
|
||||
'pad:testing': {
|
||||
atext: {
|
||||
text: 'foo\n',
|
||||
attribs: '|1+4',
|
||||
},
|
||||
pool: {
|
||||
numToAttrib: {},
|
||||
nextNum: 0,
|
||||
},
|
||||
head: 0,
|
||||
savedRevisions: [],
|
||||
},
|
||||
[`globalAuthor:${authorId}`]: {
|
||||
colorId: '#000000',
|
||||
name: 'new',
|
||||
timestamp: 1598747784631,
|
||||
padIDs: 'testing',
|
||||
},
|
||||
'pad:testing:revs:0': {
|
||||
changeset: 'Z:1>3+3$foo',
|
||||
meta: {
|
||||
author: '',
|
||||
timestamp: 1597632398288,
|
||||
pool: {
|
||||
numToAttrib: {},
|
||||
nextNum: 0,
|
||||
let padId;
|
||||
const makeAuthorId = () => `a.${randomString(16)}`;
|
||||
const makeExport = (authorId) => ({
|
||||
'pad:testing': {
|
||||
atext: {
|
||||
text: 'foo\n',
|
||||
attribs: '|1+4',
|
||||
},
|
||||
pool: {
|
||||
numToAttrib: {},
|
||||
nextNum: 0,
|
||||
},
|
||||
head: 0,
|
||||
savedRevisions: [],
|
||||
},
|
||||
atext: {
|
||||
text: 'foo\n',
|
||||
attribs: '|1+4',
|
||||
[`globalAuthor:${authorId}`]: {
|
||||
colorId: '#000000',
|
||||
name: 'new',
|
||||
timestamp: 1598747784631,
|
||||
padIDs: 'testing',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
beforeEach(async function () {
|
||||
padId = randomString(10);
|
||||
assert(!await padManager.doesPadExist(padId));
|
||||
});
|
||||
|
||||
it('unknown db records are ignored', async function () {
|
||||
const badKey = `maliciousDbKey${randomString(10)}`;
|
||||
await importEtherpad.setPadRaw(padId, JSON.stringify({
|
||||
[badKey]: 'value',
|
||||
...makeExport(makeAuthorId()),
|
||||
}));
|
||||
assert(await db.get(badKey) == null);
|
||||
});
|
||||
|
||||
it('changes are all or nothing', async function () {
|
||||
const authorId = makeAuthorId();
|
||||
const data = makeExport(authorId);
|
||||
data['pad:differentPadId:revs:0'] = data['pad:testing:revs:0'];
|
||||
delete data['pad:testing:revs:0'];
|
||||
assert.rejects(importEtherpad.setPadRaw(padId, JSON.stringify(data)), /unexpected pad ID/);
|
||||
assert(!await authorManager.doesAuthorExist(authorId));
|
||||
assert(!await padManager.doesPadExist(padId));
|
||||
});
|
||||
|
||||
describe('author pad IDs', function () {
|
||||
let existingAuthorId;
|
||||
let newAuthorId;
|
||||
|
||||
'pad:testing:revs:0': {
|
||||
changeset: 'Z:1>3+3$foo',
|
||||
meta: {
|
||||
author: '',
|
||||
timestamp: 1597632398288,
|
||||
pool: {
|
||||
numToAttrib: {},
|
||||
nextNum: 0,
|
||||
},
|
||||
atext: {
|
||||
text: 'foo\n',
|
||||
attribs: '|1+4',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
beforeEach(async function () {
|
||||
existingAuthorId = (await authorManager.createAuthor('existing')).authorID;
|
||||
assert(await authorManager.doesAuthorExist(existingAuthorId));
|
||||
assert.deepEqual((await authorManager.listPadsOfAuthor(existingAuthorId)).padIDs, []);
|
||||
newAuthorId = makeAuthorId();
|
||||
assert.notEqual(newAuthorId, existingAuthorId);
|
||||
assert(!await authorManager.doesAuthorExist(newAuthorId));
|
||||
padId = randomString(10);
|
||||
assert(!await padManager.doesPadExist(padId));
|
||||
});
|
||||
|
||||
it('author does not yet exist', async function () {
|
||||
await importEtherpad.setPadRaw(padId, JSON.stringify(makeExport(newAuthorId)));
|
||||
assert(await authorManager.doesAuthorExist(newAuthorId));
|
||||
const author = await authorManager.getAuthor(newAuthorId);
|
||||
assert.equal(author.name, 'new');
|
||||
assert.equal(author.colorId, '#000000');
|
||||
assert.deepEqual((await authorManager.listPadsOfAuthor(newAuthorId)).padIDs, [padId]);
|
||||
it('unknown db records are ignored', async function () {
|
||||
const badKey = `maliciousDbKey${randomString(10)}`;
|
||||
await importEtherpad.setPadRaw(padId, JSON.stringify({
|
||||
[badKey]: 'value',
|
||||
...makeExport(makeAuthorId()),
|
||||
}));
|
||||
assert(await db.get(badKey) == null);
|
||||
});
|
||||
|
||||
it('author already exists, no pads', async function () {
|
||||
newAuthorId = existingAuthorId;
|
||||
await importEtherpad.setPadRaw(padId, JSON.stringify(makeExport(newAuthorId)));
|
||||
assert(await authorManager.doesAuthorExist(newAuthorId));
|
||||
const author = await authorManager.getAuthor(newAuthorId);
|
||||
assert.equal(author.name, 'existing');
|
||||
assert.notEqual(author.colorId, '#000000');
|
||||
assert.deepEqual((await authorManager.listPadsOfAuthor(newAuthorId)).padIDs, [padId]);
|
||||
});
|
||||
|
||||
it('author already exists, on different pad', async function () {
|
||||
const otherPadId = randomString(10);
|
||||
await authorManager.addPad(existingAuthorId, otherPadId);
|
||||
newAuthorId = existingAuthorId;
|
||||
await importEtherpad.setPadRaw(padId, JSON.stringify(makeExport(newAuthorId)));
|
||||
assert(await authorManager.doesAuthorExist(newAuthorId));
|
||||
const author = await authorManager.getAuthor(newAuthorId);
|
||||
assert.equal(author.name, 'existing');
|
||||
assert.notEqual(author.colorId, '#000000');
|
||||
assert.deepEqual(
|
||||
(await authorManager.listPadsOfAuthor(newAuthorId)).padIDs.sort(),
|
||||
[otherPadId, padId].sort());
|
||||
});
|
||||
|
||||
it('author already exists, on same pad', async function () {
|
||||
await authorManager.addPad(existingAuthorId, padId);
|
||||
newAuthorId = existingAuthorId;
|
||||
await importEtherpad.setPadRaw(padId, JSON.stringify(makeExport(newAuthorId)));
|
||||
assert(await authorManager.doesAuthorExist(newAuthorId));
|
||||
const author = await authorManager.getAuthor(newAuthorId);
|
||||
assert.equal(author.name, 'existing');
|
||||
assert.notEqual(author.colorId, '#000000');
|
||||
assert.deepEqual((await authorManager.listPadsOfAuthor(newAuthorId)).padIDs, [padId]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('enforces consistent pad ID', function () {
|
||||
it('pad record has different pad ID', async function () {
|
||||
const data = makeExport(makeAuthorId());
|
||||
data['pad:differentPadId'] = data['pad:testing'];
|
||||
delete data['pad:testing'];
|
||||
assert.rejects(importEtherpad.setPadRaw(padId, JSON.stringify(data)), /unexpected pad ID/);
|
||||
});
|
||||
|
||||
it('globalAuthor record has different pad ID', async function () {
|
||||
const authorId = makeAuthorId();
|
||||
const data = makeExport(authorId);
|
||||
data[`globalAuthor:${authorId}`].padIDs = 'differentPadId';
|
||||
assert.rejects(importEtherpad.setPadRaw(padId, JSON.stringify(data)), /unexpected pad ID/);
|
||||
});
|
||||
|
||||
it('pad rev record has different pad ID', async function () {
|
||||
const data = makeExport(makeAuthorId());
|
||||
data['pad:differentPadId:revs:0'] = data['pad:testing:revs:0'];
|
||||
delete data['pad:testing:revs:0'];
|
||||
assert.rejects(importEtherpad.setPadRaw(padId, JSON.stringify(data)), /unexpected pad ID/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('order of records does not matter', function () {
|
||||
for (const perm of [[0, 1, 2], [0, 2, 1], [1, 0, 2], [1, 2, 0], [2, 0, 1], [2, 1, 0]]) {
|
||||
it(JSON.stringify(perm), async function () {
|
||||
it('changes are all or nothing', async function () {
|
||||
const authorId = makeAuthorId();
|
||||
const records = Object.entries(makeExport(authorId));
|
||||
assert.equal(records.length, 3);
|
||||
await importEtherpad.setPadRaw(
|
||||
padId, JSON.stringify(Object.fromEntries(perm.map((i) => records[i]))));
|
||||
assert.deepEqual((await authorManager.listPadsOfAuthor(authorId)).padIDs, [padId]);
|
||||
const pad = await padManager.getPad(padId);
|
||||
assert.equal(pad.text(), 'foo\n');
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe('exportEtherpadAdditionalContent', function () {
|
||||
let hookBackup;
|
||||
|
||||
before(async function () {
|
||||
hookBackup = plugins.hooks.exportEtherpadAdditionalContent || [];
|
||||
plugins.hooks.exportEtherpadAdditionalContent = [{hook_fn: () => ['custom']}];
|
||||
const data = makeExport(authorId);
|
||||
data['pad:differentPadId:revs:0'] = data['pad:testing:revs:0'];
|
||||
delete data['pad:testing:revs:0'];
|
||||
assert.rejects(importEtherpad.setPadRaw(padId, JSON.stringify(data)), /unexpected pad ID/);
|
||||
assert(!await authorManager.doesAuthorExist(authorId));
|
||||
assert(!await padManager.doesPadExist(padId));
|
||||
});
|
||||
|
||||
after(async function () {
|
||||
plugins.hooks.exportEtherpadAdditionalContent = hookBackup;
|
||||
describe('author pad IDs', function () {
|
||||
let existingAuthorId;
|
||||
let newAuthorId;
|
||||
beforeEach(async function () {
|
||||
existingAuthorId = (await authorManager.createAuthor('existing')).authorID;
|
||||
assert(await authorManager.doesAuthorExist(existingAuthorId));
|
||||
assert.deepEqual((await authorManager.listPadsOfAuthor(existingAuthorId)).padIDs, []);
|
||||
newAuthorId = makeAuthorId();
|
||||
assert.notEqual(newAuthorId, existingAuthorId);
|
||||
assert(!await authorManager.doesAuthorExist(newAuthorId));
|
||||
});
|
||||
it('author does not yet exist', async function () {
|
||||
await importEtherpad.setPadRaw(padId, JSON.stringify(makeExport(newAuthorId)));
|
||||
assert(await authorManager.doesAuthorExist(newAuthorId));
|
||||
const author = await authorManager.getAuthor(newAuthorId);
|
||||
assert.equal(author.name, 'new');
|
||||
assert.equal(author.colorId, '#000000');
|
||||
assert.deepEqual((await authorManager.listPadsOfAuthor(newAuthorId)).padIDs, [padId]);
|
||||
});
|
||||
it('author already exists, no pads', async function () {
|
||||
newAuthorId = existingAuthorId;
|
||||
await importEtherpad.setPadRaw(padId, JSON.stringify(makeExport(newAuthorId)));
|
||||
assert(await authorManager.doesAuthorExist(newAuthorId));
|
||||
const author = await authorManager.getAuthor(newAuthorId);
|
||||
assert.equal(author.name, 'existing');
|
||||
assert.notEqual(author.colorId, '#000000');
|
||||
assert.deepEqual((await authorManager.listPadsOfAuthor(newAuthorId)).padIDs, [padId]);
|
||||
});
|
||||
it('author already exists, on different pad', async function () {
|
||||
const otherPadId = randomString(10);
|
||||
await authorManager.addPad(existingAuthorId, otherPadId);
|
||||
newAuthorId = existingAuthorId;
|
||||
await importEtherpad.setPadRaw(padId, JSON.stringify(makeExport(newAuthorId)));
|
||||
assert(await authorManager.doesAuthorExist(newAuthorId));
|
||||
const author = await authorManager.getAuthor(newAuthorId);
|
||||
assert.equal(author.name, 'existing');
|
||||
assert.notEqual(author.colorId, '#000000');
|
||||
assert.deepEqual((await authorManager.listPadsOfAuthor(newAuthorId)).padIDs.sort(), [otherPadId, padId].sort());
|
||||
});
|
||||
it('author already exists, on same pad', async function () {
|
||||
await authorManager.addPad(existingAuthorId, padId);
|
||||
newAuthorId = existingAuthorId;
|
||||
await importEtherpad.setPadRaw(padId, JSON.stringify(makeExport(newAuthorId)));
|
||||
assert(await authorManager.doesAuthorExist(newAuthorId));
|
||||
const author = await authorManager.getAuthor(newAuthorId);
|
||||
assert.equal(author.name, 'existing');
|
||||
assert.notEqual(author.colorId, '#000000');
|
||||
assert.deepEqual((await authorManager.listPadsOfAuthor(newAuthorId)).padIDs, [padId]);
|
||||
});
|
||||
});
|
||||
|
||||
it('imports from custom prefix', async function () {
|
||||
await importEtherpad.setPadRaw(padId, JSON.stringify({
|
||||
...makeExport(makeAuthorId()),
|
||||
'custom:testing': 'a',
|
||||
'custom:testing:foo': 'b',
|
||||
}));
|
||||
const pad = await padManager.getPad(padId);
|
||||
assert.equal(await pad.db.get(`custom:${padId}`), 'a');
|
||||
assert.equal(await pad.db.get(`custom:${padId}:foo`), 'b');
|
||||
describe('enforces consistent pad ID', function () {
|
||||
it('pad record has different pad ID', async function () {
|
||||
const data = makeExport(makeAuthorId());
|
||||
data['pad:differentPadId'] = data['pad:testing'];
|
||||
delete data['pad:testing'];
|
||||
assert.rejects(importEtherpad.setPadRaw(padId, JSON.stringify(data)), /unexpected pad ID/);
|
||||
});
|
||||
it('globalAuthor record has different pad ID', async function () {
|
||||
const authorId = makeAuthorId();
|
||||
const data = makeExport(authorId);
|
||||
data[`globalAuthor:${authorId}`].padIDs = 'differentPadId';
|
||||
assert.rejects(importEtherpad.setPadRaw(padId, JSON.stringify(data)), /unexpected pad ID/);
|
||||
});
|
||||
it('pad rev record has different pad ID', async function () {
|
||||
const data = makeExport(makeAuthorId());
|
||||
data['pad:differentPadId:revs:0'] = data['pad:testing:revs:0'];
|
||||
delete data['pad:testing:revs:0'];
|
||||
assert.rejects(importEtherpad.setPadRaw(padId, JSON.stringify(data)), /unexpected pad ID/);
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects records for pad with similar ID', async function () {
|
||||
await assert.rejects(importEtherpad.setPadRaw(padId, JSON.stringify({
|
||||
...makeExport(makeAuthorId()),
|
||||
'custom:testingx': 'x',
|
||||
})), /unexpected pad ID/);
|
||||
assert(await db.get(`custom:${padId}x`) == null);
|
||||
await assert.rejects(importEtherpad.setPadRaw(padId, JSON.stringify({
|
||||
...makeExport(makeAuthorId()),
|
||||
'custom:testingx:foo': 'x',
|
||||
})), /unexpected pad ID/);
|
||||
assert(await db.get(`custom:${padId}x:foo`) == null);
|
||||
describe('order of records does not matter', function () {
|
||||
for (const perm of [[0, 1, 2], [0, 2, 1], [1, 0, 2], [1, 2, 0], [2, 0, 1], [2, 1, 0]]) {
|
||||
it(JSON.stringify(perm), async function () {
|
||||
const authorId = makeAuthorId();
|
||||
const records = Object.entries(makeExport(authorId));
|
||||
assert.equal(records.length, 3);
|
||||
await importEtherpad.setPadRaw(padId, JSON.stringify(Object.fromEntries(perm.map((i) => records[i]))));
|
||||
assert.deepEqual((await authorManager.listPadsOfAuthor(authorId)).padIDs, [padId]);
|
||||
const pad = await padManager.getPad(padId);
|
||||
assert.equal(pad.text(), 'foo\n');
|
||||
});
|
||||
}
|
||||
});
|
||||
describe('exportEtherpadAdditionalContent', function () {
|
||||
let hookBackup;
|
||||
before(async function () {
|
||||
hookBackup = plugins.hooks.exportEtherpadAdditionalContent || [];
|
||||
plugins.hooks.exportEtherpadAdditionalContent = [{ hook_fn: () => ['custom'] }];
|
||||
});
|
||||
after(async function () {
|
||||
plugins.hooks.exportEtherpadAdditionalContent = hookBackup;
|
||||
});
|
||||
it('imports from custom prefix', async function () {
|
||||
await importEtherpad.setPadRaw(padId, JSON.stringify({
|
||||
...makeExport(makeAuthorId()),
|
||||
'custom:testing': 'a',
|
||||
'custom:testing:foo': 'b',
|
||||
}));
|
||||
const pad = await padManager.getPad(padId);
|
||||
assert.equal(await pad.db.get(`custom:${padId}`), 'a');
|
||||
assert.equal(await pad.db.get(`custom:${padId}:foo`), 'b');
|
||||
});
|
||||
it('rejects records for pad with similar ID', async function () {
|
||||
await assert.rejects(importEtherpad.setPadRaw(padId, JSON.stringify({
|
||||
...makeExport(makeAuthorId()),
|
||||
'custom:testingx': 'x',
|
||||
})), /unexpected pad ID/);
|
||||
assert(await db.get(`custom:${padId}x`) == null);
|
||||
await assert.rejects(importEtherpad.setPadRaw(padId, JSON.stringify({
|
||||
...makeExport(makeAuthorId()),
|
||||
'custom:testingx:foo': 'x',
|
||||
})), /unexpected pad ID/);
|
||||
assert(await db.get(`custom:${padId}x:foo`) == null);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,134 +1,120 @@
|
|||
import * as Pad from "../../../node/db/Pad.js";
|
||||
import assert$0 from "assert";
|
||||
import * as authorManager from "../../../node/db/AuthorManager.js";
|
||||
import * as common from "../common.js";
|
||||
import * as padManager from "../../../node/db/PadManager.js";
|
||||
import * as plugins from "../../../static/js/pluginfw/plugin_defs.js";
|
||||
import * as settings from "../../../node/utils/Settings.js";
|
||||
'use strict';
|
||||
|
||||
const Pad = require('../../../node/db/Pad');
|
||||
const assert = require('assert').strict;
|
||||
const authorManager = require('../../../node/db/AuthorManager');
|
||||
const common = require('../common');
|
||||
const padManager = require('../../../node/db/PadManager');
|
||||
const plugins = require('../../../static/js/pluginfw/plugin_defs');
|
||||
const settings = require('../../../node/utils/Settings');
|
||||
|
||||
const assert = assert$0.strict;
|
||||
describe(__filename, function () {
|
||||
const backups = {};
|
||||
let pad;
|
||||
let padId;
|
||||
|
||||
before(async function () {
|
||||
backups.hooks = {
|
||||
padDefaultContent: plugins.hooks.padDefaultContent,
|
||||
};
|
||||
backups.defaultPadText = settings.defaultPadText;
|
||||
});
|
||||
|
||||
beforeEach(async function () {
|
||||
backups.hooks.padDefaultContent = [];
|
||||
padId = common.randomString();
|
||||
assert(!(await padManager.doesPadExist(padId)));
|
||||
});
|
||||
|
||||
afterEach(async function () {
|
||||
Object.assign(plugins.hooks, backups.hooks);
|
||||
if (pad != null) await pad.remove();
|
||||
pad = null;
|
||||
});
|
||||
|
||||
describe('cleanText', function () {
|
||||
const testCases = [
|
||||
['', ''],
|
||||
['\n', '\n'],
|
||||
['x', 'x'],
|
||||
['x\n', 'x\n'],
|
||||
['x\ny\n', 'x\ny\n'],
|
||||
['x\ry\n', 'x\ny\n'],
|
||||
['x\r\ny\n', 'x\ny\n'],
|
||||
['x\r\r\ny\n', 'x\n\ny\n'],
|
||||
];
|
||||
for (const [input, want] of testCases) {
|
||||
it(`${JSON.stringify(input)} -> ${JSON.stringify(want)}`, async function () {
|
||||
assert.equal(Pad.cleanText(input), want);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe('padDefaultContent hook', function () {
|
||||
it('runs when a pad is created without specific text', async function () {
|
||||
const p = new Promise((resolve) => {
|
||||
plugins.hooks.padDefaultContent.push({hook_fn: () => resolve()});
|
||||
});
|
||||
pad = await padManager.getPad(padId);
|
||||
await p;
|
||||
const backups = {};
|
||||
let pad;
|
||||
let padId;
|
||||
before(async function () {
|
||||
backups.hooks = {
|
||||
padDefaultContent: plugins.hooks.padDefaultContent,
|
||||
};
|
||||
backups.defaultPadText = settings.defaultPadText;
|
||||
});
|
||||
|
||||
it('not run if pad is created with specific text', async function () {
|
||||
plugins.hooks.padDefaultContent.push(
|
||||
{hook_fn: () => { throw new Error('should not be called'); }});
|
||||
pad = await padManager.getPad(padId, '');
|
||||
beforeEach(async function () {
|
||||
backups.hooks.padDefaultContent = [];
|
||||
padId = common.randomString();
|
||||
assert(!(await padManager.doesPadExist(padId)));
|
||||
});
|
||||
|
||||
it('defaults to settings.defaultPadText', async function () {
|
||||
const p = new Promise((resolve, reject) => {
|
||||
plugins.hooks.padDefaultContent.push({hook_fn: async (hookName, ctx) => {
|
||||
try {
|
||||
assert.equal(ctx.type, 'text');
|
||||
assert.equal(ctx.content, settings.defaultPadText);
|
||||
} catch (err) {
|
||||
return reject(err);
|
||||
}
|
||||
resolve();
|
||||
}});
|
||||
});
|
||||
pad = await padManager.getPad(padId);
|
||||
await p;
|
||||
afterEach(async function () {
|
||||
Object.assign(plugins.hooks, backups.hooks);
|
||||
if (pad != null)
|
||||
await pad.remove();
|
||||
pad = null;
|
||||
});
|
||||
|
||||
it('passes the pad object', async function () {
|
||||
const gotP = new Promise((resolve) => {
|
||||
plugins.hooks.padDefaultContent.push({hook_fn: async (hookName, {pad}) => resolve(pad)});
|
||||
});
|
||||
pad = await padManager.getPad(padId);
|
||||
assert.equal(await gotP, pad);
|
||||
describe('cleanText', function () {
|
||||
const testCases = [
|
||||
['', ''],
|
||||
['\n', '\n'],
|
||||
['x', 'x'],
|
||||
['x\n', 'x\n'],
|
||||
['x\ny\n', 'x\ny\n'],
|
||||
['x\ry\n', 'x\ny\n'],
|
||||
['x\r\ny\n', 'x\ny\n'],
|
||||
['x\r\r\ny\n', 'x\n\ny\n'],
|
||||
];
|
||||
for (const [input, want] of testCases) {
|
||||
it(`${JSON.stringify(input)} -> ${JSON.stringify(want)}`, async function () {
|
||||
assert.equal(Pad.cleanText(input), want);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it('passes empty authorId if not provided', async function () {
|
||||
const gotP = new Promise((resolve) => {
|
||||
plugins.hooks.padDefaultContent.push(
|
||||
{hook_fn: async (hookName, {authorId}) => resolve(authorId)});
|
||||
});
|
||||
pad = await padManager.getPad(padId);
|
||||
assert.equal(await gotP, '');
|
||||
describe('padDefaultContent hook', function () {
|
||||
it('runs when a pad is created without specific text', async function () {
|
||||
const p = new Promise((resolve) => {
|
||||
plugins.hooks.padDefaultContent.push({ hook_fn: () => resolve() });
|
||||
});
|
||||
pad = await padManager.getPad(padId);
|
||||
await p;
|
||||
});
|
||||
it('not run if pad is created with specific text', async function () {
|
||||
plugins.hooks.padDefaultContent.push({ hook_fn: () => { throw new Error('should not be called'); } });
|
||||
pad = await padManager.getPad(padId, '');
|
||||
});
|
||||
it('defaults to settings.defaultPadText', async function () {
|
||||
const p = new Promise((resolve, reject) => {
|
||||
plugins.hooks.padDefaultContent.push({ hook_fn: async (hookName, ctx) => {
|
||||
try {
|
||||
assert.equal(ctx.type, 'text');
|
||||
assert.equal(ctx.content, settings.defaultPadText);
|
||||
}
|
||||
catch (err) {
|
||||
return reject(err);
|
||||
}
|
||||
resolve();
|
||||
} });
|
||||
});
|
||||
pad = await padManager.getPad(padId);
|
||||
await p;
|
||||
});
|
||||
it('passes the pad object', async function () {
|
||||
const gotP = new Promise((resolve) => {
|
||||
plugins.hooks.padDefaultContent.push({ hook_fn: async (hookName, { pad }) => resolve(pad) });
|
||||
});
|
||||
pad = await padManager.getPad(padId);
|
||||
assert.equal(await gotP, pad);
|
||||
});
|
||||
it('passes empty authorId if not provided', async function () {
|
||||
const gotP = new Promise((resolve) => {
|
||||
plugins.hooks.padDefaultContent.push({ hook_fn: async (hookName, { authorId }) => resolve(authorId) });
|
||||
});
|
||||
pad = await padManager.getPad(padId);
|
||||
assert.equal(await gotP, '');
|
||||
});
|
||||
it('passes provided authorId', async function () {
|
||||
const want = await authorManager.getAuthor4Token(`t.${padId}`);
|
||||
const gotP = new Promise((resolve) => {
|
||||
plugins.hooks.padDefaultContent.push({ hook_fn: async (hookName, { authorId }) => resolve(authorId) });
|
||||
});
|
||||
pad = await padManager.getPad(padId, null, want);
|
||||
assert.equal(await gotP, want);
|
||||
});
|
||||
it('uses provided content', async function () {
|
||||
const want = 'hello world';
|
||||
assert.notEqual(want, settings.defaultPadText);
|
||||
plugins.hooks.padDefaultContent.push({ hook_fn: async (hookName, ctx) => {
|
||||
ctx.type = 'text';
|
||||
ctx.content = want;
|
||||
} });
|
||||
pad = await padManager.getPad(padId);
|
||||
assert.equal(pad.text(), `${want}\n`);
|
||||
});
|
||||
it('cleans provided content', async function () {
|
||||
const input = 'foo\r\nbar\r\tbaz';
|
||||
const want = 'foo\nbar\n baz';
|
||||
assert.notEqual(want, settings.defaultPadText);
|
||||
plugins.hooks.padDefaultContent.push({ hook_fn: async (hookName, ctx) => {
|
||||
ctx.type = 'text';
|
||||
ctx.content = input;
|
||||
} });
|
||||
pad = await padManager.getPad(padId);
|
||||
assert.equal(pad.text(), `${want}\n`);
|
||||
});
|
||||
});
|
||||
|
||||
it('passes provided authorId', async function () {
|
||||
const want = await authorManager.getAuthor4Token(`t.${padId}`);
|
||||
const gotP = new Promise((resolve) => {
|
||||
plugins.hooks.padDefaultContent.push(
|
||||
{hook_fn: async (hookName, {authorId}) => resolve(authorId)});
|
||||
});
|
||||
pad = await padManager.getPad(padId, null, want);
|
||||
assert.equal(await gotP, want);
|
||||
});
|
||||
|
||||
it('uses provided content', async function () {
|
||||
const want = 'hello world';
|
||||
assert.notEqual(want, settings.defaultPadText);
|
||||
plugins.hooks.padDefaultContent.push({hook_fn: async (hookName, ctx) => {
|
||||
ctx.type = 'text';
|
||||
ctx.content = want;
|
||||
}});
|
||||
pad = await padManager.getPad(padId);
|
||||
assert.equal(pad.text(), `${want}\n`);
|
||||
});
|
||||
|
||||
it('cleans provided content', async function () {
|
||||
const input = 'foo\r\nbar\r\tbaz';
|
||||
const want = 'foo\nbar\n baz';
|
||||
assert.notEqual(want, settings.defaultPadText);
|
||||
plugins.hooks.padDefaultContent.push({hook_fn: async (hookName, ctx) => {
|
||||
ctx.type = 'text';
|
||||
ctx.content = input;
|
||||
}});
|
||||
pad = await padManager.getPad(padId);
|
||||
assert.equal(pad.text(), `${want}\n`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,238 +1,210 @@
|
|||
import SessionStore from "../../../node/db/SessionStore.js";
|
||||
import assert$0 from "assert";
|
||||
import * as common from "../common.js";
|
||||
import * as db from "../../../node/db/DB.js";
|
||||
import util from "util";
|
||||
'use strict';
|
||||
|
||||
const SessionStore = require('../../../node/db/SessionStore');
|
||||
const assert = require('assert').strict;
|
||||
const common = require('../common');
|
||||
const db = require('../../../node/db/DB');
|
||||
const util = require('util');
|
||||
|
||||
const assert = assert$0.strict;
|
||||
describe(__filename, function () {
|
||||
let ss;
|
||||
let sid;
|
||||
|
||||
const set = async (sess) => await util.promisify(ss.set).call(ss, sid, sess);
|
||||
const get = async () => await util.promisify(ss.get).call(ss, sid);
|
||||
const destroy = async () => await util.promisify(ss.destroy).call(ss, sid);
|
||||
const touch = async (sess) => await util.promisify(ss.touch).call(ss, sid, sess);
|
||||
|
||||
before(async function () {
|
||||
await common.init();
|
||||
});
|
||||
|
||||
beforeEach(async function () {
|
||||
ss = new SessionStore();
|
||||
sid = common.randomString();
|
||||
});
|
||||
|
||||
afterEach(async function () {
|
||||
if (ss != null) {
|
||||
if (sid != null) await destroy();
|
||||
ss.shutdown();
|
||||
}
|
||||
sid = null;
|
||||
ss = null;
|
||||
});
|
||||
|
||||
describe('set', function () {
|
||||
it('set of null is a no-op', async function () {
|
||||
await set(null);
|
||||
assert(await db.get(`sessionstorage:${sid}`) == null);
|
||||
let ss;
|
||||
let sid;
|
||||
const set = async (sess) => await util.promisify(ss.set).call(ss, sid, sess);
|
||||
const get = async () => await util.promisify(ss.get).call(ss, sid);
|
||||
const destroy = async () => await util.promisify(ss.destroy).call(ss, sid);
|
||||
const touch = async (sess) => await util.promisify(ss.touch).call(ss, sid, sess);
|
||||
before(async function () {
|
||||
await common.init();
|
||||
});
|
||||
|
||||
it('set of non-expiring session', async function () {
|
||||
const sess = {foo: 'bar', baz: {asdf: 'jkl;'}};
|
||||
await set(sess);
|
||||
assert.equal(JSON.stringify(await db.get(`sessionstorage:${sid}`)), JSON.stringify(sess));
|
||||
});
|
||||
|
||||
it('set of session that expires', async function () {
|
||||
const sess = {foo: 'bar', cookie: {expires: new Date(Date.now() + 100)}};
|
||||
await set(sess);
|
||||
assert.equal(JSON.stringify(await db.get(`sessionstorage:${sid}`)), JSON.stringify(sess));
|
||||
await new Promise((resolve) => setTimeout(resolve, 110));
|
||||
// Writing should start a timeout.
|
||||
assert(await db.get(`sessionstorage:${sid}`) == null);
|
||||
});
|
||||
|
||||
it('set of already expired session', async function () {
|
||||
const sess = {foo: 'bar', cookie: {expires: new Date(1)}};
|
||||
await set(sess);
|
||||
// No record should have been created.
|
||||
assert(await db.get(`sessionstorage:${sid}`) == null);
|
||||
});
|
||||
|
||||
it('switch from non-expiring to expiring', async function () {
|
||||
const sess = {foo: 'bar'};
|
||||
await set(sess);
|
||||
const sess2 = {foo: 'bar', cookie: {expires: new Date(Date.now() + 100)}};
|
||||
await set(sess2);
|
||||
await new Promise((resolve) => setTimeout(resolve, 110));
|
||||
assert(await db.get(`sessionstorage:${sid}`) == null);
|
||||
});
|
||||
|
||||
it('switch from expiring to non-expiring', async function () {
|
||||
const sess = {foo: 'bar', cookie: {expires: new Date(Date.now() + 100)}};
|
||||
await set(sess);
|
||||
const sess2 = {foo: 'bar'};
|
||||
await set(sess2);
|
||||
await new Promise((resolve) => setTimeout(resolve, 110));
|
||||
assert.equal(JSON.stringify(await db.get(`sessionstorage:${sid}`)), JSON.stringify(sess2));
|
||||
});
|
||||
});
|
||||
|
||||
describe('get', function () {
|
||||
it('get of non-existent entry', async function () {
|
||||
assert(await get() == null);
|
||||
});
|
||||
|
||||
it('set+get round trip', async function () {
|
||||
const sess = {foo: 'bar', baz: {asdf: 'jkl;'}};
|
||||
await set(sess);
|
||||
assert.equal(JSON.stringify(await get()), JSON.stringify(sess));
|
||||
});
|
||||
|
||||
it('get of record from previous run (no expiration)', async function () {
|
||||
const sess = {foo: 'bar', baz: {asdf: 'jkl;'}};
|
||||
await db.set(`sessionstorage:${sid}`, sess);
|
||||
assert.equal(JSON.stringify(await get()), JSON.stringify(sess));
|
||||
});
|
||||
|
||||
it('get of record from previous run (not yet expired)', async function () {
|
||||
const sess = {foo: 'bar', cookie: {expires: new Date(Date.now() + 100)}};
|
||||
await db.set(`sessionstorage:${sid}`, sess);
|
||||
assert.equal(JSON.stringify(await get()), JSON.stringify(sess));
|
||||
await new Promise((resolve) => setTimeout(resolve, 110));
|
||||
// Reading should start a timeout.
|
||||
assert(await db.get(`sessionstorage:${sid}`) == null);
|
||||
});
|
||||
|
||||
it('get of record from previous run (already expired)', async function () {
|
||||
const sess = {foo: 'bar', cookie: {expires: new Date(1)}};
|
||||
await db.set(`sessionstorage:${sid}`, sess);
|
||||
assert(await get() == null);
|
||||
assert(await db.get(`sessionstorage:${sid}`) == null);
|
||||
});
|
||||
|
||||
it('external expiration update is picked up', async function () {
|
||||
const sess = {foo: 'bar', cookie: {expires: new Date(Date.now() + 100)}};
|
||||
await set(sess);
|
||||
assert.equal(JSON.stringify(await get()), JSON.stringify(sess));
|
||||
const sess2 = {...sess, cookie: {expires: new Date(Date.now() + 200)}};
|
||||
await db.set(`sessionstorage:${sid}`, sess2);
|
||||
assert.equal(JSON.stringify(await get()), JSON.stringify(sess2));
|
||||
await new Promise((resolve) => setTimeout(resolve, 110));
|
||||
// The original timeout should not have fired.
|
||||
assert.equal(JSON.stringify(await get()), JSON.stringify(sess2));
|
||||
});
|
||||
});
|
||||
|
||||
describe('shutdown', function () {
|
||||
it('shutdown cancels timeouts', async function () {
|
||||
const sess = {foo: 'bar', cookie: {expires: new Date(Date.now() + 100)}};
|
||||
await set(sess);
|
||||
assert.equal(JSON.stringify(await get()), JSON.stringify(sess));
|
||||
ss.shutdown();
|
||||
await new Promise((resolve) => setTimeout(resolve, 110));
|
||||
// The record should not have been automatically purged.
|
||||
assert.equal(JSON.stringify(await db.get(`sessionstorage:${sid}`)), JSON.stringify(sess));
|
||||
});
|
||||
});
|
||||
|
||||
describe('destroy', function () {
|
||||
it('destroy deletes the database record', async function () {
|
||||
const sess = {cookie: {expires: new Date(Date.now() + 100)}};
|
||||
await set(sess);
|
||||
await destroy();
|
||||
assert(await db.get(`sessionstorage:${sid}`) == null);
|
||||
});
|
||||
|
||||
it('destroy cancels the timeout', async function () {
|
||||
const sess = {cookie: {expires: new Date(Date.now() + 100)}};
|
||||
await set(sess);
|
||||
await destroy();
|
||||
await db.set(`sessionstorage:${sid}`, sess);
|
||||
await new Promise((resolve) => setTimeout(resolve, 110));
|
||||
assert.equal(JSON.stringify(await db.get(`sessionstorage:${sid}`)), JSON.stringify(sess));
|
||||
});
|
||||
|
||||
it('destroy session that does not exist', async function () {
|
||||
await destroy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('touch without refresh', function () {
|
||||
it('touch before set is equivalent to set if session expires', async function () {
|
||||
const sess = {cookie: {expires: new Date(Date.now() + 1000)}};
|
||||
await touch(sess);
|
||||
assert.equal(JSON.stringify(await get()), JSON.stringify(sess));
|
||||
});
|
||||
|
||||
it('touch updates observed expiration but not database', async function () {
|
||||
const start = Date.now();
|
||||
const sess = {cookie: {expires: new Date(start + 200)}};
|
||||
await set(sess);
|
||||
const sess2 = {cookie: {expires: new Date(start + 12000)}};
|
||||
await touch(sess2);
|
||||
assert.equal(JSON.stringify(await db.get(`sessionstorage:${sid}`)), JSON.stringify(sess));
|
||||
assert.equal(JSON.stringify(await get()), JSON.stringify(sess2));
|
||||
});
|
||||
});
|
||||
|
||||
describe('touch with refresh', function () {
|
||||
beforeEach(async function () {
|
||||
ss = new SessionStore(200);
|
||||
ss = new SessionStore();
|
||||
sid = common.randomString();
|
||||
});
|
||||
|
||||
it('touch before set is equivalent to set if session expires', async function () {
|
||||
const sess = {cookie: {expires: new Date(Date.now() + 1000)}};
|
||||
await touch(sess);
|
||||
assert.equal(JSON.stringify(await get()), JSON.stringify(sess));
|
||||
afterEach(async function () {
|
||||
if (ss != null) {
|
||||
if (sid != null)
|
||||
await destroy();
|
||||
ss.shutdown();
|
||||
}
|
||||
sid = null;
|
||||
ss = null;
|
||||
});
|
||||
|
||||
it('touch before eligible for refresh updates expiration but not DB', async function () {
|
||||
const now = Date.now();
|
||||
const sess = {foo: 'bar', cookie: {expires: new Date(now + 1000)}};
|
||||
await set(sess);
|
||||
const sess2 = {foo: 'bar', cookie: {expires: new Date(now + 1001)}};
|
||||
await touch(sess2);
|
||||
assert.equal(JSON.stringify(await db.get(`sessionstorage:${sid}`)), JSON.stringify(sess));
|
||||
assert.equal(JSON.stringify(await get()), JSON.stringify(sess2));
|
||||
describe('set', function () {
|
||||
it('set of null is a no-op', async function () {
|
||||
await set(null);
|
||||
assert(await db.get(`sessionstorage:${sid}`) == null);
|
||||
});
|
||||
it('set of non-expiring session', async function () {
|
||||
const sess = { foo: 'bar', baz: { asdf: 'jkl;' } };
|
||||
await set(sess);
|
||||
assert.equal(JSON.stringify(await db.get(`sessionstorage:${sid}`)), JSON.stringify(sess));
|
||||
});
|
||||
it('set of session that expires', async function () {
|
||||
const sess = { foo: 'bar', cookie: { expires: new Date(Date.now() + 100) } };
|
||||
await set(sess);
|
||||
assert.equal(JSON.stringify(await db.get(`sessionstorage:${sid}`)), JSON.stringify(sess));
|
||||
await new Promise((resolve) => setTimeout(resolve, 110));
|
||||
// Writing should start a timeout.
|
||||
assert(await db.get(`sessionstorage:${sid}`) == null);
|
||||
});
|
||||
it('set of already expired session', async function () {
|
||||
const sess = { foo: 'bar', cookie: { expires: new Date(1) } };
|
||||
await set(sess);
|
||||
// No record should have been created.
|
||||
assert(await db.get(`sessionstorage:${sid}`) == null);
|
||||
});
|
||||
it('switch from non-expiring to expiring', async function () {
|
||||
const sess = { foo: 'bar' };
|
||||
await set(sess);
|
||||
const sess2 = { foo: 'bar', cookie: { expires: new Date(Date.now() + 100) } };
|
||||
await set(sess2);
|
||||
await new Promise((resolve) => setTimeout(resolve, 110));
|
||||
assert(await db.get(`sessionstorage:${sid}`) == null);
|
||||
});
|
||||
it('switch from expiring to non-expiring', async function () {
|
||||
const sess = { foo: 'bar', cookie: { expires: new Date(Date.now() + 100) } };
|
||||
await set(sess);
|
||||
const sess2 = { foo: 'bar' };
|
||||
await set(sess2);
|
||||
await new Promise((resolve) => setTimeout(resolve, 110));
|
||||
assert.equal(JSON.stringify(await db.get(`sessionstorage:${sid}`)), JSON.stringify(sess2));
|
||||
});
|
||||
});
|
||||
|
||||
it('touch before eligible for refresh updates timeout', async function () {
|
||||
const start = Date.now();
|
||||
const sess = {foo: 'bar', cookie: {expires: new Date(start + 200)}};
|
||||
await set(sess);
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
const sess2 = {foo: 'bar', cookie: {expires: new Date(start + 399)}};
|
||||
await touch(sess2);
|
||||
await new Promise((resolve) => setTimeout(resolve, 110));
|
||||
assert.equal(JSON.stringify(await db.get(`sessionstorage:${sid}`)), JSON.stringify(sess));
|
||||
assert.equal(JSON.stringify(await get()), JSON.stringify(sess2));
|
||||
describe('get', function () {
|
||||
it('get of non-existent entry', async function () {
|
||||
assert(await get() == null);
|
||||
});
|
||||
it('set+get round trip', async function () {
|
||||
const sess = { foo: 'bar', baz: { asdf: 'jkl;' } };
|
||||
await set(sess);
|
||||
assert.equal(JSON.stringify(await get()), JSON.stringify(sess));
|
||||
});
|
||||
it('get of record from previous run (no expiration)', async function () {
|
||||
const sess = { foo: 'bar', baz: { asdf: 'jkl;' } };
|
||||
await db.set(`sessionstorage:${sid}`, sess);
|
||||
assert.equal(JSON.stringify(await get()), JSON.stringify(sess));
|
||||
});
|
||||
it('get of record from previous run (not yet expired)', async function () {
|
||||
const sess = { foo: 'bar', cookie: { expires: new Date(Date.now() + 100) } };
|
||||
await db.set(`sessionstorage:${sid}`, sess);
|
||||
assert.equal(JSON.stringify(await get()), JSON.stringify(sess));
|
||||
await new Promise((resolve) => setTimeout(resolve, 110));
|
||||
// Reading should start a timeout.
|
||||
assert(await db.get(`sessionstorage:${sid}`) == null);
|
||||
});
|
||||
it('get of record from previous run (already expired)', async function () {
|
||||
const sess = { foo: 'bar', cookie: { expires: new Date(1) } };
|
||||
await db.set(`sessionstorage:${sid}`, sess);
|
||||
assert(await get() == null);
|
||||
assert(await db.get(`sessionstorage:${sid}`) == null);
|
||||
});
|
||||
it('external expiration update is picked up', async function () {
|
||||
const sess = { foo: 'bar', cookie: { expires: new Date(Date.now() + 100) } };
|
||||
await set(sess);
|
||||
assert.equal(JSON.stringify(await get()), JSON.stringify(sess));
|
||||
const sess2 = { ...sess, cookie: { expires: new Date(Date.now() + 200) } };
|
||||
await db.set(`sessionstorage:${sid}`, sess2);
|
||||
assert.equal(JSON.stringify(await get()), JSON.stringify(sess2));
|
||||
await new Promise((resolve) => setTimeout(resolve, 110));
|
||||
// The original timeout should not have fired.
|
||||
assert.equal(JSON.stringify(await get()), JSON.stringify(sess2));
|
||||
});
|
||||
});
|
||||
|
||||
it('touch after eligible for refresh updates db', async function () {
|
||||
const start = Date.now();
|
||||
const sess = {foo: 'bar', cookie: {expires: new Date(start + 200)}};
|
||||
await set(sess);
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
const sess2 = {foo: 'bar', cookie: {expires: new Date(start + 400)}};
|
||||
await touch(sess2);
|
||||
await new Promise((resolve) => setTimeout(resolve, 110));
|
||||
assert.equal(JSON.stringify(await db.get(`sessionstorage:${sid}`)), JSON.stringify(sess2));
|
||||
assert.equal(JSON.stringify(await get()), JSON.stringify(sess2));
|
||||
describe('shutdown', function () {
|
||||
it('shutdown cancels timeouts', async function () {
|
||||
const sess = { foo: 'bar', cookie: { expires: new Date(Date.now() + 100) } };
|
||||
await set(sess);
|
||||
assert.equal(JSON.stringify(await get()), JSON.stringify(sess));
|
||||
ss.shutdown();
|
||||
await new Promise((resolve) => setTimeout(resolve, 110));
|
||||
// The record should not have been automatically purged.
|
||||
assert.equal(JSON.stringify(await db.get(`sessionstorage:${sid}`)), JSON.stringify(sess));
|
||||
});
|
||||
});
|
||||
|
||||
it('refresh=0 updates db every time', async function () {
|
||||
ss = new SessionStore(0);
|
||||
const sess = {foo: 'bar', cookie: {expires: new Date(Date.now() + 1000)}};
|
||||
await set(sess);
|
||||
await db.remove(`sessionstorage:${sid}`);
|
||||
await touch(sess); // No change in expiration time.
|
||||
assert.equal(JSON.stringify(await db.get(`sessionstorage:${sid}`)), JSON.stringify(sess));
|
||||
await db.remove(`sessionstorage:${sid}`);
|
||||
await touch(sess); // No change in expiration time.
|
||||
assert.equal(JSON.stringify(await db.get(`sessionstorage:${sid}`)), JSON.stringify(sess));
|
||||
describe('destroy', function () {
|
||||
it('destroy deletes the database record', async function () {
|
||||
const sess = { cookie: { expires: new Date(Date.now() + 100) } };
|
||||
await set(sess);
|
||||
await destroy();
|
||||
assert(await db.get(`sessionstorage:${sid}`) == null);
|
||||
});
|
||||
it('destroy cancels the timeout', async function () {
|
||||
const sess = { cookie: { expires: new Date(Date.now() + 100) } };
|
||||
await set(sess);
|
||||
await destroy();
|
||||
await db.set(`sessionstorage:${sid}`, sess);
|
||||
await new Promise((resolve) => setTimeout(resolve, 110));
|
||||
assert.equal(JSON.stringify(await db.get(`sessionstorage:${sid}`)), JSON.stringify(sess));
|
||||
});
|
||||
it('destroy session that does not exist', async function () {
|
||||
await destroy();
|
||||
});
|
||||
});
|
||||
describe('touch without refresh', function () {
|
||||
it('touch before set is equivalent to set if session expires', async function () {
|
||||
const sess = { cookie: { expires: new Date(Date.now() + 1000) } };
|
||||
await touch(sess);
|
||||
assert.equal(JSON.stringify(await get()), JSON.stringify(sess));
|
||||
});
|
||||
it('touch updates observed expiration but not database', async function () {
|
||||
const start = Date.now();
|
||||
const sess = { cookie: { expires: new Date(start + 200) } };
|
||||
await set(sess);
|
||||
const sess2 = { cookie: { expires: new Date(start + 12000) } };
|
||||
await touch(sess2);
|
||||
assert.equal(JSON.stringify(await db.get(`sessionstorage:${sid}`)), JSON.stringify(sess));
|
||||
assert.equal(JSON.stringify(await get()), JSON.stringify(sess2));
|
||||
});
|
||||
});
|
||||
describe('touch with refresh', function () {
|
||||
beforeEach(async function () {
|
||||
ss = new SessionStore(200);
|
||||
});
|
||||
it('touch before set is equivalent to set if session expires', async function () {
|
||||
const sess = { cookie: { expires: new Date(Date.now() + 1000) } };
|
||||
await touch(sess);
|
||||
assert.equal(JSON.stringify(await get()), JSON.stringify(sess));
|
||||
});
|
||||
it('touch before eligible for refresh updates expiration but not DB', async function () {
|
||||
const now = Date.now();
|
||||
const sess = { foo: 'bar', cookie: { expires: new Date(now + 1000) } };
|
||||
await set(sess);
|
||||
const sess2 = { foo: 'bar', cookie: { expires: new Date(now + 1001) } };
|
||||
await touch(sess2);
|
||||
assert.equal(JSON.stringify(await db.get(`sessionstorage:${sid}`)), JSON.stringify(sess));
|
||||
assert.equal(JSON.stringify(await get()), JSON.stringify(sess2));
|
||||
});
|
||||
it('touch before eligible for refresh updates timeout', async function () {
|
||||
const start = Date.now();
|
||||
const sess = { foo: 'bar', cookie: { expires: new Date(start + 200) } };
|
||||
await set(sess);
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
const sess2 = { foo: 'bar', cookie: { expires: new Date(start + 399) } };
|
||||
await touch(sess2);
|
||||
await new Promise((resolve) => setTimeout(resolve, 110));
|
||||
assert.equal(JSON.stringify(await db.get(`sessionstorage:${sid}`)), JSON.stringify(sess));
|
||||
assert.equal(JSON.stringify(await get()), JSON.stringify(sess2));
|
||||
});
|
||||
it('touch after eligible for refresh updates db', async function () {
|
||||
const start = Date.now();
|
||||
const sess = { foo: 'bar', cookie: { expires: new Date(start + 200) } };
|
||||
await set(sess);
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
const sess2 = { foo: 'bar', cookie: { expires: new Date(start + 400) } };
|
||||
await touch(sess2);
|
||||
await new Promise((resolve) => setTimeout(resolve, 110));
|
||||
assert.equal(JSON.stringify(await db.get(`sessionstorage:${sid}`)), JSON.stringify(sess2));
|
||||
assert.equal(JSON.stringify(await get()), JSON.stringify(sess2));
|
||||
});
|
||||
it('refresh=0 updates db every time', async function () {
|
||||
ss = new SessionStore(0);
|
||||
const sess = { foo: 'bar', cookie: { expires: new Date(Date.now() + 1000) } };
|
||||
await set(sess);
|
||||
await db.remove(`sessionstorage:${sid}`);
|
||||
await touch(sess); // No change in expiration time.
|
||||
assert.equal(JSON.stringify(await db.get(`sessionstorage:${sid}`)), JSON.stringify(sess));
|
||||
await db.remove(`sessionstorage:${sid}`);
|
||||
await touch(sess); // No change in expiration time.
|
||||
assert.equal(JSON.stringify(await db.get(`sessionstorage:${sid}`)), JSON.stringify(sess));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,358 +1,330 @@
|
|||
import * as Stream from "../../../node/utils/Stream.js";
|
||||
import assert$0 from "assert";
|
||||
'use strict';
|
||||
|
||||
const Stream = require('../../../node/utils/Stream');
|
||||
const assert = require('assert').strict;
|
||||
|
||||
const assert = assert$0.strict;
|
||||
class DemoIterable {
|
||||
constructor() {
|
||||
this.value = 0;
|
||||
this.errs = [];
|
||||
this.rets = [];
|
||||
}
|
||||
|
||||
completed() { return this.errs.length > 0 || this.rets.length > 0; }
|
||||
|
||||
next() {
|
||||
if (this.completed()) return {value: undefined, done: true}; // Mimic standard generators.
|
||||
return {value: this.value++, done: false};
|
||||
}
|
||||
|
||||
throw(err) {
|
||||
const alreadyCompleted = this.completed();
|
||||
this.errs.push(err);
|
||||
if (alreadyCompleted) throw err; // Mimic standard generator objects.
|
||||
throw err;
|
||||
}
|
||||
|
||||
return(ret) {
|
||||
const alreadyCompleted = this.completed();
|
||||
this.rets.push(ret);
|
||||
if (alreadyCompleted) return {value: ret, done: true}; // Mimic standard generator objects.
|
||||
return {value: ret, done: true};
|
||||
}
|
||||
|
||||
[Symbol.iterator]() { return this; }
|
||||
constructor() {
|
||||
this.value = 0;
|
||||
this.errs = [];
|
||||
this.rets = [];
|
||||
}
|
||||
completed() { return this.errs.length > 0 || this.rets.length > 0; }
|
||||
next() {
|
||||
if (this.completed())
|
||||
return { value: undefined, done: true }; // Mimic standard generators.
|
||||
return { value: this.value++, done: false };
|
||||
}
|
||||
throw(err) {
|
||||
const alreadyCompleted = this.completed();
|
||||
this.errs.push(err);
|
||||
if (alreadyCompleted)
|
||||
throw err; // Mimic standard generator objects.
|
||||
throw err;
|
||||
}
|
||||
return(ret) {
|
||||
const alreadyCompleted = this.completed();
|
||||
this.rets.push(ret);
|
||||
if (alreadyCompleted)
|
||||
return { value: ret, done: true }; // Mimic standard generator objects.
|
||||
return { value: ret, done: true };
|
||||
}
|
||||
[Symbol.iterator]() { return this; }
|
||||
}
|
||||
|
||||
const assertUnhandledRejection = async (action, want) => {
|
||||
// Temporarily remove unhandled Promise rejection listeners so that the unhandled rejections we
|
||||
// expect to see don't trigger a test failure (or terminate node).
|
||||
const event = 'unhandledRejection';
|
||||
const listenersBackup = process.rawListeners(event);
|
||||
process.removeAllListeners(event);
|
||||
let tempListener;
|
||||
let asyncErr;
|
||||
try {
|
||||
const seenErrPromise = new Promise((resolve) => {
|
||||
tempListener = (err) => {
|
||||
assert.equal(asyncErr, undefined);
|
||||
asyncErr = err;
|
||||
resolve();
|
||||
};
|
||||
});
|
||||
process.on(event, tempListener);
|
||||
await action();
|
||||
await seenErrPromise;
|
||||
} finally {
|
||||
// Restore the original listeners.
|
||||
process.off(event, tempListener);
|
||||
for (const listener of listenersBackup) process.on(event, listener);
|
||||
}
|
||||
await assert.rejects(Promise.reject(asyncErr), want);
|
||||
// Temporarily remove unhandled Promise rejection listeners so that the unhandled rejections we
|
||||
// expect to see don't trigger a test failure (or terminate node).
|
||||
const event = 'unhandledRejection';
|
||||
const listenersBackup = process.rawListeners(event);
|
||||
process.removeAllListeners(event);
|
||||
let tempListener;
|
||||
let asyncErr;
|
||||
try {
|
||||
const seenErrPromise = new Promise((resolve) => {
|
||||
tempListener = (err) => {
|
||||
assert.equal(asyncErr, undefined);
|
||||
asyncErr = err;
|
||||
resolve();
|
||||
};
|
||||
});
|
||||
process.on(event, tempListener);
|
||||
await action();
|
||||
await seenErrPromise;
|
||||
}
|
||||
finally {
|
||||
// Restore the original listeners.
|
||||
process.off(event, tempListener);
|
||||
for (const listener of listenersBackup)
|
||||
process.on(event, listener);
|
||||
}
|
||||
await assert.rejects(Promise.reject(asyncErr), want);
|
||||
};
|
||||
|
||||
describe(__filename, function () {
|
||||
describe('basic behavior', function () {
|
||||
it('takes a generator', async function () {
|
||||
assert.deepEqual([...new Stream((function* () { yield 0; yield 1; yield 2; })())], [0, 1, 2]);
|
||||
describe('basic behavior', function () {
|
||||
it('takes a generator', async function () {
|
||||
assert.deepEqual([...new Stream((function* () { yield 0; yield 1; yield 2; })())], [0, 1, 2]);
|
||||
});
|
||||
it('takes an array', async function () {
|
||||
assert.deepEqual([...new Stream([0, 1, 2])], [0, 1, 2]);
|
||||
});
|
||||
it('takes an iterator', async function () {
|
||||
assert.deepEqual([...new Stream([0, 1, 2][Symbol.iterator]())], [0, 1, 2]);
|
||||
});
|
||||
it('supports empty iterators', async function () {
|
||||
assert.deepEqual([...new Stream([])], []);
|
||||
});
|
||||
it('is resumable', async function () {
|
||||
const s = new Stream((function* () { yield 0; yield 1; yield 2; })());
|
||||
let iter = s[Symbol.iterator]();
|
||||
assert.deepEqual(iter.next(), { value: 0, done: false });
|
||||
iter = s[Symbol.iterator]();
|
||||
assert.deepEqual(iter.next(), { value: 1, done: false });
|
||||
assert.deepEqual([...s], [2]);
|
||||
});
|
||||
it('supports return value', async function () {
|
||||
const s = new Stream((function* () { yield 0; return 1; })());
|
||||
const iter = s[Symbol.iterator]();
|
||||
assert.deepEqual(iter.next(), { value: 0, done: false });
|
||||
assert.deepEqual(iter.next(), { value: 1, done: true });
|
||||
});
|
||||
it('does not start until needed', async function () {
|
||||
let lastYield = null;
|
||||
new Stream((function* () { yield lastYield = 0; })());
|
||||
// Fetching from the underlying iterator should not start until the first value is fetched
|
||||
// from the stream.
|
||||
assert.equal(lastYield, null);
|
||||
});
|
||||
it('throw is propagated', async function () {
|
||||
const underlying = new DemoIterable();
|
||||
const s = new Stream(underlying);
|
||||
const iter = s[Symbol.iterator]();
|
||||
assert.deepEqual(iter.next(), { value: 0, done: false });
|
||||
const err = new Error('injected');
|
||||
assert.throws(() => iter.throw(err), err);
|
||||
assert.equal(underlying.errs[0], err);
|
||||
});
|
||||
it('return is propagated', async function () {
|
||||
const underlying = new DemoIterable();
|
||||
const s = new Stream(underlying);
|
||||
const iter = s[Symbol.iterator]();
|
||||
assert.deepEqual(iter.next(), { value: 0, done: false });
|
||||
assert.deepEqual(iter.return(42), { value: 42, done: true });
|
||||
assert.equal(underlying.rets[0], 42);
|
||||
});
|
||||
});
|
||||
|
||||
it('takes an array', async function () {
|
||||
assert.deepEqual([...new Stream([0, 1, 2])], [0, 1, 2]);
|
||||
describe('range', function () {
|
||||
it('basic', async function () {
|
||||
assert.deepEqual([...Stream.range(0, 3)], [0, 1, 2]);
|
||||
});
|
||||
it('empty', async function () {
|
||||
assert.deepEqual([...Stream.range(0, 0)], []);
|
||||
});
|
||||
it('positive start', async function () {
|
||||
assert.deepEqual([...Stream.range(3, 5)], [3, 4]);
|
||||
});
|
||||
it('negative start', async function () {
|
||||
assert.deepEqual([...Stream.range(-3, 0)], [-3, -2, -1]);
|
||||
});
|
||||
it('end before start', async function () {
|
||||
assert.deepEqual([...Stream.range(3, 0)], []);
|
||||
});
|
||||
});
|
||||
|
||||
it('takes an iterator', async function () {
|
||||
assert.deepEqual([...new Stream([0, 1, 2][Symbol.iterator]())], [0, 1, 2]);
|
||||
describe('batch', function () {
|
||||
it('empty', async function () {
|
||||
assert.deepEqual([...new Stream([]).batch(10)], []);
|
||||
});
|
||||
it('does not start until needed', async function () {
|
||||
let lastYield = null;
|
||||
new Stream((function* () { yield lastYield = 0; })()).batch(10);
|
||||
assert.equal(lastYield, null);
|
||||
});
|
||||
it('fewer than batch size', async function () {
|
||||
let lastYield = null;
|
||||
const values = (function* () {
|
||||
for (let i = 0; i < 5; i++)
|
||||
yield lastYield = i;
|
||||
})();
|
||||
const s = new Stream(values).batch(10);
|
||||
assert.equal(lastYield, null);
|
||||
assert.deepEqual(s[Symbol.iterator]().next(), { value: 0, done: false });
|
||||
assert.equal(lastYield, 4);
|
||||
assert.deepEqual([...s], [1, 2, 3, 4]);
|
||||
assert.equal(lastYield, 4);
|
||||
});
|
||||
it('exactly batch size', async function () {
|
||||
let lastYield = null;
|
||||
const values = (function* () {
|
||||
for (let i = 0; i < 5; i++)
|
||||
yield lastYield = i;
|
||||
})();
|
||||
const s = new Stream(values).batch(5);
|
||||
assert.equal(lastYield, null);
|
||||
assert.deepEqual(s[Symbol.iterator]().next(), { value: 0, done: false });
|
||||
assert.equal(lastYield, 4);
|
||||
assert.deepEqual([...s], [1, 2, 3, 4]);
|
||||
assert.equal(lastYield, 4);
|
||||
});
|
||||
it('multiple batches, last batch is not full', async function () {
|
||||
let lastYield = null;
|
||||
const values = (function* () {
|
||||
for (let i = 0; i < 10; i++)
|
||||
yield lastYield = i;
|
||||
})();
|
||||
const s = new Stream(values).batch(3);
|
||||
assert.equal(lastYield, null);
|
||||
const iter = s[Symbol.iterator]();
|
||||
assert.deepEqual(iter.next(), { value: 0, done: false });
|
||||
assert.equal(lastYield, 2);
|
||||
assert.deepEqual(iter.next(), { value: 1, done: false });
|
||||
assert.deepEqual(iter.next(), { value: 2, done: false });
|
||||
assert.equal(lastYield, 2);
|
||||
assert.deepEqual(iter.next(), { value: 3, done: false });
|
||||
assert.equal(lastYield, 5);
|
||||
assert.deepEqual([...s], [4, 5, 6, 7, 8, 9]);
|
||||
assert.equal(lastYield, 9);
|
||||
});
|
||||
it('batched Promise rejections are suppressed while iterating', async function () {
|
||||
let lastYield = null;
|
||||
const err = new Error('injected');
|
||||
const values = (function* () {
|
||||
lastYield = 'promise of 0';
|
||||
yield new Promise((resolve) => setTimeout(() => resolve(0), 100));
|
||||
lastYield = 'rejected Promise';
|
||||
yield Promise.reject(err);
|
||||
lastYield = 'promise of 2';
|
||||
yield Promise.resolve(2);
|
||||
})();
|
||||
const s = new Stream(values).batch(3);
|
||||
const iter = s[Symbol.iterator]();
|
||||
const nextp = iter.next().value;
|
||||
assert.equal(lastYield, 'promise of 2');
|
||||
assert.equal(await nextp, 0);
|
||||
await assert.rejects(iter.next().value, err);
|
||||
iter.return();
|
||||
});
|
||||
it('batched Promise rejections are unsuppressed when iteration completes', async function () {
|
||||
let lastYield = null;
|
||||
const err = new Error('injected');
|
||||
const values = (function* () {
|
||||
lastYield = 'promise of 0';
|
||||
yield new Promise((resolve) => setTimeout(() => resolve(0), 100));
|
||||
lastYield = 'rejected Promise';
|
||||
yield Promise.reject(err);
|
||||
lastYield = 'promise of 2';
|
||||
yield Promise.resolve(2);
|
||||
})();
|
||||
const s = new Stream(values).batch(3);
|
||||
const iter = s[Symbol.iterator]();
|
||||
assert.equal(await iter.next().value, 0);
|
||||
assert.equal(lastYield, 'promise of 2');
|
||||
await assertUnhandledRejection(() => iter.return(), err);
|
||||
});
|
||||
});
|
||||
|
||||
it('supports empty iterators', async function () {
|
||||
assert.deepEqual([...new Stream([])], []);
|
||||
describe('buffer', function () {
|
||||
it('empty', async function () {
|
||||
assert.deepEqual([...new Stream([]).buffer(10)], []);
|
||||
});
|
||||
it('does not start until needed', async function () {
|
||||
let lastYield = null;
|
||||
new Stream((function* () { yield lastYield = 0; })()).buffer(10);
|
||||
assert.equal(lastYield, null);
|
||||
});
|
||||
it('fewer than buffer size', async function () {
|
||||
let lastYield = null;
|
||||
const values = (function* () {
|
||||
for (let i = 0; i < 5; i++)
|
||||
yield lastYield = i;
|
||||
})();
|
||||
const s = new Stream(values).buffer(10);
|
||||
assert.equal(lastYield, null);
|
||||
assert.deepEqual(s[Symbol.iterator]().next(), { value: 0, done: false });
|
||||
assert.equal(lastYield, 4);
|
||||
assert.deepEqual([...s], [1, 2, 3, 4]);
|
||||
assert.equal(lastYield, 4);
|
||||
});
|
||||
it('exactly buffer size', async function () {
|
||||
let lastYield = null;
|
||||
const values = (function* () {
|
||||
for (let i = 0; i < 5; i++)
|
||||
yield lastYield = i;
|
||||
})();
|
||||
const s = new Stream(values).buffer(5);
|
||||
assert.equal(lastYield, null);
|
||||
assert.deepEqual(s[Symbol.iterator]().next(), { value: 0, done: false });
|
||||
assert.equal(lastYield, 4);
|
||||
assert.deepEqual([...s], [1, 2, 3, 4]);
|
||||
assert.equal(lastYield, 4);
|
||||
});
|
||||
it('more than buffer size', async function () {
|
||||
let lastYield = null;
|
||||
const values = (function* () {
|
||||
for (let i = 0; i < 10; i++)
|
||||
yield lastYield = i;
|
||||
})();
|
||||
const s = new Stream(values).buffer(3);
|
||||
assert.equal(lastYield, null);
|
||||
const iter = s[Symbol.iterator]();
|
||||
assert.deepEqual(iter.next(), { value: 0, done: false });
|
||||
assert.equal(lastYield, 3);
|
||||
assert.deepEqual(iter.next(), { value: 1, done: false });
|
||||
assert.equal(lastYield, 4);
|
||||
assert.deepEqual(iter.next(), { value: 2, done: false });
|
||||
assert.equal(lastYield, 5);
|
||||
assert.deepEqual([...s], [3, 4, 5, 6, 7, 8, 9]);
|
||||
assert.equal(lastYield, 9);
|
||||
});
|
||||
it('buffered Promise rejections are suppressed while iterating', async function () {
|
||||
let lastYield = null;
|
||||
const err = new Error('injected');
|
||||
const values = (function* () {
|
||||
lastYield = 'promise of 0';
|
||||
yield new Promise((resolve) => setTimeout(() => resolve(0), 100));
|
||||
lastYield = 'rejected Promise';
|
||||
yield Promise.reject(err);
|
||||
lastYield = 'promise of 2';
|
||||
yield Promise.resolve(2);
|
||||
})();
|
||||
const s = new Stream(values).buffer(3);
|
||||
const iter = s[Symbol.iterator]();
|
||||
const nextp = iter.next().value;
|
||||
assert.equal(lastYield, 'promise of 2');
|
||||
assert.equal(await nextp, 0);
|
||||
await assert.rejects(iter.next().value, err);
|
||||
iter.return();
|
||||
});
|
||||
it('buffered Promise rejections are unsuppressed when iteration completes', async function () {
|
||||
let lastYield = null;
|
||||
const err = new Error('injected');
|
||||
const values = (function* () {
|
||||
lastYield = 'promise of 0';
|
||||
yield new Promise((resolve) => setTimeout(() => resolve(0), 100));
|
||||
lastYield = 'rejected Promise';
|
||||
yield Promise.reject(err);
|
||||
lastYield = 'promise of 2';
|
||||
yield Promise.resolve(2);
|
||||
})();
|
||||
const s = new Stream(values).buffer(3);
|
||||
const iter = s[Symbol.iterator]();
|
||||
assert.equal(await iter.next().value, 0);
|
||||
assert.equal(lastYield, 'promise of 2');
|
||||
await assertUnhandledRejection(() => iter.return(), err);
|
||||
});
|
||||
});
|
||||
|
||||
it('is resumable', async function () {
|
||||
const s = new Stream((function* () { yield 0; yield 1; yield 2; })());
|
||||
let iter = s[Symbol.iterator]();
|
||||
assert.deepEqual(iter.next(), {value: 0, done: false});
|
||||
iter = s[Symbol.iterator]();
|
||||
assert.deepEqual(iter.next(), {value: 1, done: false});
|
||||
assert.deepEqual([...s], [2]);
|
||||
describe('map', function () {
|
||||
it('empty', async function () {
|
||||
let called = false;
|
||||
assert.deepEqual([...new Stream([]).map((v) => called = true)], []);
|
||||
assert.equal(called, false);
|
||||
});
|
||||
it('does not start until needed', async function () {
|
||||
let called = false;
|
||||
assert.deepEqual([...new Stream([]).map((v) => called = true)], []);
|
||||
new Stream((function* () { yield 0; })()).map((v) => called = true);
|
||||
assert.equal(called, false);
|
||||
});
|
||||
it('works', async function () {
|
||||
const calls = [];
|
||||
assert.deepEqual([...new Stream([0, 1, 2]).map((v) => { calls.push(v); return 2 * v; })], [0, 2, 4]);
|
||||
assert.deepEqual(calls, [0, 1, 2]);
|
||||
});
|
||||
});
|
||||
|
||||
it('supports return value', async function () {
|
||||
const s = new Stream((function* () { yield 0; return 1; })());
|
||||
const iter = s[Symbol.iterator]();
|
||||
assert.deepEqual(iter.next(), {value: 0, done: false});
|
||||
assert.deepEqual(iter.next(), {value: 1, done: true});
|
||||
});
|
||||
|
||||
it('does not start until needed', async function () {
|
||||
let lastYield = null;
|
||||
new Stream((function* () { yield lastYield = 0; })());
|
||||
// Fetching from the underlying iterator should not start until the first value is fetched
|
||||
// from the stream.
|
||||
assert.equal(lastYield, null);
|
||||
});
|
||||
|
||||
it('throw is propagated', async function () {
|
||||
const underlying = new DemoIterable();
|
||||
const s = new Stream(underlying);
|
||||
const iter = s[Symbol.iterator]();
|
||||
assert.deepEqual(iter.next(), {value: 0, done: false});
|
||||
const err = new Error('injected');
|
||||
assert.throws(() => iter.throw(err), err);
|
||||
assert.equal(underlying.errs[0], err);
|
||||
});
|
||||
|
||||
it('return is propagated', async function () {
|
||||
const underlying = new DemoIterable();
|
||||
const s = new Stream(underlying);
|
||||
const iter = s[Symbol.iterator]();
|
||||
assert.deepEqual(iter.next(), {value: 0, done: false});
|
||||
assert.deepEqual(iter.return(42), {value: 42, done: true});
|
||||
assert.equal(underlying.rets[0], 42);
|
||||
});
|
||||
});
|
||||
|
||||
describe('range', function () {
|
||||
it('basic', async function () {
|
||||
assert.deepEqual([...Stream.range(0, 3)], [0, 1, 2]);
|
||||
});
|
||||
|
||||
it('empty', async function () {
|
||||
assert.deepEqual([...Stream.range(0, 0)], []);
|
||||
});
|
||||
|
||||
it('positive start', async function () {
|
||||
assert.deepEqual([...Stream.range(3, 5)], [3, 4]);
|
||||
});
|
||||
|
||||
it('negative start', async function () {
|
||||
assert.deepEqual([...Stream.range(-3, 0)], [-3, -2, -1]);
|
||||
});
|
||||
|
||||
it('end before start', async function () {
|
||||
assert.deepEqual([...Stream.range(3, 0)], []);
|
||||
});
|
||||
});
|
||||
|
||||
describe('batch', function () {
|
||||
it('empty', async function () {
|
||||
assert.deepEqual([...new Stream([]).batch(10)], []);
|
||||
});
|
||||
|
||||
it('does not start until needed', async function () {
|
||||
let lastYield = null;
|
||||
new Stream((function* () { yield lastYield = 0; })()).batch(10);
|
||||
assert.equal(lastYield, null);
|
||||
});
|
||||
|
||||
it('fewer than batch size', async function () {
|
||||
let lastYield = null;
|
||||
const values = (function* () {
|
||||
for (let i = 0; i < 5; i++) yield lastYield = i;
|
||||
})();
|
||||
const s = new Stream(values).batch(10);
|
||||
assert.equal(lastYield, null);
|
||||
assert.deepEqual(s[Symbol.iterator]().next(), {value: 0, done: false});
|
||||
assert.equal(lastYield, 4);
|
||||
assert.deepEqual([...s], [1, 2, 3, 4]);
|
||||
assert.equal(lastYield, 4);
|
||||
});
|
||||
|
||||
it('exactly batch size', async function () {
|
||||
let lastYield = null;
|
||||
const values = (function* () {
|
||||
for (let i = 0; i < 5; i++) yield lastYield = i;
|
||||
})();
|
||||
const s = new Stream(values).batch(5);
|
||||
assert.equal(lastYield, null);
|
||||
assert.deepEqual(s[Symbol.iterator]().next(), {value: 0, done: false});
|
||||
assert.equal(lastYield, 4);
|
||||
assert.deepEqual([...s], [1, 2, 3, 4]);
|
||||
assert.equal(lastYield, 4);
|
||||
});
|
||||
|
||||
it('multiple batches, last batch is not full', async function () {
|
||||
let lastYield = null;
|
||||
const values = (function* () {
|
||||
for (let i = 0; i < 10; i++) yield lastYield = i;
|
||||
})();
|
||||
const s = new Stream(values).batch(3);
|
||||
assert.equal(lastYield, null);
|
||||
const iter = s[Symbol.iterator]();
|
||||
assert.deepEqual(iter.next(), {value: 0, done: false});
|
||||
assert.equal(lastYield, 2);
|
||||
assert.deepEqual(iter.next(), {value: 1, done: false});
|
||||
assert.deepEqual(iter.next(), {value: 2, done: false});
|
||||
assert.equal(lastYield, 2);
|
||||
assert.deepEqual(iter.next(), {value: 3, done: false});
|
||||
assert.equal(lastYield, 5);
|
||||
assert.deepEqual([...s], [4, 5, 6, 7, 8, 9]);
|
||||
assert.equal(lastYield, 9);
|
||||
});
|
||||
|
||||
it('batched Promise rejections are suppressed while iterating', async function () {
|
||||
let lastYield = null;
|
||||
const err = new Error('injected');
|
||||
const values = (function* () {
|
||||
lastYield = 'promise of 0';
|
||||
yield new Promise((resolve) => setTimeout(() => resolve(0), 100));
|
||||
lastYield = 'rejected Promise';
|
||||
yield Promise.reject(err);
|
||||
lastYield = 'promise of 2';
|
||||
yield Promise.resolve(2);
|
||||
})();
|
||||
const s = new Stream(values).batch(3);
|
||||
const iter = s[Symbol.iterator]();
|
||||
const nextp = iter.next().value;
|
||||
assert.equal(lastYield, 'promise of 2');
|
||||
assert.equal(await nextp, 0);
|
||||
await assert.rejects(iter.next().value, err);
|
||||
iter.return();
|
||||
});
|
||||
|
||||
it('batched Promise rejections are unsuppressed when iteration completes', async function () {
|
||||
let lastYield = null;
|
||||
const err = new Error('injected');
|
||||
const values = (function* () {
|
||||
lastYield = 'promise of 0';
|
||||
yield new Promise((resolve) => setTimeout(() => resolve(0), 100));
|
||||
lastYield = 'rejected Promise';
|
||||
yield Promise.reject(err);
|
||||
lastYield = 'promise of 2';
|
||||
yield Promise.resolve(2);
|
||||
})();
|
||||
const s = new Stream(values).batch(3);
|
||||
const iter = s[Symbol.iterator]();
|
||||
assert.equal(await iter.next().value, 0);
|
||||
assert.equal(lastYield, 'promise of 2');
|
||||
await assertUnhandledRejection(() => iter.return(), err);
|
||||
});
|
||||
});
|
||||
|
||||
describe('buffer', function () {
|
||||
it('empty', async function () {
|
||||
assert.deepEqual([...new Stream([]).buffer(10)], []);
|
||||
});
|
||||
|
||||
it('does not start until needed', async function () {
|
||||
let lastYield = null;
|
||||
new Stream((function* () { yield lastYield = 0; })()).buffer(10);
|
||||
assert.equal(lastYield, null);
|
||||
});
|
||||
|
||||
it('fewer than buffer size', async function () {
|
||||
let lastYield = null;
|
||||
const values = (function* () {
|
||||
for (let i = 0; i < 5; i++) yield lastYield = i;
|
||||
})();
|
||||
const s = new Stream(values).buffer(10);
|
||||
assert.equal(lastYield, null);
|
||||
assert.deepEqual(s[Symbol.iterator]().next(), {value: 0, done: false});
|
||||
assert.equal(lastYield, 4);
|
||||
assert.deepEqual([...s], [1, 2, 3, 4]);
|
||||
assert.equal(lastYield, 4);
|
||||
});
|
||||
|
||||
it('exactly buffer size', async function () {
|
||||
let lastYield = null;
|
||||
const values = (function* () {
|
||||
for (let i = 0; i < 5; i++) yield lastYield = i;
|
||||
})();
|
||||
const s = new Stream(values).buffer(5);
|
||||
assert.equal(lastYield, null);
|
||||
assert.deepEqual(s[Symbol.iterator]().next(), {value: 0, done: false});
|
||||
assert.equal(lastYield, 4);
|
||||
assert.deepEqual([...s], [1, 2, 3, 4]);
|
||||
assert.equal(lastYield, 4);
|
||||
});
|
||||
|
||||
it('more than buffer size', async function () {
|
||||
let lastYield = null;
|
||||
const values = (function* () {
|
||||
for (let i = 0; i < 10; i++) yield lastYield = i;
|
||||
})();
|
||||
const s = new Stream(values).buffer(3);
|
||||
assert.equal(lastYield, null);
|
||||
const iter = s[Symbol.iterator]();
|
||||
assert.deepEqual(iter.next(), {value: 0, done: false});
|
||||
assert.equal(lastYield, 3);
|
||||
assert.deepEqual(iter.next(), {value: 1, done: false});
|
||||
assert.equal(lastYield, 4);
|
||||
assert.deepEqual(iter.next(), {value: 2, done: false});
|
||||
assert.equal(lastYield, 5);
|
||||
assert.deepEqual([...s], [3, 4, 5, 6, 7, 8, 9]);
|
||||
assert.equal(lastYield, 9);
|
||||
});
|
||||
|
||||
it('buffered Promise rejections are suppressed while iterating', async function () {
|
||||
let lastYield = null;
|
||||
const err = new Error('injected');
|
||||
const values = (function* () {
|
||||
lastYield = 'promise of 0';
|
||||
yield new Promise((resolve) => setTimeout(() => resolve(0), 100));
|
||||
lastYield = 'rejected Promise';
|
||||
yield Promise.reject(err);
|
||||
lastYield = 'promise of 2';
|
||||
yield Promise.resolve(2);
|
||||
})();
|
||||
const s = new Stream(values).buffer(3);
|
||||
const iter = s[Symbol.iterator]();
|
||||
const nextp = iter.next().value;
|
||||
assert.equal(lastYield, 'promise of 2');
|
||||
assert.equal(await nextp, 0);
|
||||
await assert.rejects(iter.next().value, err);
|
||||
iter.return();
|
||||
});
|
||||
|
||||
it('buffered Promise rejections are unsuppressed when iteration completes', async function () {
|
||||
let lastYield = null;
|
||||
const err = new Error('injected');
|
||||
const values = (function* () {
|
||||
lastYield = 'promise of 0';
|
||||
yield new Promise((resolve) => setTimeout(() => resolve(0), 100));
|
||||
lastYield = 'rejected Promise';
|
||||
yield Promise.reject(err);
|
||||
lastYield = 'promise of 2';
|
||||
yield Promise.resolve(2);
|
||||
})();
|
||||
const s = new Stream(values).buffer(3);
|
||||
const iter = s[Symbol.iterator]();
|
||||
assert.equal(await iter.next().value, 0);
|
||||
assert.equal(lastYield, 'promise of 2');
|
||||
await assertUnhandledRejection(() => iter.return(), err);
|
||||
});
|
||||
});
|
||||
|
||||
describe('map', function () {
|
||||
it('empty', async function () {
|
||||
let called = false;
|
||||
assert.deepEqual([...new Stream([]).map((v) => called = true)], []);
|
||||
assert.equal(called, false);
|
||||
});
|
||||
|
||||
it('does not start until needed', async function () {
|
||||
let called = false;
|
||||
assert.deepEqual([...new Stream([]).map((v) => called = true)], []);
|
||||
new Stream((function* () { yield 0; })()).map((v) => called = true);
|
||||
assert.equal(called, false);
|
||||
});
|
||||
|
||||
it('works', async function () {
|
||||
const calls = [];
|
||||
assert.deepEqual(
|
||||
[...new Stream([0, 1, 2]).map((v) => { calls.push(v); return 2 * v; })], [0, 2, 4]);
|
||||
assert.deepEqual(calls, [0, 1, 2]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,58 +1,43 @@
|
|||
import * as common from "../../common.js";
|
||||
import { validate } from "openapi-schema-validation";
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* API specs
|
||||
*
|
||||
* Tests for generic overarching HTTP API related features not related to any
|
||||
* specific part of the data model or domain. For example: tests for versioning
|
||||
* and openapi definitions.
|
||||
*/
|
||||
|
||||
const common = require('../../common');
|
||||
const validateOpenAPI = require('openapi-schema-validation').validate;
|
||||
|
||||
const validateOpenAPI = { validate }.validate;
|
||||
let agent;
|
||||
const apiKey = common.apiKey;
|
||||
let apiVersion = 1;
|
||||
|
||||
const makeid = () => {
|
||||
let text = '';
|
||||
const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
|
||||
for (let i = 0; i < 5; i++) {
|
||||
text += possible.charAt(Math.floor(Math.random() * possible.length));
|
||||
}
|
||||
return text;
|
||||
let text = '';
|
||||
const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
for (let i = 0; i < 5; i++) {
|
||||
text += possible.charAt(Math.floor(Math.random() * possible.length));
|
||||
}
|
||||
return text;
|
||||
};
|
||||
|
||||
const testPadId = makeid();
|
||||
|
||||
const endPoint = (point) => `/api/${apiVersion}/${point}?apikey=${apiKey}`;
|
||||
|
||||
describe(__filename, function () {
|
||||
before(async function () { agent = await common.init(); });
|
||||
|
||||
it('can obtain API version', async function () {
|
||||
await agent.get('/api/')
|
||||
.expect(200)
|
||||
.expect((res) => {
|
||||
apiVersion = res.body.currentVersion;
|
||||
if (!res.body.currentVersion) throw new Error('No version set in API');
|
||||
return;
|
||||
before(async function () { agent = await common.init(); });
|
||||
it('can obtain API version', async function () {
|
||||
await agent.get('/api/')
|
||||
.expect(200)
|
||||
.expect((res) => {
|
||||
apiVersion = res.body.currentVersion;
|
||||
if (!res.body.currentVersion)
|
||||
throw new Error('No version set in API');
|
||||
return;
|
||||
});
|
||||
});
|
||||
|
||||
it('can obtain valid openapi definition document', async function () {
|
||||
this.timeout(15000);
|
||||
await agent.get('/api/openapi.json')
|
||||
.expect(200)
|
||||
.expect((res) => {
|
||||
const {valid, errors} = validateOpenAPI(res.body, 3);
|
||||
if (!valid) {
|
||||
const prettyErrors = JSON.stringify(errors, null, 2);
|
||||
throw new Error(`Document is not valid OpenAPI. ${errors.length} ` +
|
||||
`validation errors:\n${prettyErrors}`);
|
||||
}
|
||||
});
|
||||
it('can obtain valid openapi definition document', async function () {
|
||||
this.timeout(15000);
|
||||
await agent.get('/api/openapi.json')
|
||||
.expect(200)
|
||||
.expect((res) => {
|
||||
const { valid, errors } = validateOpenAPI(res.body, 3);
|
||||
if (!valid) {
|
||||
const prettyErrors = JSON.stringify(errors, null, 2);
|
||||
throw new Error(`Document is not valid OpenAPI. ${errors.length} ` +
|
||||
`validation errors:\n${prettyErrors}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,88 +1,75 @@
|
|||
import assert$0 from "assert";
|
||||
import * as common from "../../common.js";
|
||||
import fs from "fs";
|
||||
'use strict';
|
||||
|
||||
/*
|
||||
* This file is copied & modified from <basedir>/src/tests/backend/specs/api/pad.js
|
||||
*
|
||||
* TODO: maybe unify those two files and merge in a single one.
|
||||
*/
|
||||
|
||||
const assert = require('assert').strict;
|
||||
const common = require('../../common');
|
||||
const fs = require('fs');
|
||||
const assert = assert$0.strict;
|
||||
const fsp = fs.promises;
|
||||
|
||||
let agent;
|
||||
const apiKey = common.apiKey;
|
||||
let apiVersion = 1;
|
||||
const testPadId = makeid();
|
||||
|
||||
const endPoint = (point, version) => `/api/${version || apiVersion}/${point}?apikey=${apiKey}`;
|
||||
|
||||
describe(__filename, function () {
|
||||
before(async function () { agent = await common.init(); });
|
||||
|
||||
describe('Sanity checks', function () {
|
||||
it('can connect', async function () {
|
||||
await agent.get('/api/')
|
||||
.expect(200)
|
||||
.expect('Content-Type', /json/);
|
||||
before(async function () { agent = await common.init(); });
|
||||
describe('Sanity checks', function () {
|
||||
it('can connect', async function () {
|
||||
await agent.get('/api/')
|
||||
.expect(200)
|
||||
.expect('Content-Type', /json/);
|
||||
});
|
||||
it('finds the version tag', async function () {
|
||||
const res = await agent.get('/api/')
|
||||
.expect(200);
|
||||
apiVersion = res.body.currentVersion;
|
||||
assert(apiVersion);
|
||||
});
|
||||
it('errors with invalid APIKey', async function () {
|
||||
// This is broken because Etherpad doesn't handle HTTP codes properly see #2343
|
||||
// If your APIKey is password you deserve to fail all tests anyway
|
||||
await agent.get(`/api/${apiVersion}/createPad?apikey=password&padID=test`)
|
||||
.expect(401);
|
||||
});
|
||||
});
|
||||
|
||||
it('finds the version tag', async function () {
|
||||
const res = await agent.get('/api/')
|
||||
.expect(200);
|
||||
apiVersion = res.body.currentVersion;
|
||||
assert(apiVersion);
|
||||
describe('Tests', function () {
|
||||
it('creates a new Pad', async function () {
|
||||
const res = await agent.get(`${endPoint('createPad')}&padID=${testPadId}`)
|
||||
.expect(200)
|
||||
.expect('Content-Type', /json/);
|
||||
assert.equal(res.body.code, 0);
|
||||
});
|
||||
it('Sets the HTML of a Pad attempting to weird utf8 encoded content', async function () {
|
||||
const res = await agent.post(endPoint('setHTML'))
|
||||
.send({
|
||||
padID: testPadId,
|
||||
html: await fsp.readFile('tests/backend/specs/api/emojis.html', 'utf8'),
|
||||
})
|
||||
.expect(200)
|
||||
.expect('Content-Type', /json/);
|
||||
assert.equal(res.body.code, 0);
|
||||
});
|
||||
it('get the HTML of Pad with emojis', async function () {
|
||||
const res = await agent.get(`${endPoint('getHTML')}&padID=${testPadId}`)
|
||||
.expect(200)
|
||||
.expect('Content-Type', /json/);
|
||||
assert.match(res.body.data.html, /🇼/);
|
||||
});
|
||||
});
|
||||
|
||||
it('errors with invalid APIKey', async function () {
|
||||
// This is broken because Etherpad doesn't handle HTTP codes properly see #2343
|
||||
// If your APIKey is password you deserve to fail all tests anyway
|
||||
await agent.get(`/api/${apiVersion}/createPad?apikey=password&padID=test`)
|
||||
.expect(401);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Tests', function () {
|
||||
it('creates a new Pad', async function () {
|
||||
const res = await agent.get(`${endPoint('createPad')}&padID=${testPadId}`)
|
||||
.expect(200)
|
||||
.expect('Content-Type', /json/);
|
||||
assert.equal(res.body.code, 0);
|
||||
});
|
||||
|
||||
it('Sets the HTML of a Pad attempting to weird utf8 encoded content', async function () {
|
||||
const res = await agent.post(endPoint('setHTML'))
|
||||
.send({
|
||||
padID: testPadId,
|
||||
html: await fsp.readFile('tests/backend/specs/api/emojis.html', 'utf8'),
|
||||
})
|
||||
.expect(200)
|
||||
.expect('Content-Type', /json/);
|
||||
assert.equal(res.body.code, 0);
|
||||
});
|
||||
|
||||
it('get the HTML of Pad with emojis', async function () {
|
||||
const res = await agent.get(`${endPoint('getHTML')}&padID=${testPadId}`)
|
||||
.expect(200)
|
||||
.expect('Content-Type', /json/);
|
||||
assert.match(res.body.data.html, /🇼/);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/*
|
||||
|
||||
End of test
|
||||
|
||||
*/
|
||||
|
||||
function makeid() {
|
||||
let text = '';
|
||||
const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
|
||||
for (let i = 0; i < 10; i++) {
|
||||
text += possible.charAt(Math.floor(Math.random() * possible.length));
|
||||
}
|
||||
return text;
|
||||
let text = '';
|
||||
const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
for (let i = 0; i < 10; i++) {
|
||||
text += possible.charAt(Math.floor(Math.random() * possible.length));
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
|
|
@ -1,115 +1,105 @@
|
|||
import * as common from "../../common.js";
|
||||
'use strict';
|
||||
|
||||
const common = require('../../common');
|
||||
|
||||
let agent;
|
||||
const apiKey = common.apiKey;
|
||||
let apiVersion = 1;
|
||||
let authorID = '';
|
||||
const padID = makeid();
|
||||
const timestamp = Date.now();
|
||||
|
||||
const endPoint = (point) => `/api/${apiVersion}/${point}?apikey=${apiKey}`;
|
||||
|
||||
describe(__filename, function () {
|
||||
before(async function () { agent = await common.init(); });
|
||||
|
||||
describe('API Versioning', function () {
|
||||
it('errors if can not connect', function (done) {
|
||||
agent.get('/api/')
|
||||
.expect((res) => {
|
||||
apiVersion = res.body.currentVersion;
|
||||
if (!res.body.currentVersion) throw new Error('No version set in API');
|
||||
return;
|
||||
})
|
||||
.expect(200, done);
|
||||
before(async function () { agent = await common.init(); });
|
||||
describe('API Versioning', function () {
|
||||
it('errors if can not connect', function (done) {
|
||||
agent.get('/api/')
|
||||
.expect((res) => {
|
||||
apiVersion = res.body.currentVersion;
|
||||
if (!res.body.currentVersion)
|
||||
throw new Error('No version set in API');
|
||||
return;
|
||||
})
|
||||
.expect(200, done);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// BEGIN GROUP AND AUTHOR TESTS
|
||||
// ///////////////////////////////////
|
||||
// ///////////////////////////////////
|
||||
|
||||
/* Tests performed
|
||||
-> createPad(padID)
|
||||
-> createAuthor([name]) -- should return an authorID
|
||||
-> appendChatMessage(padID, text, authorID, time)
|
||||
-> getChatHead(padID)
|
||||
-> getChatHistory(padID)
|
||||
*/
|
||||
|
||||
describe('createPad', function () {
|
||||
it('creates a new Pad', function (done) {
|
||||
agent.get(`${endPoint('createPad')}&padID=${padID}`)
|
||||
.expect((res) => {
|
||||
if (res.body.code !== 0) throw new Error('Unable to create new Pad');
|
||||
})
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200, done);
|
||||
// BEGIN GROUP AND AUTHOR TESTS
|
||||
// ///////////////////////////////////
|
||||
// ///////////////////////////////////
|
||||
/* Tests performed
|
||||
-> createPad(padID)
|
||||
-> createAuthor([name]) -- should return an authorID
|
||||
-> appendChatMessage(padID, text, authorID, time)
|
||||
-> getChatHead(padID)
|
||||
-> getChatHistory(padID)
|
||||
*/
|
||||
describe('createPad', function () {
|
||||
it('creates a new Pad', function (done) {
|
||||
agent.get(`${endPoint('createPad')}&padID=${padID}`)
|
||||
.expect((res) => {
|
||||
if (res.body.code !== 0)
|
||||
throw new Error('Unable to create new Pad');
|
||||
})
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200, done);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('createAuthor', function () {
|
||||
it('Creates an author with a name set', function (done) {
|
||||
agent.get(endPoint('createAuthor'))
|
||||
.expect((res) => {
|
||||
if (res.body.code !== 0 || !res.body.data.authorID) {
|
||||
throw new Error('Unable to create author');
|
||||
}
|
||||
authorID = res.body.data.authorID; // we will be this author for the rest of the tests
|
||||
})
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200, done);
|
||||
describe('createAuthor', function () {
|
||||
it('Creates an author with a name set', function (done) {
|
||||
agent.get(endPoint('createAuthor'))
|
||||
.expect((res) => {
|
||||
if (res.body.code !== 0 || !res.body.data.authorID) {
|
||||
throw new Error('Unable to create author');
|
||||
}
|
||||
authorID = res.body.data.authorID; // we will be this author for the rest of the tests
|
||||
})
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200, done);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('appendChatMessage', function () {
|
||||
it('Adds a chat message to the pad', function (done) {
|
||||
agent.get(`${endPoint('appendChatMessage')}&padID=${padID}&text=blalblalbha` +
|
||||
describe('appendChatMessage', function () {
|
||||
it('Adds a chat message to the pad', function (done) {
|
||||
agent.get(`${endPoint('appendChatMessage')}&padID=${padID}&text=blalblalbha` +
|
||||
`&authorID=${authorID}&time=${timestamp}`)
|
||||
.expect((res) => {
|
||||
if (res.body.code !== 0) throw new Error('Unable to create chat message');
|
||||
})
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200, done);
|
||||
.expect((res) => {
|
||||
if (res.body.code !== 0)
|
||||
throw new Error('Unable to create chat message');
|
||||
})
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200, done);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('getChatHead', function () {
|
||||
it('Gets the head of chat', function (done) {
|
||||
agent.get(`${endPoint('getChatHead')}&padID=${padID}`)
|
||||
.expect((res) => {
|
||||
if (res.body.data.chatHead !== 0) throw new Error('Chat Head Length is wrong');
|
||||
|
||||
if (res.body.code !== 0) throw new Error('Unable to get chat head');
|
||||
})
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200, done);
|
||||
describe('getChatHead', function () {
|
||||
it('Gets the head of chat', function (done) {
|
||||
agent.get(`${endPoint('getChatHead')}&padID=${padID}`)
|
||||
.expect((res) => {
|
||||
if (res.body.data.chatHead !== 0)
|
||||
throw new Error('Chat Head Length is wrong');
|
||||
if (res.body.code !== 0)
|
||||
throw new Error('Unable to get chat head');
|
||||
})
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200, done);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getChatHistory', function () {
|
||||
it('Gets Chat History of a Pad', function (done) {
|
||||
agent.get(`${endPoint('getChatHistory')}&padID=${padID}`)
|
||||
.expect((res) => {
|
||||
if (res.body.data.messages.length !== 1) {
|
||||
throw new Error('Chat History Length is wrong');
|
||||
}
|
||||
if (res.body.code !== 0) throw new Error('Unable to get chat history');
|
||||
})
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200, done);
|
||||
describe('getChatHistory', function () {
|
||||
it('Gets Chat History of a Pad', function (done) {
|
||||
agent.get(`${endPoint('getChatHistory')}&padID=${padID}`)
|
||||
.expect((res) => {
|
||||
if (res.body.data.messages.length !== 1) {
|
||||
throw new Error('Chat History Length is wrong');
|
||||
}
|
||||
if (res.body.code !== 0)
|
||||
throw new Error('Unable to get chat history');
|
||||
})
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200, done);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function makeid() {
|
||||
let text = '';
|
||||
const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
|
||||
for (let i = 0; i < 5; i++) {
|
||||
text += possible.charAt(Math.floor(Math.random() * possible.length));
|
||||
}
|
||||
return text;
|
||||
let text = '';
|
||||
const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
for (let i = 0; i < 5; i++) {
|
||||
text += possible.charAt(Math.floor(Math.random() * possible.length));
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import assert$0 from "assert";
|
||||
import * as common from "../../common.js";
|
||||
'use strict';
|
||||
/*
|
||||
* ACHTUNG: there is a copied & modified version of this file in
|
||||
|
@ -5,181 +7,175 @@
|
|||
*
|
||||
* TODO: unify those two files, and merge in a single one.
|
||||
*/
|
||||
|
||||
const assert = require('assert').strict;
|
||||
const common = require('../../common');
|
||||
|
||||
const assert = assert$0.strict;
|
||||
let agent;
|
||||
const apiKey = common.apiKey;
|
||||
const apiVersion = 1;
|
||||
|
||||
const endPoint = (point, version) => `/api/${version || apiVersion}/${point}?apikey=${apiKey}`;
|
||||
|
||||
const testImports = {
|
||||
'malformed': {
|
||||
input: '<html><body><li>wtf</ul></body></html>',
|
||||
wantHTML: '<!DOCTYPE HTML><html><body>wtf<br><br></body></html>',
|
||||
wantText: 'wtf\n\n',
|
||||
disabled: true,
|
||||
},
|
||||
'nonelistiteminlist #3620': {
|
||||
input: '<html><body><ul>test<li>FOO</li></ul></body></html>',
|
||||
wantHTML: '<!DOCTYPE HTML><html><body><ul class="bullet">test<li>FOO</ul><br></body></html>',
|
||||
wantText: '\ttest\n\t* FOO\n\n',
|
||||
disabled: true,
|
||||
},
|
||||
'whitespaceinlist #3620': {
|
||||
input: '<html><body><ul> <li>FOO</li></ul></body></html>',
|
||||
wantHTML: '<!DOCTYPE HTML><html><body><ul class="bullet"><li>FOO</ul><br></body></html>',
|
||||
wantText: '\t* FOO\n\n',
|
||||
},
|
||||
'prefixcorrectlinenumber': {
|
||||
input: '<html><body><ol><li>should be 1</li><li>should be 2</li></ol></body></html>',
|
||||
wantHTML: '<!DOCTYPE HTML><html><body><ol start="1" class="number"><li>should be 1</li><li>should be 2</ol><br></body></html>',
|
||||
wantText: '\t1. should be 1\n\t2. should be 2\n\n',
|
||||
},
|
||||
'prefixcorrectlinenumbernested': {
|
||||
input: '<html><body><ol><li>should be 1</li><ol><li>foo</li></ol><li>should be 2</li></ol></body></html>',
|
||||
wantHTML: '<!DOCTYPE HTML><html><body><ol start="1" class="number"><li>should be 1<ol start="2" class="number"><li>foo</ol><li>should be 2</ol><br></body></html>',
|
||||
wantText: '\t1. should be 1\n\t\t1.1. foo\n\t2. should be 2\n\n',
|
||||
},
|
||||
|
||||
/*
|
||||
"prefixcorrectlinenumber when introduced none list item - currently not supported see #3450": {
|
||||
input: '<html><body><ol><li>should be 1</li>test<li>should be 2</li></ol></body></html>',
|
||||
wantHTML: '<!DOCTYPE HTML><html><body><ol start="1" class="number"><li>should be 1</li>test<li>should be 2</li></ol><br></body></html>',
|
||||
wantText: '\t1. should be 1\n\ttest\n\t2. should be 2\n\n',
|
||||
}
|
||||
,
|
||||
"newlinesshouldntresetlinenumber #2194": {
|
||||
input: '<html><body><ol><li>should be 1</li>test<li>should be 2</li></ol></body></html>',
|
||||
wantHTML: '<!DOCTYPE HTML><html><body><ol class="number"><li>should be 1</li>test<li>should be 2</li></ol><br></body></html>',
|
||||
wantText: '\t1. should be 1\n\ttest\n\t2. should be 2\n\n',
|
||||
}
|
||||
*/
|
||||
'ignoreAnyTagsOutsideBody': {
|
||||
description: 'Content outside body should be ignored',
|
||||
input: '<html><head><title>title</title><style></style></head><body>empty<br></body></html>',
|
||||
wantHTML: '<!DOCTYPE HTML><html><body>empty<br><br></body></html>',
|
||||
wantText: 'empty\n\n',
|
||||
},
|
||||
'indentedListsAreNotBullets': {
|
||||
description: 'Indented lists are represented with tabs and without bullets',
|
||||
input: '<html><body><ul class="indent"><li>indent</li><li>indent</ul></body></html>',
|
||||
wantHTML: '<!DOCTYPE HTML><html><body><ul class="indent"><li>indent</li><li>indent</ul><br></body></html>',
|
||||
wantText: '\tindent\n\tindent\n\n',
|
||||
},
|
||||
'lineWithMultipleSpaces': {
|
||||
description: 'Multiple spaces should be collapsed',
|
||||
input: '<html><body>Text with more than one space.<br></body></html>',
|
||||
wantHTML: '<!DOCTYPE HTML><html><body>Text with more than one space.<br><br></body></html>',
|
||||
wantText: 'Text with more than one space.\n\n',
|
||||
},
|
||||
'lineWithMultipleNonBreakingAndNormalSpaces': {
|
||||
// XXX the HTML between "than" and "one" looks strange
|
||||
description: 'non-breaking space should be preserved, but can be replaced when it',
|
||||
input: '<html><body>Text with more than one space.<br></body></html>',
|
||||
wantHTML: '<!DOCTYPE HTML><html><body>Text with more than one space.<br><br></body></html>',
|
||||
wantText: 'Text with more than one space.\n\n',
|
||||
},
|
||||
'multiplenbsp': {
|
||||
description: 'Multiple non-breaking space should be preserved',
|
||||
input: '<html><body> <br></body></html>',
|
||||
wantHTML: '<!DOCTYPE HTML><html><body> <br><br></body></html>',
|
||||
wantText: ' \n\n',
|
||||
},
|
||||
'multipleNonBreakingSpaceBetweenWords': {
|
||||
description: 'A normal space is always inserted before a word',
|
||||
input: '<html><body> word1 word2 word3<br></body></html>',
|
||||
wantHTML: '<!DOCTYPE HTML><html><body> word1 word2 word3<br><br></body></html>',
|
||||
wantText: ' word1 word2 word3\n\n',
|
||||
},
|
||||
'nonBreakingSpacePreceededBySpaceBetweenWords': {
|
||||
description: 'A non-breaking space preceded by a normal space',
|
||||
input: '<html><body> word1 word2 word3<br></body></html>',
|
||||
wantHTML: '<!DOCTYPE HTML><html><body> word1 word2 word3<br><br></body></html>',
|
||||
wantText: ' word1 word2 word3\n\n',
|
||||
},
|
||||
'nonBreakingSpaceFollowededBySpaceBetweenWords': {
|
||||
description: 'A non-breaking space followed by a normal space',
|
||||
input: '<html><body> word1 word2 word3<br></body></html>',
|
||||
wantHTML: '<!DOCTYPE HTML><html><body> word1 word2 word3<br><br></body></html>',
|
||||
wantText: ' word1 word2 word3\n\n',
|
||||
},
|
||||
'spacesAfterNewline': {
|
||||
description: 'Collapse spaces that follow a newline',
|
||||
input: '<!doctype html><html><body>something<br> something<br></body></html>',
|
||||
wantHTML: '<!DOCTYPE HTML><html><body>something<br>something<br><br></body></html>',
|
||||
wantText: 'something\nsomething\n\n',
|
||||
},
|
||||
'spacesAfterNewlineP': {
|
||||
description: 'Collapse spaces that follow a paragraph',
|
||||
input: '<!doctype html><html><body>something<p></p> something<br></body></html>',
|
||||
wantHTML: '<!DOCTYPE HTML><html><body>something<br><br>something<br><br></body></html>',
|
||||
wantText: 'something\n\nsomething\n\n',
|
||||
},
|
||||
'spacesAtEndOfLine': {
|
||||
description: 'Collapse spaces that preceed/follow a newline',
|
||||
input: '<html><body>something <br> something<br></body></html>',
|
||||
wantHTML: '<!DOCTYPE HTML><html><body>something<br>something<br><br></body></html>',
|
||||
wantText: 'something\nsomething\n\n',
|
||||
},
|
||||
'spacesAtEndOfLineP': {
|
||||
description: 'Collapse spaces that preceed/follow a paragraph',
|
||||
input: '<html><body>something <p></p> something<br></body></html>',
|
||||
wantHTML: '<!DOCTYPE HTML><html><body>something<br><br>something<br><br></body></html>',
|
||||
wantText: 'something\n\nsomething\n\n',
|
||||
},
|
||||
'nonBreakingSpacesAfterNewlines': {
|
||||
description: 'Don\'t collapse non-breaking spaces that follow a newline',
|
||||
input: '<html><body>something<br> something<br></body></html>',
|
||||
wantHTML: '<!DOCTYPE HTML><html><body>something<br> something<br><br></body></html>',
|
||||
wantText: 'something\n something\n\n',
|
||||
},
|
||||
'nonBreakingSpacesAfterNewlinesP': {
|
||||
description: 'Don\'t collapse non-breaking spaces that follow a paragraph',
|
||||
input: '<html><body>something<p></p> something<br></body></html>',
|
||||
wantHTML: '<!DOCTYPE HTML><html><body>something<br><br> something<br><br></body></html>',
|
||||
wantText: 'something\n\n something\n\n',
|
||||
},
|
||||
'collapseSpacesInsideElements': {
|
||||
description: 'Preserve only one space when multiple are present',
|
||||
input: '<html><body>Need <span> more </span> space<i> s </i> !<br></body></html>',
|
||||
wantHTML: '<!DOCTYPE HTML><html><body>Need more space<em> s </em>!<br><br></body></html>',
|
||||
wantText: 'Need more space s !\n\n',
|
||||
},
|
||||
'collapseSpacesAcrossNewlines': {
|
||||
description: 'Newlines and multiple spaces across newlines should be collapsed',
|
||||
input: `
|
||||
'malformed': {
|
||||
input: '<html><body><li>wtf</ul></body></html>',
|
||||
wantHTML: '<!DOCTYPE HTML><html><body>wtf<br><br></body></html>',
|
||||
wantText: 'wtf\n\n',
|
||||
disabled: true,
|
||||
},
|
||||
'nonelistiteminlist #3620': {
|
||||
input: '<html><body><ul>test<li>FOO</li></ul></body></html>',
|
||||
wantHTML: '<!DOCTYPE HTML><html><body><ul class="bullet">test<li>FOO</ul><br></body></html>',
|
||||
wantText: '\ttest\n\t* FOO\n\n',
|
||||
disabled: true,
|
||||
},
|
||||
'whitespaceinlist #3620': {
|
||||
input: '<html><body><ul> <li>FOO</li></ul></body></html>',
|
||||
wantHTML: '<!DOCTYPE HTML><html><body><ul class="bullet"><li>FOO</ul><br></body></html>',
|
||||
wantText: '\t* FOO\n\n',
|
||||
},
|
||||
'prefixcorrectlinenumber': {
|
||||
input: '<html><body><ol><li>should be 1</li><li>should be 2</li></ol></body></html>',
|
||||
wantHTML: '<!DOCTYPE HTML><html><body><ol start="1" class="number"><li>should be 1</li><li>should be 2</ol><br></body></html>',
|
||||
wantText: '\t1. should be 1\n\t2. should be 2\n\n',
|
||||
},
|
||||
'prefixcorrectlinenumbernested': {
|
||||
input: '<html><body><ol><li>should be 1</li><ol><li>foo</li></ol><li>should be 2</li></ol></body></html>',
|
||||
wantHTML: '<!DOCTYPE HTML><html><body><ol start="1" class="number"><li>should be 1<ol start="2" class="number"><li>foo</ol><li>should be 2</ol><br></body></html>',
|
||||
wantText: '\t1. should be 1\n\t\t1.1. foo\n\t2. should be 2\n\n',
|
||||
},
|
||||
/*
|
||||
"prefixcorrectlinenumber when introduced none list item - currently not supported see #3450": {
|
||||
input: '<html><body><ol><li>should be 1</li>test<li>should be 2</li></ol></body></html>',
|
||||
wantHTML: '<!DOCTYPE HTML><html><body><ol start="1" class="number"><li>should be 1</li>test<li>should be 2</li></ol><br></body></html>',
|
||||
wantText: '\t1. should be 1\n\ttest\n\t2. should be 2\n\n',
|
||||
}
|
||||
,
|
||||
"newlinesshouldntresetlinenumber #2194": {
|
||||
input: '<html><body><ol><li>should be 1</li>test<li>should be 2</li></ol></body></html>',
|
||||
wantHTML: '<!DOCTYPE HTML><html><body><ol class="number"><li>should be 1</li>test<li>should be 2</li></ol><br></body></html>',
|
||||
wantText: '\t1. should be 1\n\ttest\n\t2. should be 2\n\n',
|
||||
}
|
||||
*/
|
||||
'ignoreAnyTagsOutsideBody': {
|
||||
description: 'Content outside body should be ignored',
|
||||
input: '<html><head><title>title</title><style></style></head><body>empty<br></body></html>',
|
||||
wantHTML: '<!DOCTYPE HTML><html><body>empty<br><br></body></html>',
|
||||
wantText: 'empty\n\n',
|
||||
},
|
||||
'indentedListsAreNotBullets': {
|
||||
description: 'Indented lists are represented with tabs and without bullets',
|
||||
input: '<html><body><ul class="indent"><li>indent</li><li>indent</ul></body></html>',
|
||||
wantHTML: '<!DOCTYPE HTML><html><body><ul class="indent"><li>indent</li><li>indent</ul><br></body></html>',
|
||||
wantText: '\tindent\n\tindent\n\n',
|
||||
},
|
||||
'lineWithMultipleSpaces': {
|
||||
description: 'Multiple spaces should be collapsed',
|
||||
input: '<html><body>Text with more than one space.<br></body></html>',
|
||||
wantHTML: '<!DOCTYPE HTML><html><body>Text with more than one space.<br><br></body></html>',
|
||||
wantText: 'Text with more than one space.\n\n',
|
||||
},
|
||||
'lineWithMultipleNonBreakingAndNormalSpaces': {
|
||||
// XXX the HTML between "than" and "one" looks strange
|
||||
description: 'non-breaking space should be preserved, but can be replaced when it',
|
||||
input: '<html><body>Text with more than one space.<br></body></html>',
|
||||
wantHTML: '<!DOCTYPE HTML><html><body>Text with more than one space.<br><br></body></html>',
|
||||
wantText: 'Text with more than one space.\n\n',
|
||||
},
|
||||
'multiplenbsp': {
|
||||
description: 'Multiple non-breaking space should be preserved',
|
||||
input: '<html><body> <br></body></html>',
|
||||
wantHTML: '<!DOCTYPE HTML><html><body> <br><br></body></html>',
|
||||
wantText: ' \n\n',
|
||||
},
|
||||
'multipleNonBreakingSpaceBetweenWords': {
|
||||
description: 'A normal space is always inserted before a word',
|
||||
input: '<html><body> word1 word2 word3<br></body></html>',
|
||||
wantHTML: '<!DOCTYPE HTML><html><body> word1 word2 word3<br><br></body></html>',
|
||||
wantText: ' word1 word2 word3\n\n',
|
||||
},
|
||||
'nonBreakingSpacePreceededBySpaceBetweenWords': {
|
||||
description: 'A non-breaking space preceded by a normal space',
|
||||
input: '<html><body> word1 word2 word3<br></body></html>',
|
||||
wantHTML: '<!DOCTYPE HTML><html><body> word1 word2 word3<br><br></body></html>',
|
||||
wantText: ' word1 word2 word3\n\n',
|
||||
},
|
||||
'nonBreakingSpaceFollowededBySpaceBetweenWords': {
|
||||
description: 'A non-breaking space followed by a normal space',
|
||||
input: '<html><body> word1 word2 word3<br></body></html>',
|
||||
wantHTML: '<!DOCTYPE HTML><html><body> word1 word2 word3<br><br></body></html>',
|
||||
wantText: ' word1 word2 word3\n\n',
|
||||
},
|
||||
'spacesAfterNewline': {
|
||||
description: 'Collapse spaces that follow a newline',
|
||||
input: '<!doctype html><html><body>something<br> something<br></body></html>',
|
||||
wantHTML: '<!DOCTYPE HTML><html><body>something<br>something<br><br></body></html>',
|
||||
wantText: 'something\nsomething\n\n',
|
||||
},
|
||||
'spacesAfterNewlineP': {
|
||||
description: 'Collapse spaces that follow a paragraph',
|
||||
input: '<!doctype html><html><body>something<p></p> something<br></body></html>',
|
||||
wantHTML: '<!DOCTYPE HTML><html><body>something<br><br>something<br><br></body></html>',
|
||||
wantText: 'something\n\nsomething\n\n',
|
||||
},
|
||||
'spacesAtEndOfLine': {
|
||||
description: 'Collapse spaces that preceed/follow a newline',
|
||||
input: '<html><body>something <br> something<br></body></html>',
|
||||
wantHTML: '<!DOCTYPE HTML><html><body>something<br>something<br><br></body></html>',
|
||||
wantText: 'something\nsomething\n\n',
|
||||
},
|
||||
'spacesAtEndOfLineP': {
|
||||
description: 'Collapse spaces that preceed/follow a paragraph',
|
||||
input: '<html><body>something <p></p> something<br></body></html>',
|
||||
wantHTML: '<!DOCTYPE HTML><html><body>something<br><br>something<br><br></body></html>',
|
||||
wantText: 'something\n\nsomething\n\n',
|
||||
},
|
||||
'nonBreakingSpacesAfterNewlines': {
|
||||
description: 'Don\'t collapse non-breaking spaces that follow a newline',
|
||||
input: '<html><body>something<br> something<br></body></html>',
|
||||
wantHTML: '<!DOCTYPE HTML><html><body>something<br> something<br><br></body></html>',
|
||||
wantText: 'something\n something\n\n',
|
||||
},
|
||||
'nonBreakingSpacesAfterNewlinesP': {
|
||||
description: 'Don\'t collapse non-breaking spaces that follow a paragraph',
|
||||
input: '<html><body>something<p></p> something<br></body></html>',
|
||||
wantHTML: '<!DOCTYPE HTML><html><body>something<br><br> something<br><br></body></html>',
|
||||
wantText: 'something\n\n something\n\n',
|
||||
},
|
||||
'collapseSpacesInsideElements': {
|
||||
description: 'Preserve only one space when multiple are present',
|
||||
input: '<html><body>Need <span> more </span> space<i> s </i> !<br></body></html>',
|
||||
wantHTML: '<!DOCTYPE HTML><html><body>Need more space<em> s </em>!<br><br></body></html>',
|
||||
wantText: 'Need more space s !\n\n',
|
||||
},
|
||||
'collapseSpacesAcrossNewlines': {
|
||||
description: 'Newlines and multiple spaces across newlines should be collapsed',
|
||||
input: `
|
||||
<html><body>Need
|
||||
<span> more </span>
|
||||
space
|
||||
<i> s </i>
|
||||
!<br></body></html>`,
|
||||
wantHTML: '<!DOCTYPE HTML><html><body>Need more space <em>s </em>!<br><br></body></html>',
|
||||
wantText: 'Need more space s !\n\n',
|
||||
},
|
||||
'multipleNewLinesAtBeginning': {
|
||||
description: 'Multiple new lines and paragraphs at the beginning should be preserved',
|
||||
input: '<html><body><br><br><p></p><p></p>first line<br><br>second line<br></body></html>',
|
||||
wantHTML: '<!DOCTYPE HTML><html><body><br><br><br><br>first line<br><br>second line<br><br></body></html>',
|
||||
wantText: '\n\n\n\nfirst line\n\nsecond line\n\n',
|
||||
},
|
||||
'multiLineParagraph': {
|
||||
description: 'A paragraph with multiple lines should not loose spaces when lines are combined',
|
||||
input: `<html><body>
|
||||
wantHTML: '<!DOCTYPE HTML><html><body>Need more space <em>s </em>!<br><br></body></html>',
|
||||
wantText: 'Need more space s !\n\n',
|
||||
},
|
||||
'multipleNewLinesAtBeginning': {
|
||||
description: 'Multiple new lines and paragraphs at the beginning should be preserved',
|
||||
input: '<html><body><br><br><p></p><p></p>first line<br><br>second line<br></body></html>',
|
||||
wantHTML: '<!DOCTYPE HTML><html><body><br><br><br><br>first line<br><br>second line<br><br></body></html>',
|
||||
wantText: '\n\n\n\nfirst line\n\nsecond line\n\n',
|
||||
},
|
||||
'multiLineParagraph': {
|
||||
description: 'A paragraph with multiple lines should not loose spaces when lines are combined',
|
||||
input: `<html><body>
|
||||
<p>
|
||||
а б в г ґ д е є ж з и і ї й к л м н о
|
||||
п р с т у ф х ц ч ш щ ю я ь
|
||||
</p>
|
||||
</body></html>`,
|
||||
wantHTML: '<!DOCTYPE HTML><html><body>а б в г ґ д е є ж з и і ї й к л м н о п р с т у ф х ц ч ш щ ю я ь<br><br></body></html>',
|
||||
wantText: 'а б в г ґ д е є ж з и і ї й к л м н о п р с т у ф х ц ч ш щ ю я ь\n\n',
|
||||
},
|
||||
'multiLineParagraphWithPre': {
|
||||
// XXX why is there before "in"?
|
||||
description: 'lines in preformatted text should be kept intact',
|
||||
input: `<html><body>
|
||||
wantHTML: '<!DOCTYPE HTML><html><body>а б в г ґ д е є ж з и і ї й к л м н о п р с т у ф х ц ч ш щ ю я ь<br><br></body></html>',
|
||||
wantText: 'а б в г ґ д е є ж з и і ї й к л м н о п р с т у ф х ц ч ш щ ю я ь\n\n',
|
||||
},
|
||||
'multiLineParagraphWithPre': {
|
||||
// XXX why is there before "in"?
|
||||
description: 'lines in preformatted text should be kept intact',
|
||||
input: `<html><body>
|
||||
<p>
|
||||
а б в г ґ д е є ж з и і ї й к л м н о<pre>multiple
|
||||
lines
|
||||
|
@ -188,97 +184,88 @@ const testImports = {
|
|||
</pre></p><p>п р с т у ф х ц ч ш щ ю я
|
||||
ь</p>
|
||||
</body></html>`,
|
||||
wantHTML: '<!DOCTYPE HTML><html><body>а б в г ґ д е є ж з и і ї й к л м н о<br>multiple<br> lines<br> in<br> pre<br><br>п р с т у ф х ц ч ш щ ю я ь<br><br></body></html>',
|
||||
wantText: 'а б в г ґ д е є ж з и і ї й к л м н о\nmultiple\n lines\n in\n pre\n\nп р с т у ф х ц ч ш щ ю я ь\n\n',
|
||||
},
|
||||
'preIntroducesASpace': {
|
||||
description: 'pre should be on a new line not preceded by a space',
|
||||
input: `<html><body><p>
|
||||
wantHTML: '<!DOCTYPE HTML><html><body>а б в г ґ д е є ж з и і ї й к л м н о<br>multiple<br> lines<br> in<br> pre<br><br>п р с т у ф х ц ч ш щ ю я ь<br><br></body></html>',
|
||||
wantText: 'а б в г ґ д е є ж з и і ї й к л м н о\nmultiple\n lines\n in\n pre\n\nп р с т у ф х ц ч ш щ ю я ь\n\n',
|
||||
},
|
||||
'preIntroducesASpace': {
|
||||
description: 'pre should be on a new line not preceded by a space',
|
||||
input: `<html><body><p>
|
||||
1
|
||||
<pre>preline
|
||||
</pre></p></body></html>`,
|
||||
wantHTML: '<!DOCTYPE HTML><html><body>1<br>preline<br><br><br></body></html>',
|
||||
wantText: '1\npreline\n\n\n',
|
||||
},
|
||||
'dontDeleteSpaceInsideElements': {
|
||||
description: 'Preserve spaces inside elements',
|
||||
input: '<html><body>Need<span> more </span>space<i> s </i>!<br></body></html>',
|
||||
wantHTML: '<!DOCTYPE HTML><html><body>Need more space<em> s </em>!<br><br></body></html>',
|
||||
wantText: 'Need more space s !\n\n',
|
||||
},
|
||||
'dontDeleteSpaceOutsideElements': {
|
||||
description: 'Preserve spaces outside elements',
|
||||
input: '<html><body>Need <span>more</span> space <i>s</i> !<br></body></html>',
|
||||
wantHTML: '<!DOCTYPE HTML><html><body>Need more space <em>s</em> !<br><br></body></html>',
|
||||
wantText: 'Need more space s !\n\n',
|
||||
},
|
||||
'dontDeleteSpaceAtEndOfElement': {
|
||||
description: 'Preserve spaces at the end of an element',
|
||||
input: '<html><body>Need <span>more </span>space <i>s </i>!<br></body></html>',
|
||||
wantHTML: '<!DOCTYPE HTML><html><body>Need more space <em>s </em>!<br><br></body></html>',
|
||||
wantText: 'Need more space s !\n\n',
|
||||
},
|
||||
'dontDeleteSpaceAtBeginOfElements': {
|
||||
description: 'Preserve spaces at the start of an element',
|
||||
input: '<html><body>Need<span> more</span> space<i> s</i> !<br></body></html>',
|
||||
wantHTML: '<!DOCTYPE HTML><html><body>Need more space<em> s</em> !<br><br></body></html>',
|
||||
wantText: 'Need more space s !\n\n',
|
||||
},
|
||||
wantHTML: '<!DOCTYPE HTML><html><body>1<br>preline<br><br><br></body></html>',
|
||||
wantText: '1\npreline\n\n\n',
|
||||
},
|
||||
'dontDeleteSpaceInsideElements': {
|
||||
description: 'Preserve spaces inside elements',
|
||||
input: '<html><body>Need<span> more </span>space<i> s </i>!<br></body></html>',
|
||||
wantHTML: '<!DOCTYPE HTML><html><body>Need more space<em> s </em>!<br><br></body></html>',
|
||||
wantText: 'Need more space s !\n\n',
|
||||
},
|
||||
'dontDeleteSpaceOutsideElements': {
|
||||
description: 'Preserve spaces outside elements',
|
||||
input: '<html><body>Need <span>more</span> space <i>s</i> !<br></body></html>',
|
||||
wantHTML: '<!DOCTYPE HTML><html><body>Need more space <em>s</em> !<br><br></body></html>',
|
||||
wantText: 'Need more space s !\n\n',
|
||||
},
|
||||
'dontDeleteSpaceAtEndOfElement': {
|
||||
description: 'Preserve spaces at the end of an element',
|
||||
input: '<html><body>Need <span>more </span>space <i>s </i>!<br></body></html>',
|
||||
wantHTML: '<!DOCTYPE HTML><html><body>Need more space <em>s </em>!<br><br></body></html>',
|
||||
wantText: 'Need more space s !\n\n',
|
||||
},
|
||||
'dontDeleteSpaceAtBeginOfElements': {
|
||||
description: 'Preserve spaces at the start of an element',
|
||||
input: '<html><body>Need<span> more</span> space<i> s</i> !<br></body></html>',
|
||||
wantHTML: '<!DOCTYPE HTML><html><body>Need more space<em> s</em> !<br><br></body></html>',
|
||||
wantText: 'Need more space s !\n\n',
|
||||
},
|
||||
};
|
||||
|
||||
describe(__filename, function () {
|
||||
this.timeout(1000);
|
||||
|
||||
before(async function () { agent = await common.init(); });
|
||||
|
||||
Object.keys(testImports).forEach((testName) => {
|
||||
describe(testName, function () {
|
||||
const testPadId = makeid();
|
||||
const test = testImports[testName];
|
||||
if (test.disabled) {
|
||||
return xit(`DISABLED: ${testName}`, function (done) {
|
||||
done();
|
||||
this.timeout(1000);
|
||||
before(async function () { agent = await common.init(); });
|
||||
Object.keys(testImports).forEach((testName) => {
|
||||
describe(testName, function () {
|
||||
const testPadId = makeid();
|
||||
const test = testImports[testName];
|
||||
if (test.disabled) {
|
||||
return xit(`DISABLED: ${testName}`, function (done) {
|
||||
done();
|
||||
});
|
||||
}
|
||||
it('createPad', async function () {
|
||||
const res = await agent.get(`${endPoint('createPad')}&padID=${testPadId}`)
|
||||
.expect(200)
|
||||
.expect('Content-Type', /json/);
|
||||
assert.equal(res.body.code, 0);
|
||||
});
|
||||
it('setHTML', async function () {
|
||||
const res = await agent.get(`${endPoint('setHTML')}&padID=${testPadId}` +
|
||||
`&html=${encodeURIComponent(test.input)}`)
|
||||
.expect(200)
|
||||
.expect('Content-Type', /json/);
|
||||
assert.equal(res.body.code, 0);
|
||||
});
|
||||
it('getHTML', async function () {
|
||||
const res = await agent.get(`${endPoint('getHTML')}&padID=${testPadId}`)
|
||||
.expect(200)
|
||||
.expect('Content-Type', /json/);
|
||||
assert.equal(res.body.data.html, test.wantHTML);
|
||||
});
|
||||
it('getText', async function () {
|
||||
const res = await agent.get(`${endPoint('getText')}&padID=${testPadId}`)
|
||||
.expect(200)
|
||||
.expect('Content-Type', /json/);
|
||||
assert.equal(res.body.data.text, test.wantText);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
it('createPad', async function () {
|
||||
const res = await agent.get(`${endPoint('createPad')}&padID=${testPadId}`)
|
||||
.expect(200)
|
||||
.expect('Content-Type', /json/);
|
||||
assert.equal(res.body.code, 0);
|
||||
});
|
||||
|
||||
it('setHTML', async function () {
|
||||
const res = await agent.get(`${endPoint('setHTML')}&padID=${testPadId}` +
|
||||
`&html=${encodeURIComponent(test.input)}`)
|
||||
.expect(200)
|
||||
.expect('Content-Type', /json/);
|
||||
assert.equal(res.body.code, 0);
|
||||
});
|
||||
|
||||
it('getHTML', async function () {
|
||||
const res = await agent.get(`${endPoint('getHTML')}&padID=${testPadId}`)
|
||||
.expect(200)
|
||||
.expect('Content-Type', /json/);
|
||||
assert.equal(res.body.data.html, test.wantHTML);
|
||||
});
|
||||
|
||||
it('getText', async function () {
|
||||
const res = await agent.get(`${endPoint('getText')}&padID=${testPadId}`)
|
||||
.expect(200)
|
||||
.expect('Content-Type', /json/);
|
||||
assert.equal(res.body.data.text, test.wantText);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function makeid() {
|
||||
let text = '';
|
||||
const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
|
||||
for (let i = 0; i < 5; i++) {
|
||||
text += possible.charAt(Math.floor(Math.random() * possible.length));
|
||||
}
|
||||
return text;
|
||||
let text = '';
|
||||
const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
for (let i = 0; i < 5; i++) {
|
||||
text += possible.charAt(Math.floor(Math.random() * possible.length));
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
|
|
@ -1,18 +1,10 @@
|
|||
import * as common from "../common.js";
|
||||
import assertLegacy from "../assert-legacy.js";
|
||||
import queryString from "querystring";
|
||||
import * as settings from "../../../node/utils/Settings.js";
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* caching_middleware is responsible for serving everything under path `/javascripts/`
|
||||
* That includes packages as defined in `src/node/utils/tar.json` and probably also plugin code
|
||||
*
|
||||
*/
|
||||
|
||||
const common = require('../common');
|
||||
const assert = require('../assert-legacy').strict;
|
||||
const queryString = require('querystring');
|
||||
const settings = require('../../../node/utils/Settings');
|
||||
|
||||
const assert = assertLegacy.strict;
|
||||
let agent;
|
||||
|
||||
/**
|
||||
* Hack! Returns true if the resource is not plaintext
|
||||
* The file should start with the callback method, so we need the
|
||||
|
@ -23,95 +15,87 @@ let agent;
|
|||
* @returns {boolean} if it is plaintext
|
||||
*/
|
||||
const isPlaintextResponse = (fileContent, resource) => {
|
||||
// callback=require.define&v=1234
|
||||
const query = (new URL(resource, 'http://localhost')).search.slice(1);
|
||||
// require.define
|
||||
const jsonp = queryString.parse(query).callback;
|
||||
|
||||
// returns true if the first letters in fileContent equal the content of `jsonp`
|
||||
return fileContent.substring(0, jsonp.length) === jsonp;
|
||||
// callback=require.define&v=1234
|
||||
const query = (new URL(resource, 'http://localhost')).search.slice(1);
|
||||
// require.define
|
||||
const jsonp = queryString.parse(query).callback;
|
||||
// returns true if the first letters in fileContent equal the content of `jsonp`
|
||||
return fileContent.substring(0, jsonp.length) === jsonp;
|
||||
};
|
||||
|
||||
/**
|
||||
* A hack to disable `superagent`'s auto unzip functionality
|
||||
*
|
||||
* @param {Request} request
|
||||
*/
|
||||
const disableAutoDeflate = (request) => {
|
||||
request._shouldUnzip = () => false;
|
||||
request._shouldUnzip = () => false;
|
||||
};
|
||||
|
||||
describe(__filename, function () {
|
||||
const backups = {};
|
||||
const fantasyEncoding = 'brainwaves'; // non-working encoding until https://github.com/visionmedia/superagent/pull/1560 is resolved
|
||||
const packages = [
|
||||
'/javascripts/lib/ep_etherpad-lite/static/js/ace2_common.js?callback=require.define',
|
||||
'/javascripts/lib/ep_etherpad-lite/static/js/ace2_inner.js?callback=require.define',
|
||||
'/javascripts/lib/ep_etherpad-lite/static/js/pad.js?callback=require.define',
|
||||
'/javascripts/lib/ep_etherpad-lite/static/js/timeslider.js?callback=require.define',
|
||||
];
|
||||
|
||||
before(async function () {
|
||||
agent = await common.init();
|
||||
backups.settings = {};
|
||||
backups.settings.minify = settings.minify;
|
||||
});
|
||||
after(async function () {
|
||||
Object.assign(settings, backups.settings);
|
||||
});
|
||||
|
||||
for (const minify of [false, true]) {
|
||||
context(`when minify is ${minify}`, function () {
|
||||
before(async function () {
|
||||
settings.minify = minify;
|
||||
});
|
||||
|
||||
describe('gets packages uncompressed without Accept-Encoding gzip', function () {
|
||||
for (const resource of packages) {
|
||||
it(resource, async function () {
|
||||
await agent.get(resource)
|
||||
.set('Accept-Encoding', fantasyEncoding)
|
||||
.use(disableAutoDeflate)
|
||||
.expect(200)
|
||||
.expect('Content-Type', /application\/javascript/)
|
||||
.expect((res) => {
|
||||
assert.equal(res.header['content-encoding'], undefined);
|
||||
assert(isPlaintextResponse(res.text, resource));
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe('gets packages compressed with Accept-Encoding gzip', function () {
|
||||
for (const resource of packages) {
|
||||
it(resource, async function () {
|
||||
await agent.get(resource)
|
||||
.set('Accept-Encoding', 'gzip')
|
||||
.use(disableAutoDeflate)
|
||||
.expect(200)
|
||||
.expect('Content-Type', /application\/javascript/)
|
||||
.expect('Content-Encoding', 'gzip')
|
||||
.expect((res) => {
|
||||
assert(!isPlaintextResponse(res.text, resource));
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it('does not cache content-encoding headers', async function () {
|
||||
await agent.get(packages[0])
|
||||
.set('Accept-Encoding', fantasyEncoding)
|
||||
.expect(200)
|
||||
.expect((res) => assert.equal(res.header['content-encoding'], undefined));
|
||||
await agent.get(packages[0])
|
||||
.set('Accept-Encoding', 'gzip')
|
||||
.expect(200)
|
||||
.expect('Content-Encoding', 'gzip');
|
||||
await agent.get(packages[0])
|
||||
.set('Accept-Encoding', fantasyEncoding)
|
||||
.expect(200)
|
||||
.expect((res) => assert.equal(res.header['content-encoding'], undefined));
|
||||
});
|
||||
const backups = {};
|
||||
const fantasyEncoding = 'brainwaves'; // non-working encoding until https://github.com/visionmedia/superagent/pull/1560 is resolved
|
||||
const packages = [
|
||||
'/javascripts/lib/ep_etherpad-lite/static/js/ace2_common.js?callback=require.define',
|
||||
'/javascripts/lib/ep_etherpad-lite/static/js/ace2_inner.js?callback=require.define',
|
||||
'/javascripts/lib/ep_etherpad-lite/static/js/pad.js?callback=require.define',
|
||||
'/javascripts/lib/ep_etherpad-lite/static/js/timeslider.js?callback=require.define',
|
||||
];
|
||||
before(async function () {
|
||||
agent = await common.init();
|
||||
backups.settings = {};
|
||||
backups.settings.minify = settings.minify;
|
||||
});
|
||||
}
|
||||
after(async function () {
|
||||
Object.assign(settings, backups.settings);
|
||||
});
|
||||
for (const minify of [false, true]) {
|
||||
context(`when minify is ${minify}`, function () {
|
||||
before(async function () {
|
||||
settings.minify = minify;
|
||||
});
|
||||
describe('gets packages uncompressed without Accept-Encoding gzip', function () {
|
||||
for (const resource of packages) {
|
||||
it(resource, async function () {
|
||||
await agent.get(resource)
|
||||
.set('Accept-Encoding', fantasyEncoding)
|
||||
.use(disableAutoDeflate)
|
||||
.expect(200)
|
||||
.expect('Content-Type', /application\/javascript/)
|
||||
.expect((res) => {
|
||||
assert.equal(res.header['content-encoding'], undefined);
|
||||
assert(isPlaintextResponse(res.text, resource));
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
describe('gets packages compressed with Accept-Encoding gzip', function () {
|
||||
for (const resource of packages) {
|
||||
it(resource, async function () {
|
||||
await agent.get(resource)
|
||||
.set('Accept-Encoding', 'gzip')
|
||||
.use(disableAutoDeflate)
|
||||
.expect(200)
|
||||
.expect('Content-Type', /application\/javascript/)
|
||||
.expect('Content-Encoding', 'gzip')
|
||||
.expect((res) => {
|
||||
assert(!isPlaintextResponse(res.text, resource));
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
it('does not cache content-encoding headers', async function () {
|
||||
await agent.get(packages[0])
|
||||
.set('Accept-Encoding', fantasyEncoding)
|
||||
.expect(200)
|
||||
.expect((res) => assert.equal(res.header['content-encoding'], undefined));
|
||||
await agent.get(packages[0])
|
||||
.set('Accept-Encoding', 'gzip')
|
||||
.expect(200)
|
||||
.expect('Content-Encoding', 'gzip');
|
||||
await agent.get(packages[0])
|
||||
.set('Accept-Encoding', fantasyEncoding)
|
||||
.expect(200)
|
||||
.expect((res) => assert.equal(res.header['content-encoding'], undefined));
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
|
@ -1,160 +1,153 @@
|
|||
import ChatMessage from "../../../static/js/ChatMessage.js";
|
||||
import { Pad } from "../../../node/db/Pad.js";
|
||||
import assert$0 from "assert";
|
||||
import * as common from "../common.js";
|
||||
import * as padManager from "../../../node/db/PadManager.js";
|
||||
import * as pluginDefs from "../../../static/js/pluginfw/plugin_defs.js";
|
||||
'use strict';
|
||||
|
||||
const ChatMessage = require('../../../static/js/ChatMessage');
|
||||
const {Pad} = require('../../../node/db/Pad');
|
||||
const assert = require('assert').strict;
|
||||
const common = require('../common');
|
||||
const padManager = require('../../../node/db/PadManager');
|
||||
const pluginDefs = require('../../../static/js/pluginfw/plugin_defs');
|
||||
|
||||
const assert = assert$0.strict;
|
||||
const logger = common.logger;
|
||||
|
||||
const checkHook = async (hookName, checkFn) => {
|
||||
if (pluginDefs.hooks[hookName] == null) pluginDefs.hooks[hookName] = [];
|
||||
await new Promise((resolve, reject) => {
|
||||
pluginDefs.hooks[hookName].push({
|
||||
hook_fn: async (hookName, context) => {
|
||||
if (checkFn == null) return;
|
||||
logger.debug(`hook ${hookName} invoked`);
|
||||
try {
|
||||
// Make sure checkFn is called only once.
|
||||
const _checkFn = checkFn;
|
||||
checkFn = null;
|
||||
await _checkFn(context);
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
resolve();
|
||||
},
|
||||
if (pluginDefs.hooks[hookName] == null)
|
||||
pluginDefs.hooks[hookName] = [];
|
||||
await new Promise((resolve, reject) => {
|
||||
pluginDefs.hooks[hookName].push({
|
||||
hook_fn: async (hookName, context) => {
|
||||
if (checkFn == null)
|
||||
return;
|
||||
logger.debug(`hook ${hookName} invoked`);
|
||||
try {
|
||||
// Make sure checkFn is called only once.
|
||||
const _checkFn = checkFn;
|
||||
checkFn = null;
|
||||
await _checkFn(context);
|
||||
}
|
||||
catch (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
resolve();
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const sendMessage = (socket, data) => {
|
||||
socket.send({
|
||||
type: 'COLLABROOM',
|
||||
component: 'pad',
|
||||
data,
|
||||
});
|
||||
socket.send({
|
||||
type: 'COLLABROOM',
|
||||
component: 'pad',
|
||||
data,
|
||||
});
|
||||
};
|
||||
|
||||
const sendChat = (socket, message) => sendMessage(socket, {type: 'CHAT_MESSAGE', message});
|
||||
|
||||
const sendChat = (socket, message) => sendMessage(socket, { type: 'CHAT_MESSAGE', message });
|
||||
describe(__filename, function () {
|
||||
const padId = 'testChatPad';
|
||||
const hooksBackup = {};
|
||||
|
||||
before(async function () {
|
||||
for (const [name, defs] of Object.entries(pluginDefs.hooks)) {
|
||||
if (defs == null) continue;
|
||||
hooksBackup[name] = defs;
|
||||
}
|
||||
});
|
||||
|
||||
beforeEach(async function () {
|
||||
for (const [name, defs] of Object.entries(hooksBackup)) pluginDefs.hooks[name] = [...defs];
|
||||
for (const name of Object.keys(pluginDefs.hooks)) {
|
||||
if (hooksBackup[name] == null) delete pluginDefs.hooks[name];
|
||||
}
|
||||
if (await padManager.doesPadExist(padId)) {
|
||||
const pad = await padManager.getPad(padId);
|
||||
await pad.remove();
|
||||
}
|
||||
});
|
||||
|
||||
after(async function () {
|
||||
Object.assign(pluginDefs.hooks, hooksBackup);
|
||||
for (const name of Object.keys(pluginDefs.hooks)) {
|
||||
if (hooksBackup[name] == null) delete pluginDefs.hooks[name];
|
||||
}
|
||||
});
|
||||
|
||||
describe('chatNewMessage hook', function () {
|
||||
let authorId;
|
||||
let socket;
|
||||
|
||||
const padId = 'testChatPad';
|
||||
const hooksBackup = {};
|
||||
before(async function () {
|
||||
for (const [name, defs] of Object.entries(pluginDefs.hooks)) {
|
||||
if (defs == null)
|
||||
continue;
|
||||
hooksBackup[name] = defs;
|
||||
}
|
||||
});
|
||||
beforeEach(async function () {
|
||||
socket = await common.connect();
|
||||
const {data: clientVars} = await common.handshake(socket, padId);
|
||||
authorId = clientVars.userId;
|
||||
for (const [name, defs] of Object.entries(hooksBackup))
|
||||
pluginDefs.hooks[name] = [...defs];
|
||||
for (const name of Object.keys(pluginDefs.hooks)) {
|
||||
if (hooksBackup[name] == null)
|
||||
delete pluginDefs.hooks[name];
|
||||
}
|
||||
if (await padManager.doesPadExist(padId)) {
|
||||
const pad = await padManager.getPad(padId);
|
||||
await pad.remove();
|
||||
}
|
||||
});
|
||||
|
||||
afterEach(async function () {
|
||||
socket.close();
|
||||
after(async function () {
|
||||
Object.assign(pluginDefs.hooks, hooksBackup);
|
||||
for (const name of Object.keys(pluginDefs.hooks)) {
|
||||
if (hooksBackup[name] == null)
|
||||
delete pluginDefs.hooks[name];
|
||||
}
|
||||
});
|
||||
|
||||
it('message', async function () {
|
||||
const start = Date.now();
|
||||
await Promise.all([
|
||||
checkHook('chatNewMessage', ({message}) => {
|
||||
assert(message != null);
|
||||
assert(message instanceof ChatMessage);
|
||||
assert.equal(message.authorId, authorId);
|
||||
assert.equal(message.text, this.test.title);
|
||||
assert(message.time >= start);
|
||||
assert(message.time <= Date.now());
|
||||
}),
|
||||
sendChat(socket, {text: this.test.title}),
|
||||
]);
|
||||
describe('chatNewMessage hook', function () {
|
||||
let authorId;
|
||||
let socket;
|
||||
beforeEach(async function () {
|
||||
socket = await common.connect();
|
||||
const { data: clientVars } = await common.handshake(socket, padId);
|
||||
authorId = clientVars.userId;
|
||||
});
|
||||
afterEach(async function () {
|
||||
socket.close();
|
||||
});
|
||||
it('message', async function () {
|
||||
const start = Date.now();
|
||||
await Promise.all([
|
||||
checkHook('chatNewMessage', ({ message }) => {
|
||||
assert(message != null);
|
||||
assert(message instanceof ChatMessage);
|
||||
assert.equal(message.authorId, authorId);
|
||||
assert.equal(message.text, this.test.title);
|
||||
assert(message.time >= start);
|
||||
assert(message.time <= Date.now());
|
||||
}),
|
||||
sendChat(socket, { text: this.test.title }),
|
||||
]);
|
||||
});
|
||||
it('pad', async function () {
|
||||
await Promise.all([
|
||||
checkHook('chatNewMessage', ({ pad }) => {
|
||||
assert(pad != null);
|
||||
assert(pad instanceof Pad);
|
||||
assert.equal(pad.id, padId);
|
||||
}),
|
||||
sendChat(socket, { text: this.test.title }),
|
||||
]);
|
||||
});
|
||||
it('padId', async function () {
|
||||
await Promise.all([
|
||||
checkHook('chatNewMessage', (context) => {
|
||||
assert.equal(context.padId, padId);
|
||||
}),
|
||||
sendChat(socket, { text: this.test.title }),
|
||||
]);
|
||||
});
|
||||
it('mutations propagate', async function () {
|
||||
const listen = async (type) => await new Promise((resolve) => {
|
||||
const handler = (msg) => {
|
||||
if (msg.type !== 'COLLABROOM')
|
||||
return;
|
||||
if (msg.data == null || msg.data.type !== type)
|
||||
return;
|
||||
resolve(msg.data);
|
||||
socket.off('message', handler);
|
||||
};
|
||||
socket.on('message', handler);
|
||||
});
|
||||
const modifiedText = `${this.test.title} <added changes>`;
|
||||
const customMetadata = { foo: this.test.title };
|
||||
await Promise.all([
|
||||
checkHook('chatNewMessage', ({ message }) => {
|
||||
message.text = modifiedText;
|
||||
message.customMetadata = customMetadata;
|
||||
}),
|
||||
(async () => {
|
||||
const { message } = await listen('CHAT_MESSAGE');
|
||||
assert(message != null);
|
||||
assert.equal(message.text, modifiedText);
|
||||
assert.deepEqual(message.customMetadata, customMetadata);
|
||||
})(),
|
||||
sendChat(socket, { text: this.test.title }),
|
||||
]);
|
||||
// Simulate fetch of historical chat messages when a pad is first loaded.
|
||||
await Promise.all([
|
||||
(async () => {
|
||||
const { messages: [message] } = await listen('CHAT_MESSAGES');
|
||||
assert(message != null);
|
||||
assert.equal(message.text, modifiedText);
|
||||
assert.deepEqual(message.customMetadata, customMetadata);
|
||||
})(),
|
||||
sendMessage(socket, { type: 'GET_CHAT_MESSAGES', start: 0, end: 0 }),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
it('pad', async function () {
|
||||
await Promise.all([
|
||||
checkHook('chatNewMessage', ({pad}) => {
|
||||
assert(pad != null);
|
||||
assert(pad instanceof Pad);
|
||||
assert.equal(pad.id, padId);
|
||||
}),
|
||||
sendChat(socket, {text: this.test.title}),
|
||||
]);
|
||||
});
|
||||
|
||||
it('padId', async function () {
|
||||
await Promise.all([
|
||||
checkHook('chatNewMessage', (context) => {
|
||||
assert.equal(context.padId, padId);
|
||||
}),
|
||||
sendChat(socket, {text: this.test.title}),
|
||||
]);
|
||||
});
|
||||
|
||||
it('mutations propagate', async function () {
|
||||
const listen = async (type) => await new Promise((resolve) => {
|
||||
const handler = (msg) => {
|
||||
if (msg.type !== 'COLLABROOM') return;
|
||||
if (msg.data == null || msg.data.type !== type) return;
|
||||
resolve(msg.data);
|
||||
socket.off('message', handler);
|
||||
};
|
||||
socket.on('message', handler);
|
||||
});
|
||||
|
||||
const modifiedText = `${this.test.title} <added changes>`;
|
||||
const customMetadata = {foo: this.test.title};
|
||||
await Promise.all([
|
||||
checkHook('chatNewMessage', ({message}) => {
|
||||
message.text = modifiedText;
|
||||
message.customMetadata = customMetadata;
|
||||
}),
|
||||
(async () => {
|
||||
const {message} = await listen('CHAT_MESSAGE');
|
||||
assert(message != null);
|
||||
assert.equal(message.text, modifiedText);
|
||||
assert.deepEqual(message.customMetadata, customMetadata);
|
||||
})(),
|
||||
sendChat(socket, {text: this.test.title}),
|
||||
]);
|
||||
// Simulate fetch of historical chat messages when a pad is first loaded.
|
||||
await Promise.all([
|
||||
(async () => {
|
||||
const {messages: [message]} = await listen('CHAT_MESSAGES');
|
||||
assert(message != null);
|
||||
assert.equal(message.text, modifiedText);
|
||||
assert.deepEqual(message.customMetadata, customMetadata);
|
||||
})(),
|
||||
sendMessage(socket, {type: 'GET_CHAT_MESSAGES', start: 0, end: 0}),
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,284 +1,273 @@
|
|||
import AttributePool from "../../../static/js/AttributePool.js";
|
||||
import * as Changeset from "../../../static/js/Changeset.js";
|
||||
import assert$0 from "assert";
|
||||
import * as attributes from "../../../static/js/attributes.js";
|
||||
import * as contentcollector from "../../../static/js/contentcollector.js";
|
||||
import * as jsdom from "jsdom";
|
||||
'use strict';
|
||||
|
||||
/*
|
||||
* While importexport tests target the `setHTML` API endpoint, which is nearly identical to what
|
||||
* happens when a user manually imports a document via the UI, the contentcollector tests here don't
|
||||
* use rehype to process the document. Rehype removes spaces and newĺines were applicable, so the
|
||||
* expected results here can differ from importexport.js.
|
||||
*
|
||||
* If you add tests here, please also add them to importexport.js
|
||||
*/
|
||||
|
||||
const AttributePool = require('../../../static/js/AttributePool');
|
||||
const Changeset = require('../../../static/js/Changeset');
|
||||
const assert = require('assert').strict;
|
||||
const attributes = require('../../../static/js/attributes');
|
||||
const contentcollector = require('../../../static/js/contentcollector');
|
||||
const jsdom = require('jsdom');
|
||||
|
||||
const assert = assert$0.strict;
|
||||
// All test case `wantAlines` values must only refer to attributes in this list so that the
|
||||
// attribute numbers do not change due to changes in pool insertion order.
|
||||
const knownAttribs = [
|
||||
['insertorder', 'first'],
|
||||
['italic', 'true'],
|
||||
['list', 'bullet1'],
|
||||
['list', 'bullet2'],
|
||||
['list', 'number1'],
|
||||
['list', 'number2'],
|
||||
['lmkr', '1'],
|
||||
['start', '1'],
|
||||
['start', '2'],
|
||||
['insertorder', 'first'],
|
||||
['italic', 'true'],
|
||||
['list', 'bullet1'],
|
||||
['list', 'bullet2'],
|
||||
['list', 'number1'],
|
||||
['list', 'number2'],
|
||||
['lmkr', '1'],
|
||||
['start', '1'],
|
||||
['start', '2'],
|
||||
];
|
||||
|
||||
const testCases = [
|
||||
{
|
||||
description: 'Simple',
|
||||
html: '<html><body><p>foo</p></body></html>',
|
||||
wantAlines: ['+3'],
|
||||
wantText: ['foo'],
|
||||
},
|
||||
{
|
||||
description: 'Line starts with asterisk',
|
||||
html: '<html><body><p>*foo</p></body></html>',
|
||||
wantAlines: ['+4'],
|
||||
wantText: ['*foo'],
|
||||
},
|
||||
{
|
||||
description: 'Complex nested Li',
|
||||
html: '<!doctype html><html><body><ol><li>one</li><li><ol><li>1.1</li></ol></li><li>two</li></ol></body></html>',
|
||||
wantAlines: [
|
||||
'*0*4*6*7+1+3',
|
||||
'*0*5*6*8+1+3',
|
||||
'*0*4*6*8+1+3',
|
||||
],
|
||||
wantText: [
|
||||
'*one', '*1.1', '*two',
|
||||
],
|
||||
},
|
||||
{
|
||||
description: 'Complex list of different types',
|
||||
html: '<!doctype html><html><body><ul class="bullet"><li>one</li><li>two</li><li>0</li><li>1</li><li>2<ul class="bullet"><li>3</li><li>4</li></ul></li></ul><ol class="number"><li>item<ol class="number"><li>item1</li><li>item2</li></ol></li></ol></body></html>',
|
||||
wantAlines: [
|
||||
'*0*2*6+1+3',
|
||||
'*0*2*6+1+3',
|
||||
'*0*2*6+1+1',
|
||||
'*0*2*6+1+1',
|
||||
'*0*2*6+1+1',
|
||||
'*0*3*6+1+1',
|
||||
'*0*3*6+1+1',
|
||||
'*0*4*6*7+1+4',
|
||||
'*0*5*6*8+1+5',
|
||||
'*0*5*6*8+1+5',
|
||||
],
|
||||
wantText: [
|
||||
'*one',
|
||||
'*two',
|
||||
'*0',
|
||||
'*1',
|
||||
'*2',
|
||||
'*3',
|
||||
'*4',
|
||||
'*item',
|
||||
'*item1',
|
||||
'*item2',
|
||||
],
|
||||
},
|
||||
{
|
||||
description: 'Tests if uls properly get attributes',
|
||||
html: '<html><body><ul><li>a</li><li>b</li></ul><div>div</div><p>foo</p></body></html>',
|
||||
wantAlines: [
|
||||
'*0*2*6+1+1',
|
||||
'*0*2*6+1+1',
|
||||
'+3',
|
||||
'+3',
|
||||
],
|
||||
wantText: ['*a', '*b', 'div', 'foo'],
|
||||
},
|
||||
{
|
||||
description: 'Tests if indented uls properly get attributes',
|
||||
html: '<html><body><ul><li>a</li><ul><li>b</li></ul><li>a</li></ul><p>foo</p></body></html>',
|
||||
wantAlines: [
|
||||
'*0*2*6+1+1',
|
||||
'*0*3*6+1+1',
|
||||
'*0*2*6+1+1',
|
||||
'+3',
|
||||
],
|
||||
wantText: ['*a', '*b', '*a', 'foo'],
|
||||
},
|
||||
{
|
||||
description: 'Tests if ols properly get line numbers when in a normal OL',
|
||||
html: '<html><body><ol><li>a</li><li>b</li><li>c</li></ol><p>test</p></body></html>',
|
||||
wantAlines: [
|
||||
'*0*4*6*7+1+1',
|
||||
'*0*4*6*7+1+1',
|
||||
'*0*4*6*7+1+1',
|
||||
'+4',
|
||||
],
|
||||
wantText: ['*a', '*b', '*c', 'test'],
|
||||
noteToSelf: 'Ensure empty P does not induce line attribute marker, wont this break the editor?',
|
||||
},
|
||||
{
|
||||
description: 'A single completely empty line break within an ol should reset count if OL is closed off..',
|
||||
html: '<html><body><ol><li>should be 1</li></ol><p>hello</p><ol><li>should be 1</li><li>should be 2</li></ol><p></p></body></html>',
|
||||
wantAlines: [
|
||||
'*0*4*6*7+1+b',
|
||||
'+5',
|
||||
'*0*4*6*8+1+b',
|
||||
'*0*4*6*8+1+b',
|
||||
'',
|
||||
],
|
||||
wantText: ['*should be 1', 'hello', '*should be 1', '*should be 2', ''],
|
||||
noteToSelf: "Shouldn't include attribute marker in the <p> line",
|
||||
},
|
||||
{
|
||||
description: 'A single <p></p> should create a new line',
|
||||
html: '<html><body><p></p><p></p></body></html>',
|
||||
wantAlines: ['', ''],
|
||||
wantText: ['', ''],
|
||||
noteToSelf: '<p></p>should create a line break but not break numbering',
|
||||
},
|
||||
{
|
||||
description: 'Tests if ols properly get line numbers when in a normal OL #2',
|
||||
html: '<html><body>a<ol><li>b<ol><li>c</li></ol></ol>notlist<p>foo</p></body></html>',
|
||||
wantAlines: [
|
||||
'+1',
|
||||
'*0*4*6*7+1+1',
|
||||
'*0*5*6*8+1+1',
|
||||
'+7',
|
||||
'+3',
|
||||
],
|
||||
wantText: ['a', '*b', '*c', 'notlist', 'foo'],
|
||||
noteToSelf: 'Ensure empty P does not induce line attribute marker, wont this break the editor?',
|
||||
},
|
||||
{
|
||||
description: 'First item being an UL then subsequent being OL will fail',
|
||||
html: '<html><body><ul><li>a<ol><li>b</li><li>c</li></ol></li></ul></body></html>',
|
||||
wantAlines: ['+1', '*0*1*2*3+1+1', '*0*4*2*5+1+1'],
|
||||
wantText: ['a', '*b', '*c'],
|
||||
noteToSelf: 'Ensure empty P does not induce line attribute marker, wont this break the editor?',
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
description: 'A single completely empty line break within an ol should NOT reset count',
|
||||
html: '<html><body><ol><li>should be 1</li><p></p><li>should be 2</li><li>should be 3</li></ol><p></p></body></html>',
|
||||
wantAlines: [],
|
||||
wantText: ['*should be 1', '*should be 2', '*should be 3'],
|
||||
noteToSelf: "<p></p>should create a line break but not break numbering -- This is what I can't get working!",
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
description: 'Content outside body should be ignored',
|
||||
html: '<html><head><title>title</title><style></style></head><body>empty<br></body></html>',
|
||||
wantAlines: ['+5'],
|
||||
wantText: ['empty'],
|
||||
},
|
||||
{
|
||||
description: 'Multiple spaces should be preserved',
|
||||
html: '<html><body>Text with more than one space.<br></body></html>',
|
||||
wantAlines: ['+10'],
|
||||
wantText: ['Text with more than one space.'],
|
||||
},
|
||||
{
|
||||
description: 'non-breaking and normal space should be preserved',
|
||||
html: '<html><body>Text with more than one space.<br></body></html>',
|
||||
wantAlines: ['+10'],
|
||||
wantText: ['Text with more than one space.'],
|
||||
},
|
||||
{
|
||||
description: 'Multiple nbsp should be preserved',
|
||||
html: '<html><body> <br></body></html>',
|
||||
wantAlines: ['+2'],
|
||||
wantText: [' '],
|
||||
},
|
||||
{
|
||||
description: 'Multiple nbsp between words ',
|
||||
html: '<html><body> word1 word2 word3<br></body></html>',
|
||||
wantAlines: ['+m'],
|
||||
wantText: [' word1 word2 word3'],
|
||||
},
|
||||
{
|
||||
description: 'A non-breaking space preceded by a normal space',
|
||||
html: '<html><body> word1 word2 word3<br></body></html>',
|
||||
wantAlines: ['+l'],
|
||||
wantText: [' word1 word2 word3'],
|
||||
},
|
||||
{
|
||||
description: 'A non-breaking space followed by a normal space',
|
||||
html: '<html><body> word1 word2 word3<br></body></html>',
|
||||
wantAlines: ['+l'],
|
||||
wantText: [' word1 word2 word3'],
|
||||
},
|
||||
{
|
||||
description: 'Don\'t collapse spaces that follow a newline',
|
||||
html: '<!doctype html><html><body>something<br> something<br></body></html>',
|
||||
wantAlines: ['+9', '+m'],
|
||||
wantText: ['something', ' something'],
|
||||
},
|
||||
{
|
||||
description: 'Don\'t collapse spaces that follow a empty paragraph',
|
||||
html: '<!doctype html><html><body>something<p></p> something<br></body></html>',
|
||||
wantAlines: ['+9', '', '+m'],
|
||||
wantText: ['something', '', ' something'],
|
||||
},
|
||||
{
|
||||
description: 'Don\'t collapse spaces that preceed/follow a newline',
|
||||
html: '<html><body>something <br> something<br></body></html>',
|
||||
wantAlines: ['+l', '+m'],
|
||||
wantText: ['something ', ' something'],
|
||||
},
|
||||
{
|
||||
description: 'Don\'t collapse spaces that preceed/follow a empty paragraph',
|
||||
html: '<html><body>something <p></p> something<br></body></html>',
|
||||
wantAlines: ['+l', '', '+m'],
|
||||
wantText: ['something ', '', ' something'],
|
||||
},
|
||||
{
|
||||
description: 'Don\'t collapse non-breaking spaces that follow a newline',
|
||||
html: '<html><body>something<br> something<br></body></html>',
|
||||
wantAlines: ['+9', '+c'],
|
||||
wantText: ['something', ' something'],
|
||||
},
|
||||
{
|
||||
description: 'Don\'t collapse non-breaking spaces that follow a paragraph',
|
||||
html: '<html><body>something<p></p> something<br></body></html>',
|
||||
wantAlines: ['+9', '', '+c'],
|
||||
wantText: ['something', '', ' something'],
|
||||
},
|
||||
{
|
||||
description: 'Preserve all spaces when multiple are present',
|
||||
html: '<html><body>Need <span> more </span> space<i> s </i> !<br></body></html>',
|
||||
wantAlines: ['+h*1+4+2'],
|
||||
wantText: ['Need more space s !'],
|
||||
},
|
||||
{
|
||||
description: 'Newlines and multiple spaces across newlines should be preserved',
|
||||
html: `
|
||||
{
|
||||
description: 'Simple',
|
||||
html: '<html><body><p>foo</p></body></html>',
|
||||
wantAlines: ['+3'],
|
||||
wantText: ['foo'],
|
||||
},
|
||||
{
|
||||
description: 'Line starts with asterisk',
|
||||
html: '<html><body><p>*foo</p></body></html>',
|
||||
wantAlines: ['+4'],
|
||||
wantText: ['*foo'],
|
||||
},
|
||||
{
|
||||
description: 'Complex nested Li',
|
||||
html: '<!doctype html><html><body><ol><li>one</li><li><ol><li>1.1</li></ol></li><li>two</li></ol></body></html>',
|
||||
wantAlines: [
|
||||
'*0*4*6*7+1+3',
|
||||
'*0*5*6*8+1+3',
|
||||
'*0*4*6*8+1+3',
|
||||
],
|
||||
wantText: [
|
||||
'*one', '*1.1', '*two',
|
||||
],
|
||||
},
|
||||
{
|
||||
description: 'Complex list of different types',
|
||||
html: '<!doctype html><html><body><ul class="bullet"><li>one</li><li>two</li><li>0</li><li>1</li><li>2<ul class="bullet"><li>3</li><li>4</li></ul></li></ul><ol class="number"><li>item<ol class="number"><li>item1</li><li>item2</li></ol></li></ol></body></html>',
|
||||
wantAlines: [
|
||||
'*0*2*6+1+3',
|
||||
'*0*2*6+1+3',
|
||||
'*0*2*6+1+1',
|
||||
'*0*2*6+1+1',
|
||||
'*0*2*6+1+1',
|
||||
'*0*3*6+1+1',
|
||||
'*0*3*6+1+1',
|
||||
'*0*4*6*7+1+4',
|
||||
'*0*5*6*8+1+5',
|
||||
'*0*5*6*8+1+5',
|
||||
],
|
||||
wantText: [
|
||||
'*one',
|
||||
'*two',
|
||||
'*0',
|
||||
'*1',
|
||||
'*2',
|
||||
'*3',
|
||||
'*4',
|
||||
'*item',
|
||||
'*item1',
|
||||
'*item2',
|
||||
],
|
||||
},
|
||||
{
|
||||
description: 'Tests if uls properly get attributes',
|
||||
html: '<html><body><ul><li>a</li><li>b</li></ul><div>div</div><p>foo</p></body></html>',
|
||||
wantAlines: [
|
||||
'*0*2*6+1+1',
|
||||
'*0*2*6+1+1',
|
||||
'+3',
|
||||
'+3',
|
||||
],
|
||||
wantText: ['*a', '*b', 'div', 'foo'],
|
||||
},
|
||||
{
|
||||
description: 'Tests if indented uls properly get attributes',
|
||||
html: '<html><body><ul><li>a</li><ul><li>b</li></ul><li>a</li></ul><p>foo</p></body></html>',
|
||||
wantAlines: [
|
||||
'*0*2*6+1+1',
|
||||
'*0*3*6+1+1',
|
||||
'*0*2*6+1+1',
|
||||
'+3',
|
||||
],
|
||||
wantText: ['*a', '*b', '*a', 'foo'],
|
||||
},
|
||||
{
|
||||
description: 'Tests if ols properly get line numbers when in a normal OL',
|
||||
html: '<html><body><ol><li>a</li><li>b</li><li>c</li></ol><p>test</p></body></html>',
|
||||
wantAlines: [
|
||||
'*0*4*6*7+1+1',
|
||||
'*0*4*6*7+1+1',
|
||||
'*0*4*6*7+1+1',
|
||||
'+4',
|
||||
],
|
||||
wantText: ['*a', '*b', '*c', 'test'],
|
||||
noteToSelf: 'Ensure empty P does not induce line attribute marker, wont this break the editor?',
|
||||
},
|
||||
{
|
||||
description: 'A single completely empty line break within an ol should reset count if OL is closed off..',
|
||||
html: '<html><body><ol><li>should be 1</li></ol><p>hello</p><ol><li>should be 1</li><li>should be 2</li></ol><p></p></body></html>',
|
||||
wantAlines: [
|
||||
'*0*4*6*7+1+b',
|
||||
'+5',
|
||||
'*0*4*6*8+1+b',
|
||||
'*0*4*6*8+1+b',
|
||||
'',
|
||||
],
|
||||
wantText: ['*should be 1', 'hello', '*should be 1', '*should be 2', ''],
|
||||
noteToSelf: "Shouldn't include attribute marker in the <p> line",
|
||||
},
|
||||
{
|
||||
description: 'A single <p></p> should create a new line',
|
||||
html: '<html><body><p></p><p></p></body></html>',
|
||||
wantAlines: ['', ''],
|
||||
wantText: ['', ''],
|
||||
noteToSelf: '<p></p>should create a line break but not break numbering',
|
||||
},
|
||||
{
|
||||
description: 'Tests if ols properly get line numbers when in a normal OL #2',
|
||||
html: '<html><body>a<ol><li>b<ol><li>c</li></ol></ol>notlist<p>foo</p></body></html>',
|
||||
wantAlines: [
|
||||
'+1',
|
||||
'*0*4*6*7+1+1',
|
||||
'*0*5*6*8+1+1',
|
||||
'+7',
|
||||
'+3',
|
||||
],
|
||||
wantText: ['a', '*b', '*c', 'notlist', 'foo'],
|
||||
noteToSelf: 'Ensure empty P does not induce line attribute marker, wont this break the editor?',
|
||||
},
|
||||
{
|
||||
description: 'First item being an UL then subsequent being OL will fail',
|
||||
html: '<html><body><ul><li>a<ol><li>b</li><li>c</li></ol></li></ul></body></html>',
|
||||
wantAlines: ['+1', '*0*1*2*3+1+1', '*0*4*2*5+1+1'],
|
||||
wantText: ['a', '*b', '*c'],
|
||||
noteToSelf: 'Ensure empty P does not induce line attribute marker, wont this break the editor?',
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
description: 'A single completely empty line break within an ol should NOT reset count',
|
||||
html: '<html><body><ol><li>should be 1</li><p></p><li>should be 2</li><li>should be 3</li></ol><p></p></body></html>',
|
||||
wantAlines: [],
|
||||
wantText: ['*should be 1', '*should be 2', '*should be 3'],
|
||||
noteToSelf: "<p></p>should create a line break but not break numbering -- This is what I can't get working!",
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
description: 'Content outside body should be ignored',
|
||||
html: '<html><head><title>title</title><style></style></head><body>empty<br></body></html>',
|
||||
wantAlines: ['+5'],
|
||||
wantText: ['empty'],
|
||||
},
|
||||
{
|
||||
description: 'Multiple spaces should be preserved',
|
||||
html: '<html><body>Text with more than one space.<br></body></html>',
|
||||
wantAlines: ['+10'],
|
||||
wantText: ['Text with more than one space.'],
|
||||
},
|
||||
{
|
||||
description: 'non-breaking and normal space should be preserved',
|
||||
html: '<html><body>Text with more than one space.<br></body></html>',
|
||||
wantAlines: ['+10'],
|
||||
wantText: ['Text with more than one space.'],
|
||||
},
|
||||
{
|
||||
description: 'Multiple nbsp should be preserved',
|
||||
html: '<html><body> <br></body></html>',
|
||||
wantAlines: ['+2'],
|
||||
wantText: [' '],
|
||||
},
|
||||
{
|
||||
description: 'Multiple nbsp between words ',
|
||||
html: '<html><body> word1 word2 word3<br></body></html>',
|
||||
wantAlines: ['+m'],
|
||||
wantText: [' word1 word2 word3'],
|
||||
},
|
||||
{
|
||||
description: 'A non-breaking space preceded by a normal space',
|
||||
html: '<html><body> word1 word2 word3<br></body></html>',
|
||||
wantAlines: ['+l'],
|
||||
wantText: [' word1 word2 word3'],
|
||||
},
|
||||
{
|
||||
description: 'A non-breaking space followed by a normal space',
|
||||
html: '<html><body> word1 word2 word3<br></body></html>',
|
||||
wantAlines: ['+l'],
|
||||
wantText: [' word1 word2 word3'],
|
||||
},
|
||||
{
|
||||
description: 'Don\'t collapse spaces that follow a newline',
|
||||
html: '<!doctype html><html><body>something<br> something<br></body></html>',
|
||||
wantAlines: ['+9', '+m'],
|
||||
wantText: ['something', ' something'],
|
||||
},
|
||||
{
|
||||
description: 'Don\'t collapse spaces that follow a empty paragraph',
|
||||
html: '<!doctype html><html><body>something<p></p> something<br></body></html>',
|
||||
wantAlines: ['+9', '', '+m'],
|
||||
wantText: ['something', '', ' something'],
|
||||
},
|
||||
{
|
||||
description: 'Don\'t collapse spaces that preceed/follow a newline',
|
||||
html: '<html><body>something <br> something<br></body></html>',
|
||||
wantAlines: ['+l', '+m'],
|
||||
wantText: ['something ', ' something'],
|
||||
},
|
||||
{
|
||||
description: 'Don\'t collapse spaces that preceed/follow a empty paragraph',
|
||||
html: '<html><body>something <p></p> something<br></body></html>',
|
||||
wantAlines: ['+l', '', '+m'],
|
||||
wantText: ['something ', '', ' something'],
|
||||
},
|
||||
{
|
||||
description: 'Don\'t collapse non-breaking spaces that follow a newline',
|
||||
html: '<html><body>something<br> something<br></body></html>',
|
||||
wantAlines: ['+9', '+c'],
|
||||
wantText: ['something', ' something'],
|
||||
},
|
||||
{
|
||||
description: 'Don\'t collapse non-breaking spaces that follow a paragraph',
|
||||
html: '<html><body>something<p></p> something<br></body></html>',
|
||||
wantAlines: ['+9', '', '+c'],
|
||||
wantText: ['something', '', ' something'],
|
||||
},
|
||||
{
|
||||
description: 'Preserve all spaces when multiple are present',
|
||||
html: '<html><body>Need <span> more </span> space<i> s </i> !<br></body></html>',
|
||||
wantAlines: ['+h*1+4+2'],
|
||||
wantText: ['Need more space s !'],
|
||||
},
|
||||
{
|
||||
description: 'Newlines and multiple spaces across newlines should be preserved',
|
||||
html: `
|
||||
<html><body>Need
|
||||
<span> more </span>
|
||||
space
|
||||
<i> s </i>
|
||||
!<br></body></html>`,
|
||||
wantAlines: ['+19*1+4+b'],
|
||||
wantText: ['Need more space s !'],
|
||||
},
|
||||
{
|
||||
description: 'Multiple new lines at the beginning should be preserved',
|
||||
html: '<html><body><br><br><p></p><p></p>first line<br><br>second line<br></body></html>',
|
||||
wantAlines: ['', '', '', '', '+a', '', '+b'],
|
||||
wantText: ['', '', '', '', 'first line', '', 'second line'],
|
||||
},
|
||||
{
|
||||
description: 'A paragraph with multiple lines should not loose spaces when lines are combined',
|
||||
html: `<html><body><p>
|
||||
wantAlines: ['+19*1+4+b'],
|
||||
wantText: ['Need more space s !'],
|
||||
},
|
||||
{
|
||||
description: 'Multiple new lines at the beginning should be preserved',
|
||||
html: '<html><body><br><br><p></p><p></p>first line<br><br>second line<br></body></html>',
|
||||
wantAlines: ['', '', '', '', '+a', '', '+b'],
|
||||
wantText: ['', '', '', '', 'first line', '', 'second line'],
|
||||
},
|
||||
{
|
||||
description: 'A paragraph with multiple lines should not loose spaces when lines are combined',
|
||||
html: `<html><body><p>
|
||||
а б в г ґ д е є ж з и і ї й к л м н о
|
||||
п р с т у ф х ц ч ш щ ю я ь</p>
|
||||
</body></html>`,
|
||||
wantAlines: ['+1t'],
|
||||
wantText: ['а б в г ґ д е є ж з и і ї й к л м н о п р с т у ф х ц ч ш щ ю я ь'],
|
||||
},
|
||||
{
|
||||
description: 'lines in preformatted text should be kept intact',
|
||||
html: `<html><body><p>
|
||||
wantAlines: ['+1t'],
|
||||
wantText: ['а б в г ґ д е є ж з и і ї й к л м н о п р с т у ф х ц ч ш щ ю я ь'],
|
||||
},
|
||||
{
|
||||
description: 'lines in preformatted text should be kept intact',
|
||||
html: `<html><body><p>
|
||||
а б в г ґ д е є ж з и і ї й к л м н о</p><pre>multiple
|
||||
lines
|
||||
in
|
||||
|
@ -286,101 +275,98 @@ pre
|
|||
</pre><p>п р с т у ф х ц ч ш щ ю я
|
||||
ь</p>
|
||||
</body></html>`,
|
||||
wantAlines: ['+11', '+8', '+5', '+2', '+3', '+r'],
|
||||
wantText: [
|
||||
'а б в г ґ д е є ж з и і ї й к л м н о',
|
||||
'multiple',
|
||||
'lines',
|
||||
'in',
|
||||
'pre',
|
||||
'п р с т у ф х ц ч ш щ ю я ь',
|
||||
],
|
||||
},
|
||||
{
|
||||
description: 'pre should be on a new line not preceded by a space',
|
||||
html: `<html><body><p>
|
||||
wantAlines: ['+11', '+8', '+5', '+2', '+3', '+r'],
|
||||
wantText: [
|
||||
'а б в г ґ д е є ж з и і ї й к л м н о',
|
||||
'multiple',
|
||||
'lines',
|
||||
'in',
|
||||
'pre',
|
||||
'п р с т у ф х ц ч ш щ ю я ь',
|
||||
],
|
||||
},
|
||||
{
|
||||
description: 'pre should be on a new line not preceded by a space',
|
||||
html: `<html><body><p>
|
||||
1
|
||||
</p><pre>preline
|
||||
</pre></body></html>`,
|
||||
wantAlines: ['+6', '+7'],
|
||||
wantText: [' 1 ', 'preline'],
|
||||
},
|
||||
{
|
||||
description: 'Preserve spaces on the beginning and end of a element',
|
||||
html: '<html><body>Need<span> more </span>space<i> s </i>!<br></body></html>',
|
||||
wantAlines: ['+f*1+3+1'],
|
||||
wantText: ['Need more space s !'],
|
||||
},
|
||||
{
|
||||
description: 'Preserve spaces outside elements',
|
||||
html: '<html><body>Need <span>more</span> space <i>s</i> !<br></body></html>',
|
||||
wantAlines: ['+g*1+1+2'],
|
||||
wantText: ['Need more space s !'],
|
||||
},
|
||||
{
|
||||
description: 'Preserve spaces at the end of an element',
|
||||
html: '<html><body>Need <span>more </span>space <i>s </i>!<br></body></html>',
|
||||
wantAlines: ['+g*1+2+1'],
|
||||
wantText: ['Need more space s !'],
|
||||
},
|
||||
{
|
||||
description: 'Preserve spaces at the start of an element',
|
||||
html: '<html><body>Need<span> more</span> space<i> s</i> !<br></body></html>',
|
||||
wantAlines: ['+f*1+2+2'],
|
||||
wantText: ['Need more space s !'],
|
||||
},
|
||||
wantAlines: ['+6', '+7'],
|
||||
wantText: [' 1 ', 'preline'],
|
||||
},
|
||||
{
|
||||
description: 'Preserve spaces on the beginning and end of a element',
|
||||
html: '<html><body>Need<span> more </span>space<i> s </i>!<br></body></html>',
|
||||
wantAlines: ['+f*1+3+1'],
|
||||
wantText: ['Need more space s !'],
|
||||
},
|
||||
{
|
||||
description: 'Preserve spaces outside elements',
|
||||
html: '<html><body>Need <span>more</span> space <i>s</i> !<br></body></html>',
|
||||
wantAlines: ['+g*1+1+2'],
|
||||
wantText: ['Need more space s !'],
|
||||
},
|
||||
{
|
||||
description: 'Preserve spaces at the end of an element',
|
||||
html: '<html><body>Need <span>more </span>space <i>s </i>!<br></body></html>',
|
||||
wantAlines: ['+g*1+2+1'],
|
||||
wantText: ['Need more space s !'],
|
||||
},
|
||||
{
|
||||
description: 'Preserve spaces at the start of an element',
|
||||
html: '<html><body>Need<span> more</span> space<i> s</i> !<br></body></html>',
|
||||
wantAlines: ['+f*1+2+2'],
|
||||
wantText: ['Need more space s !'],
|
||||
},
|
||||
];
|
||||
|
||||
describe(__filename, function () {
|
||||
for (const tc of testCases) {
|
||||
describe(tc.description, function () {
|
||||
let apool;
|
||||
let result;
|
||||
|
||||
before(async function () {
|
||||
if (tc.disabled) return this.skip();
|
||||
const {window: {document}} = new jsdom.JSDOM(tc.html);
|
||||
apool = new AttributePool();
|
||||
// To reduce test fragility, the attribute pool is seeded with `knownAttribs`, and all
|
||||
// attributes in `tc.wantAlines` must be in `knownAttribs`. (This guarantees that attribute
|
||||
// numbers do not change if the attribute processing code changes.)
|
||||
for (const attrib of knownAttribs) apool.putAttrib(attrib);
|
||||
for (const aline of tc.wantAlines) {
|
||||
for (const op of Changeset.deserializeOps(aline)) {
|
||||
for (const n of attributes.decodeAttribString(op.attribs)) {
|
||||
assert(n < knownAttribs.length);
|
||||
}
|
||||
}
|
||||
}
|
||||
const cc = contentcollector.makeContentCollector(true, null, apool);
|
||||
cc.collectContent(document.body);
|
||||
result = cc.finish();
|
||||
});
|
||||
|
||||
it('text matches', async function () {
|
||||
assert.deepEqual(result.lines, tc.wantText);
|
||||
});
|
||||
|
||||
it('alines match', async function () {
|
||||
assert.deepEqual(result.lineAttribs, tc.wantAlines);
|
||||
});
|
||||
|
||||
it('attributes are sorted in canonical order', async function () {
|
||||
const gotAttribs = [];
|
||||
const wantAttribs = [];
|
||||
for (const aline of result.lineAttribs) {
|
||||
const gotAlineAttribs = [];
|
||||
gotAttribs.push(gotAlineAttribs);
|
||||
const wantAlineAttribs = [];
|
||||
wantAttribs.push(wantAlineAttribs);
|
||||
for (const op of Changeset.deserializeOps(aline)) {
|
||||
const gotOpAttribs = [...attributes.attribsFromString(op.attribs, apool)];
|
||||
gotAlineAttribs.push(gotOpAttribs);
|
||||
wantAlineAttribs.push(attributes.sort([...gotOpAttribs]));
|
||||
}
|
||||
}
|
||||
assert.deepEqual(gotAttribs, wantAttribs);
|
||||
});
|
||||
});
|
||||
}
|
||||
for (const tc of testCases) {
|
||||
describe(tc.description, function () {
|
||||
let apool;
|
||||
let result;
|
||||
before(async function () {
|
||||
if (tc.disabled)
|
||||
return this.skip();
|
||||
const { window: { document } } = new jsdom.JSDOM(tc.html);
|
||||
apool = new AttributePool();
|
||||
// To reduce test fragility, the attribute pool is seeded with `knownAttribs`, and all
|
||||
// attributes in `tc.wantAlines` must be in `knownAttribs`. (This guarantees that attribute
|
||||
// numbers do not change if the attribute processing code changes.)
|
||||
for (const attrib of knownAttribs)
|
||||
apool.putAttrib(attrib);
|
||||
for (const aline of tc.wantAlines) {
|
||||
for (const op of Changeset.deserializeOps(aline)) {
|
||||
for (const n of attributes.decodeAttribString(op.attribs)) {
|
||||
assert(n < knownAttribs.length);
|
||||
}
|
||||
}
|
||||
}
|
||||
const cc = contentcollector.makeContentCollector(true, null, apool);
|
||||
cc.collectContent(document.body);
|
||||
result = cc.finish();
|
||||
});
|
||||
it('text matches', async function () {
|
||||
assert.deepEqual(result.lines, tc.wantText);
|
||||
});
|
||||
it('alines match', async function () {
|
||||
assert.deepEqual(result.lineAttribs, tc.wantAlines);
|
||||
});
|
||||
it('attributes are sorted in canonical order', async function () {
|
||||
const gotAttribs = [];
|
||||
const wantAttribs = [];
|
||||
for (const aline of result.lineAttribs) {
|
||||
const gotAlineAttribs = [];
|
||||
gotAttribs.push(gotAlineAttribs);
|
||||
const wantAlineAttribs = [];
|
||||
wantAttribs.push(wantAlineAttribs);
|
||||
for (const op of Changeset.deserializeOps(aline)) {
|
||||
const gotOpAttribs = [...attributes.attribsFromString(op.attribs, apool)];
|
||||
gotAlineAttribs.push(gotOpAttribs);
|
||||
wantAlineAttribs.push(attributes.sort([...gotOpAttribs]));
|
||||
}
|
||||
}
|
||||
assert.deepEqual(gotAttribs, wantAttribs);
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
|
@ -1,26 +1,21 @@
|
|||
import * as common from "../common.js";
|
||||
import * as padManager from "../../../node/db/PadManager.js";
|
||||
import * as settings from "../../../node/utils/Settings.js";
|
||||
'use strict';
|
||||
|
||||
const common = require('../common');
|
||||
const padManager = require('../../../node/db/PadManager');
|
||||
const settings = require('../../../node/utils/Settings');
|
||||
|
||||
describe(__filename, function () {
|
||||
let agent;
|
||||
const settingsBackup = {};
|
||||
|
||||
before(async function () {
|
||||
agent = await common.init();
|
||||
settingsBackup.soffice = settings.soffice;
|
||||
await padManager.getPad('testExportPad', 'test content');
|
||||
});
|
||||
|
||||
after(async function () {
|
||||
Object.assign(settings, settingsBackup);
|
||||
});
|
||||
|
||||
it('returns 500 on export error', async function () {
|
||||
settings.soffice = 'false'; // '/bin/false' doesn't work on Windows
|
||||
await agent.get('/p/testExportPad/export/doc')
|
||||
.expect(500);
|
||||
});
|
||||
let agent;
|
||||
const settingsBackup = {};
|
||||
before(async function () {
|
||||
agent = await common.init();
|
||||
settingsBackup.soffice = settings.soffice;
|
||||
await padManager.getPad('testExportPad', 'test content');
|
||||
});
|
||||
after(async function () {
|
||||
Object.assign(settings, settingsBackup);
|
||||
});
|
||||
it('returns 500 on export error', async function () {
|
||||
settings.soffice = 'false'; // '/bin/false' doesn't work on Windows
|
||||
await agent.get('/p/testExportPad/export/doc')
|
||||
.expect(500);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,91 +1,83 @@
|
|||
import assert$0 from "assert";
|
||||
import * as common from "../common.js";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import * as settings from "../../../node/utils/Settings.js";
|
||||
import superagent from "superagent";
|
||||
'use strict';
|
||||
|
||||
const assert = require('assert').strict;
|
||||
const common = require('../common');
|
||||
const fs = require('fs');
|
||||
const assert = assert$0.strict;
|
||||
const fsp = fs.promises;
|
||||
const path = require('path');
|
||||
const settings = require('../../../node/utils/Settings');
|
||||
const superagent = require('superagent');
|
||||
|
||||
describe(__filename, function () {
|
||||
let agent;
|
||||
let backupSettings;
|
||||
let skinDir;
|
||||
let wantCustomIcon;
|
||||
let wantDefaultIcon;
|
||||
let wantSkinIcon;
|
||||
|
||||
before(async function () {
|
||||
agent = await common.init();
|
||||
wantCustomIcon = await fsp.readFile(path.join(__dirname, 'favicon-test-custom.png'));
|
||||
wantDefaultIcon = await fsp.readFile(path.join(settings.root, 'src', 'static', 'favicon.ico'));
|
||||
wantSkinIcon = await fsp.readFile(path.join(__dirname, 'favicon-test-skin.png'));
|
||||
});
|
||||
|
||||
beforeEach(async function () {
|
||||
backupSettings = {...settings};
|
||||
skinDir = await fsp.mkdtemp(path.join(settings.root, 'src', 'static', 'skins', 'test-'));
|
||||
settings.skinName = path.basename(skinDir);
|
||||
});
|
||||
|
||||
afterEach(async function () {
|
||||
delete settings.favicon;
|
||||
delete settings.skinName;
|
||||
Object.assign(settings, backupSettings);
|
||||
try {
|
||||
// TODO: The {recursive: true} option wasn't added to fsp.rmdir() until Node.js v12.10.0 so we
|
||||
// can't rely on it until support for Node.js v10 is dropped.
|
||||
await fsp.unlink(path.join(skinDir, 'favicon.ico'));
|
||||
await fsp.rmdir(skinDir, {recursive: true});
|
||||
} catch (err) { /* intentionally ignored */ }
|
||||
});
|
||||
|
||||
it('uses custom favicon if set (relative pathname)', async function () {
|
||||
settings.favicon =
|
||||
path.relative(settings.root, path.join(__dirname, 'favicon-test-custom.png'));
|
||||
assert(!path.isAbsolute(settings.favicon));
|
||||
const {body: gotIcon} = await agent.get('/favicon.ico')
|
||||
.accept('png').buffer(true).parse(superagent.parse.image)
|
||||
.expect(200);
|
||||
assert(gotIcon.equals(wantCustomIcon));
|
||||
});
|
||||
|
||||
it('uses custom favicon if set (absolute pathname)', async function () {
|
||||
settings.favicon = path.join(__dirname, 'favicon-test-custom.png');
|
||||
assert(path.isAbsolute(settings.favicon));
|
||||
const {body: gotIcon} = await agent.get('/favicon.ico')
|
||||
.accept('png').buffer(true).parse(superagent.parse.image)
|
||||
.expect(200);
|
||||
assert(gotIcon.equals(wantCustomIcon));
|
||||
});
|
||||
|
||||
it('falls back if custom favicon is missing', async function () {
|
||||
// The previous default for settings.favicon was 'favicon.ico', so many users will continue to
|
||||
// have that in their settings.json for a long time. There is unlikely to be a favicon at
|
||||
// path.resolve(settings.root, 'favicon.ico'), so this test ensures that 'favicon.ico' won't be
|
||||
// a problem for those users.
|
||||
settings.favicon = 'favicon.ico';
|
||||
const {body: gotIcon} = await agent.get('/favicon.ico')
|
||||
.accept('png').buffer(true).parse(superagent.parse.image)
|
||||
.expect(200);
|
||||
assert(gotIcon.equals(wantDefaultIcon));
|
||||
});
|
||||
|
||||
it('uses skin favicon if present', async function () {
|
||||
await fsp.writeFile(path.join(skinDir, 'favicon.ico'), wantSkinIcon);
|
||||
settings.favicon = null;
|
||||
const {body: gotIcon} = await agent.get('/favicon.ico')
|
||||
.accept('png').buffer(true).parse(superagent.parse.image)
|
||||
.expect(200);
|
||||
assert(gotIcon.equals(wantSkinIcon));
|
||||
});
|
||||
|
||||
it('falls back to default favicon', async function () {
|
||||
settings.favicon = null;
|
||||
const {body: gotIcon} = await agent.get('/favicon.ico')
|
||||
.accept('png').buffer(true).parse(superagent.parse.image)
|
||||
.expect(200);
|
||||
assert(gotIcon.equals(wantDefaultIcon));
|
||||
});
|
||||
let agent;
|
||||
let backupSettings;
|
||||
let skinDir;
|
||||
let wantCustomIcon;
|
||||
let wantDefaultIcon;
|
||||
let wantSkinIcon;
|
||||
before(async function () {
|
||||
agent = await common.init();
|
||||
wantCustomIcon = await fsp.readFile(path.join(__dirname, 'favicon-test-custom.png'));
|
||||
wantDefaultIcon = await fsp.readFile(path.join(settings.root, 'src', 'static', 'favicon.ico'));
|
||||
wantSkinIcon = await fsp.readFile(path.join(__dirname, 'favicon-test-skin.png'));
|
||||
});
|
||||
beforeEach(async function () {
|
||||
backupSettings = { ...settings };
|
||||
skinDir = await fsp.mkdtemp(path.join(settings.root, 'src', 'static', 'skins', 'test-'));
|
||||
settings.skinName = path.basename(skinDir);
|
||||
});
|
||||
afterEach(async function () {
|
||||
delete settings.favicon;
|
||||
delete settings.skinName;
|
||||
Object.assign(settings, backupSettings);
|
||||
try {
|
||||
// TODO: The {recursive: true} option wasn't added to fsp.rmdir() until Node.js v12.10.0 so we
|
||||
// can't rely on it until support for Node.js v10 is dropped.
|
||||
await fsp.unlink(path.join(skinDir, 'favicon.ico'));
|
||||
await fsp.rmdir(skinDir, { recursive: true });
|
||||
}
|
||||
catch (err) { /* intentionally ignored */ }
|
||||
});
|
||||
it('uses custom favicon if set (relative pathname)', async function () {
|
||||
settings.favicon =
|
||||
path.relative(settings.root, path.join(__dirname, 'favicon-test-custom.png'));
|
||||
assert(!path.isAbsolute(settings.favicon));
|
||||
const { body: gotIcon } = await agent.get('/favicon.ico')
|
||||
.accept('png').buffer(true).parse(superagent.parse.image)
|
||||
.expect(200);
|
||||
assert(gotIcon.equals(wantCustomIcon));
|
||||
});
|
||||
it('uses custom favicon if set (absolute pathname)', async function () {
|
||||
settings.favicon = path.join(__dirname, 'favicon-test-custom.png');
|
||||
assert(path.isAbsolute(settings.favicon));
|
||||
const { body: gotIcon } = await agent.get('/favicon.ico')
|
||||
.accept('png').buffer(true).parse(superagent.parse.image)
|
||||
.expect(200);
|
||||
assert(gotIcon.equals(wantCustomIcon));
|
||||
});
|
||||
it('falls back if custom favicon is missing', async function () {
|
||||
// The previous default for settings.favicon was 'favicon.ico', so many users will continue to
|
||||
// have that in their settings.json for a long time. There is unlikely to be a favicon at
|
||||
// path.resolve(settings.root, 'favicon.ico'), so this test ensures that 'favicon.ico' won't be
|
||||
// a problem for those users.
|
||||
settings.favicon = 'favicon.ico';
|
||||
const { body: gotIcon } = await agent.get('/favicon.ico')
|
||||
.accept('png').buffer(true).parse(superagent.parse.image)
|
||||
.expect(200);
|
||||
assert(gotIcon.equals(wantDefaultIcon));
|
||||
});
|
||||
it('uses skin favicon if present', async function () {
|
||||
await fsp.writeFile(path.join(skinDir, 'favicon.ico'), wantSkinIcon);
|
||||
settings.favicon = null;
|
||||
const { body: gotIcon } = await agent.get('/favicon.ico')
|
||||
.accept('png').buffer(true).parse(superagent.parse.image)
|
||||
.expect(200);
|
||||
assert(gotIcon.equals(wantSkinIcon));
|
||||
});
|
||||
it('falls back to default favicon', async function () {
|
||||
settings.favicon = null;
|
||||
const { body: gotIcon } = await agent.get('/favicon.ico')
|
||||
.accept('png').buffer(true).parse(superagent.parse.image)
|
||||
.expect(200);
|
||||
assert(gotIcon.equals(wantDefaultIcon));
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,56 +1,48 @@
|
|||
import assert$0 from "assert";
|
||||
import * as common from "../common.js";
|
||||
import * as settings from "../../../node/utils/Settings.js";
|
||||
import superagent from "superagent";
|
||||
'use strict';
|
||||
|
||||
const assert = require('assert').strict;
|
||||
const common = require('../common');
|
||||
const settings = require('../../../node/utils/Settings');
|
||||
const superagent = require('superagent');
|
||||
|
||||
const assert = assert$0.strict;
|
||||
describe(__filename, function () {
|
||||
let agent;
|
||||
const backup = {};
|
||||
|
||||
const getHealth = () => agent.get('/health')
|
||||
.accept('application/health+json')
|
||||
.buffer(true)
|
||||
.parse(superagent.parse['application/json'])
|
||||
.expect(200)
|
||||
.expect((res) => assert.equal(res.type, 'application/health+json'));
|
||||
|
||||
before(async function () {
|
||||
agent = await common.init();
|
||||
});
|
||||
|
||||
beforeEach(async function () {
|
||||
backup.settings = {};
|
||||
for (const setting of ['requireAuthentication', 'requireAuthorization']) {
|
||||
backup.settings[setting] = settings[setting];
|
||||
}
|
||||
});
|
||||
|
||||
afterEach(async function () {
|
||||
Object.assign(settings, backup.settings);
|
||||
});
|
||||
|
||||
it('/health works', async function () {
|
||||
const res = await getHealth();
|
||||
assert.equal(res.body.status, 'pass');
|
||||
assert.equal(res.body.releaseId, settings.getEpVersion());
|
||||
});
|
||||
|
||||
it('auth is not required', async function () {
|
||||
settings.requireAuthentication = true;
|
||||
settings.requireAuthorization = true;
|
||||
const res = await getHealth();
|
||||
assert.equal(res.body.status, 'pass');
|
||||
});
|
||||
|
||||
// We actually want to test that no express-session state is created, but that is difficult to do
|
||||
// without intrusive changes or unpleasant ueberdb digging. Instead, we assume that the lack of a
|
||||
// cookie means that no express-session state was created (how would express-session look up the
|
||||
// session state if no ID was returned to the client?).
|
||||
it('no cookie is returned', async function () {
|
||||
const res = await getHealth();
|
||||
const cookie = res.headers['set-cookie'];
|
||||
assert(cookie == null, `unexpected Set-Cookie: ${cookie}`);
|
||||
});
|
||||
let agent;
|
||||
const backup = {};
|
||||
const getHealth = () => agent.get('/health')
|
||||
.accept('application/health+json')
|
||||
.buffer(true)
|
||||
.parse(superagent.parse['application/json'])
|
||||
.expect(200)
|
||||
.expect((res) => assert.equal(res.type, 'application/health+json'));
|
||||
before(async function () {
|
||||
agent = await common.init();
|
||||
});
|
||||
beforeEach(async function () {
|
||||
backup.settings = {};
|
||||
for (const setting of ['requireAuthentication', 'requireAuthorization']) {
|
||||
backup.settings[setting] = settings[setting];
|
||||
}
|
||||
});
|
||||
afterEach(async function () {
|
||||
Object.assign(settings, backup.settings);
|
||||
});
|
||||
it('/health works', async function () {
|
||||
const res = await getHealth();
|
||||
assert.equal(res.body.status, 'pass');
|
||||
assert.equal(res.body.releaseId, settings.getEpVersion());
|
||||
});
|
||||
it('auth is not required', async function () {
|
||||
settings.requireAuthentication = true;
|
||||
settings.requireAuthorization = true;
|
||||
const res = await getHealth();
|
||||
assert.equal(res.body.status, 'pass');
|
||||
});
|
||||
// We actually want to test that no express-session state is created, but that is difficult to do
|
||||
// without intrusive changes or unpleasant ueberdb digging. Instead, we assume that the lack of a
|
||||
// cookie means that no express-session state was created (how would express-session look up the
|
||||
// session state if no ID was returned to the client?).
|
||||
it('no cookie is returned', async function () {
|
||||
const res = await getHealth();
|
||||
const cookie = res.headers['set-cookie'];
|
||||
assert(cookie == null, `unexpected Set-Cookie: ${cookie}`);
|
||||
});
|
||||
});
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,171 +1,159 @@
|
|||
import assert$0 from "assert";
|
||||
import * as common from "../common.js";
|
||||
import * as padManager from "../../../node/db/PadManager.js";
|
||||
import * as plugins from "../../../static/js/pluginfw/plugin_defs.js";
|
||||
import * as readOnlyManager from "../../../node/db/ReadOnlyManager.js";
|
||||
'use strict';
|
||||
|
||||
const assert = require('assert').strict;
|
||||
const common = require('../common');
|
||||
const padManager = require('../../../node/db/PadManager');
|
||||
const plugins = require('../../../static/js/pluginfw/plugin_defs');
|
||||
const readOnlyManager = require('../../../node/db/ReadOnlyManager');
|
||||
|
||||
const assert = assert$0.strict;
|
||||
describe(__filename, function () {
|
||||
let agent;
|
||||
let pad;
|
||||
let padId;
|
||||
let roPadId;
|
||||
let rev;
|
||||
let socket;
|
||||
let roSocket;
|
||||
const backups = {};
|
||||
|
||||
before(async function () {
|
||||
agent = await common.init();
|
||||
});
|
||||
|
||||
beforeEach(async function () {
|
||||
backups.hooks = {handleMessageSecurity: plugins.hooks.handleMessageSecurity};
|
||||
plugins.hooks.handleMessageSecurity = [];
|
||||
padId = common.randomString();
|
||||
assert(!await padManager.doesPadExist(padId));
|
||||
pad = await padManager.getPad(padId, 'dummy text\n');
|
||||
await pad.setText('\n'); // Make sure the pad is created.
|
||||
assert.equal(pad.text(), '\n');
|
||||
let res = await agent.get(`/p/${padId}`).expect(200);
|
||||
socket = await common.connect(res);
|
||||
const {type, data: clientVars} = await common.handshake(socket, padId);
|
||||
assert.equal(type, 'CLIENT_VARS');
|
||||
rev = clientVars.collab_client_vars.rev;
|
||||
|
||||
roPadId = await readOnlyManager.getReadOnlyId(padId);
|
||||
res = await agent.get(`/p/${roPadId}`).expect(200);
|
||||
roSocket = await common.connect(res);
|
||||
await common.handshake(roSocket, roPadId);
|
||||
});
|
||||
|
||||
afterEach(async function () {
|
||||
Object.assign(plugins.hooks, backups.hooks);
|
||||
if (socket != null) socket.close();
|
||||
socket = null;
|
||||
if (roSocket != null) roSocket.close();
|
||||
roSocket = null;
|
||||
if (pad != null) await pad.remove();
|
||||
pad = null;
|
||||
});
|
||||
|
||||
describe('CHANGESET_REQ', function () {
|
||||
it('users are unable to read changesets from other pads', async function () {
|
||||
const otherPadId = `${padId}other`;
|
||||
assert(!await padManager.doesPadExist(otherPadId));
|
||||
const otherPad = await padManager.getPad(otherPadId, 'other text\n');
|
||||
try {
|
||||
await otherPad.setText('other text\n');
|
||||
const resP = common.waitForSocketEvent(roSocket, 'message');
|
||||
await common.sendMessage(roSocket, {
|
||||
component: 'pad',
|
||||
padId: otherPadId, // The server should ignore this.
|
||||
type: 'CHANGESET_REQ',
|
||||
data: {
|
||||
granularity: 1,
|
||||
start: 0,
|
||||
requestID: 'requestId',
|
||||
},
|
||||
let agent;
|
||||
let pad;
|
||||
let padId;
|
||||
let roPadId;
|
||||
let rev;
|
||||
let socket;
|
||||
let roSocket;
|
||||
const backups = {};
|
||||
before(async function () {
|
||||
agent = await common.init();
|
||||
});
|
||||
beforeEach(async function () {
|
||||
backups.hooks = { handleMessageSecurity: plugins.hooks.handleMessageSecurity };
|
||||
plugins.hooks.handleMessageSecurity = [];
|
||||
padId = common.randomString();
|
||||
assert(!await padManager.doesPadExist(padId));
|
||||
pad = await padManager.getPad(padId, 'dummy text\n');
|
||||
await pad.setText('\n'); // Make sure the pad is created.
|
||||
assert.equal(pad.text(), '\n');
|
||||
let res = await agent.get(`/p/${padId}`).expect(200);
|
||||
socket = await common.connect(res);
|
||||
const { type, data: clientVars } = await common.handshake(socket, padId);
|
||||
assert.equal(type, 'CLIENT_VARS');
|
||||
rev = clientVars.collab_client_vars.rev;
|
||||
roPadId = await readOnlyManager.getReadOnlyId(padId);
|
||||
res = await agent.get(`/p/${roPadId}`).expect(200);
|
||||
roSocket = await common.connect(res);
|
||||
await common.handshake(roSocket, roPadId);
|
||||
});
|
||||
afterEach(async function () {
|
||||
Object.assign(plugins.hooks, backups.hooks);
|
||||
if (socket != null)
|
||||
socket.close();
|
||||
socket = null;
|
||||
if (roSocket != null)
|
||||
roSocket.close();
|
||||
roSocket = null;
|
||||
if (pad != null)
|
||||
await pad.remove();
|
||||
pad = null;
|
||||
});
|
||||
describe('CHANGESET_REQ', function () {
|
||||
it('users are unable to read changesets from other pads', async function () {
|
||||
const otherPadId = `${padId}other`;
|
||||
assert(!await padManager.doesPadExist(otherPadId));
|
||||
const otherPad = await padManager.getPad(otherPadId, 'other text\n');
|
||||
try {
|
||||
await otherPad.setText('other text\n');
|
||||
const resP = common.waitForSocketEvent(roSocket, 'message');
|
||||
await common.sendMessage(roSocket, {
|
||||
component: 'pad',
|
||||
padId: otherPadId,
|
||||
type: 'CHANGESET_REQ',
|
||||
data: {
|
||||
granularity: 1,
|
||||
start: 0,
|
||||
requestID: 'requestId',
|
||||
},
|
||||
});
|
||||
const res = await resP;
|
||||
assert.equal(res.type, 'CHANGESET_REQ');
|
||||
assert.equal(res.data.requestID, 'requestId');
|
||||
// Should match padId's text, not otherPadId's text.
|
||||
assert.match(res.data.forwardsChangesets[0], /^[^$]*\$dummy text\n/);
|
||||
}
|
||||
finally {
|
||||
await otherPad.remove();
|
||||
}
|
||||
});
|
||||
const res = await resP;
|
||||
assert.equal(res.type, 'CHANGESET_REQ');
|
||||
assert.equal(res.data.requestID, 'requestId');
|
||||
// Should match padId's text, not otherPadId's text.
|
||||
assert.match(res.data.forwardsChangesets[0], /^[^$]*\$dummy text\n/);
|
||||
} finally {
|
||||
await otherPad.remove();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('USER_CHANGES', function () {
|
||||
const sendUserChanges =
|
||||
async (socket, cs) => await common.sendUserChanges(socket, {baseRev: rev, changeset: cs});
|
||||
const assertAccepted = async (socket, wantRev) => {
|
||||
await common.waitForAcceptCommit(socket, wantRev);
|
||||
rev = wantRev;
|
||||
};
|
||||
const assertRejected = async (socket) => {
|
||||
const msg = await common.waitForSocketEvent(socket, 'message');
|
||||
assert.deepEqual(msg, {disconnect: 'badChangeset'});
|
||||
};
|
||||
|
||||
it('changes are applied', async function () {
|
||||
await Promise.all([
|
||||
assertAccepted(socket, rev + 1),
|
||||
sendUserChanges(socket, 'Z:1>5+5$hello'),
|
||||
]);
|
||||
assert.equal(pad.text(), 'hello\n');
|
||||
describe('USER_CHANGES', function () {
|
||||
const sendUserChanges = async (socket, cs) => await common.sendUserChanges(socket, { baseRev: rev, changeset: cs });
|
||||
const assertAccepted = async (socket, wantRev) => {
|
||||
await common.waitForAcceptCommit(socket, wantRev);
|
||||
rev = wantRev;
|
||||
};
|
||||
const assertRejected = async (socket) => {
|
||||
const msg = await common.waitForSocketEvent(socket, 'message');
|
||||
assert.deepEqual(msg, { disconnect: 'badChangeset' });
|
||||
};
|
||||
it('changes are applied', async function () {
|
||||
await Promise.all([
|
||||
assertAccepted(socket, rev + 1),
|
||||
sendUserChanges(socket, 'Z:1>5+5$hello'),
|
||||
]);
|
||||
assert.equal(pad.text(), 'hello\n');
|
||||
});
|
||||
it('bad changeset is rejected', async function () {
|
||||
await Promise.all([
|
||||
assertRejected(socket),
|
||||
sendUserChanges(socket, 'this is not a valid changeset'),
|
||||
]);
|
||||
});
|
||||
it('retransmission is accepted, has no effect', async function () {
|
||||
const cs = 'Z:1>5+5$hello';
|
||||
await Promise.all([
|
||||
assertAccepted(socket, rev + 1),
|
||||
sendUserChanges(socket, cs),
|
||||
]);
|
||||
--rev;
|
||||
await Promise.all([
|
||||
assertAccepted(socket, rev + 1),
|
||||
sendUserChanges(socket, cs),
|
||||
]);
|
||||
assert.equal(pad.text(), 'hello\n');
|
||||
});
|
||||
it('identity changeset is accepted, has no effect', async function () {
|
||||
await Promise.all([
|
||||
assertAccepted(socket, rev + 1),
|
||||
sendUserChanges(socket, 'Z:1>5+5$hello'),
|
||||
]);
|
||||
await Promise.all([
|
||||
assertAccepted(socket, rev),
|
||||
sendUserChanges(socket, 'Z:6>0$'),
|
||||
]);
|
||||
assert.equal(pad.text(), 'hello\n');
|
||||
});
|
||||
it('non-identity changeset with no net change is accepted, has no effect', async function () {
|
||||
await Promise.all([
|
||||
assertAccepted(socket, rev + 1),
|
||||
sendUserChanges(socket, 'Z:1>5+5$hello'),
|
||||
]);
|
||||
await Promise.all([
|
||||
assertAccepted(socket, rev),
|
||||
sendUserChanges(socket, 'Z:6>0-5+5$hello'),
|
||||
]);
|
||||
assert.equal(pad.text(), 'hello\n');
|
||||
});
|
||||
it('handleMessageSecurity can grant one-time write access', async function () {
|
||||
const cs = 'Z:1>5+5$hello';
|
||||
const errRegEx = /write attempt on read-only pad/;
|
||||
// First try to send a change and verify that it was dropped.
|
||||
await assert.rejects(sendUserChanges(roSocket, cs), errRegEx);
|
||||
// sendUserChanges() waits for message ack, so if the message was accepted then head should
|
||||
// have already incremented by the time we get here.
|
||||
assert.equal(pad.head, rev); // Not incremented.
|
||||
// Now allow the change.
|
||||
plugins.hooks.handleMessageSecurity.push({ hook_fn: () => 'permitOnce' });
|
||||
await Promise.all([
|
||||
assertAccepted(roSocket, rev + 1),
|
||||
sendUserChanges(roSocket, cs),
|
||||
]);
|
||||
assert.equal(pad.text(), 'hello\n');
|
||||
// The next change should be dropped.
|
||||
plugins.hooks.handleMessageSecurity = [];
|
||||
await assert.rejects(sendUserChanges(roSocket, 'Z:6>6=5+6$ world'), errRegEx);
|
||||
assert.equal(pad.head, rev); // Not incremented.
|
||||
assert.equal(pad.text(), 'hello\n');
|
||||
});
|
||||
});
|
||||
|
||||
it('bad changeset is rejected', async function () {
|
||||
await Promise.all([
|
||||
assertRejected(socket),
|
||||
sendUserChanges(socket, 'this is not a valid changeset'),
|
||||
]);
|
||||
});
|
||||
|
||||
it('retransmission is accepted, has no effect', async function () {
|
||||
const cs = 'Z:1>5+5$hello';
|
||||
await Promise.all([
|
||||
assertAccepted(socket, rev + 1),
|
||||
sendUserChanges(socket, cs),
|
||||
]);
|
||||
--rev;
|
||||
await Promise.all([
|
||||
assertAccepted(socket, rev + 1),
|
||||
sendUserChanges(socket, cs),
|
||||
]);
|
||||
assert.equal(pad.text(), 'hello\n');
|
||||
});
|
||||
|
||||
it('identity changeset is accepted, has no effect', async function () {
|
||||
await Promise.all([
|
||||
assertAccepted(socket, rev + 1),
|
||||
sendUserChanges(socket, 'Z:1>5+5$hello'),
|
||||
]);
|
||||
await Promise.all([
|
||||
assertAccepted(socket, rev),
|
||||
sendUserChanges(socket, 'Z:6>0$'),
|
||||
]);
|
||||
assert.equal(pad.text(), 'hello\n');
|
||||
});
|
||||
|
||||
it('non-identity changeset with no net change is accepted, has no effect', async function () {
|
||||
await Promise.all([
|
||||
assertAccepted(socket, rev + 1),
|
||||
sendUserChanges(socket, 'Z:1>5+5$hello'),
|
||||
]);
|
||||
await Promise.all([
|
||||
assertAccepted(socket, rev),
|
||||
sendUserChanges(socket, 'Z:6>0-5+5$hello'),
|
||||
]);
|
||||
assert.equal(pad.text(), 'hello\n');
|
||||
});
|
||||
|
||||
it('handleMessageSecurity can grant one-time write access', async function () {
|
||||
const cs = 'Z:1>5+5$hello';
|
||||
const errRegEx = /write attempt on read-only pad/;
|
||||
// First try to send a change and verify that it was dropped.
|
||||
await assert.rejects(sendUserChanges(roSocket, cs), errRegEx);
|
||||
// sendUserChanges() waits for message ack, so if the message was accepted then head should
|
||||
// have already incremented by the time we get here.
|
||||
assert.equal(pad.head, rev); // Not incremented.
|
||||
|
||||
// Now allow the change.
|
||||
plugins.hooks.handleMessageSecurity.push({hook_fn: () => 'permitOnce'});
|
||||
await Promise.all([
|
||||
assertAccepted(roSocket, rev + 1),
|
||||
sendUserChanges(roSocket, cs),
|
||||
]);
|
||||
assert.equal(pad.text(), 'hello\n');
|
||||
|
||||
// The next change should be dropped.
|
||||
plugins.hooks.handleMessageSecurity = [];
|
||||
await assert.rejects(sendUserChanges(roSocket, 'Z:6>6=5+6$ world'), errRegEx);
|
||||
assert.equal(pad.head, rev); // Not incremented.
|
||||
assert.equal(pad.text(), 'hello\n');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,43 +1,38 @@
|
|||
import assert$0 from "assert";
|
||||
import { padutils } from "../../../static/js/pad_utils.js";
|
||||
'use strict';
|
||||
|
||||
const assert = require('assert').strict;
|
||||
const {padutils} = require('../../../static/js/pad_utils');
|
||||
|
||||
const assert = assert$0.strict;
|
||||
describe(__filename, function () {
|
||||
describe('warnDeprecated', function () {
|
||||
const {warnDeprecated} = padutils;
|
||||
const backups = {};
|
||||
|
||||
before(async function () {
|
||||
backups.logger = warnDeprecated.logger;
|
||||
describe('warnDeprecated', function () {
|
||||
const { warnDeprecated } = padutils;
|
||||
const backups = {};
|
||||
before(async function () {
|
||||
backups.logger = warnDeprecated.logger;
|
||||
});
|
||||
afterEach(async function () {
|
||||
warnDeprecated.logger = backups.logger;
|
||||
delete warnDeprecated._rl; // Reset internal rate limiter state.
|
||||
});
|
||||
it('includes the stack', async function () {
|
||||
let got;
|
||||
warnDeprecated.logger = { warn: (stack) => got = stack };
|
||||
warnDeprecated();
|
||||
assert(got.includes(__filename));
|
||||
});
|
||||
it('rate limited', async function () {
|
||||
let got = 0;
|
||||
warnDeprecated.logger = { warn: () => ++got };
|
||||
warnDeprecated(); // Initialize internal rate limiter state.
|
||||
const { period } = warnDeprecated._rl;
|
||||
got = 0;
|
||||
const testCases = [[0, 1], [0, 1], [period - 1, 1], [period, 2]];
|
||||
for (const [now, want] of testCases) { // In a loop so that the stack trace is the same.
|
||||
warnDeprecated._rl.now = () => now;
|
||||
warnDeprecated();
|
||||
assert.equal(got, want);
|
||||
}
|
||||
warnDeprecated(); // Should have a different stack trace.
|
||||
assert.equal(got, testCases[testCases.length - 1][1] + 1);
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(async function () {
|
||||
warnDeprecated.logger = backups.logger;
|
||||
delete warnDeprecated._rl; // Reset internal rate limiter state.
|
||||
});
|
||||
|
||||
it('includes the stack', async function () {
|
||||
let got;
|
||||
warnDeprecated.logger = {warn: (stack) => got = stack};
|
||||
warnDeprecated();
|
||||
assert(got.includes(__filename));
|
||||
});
|
||||
|
||||
it('rate limited', async function () {
|
||||
let got = 0;
|
||||
warnDeprecated.logger = {warn: () => ++got};
|
||||
warnDeprecated(); // Initialize internal rate limiter state.
|
||||
const {period} = warnDeprecated._rl;
|
||||
got = 0;
|
||||
const testCases = [[0, 1], [0, 1], [period - 1, 1], [period, 2]];
|
||||
for (const [now, want] of testCases) { // In a loop so that the stack trace is the same.
|
||||
warnDeprecated._rl.now = () => now;
|
||||
warnDeprecated();
|
||||
assert.equal(got, want);
|
||||
}
|
||||
warnDeprecated(); // Should have a different stack trace.
|
||||
assert.equal(got, testCases[testCases.length - 1][1] + 1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,24 +1,20 @@
|
|||
import * as common from "../common.js";
|
||||
import assertLegacy from "../assert-legacy.js";
|
||||
'use strict';
|
||||
|
||||
const common = require('../common');
|
||||
const assert = require('../assert-legacy').strict;
|
||||
|
||||
const assert = assertLegacy.strict;
|
||||
let agent;
|
||||
|
||||
describe(__filename, function () {
|
||||
before(async function () {
|
||||
agent = await common.init();
|
||||
});
|
||||
|
||||
it('supports pads with spaces, regression test for #4883', async function () {
|
||||
await agent.get('/p/pads with spaces')
|
||||
.expect(302)
|
||||
.expect('location', 'pads_with_spaces');
|
||||
});
|
||||
|
||||
it('supports pads with spaces and query, regression test for #4883', async function () {
|
||||
await agent.get('/p/pads with spaces?showChat=true&noColors=false')
|
||||
.expect(302)
|
||||
.expect('location', 'pads_with_spaces?showChat=true&noColors=false');
|
||||
});
|
||||
before(async function () {
|
||||
agent = await common.init();
|
||||
});
|
||||
it('supports pads with spaces, regression test for #4883', async function () {
|
||||
await agent.get('/p/pads with spaces')
|
||||
.expect(302)
|
||||
.expect('location', 'pads_with_spaces');
|
||||
});
|
||||
it('supports pads with spaces and query, regression test for #4883', async function () {
|
||||
await agent.get('/p/pads with spaces?showChat=true&noColors=false')
|
||||
.expect(302)
|
||||
.expect('location', 'pads_with_spaces?showChat=true&noColors=false');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,85 +1,76 @@
|
|||
const assert = require('assert').strict;
|
||||
const promises = require('../../../node/utils/promises');
|
||||
|
||||
import assert$0 from "assert";
|
||||
import * as promises from "../../../node/utils/promises.js";
|
||||
const assert = assert$0.strict;
|
||||
describe(__filename, function () {
|
||||
describe('promises.timesLimit', function () {
|
||||
let wantIndex = 0;
|
||||
const testPromises = [];
|
||||
const makePromise = (index) => {
|
||||
// Make sure index increases by one each time.
|
||||
assert.equal(index, wantIndex++);
|
||||
// Save the resolve callback (so the test can trigger resolution)
|
||||
// and the promise itself (to wait for resolve to take effect).
|
||||
const p = {};
|
||||
const promise = new Promise((resolve) => {
|
||||
p.resolve = resolve;
|
||||
});
|
||||
p.promise = promise;
|
||||
testPromises.push(p);
|
||||
return p.promise;
|
||||
};
|
||||
|
||||
const total = 11;
|
||||
const concurrency = 7;
|
||||
const timesLimitPromise = promises.timesLimit(total, concurrency, makePromise);
|
||||
|
||||
it('honors concurrency', async function () {
|
||||
assert.equal(wantIndex, concurrency);
|
||||
describe('promises.timesLimit', function () {
|
||||
let wantIndex = 0;
|
||||
const testPromises = [];
|
||||
const makePromise = (index) => {
|
||||
// Make sure index increases by one each time.
|
||||
assert.equal(index, wantIndex++);
|
||||
// Save the resolve callback (so the test can trigger resolution)
|
||||
// and the promise itself (to wait for resolve to take effect).
|
||||
const p = {};
|
||||
const promise = new Promise((resolve) => {
|
||||
p.resolve = resolve;
|
||||
});
|
||||
p.promise = promise;
|
||||
testPromises.push(p);
|
||||
return p.promise;
|
||||
};
|
||||
const total = 11;
|
||||
const concurrency = 7;
|
||||
const timesLimitPromise = promises.timesLimit(total, concurrency, makePromise);
|
||||
it('honors concurrency', async function () {
|
||||
assert.equal(wantIndex, concurrency);
|
||||
});
|
||||
it('creates another when one completes', async function () {
|
||||
const { promise, resolve } = testPromises.shift();
|
||||
resolve();
|
||||
await promise;
|
||||
assert.equal(wantIndex, concurrency + 1);
|
||||
});
|
||||
it('creates the expected total number of promises', async function () {
|
||||
while (testPromises.length > 0) {
|
||||
// Resolve them in random order to ensure that the resolution order doesn't matter.
|
||||
const i = Math.floor(Math.random() * Math.floor(testPromises.length));
|
||||
const { promise, resolve } = testPromises.splice(i, 1)[0];
|
||||
resolve();
|
||||
await promise;
|
||||
}
|
||||
assert.equal(wantIndex, total);
|
||||
});
|
||||
it('resolves', async function () {
|
||||
await timesLimitPromise;
|
||||
});
|
||||
it('does not create too many promises if total < concurrency', async function () {
|
||||
wantIndex = 0;
|
||||
assert.equal(testPromises.length, 0);
|
||||
const total = 7;
|
||||
const concurrency = 11;
|
||||
const timesLimitPromise = promises.timesLimit(total, concurrency, makePromise);
|
||||
while (testPromises.length > 0) {
|
||||
const { promise, resolve } = testPromises.pop();
|
||||
resolve();
|
||||
await promise;
|
||||
}
|
||||
await timesLimitPromise;
|
||||
assert.equal(wantIndex, total);
|
||||
});
|
||||
it('accepts total === 0, concurrency > 0', async function () {
|
||||
wantIndex = 0;
|
||||
assert.equal(testPromises.length, 0);
|
||||
await promises.timesLimit(0, concurrency, makePromise);
|
||||
assert.equal(wantIndex, 0);
|
||||
});
|
||||
it('accepts total === 0, concurrency === 0', async function () {
|
||||
wantIndex = 0;
|
||||
assert.equal(testPromises.length, 0);
|
||||
await promises.timesLimit(0, 0, makePromise);
|
||||
assert.equal(wantIndex, 0);
|
||||
});
|
||||
it('rejects total > 0, concurrency === 0', async function () {
|
||||
await assert.rejects(promises.timesLimit(total, 0, makePromise), RangeError);
|
||||
});
|
||||
});
|
||||
|
||||
it('creates another when one completes', async function () {
|
||||
const {promise, resolve} = testPromises.shift();
|
||||
resolve();
|
||||
await promise;
|
||||
assert.equal(wantIndex, concurrency + 1);
|
||||
});
|
||||
|
||||
it('creates the expected total number of promises', async function () {
|
||||
while (testPromises.length > 0) {
|
||||
// Resolve them in random order to ensure that the resolution order doesn't matter.
|
||||
const i = Math.floor(Math.random() * Math.floor(testPromises.length));
|
||||
const {promise, resolve} = testPromises.splice(i, 1)[0];
|
||||
resolve();
|
||||
await promise;
|
||||
}
|
||||
assert.equal(wantIndex, total);
|
||||
});
|
||||
|
||||
it('resolves', async function () {
|
||||
await timesLimitPromise;
|
||||
});
|
||||
|
||||
it('does not create too many promises if total < concurrency', async function () {
|
||||
wantIndex = 0;
|
||||
assert.equal(testPromises.length, 0);
|
||||
const total = 7;
|
||||
const concurrency = 11;
|
||||
const timesLimitPromise = promises.timesLimit(total, concurrency, makePromise);
|
||||
while (testPromises.length > 0) {
|
||||
const {promise, resolve} = testPromises.pop();
|
||||
resolve();
|
||||
await promise;
|
||||
}
|
||||
await timesLimitPromise;
|
||||
assert.equal(wantIndex, total);
|
||||
});
|
||||
|
||||
it('accepts total === 0, concurrency > 0', async function () {
|
||||
wantIndex = 0;
|
||||
assert.equal(testPromises.length, 0);
|
||||
await promises.timesLimit(0, concurrency, makePromise);
|
||||
assert.equal(wantIndex, 0);
|
||||
});
|
||||
|
||||
it('accepts total === 0, concurrency === 0', async function () {
|
||||
wantIndex = 0;
|
||||
assert.equal(testPromises.length, 0);
|
||||
await promises.timesLimit(0, 0, makePromise);
|
||||
assert.equal(wantIndex, 0);
|
||||
});
|
||||
|
||||
it('rejects total > 0, concurrency === 0', async function () {
|
||||
await assert.rejects(promises.timesLimit(total, 0, makePromise), RangeError);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,30 +1,25 @@
|
|||
import * as AuthorManager from "../../../node/db/AuthorManager.js";
|
||||
import assert$0 from "assert";
|
||||
import * as common from "../common.js";
|
||||
import * as db from "../../../node/db/DB.js";
|
||||
'use strict';
|
||||
|
||||
const AuthorManager = require('../../../node/db/AuthorManager');
|
||||
const assert = require('assert').strict;
|
||||
const common = require('../common');
|
||||
const db = require('../../../node/db/DB');
|
||||
|
||||
const assert = assert$0.strict;
|
||||
describe(__filename, function () {
|
||||
let setBackup;
|
||||
|
||||
before(async function () {
|
||||
await common.init();
|
||||
setBackup = db.set;
|
||||
|
||||
db.set = async (...args) => {
|
||||
// delay db.set
|
||||
await new Promise((resolve) => { setTimeout(() => resolve(), 500); });
|
||||
return await setBackup.call(db, ...args);
|
||||
};
|
||||
});
|
||||
|
||||
after(async function () {
|
||||
db.set = setBackup;
|
||||
});
|
||||
|
||||
it('regression test for missing await in createAuthor (#5000)', async function () {
|
||||
const {authorID} = await AuthorManager.createAuthor(); // Should block until db.set() finishes.
|
||||
assert(await AuthorManager.doesAuthorExist(authorID));
|
||||
});
|
||||
let setBackup;
|
||||
before(async function () {
|
||||
await common.init();
|
||||
setBackup = db.set;
|
||||
db.set = async (...args) => {
|
||||
// delay db.set
|
||||
await new Promise((resolve) => { setTimeout(() => resolve(), 500); });
|
||||
return await setBackup.call(db, ...args);
|
||||
};
|
||||
});
|
||||
after(async function () {
|
||||
db.set = setBackup;
|
||||
});
|
||||
it('regression test for missing await in createAuthor (#5000)', async function () {
|
||||
const { authorID } = await AuthorManager.createAuthor(); // Should block until db.set() finishes.
|
||||
assert(await AuthorManager.doesAuthorExist(authorID));
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,96 +1,93 @@
|
|||
import assert$0 from "assert";
|
||||
import path from "path";
|
||||
import sanitizePathname from "../../../node/utils/sanitizePathname.js";
|
||||
'use strict';
|
||||
|
||||
const assert = require('assert').strict;
|
||||
const path = require('path');
|
||||
const sanitizePathname = require('../../../node/utils/sanitizePathname');
|
||||
|
||||
const assert = assert$0.strict;
|
||||
describe(__filename, function () {
|
||||
describe('absolute paths rejected', function () {
|
||||
const testCases = [
|
||||
['posix', '/'],
|
||||
['posix', '/foo'],
|
||||
['win32', '/'],
|
||||
['win32', '\\'],
|
||||
['win32', 'C:/foo'],
|
||||
['win32', 'C:\\foo'],
|
||||
['win32', 'c:/foo'],
|
||||
['win32', 'c:\\foo'],
|
||||
['win32', '/foo'],
|
||||
['win32', '\\foo'],
|
||||
];
|
||||
for (const [platform, p] of testCases) {
|
||||
it(`${platform} ${p}`, async function () {
|
||||
assert.throws(() => sanitizePathname(p, path[platform]), {message: /absolute path/});
|
||||
});
|
||||
}
|
||||
});
|
||||
describe('directory traversal rejected', function () {
|
||||
const testCases = [
|
||||
['posix', '..'],
|
||||
['posix', '../'],
|
||||
['posix', '../foo'],
|
||||
['posix', 'foo/../..'],
|
||||
['win32', '..'],
|
||||
['win32', '../'],
|
||||
['win32', '..\\'],
|
||||
['win32', '../foo'],
|
||||
['win32', '..\\foo'],
|
||||
['win32', 'foo/../..'],
|
||||
['win32', 'foo\\..\\..'],
|
||||
];
|
||||
for (const [platform, p] of testCases) {
|
||||
it(`${platform} ${p}`, async function () {
|
||||
assert.throws(() => sanitizePathname(p, path[platform]), {message: /travers/});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe('accepted paths', function () {
|
||||
const testCases = [
|
||||
['posix', '', '.'],
|
||||
['posix', '.'],
|
||||
['posix', './'],
|
||||
['posix', 'foo'],
|
||||
['posix', 'foo/'],
|
||||
['posix', 'foo/bar/..', 'foo'],
|
||||
['posix', 'foo/bar/../', 'foo/'],
|
||||
['posix', './foo', 'foo'],
|
||||
['posix', 'foo/bar'],
|
||||
['posix', 'foo\\bar'],
|
||||
['posix', '\\foo'],
|
||||
['posix', '..\\foo'],
|
||||
['posix', 'foo/../bar', 'bar'],
|
||||
['posix', 'C:/foo'],
|
||||
['posix', 'C:\\foo'],
|
||||
['win32', '', '.'],
|
||||
['win32', '.'],
|
||||
['win32', './'],
|
||||
['win32', '.\\', './'],
|
||||
['win32', 'foo'],
|
||||
['win32', 'foo/'],
|
||||
['win32', 'foo\\', 'foo/'],
|
||||
['win32', 'foo/bar/..', 'foo'],
|
||||
['win32', 'foo\\bar\\..', 'foo'],
|
||||
['win32', 'foo/bar/../', 'foo/'],
|
||||
['win32', 'foo\\bar\\..\\', 'foo/'],
|
||||
['win32', './foo', 'foo'],
|
||||
['win32', '.\\foo', 'foo'],
|
||||
['win32', 'foo/bar'],
|
||||
['win32', 'foo\\bar', 'foo/bar'],
|
||||
['win32', 'foo/../bar', 'bar'],
|
||||
['win32', 'foo\\..\\bar', 'bar'],
|
||||
['win32', 'foo/..\\bar', 'bar'],
|
||||
['win32', 'foo\\../bar', 'bar'],
|
||||
];
|
||||
for (const [platform, p, tcWant] of testCases) {
|
||||
const want = tcWant == null ? p : tcWant;
|
||||
it(`${platform} ${p || '<empty string>'} -> ${want}`, async function () {
|
||||
assert.equal(sanitizePathname(p, path[platform]), want);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it('default path API', async function () {
|
||||
assert.equal(sanitizePathname('foo'), 'foo');
|
||||
});
|
||||
describe('absolute paths rejected', function () {
|
||||
const testCases = [
|
||||
['posix', '/'],
|
||||
['posix', '/foo'],
|
||||
['win32', '/'],
|
||||
['win32', '\\'],
|
||||
['win32', 'C:/foo'],
|
||||
['win32', 'C:\\foo'],
|
||||
['win32', 'c:/foo'],
|
||||
['win32', 'c:\\foo'],
|
||||
['win32', '/foo'],
|
||||
['win32', '\\foo'],
|
||||
];
|
||||
for (const [platform, p] of testCases) {
|
||||
it(`${platform} ${p}`, async function () {
|
||||
assert.throws(() => sanitizePathname(p, path[platform]), { message: /absolute path/ });
|
||||
});
|
||||
}
|
||||
});
|
||||
describe('directory traversal rejected', function () {
|
||||
const testCases = [
|
||||
['posix', '..'],
|
||||
['posix', '../'],
|
||||
['posix', '../foo'],
|
||||
['posix', 'foo/../..'],
|
||||
['win32', '..'],
|
||||
['win32', '../'],
|
||||
['win32', '..\\'],
|
||||
['win32', '../foo'],
|
||||
['win32', '..\\foo'],
|
||||
['win32', 'foo/../..'],
|
||||
['win32', 'foo\\..\\..'],
|
||||
];
|
||||
for (const [platform, p] of testCases) {
|
||||
it(`${platform} ${p}`, async function () {
|
||||
assert.throws(() => sanitizePathname(p, path[platform]), { message: /travers/ });
|
||||
});
|
||||
}
|
||||
});
|
||||
describe('accepted paths', function () {
|
||||
const testCases = [
|
||||
['posix', '', '.'],
|
||||
['posix', '.'],
|
||||
['posix', './'],
|
||||
['posix', 'foo'],
|
||||
['posix', 'foo/'],
|
||||
['posix', 'foo/bar/..', 'foo'],
|
||||
['posix', 'foo/bar/../', 'foo/'],
|
||||
['posix', './foo', 'foo'],
|
||||
['posix', 'foo/bar'],
|
||||
['posix', 'foo\\bar'],
|
||||
['posix', '\\foo'],
|
||||
['posix', '..\\foo'],
|
||||
['posix', 'foo/../bar', 'bar'],
|
||||
['posix', 'C:/foo'],
|
||||
['posix', 'C:\\foo'],
|
||||
['win32', '', '.'],
|
||||
['win32', '.'],
|
||||
['win32', './'],
|
||||
['win32', '.\\', './'],
|
||||
['win32', 'foo'],
|
||||
['win32', 'foo/'],
|
||||
['win32', 'foo\\', 'foo/'],
|
||||
['win32', 'foo/bar/..', 'foo'],
|
||||
['win32', 'foo\\bar\\..', 'foo'],
|
||||
['win32', 'foo/bar/../', 'foo/'],
|
||||
['win32', 'foo\\bar\\..\\', 'foo/'],
|
||||
['win32', './foo', 'foo'],
|
||||
['win32', '.\\foo', 'foo'],
|
||||
['win32', 'foo/bar'],
|
||||
['win32', 'foo\\bar', 'foo/bar'],
|
||||
['win32', 'foo/../bar', 'bar'],
|
||||
['win32', 'foo\\..\\bar', 'bar'],
|
||||
['win32', 'foo/..\\bar', 'bar'],
|
||||
['win32', 'foo\\../bar', 'bar'],
|
||||
];
|
||||
for (const [platform, p, tcWant] of testCases) {
|
||||
const want = tcWant == null ? p : tcWant;
|
||||
it(`${platform} ${p || '<empty string>'} -> ${want}`, async function () {
|
||||
assert.equal(sanitizePathname(p, path[platform]), want);
|
||||
});
|
||||
}
|
||||
});
|
||||
it('default path API', async function () {
|
||||
assert.equal(sanitizePathname('foo'), 'foo');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,61 +1,60 @@
|
|||
import assert$0 from "assert";
|
||||
import { exportedForTestingOnly } from "../../../node/utils/Settings.js";
|
||||
import path from "path";
|
||||
import process from "process";
|
||||
'use strict';
|
||||
|
||||
const assert = require('assert').strict;
|
||||
const {parseSettings} = require('../../../node/utils/Settings').exportedForTestingOnly;
|
||||
const path = require('path');
|
||||
const process = require('process');
|
||||
|
||||
const assert = assert$0.strict;
|
||||
const { parseSettings } = { exportedForTestingOnly }.exportedForTestingOnly;
|
||||
describe(__filename, function () {
|
||||
describe('parseSettings', function () {
|
||||
let settings;
|
||||
const envVarSubstTestCases = [
|
||||
{name: 'true', val: 'true', var: 'SET_VAR_TRUE', want: true},
|
||||
{name: 'false', val: 'false', var: 'SET_VAR_FALSE', want: false},
|
||||
{name: 'null', val: 'null', var: 'SET_VAR_NULL', want: null},
|
||||
{name: 'undefined', val: 'undefined', var: 'SET_VAR_UNDEFINED', want: undefined},
|
||||
{name: 'number', val: '123', var: 'SET_VAR_NUMBER', want: 123},
|
||||
{name: 'string', val: 'foo', var: 'SET_VAR_STRING', want: 'foo'},
|
||||
{name: 'empty string', val: '', var: 'SET_VAR_EMPTY_STRING', want: ''},
|
||||
];
|
||||
|
||||
before(async function () {
|
||||
for (const tc of envVarSubstTestCases) process.env[tc.var] = tc.val;
|
||||
delete process.env.UNSET_VAR;
|
||||
settings = parseSettings(path.join(__dirname, 'settings.json'), true);
|
||||
assert(settings != null);
|
||||
});
|
||||
|
||||
describe('environment variable substitution', function () {
|
||||
describe('set', function () {
|
||||
for (const tc of envVarSubstTestCases) {
|
||||
it(tc.name, async function () {
|
||||
const obj = settings['environment variable substitution'].set;
|
||||
if (tc.name === 'undefined') {
|
||||
assert(!(tc.name in obj));
|
||||
} else {
|
||||
assert.equal(obj[tc.name], tc.want);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe('unset', function () {
|
||||
it('no default', async function () {
|
||||
const obj = settings['environment variable substitution'].unset;
|
||||
assert.equal(obj['no default'], null);
|
||||
describe('parseSettings', function () {
|
||||
let settings;
|
||||
const envVarSubstTestCases = [
|
||||
{ name: 'true', val: 'true', var: 'SET_VAR_TRUE', want: true },
|
||||
{ name: 'false', val: 'false', var: 'SET_VAR_FALSE', want: false },
|
||||
{ name: 'null', val: 'null', var: 'SET_VAR_NULL', want: null },
|
||||
{ name: 'undefined', val: 'undefined', var: 'SET_VAR_UNDEFINED', want: undefined },
|
||||
{ name: 'number', val: '123', var: 'SET_VAR_NUMBER', want: 123 },
|
||||
{ name: 'string', val: 'foo', var: 'SET_VAR_STRING', want: 'foo' },
|
||||
{ name: 'empty string', val: '', var: 'SET_VAR_EMPTY_STRING', want: '' },
|
||||
];
|
||||
before(async function () {
|
||||
for (const tc of envVarSubstTestCases)
|
||||
process.env[tc.var] = tc.val;
|
||||
delete process.env.UNSET_VAR;
|
||||
settings = parseSettings(path.join(__dirname, 'settings.json'), true);
|
||||
assert(settings != null);
|
||||
});
|
||||
describe('environment variable substitution', function () {
|
||||
describe('set', function () {
|
||||
for (const tc of envVarSubstTestCases) {
|
||||
it(tc.name, async function () {
|
||||
const obj = settings['environment variable substitution'].set;
|
||||
if (tc.name === 'undefined') {
|
||||
assert(!(tc.name in obj));
|
||||
}
|
||||
else {
|
||||
assert.equal(obj[tc.name], tc.want);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
describe('unset', function () {
|
||||
it('no default', async function () {
|
||||
const obj = settings['environment variable substitution'].unset;
|
||||
assert.equal(obj['no default'], null);
|
||||
});
|
||||
for (const tc of envVarSubstTestCases) {
|
||||
it(tc.name, async function () {
|
||||
const obj = settings['environment variable substitution'].unset;
|
||||
if (tc.name === 'undefined') {
|
||||
assert(!(tc.name in obj));
|
||||
}
|
||||
else {
|
||||
assert.equal(obj[tc.name], tc.want);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
for (const tc of envVarSubstTestCases) {
|
||||
it(tc.name, async function () {
|
||||
const obj = settings['environment variable substitution'].unset;
|
||||
if (tc.name === 'undefined') {
|
||||
assert(!(tc.name in obj));
|
||||
} else {
|
||||
assert.equal(obj[tc.name], tc.want);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,426 +1,405 @@
|
|||
import assert$0 from "assert";
|
||||
import * as common from "../common.js";
|
||||
import * as padManager from "../../../node/db/PadManager.js";
|
||||
import * as plugins from "../../../static/js/pluginfw/plugin_defs.js";
|
||||
import * as readOnlyManager from "../../../node/db/ReadOnlyManager.js";
|
||||
import * as settings from "../../../node/utils/Settings.js";
|
||||
import * as socketIoRouter from "../../../node/handler/SocketIORouter.js";
|
||||
'use strict';
|
||||
|
||||
const assert = require('assert').strict;
|
||||
const common = require('../common');
|
||||
const padManager = require('../../../node/db/PadManager');
|
||||
const plugins = require('../../../static/js/pluginfw/plugin_defs');
|
||||
const readOnlyManager = require('../../../node/db/ReadOnlyManager');
|
||||
const settings = require('../../../node/utils/Settings');
|
||||
const socketIoRouter = require('../../../node/handler/SocketIORouter');
|
||||
|
||||
const assert = assert$0.strict;
|
||||
describe(__filename, function () {
|
||||
this.timeout(30000);
|
||||
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: 'admin-password', is_admin: true},
|
||||
user: {password: 'user-password'},
|
||||
this.timeout(30000);
|
||||
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();
|
||||
}
|
||||
}));
|
||||
};
|
||||
assert(socket == null);
|
||||
authorize = () => true;
|
||||
plugins.hooks.authorize = [{hook_fn: (hookName, {req}, cb) => 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 common.connect(res);
|
||||
const clientVars = await common.handshake(socket, 'pad');
|
||||
assert.equal(clientVars.type, 'CLIENT_VARS');
|
||||
});
|
||||
it('!authn !cookie -> ok', async function () {
|
||||
socket = await common.connect(null);
|
||||
const clientVars = await common.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 common.connect(res);
|
||||
const clientVars = await common.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 common.connect(res);
|
||||
const clientVars = await common.handshake(socket, 'pad');
|
||||
assert.equal(clientVars.type, 'CLIENT_VARS');
|
||||
});
|
||||
|
||||
for (const authn of [false, true]) {
|
||||
const desc = authn ? 'authn user' : '!authn anonymous';
|
||||
it(`${desc} read-only /p/pad -> 200, ok`, async function () {
|
||||
const get = (ep) => {
|
||||
let res = agent.get(ep);
|
||||
if (authn) res = res.auth('user', 'user-password');
|
||||
return res.expect(200);
|
||||
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: 'admin-password', is_admin: true },
|
||||
user: { password: 'user-password' },
|
||||
};
|
||||
settings.requireAuthentication = authn;
|
||||
let res = await get('/p/pad');
|
||||
socket = await common.connect(res);
|
||||
let clientVars = await common.handshake(socket, 'pad');
|
||||
assert.equal(clientVars.type, 'CLIENT_VARS');
|
||||
assert.equal(clientVars.data.readonly, false);
|
||||
const readOnlyId = clientVars.data.readOnlyId;
|
||||
assert(readOnlyManager.isReadOnlyId(readOnlyId));
|
||||
socket.close();
|
||||
res = await get(`/p/${readOnlyId}`);
|
||||
socket = await common.connect(res);
|
||||
clientVars = await common.handshake(socket, readOnlyId);
|
||||
assert.equal(clientVars.type, 'CLIENT_VARS');
|
||||
assert.equal(clientVars.data.readonly, true);
|
||||
});
|
||||
}
|
||||
|
||||
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 common.connect(res);
|
||||
const clientVars = await common.handshake(socket, 'pad');
|
||||
assert.equal(clientVars.type, 'CLIENT_VARS');
|
||||
assert(socket == null);
|
||||
authorize = () => true;
|
||||
plugins.hooks.authorize = [{ hook_fn: (hookName, { req }, cb) => cb([authorize(req)]) }];
|
||||
await cleanUpPads();
|
||||
});
|
||||
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 common.connect(res);
|
||||
const clientVars = await common.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 common.connect(res);
|
||||
const message = await common.handshake(socket, 'pad');
|
||||
assert.equal(message.accessStatus, 'deny');
|
||||
});
|
||||
|
||||
it('authn anonymous read-only /p/pad -> 401, error', async function () {
|
||||
settings.requireAuthentication = true;
|
||||
let res = await agent.get('/p/pad').auth('user', 'user-password').expect(200);
|
||||
socket = await common.connect(res);
|
||||
const clientVars = await common.handshake(socket, 'pad');
|
||||
assert.equal(clientVars.type, 'CLIENT_VARS');
|
||||
const readOnlyId = clientVars.data.readOnlyId;
|
||||
assert(readOnlyManager.isReadOnlyId(readOnlyId));
|
||||
socket.close();
|
||||
res = await agent.get(`/p/${readOnlyId}`).expect(401);
|
||||
// Despite the 401, try to read the pad via a socket.io connection anyway.
|
||||
socket = await common.connect(res);
|
||||
const message = await common.handshake(socket, readOnlyId);
|
||||
assert.equal(message.accessStatus, 'deny');
|
||||
});
|
||||
|
||||
it('authn !cookie -> error', async function () {
|
||||
settings.requireAuthentication = true;
|
||||
socket = await common.connect(null);
|
||||
const message = await common.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 common.connect(res);
|
||||
// Accessing /p/other-pad should fail, despite the successful fetch of /p/pad.
|
||||
const message = await common.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', 'user-password').expect(200);
|
||||
socket = await common.connect(res);
|
||||
const clientVars = await common.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', 'user-password').expect(200);
|
||||
socket = await common.connect(res);
|
||||
const clientVars = await common.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', 'user-password').expect(200);
|
||||
socket = await common.connect(res);
|
||||
const clientVars = await common.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', 'user-password').expect(200);
|
||||
socket = await common.connect(res);
|
||||
const message = await common.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 common.connect(res);
|
||||
const message = await common.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 common.connect(res);
|
||||
const message = await common.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 common.connect(res);
|
||||
const clientVars = await common.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 common.connect(res);
|
||||
const clientVars = await common.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 common.connect(res);
|
||||
const message = await common.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 common.connect(res);
|
||||
const message = await common.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 common.connect(res);
|
||||
const clientVars = await common.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 common.connect(res);
|
||||
const clientVars = await common.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 common.connect(res);
|
||||
const message = await common.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', 'user-password').expect(200);
|
||||
socket = await common.connect(res);
|
||||
const message = await common.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 common.connect(res);
|
||||
const message = await common.handshake(socket, 'pad');
|
||||
assert.equal(message.accessStatus, 'deny');
|
||||
});
|
||||
});
|
||||
|
||||
describe('SocketIORouter.js', function () {
|
||||
const Module = class {
|
||||
setSocketIO(io) {}
|
||||
handleConnect(socket) {}
|
||||
handleDisconnect(socket) {}
|
||||
handleMessage(socket, message) {}
|
||||
};
|
||||
|
||||
afterEach(async function () {
|
||||
socketIoRouter.deleteComponent(this.test.fullTitle());
|
||||
socketIoRouter.deleteComponent(`${this.test.fullTitle()} #2`);
|
||||
if (socket)
|
||||
socket.close();
|
||||
socket = null;
|
||||
await cleanUpPads();
|
||||
Object.assign(plugins.hooks, backups.hooks);
|
||||
Object.assign(settings, backups.settings);
|
||||
});
|
||||
|
||||
it('setSocketIO', async function () {
|
||||
let ioServer;
|
||||
socketIoRouter.addComponent(this.test.fullTitle(), new class extends Module {
|
||||
setSocketIO(io) { ioServer = io; }
|
||||
}());
|
||||
assert(ioServer != null);
|
||||
});
|
||||
|
||||
it('handleConnect', async function () {
|
||||
let serverSocket;
|
||||
socketIoRouter.addComponent(this.test.fullTitle(), new class extends Module {
|
||||
handleConnect(socket) { serverSocket = socket; }
|
||||
}());
|
||||
socket = await common.connect();
|
||||
assert(serverSocket != null);
|
||||
});
|
||||
|
||||
it('handleDisconnect', async function () {
|
||||
let resolveConnected;
|
||||
const connected = new Promise((resolve) => resolveConnected = resolve);
|
||||
let resolveDisconnected;
|
||||
const disconnected = new Promise((resolve) => resolveDisconnected = resolve);
|
||||
socketIoRouter.addComponent(this.test.fullTitle(), new class extends Module {
|
||||
handleConnect(socket) {
|
||||
this._socket = socket;
|
||||
resolveConnected();
|
||||
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 common.connect(res);
|
||||
const clientVars = await common.handshake(socket, 'pad');
|
||||
assert.equal(clientVars.type, 'CLIENT_VARS');
|
||||
});
|
||||
it('!authn !cookie -> ok', async function () {
|
||||
socket = await common.connect(null);
|
||||
const clientVars = await common.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 common.connect(res);
|
||||
const clientVars = await common.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 common.connect(res);
|
||||
const clientVars = await common.handshake(socket, 'pad');
|
||||
assert.equal(clientVars.type, 'CLIENT_VARS');
|
||||
});
|
||||
for (const authn of [false, true]) {
|
||||
const desc = authn ? 'authn user' : '!authn anonymous';
|
||||
it(`${desc} read-only /p/pad -> 200, ok`, async function () {
|
||||
const get = (ep) => {
|
||||
let res = agent.get(ep);
|
||||
if (authn)
|
||||
res = res.auth('user', 'user-password');
|
||||
return res.expect(200);
|
||||
};
|
||||
settings.requireAuthentication = authn;
|
||||
let res = await get('/p/pad');
|
||||
socket = await common.connect(res);
|
||||
let clientVars = await common.handshake(socket, 'pad');
|
||||
assert.equal(clientVars.type, 'CLIENT_VARS');
|
||||
assert.equal(clientVars.data.readonly, false);
|
||||
const readOnlyId = clientVars.data.readOnlyId;
|
||||
assert(readOnlyManager.isReadOnlyId(readOnlyId));
|
||||
socket.close();
|
||||
res = await get(`/p/${readOnlyId}`);
|
||||
socket = await common.connect(res);
|
||||
clientVars = await common.handshake(socket, readOnlyId);
|
||||
assert.equal(clientVars.type, 'CLIENT_VARS');
|
||||
assert.equal(clientVars.data.readonly, true);
|
||||
});
|
||||
}
|
||||
handleDisconnect(socket) {
|
||||
assert(socket != null);
|
||||
// There might be lingering disconnect events from sockets created by other tests.
|
||||
if (this._socket == null || socket.id !== this._socket.id) return;
|
||||
assert.equal(socket, this._socket);
|
||||
resolveDisconnected();
|
||||
}
|
||||
}());
|
||||
socket = await common.connect();
|
||||
await connected;
|
||||
socket.close();
|
||||
socket = null;
|
||||
await disconnected;
|
||||
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 common.connect(res);
|
||||
const clientVars = await common.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 common.connect(res);
|
||||
const clientVars = await common.handshake(socket, 'päd');
|
||||
assert.equal(clientVars.type, 'CLIENT_VARS');
|
||||
});
|
||||
});
|
||||
|
||||
it('handleMessage (success)', async function () {
|
||||
let serverSocket;
|
||||
const want = {
|
||||
component: this.test.fullTitle(),
|
||||
foo: {bar: 'asdf'},
|
||||
};
|
||||
let rx;
|
||||
const got = new Promise((resolve) => { rx = resolve; });
|
||||
socketIoRouter.addComponent(this.test.fullTitle(), new class extends Module {
|
||||
handleConnect(socket) { serverSocket = socket; }
|
||||
handleMessage(socket, message) { assert.equal(socket, serverSocket); rx(message); }
|
||||
}());
|
||||
socketIoRouter.addComponent(`${this.test.fullTitle()} #2`, new class extends Module {
|
||||
handleMessage(socket, message) { assert.fail('wrong handler called'); }
|
||||
}());
|
||||
socket = await common.connect();
|
||||
socket.send(want);
|
||||
assert.deepEqual(await got, want);
|
||||
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 common.connect(res);
|
||||
const message = await common.handshake(socket, 'pad');
|
||||
assert.equal(message.accessStatus, 'deny');
|
||||
});
|
||||
it('authn anonymous read-only /p/pad -> 401, error', async function () {
|
||||
settings.requireAuthentication = true;
|
||||
let res = await agent.get('/p/pad').auth('user', 'user-password').expect(200);
|
||||
socket = await common.connect(res);
|
||||
const clientVars = await common.handshake(socket, 'pad');
|
||||
assert.equal(clientVars.type, 'CLIENT_VARS');
|
||||
const readOnlyId = clientVars.data.readOnlyId;
|
||||
assert(readOnlyManager.isReadOnlyId(readOnlyId));
|
||||
socket.close();
|
||||
res = await agent.get(`/p/${readOnlyId}`).expect(401);
|
||||
// Despite the 401, try to read the pad via a socket.io connection anyway.
|
||||
socket = await common.connect(res);
|
||||
const message = await common.handshake(socket, readOnlyId);
|
||||
assert.equal(message.accessStatus, 'deny');
|
||||
});
|
||||
it('authn !cookie -> error', async function () {
|
||||
settings.requireAuthentication = true;
|
||||
socket = await common.connect(null);
|
||||
const message = await common.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 common.connect(res);
|
||||
// Accessing /p/other-pad should fail, despite the successful fetch of /p/pad.
|
||||
const message = await common.handshake(socket, 'other-pad');
|
||||
assert.equal(message.accessStatus, 'deny');
|
||||
});
|
||||
});
|
||||
|
||||
const tx = async (socket, message = {}) => await new Promise((resolve, reject) => {
|
||||
const AckErr = class extends Error {
|
||||
constructor(name, ...args) { super(...args); this.name = name; }
|
||||
};
|
||||
socket.send(message,
|
||||
(errj, val) => errj != null ? reject(new AckErr(errj.name, errj.message)) : resolve(val));
|
||||
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', 'user-password').expect(200);
|
||||
socket = await common.connect(res);
|
||||
const clientVars = await common.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', 'user-password').expect(200);
|
||||
socket = await common.connect(res);
|
||||
const clientVars = await common.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', 'user-password').expect(200);
|
||||
socket = await common.connect(res);
|
||||
const clientVars = await common.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', 'user-password').expect(200);
|
||||
socket = await common.connect(res);
|
||||
const message = await common.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 common.connect(res);
|
||||
const message = await common.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 common.connect(res);
|
||||
const message = await common.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 common.connect(res);
|
||||
const clientVars = await common.handshake(socket, 'pad');
|
||||
assert.equal(clientVars.type, 'CLIENT_VARS');
|
||||
assert.equal(clientVars.data.readonly, true);
|
||||
});
|
||||
});
|
||||
|
||||
it('handleMessage with ack (success)', async function () {
|
||||
const want = 'value';
|
||||
socketIoRouter.addComponent(this.test.fullTitle(), new class extends Module {
|
||||
handleMessage(socket, msg) { return want; }
|
||||
}());
|
||||
socket = await common.connect();
|
||||
const got = await tx(socket, {component: this.test.fullTitle()});
|
||||
assert.equal(got, want);
|
||||
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 common.connect(res);
|
||||
const clientVars = await common.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 common.connect(res);
|
||||
const message = await common.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 common.connect(res);
|
||||
const message = await common.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 common.connect(res);
|
||||
const clientVars = await common.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 common.connect(res);
|
||||
const clientVars = await common.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 common.connect(res);
|
||||
const message = await common.handshake(socket, 'pad');
|
||||
assert.equal(message.accessStatus, 'deny');
|
||||
});
|
||||
});
|
||||
|
||||
it('handleMessage with ack (error)', async function () {
|
||||
const InjectedError = class extends Error {
|
||||
constructor() { super('injected test error'); this.name = 'InjectedError'; }
|
||||
};
|
||||
socketIoRouter.addComponent(this.test.fullTitle(), new class extends Module {
|
||||
handleMessage(socket, msg) { throw new InjectedError(); }
|
||||
}());
|
||||
socket = await common.connect();
|
||||
await assert.rejects(tx(socket, {component: this.test.fullTitle()}), new InjectedError());
|
||||
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', 'user-password').expect(200);
|
||||
socket = await common.connect(res);
|
||||
const message = await common.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 common.connect(res);
|
||||
const message = await common.handshake(socket, 'pad');
|
||||
assert.equal(message.accessStatus, 'deny');
|
||||
});
|
||||
});
|
||||
describe('SocketIORouter.js', function () {
|
||||
const Module = class {
|
||||
setSocketIO(io) { }
|
||||
handleConnect(socket) { }
|
||||
handleDisconnect(socket) { }
|
||||
handleMessage(socket, message) { }
|
||||
};
|
||||
afterEach(async function () {
|
||||
socketIoRouter.deleteComponent(this.test.fullTitle());
|
||||
socketIoRouter.deleteComponent(`${this.test.fullTitle()} #2`);
|
||||
});
|
||||
it('setSocketIO', async function () {
|
||||
let ioServer;
|
||||
socketIoRouter.addComponent(this.test.fullTitle(), new class extends Module {
|
||||
setSocketIO(io) { ioServer = io; }
|
||||
}());
|
||||
assert(ioServer != null);
|
||||
});
|
||||
it('handleConnect', async function () {
|
||||
let serverSocket;
|
||||
socketIoRouter.addComponent(this.test.fullTitle(), new class extends Module {
|
||||
handleConnect(socket) { serverSocket = socket; }
|
||||
}());
|
||||
socket = await common.connect();
|
||||
assert(serverSocket != null);
|
||||
});
|
||||
it('handleDisconnect', async function () {
|
||||
let resolveConnected;
|
||||
const connected = new Promise((resolve) => resolveConnected = resolve);
|
||||
let resolveDisconnected;
|
||||
const disconnected = new Promise((resolve) => resolveDisconnected = resolve);
|
||||
socketIoRouter.addComponent(this.test.fullTitle(), new class extends Module {
|
||||
handleConnect(socket) {
|
||||
this._socket = socket;
|
||||
resolveConnected();
|
||||
}
|
||||
handleDisconnect(socket) {
|
||||
assert(socket != null);
|
||||
// There might be lingering disconnect events from sockets created by other tests.
|
||||
if (this._socket == null || socket.id !== this._socket.id)
|
||||
return;
|
||||
assert.equal(socket, this._socket);
|
||||
resolveDisconnected();
|
||||
}
|
||||
}());
|
||||
socket = await common.connect();
|
||||
await connected;
|
||||
socket.close();
|
||||
socket = null;
|
||||
await disconnected;
|
||||
});
|
||||
it('handleMessage (success)', async function () {
|
||||
let serverSocket;
|
||||
const want = {
|
||||
component: this.test.fullTitle(),
|
||||
foo: { bar: 'asdf' },
|
||||
};
|
||||
let rx;
|
||||
const got = new Promise((resolve) => { rx = resolve; });
|
||||
socketIoRouter.addComponent(this.test.fullTitle(), new class extends Module {
|
||||
handleConnect(socket) { serverSocket = socket; }
|
||||
handleMessage(socket, message) { assert.equal(socket, serverSocket); rx(message); }
|
||||
}());
|
||||
socketIoRouter.addComponent(`${this.test.fullTitle()} #2`, new class extends Module {
|
||||
handleMessage(socket, message) { assert.fail('wrong handler called'); }
|
||||
}());
|
||||
socket = await common.connect();
|
||||
socket.send(want);
|
||||
assert.deepEqual(await got, want);
|
||||
});
|
||||
const tx = async (socket, message = {}) => await new Promise((resolve, reject) => {
|
||||
const AckErr = class extends Error {
|
||||
constructor(name, ...args) { super(...args); this.name = name; }
|
||||
};
|
||||
socket.send(message, (errj, val) => errj != null ? reject(new AckErr(errj.name, errj.message)) : resolve(val));
|
||||
});
|
||||
it('handleMessage with ack (success)', async function () {
|
||||
const want = 'value';
|
||||
socketIoRouter.addComponent(this.test.fullTitle(), new class extends Module {
|
||||
handleMessage(socket, msg) { return want; }
|
||||
}());
|
||||
socket = await common.connect();
|
||||
const got = await tx(socket, { component: this.test.fullTitle() });
|
||||
assert.equal(got, want);
|
||||
});
|
||||
it('handleMessage with ack (error)', async function () {
|
||||
const InjectedError = class extends Error {
|
||||
constructor() { super('injected test error'); this.name = 'InjectedError'; }
|
||||
};
|
||||
socketIoRouter.addComponent(this.test.fullTitle(), new class extends Module {
|
||||
handleMessage(socket, msg) { throw new InjectedError(); }
|
||||
}());
|
||||
socket = await common.connect();
|
||||
await assert.rejects(tx(socket, { component: this.test.fullTitle() }), new InjectedError());
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,28 +1,25 @@
|
|||
import * as common from "../common.js";
|
||||
import * as settings from "../../../node/utils/Settings.js";
|
||||
'use strict';
|
||||
|
||||
const common = require('../common');
|
||||
const settings = require('../../../node/utils/Settings');
|
||||
|
||||
describe(__filename, function () {
|
||||
this.timeout(30000);
|
||||
let agent;
|
||||
const backups = {};
|
||||
before(async function () { agent = await common.init(); });
|
||||
beforeEach(async function () {
|
||||
backups.settings = {};
|
||||
for (const setting of ['requireAuthentication', 'requireAuthorization']) {
|
||||
backups.settings[setting] = settings[setting];
|
||||
}
|
||||
settings.requireAuthentication = false;
|
||||
settings.requireAuthorization = false;
|
||||
});
|
||||
afterEach(async function () {
|
||||
Object.assign(settings, backups.settings);
|
||||
});
|
||||
|
||||
describe('/javascript', function () {
|
||||
it('/javascript -> 200', async function () {
|
||||
await agent.get('/javascript').expect(200);
|
||||
this.timeout(30000);
|
||||
let agent;
|
||||
const backups = {};
|
||||
before(async function () { agent = await common.init(); });
|
||||
beforeEach(async function () {
|
||||
backups.settings = {};
|
||||
for (const setting of ['requireAuthentication', 'requireAuthorization']) {
|
||||
backups.settings[setting] = settings[setting];
|
||||
}
|
||||
settings.requireAuthentication = false;
|
||||
settings.requireAuthorization = false;
|
||||
});
|
||||
afterEach(async function () {
|
||||
Object.assign(settings, backups.settings);
|
||||
});
|
||||
describe('/javascript', function () {
|
||||
it('/javascript -> 200', async function () {
|
||||
await agent.get('/javascript').expect(200);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,494 +1,478 @@
|
|||
import assert$0 from "assert";
|
||||
import * as common from "../common.js";
|
||||
import * as plugins from "../../../static/js/pluginfw/plugin_defs.js";
|
||||
import * as settings from "../../../node/utils/Settings.js";
|
||||
'use strict';
|
||||
|
||||
const assert = require('assert').strict;
|
||||
const common = require('../common');
|
||||
const plugins = require('../../../static/js/pluginfw/plugin_defs');
|
||||
const settings = require('../../../node/utils/Settings');
|
||||
|
||||
const assert = assert$0.strict;
|
||||
describe(__filename, function () {
|
||||
this.timeout(30000);
|
||||
let agent;
|
||||
const backups = {};
|
||||
const authHookNames = ['preAuthorize', 'authenticate', 'authorize'];
|
||||
const failHookNames = ['preAuthzFailure', 'authnFailure', 'authzFailure', 'authFailure'];
|
||||
const makeHook = (hookName, hookFn) => ({
|
||||
hook_fn: hookFn,
|
||||
hook_fn_name: `fake_plugin/${hookName}`,
|
||||
hook_name: hookName,
|
||||
part: {plugin: 'fake_plugin'},
|
||||
});
|
||||
|
||||
before(async function () { agent = await common.init(); });
|
||||
beforeEach(async function () {
|
||||
backups.hooks = {};
|
||||
for (const hookName of authHookNames.concat(failHookNames)) {
|
||||
backups.hooks[hookName] = plugins.hooks[hookName];
|
||||
plugins.hooks[hookName] = [];
|
||||
}
|
||||
backups.settings = {};
|
||||
for (const setting of ['requireAuthentication', 'requireAuthorization', 'users']) {
|
||||
backups.settings[setting] = settings[setting];
|
||||
}
|
||||
settings.requireAuthentication = false;
|
||||
settings.requireAuthorization = false;
|
||||
settings.users = {
|
||||
admin: {password: 'admin-password', is_admin: true},
|
||||
user: {password: 'user-password'},
|
||||
};
|
||||
});
|
||||
afterEach(async function () {
|
||||
Object.assign(plugins.hooks, backups.hooks);
|
||||
Object.assign(settings, backups.settings);
|
||||
});
|
||||
|
||||
describe('webaccess: without plugins', function () {
|
||||
it('!authn !authz anonymous / -> 200', async function () {
|
||||
settings.requireAuthentication = false;
|
||||
settings.requireAuthorization = false;
|
||||
await agent.get('/').expect(200);
|
||||
this.timeout(30000);
|
||||
let agent;
|
||||
const backups = {};
|
||||
const authHookNames = ['preAuthorize', 'authenticate', 'authorize'];
|
||||
const failHookNames = ['preAuthzFailure', 'authnFailure', 'authzFailure', 'authFailure'];
|
||||
const makeHook = (hookName, hookFn) => ({
|
||||
hook_fn: hookFn,
|
||||
hook_fn_name: `fake_plugin/${hookName}`,
|
||||
hook_name: hookName,
|
||||
part: { plugin: 'fake_plugin' },
|
||||
});
|
||||
it('!authn !authz anonymous /admin/ -> 401', async function () {
|
||||
settings.requireAuthentication = false;
|
||||
settings.requireAuthorization = false;
|
||||
await agent.get('/admin/').expect(401);
|
||||
});
|
||||
it('authn !authz anonymous / -> 401', async function () {
|
||||
settings.requireAuthentication = true;
|
||||
settings.requireAuthorization = false;
|
||||
await agent.get('/').expect(401);
|
||||
});
|
||||
it('authn !authz user / -> 200', async function () {
|
||||
settings.requireAuthentication = true;
|
||||
settings.requireAuthorization = false;
|
||||
await agent.get('/').auth('user', 'user-password').expect(200);
|
||||
});
|
||||
it('authn !authz user /admin/ -> 403', async function () {
|
||||
settings.requireAuthentication = true;
|
||||
settings.requireAuthorization = false;
|
||||
await agent.get('/admin/').auth('user', 'user-password').expect(403);
|
||||
});
|
||||
it('authn !authz admin / -> 200', async function () {
|
||||
settings.requireAuthentication = true;
|
||||
settings.requireAuthorization = false;
|
||||
await agent.get('/').auth('admin', 'admin-password').expect(200);
|
||||
});
|
||||
it('authn !authz admin /admin/ -> 200', async function () {
|
||||
settings.requireAuthentication = true;
|
||||
settings.requireAuthorization = false;
|
||||
await agent.get('/admin/').auth('admin', 'admin-password').expect(200);
|
||||
});
|
||||
it('authn authz anonymous /robots.txt -> 200', async function () {
|
||||
settings.requireAuthentication = true;
|
||||
settings.requireAuthorization = true;
|
||||
await agent.get('/robots.txt').expect(200);
|
||||
});
|
||||
it('authn authz user / -> 403', async function () {
|
||||
settings.requireAuthentication = true;
|
||||
settings.requireAuthorization = true;
|
||||
await agent.get('/').auth('user', 'user-password').expect(403);
|
||||
});
|
||||
it('authn authz user /admin/ -> 403', async function () {
|
||||
settings.requireAuthentication = true;
|
||||
settings.requireAuthorization = true;
|
||||
await agent.get('/admin/').auth('user', 'user-password').expect(403);
|
||||
});
|
||||
it('authn authz admin / -> 200', async function () {
|
||||
settings.requireAuthentication = true;
|
||||
settings.requireAuthorization = true;
|
||||
await agent.get('/').auth('admin', 'admin-password').expect(200);
|
||||
});
|
||||
it('authn authz admin /admin/ -> 200', async function () {
|
||||
settings.requireAuthentication = true;
|
||||
settings.requireAuthorization = true;
|
||||
await agent.get('/admin/').auth('admin', 'admin-password').expect(200);
|
||||
});
|
||||
|
||||
describe('login fails if password is nullish', function () {
|
||||
for (const adminPassword of [undefined, null]) {
|
||||
// https://tools.ietf.org/html/rfc7617 says that the username and password are sent as
|
||||
// base64(username + ':' + password), but there's nothing stopping a malicious user from
|
||||
// sending just base64(username) (no colon). The lack of colon could throw off credential
|
||||
// parsing, resulting in successful comparisons against a null or undefined password.
|
||||
for (const creds of ['admin', 'admin:']) {
|
||||
it(`admin password: ${adminPassword} credentials: ${creds}`, async function () {
|
||||
settings.users.admin.password = adminPassword;
|
||||
const encCreds = Buffer.from(creds).toString('base64');
|
||||
await agent.get('/admin/').set('Authorization', `Basic ${encCreds}`).expect(401);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('webaccess: preAuthorize, authenticate, and authorize hooks', function () {
|
||||
let callOrder;
|
||||
const Handler = class {
|
||||
constructor(hookName, suffix) {
|
||||
this.called = false;
|
||||
this.hookName = hookName;
|
||||
this.innerHandle = () => [];
|
||||
this.id = hookName + suffix;
|
||||
this.checkContext = () => {};
|
||||
}
|
||||
handle(hookName, context, cb) {
|
||||
assert.equal(hookName, this.hookName);
|
||||
assert(context != null);
|
||||
assert(context.req != null);
|
||||
assert(context.res != null);
|
||||
assert(context.next != null);
|
||||
this.checkContext(context);
|
||||
assert(!this.called);
|
||||
this.called = true;
|
||||
callOrder.push(this.id);
|
||||
return cb(this.innerHandle(context));
|
||||
}
|
||||
};
|
||||
const handlers = {};
|
||||
|
||||
before(async function () { agent = await common.init(); });
|
||||
beforeEach(async function () {
|
||||
callOrder = [];
|
||||
for (const hookName of authHookNames) {
|
||||
// Create two handlers for each hook to test deferral to the next function.
|
||||
const h0 = new Handler(hookName, '_0');
|
||||
const h1 = new Handler(hookName, '_1');
|
||||
handlers[hookName] = [h0, h1];
|
||||
plugins.hooks[hookName] = [
|
||||
makeHook(hookName, h0.handle.bind(h0)),
|
||||
makeHook(hookName, h1.handle.bind(h1)),
|
||||
];
|
||||
}
|
||||
});
|
||||
|
||||
describe('preAuthorize', function () {
|
||||
beforeEach(async function () {
|
||||
settings.requireAuthentication = false;
|
||||
settings.requireAuthorization = false;
|
||||
});
|
||||
|
||||
it('defers if it returns []', async function () {
|
||||
await agent.get('/').expect(200);
|
||||
// Note: The preAuthorize hook always runs even if requireAuthorization is false.
|
||||
assert.deepEqual(callOrder, ['preAuthorize_0', 'preAuthorize_1']);
|
||||
});
|
||||
it('bypasses authenticate and authorize hooks when true is returned', async function () {
|
||||
settings.requireAuthentication = true;
|
||||
settings.requireAuthorization = true;
|
||||
handlers.preAuthorize[0].innerHandle = () => [true];
|
||||
await agent.get('/').expect(200);
|
||||
assert.deepEqual(callOrder, ['preAuthorize_0']);
|
||||
});
|
||||
it('bypasses authenticate and authorize hooks when false is returned', async function () {
|
||||
settings.requireAuthentication = true;
|
||||
settings.requireAuthorization = true;
|
||||
handlers.preAuthorize[0].innerHandle = () => [false];
|
||||
await agent.get('/').expect(403);
|
||||
assert.deepEqual(callOrder, ['preAuthorize_0']);
|
||||
});
|
||||
it('bypasses authenticate and authorize hooks when next is called', async function () {
|
||||
settings.requireAuthentication = true;
|
||||
settings.requireAuthorization = true;
|
||||
handlers.preAuthorize[0].innerHandle = ({next}) => next();
|
||||
await agent.get('/').expect(200);
|
||||
assert.deepEqual(callOrder, ['preAuthorize_0']);
|
||||
});
|
||||
it('static content (expressPreSession) bypasses all auth checks', async function () {
|
||||
settings.requireAuthentication = true;
|
||||
settings.requireAuthorization = true;
|
||||
await agent.get('/static/robots.txt').expect(200);
|
||||
assert.deepEqual(callOrder, []);
|
||||
});
|
||||
it('cannot grant access to /admin', async function () {
|
||||
handlers.preAuthorize[0].innerHandle = () => [true];
|
||||
await agent.get('/admin/').expect(401);
|
||||
// Notes:
|
||||
// * preAuthorize[1] is called despite preAuthorize[0] returning a non-empty list because
|
||||
// 'true' entries are ignored for /admin/* requests.
|
||||
// * The authenticate hook always runs for /admin/* requests even if
|
||||
// settings.requireAuthentication is false.
|
||||
assert.deepEqual(callOrder, ['preAuthorize_0',
|
||||
'preAuthorize_1',
|
||||
'authenticate_0',
|
||||
'authenticate_1']);
|
||||
});
|
||||
it('can deny access to /admin', async function () {
|
||||
handlers.preAuthorize[0].innerHandle = () => [false];
|
||||
await agent.get('/admin/').auth('admin', 'admin-password').expect(403);
|
||||
assert.deepEqual(callOrder, ['preAuthorize_0']);
|
||||
});
|
||||
it('runs preAuthzFailure hook when access is denied', async function () {
|
||||
handlers.preAuthorize[0].innerHandle = () => [false];
|
||||
let called = false;
|
||||
plugins.hooks.preAuthzFailure = [makeHook('preAuthzFailure', (hookName, {req, res}, cb) => {
|
||||
assert.equal(hookName, 'preAuthzFailure');
|
||||
assert(req != null);
|
||||
assert(res != null);
|
||||
assert(!called);
|
||||
called = true;
|
||||
res.status(200).send('injected');
|
||||
return cb([true]);
|
||||
})];
|
||||
await agent.get('/admin/').auth('admin', 'admin-password').expect(200, 'injected');
|
||||
assert(called);
|
||||
});
|
||||
it('returns 500 if an exception is thrown', async function () {
|
||||
handlers.preAuthorize[0].innerHandle = () => { throw new Error('exception test'); };
|
||||
await agent.get('/').expect(500);
|
||||
});
|
||||
});
|
||||
|
||||
describe('authenticate', function () {
|
||||
beforeEach(async function () {
|
||||
settings.requireAuthentication = true;
|
||||
settings.requireAuthorization = false;
|
||||
});
|
||||
|
||||
it('is not called if !requireAuthentication and not /admin/*', async function () {
|
||||
settings.requireAuthentication = false;
|
||||
await agent.get('/').expect(200);
|
||||
assert.deepEqual(callOrder, ['preAuthorize_0', 'preAuthorize_1']);
|
||||
});
|
||||
it('is called if !requireAuthentication and /admin/*', async function () {
|
||||
settings.requireAuthentication = false;
|
||||
await agent.get('/admin/').expect(401);
|
||||
assert.deepEqual(callOrder, ['preAuthorize_0',
|
||||
'preAuthorize_1',
|
||||
'authenticate_0',
|
||||
'authenticate_1']);
|
||||
});
|
||||
it('defers if empty list returned', async function () {
|
||||
await agent.get('/').expect(401);
|
||||
assert.deepEqual(callOrder, ['preAuthorize_0',
|
||||
'preAuthorize_1',
|
||||
'authenticate_0',
|
||||
'authenticate_1']);
|
||||
});
|
||||
it('does not defer if return [true], 200', async function () {
|
||||
handlers.authenticate[0].innerHandle = ({req}) => { req.session.user = {}; return [true]; };
|
||||
await agent.get('/').expect(200);
|
||||
// Note: authenticate_1 was not called because authenticate_0 handled it.
|
||||
assert.deepEqual(callOrder, ['preAuthorize_0', 'preAuthorize_1', 'authenticate_0']);
|
||||
});
|
||||
it('does not defer if return [false], 401', async function () {
|
||||
handlers.authenticate[0].innerHandle = () => [false];
|
||||
await agent.get('/').expect(401);
|
||||
// Note: authenticate_1 was not called because authenticate_0 handled it.
|
||||
assert.deepEqual(callOrder, ['preAuthorize_0', 'preAuthorize_1', 'authenticate_0']);
|
||||
});
|
||||
it('falls back to HTTP basic auth', async function () {
|
||||
await agent.get('/').auth('user', 'user-password').expect(200);
|
||||
assert.deepEqual(callOrder, ['preAuthorize_0',
|
||||
'preAuthorize_1',
|
||||
'authenticate_0',
|
||||
'authenticate_1']);
|
||||
});
|
||||
it('passes settings.users in context', async function () {
|
||||
handlers.authenticate[0].checkContext = ({users}) => {
|
||||
assert.equal(users, settings.users);
|
||||
};
|
||||
await agent.get('/').expect(401);
|
||||
assert.deepEqual(callOrder, ['preAuthorize_0',
|
||||
'preAuthorize_1',
|
||||
'authenticate_0',
|
||||
'authenticate_1']);
|
||||
});
|
||||
it('passes user, password in context if provided', async function () {
|
||||
handlers.authenticate[0].checkContext = ({username, password}) => {
|
||||
assert.equal(username, 'user');
|
||||
assert.equal(password, 'user-password');
|
||||
};
|
||||
await agent.get('/').auth('user', 'user-password').expect(200);
|
||||
assert.deepEqual(callOrder, ['preAuthorize_0',
|
||||
'preAuthorize_1',
|
||||
'authenticate_0',
|
||||
'authenticate_1']);
|
||||
});
|
||||
it('does not pass user, password in context if not provided', async function () {
|
||||
handlers.authenticate[0].checkContext = ({username, password}) => {
|
||||
assert(username == null);
|
||||
assert(password == null);
|
||||
};
|
||||
await agent.get('/').expect(401);
|
||||
assert.deepEqual(callOrder, ['preAuthorize_0',
|
||||
'preAuthorize_1',
|
||||
'authenticate_0',
|
||||
'authenticate_1']);
|
||||
});
|
||||
it('errors if req.session.user is not created', async function () {
|
||||
handlers.authenticate[0].innerHandle = () => [true];
|
||||
await agent.get('/').expect(500);
|
||||
assert.deepEqual(callOrder, ['preAuthorize_0', 'preAuthorize_1', 'authenticate_0']);
|
||||
});
|
||||
it('returns 500 if an exception is thrown', async function () {
|
||||
handlers.authenticate[0].innerHandle = () => { throw new Error('exception test'); };
|
||||
await agent.get('/').expect(500);
|
||||
assert.deepEqual(callOrder, ['preAuthorize_0', 'preAuthorize_1', 'authenticate_0']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('authorize', function () {
|
||||
beforeEach(async function () {
|
||||
settings.requireAuthentication = true;
|
||||
settings.requireAuthorization = true;
|
||||
});
|
||||
|
||||
it('is not called if !requireAuthorization (non-/admin)', async function () {
|
||||
settings.requireAuthorization = false;
|
||||
await agent.get('/').auth('user', 'user-password').expect(200);
|
||||
assert.deepEqual(callOrder, ['preAuthorize_0',
|
||||
'preAuthorize_1',
|
||||
'authenticate_0',
|
||||
'authenticate_1']);
|
||||
});
|
||||
it('is not called if !requireAuthorization (/admin)', async function () {
|
||||
settings.requireAuthorization = false;
|
||||
await agent.get('/admin/').auth('admin', 'admin-password').expect(200);
|
||||
assert.deepEqual(callOrder, ['preAuthorize_0',
|
||||
'preAuthorize_1',
|
||||
'authenticate_0',
|
||||
'authenticate_1']);
|
||||
});
|
||||
it('defers if empty list returned', async function () {
|
||||
await agent.get('/').auth('user', 'user-password').expect(403);
|
||||
assert.deepEqual(callOrder, ['preAuthorize_0',
|
||||
'preAuthorize_1',
|
||||
'authenticate_0',
|
||||
'authenticate_1',
|
||||
'authorize_0',
|
||||
'authorize_1']);
|
||||
});
|
||||
it('does not defer if return [true], 200', async function () {
|
||||
handlers.authorize[0].innerHandle = () => [true];
|
||||
await agent.get('/').auth('user', 'user-password').expect(200);
|
||||
// Note: authorize_1 was not called because authorize_0 handled it.
|
||||
assert.deepEqual(callOrder, ['preAuthorize_0',
|
||||
'preAuthorize_1',
|
||||
'authenticate_0',
|
||||
'authenticate_1',
|
||||
'authorize_0']);
|
||||
});
|
||||
it('does not defer if return [false], 403', async function () {
|
||||
handlers.authorize[0].innerHandle = () => [false];
|
||||
await agent.get('/').auth('user', 'user-password').expect(403);
|
||||
// Note: authorize_1 was not called because authorize_0 handled it.
|
||||
assert.deepEqual(callOrder, ['preAuthorize_0',
|
||||
'preAuthorize_1',
|
||||
'authenticate_0',
|
||||
'authenticate_1',
|
||||
'authorize_0']);
|
||||
});
|
||||
it('passes req.path in context', async function () {
|
||||
handlers.authorize[0].checkContext = ({resource}) => {
|
||||
assert.equal(resource, '/');
|
||||
};
|
||||
await agent.get('/').auth('user', 'user-password').expect(403);
|
||||
assert.deepEqual(callOrder, ['preAuthorize_0',
|
||||
'preAuthorize_1',
|
||||
'authenticate_0',
|
||||
'authenticate_1',
|
||||
'authorize_0',
|
||||
'authorize_1']);
|
||||
});
|
||||
it('returns 500 if an exception is thrown', async function () {
|
||||
handlers.authorize[0].innerHandle = () => { throw new Error('exception test'); };
|
||||
await agent.get('/').auth('user', 'user-password').expect(500);
|
||||
assert.deepEqual(callOrder, ['preAuthorize_0',
|
||||
'preAuthorize_1',
|
||||
'authenticate_0',
|
||||
'authenticate_1',
|
||||
'authorize_0']);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('webaccess: authnFailure, authzFailure, authFailure hooks', function () {
|
||||
const Handler = class {
|
||||
constructor(hookName) {
|
||||
this.hookName = hookName;
|
||||
this.shouldHandle = false;
|
||||
this.called = false;
|
||||
}
|
||||
handle(hookName, context, cb) {
|
||||
assert.equal(hookName, this.hookName);
|
||||
assert(context != null);
|
||||
assert(context.req != null);
|
||||
assert(context.res != null);
|
||||
assert(!this.called);
|
||||
this.called = true;
|
||||
if (this.shouldHandle) {
|
||||
context.res.status(200).send(this.hookName);
|
||||
return cb([true]);
|
||||
backups.hooks = {};
|
||||
for (const hookName of authHookNames.concat(failHookNames)) {
|
||||
backups.hooks[hookName] = plugins.hooks[hookName];
|
||||
plugins.hooks[hookName] = [];
|
||||
}
|
||||
return cb([]);
|
||||
}
|
||||
};
|
||||
const handlers = {};
|
||||
|
||||
beforeEach(async function () {
|
||||
failHookNames.forEach((hookName) => {
|
||||
const handler = new Handler(hookName);
|
||||
handlers[hookName] = handler;
|
||||
plugins.hooks[hookName] = [makeHook(hookName, handler.handle.bind(handler))];
|
||||
});
|
||||
settings.requireAuthentication = true;
|
||||
settings.requireAuthorization = true;
|
||||
backups.settings = {};
|
||||
for (const setting of ['requireAuthentication', 'requireAuthorization', 'users']) {
|
||||
backups.settings[setting] = settings[setting];
|
||||
}
|
||||
settings.requireAuthentication = false;
|
||||
settings.requireAuthorization = false;
|
||||
settings.users = {
|
||||
admin: { password: 'admin-password', is_admin: true },
|
||||
user: { password: 'user-password' },
|
||||
};
|
||||
});
|
||||
|
||||
// authn failure tests
|
||||
it('authn fail, no hooks handle -> 401', async function () {
|
||||
await agent.get('/').expect(401);
|
||||
assert(handlers.authnFailure.called);
|
||||
assert(!handlers.authzFailure.called);
|
||||
assert(handlers.authFailure.called);
|
||||
afterEach(async function () {
|
||||
Object.assign(plugins.hooks, backups.hooks);
|
||||
Object.assign(settings, backups.settings);
|
||||
});
|
||||
it('authn fail, authnFailure handles', async function () {
|
||||
handlers.authnFailure.shouldHandle = true;
|
||||
await agent.get('/').expect(200, 'authnFailure');
|
||||
assert(handlers.authnFailure.called);
|
||||
assert(!handlers.authzFailure.called);
|
||||
assert(!handlers.authFailure.called);
|
||||
describe('webaccess: without plugins', function () {
|
||||
it('!authn !authz anonymous / -> 200', async function () {
|
||||
settings.requireAuthentication = false;
|
||||
settings.requireAuthorization = false;
|
||||
await agent.get('/').expect(200);
|
||||
});
|
||||
it('!authn !authz anonymous /admin/ -> 401', async function () {
|
||||
settings.requireAuthentication = false;
|
||||
settings.requireAuthorization = false;
|
||||
await agent.get('/admin/').expect(401);
|
||||
});
|
||||
it('authn !authz anonymous / -> 401', async function () {
|
||||
settings.requireAuthentication = true;
|
||||
settings.requireAuthorization = false;
|
||||
await agent.get('/').expect(401);
|
||||
});
|
||||
it('authn !authz user / -> 200', async function () {
|
||||
settings.requireAuthentication = true;
|
||||
settings.requireAuthorization = false;
|
||||
await agent.get('/').auth('user', 'user-password').expect(200);
|
||||
});
|
||||
it('authn !authz user /admin/ -> 403', async function () {
|
||||
settings.requireAuthentication = true;
|
||||
settings.requireAuthorization = false;
|
||||
await agent.get('/admin/').auth('user', 'user-password').expect(403);
|
||||
});
|
||||
it('authn !authz admin / -> 200', async function () {
|
||||
settings.requireAuthentication = true;
|
||||
settings.requireAuthorization = false;
|
||||
await agent.get('/').auth('admin', 'admin-password').expect(200);
|
||||
});
|
||||
it('authn !authz admin /admin/ -> 200', async function () {
|
||||
settings.requireAuthentication = true;
|
||||
settings.requireAuthorization = false;
|
||||
await agent.get('/admin/').auth('admin', 'admin-password').expect(200);
|
||||
});
|
||||
it('authn authz anonymous /robots.txt -> 200', async function () {
|
||||
settings.requireAuthentication = true;
|
||||
settings.requireAuthorization = true;
|
||||
await agent.get('/robots.txt').expect(200);
|
||||
});
|
||||
it('authn authz user / -> 403', async function () {
|
||||
settings.requireAuthentication = true;
|
||||
settings.requireAuthorization = true;
|
||||
await agent.get('/').auth('user', 'user-password').expect(403);
|
||||
});
|
||||
it('authn authz user /admin/ -> 403', async function () {
|
||||
settings.requireAuthentication = true;
|
||||
settings.requireAuthorization = true;
|
||||
await agent.get('/admin/').auth('user', 'user-password').expect(403);
|
||||
});
|
||||
it('authn authz admin / -> 200', async function () {
|
||||
settings.requireAuthentication = true;
|
||||
settings.requireAuthorization = true;
|
||||
await agent.get('/').auth('admin', 'admin-password').expect(200);
|
||||
});
|
||||
it('authn authz admin /admin/ -> 200', async function () {
|
||||
settings.requireAuthentication = true;
|
||||
settings.requireAuthorization = true;
|
||||
await agent.get('/admin/').auth('admin', 'admin-password').expect(200);
|
||||
});
|
||||
describe('login fails if password is nullish', function () {
|
||||
for (const adminPassword of [undefined, null]) {
|
||||
// https://tools.ietf.org/html/rfc7617 says that the username and password are sent as
|
||||
// base64(username + ':' + password), but there's nothing stopping a malicious user from
|
||||
// sending just base64(username) (no colon). The lack of colon could throw off credential
|
||||
// parsing, resulting in successful comparisons against a null or undefined password.
|
||||
for (const creds of ['admin', 'admin:']) {
|
||||
it(`admin password: ${adminPassword} credentials: ${creds}`, async function () {
|
||||
settings.users.admin.password = adminPassword;
|
||||
const encCreds = Buffer.from(creds).toString('base64');
|
||||
await agent.get('/admin/').set('Authorization', `Basic ${encCreds}`).expect(401);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
it('authn fail, authFailure handles', async function () {
|
||||
handlers.authFailure.shouldHandle = true;
|
||||
await agent.get('/').expect(200, 'authFailure');
|
||||
assert(handlers.authnFailure.called);
|
||||
assert(!handlers.authzFailure.called);
|
||||
assert(handlers.authFailure.called);
|
||||
describe('webaccess: preAuthorize, authenticate, and authorize hooks', function () {
|
||||
let callOrder;
|
||||
const Handler = class {
|
||||
constructor(hookName, suffix) {
|
||||
this.called = false;
|
||||
this.hookName = hookName;
|
||||
this.innerHandle = () => [];
|
||||
this.id = hookName + suffix;
|
||||
this.checkContext = () => { };
|
||||
}
|
||||
handle(hookName, context, cb) {
|
||||
assert.equal(hookName, this.hookName);
|
||||
assert(context != null);
|
||||
assert(context.req != null);
|
||||
assert(context.res != null);
|
||||
assert(context.next != null);
|
||||
this.checkContext(context);
|
||||
assert(!this.called);
|
||||
this.called = true;
|
||||
callOrder.push(this.id);
|
||||
return cb(this.innerHandle(context));
|
||||
}
|
||||
};
|
||||
const handlers = {};
|
||||
beforeEach(async function () {
|
||||
callOrder = [];
|
||||
for (const hookName of authHookNames) {
|
||||
// Create two handlers for each hook to test deferral to the next function.
|
||||
const h0 = new Handler(hookName, '_0');
|
||||
const h1 = new Handler(hookName, '_1');
|
||||
handlers[hookName] = [h0, h1];
|
||||
plugins.hooks[hookName] = [
|
||||
makeHook(hookName, h0.handle.bind(h0)),
|
||||
makeHook(hookName, h1.handle.bind(h1)),
|
||||
];
|
||||
}
|
||||
});
|
||||
describe('preAuthorize', function () {
|
||||
beforeEach(async function () {
|
||||
settings.requireAuthentication = false;
|
||||
settings.requireAuthorization = false;
|
||||
});
|
||||
it('defers if it returns []', async function () {
|
||||
await agent.get('/').expect(200);
|
||||
// Note: The preAuthorize hook always runs even if requireAuthorization is false.
|
||||
assert.deepEqual(callOrder, ['preAuthorize_0', 'preAuthorize_1']);
|
||||
});
|
||||
it('bypasses authenticate and authorize hooks when true is returned', async function () {
|
||||
settings.requireAuthentication = true;
|
||||
settings.requireAuthorization = true;
|
||||
handlers.preAuthorize[0].innerHandle = () => [true];
|
||||
await agent.get('/').expect(200);
|
||||
assert.deepEqual(callOrder, ['preAuthorize_0']);
|
||||
});
|
||||
it('bypasses authenticate and authorize hooks when false is returned', async function () {
|
||||
settings.requireAuthentication = true;
|
||||
settings.requireAuthorization = true;
|
||||
handlers.preAuthorize[0].innerHandle = () => [false];
|
||||
await agent.get('/').expect(403);
|
||||
assert.deepEqual(callOrder, ['preAuthorize_0']);
|
||||
});
|
||||
it('bypasses authenticate and authorize hooks when next is called', async function () {
|
||||
settings.requireAuthentication = true;
|
||||
settings.requireAuthorization = true;
|
||||
handlers.preAuthorize[0].innerHandle = ({ next }) => next();
|
||||
await agent.get('/').expect(200);
|
||||
assert.deepEqual(callOrder, ['preAuthorize_0']);
|
||||
});
|
||||
it('static content (expressPreSession) bypasses all auth checks', async function () {
|
||||
settings.requireAuthentication = true;
|
||||
settings.requireAuthorization = true;
|
||||
await agent.get('/static/robots.txt').expect(200);
|
||||
assert.deepEqual(callOrder, []);
|
||||
});
|
||||
it('cannot grant access to /admin', async function () {
|
||||
handlers.preAuthorize[0].innerHandle = () => [true];
|
||||
await agent.get('/admin/').expect(401);
|
||||
// Notes:
|
||||
// * preAuthorize[1] is called despite preAuthorize[0] returning a non-empty list because
|
||||
// 'true' entries are ignored for /admin/* requests.
|
||||
// * The authenticate hook always runs for /admin/* requests even if
|
||||
// settings.requireAuthentication is false.
|
||||
assert.deepEqual(callOrder, ['preAuthorize_0',
|
||||
'preAuthorize_1',
|
||||
'authenticate_0',
|
||||
'authenticate_1']);
|
||||
});
|
||||
it('can deny access to /admin', async function () {
|
||||
handlers.preAuthorize[0].innerHandle = () => [false];
|
||||
await agent.get('/admin/').auth('admin', 'admin-password').expect(403);
|
||||
assert.deepEqual(callOrder, ['preAuthorize_0']);
|
||||
});
|
||||
it('runs preAuthzFailure hook when access is denied', async function () {
|
||||
handlers.preAuthorize[0].innerHandle = () => [false];
|
||||
let called = false;
|
||||
plugins.hooks.preAuthzFailure = [makeHook('preAuthzFailure', (hookName, { req, res }, cb) => {
|
||||
assert.equal(hookName, 'preAuthzFailure');
|
||||
assert(req != null);
|
||||
assert(res != null);
|
||||
assert(!called);
|
||||
called = true;
|
||||
res.status(200).send('injected');
|
||||
return cb([true]);
|
||||
})];
|
||||
await agent.get('/admin/').auth('admin', 'admin-password').expect(200, 'injected');
|
||||
assert(called);
|
||||
});
|
||||
it('returns 500 if an exception is thrown', async function () {
|
||||
handlers.preAuthorize[0].innerHandle = () => { throw new Error('exception test'); };
|
||||
await agent.get('/').expect(500);
|
||||
});
|
||||
});
|
||||
describe('authenticate', function () {
|
||||
beforeEach(async function () {
|
||||
settings.requireAuthentication = true;
|
||||
settings.requireAuthorization = false;
|
||||
});
|
||||
it('is not called if !requireAuthentication and not /admin/*', async function () {
|
||||
settings.requireAuthentication = false;
|
||||
await agent.get('/').expect(200);
|
||||
assert.deepEqual(callOrder, ['preAuthorize_0', 'preAuthorize_1']);
|
||||
});
|
||||
it('is called if !requireAuthentication and /admin/*', async function () {
|
||||
settings.requireAuthentication = false;
|
||||
await agent.get('/admin/').expect(401);
|
||||
assert.deepEqual(callOrder, ['preAuthorize_0',
|
||||
'preAuthorize_1',
|
||||
'authenticate_0',
|
||||
'authenticate_1']);
|
||||
});
|
||||
it('defers if empty list returned', async function () {
|
||||
await agent.get('/').expect(401);
|
||||
assert.deepEqual(callOrder, ['preAuthorize_0',
|
||||
'preAuthorize_1',
|
||||
'authenticate_0',
|
||||
'authenticate_1']);
|
||||
});
|
||||
it('does not defer if return [true], 200', async function () {
|
||||
handlers.authenticate[0].innerHandle = ({ req }) => { req.session.user = {}; return [true]; };
|
||||
await agent.get('/').expect(200);
|
||||
// Note: authenticate_1 was not called because authenticate_0 handled it.
|
||||
assert.deepEqual(callOrder, ['preAuthorize_0', 'preAuthorize_1', 'authenticate_0']);
|
||||
});
|
||||
it('does not defer if return [false], 401', async function () {
|
||||
handlers.authenticate[0].innerHandle = () => [false];
|
||||
await agent.get('/').expect(401);
|
||||
// Note: authenticate_1 was not called because authenticate_0 handled it.
|
||||
assert.deepEqual(callOrder, ['preAuthorize_0', 'preAuthorize_1', 'authenticate_0']);
|
||||
});
|
||||
it('falls back to HTTP basic auth', async function () {
|
||||
await agent.get('/').auth('user', 'user-password').expect(200);
|
||||
assert.deepEqual(callOrder, ['preAuthorize_0',
|
||||
'preAuthorize_1',
|
||||
'authenticate_0',
|
||||
'authenticate_1']);
|
||||
});
|
||||
it('passes settings.users in context', async function () {
|
||||
handlers.authenticate[0].checkContext = ({ users }) => {
|
||||
assert.equal(users, settings.users);
|
||||
};
|
||||
await agent.get('/').expect(401);
|
||||
assert.deepEqual(callOrder, ['preAuthorize_0',
|
||||
'preAuthorize_1',
|
||||
'authenticate_0',
|
||||
'authenticate_1']);
|
||||
});
|
||||
it('passes user, password in context if provided', async function () {
|
||||
handlers.authenticate[0].checkContext = ({ username, password }) => {
|
||||
assert.equal(username, 'user');
|
||||
assert.equal(password, 'user-password');
|
||||
};
|
||||
await agent.get('/').auth('user', 'user-password').expect(200);
|
||||
assert.deepEqual(callOrder, ['preAuthorize_0',
|
||||
'preAuthorize_1',
|
||||
'authenticate_0',
|
||||
'authenticate_1']);
|
||||
});
|
||||
it('does not pass user, password in context if not provided', async function () {
|
||||
handlers.authenticate[0].checkContext = ({ username, password }) => {
|
||||
assert(username == null);
|
||||
assert(password == null);
|
||||
};
|
||||
await agent.get('/').expect(401);
|
||||
assert.deepEqual(callOrder, ['preAuthorize_0',
|
||||
'preAuthorize_1',
|
||||
'authenticate_0',
|
||||
'authenticate_1']);
|
||||
});
|
||||
it('errors if req.session.user is not created', async function () {
|
||||
handlers.authenticate[0].innerHandle = () => [true];
|
||||
await agent.get('/').expect(500);
|
||||
assert.deepEqual(callOrder, ['preAuthorize_0', 'preAuthorize_1', 'authenticate_0']);
|
||||
});
|
||||
it('returns 500 if an exception is thrown', async function () {
|
||||
handlers.authenticate[0].innerHandle = () => { throw new Error('exception test'); };
|
||||
await agent.get('/').expect(500);
|
||||
assert.deepEqual(callOrder, ['preAuthorize_0', 'preAuthorize_1', 'authenticate_0']);
|
||||
});
|
||||
});
|
||||
describe('authorize', function () {
|
||||
beforeEach(async function () {
|
||||
settings.requireAuthentication = true;
|
||||
settings.requireAuthorization = true;
|
||||
});
|
||||
it('is not called if !requireAuthorization (non-/admin)', async function () {
|
||||
settings.requireAuthorization = false;
|
||||
await agent.get('/').auth('user', 'user-password').expect(200);
|
||||
assert.deepEqual(callOrder, ['preAuthorize_0',
|
||||
'preAuthorize_1',
|
||||
'authenticate_0',
|
||||
'authenticate_1']);
|
||||
});
|
||||
it('is not called if !requireAuthorization (/admin)', async function () {
|
||||
settings.requireAuthorization = false;
|
||||
await agent.get('/admin/').auth('admin', 'admin-password').expect(200);
|
||||
assert.deepEqual(callOrder, ['preAuthorize_0',
|
||||
'preAuthorize_1',
|
||||
'authenticate_0',
|
||||
'authenticate_1']);
|
||||
});
|
||||
it('defers if empty list returned', async function () {
|
||||
await agent.get('/').auth('user', 'user-password').expect(403);
|
||||
assert.deepEqual(callOrder, ['preAuthorize_0',
|
||||
'preAuthorize_1',
|
||||
'authenticate_0',
|
||||
'authenticate_1',
|
||||
'authorize_0',
|
||||
'authorize_1']);
|
||||
});
|
||||
it('does not defer if return [true], 200', async function () {
|
||||
handlers.authorize[0].innerHandle = () => [true];
|
||||
await agent.get('/').auth('user', 'user-password').expect(200);
|
||||
// Note: authorize_1 was not called because authorize_0 handled it.
|
||||
assert.deepEqual(callOrder, ['preAuthorize_0',
|
||||
'preAuthorize_1',
|
||||
'authenticate_0',
|
||||
'authenticate_1',
|
||||
'authorize_0']);
|
||||
});
|
||||
it('does not defer if return [false], 403', async function () {
|
||||
handlers.authorize[0].innerHandle = () => [false];
|
||||
await agent.get('/').auth('user', 'user-password').expect(403);
|
||||
// Note: authorize_1 was not called because authorize_0 handled it.
|
||||
assert.deepEqual(callOrder, ['preAuthorize_0',
|
||||
'preAuthorize_1',
|
||||
'authenticate_0',
|
||||
'authenticate_1',
|
||||
'authorize_0']);
|
||||
});
|
||||
it('passes req.path in context', async function () {
|
||||
handlers.authorize[0].checkContext = ({ resource }) => {
|
||||
assert.equal(resource, '/');
|
||||
};
|
||||
await agent.get('/').auth('user', 'user-password').expect(403);
|
||||
assert.deepEqual(callOrder, ['preAuthorize_0',
|
||||
'preAuthorize_1',
|
||||
'authenticate_0',
|
||||
'authenticate_1',
|
||||
'authorize_0',
|
||||
'authorize_1']);
|
||||
});
|
||||
it('returns 500 if an exception is thrown', async function () {
|
||||
handlers.authorize[0].innerHandle = () => { throw new Error('exception test'); };
|
||||
await agent.get('/').auth('user', 'user-password').expect(500);
|
||||
assert.deepEqual(callOrder, ['preAuthorize_0',
|
||||
'preAuthorize_1',
|
||||
'authenticate_0',
|
||||
'authenticate_1',
|
||||
'authorize_0']);
|
||||
});
|
||||
});
|
||||
});
|
||||
it('authnFailure trumps authFailure', async function () {
|
||||
handlers.authnFailure.shouldHandle = true;
|
||||
handlers.authFailure.shouldHandle = true;
|
||||
await agent.get('/').expect(200, 'authnFailure');
|
||||
assert(handlers.authnFailure.called);
|
||||
assert(!handlers.authFailure.called);
|
||||
describe('webaccess: authnFailure, authzFailure, authFailure hooks', function () {
|
||||
const Handler = class {
|
||||
constructor(hookName) {
|
||||
this.hookName = hookName;
|
||||
this.shouldHandle = false;
|
||||
this.called = false;
|
||||
}
|
||||
handle(hookName, context, cb) {
|
||||
assert.equal(hookName, this.hookName);
|
||||
assert(context != null);
|
||||
assert(context.req != null);
|
||||
assert(context.res != null);
|
||||
assert(!this.called);
|
||||
this.called = true;
|
||||
if (this.shouldHandle) {
|
||||
context.res.status(200).send(this.hookName);
|
||||
return cb([true]);
|
||||
}
|
||||
return cb([]);
|
||||
}
|
||||
};
|
||||
const handlers = {};
|
||||
beforeEach(async function () {
|
||||
failHookNames.forEach((hookName) => {
|
||||
const handler = new Handler(hookName);
|
||||
handlers[hookName] = handler;
|
||||
plugins.hooks[hookName] = [makeHook(hookName, handler.handle.bind(handler))];
|
||||
});
|
||||
settings.requireAuthentication = true;
|
||||
settings.requireAuthorization = true;
|
||||
});
|
||||
// authn failure tests
|
||||
it('authn fail, no hooks handle -> 401', async function () {
|
||||
await agent.get('/').expect(401);
|
||||
assert(handlers.authnFailure.called);
|
||||
assert(!handlers.authzFailure.called);
|
||||
assert(handlers.authFailure.called);
|
||||
});
|
||||
it('authn fail, authnFailure handles', async function () {
|
||||
handlers.authnFailure.shouldHandle = true;
|
||||
await agent.get('/').expect(200, 'authnFailure');
|
||||
assert(handlers.authnFailure.called);
|
||||
assert(!handlers.authzFailure.called);
|
||||
assert(!handlers.authFailure.called);
|
||||
});
|
||||
it('authn fail, authFailure handles', async function () {
|
||||
handlers.authFailure.shouldHandle = true;
|
||||
await agent.get('/').expect(200, 'authFailure');
|
||||
assert(handlers.authnFailure.called);
|
||||
assert(!handlers.authzFailure.called);
|
||||
assert(handlers.authFailure.called);
|
||||
});
|
||||
it('authnFailure trumps authFailure', async function () {
|
||||
handlers.authnFailure.shouldHandle = true;
|
||||
handlers.authFailure.shouldHandle = true;
|
||||
await agent.get('/').expect(200, 'authnFailure');
|
||||
assert(handlers.authnFailure.called);
|
||||
assert(!handlers.authFailure.called);
|
||||
});
|
||||
// authz failure tests
|
||||
it('authz fail, no hooks handle -> 403', async function () {
|
||||
await agent.get('/').auth('user', 'user-password').expect(403);
|
||||
assert(!handlers.authnFailure.called);
|
||||
assert(handlers.authzFailure.called);
|
||||
assert(handlers.authFailure.called);
|
||||
});
|
||||
it('authz fail, authzFailure handles', async function () {
|
||||
handlers.authzFailure.shouldHandle = true;
|
||||
await agent.get('/').auth('user', 'user-password').expect(200, 'authzFailure');
|
||||
assert(!handlers.authnFailure.called);
|
||||
assert(handlers.authzFailure.called);
|
||||
assert(!handlers.authFailure.called);
|
||||
});
|
||||
it('authz fail, authFailure handles', async function () {
|
||||
handlers.authFailure.shouldHandle = true;
|
||||
await agent.get('/').auth('user', 'user-password').expect(200, 'authFailure');
|
||||
assert(!handlers.authnFailure.called);
|
||||
assert(handlers.authzFailure.called);
|
||||
assert(handlers.authFailure.called);
|
||||
});
|
||||
it('authzFailure trumps authFailure', async function () {
|
||||
handlers.authzFailure.shouldHandle = true;
|
||||
handlers.authFailure.shouldHandle = true;
|
||||
await agent.get('/').auth('user', 'user-password').expect(200, 'authzFailure');
|
||||
assert(handlers.authzFailure.called);
|
||||
assert(!handlers.authFailure.called);
|
||||
});
|
||||
});
|
||||
|
||||
// authz failure tests
|
||||
it('authz fail, no hooks handle -> 403', async function () {
|
||||
await agent.get('/').auth('user', 'user-password').expect(403);
|
||||
assert(!handlers.authnFailure.called);
|
||||
assert(handlers.authzFailure.called);
|
||||
assert(handlers.authFailure.called);
|
||||
});
|
||||
it('authz fail, authzFailure handles', async function () {
|
||||
handlers.authzFailure.shouldHandle = true;
|
||||
await agent.get('/').auth('user', 'user-password').expect(200, 'authzFailure');
|
||||
assert(!handlers.authnFailure.called);
|
||||
assert(handlers.authzFailure.called);
|
||||
assert(!handlers.authFailure.called);
|
||||
});
|
||||
it('authz fail, authFailure handles', async function () {
|
||||
handlers.authFailure.shouldHandle = true;
|
||||
await agent.get('/').auth('user', 'user-password').expect(200, 'authFailure');
|
||||
assert(!handlers.authnFailure.called);
|
||||
assert(handlers.authzFailure.called);
|
||||
assert(handlers.authFailure.called);
|
||||
});
|
||||
it('authzFailure trumps authFailure', async function () {
|
||||
handlers.authzFailure.shouldHandle = true;
|
||||
handlers.authFailure.shouldHandle = true;
|
||||
await agent.get('/').auth('user', 'user-password').expect(200, 'authzFailure');
|
||||
assert(handlers.authzFailure.called);
|
||||
assert(!handlers.authFailure.called);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue