Cleanup Repo
This commit is contained in:
parent
56e15894c0
commit
0cabedbac6
91
test/camo.js
91
test/camo.js
|
|
@ -1,91 +0,0 @@
|
||||||
const assert = require('assert');
|
|
||||||
const Camo = require('../lib/camo');
|
|
||||||
const CamoConfig = require('../lib/configuration/camoconfig').CamoConfig;
|
|
||||||
|
|
||||||
describe('Camo', () => {
|
|
||||||
const config = new CamoConfig({
|
|
||||||
camo: {
|
|
||||||
server: 'http://localhost:8081',
|
|
||||||
key: '9LKC7708ZHOVRCTLOLE3G2YJ0U1T8F96',
|
|
||||||
'whitelisted-domains': ['def.xyz', 'tii.kzz.qqq'],
|
|
||||||
encoding: 'hex'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('#camoify', () => {
|
|
||||||
it('constructs a camo url', () => {
|
|
||||||
const result = Camo.camoify(config, 'http://abc.xyz/image.jpeg');
|
|
||||||
assert.strictEqual(result, 'http://localhost:8081/a9c295dd7d8dcbc8247dec97ac5d9b4ee8baeb31/687474703a2f2f6162632e78797a2f696d6167652e6a706567');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('constructs a camo url using url encoding', () => {
|
|
||||||
const config = new CamoConfig({
|
|
||||||
camo: {
|
|
||||||
server: 'http://localhost:8081',
|
|
||||||
key: '9LKC7708ZHOVRCTLOLE3G2YJ0U1T8F96',
|
|
||||||
'whitelisted-domains': ['def.xyz'],
|
|
||||||
encoding: 'url'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = Camo.camoify(config, 'http://abc.xyz/image.jpeg');
|
|
||||||
assert.strictEqual(result, 'http://localhost:8081/a9c295dd7d8dcbc8247dec97ac5d9b4ee8baeb31?url=http%3A%2F%2Fabc.xyz%2Fimage.jpeg');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('bypasses camo for whitelisted domains', () => {
|
|
||||||
const result = Camo.camoify(config, 'http://def.xyz/image.jpeg');
|
|
||||||
assert.strictEqual(result, 'https://def.xyz/image.jpeg');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('bypasses camo for whitelisted domains subdomains', () => {
|
|
||||||
const result = Camo.camoify(config, 'http://abc.def.xyz/image.jpeg');
|
|
||||||
assert.strictEqual(result, 'https://abc.def.xyz/image.jpeg');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('does not bypass camo for a non-subdomain match', () => {
|
|
||||||
const result = Camo.camoify(config, 'http://abcdef.xyz/image.jpeg');
|
|
||||||
assert.strictEqual(result, 'http://localhost:8081/19f53f65e8081a064cff54fbd665e8bb08612aa6/687474703a2f2f6162636465662e78797a2f696d6167652e6a706567');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('does not bypass camo when no whitelist is configured', () => {
|
|
||||||
const config = new CamoConfig({
|
|
||||||
camo: {
|
|
||||||
server: 'http://localhost:8081',
|
|
||||||
key: '9LKC7708ZHOVRCTLOLE3G2YJ0U1T8F96',
|
|
||||||
encoding: 'hex'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const result = Camo.camoify(config, 'http://abcdef.xyz/image.jpeg');
|
|
||||||
assert.strictEqual(result, 'http://localhost:8081/19f53f65e8081a064cff54fbd665e8bb08612aa6/687474703a2f2f6162636465662e78797a2f696d6167652e6a706567');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('#transformImgTags', () => {
|
|
||||||
it('transforms an img tag with a src', () => {
|
|
||||||
const attribs = {
|
|
||||||
src: 'http://abc.xyz/image.jpeg',
|
|
||||||
'class': 'some-image'
|
|
||||||
};
|
|
||||||
const expectedAttribs = {
|
|
||||||
src: 'http://localhost:8081/a9c295dd7d8dcbc8247dec97ac5d9b4ee8baeb31/687474703a2f2f6162632e78797a2f696d6167652e6a706567',
|
|
||||||
'class': 'some-image'
|
|
||||||
};
|
|
||||||
const result = Camo.transformImgTags(config, 'img', attribs);
|
|
||||||
assert.deepStrictEqual(result, { tagName: 'img', attribs: expectedAttribs });
|
|
||||||
});
|
|
||||||
|
|
||||||
it('skips img tags with no src', () => {
|
|
||||||
const attribs = { 'class': 'some-image' };
|
|
||||||
const result = Camo.transformImgTags(config, 'img', attribs);
|
|
||||||
assert.deepStrictEqual(result, { tagName: 'img', attribs: attribs });
|
|
||||||
});
|
|
||||||
|
|
||||||
it('fails gracefully', () => {
|
|
||||||
const attribs = { src: 'http://abc.xyz/image.jpeg' };
|
|
||||||
const config = new CamoConfig({ camo: { enabled: true }});
|
|
||||||
config.getKey = () => { throw new Error('something happened'); };
|
|
||||||
const result = Camo.transformImgTags(config, 'img', attribs);
|
|
||||||
assert.deepStrictEqual(result, { tagName: 'img', attribs: attribs });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,53 +0,0 @@
|
||||||
const PlaylistModule = require('../../lib/channel/playlist');
|
|
||||||
const assert = require('assert');
|
|
||||||
const Config = require('../../lib/config');
|
|
||||||
|
|
||||||
describe('PlaylistModule', () => {
|
|
||||||
describe('#handleClean', () => {
|
|
||||||
let fakeChannel = {
|
|
||||||
uniqueName: 'testChannel',
|
|
||||||
logger: {
|
|
||||||
log() {
|
|
||||||
|
|
||||||
}
|
|
||||||
},
|
|
||||||
broadcastToRoom() {
|
|
||||||
},
|
|
||||||
broadcastAll() {
|
|
||||||
},
|
|
||||||
modules: {
|
|
||||||
permissions: {
|
|
||||||
canDeleteVideo() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let fakeUser = {
|
|
||||||
getName() {
|
|
||||||
return 'testUser';
|
|
||||||
},
|
|
||||||
socket: {
|
|
||||||
emit() {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let playlistModule = new PlaylistModule(fakeChannel);
|
|
||||||
|
|
||||||
it('rejects invalid regexes', () => {
|
|
||||||
let sentError = false;
|
|
||||||
|
|
||||||
fakeUser.socket.emit = (event, payload) => {
|
|
||||||
assert.strictEqual(event, 'errorMsg');
|
|
||||||
assert.deepStrictEqual(payload, {
|
|
||||||
msg: "Invalid target: -i * -m"
|
|
||||||
});
|
|
||||||
sentError = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
playlistModule.handleClean(fakeUser, "/clean -i * -m", {});
|
|
||||||
|
|
||||||
assert(sentError, 'Expected error due to invalid regex');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,221 +0,0 @@
|
||||||
const PollModule = require('../../lib/channel/poll');
|
|
||||||
const assert = require('assert');
|
|
||||||
const Config = require('../../lib/config');
|
|
||||||
|
|
||||||
describe('PollModule', () => {
|
|
||||||
describe('#validatePollInput', () => {
|
|
||||||
let pollModule = new PollModule({ uniqueName: 'testChannel', modules: {} });
|
|
||||||
|
|
||||||
it('accepts valid input', () => {
|
|
||||||
let title = '';
|
|
||||||
for (let i = 0; i < 20; i++) {
|
|
||||||
title += 'x';
|
|
||||||
}
|
|
||||||
|
|
||||||
pollModule.validatePollInput(title, ['ab', 'cd']);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('rejects non-string titles', () => {
|
|
||||||
assert.throws(() => {
|
|
||||||
pollModule.validatePollInput(null, []);
|
|
||||||
}, /title/);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('rejects invalidly long titles', () => {
|
|
||||||
let title = '';
|
|
||||||
for (let i = 0; i < 256; i++) {
|
|
||||||
title += 'x';
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.throws(() => {
|
|
||||||
pollModule.validatePollInput(title, []);
|
|
||||||
}, /title/);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('rejects non-array option parameter', () => {
|
|
||||||
assert.throws(() => {
|
|
||||||
pollModule.validatePollInput('poll', 1234);
|
|
||||||
}, /options/);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('rejects too many options', () => {
|
|
||||||
const limit = Config.get('poll.max-options');
|
|
||||||
Config.set('poll.max-options', 2);
|
|
||||||
try {
|
|
||||||
assert.throws(() => {
|
|
||||||
pollModule.validatePollInput('poll', ['1', '2', '3', '4']);
|
|
||||||
}, /maximum of 2 options/);
|
|
||||||
} finally {
|
|
||||||
Config.set('poll.max-options', limit);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it('rejects non-string options', () => {
|
|
||||||
assert.throws(() => {
|
|
||||||
pollModule.validatePollInput('poll', [null]);
|
|
||||||
}, /options must be strings/);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('rejects invalidly long options', () => {
|
|
||||||
let option = '';
|
|
||||||
for (let i = 0; i < 256; i++) {
|
|
||||||
option += 'x';
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.throws(() => {
|
|
||||||
pollModule.validatePollInput('poll', [option]);
|
|
||||||
}, /options must be 1-255 characters/);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('#handleNewPoll', () => {
|
|
||||||
let fakeChannel = {
|
|
||||||
uniqueName: 'testChannel',
|
|
||||||
logger: {
|
|
||||||
log() {
|
|
||||||
|
|
||||||
}
|
|
||||||
},
|
|
||||||
broadcastToRoom() {
|
|
||||||
},
|
|
||||||
broadcastAll() {
|
|
||||||
},
|
|
||||||
modules: {
|
|
||||||
permissions: {
|
|
||||||
canControlPoll() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let fakeUser = {
|
|
||||||
getName() {
|
|
||||||
return 'testUser';
|
|
||||||
},
|
|
||||||
socket: {
|
|
||||||
emit() {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let pollModule;
|
|
||||||
beforeEach(() => {
|
|
||||||
pollModule = new PollModule(fakeChannel);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('creates a valid poll', () => {
|
|
||||||
let sentNewPoll = false;
|
|
||||||
let sentClosePoll = false;
|
|
||||||
fakeChannel.broadcastToRoom = (event, data, room) => {
|
|
||||||
if (room === 'testChannel:viewHidden' && event === 'newPoll') {
|
|
||||||
sentNewPoll = true;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
fakeChannel.broadcastAll = (event) => {
|
|
||||||
if (event === 'closePoll') {
|
|
||||||
sentClosePoll = true;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
pollModule.handleNewPoll(fakeUser, {
|
|
||||||
title: 'test poll',
|
|
||||||
opts: [
|
|
||||||
'option 1',
|
|
||||||
'option 2'
|
|
||||||
],
|
|
||||||
obscured: false
|
|
||||||
}, (ackResult) => {
|
|
||||||
assert(!ackResult.error, `Unexpected error: ${ackResult.error}`);
|
|
||||||
});
|
|
||||||
assert(!sentClosePoll, 'Unexpected broadcast of closePoll event');
|
|
||||||
assert(sentNewPoll, 'Expected broadcast of newPoll event');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('closes an existing poll when a new one is created', () => {
|
|
||||||
let sentNewPoll = 0;
|
|
||||||
let sentClosePoll = 0;
|
|
||||||
let sentUpdatePoll = 0;
|
|
||||||
fakeChannel.broadcastToRoom = (event, data, room) => {
|
|
||||||
if (room === 'testChannel:viewHidden' && event === 'newPoll') {
|
|
||||||
sentNewPoll++;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
fakeChannel.broadcastAll = (event, data) => {
|
|
||||||
if (event === 'closePoll') {
|
|
||||||
sentClosePoll++;
|
|
||||||
} else if (event === 'updatePoll') {
|
|
||||||
sentUpdatePoll++;
|
|
||||||
assert.deepStrictEqual(data.counts, [0, 0]);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
pollModule.handleNewPoll(fakeUser, {
|
|
||||||
title: 'test poll',
|
|
||||||
opts: [
|
|
||||||
'option 1',
|
|
||||||
'option 2'
|
|
||||||
],
|
|
||||||
obscured: true
|
|
||||||
}, (ackResult) => {
|
|
||||||
assert(!ackResult.error, `Unexpected error: ${ackResult.error}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
pollModule.handleNewPoll(fakeUser, {
|
|
||||||
title: 'poll 2',
|
|
||||||
opts: [
|
|
||||||
'option 3',
|
|
||||||
'option 4'
|
|
||||||
],
|
|
||||||
obscured: false
|
|
||||||
}, (ackResult) => {
|
|
||||||
assert(!ackResult.error, `Unexpected error: ${ackResult.error}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
assert.strictEqual(sentClosePoll, 1, 'Expected 1 broadcast of closePoll event');
|
|
||||||
assert.strictEqual(sentUpdatePoll, 1, 'Expected 1 broadcast of updatePoll event');
|
|
||||||
assert.strictEqual(sentNewPoll, 2, 'Expected 2 broadcasts of newPoll event');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('rejects an invalid poll', () => {
|
|
||||||
fakeChannel.broadcastToRoom = (event, data, room) => {
|
|
||||||
assert(false, 'Expected no events to be sent');
|
|
||||||
};
|
|
||||||
fakeChannel.broadcastAll = (event) => {
|
|
||||||
assert(false, 'Expected no events to be sent');
|
|
||||||
};
|
|
||||||
const options = [];
|
|
||||||
for (let i = 0; i < 200; i++) {
|
|
||||||
options.push('option ' + i);
|
|
||||||
}
|
|
||||||
pollModule.handleNewPoll(fakeUser, {
|
|
||||||
title: 'test poll',
|
|
||||||
opts: options,
|
|
||||||
obscured: false
|
|
||||||
}, (ackResult) => {
|
|
||||||
assert.equal(ackResult.error.message, 'Polls are limited to a maximum of 50 options.');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('handles a rejection with no ack provided by socket.io', () => {
|
|
||||||
fakeChannel.broadcastToRoom = (event, data, room) => {
|
|
||||||
assert(false, 'Expected no events to be sent');
|
|
||||||
};
|
|
||||||
fakeChannel.broadcastAll = (event) => {
|
|
||||||
assert(false, 'Expected no events to be sent');
|
|
||||||
};
|
|
||||||
let sentErrorMsg = false;
|
|
||||||
fakeUser.socket.emit = (event, data) => {
|
|
||||||
if (event === 'errorMsg') {
|
|
||||||
sentErrorMsg = true;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const options = [];
|
|
||||||
for (let i = 0; i < 200; i++) {
|
|
||||||
options.push('option ' + i);
|
|
||||||
}
|
|
||||||
pollModule.handleNewPoll(fakeUser, {
|
|
||||||
title: 'test poll',
|
|
||||||
opts: options,
|
|
||||||
obscured: false
|
|
||||||
});
|
|
||||||
assert(sentErrorMsg, 'Expected to send errorMsg since ack was missing');
|
|
||||||
});
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
@ -1,138 +0,0 @@
|
||||||
const VoteskipModule = require('../../lib/channel/voteskip');
|
|
||||||
const assert = require('assert');
|
|
||||||
const Flags = require('../../lib/flags');
|
|
||||||
|
|
||||||
describe('VoteskipModule', () => {
|
|
||||||
let fakeUser;
|
|
||||||
let fakeChannel;
|
|
||||||
let voteskipModule;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
fakeUser = {
|
|
||||||
socket: {
|
|
||||||
emit() {
|
|
||||||
|
|
||||||
}
|
|
||||||
},
|
|
||||||
is() {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
};
|
|
||||||
fakeChannel = {
|
|
||||||
logger: {
|
|
||||||
log() {
|
|
||||||
|
|
||||||
}
|
|
||||||
},
|
|
||||||
modules: {
|
|
||||||
permissions: {
|
|
||||||
canSeeVoteskipResults() {
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
canVoteskip() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
options: {
|
|
||||||
get(key) {
|
|
||||||
if (key === 'voteskip_ratio') {
|
|
||||||
return 0.5;
|
|
||||||
} else if (key === 'allow_voteskip') {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
playlist: {
|
|
||||||
_playNext() {
|
|
||||||
},
|
|
||||||
|
|
||||||
meta: {
|
|
||||||
count: 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
users: [fakeUser],
|
|
||||||
broadcastAll() {
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
voteskipModule = new VoteskipModule(fakeChannel);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('#update', () => {
|
|
||||||
it('resets the vote before changing to the next video', () => {
|
|
||||||
let reset = false, playNext = false;
|
|
||||||
fakeChannel.modules.playlist._playNext = () => {
|
|
||||||
if (!reset) {
|
|
||||||
assert(false, 'Expected voteskip reset prior to playlist._playNext');
|
|
||||||
}
|
|
||||||
|
|
||||||
playNext = true;
|
|
||||||
};
|
|
||||||
fakeUser.socket.emit = (event, data) => {
|
|
||||||
if (event === 'voteskip') {
|
|
||||||
assert.deepEqual(data, { count: 0, need: 0 });
|
|
||||||
reset = true;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
voteskipModule.poll = {
|
|
||||||
toUpdateFrame() {
|
|
||||||
return { counts: [1] };
|
|
||||||
}
|
|
||||||
};
|
|
||||||
voteskipModule.update();
|
|
||||||
assert.equal(voteskipModule.poll, false, 'Expected voteskip poll to be reset to false');
|
|
||||||
assert(reset, 'Expected voteskip to be reset');
|
|
||||||
assert(playNext, 'Expected playlist to be advanced');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('broadcasts a message', () => {
|
|
||||||
let sentMessage = false;
|
|
||||||
fakeChannel.broadcastAll = (frame, data) => {
|
|
||||||
assert.strictEqual(frame, 'chatMsg');
|
|
||||||
assert(/voteskip passed/i.test(data.msg), 'Expected voteskip passed message')
|
|
||||||
sentMessage = true;
|
|
||||||
};
|
|
||||||
voteskipModule.poll = {
|
|
||||||
toUpdateFrame() {
|
|
||||||
return { counts: [1] };
|
|
||||||
}
|
|
||||||
};
|
|
||||||
voteskipModule.update();
|
|
||||||
assert(sentMessage, 'Expected voteskip passed message');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('#calcUsercounts', () => {
|
|
||||||
it('calculates correctly', () => {
|
|
||||||
fakeChannel.users = [
|
|
||||||
// 1 with permission and not AFK
|
|
||||||
{ is(f) { return false; }, _has_permission: true },
|
|
||||||
// 1 without permission and not AFK
|
|
||||||
{ is(f) { return false; }, _has_permission: false },
|
|
||||||
// 1 afk with permission
|
|
||||||
{ is(f) { return f === Flags.U_AFK; }, _has_permission: true },
|
|
||||||
// 1 afk without permission
|
|
||||||
{ is(f) { return f === Flags.U_AFK; }, _has_permission: false }
|
|
||||||
]
|
|
||||||
|
|
||||||
fakeChannel.modules.permissions.canVoteskip = u => u._has_permission;
|
|
||||||
|
|
||||||
const {
|
|
||||||
total,
|
|
||||||
eligible,
|
|
||||||
afk,
|
|
||||||
noPermission
|
|
||||||
} = voteskipModule.calcUsercounts();
|
|
||||||
|
|
||||||
assert.strictEqual(total, 4, 'mismatch: total');
|
|
||||||
assert.strictEqual(eligible, 1, 'mismatch: eligible');
|
|
||||||
// Permission is checked before AFK; if user is AFK and also does
|
|
||||||
// not have permission, they should be counted in noPermission
|
|
||||||
// but not afk
|
|
||||||
assert.strictEqual(afk, 1, 'mismatch: afk');
|
|
||||||
assert.strictEqual(noPermission, 2, 'mismatch: noPermission');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,56 +0,0 @@
|
||||||
const assert = require('assert');
|
|
||||||
const CamoConfig = require('../../lib/configuration/camoconfig').CamoConfig;
|
|
||||||
|
|
||||||
describe('CamoConfig', () => {
|
|
||||||
describe('#constructor', () => {
|
|
||||||
it('strips trailing slashes from the server', () => {
|
|
||||||
const config = new CamoConfig({
|
|
||||||
camo: {
|
|
||||||
server: 'http://abc.xyz/'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
assert.strictEqual(config.getServer(), 'http://abc.xyz');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('defaults to enabled=false', () => {
|
|
||||||
assert.strictEqual(new CamoConfig().isEnabled(), false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('validates that encoding must be either url or hex', () => {
|
|
||||||
|
|
||||||
assert.throws(() => {
|
|
||||||
new CamoConfig({
|
|
||||||
camo: {
|
|
||||||
encoding: 'asdjfnasdf'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, /must be either 'url' or 'hex'/);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('#getWhitelistedDomains', () => {
|
|
||||||
it('defaults to an empty array', () => {
|
|
||||||
assert.deepStrictEqual(new CamoConfig().getWhitelistedDomains(), []);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('#getEncoding', () => {
|
|
||||||
it('defaults to url', () => {
|
|
||||||
assert.deepStrictEqual(new CamoConfig().getEncoding(), 'url');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('#getWhitelistedDomainsRegexp', () => {
|
|
||||||
it('generates a regex based on the whitelisted domains', () => {
|
|
||||||
const config = new CamoConfig({
|
|
||||||
camo: {
|
|
||||||
server: 'localhost:8081',
|
|
||||||
'whitelisted-domains': ['abc.xyz', 'tii.kzz.qqq']
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const re = config.getWhitelistedDomainsRegexp();
|
|
||||||
assert.deepStrictEqual(re, /\.abc\.xyz$|\.tii\.kzz\.qqq$/i);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
const assert = require('assert');
|
|
||||||
const PrometheusConfig = require('../../lib/configuration/prometheusconfig').PrometheusConfig;
|
|
||||||
|
|
||||||
describe('PrometheusConfig', () => {
|
|
||||||
describe('#constructor', () => {
|
|
||||||
it('defaults to enabled=false', () => {
|
|
||||||
assert.strictEqual(new PrometheusConfig().isEnabled(), false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,50 +0,0 @@
|
||||||
const assert = require('assert');
|
|
||||||
const { createTransport } = require('nodemailer');
|
|
||||||
const { EmailController } = require('../../lib/controller/email');
|
|
||||||
const { EmailConfig } = require('../../lib/configuration/emailconfig');
|
|
||||||
|
|
||||||
describe('EmailController', () => {
|
|
||||||
describe('sendPasswordReset', () => {
|
|
||||||
it('sends a password reset email', () => {
|
|
||||||
const mailer = createTransport({
|
|
||||||
jsonTransport: true
|
|
||||||
});
|
|
||||||
const config = new EmailConfig({
|
|
||||||
'password-reset': {
|
|
||||||
from: 'Test <test@example.com>',
|
|
||||||
subject: 'Password Reset',
|
|
||||||
'html-template': 'Reset <a href="$url$">here</a> $user$',
|
|
||||||
'text-template': 'Text is better than HTML $user$ $url$'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const controller = new EmailController(mailer, config);
|
|
||||||
|
|
||||||
return controller.sendPasswordReset({
|
|
||||||
address: 'some-user@example.com',
|
|
||||||
username: 'SomeUser',
|
|
||||||
url: 'http://localhost/password-reset/blah'
|
|
||||||
}).then(info => {
|
|
||||||
const sentMessage = JSON.parse(info.message);
|
|
||||||
|
|
||||||
assert.strictEqual(sentMessage.subject, 'Password Reset');
|
|
||||||
assert.deepStrictEqual(
|
|
||||||
sentMessage.from,
|
|
||||||
{ name: 'Test', address: 'test@example.com' }
|
|
||||||
);
|
|
||||||
assert.deepStrictEqual(
|
|
||||||
sentMessage.to,
|
|
||||||
[{ name: 'SomeUser', address: 'some-user@example.com' }]
|
|
||||||
);
|
|
||||||
assert.strictEqual(
|
|
||||||
sentMessage.html,
|
|
||||||
'Reset <a href="http://localhost/password-reset/blah">here</a> SomeUser'
|
|
||||||
);
|
|
||||||
assert.strictEqual(
|
|
||||||
sentMessage.text,
|
|
||||||
'Text is better than HTML SomeUser http://localhost/password-reset/blah'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,435 +0,0 @@
|
||||||
const assert = require('assert');
|
|
||||||
const { validate, convert, lookup } = require('../lib/custom-media');
|
|
||||||
const http = require('http');
|
|
||||||
|
|
||||||
describe('custom-media', () => {
|
|
||||||
let valid, invalid;
|
|
||||||
beforeEach(() => {
|
|
||||||
invalid = valid = {
|
|
||||||
title: 'Test Video',
|
|
||||||
duration: 10,
|
|
||||||
live: false,
|
|
||||||
thumbnail: 'https://example.com/thumb.jpg',
|
|
||||||
sources: [
|
|
||||||
{
|
|
||||||
url: 'https://example.com/video.mp4',
|
|
||||||
contentType: 'video/mp4',
|
|
||||||
quality: 1080,
|
|
||||||
bitrate: 5000
|
|
||||||
}
|
|
||||||
],
|
|
||||||
textTracks: [
|
|
||||||
{
|
|
||||||
url: 'https://example.com/subtitles.vtt',
|
|
||||||
contentType: 'text/vtt',
|
|
||||||
name: 'English Subtitles'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('#validate', () => {
|
|
||||||
it('accepts valid metadata', () => {
|
|
||||||
validate(valid);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('accepts valid metadata with no optional params', () => {
|
|
||||||
delete valid.live;
|
|
||||||
delete valid.thumbnail;
|
|
||||||
delete valid.textTracks;
|
|
||||||
delete valid.sources[0].bitrate;
|
|
||||||
|
|
||||||
validate(valid);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('rejects missing title', () => {
|
|
||||||
delete invalid.title;
|
|
||||||
|
|
||||||
assert.throws(() => validate(invalid), /title must be a string/);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('rejects blank title', () => {
|
|
||||||
invalid.title = '';
|
|
||||||
|
|
||||||
assert.throws(() => validate(invalid), /title must not be blank/);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('rejects non-numeric duration', () => {
|
|
||||||
invalid.duration = 'twenty four seconds';
|
|
||||||
|
|
||||||
assert.throws(() => validate(invalid), /duration must be a number/);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('rejects non-finite duration', () => {
|
|
||||||
invalid.duration = NaN;
|
|
||||||
|
|
||||||
assert.throws(() => validate(invalid), /duration must be a non-negative finite number/);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('rejects negative duration', () => {
|
|
||||||
invalid.duration = -1;
|
|
||||||
|
|
||||||
assert.throws(() => validate(invalid), /duration must be a non-negative finite number/);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('rejects non-boolean live', () => {
|
|
||||||
invalid.live = 'false';
|
|
||||||
|
|
||||||
assert.throws(() => validate(invalid), /live must be a boolean/);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('rejects non-string thumbnail', () => {
|
|
||||||
invalid.thumbnail = 1234;
|
|
||||||
|
|
||||||
assert.throws(() => validate(invalid), /thumbnail must be a string/);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('rejects invalid thumbnail URL', () => {
|
|
||||||
invalid.thumbnail = 'http://example.com/thumb.jpg';
|
|
||||||
|
|
||||||
assert.throws(() => validate(invalid), /URL protocol must be HTTPS/);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('rejects non-live DASH', () => {
|
|
||||||
invalid.live = false;
|
|
||||||
invalid.sources[0].contentType = 'application/dash+xml';
|
|
||||||
|
|
||||||
assert.throws(
|
|
||||||
() => validate(invalid),
|
|
||||||
/contentType "application\/dash\+xml" requires live: true/
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('#validateSources', () => {
|
|
||||||
it('rejects non-array sources', () => {
|
|
||||||
invalid.sources = { a: 'b' };
|
|
||||||
|
|
||||||
assert.throws(() => validate(invalid), /sources must be a list/);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('rejects empty source list', () => {
|
|
||||||
invalid.sources = [];
|
|
||||||
|
|
||||||
assert.throws(() => validate(invalid), /source list must be nonempty/);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('rejects non-string source url', () => {
|
|
||||||
invalid.sources[0].url = 1234;
|
|
||||||
|
|
||||||
assert.throws(() => validate(invalid), /source URL must be a string/);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('rejects invalid source URL', () => {
|
|
||||||
invalid.sources[0].url = 'http://example.com/thumb.jpg';
|
|
||||||
|
|
||||||
assert.throws(() => validate(invalid), /URL protocol must be HTTPS/);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('rejects unacceptable source contentType', () => {
|
|
||||||
invalid.sources[0].contentType = 'rtmp/flv';
|
|
||||||
|
|
||||||
assert.throws(() => validate(invalid), /unacceptable source contentType/);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('rejects unacceptable source quality', () => {
|
|
||||||
invalid.sources[0].quality = 144;
|
|
||||||
|
|
||||||
assert.throws(() => validate(invalid), /unacceptable source quality/);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('rejects non-numeric source bitrate', () => {
|
|
||||||
invalid.sources[0].bitrate = '1000kbps'
|
|
||||||
|
|
||||||
assert.throws(() => validate(invalid), /source bitrate must be a number/);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('rejects non-finite source bitrate', () => {
|
|
||||||
invalid.sources[0].bitrate = Infinity;
|
|
||||||
|
|
||||||
assert.throws(() => validate(invalid), /source bitrate must be a non-negative finite number/);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('rejects negative source bitrate', () => {
|
|
||||||
invalid.sources[0].bitrate = -1000;
|
|
||||||
|
|
||||||
assert.throws(() => validate(invalid), /source bitrate must be a non-negative finite number/);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('#validateTextTracks', () => {
|
|
||||||
it('rejects non-array text track list', () => {
|
|
||||||
invalid.textTracks = { a: 'b' };
|
|
||||||
|
|
||||||
assert.throws(() => validate(invalid), /textTracks must be a list/);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('rejects non-string track url', () => {
|
|
||||||
invalid.textTracks[0].url = 1234;
|
|
||||||
|
|
||||||
assert.throws(() => validate(invalid), /text track URL must be a string/);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('rejects invalid track URL', () => {
|
|
||||||
invalid.textTracks[0].url = 'http://example.com/thumb.jpg';
|
|
||||||
|
|
||||||
assert.throws(() => validate(invalid), /URL protocol must be HTTPS/);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('rejects unacceptable track contentType', () => {
|
|
||||||
invalid.textTracks[0].contentType = 'text/plain';
|
|
||||||
|
|
||||||
assert.throws(() => validate(invalid), /unacceptable text track contentType/);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('rejects non-string track name', () => {
|
|
||||||
invalid.textTracks[0].name = 1234;
|
|
||||||
|
|
||||||
assert.throws(() => validate(invalid), /text track name must be a string/);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('rejects blank track name', () => {
|
|
||||||
invalid.textTracks[0].name = '';
|
|
||||||
|
|
||||||
assert.throws(() => validate(invalid), /text track name must be nonempty/);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('#validateURL', () => {
|
|
||||||
it('rejects non-URLs', () => {
|
|
||||||
invalid.sources[0].url = 'not a url';
|
|
||||||
|
|
||||||
assert.throws(() => validate(invalid), /invalid URL/);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('rejects non-https', () => {
|
|
||||||
invalid.sources[0].url = 'http://example.com/thumb.jpg';
|
|
||||||
|
|
||||||
assert.throws(() => validate(invalid), /URL protocol must be HTTPS/);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('rejects IP addresses', () => {
|
|
||||||
invalid.sources[0].url = 'https://0.0.0.0/thumb.jpg';
|
|
||||||
|
|
||||||
assert.throws(() => validate(invalid), /URL hostname must be a domain name/);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('#convert', () => {
|
|
||||||
let expected;
|
|
||||||
let id = 'testing';
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
expected = {
|
|
||||||
id: 'testing',
|
|
||||||
title: 'Test Video',
|
|
||||||
seconds: 10,
|
|
||||||
duration: '00:10',
|
|
||||||
type: 'cm',
|
|
||||||
meta: {
|
|
||||||
direct: {
|
|
||||||
1080: [
|
|
||||||
{
|
|
||||||
link: 'https://example.com/video.mp4',
|
|
||||||
contentType: 'video/mp4',
|
|
||||||
quality: 1080
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
textTracks: [
|
|
||||||
{
|
|
||||||
url: 'https://example.com/subtitles.vtt',
|
|
||||||
contentType: 'text/vtt',
|
|
||||||
name: 'English Subtitles'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
function cleanForComparison(actual) {
|
|
||||||
actual = actual.pack();
|
|
||||||
|
|
||||||
// Strip out extraneous undefineds
|
|
||||||
for (let key in actual.meta) {
|
|
||||||
if (actual.meta[key] === undefined) delete actual.meta[key];
|
|
||||||
}
|
|
||||||
|
|
||||||
return actual;
|
|
||||||
}
|
|
||||||
|
|
||||||
it('converts custom metadata to a CyTube Media object', () => {
|
|
||||||
const media = convert(id, valid);
|
|
||||||
const actual = cleanForComparison(media);
|
|
||||||
|
|
||||||
assert.deepStrictEqual(actual, expected);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('sets duration to 0 if live = true', () => {
|
|
||||||
valid.live = true;
|
|
||||||
expected.duration = '00:00';
|
|
||||||
expected.seconds = 0;
|
|
||||||
|
|
||||||
const media = convert(id, valid);
|
|
||||||
const actual = cleanForComparison(media);
|
|
||||||
|
|
||||||
assert.deepStrictEqual(actual, expected);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('#lookup', () => {
|
|
||||||
let server;
|
|
||||||
let serveFunc;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
serveFunc = function (req, res) {
|
|
||||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
||||||
res.write(JSON.stringify(valid, null, 2));
|
|
||||||
res.end();
|
|
||||||
};
|
|
||||||
|
|
||||||
server = http.createServer((req, res) => serveFunc(req, res));
|
|
||||||
server.listen(10111);
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(done => {
|
|
||||||
server.close(() => done());
|
|
||||||
});
|
|
||||||
|
|
||||||
it('retrieves metadata', () => {
|
|
||||||
function cleanForComparison(actual) {
|
|
||||||
actual = actual.pack();
|
|
||||||
delete actual.id;
|
|
||||||
|
|
||||||
// Strip out extraneous undefineds
|
|
||||||
for (let key in actual.meta) {
|
|
||||||
if (actual.meta[key] === undefined) delete actual.meta[key];
|
|
||||||
}
|
|
||||||
|
|
||||||
return actual;
|
|
||||||
}
|
|
||||||
|
|
||||||
const expected = {
|
|
||||||
title: 'Test Video',
|
|
||||||
seconds: 10,
|
|
||||||
duration: '00:10',
|
|
||||||
type: 'cm',
|
|
||||||
meta: {
|
|
||||||
direct: {
|
|
||||||
1080: [
|
|
||||||
{
|
|
||||||
link: 'https://example.com/video.mp4',
|
|
||||||
contentType: 'video/mp4',
|
|
||||||
quality: 1080
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
textTracks: [
|
|
||||||
{
|
|
||||||
url: 'https://example.com/subtitles.vtt',
|
|
||||||
contentType: 'text/vtt',
|
|
||||||
name: 'English Subtitles'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return lookup('http://127.0.0.1:10111/').then(result => {
|
|
||||||
assert.deepStrictEqual(cleanForComparison(result), expected);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('rejects the wrong content-type', () => {
|
|
||||||
serveFunc = (req, res) => {
|
|
||||||
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
|
||||||
res.write(JSON.stringify(valid, null, 2));
|
|
||||||
res.end();
|
|
||||||
};
|
|
||||||
|
|
||||||
return lookup('http://127.0.0.1:10111/').then(() => {
|
|
||||||
throw new Error('Expected failure due to wrong content-type');
|
|
||||||
}).catch(error => {
|
|
||||||
assert.strictEqual(
|
|
||||||
error.message,
|
|
||||||
'Expected content-type application/json, not text/plain'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('rejects non-200 status codes', () => {
|
|
||||||
serveFunc = (req, res) => {
|
|
||||||
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
||||||
res.write(JSON.stringify(valid, null, 2));
|
|
||||||
res.end();
|
|
||||||
};
|
|
||||||
|
|
||||||
return lookup('http://127.0.0.1:10111/').then(() => {
|
|
||||||
throw new Error('Expected failure due to 404');
|
|
||||||
}).catch(error => {
|
|
||||||
assert.strictEqual(
|
|
||||||
error.message,
|
|
||||||
'Expected HTTP 200 OK, not 404 Not Found'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('rejects responses >100KB', () => {
|
|
||||||
serveFunc = (req, res) => {
|
|
||||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
||||||
res.write(Buffer.alloc(200 * 1024));
|
|
||||||
res.end();
|
|
||||||
};
|
|
||||||
|
|
||||||
return lookup('http://127.0.0.1:10111/').then(() => {
|
|
||||||
throw new Error('Expected failure due to response size');
|
|
||||||
}).catch(error => {
|
|
||||||
assert.strictEqual(
|
|
||||||
error.message,
|
|
||||||
'Response size exceeds 100KB'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('times out', () => {
|
|
||||||
serveFunc = (req, res) => {
|
|
||||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
||||||
res.write(JSON.stringify(valid, null, 2));
|
|
||||||
|
|
||||||
setTimeout(() => res.end(), 100);
|
|
||||||
};
|
|
||||||
|
|
||||||
return lookup('http://127.0.0.1:10111/', { timeout: 1 }).then(() => {
|
|
||||||
throw new Error('Expected failure due to request timeout');
|
|
||||||
}).catch(error => {
|
|
||||||
assert.strictEqual(
|
|
||||||
error.message,
|
|
||||||
'Request timed out'
|
|
||||||
);
|
|
||||||
assert.strictEqual(error.code, 'ETIMEDOUT');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('rejects URLs with non-http(s) protocols', () => {
|
|
||||||
return lookup('ftp://127.0.0.1:10111/').then(() => {
|
|
||||||
throw new Error('Expected failure due to unacceptable URL protocol');
|
|
||||||
}).catch(error => {
|
|
||||||
assert.strictEqual(
|
|
||||||
error.message,
|
|
||||||
'Unacceptable protocol "ftp:". Custom metadata must be retrieved'
|
|
||||||
+ ' by HTTP or HTTPS'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('rejects invalid URLs', () => {
|
|
||||||
return lookup('not valid').then(() => {
|
|
||||||
throw new Error('Expected failure due to invalid URL');
|
|
||||||
}).catch(error => {
|
|
||||||
assert.strictEqual(
|
|
||||||
error.message,
|
|
||||||
'Invalid URL "not valid"'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,47 +0,0 @@
|
||||||
const customembed = require('../lib/customembed');
|
|
||||||
const assert = require('assert');
|
|
||||||
const crypto = require("crypto");
|
|
||||||
|
|
||||||
function sha256(input) {
|
|
||||||
let hash = crypto.createHash('sha256');
|
|
||||||
hash.update(input);
|
|
||||||
return hash.digest('base64');
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('customembed', () => {
|
|
||||||
describe('#filter', () => {
|
|
||||||
it('rejects <embed> inputs', () => {
|
|
||||||
const input = '<embed src="https://example.com/baz.swf" type="application/x-shockwave-flash"></embed>';
|
|
||||||
assert.throws(() => { customembed.filter(input) }, /must be an <iframe>/);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('rejects <object> inputs', () => {
|
|
||||||
const input = '<object data="https://example.com/baz.swf" type="application/x-shockwave-flash"></object>';
|
|
||||||
assert.throws(() => { customembed.filter(input) }, /must be an <iframe>/);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('rejects plain-HTTP <iframe> inputs', () => {
|
|
||||||
const input = '<iframe src="http://foo.bar/baz.swf"></iframe>';
|
|
||||||
assert.throws(() => { customembed.filter(input) }, /must be HTTPS/);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('accepts a valid iframe', () => {
|
|
||||||
let input = '<iframe src="https://example.com/video.html"</iframe>';
|
|
||||||
const { id, title, seconds, duration, type, meta } = customembed.filter(input).pack();
|
|
||||||
const { embed } = meta;
|
|
||||||
|
|
||||||
assert.strictEqual(id, `cu:${sha256(input)}`);
|
|
||||||
assert.strictEqual(title, 'Custom Media');
|
|
||||||
assert.strictEqual(seconds, 0);
|
|
||||||
assert.strictEqual(duration, '--:--');
|
|
||||||
assert.strictEqual(type, 'cu');
|
|
||||||
assert.deepStrictEqual(
|
|
||||||
embed,
|
|
||||||
{
|
|
||||||
tag: 'iframe',
|
|
||||||
src: 'https://example.com/video.html'
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,108 +0,0 @@
|
||||||
const assert = require('assert');
|
|
||||||
const sinon = require('sinon');
|
|
||||||
const TestUtilDB = require('../testutil/db');
|
|
||||||
const AliasesDB = require('../../lib/db/aliases').AliasesDB;
|
|
||||||
|
|
||||||
describe('AliasesDB', () => {
|
|
||||||
let mockTx, mockDB, aliasesDB;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
mockTx = new TestUtilDB.MockTx();
|
|
||||||
mockDB = new TestUtilDB.MockDB(mockTx);
|
|
||||||
aliasesDB = new AliasesDB(mockDB);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('#addAlias', () => {
|
|
||||||
it('adds a new alias', () => {
|
|
||||||
const ip = '1.2.3.4';
|
|
||||||
const name = 'foo';
|
|
||||||
sinon.stub(mockTx, 'table').withArgs('aliases').returns(mockTx);
|
|
||||||
sinon.stub(mockTx, 'where').withArgs({ ip, name }).returns(mockTx);
|
|
||||||
const del = sinon.stub(mockTx, 'del').resolves();
|
|
||||||
const insert = sinon.stub(mockTx, 'insert').resolves();
|
|
||||||
return aliasesDB.addAlias(ip, name).then(() => {
|
|
||||||
assert(del.called, 'Expected old alias to be purged');
|
|
||||||
assert(insert.called, 'Expected new alias to be inserted');
|
|
||||||
const record = insert.getCall(0).args[0];
|
|
||||||
assert.strictEqual(record.ip, ip);
|
|
||||||
assert.strictEqual(record.name, name);
|
|
||||||
assert(typeof record.time === 'number', 'Expected time field to be a number');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('#getAliasesByIP', () => {
|
|
||||||
it('retrieves aliases by full IP', () => {
|
|
||||||
const ip = '1.2.3.4';
|
|
||||||
const rows = [
|
|
||||||
{ ip, name: 'foo' },
|
|
||||||
{ ip, name: 'bar' }
|
|
||||||
];
|
|
||||||
|
|
||||||
sinon.stub(mockTx, 'table').withArgs('aliases').returns(mockTx);
|
|
||||||
sinon.stub(mockTx, 'where').withArgs({ ip }).returns(mockTx);
|
|
||||||
sinon.stub(mockTx, 'select').returns(mockTx);
|
|
||||||
sinon.stub(mockTx, 'distinct').withArgs('name').returns(mockTx);
|
|
||||||
sinon.stub(mockTx, 'orderBy').withArgs('time', 'desc').returns(mockTx);
|
|
||||||
sinon.stub(mockTx, 'limit').withArgs(5).resolves(rows);
|
|
||||||
return aliasesDB.getAliasesByIP(ip).then(names => {
|
|
||||||
assert.deepStrictEqual(names.sort(), ['bar', 'foo']);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('retrieves aliases by IPv4 range', () => {
|
|
||||||
const ip = '1.2.3';
|
|
||||||
const rows = [
|
|
||||||
{ ip: ip + '.4', name: 'foo' },
|
|
||||||
{ ip: ip + '.5', name: 'bar' }
|
|
||||||
];
|
|
||||||
|
|
||||||
sinon.stub(mockTx, 'table').withArgs('aliases').returns(mockTx);
|
|
||||||
sinon.stub(mockTx, 'where').withArgs('ip', 'LIKE', `${ip}.%`).returns(mockTx);
|
|
||||||
sinon.stub(mockTx, 'select').returns(mockTx);
|
|
||||||
sinon.stub(mockTx, 'distinct').withArgs('name').returns(mockTx);
|
|
||||||
sinon.stub(mockTx, 'orderBy').withArgs('time', 'desc').returns(mockTx);
|
|
||||||
sinon.stub(mockTx, 'limit').withArgs(5).resolves(rows);
|
|
||||||
return aliasesDB.getAliasesByIP(ip).then(names => {
|
|
||||||
assert.deepStrictEqual(names.sort(), ['bar', 'foo']);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('retrieves aliases by IPv6 range', () => {
|
|
||||||
const ip = '1:2:3';
|
|
||||||
const rows = [
|
|
||||||
{ ip: ip + '::4', name: 'foo' },
|
|
||||||
{ ip: ip + '::5', name: 'bar' }
|
|
||||||
];
|
|
||||||
|
|
||||||
sinon.stub(mockTx, 'table').withArgs('aliases').returns(mockTx);
|
|
||||||
const where = sinon.stub(mockTx, 'where')
|
|
||||||
.withArgs('ip', 'LIKE', `${ip}:%`).returns(mockTx);
|
|
||||||
sinon.stub(mockTx, 'select').returns(mockTx);
|
|
||||||
sinon.stub(mockTx, 'distinct').withArgs('name').returns(mockTx);
|
|
||||||
sinon.stub(mockTx, 'orderBy').withArgs('time', 'desc').returns(mockTx);
|
|
||||||
sinon.stub(mockTx, 'limit').withArgs(5).resolves(rows);
|
|
||||||
return aliasesDB.getAliasesByIP(ip).then(names => {
|
|
||||||
assert(where.called, 'Expected WHERE LIKE clause');
|
|
||||||
assert.deepStrictEqual(names.sort(), ['bar', 'foo']);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('#getIPsByName', () => {
|
|
||||||
it('retrieves IPs by name', () => {
|
|
||||||
const name = 'foo';
|
|
||||||
const rows = [
|
|
||||||
{ name, ip: '1.2.3.4' },
|
|
||||||
{ name, ip: '5.6.7.8' }
|
|
||||||
];
|
|
||||||
|
|
||||||
sinon.stub(mockTx, 'table').withArgs('aliases').returns(mockTx);
|
|
||||||
sinon.stub(mockTx, 'select').withArgs('ip').returns(mockTx);
|
|
||||||
sinon.stub(mockTx, 'where').withArgs({ name }).resolves(rows);
|
|
||||||
return aliasesDB.getIPsByName(name).then(ips => {
|
|
||||||
assert.deepStrictEqual(ips.sort(), ['1.2.3.4', '5.6.7.8']);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,84 +0,0 @@
|
||||||
const assert = require('assert');
|
|
||||||
const sinon = require('sinon');
|
|
||||||
const TestUtilDB = require('../testutil/db');
|
|
||||||
const GlobalBanDB = require('../../lib/db/globalban').GlobalBanDB;
|
|
||||||
|
|
||||||
describe('GlobalBanDB', () => {
|
|
||||||
let mockTx, mockDB, globalBanDB;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
mockTx = new TestUtilDB.MockTx();
|
|
||||||
mockDB = new TestUtilDB.MockDB(mockTx);
|
|
||||||
globalBanDB = new GlobalBanDB(mockDB);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('#listGlobalBans', () => {
|
|
||||||
it('lists bans from the database', () => {
|
|
||||||
const expected = [
|
|
||||||
{ ip: '1.2.3.4', reason: 'spam' },
|
|
||||||
{ ip: '5.6', reason: 'ham' }
|
|
||||||
];
|
|
||||||
|
|
||||||
sinon.stub(mockTx, 'table').withArgs('global_bans').returns(mockTx);
|
|
||||||
sinon.stub(mockTx, 'select').resolves(expected);
|
|
||||||
return globalBanDB.listGlobalBans().then(bans => {
|
|
||||||
assert.deepStrictEqual(bans, expected);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('#addGlobalIPBan', () => {
|
|
||||||
it('adds a new ban', () => {
|
|
||||||
const input = { ip: '1.2.3.4', reason: 'spam' };
|
|
||||||
|
|
||||||
sinon.stub(mockTx, 'table').withArgs('global_bans').returns(mockTx);
|
|
||||||
const insert = sinon.stub(mockTx, 'insert').withArgs(input).resolves();
|
|
||||||
return globalBanDB.addGlobalIPBan(input.ip, input.reason).then(() => {
|
|
||||||
assert(insert.called, 'Expected insert to be called');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('updates the ban reason for an existing ban', () => {
|
|
||||||
const input = { ip: '1.2.3.4', reason: 'spam' };
|
|
||||||
|
|
||||||
sinon.stub(mockTx, 'table').withArgs('global_bans').returns(mockTx);
|
|
||||||
const err = new Error();
|
|
||||||
err.code = 'ER_DUP_ENTRY';
|
|
||||||
const insert = sinon.stub(mockTx, 'insert').withArgs(input).rejects(err);
|
|
||||||
const where = sinon.stub(mockTx, 'where').withArgs({ ip: input.ip }).returns(mockTx);
|
|
||||||
const update = sinon.stub(mockTx, 'update').withArgs({ reason: input.reason }).resolves();
|
|
||||||
return globalBanDB.addGlobalIPBan(input.ip, input.reason).then(() => {
|
|
||||||
assert(insert.called, 'Expected insert to be called');
|
|
||||||
assert(where.called, 'Expected where({ ip }) to be called');
|
|
||||||
assert(update.called, 'Expected update({ reason }) to be called');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('doesn\'t call update for a non-ER_DUP_ENTRY error', () => {
|
|
||||||
const input = { ip: '1.2.3.4', reason: 'spam' };
|
|
||||||
|
|
||||||
sinon.stub(mockTx, 'table').withArgs('global_bans').returns(mockTx);
|
|
||||||
const err = new Error('explosions');
|
|
||||||
const insert = sinon.stub(mockTx, 'insert').withArgs(input).rejects(err);
|
|
||||||
return globalBanDB.addGlobalIPBan(input.ip, input.reason).then(() => {
|
|
||||||
assert(false, 'Expected an error');
|
|
||||||
}).catch(error => {
|
|
||||||
assert.strictEqual(error, err, 'Expected error to match');
|
|
||||||
assert(insert.called, 'Expected insert to be called');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('#removeGlobalIPBan', () => {
|
|
||||||
it('removes a global ban', () => {
|
|
||||||
const ip = '1.2.3.4';
|
|
||||||
sinon.stub(mockTx, 'table').withArgs('global_bans').returns(mockTx);
|
|
||||||
const where = sinon.stub(mockTx, 'where').returns(mockTx);
|
|
||||||
const del = sinon.stub(mockTx, 'del').resolves();
|
|
||||||
return globalBanDB.removeGlobalIPBan(ip).then(() => {
|
|
||||||
assert(where.called, 'Expected where({ ip }) to be called');
|
|
||||||
assert(del.called, 'Expected del to be called');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
const assert = require('assert');
|
|
||||||
const ffmpeg = require('../lib/ffmpeg');
|
|
||||||
const Config = require('../lib/config');
|
|
||||||
|
|
||||||
describe('ffmpeg', () => {
|
|
||||||
describe('#query', () => {
|
|
||||||
it('rejects plain http links', done => {
|
|
||||||
Config.set('ffmpeg.enabled', true);
|
|
||||||
ffmpeg.query('http://foo.bar/baz.mp4', err => {
|
|
||||||
assert(/begins with 'https:/.test(err),
|
|
||||||
`Expected error due to plain HTTP but got "${err}"`);
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,53 +0,0 @@
|
||||||
const assert = require('assert');
|
|
||||||
const sinon = require('sinon');
|
|
||||||
const GlobalBanDB = require('../../lib/db/globalban').GlobalBanDB;
|
|
||||||
const CachingGlobalBanlist = require('../../lib/io/globalban').CachingGlobalBanlist;
|
|
||||||
|
|
||||||
describe('CachingGlobalBanlist', () => {
|
|
||||||
let banlist = null;
|
|
||||||
let banDB = null;
|
|
||||||
beforeEach(() => {
|
|
||||||
banDB = new GlobalBanDB();
|
|
||||||
banlist = new CachingGlobalBanlist(banDB);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('refreshCache', () => {
|
|
||||||
it('caches bans', () => {
|
|
||||||
const bans = [{ ip: '1.1.1.1', reason: 'test' }];
|
|
||||||
sinon.stub(banDB, 'listGlobalBans').resolves(bans);
|
|
||||||
return banlist.refreshCache().then(() => {
|
|
||||||
assert(banlist.cache.has(bans[0].ip), 'Cache was not populated');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('clears removed bans', () => {
|
|
||||||
banlist.cache.add('1.1.1.1');
|
|
||||||
sinon.stub(banDB, 'listGlobalBans').resolves([]);
|
|
||||||
return banlist.refreshCache().then(() => {
|
|
||||||
assert(!banlist.cache.has('1.1.1.1'), 'Cache was not updated');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('fails open', () => {
|
|
||||||
sinon.stub(banDB, 'listGlobalBans').rejects(new Error('Broken'));
|
|
||||||
return banlist.refreshCache();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('isIPGlobalBanned', () => {
|
|
||||||
it('checks the full IP', () => {
|
|
||||||
banlist.cache.add('1.2.3.4');
|
|
||||||
assert(banlist.isIPGlobalBanned('1.2.3.4'), 'Expected IP to be banned');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('checks the range IP', () => {
|
|
||||||
banlist.cache.add('1.2.3');
|
|
||||||
assert(banlist.isIPGlobalBanned('1.2.3.4'), 'Expected IP to be banned');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('checks the wrange IP', () => {
|
|
||||||
banlist.cache.add('1.2');
|
|
||||||
assert(banlist.isIPGlobalBanned('1.2.3.4'), 'Expected IP to be banned');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,98 +0,0 @@
|
||||||
const assert = require('assert');
|
|
||||||
const IOServer = require('../../lib/io/ioserver').IOServer;
|
|
||||||
|
|
||||||
describe('IOServer', () => {
|
|
||||||
let server;
|
|
||||||
let socket;
|
|
||||||
beforeEach(() => {
|
|
||||||
server = new IOServer();
|
|
||||||
socket = {
|
|
||||||
context: {
|
|
||||||
ipAddress: '9.9.9.9'
|
|
||||||
},
|
|
||||||
handshake: {
|
|
||||||
address: '127.0.0.1',
|
|
||||||
headers: {
|
|
||||||
'x-forwarded-for': '1.2.3.4'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('#ipProxyMiddleware', () => {
|
|
||||||
it('proxies from a trusted address', done => {
|
|
||||||
server.ipProxyMiddleware(socket, error => {
|
|
||||||
assert(!error);
|
|
||||||
assert.strictEqual(socket.context.ipAddress, '1.2.3.4');
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('does not proxy from a non-trusted address', done => {
|
|
||||||
socket.handshake.address = '5.6.7.8';
|
|
||||||
server.ipProxyMiddleware(socket, error => {
|
|
||||||
assert(!error);
|
|
||||||
assert.strictEqual(socket.context.ipAddress, '5.6.7.8');
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('sets context.torConnection = true for Tor exits', () => {
|
|
||||||
// TODO
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('#ipBanMiddleware', () => {
|
|
||||||
// TODO
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('#ipThrottleMiddleware', () => {
|
|
||||||
it('throttles connections', done => {
|
|
||||||
let i = 0;
|
|
||||||
function callback(error) {
|
|
||||||
if (i < 5) {
|
|
||||||
assert(!error);
|
|
||||||
} else {
|
|
||||||
assert.strictEqual(error.message, 'Rate limit exceeded');
|
|
||||||
done();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function next() {
|
|
||||||
server.ipThrottleMiddleware(socket, error => {
|
|
||||||
callback(error);
|
|
||||||
if (++i < 6) next();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
next();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('#cookieParsingMiddleware', () => {
|
|
||||||
it('parses cookies', done => {
|
|
||||||
socket.handshake.headers.cookie = 'flavor=chocolate%20chip';
|
|
||||||
|
|
||||||
server.cookieParsingMiddleware(socket, () => {
|
|
||||||
assert.strictEqual(socket.handshake.cookies.flavor, 'chocolate chip');
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('defaults to empty objects if no cookies', done => {
|
|
||||||
server.cookieParsingMiddleware(socket, () => {
|
|
||||||
assert.deepStrictEqual(socket.handshake.cookies, {});
|
|
||||||
assert.deepStrictEqual(socket.handshake.signedCookies, {});
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('#ipSessionCookieMiddleware', () => {
|
|
||||||
// TODO
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('#authUserMiddleware', () => {
|
|
||||||
// TODO
|
|
||||||
});
|
|
||||||
});
|
|
||||||
262
test/poll.js
262
test/poll.js
|
|
@ -1,262 +0,0 @@
|
||||||
const assert = require('assert');
|
|
||||||
const { Poll } = require('../lib/poll');
|
|
||||||
|
|
||||||
describe('Poll', () => {
|
|
||||||
describe('constructor', () => {
|
|
||||||
it('constructs a poll', () => {
|
|
||||||
let poll = Poll.create(
|
|
||||||
'pollster',
|
|
||||||
'Which is better?',
|
|
||||||
[
|
|
||||||
'Coke',
|
|
||||||
'Pepsi'
|
|
||||||
]
|
|
||||||
/* default opts */
|
|
||||||
);
|
|
||||||
|
|
||||||
assert.strictEqual(poll.createdBy, 'pollster');
|
|
||||||
assert.strictEqual(poll.title, 'Which is better?');
|
|
||||||
assert.deepStrictEqual(poll.choices, ['Coke', 'Pepsi']);
|
|
||||||
assert.strictEqual(poll.hideVotes, false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('constructs a poll with hidden vote setting', () => {
|
|
||||||
let poll = Poll.create(
|
|
||||||
'pollster',
|
|
||||||
'Which is better?',
|
|
||||||
[
|
|
||||||
'Coke',
|
|
||||||
'Pepsi'
|
|
||||||
],
|
|
||||||
{ hideVotes: true }
|
|
||||||
);
|
|
||||||
|
|
||||||
assert.strictEqual(poll.hideVotes, true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('sanitizes title and choices', () => {
|
|
||||||
let poll = Poll.create(
|
|
||||||
'pollster',
|
|
||||||
'Which is better? <script></script>',
|
|
||||||
[
|
|
||||||
'<strong>Coke</strong>',
|
|
||||||
'Pepsi'
|
|
||||||
]
|
|
||||||
/* default opts */
|
|
||||||
);
|
|
||||||
|
|
||||||
assert.strictEqual(poll.title, 'Which is better? <script></script>');
|
|
||||||
assert.deepStrictEqual(poll.choices, ['<strong>Coke</strong>', 'Pepsi']);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('replaces URLs in title and choices', () => {
|
|
||||||
let poll = Poll.create(
|
|
||||||
'pollster',
|
|
||||||
'Which is better? https://example.com',
|
|
||||||
[
|
|
||||||
'Coke https://example.com',
|
|
||||||
'Pepsi'
|
|
||||||
]
|
|
||||||
/* default opts */
|
|
||||||
);
|
|
||||||
|
|
||||||
assert.strictEqual(
|
|
||||||
poll.title,
|
|
||||||
'Which is better? <a href="https://example.com" target="_blank" rel="noopener noreferer">https://example.com</a>'
|
|
||||||
);
|
|
||||||
assert.deepStrictEqual(
|
|
||||||
poll.choices,
|
|
||||||
[
|
|
||||||
'Coke <a href="https://example.com" target="_blank" rel="noopener noreferer">https://example.com</a>',
|
|
||||||
'Pepsi'
|
|
||||||
]
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('#countVote', () => {
|
|
||||||
let poll;
|
|
||||||
beforeEach(() => {
|
|
||||||
poll = Poll.create(
|
|
||||||
'pollster',
|
|
||||||
'Which is better?',
|
|
||||||
[
|
|
||||||
'Coke',
|
|
||||||
'Pepsi'
|
|
||||||
]
|
|
||||||
/* default opts */
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('counts a new vote', () => {
|
|
||||||
assert.strictEqual(poll.countVote('userA', 0), true);
|
|
||||||
assert.strictEqual(poll.countVote('userB', 1), true);
|
|
||||||
assert.strictEqual(poll.countVote('userC', 0), true);
|
|
||||||
|
|
||||||
let { counts } = poll.toUpdateFrame();
|
|
||||||
assert.deepStrictEqual(counts, [2, 1]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('does not count a revote for the same choice', () => {
|
|
||||||
assert.strictEqual(poll.countVote('userA', 0), true);
|
|
||||||
assert.strictEqual(poll.countVote('userA', 0), false);
|
|
||||||
|
|
||||||
let { counts } = poll.toUpdateFrame();
|
|
||||||
assert.deepStrictEqual(counts, [1, 0]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('changes a vote to a different choice', () => {
|
|
||||||
assert.strictEqual(poll.countVote('userA', 0), true);
|
|
||||||
assert.strictEqual(poll.countVote('userA', 1), true);
|
|
||||||
|
|
||||||
let { counts } = poll.toUpdateFrame();
|
|
||||||
assert.deepStrictEqual(counts, [0, 1]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('ignores out of range votes', () => {
|
|
||||||
assert.strictEqual(poll.countVote('userA', 1000), false);
|
|
||||||
assert.strictEqual(poll.countVote('userA', -10), false);
|
|
||||||
|
|
||||||
let { counts } = poll.toUpdateFrame();
|
|
||||||
assert.deepStrictEqual(counts, [0, 0]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('#uncountVote', () => {
|
|
||||||
let poll;
|
|
||||||
beforeEach(() => {
|
|
||||||
poll = Poll.create(
|
|
||||||
'pollster',
|
|
||||||
'Which is better?',
|
|
||||||
[
|
|
||||||
'Coke',
|
|
||||||
'Pepsi'
|
|
||||||
]
|
|
||||||
/* default opts */
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('uncounts an existing vote', () => {
|
|
||||||
assert.strictEqual(poll.countVote('userA', 0), true);
|
|
||||||
assert.strictEqual(poll.uncountVote('userA', 0), true);
|
|
||||||
|
|
||||||
let { counts } = poll.toUpdateFrame();
|
|
||||||
assert.deepStrictEqual(counts, [0, 0]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('does not uncount if there is no existing vote', () => {
|
|
||||||
assert.strictEqual(poll.uncountVote('userA', 0), false);
|
|
||||||
|
|
||||||
let { counts } = poll.toUpdateFrame();
|
|
||||||
assert.deepStrictEqual(counts, [0, 0]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('#toUpdateFrame', () => {
|
|
||||||
let poll;
|
|
||||||
beforeEach(() => {
|
|
||||||
poll = Poll.create(
|
|
||||||
'pollster',
|
|
||||||
'Which is better?',
|
|
||||||
[
|
|
||||||
'Coke',
|
|
||||||
'Pepsi'
|
|
||||||
]
|
|
||||||
/* default opts */
|
|
||||||
);
|
|
||||||
poll.countVote('userA', 0);
|
|
||||||
poll.countVote('userB', 1);
|
|
||||||
poll.countVote('userC', 0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('generates an update frame', () => {
|
|
||||||
assert.deepStrictEqual(
|
|
||||||
poll.toUpdateFrame(),
|
|
||||||
{
|
|
||||||
title: 'Which is better?',
|
|
||||||
options: ['Coke', 'Pepsi'],
|
|
||||||
counts: [2, 1],
|
|
||||||
initiator: 'pollster',
|
|
||||||
timestamp: poll.createdAt.getTime()
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('hides votes when poll is hidden', () => {
|
|
||||||
poll.hideVotes = true;
|
|
||||||
|
|
||||||
assert.deepStrictEqual(
|
|
||||||
poll.toUpdateFrame(),
|
|
||||||
{
|
|
||||||
title: 'Which is better?',
|
|
||||||
options: ['Coke', 'Pepsi'],
|
|
||||||
counts: ['?', '?'],
|
|
||||||
initiator: 'pollster',
|
|
||||||
timestamp: poll.createdAt.getTime()
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('displays hidden votes when requested', () => {
|
|
||||||
poll.hideVotes = true;
|
|
||||||
|
|
||||||
assert.deepStrictEqual(
|
|
||||||
poll.toUpdateFrame(true),
|
|
||||||
{
|
|
||||||
title: 'Which is better?',
|
|
||||||
options: ['Coke', 'Pepsi'],
|
|
||||||
counts: ['2?', '1?'],
|
|
||||||
initiator: 'pollster',
|
|
||||||
timestamp: poll.createdAt.getTime()
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('#toChannelData/fromChannelData', () => {
|
|
||||||
it('round trips a poll', () => {
|
|
||||||
let data = {
|
|
||||||
title: '<strong>ready?</strong>',
|
|
||||||
initiator: 'aUser',
|
|
||||||
options: ['yes', 'no'],
|
|
||||||
counts: [0, 1],
|
|
||||||
votes:{
|
|
||||||
'1.2.3.4': null, // Previous poll code would set removed votes to null
|
|
||||||
'5.6.7.8': 1
|
|
||||||
},
|
|
||||||
obscured: false,
|
|
||||||
timestamp: 1483414981110
|
|
||||||
};
|
|
||||||
|
|
||||||
let poll = Poll.fromChannelData(data);
|
|
||||||
|
|
||||||
// New code does not store null votes
|
|
||||||
data.votes = { '5.6.7.8': 1 };
|
|
||||||
data.retainVotes = false;
|
|
||||||
assert.deepStrictEqual(poll.toChannelData(), data);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('coerces a missing timestamp to the current time', () => {
|
|
||||||
let data = {
|
|
||||||
title: '<strong>ready?</strong>',
|
|
||||||
initiator: 'aUser',
|
|
||||||
options: ['yes', 'no'],
|
|
||||||
counts: [0, 1],
|
|
||||||
votes:{
|
|
||||||
'1.2.3.4': null,
|
|
||||||
'5.6.7.8': 1
|
|
||||||
},
|
|
||||||
obscured: false
|
|
||||||
};
|
|
||||||
|
|
||||||
let now = Date.now();
|
|
||||||
let poll = Poll.fromChannelData(data);
|
|
||||||
const { timestamp } = poll.toChannelData();
|
|
||||||
if (typeof timestamp !== 'number' || isNaN(timestamp))
|
|
||||||
assert.fail(`Unexpected timestamp: ${timestamp}`);
|
|
||||||
|
|
||||||
if (Math.abs(timestamp - now) > 1000)
|
|
||||||
assert.fail(`Unexpected timestamp: ${timestamp}`);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,65 +0,0 @@
|
||||||
const assert = require('assert');
|
|
||||||
const http = require('http');
|
|
||||||
const server = require('../lib/prometheus-server');
|
|
||||||
const PrometheusConfig = require('../lib/configuration/prometheusconfig').PrometheusConfig;
|
|
||||||
|
|
||||||
describe('prometheus-server', () => {
|
|
||||||
before(done => {
|
|
||||||
let inst = server.init(new PrometheusConfig({
|
|
||||||
prometheus: {
|
|
||||||
enabled: true,
|
|
||||||
port: 19820,
|
|
||||||
host: '127.0.0.1',
|
|
||||||
path: '/metrics'
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
inst.once('listening', () => done());
|
|
||||||
});
|
|
||||||
|
|
||||||
function checkReq(options, done) {
|
|
||||||
const req = http.request({
|
|
||||||
method: options.method,
|
|
||||||
host: '127.0.0.1',
|
|
||||||
port: 19820,
|
|
||||||
path: options.path
|
|
||||||
}, res => {
|
|
||||||
assert.strictEqual(res.statusCode, options.expectedStatusCode);
|
|
||||||
assert.strictEqual(res.headers['content-type'], options.expectedContentType);
|
|
||||||
res.on('data', () => {});
|
|
||||||
res.on('end', () => done());
|
|
||||||
});
|
|
||||||
|
|
||||||
req.end();
|
|
||||||
}
|
|
||||||
|
|
||||||
it('rejects a non-GET request', done => {
|
|
||||||
checkReq({
|
|
||||||
method: 'POST',
|
|
||||||
path: '/metrics',
|
|
||||||
expectedStatusCode: 400,
|
|
||||||
expectedContentType: 'text/plain'
|
|
||||||
}, done);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('rejects a request for the wrong path', done => {
|
|
||||||
checkReq({
|
|
||||||
method: 'GET',
|
|
||||||
path: '/qwerty',
|
|
||||||
expectedStatusCode: 400,
|
|
||||||
expectedContentType: 'text/plain'
|
|
||||||
}, done);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('accepts a request for the configured path', done => {
|
|
||||||
checkReq({
|
|
||||||
method: 'GET',
|
|
||||||
path: '/metrics',
|
|
||||||
expectedStatusCode: 200,
|
|
||||||
expectedContentType: 'text/plain; version=0.0.4; charset=utf-8'
|
|
||||||
}, done);
|
|
||||||
});
|
|
||||||
|
|
||||||
after(() => {
|
|
||||||
server.shutdown();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,52 +0,0 @@
|
||||||
const assert = require('assert');
|
|
||||||
const { RedisMessageBus } = require('../../lib/pubsub/redis');
|
|
||||||
const { EventEmitter } = require('events');
|
|
||||||
const sinon = require('sinon');
|
|
||||||
|
|
||||||
describe('RedisMessageBus', () => {
|
|
||||||
let pubClient, subClient, messageBus, publishSpy, subscribeSpy;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
pubClient = { publish: () => {} };
|
|
||||||
subClient = new EventEmitter();
|
|
||||||
|
|
||||||
subClient.subscribe = () => {};
|
|
||||||
subscribeSpy = sinon.spy(subClient, 'subscribe');
|
|
||||||
|
|
||||||
publishSpy = sinon.spy(pubClient, 'publish');
|
|
||||||
|
|
||||||
messageBus = new RedisMessageBus(pubClient, subClient, 'test');
|
|
||||||
|
|
||||||
subClient.emit('ready');
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('#onMessage', () => {
|
|
||||||
it('processes a valid message', done => {
|
|
||||||
messageBus.once('testEvent', payload => {
|
|
||||||
assert(subscribeSpy.withArgs('test').calledOnce);
|
|
||||||
assert.deepStrictEqual(payload, { foo: 'bar' });
|
|
||||||
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
|
|
||||||
messageBus.onMessage('test', '{"event":"testEvent","payload":{"foo":"bar"}}');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('processes a syntactically invalid message', done => {
|
|
||||||
messageBus.onMessage('test', 'not valid json lol');
|
|
||||||
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('#emit', () => {
|
|
||||||
it('emits messages', () => {
|
|
||||||
messageBus.emit('testEvent', { foo: 'bar' });
|
|
||||||
|
|
||||||
assert(publishSpy.withArgs('test', sinon.match(arg => {
|
|
||||||
arg = JSON.parse(arg);
|
|
||||||
return arg.event === 'testEvent' && arg.payload.foo === 'bar';
|
|
||||||
})).calledOnce);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,34 +0,0 @@
|
||||||
const Promise = require('bluebird');
|
|
||||||
|
|
||||||
function MockDB(mockTx) {
|
|
||||||
this.mockTx = mockTx;
|
|
||||||
}
|
|
||||||
|
|
||||||
MockDB.prototype = {
|
|
||||||
runTransaction: function runTransaction(fn) {
|
|
||||||
return fn(this.mockTx);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
function MockTx() {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
[
|
|
||||||
'del',
|
|
||||||
'distinct',
|
|
||||||
'insert',
|
|
||||||
'limit',
|
|
||||||
'orderBy',
|
|
||||||
'select',
|
|
||||||
'table',
|
|
||||||
'update',
|
|
||||||
'where',
|
|
||||||
].forEach(method => {
|
|
||||||
MockTx.prototype[method] = function () {
|
|
||||||
return Promise.reject(new Error(`No stub defined for method "${method}"`));
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
exports.MockDB = MockDB;
|
|
||||||
exports.MockTx = MockTx;
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
const { hash } = require('../../lib/util/hash');
|
|
||||||
const assert = require('assert');
|
|
||||||
|
|
||||||
describe('hash', () => {
|
|
||||||
describe('#hash', () => {
|
|
||||||
const input = 'this is a test';
|
|
||||||
|
|
||||||
it('hashes input correctly', () => {
|
|
||||||
const sha256_hex = '2e99758548972a8e8822ad47fa1017ff72f06f3ff6a016851f45c398732bc50c';
|
|
||||||
assert.strictEqual(hash('sha256', input, 'hex'), sha256_hex);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('hashes input to base64', () => {
|
|
||||||
const sha256_base64 = 'Lpl1hUiXKo6IIq1H+hAX/3Lwbz/2oBaFH0XDmHMrxQw=';
|
|
||||||
assert.strictEqual(hash('sha256', input, 'base64'), sha256_base64);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,66 +0,0 @@
|
||||||
const assert = require('assert');
|
|
||||||
const TokenBucket = require('../../lib/util/token-bucket').TokenBucket;
|
|
||||||
|
|
||||||
describe('TokenBucket', () => {
|
|
||||||
describe('#throttle', () => {
|
|
||||||
let bucket;
|
|
||||||
beforeEach(() => {
|
|
||||||
bucket = new TokenBucket(5, 5);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('consumes capacity and then throttles', () => {
|
|
||||||
assert(!bucket.throttle(), 'should not be empty yet');
|
|
||||||
assert(!bucket.throttle(), 'should not be empty yet');
|
|
||||||
assert(!bucket.throttle(), 'should not be empty yet');
|
|
||||||
assert(!bucket.throttle(), 'should not be empty yet');
|
|
||||||
assert(!bucket.throttle(), 'should not be empty yet');
|
|
||||||
assert(bucket.throttle(), 'should be empty now');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('refills tokens', () => {
|
|
||||||
bucket.count = 0;
|
|
||||||
const oldRefill = bucket.lastRefill = Date.now() - 1000;
|
|
||||||
assert(!bucket.throttle(), 'should have refilled');
|
|
||||||
assert(bucket.lastRefill >= oldRefill + 1000, 'should have updated lastRefill');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('refills at most {capacity} tokens', () => {
|
|
||||||
bucket.count = 0;
|
|
||||||
bucket.lastRefill = Date.now() - 10000;
|
|
||||||
bucket.throttle();
|
|
||||||
assert.strictEqual(bucket.count, 4);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('does a partial refill', () => {
|
|
||||||
bucket.count = 0;
|
|
||||||
bucket.lastRefill = Date.now() - 400;
|
|
||||||
bucket.throttle();
|
|
||||||
assert.strictEqual(bucket.count, 1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('skips refilling if delta = 0', () => {
|
|
||||||
bucket.count = 0;
|
|
||||||
const oldRefill = bucket.lastRefill;
|
|
||||||
bucket.throttle();
|
|
||||||
assert.strictEqual(bucket.count, 0);
|
|
||||||
assert.strictEqual(bucket.lastRefill, oldRefill);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('handles fractional refill rates', () => {
|
|
||||||
bucket = new TokenBucket(5, 0.1);
|
|
||||||
bucket.count = 0;
|
|
||||||
assert(bucket.throttle());
|
|
||||||
bucket.lastRefill = Date.now() - 10000;
|
|
||||||
assert(!bucket.throttle());
|
|
||||||
assert.strictEqual(bucket.count, 0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('handles infinite refill rate and capacity', () => {
|
|
||||||
bucket = new TokenBucket(Infinity, Infinity);
|
|
||||||
|
|
||||||
for (let i = 0; i < 100; i++) {
|
|
||||||
assert(!bucket.throttle(), 'should not throttle');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,266 +0,0 @@
|
||||||
const assert = require('assert');
|
|
||||||
global.CyTube = {};
|
|
||||||
require('../../www/js/tabcomplete');
|
|
||||||
|
|
||||||
|
|
||||||
describe('CyTube.tabCompletionMethods', () => {
|
|
||||||
describe('"Longest unique match"', () => {
|
|
||||||
const testcases = [
|
|
||||||
{
|
|
||||||
input: 'and his name is j',
|
|
||||||
position: 17,
|
|
||||||
options: ['johncena', 'johnstamos', 'johto'],
|
|
||||||
output: {
|
|
||||||
text: 'and his name is joh',
|
|
||||||
newPosition: 19
|
|
||||||
},
|
|
||||||
description: 'completes the longest unique substring'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: 'and his name is johnc',
|
|
||||||
position: 21,
|
|
||||||
options: ['johncena', 'johnstamos', 'johto'],
|
|
||||||
output: {
|
|
||||||
text: 'and his name is johncena ',
|
|
||||||
newPosition: 25
|
|
||||||
},
|
|
||||||
description: 'completes a unique match'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: 'johnc',
|
|
||||||
position: 5,
|
|
||||||
options: ['johncena', 'johnstamos', 'johto'],
|
|
||||||
output: {
|
|
||||||
text: 'johncena ',
|
|
||||||
newPosition: 9
|
|
||||||
},
|
|
||||||
description: 'completes a unique match at the beginning of the string'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: 'and his name is johnc',
|
|
||||||
position: 21,
|
|
||||||
options: ['asdf'],
|
|
||||||
output: {
|
|
||||||
text: 'and his name is johnc',
|
|
||||||
newPosition: 21
|
|
||||||
},
|
|
||||||
description: 'does not complete when there is no match'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: 'and his name is ',
|
|
||||||
position: 16,
|
|
||||||
options: ['asdf'],
|
|
||||||
output: {
|
|
||||||
text: 'and his name is ',
|
|
||||||
newPosition: 16
|
|
||||||
},
|
|
||||||
description: 'does not complete when there is an empty prefix'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: 'and his name is johnc',
|
|
||||||
position: 21,
|
|
||||||
options: [],
|
|
||||||
output: {
|
|
||||||
text: 'and his name is johnc',
|
|
||||||
newPosition: 21
|
|
||||||
},
|
|
||||||
description: 'does not complete when there are no options'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: '',
|
|
||||||
position: 0,
|
|
||||||
options: ['abc', 'def', 'ghi'],
|
|
||||||
output: {
|
|
||||||
text: '',
|
|
||||||
newPosition: 0
|
|
||||||
},
|
|
||||||
description: 'does not complete when the input is empty'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
testcases.forEach(test => {
|
|
||||||
it(test.description, () => {
|
|
||||||
assert.deepEqual(
|
|
||||||
CyTube.tabCompleteMethods['Longest unique match'](
|
|
||||||
test.input,
|
|
||||||
test.position,
|
|
||||||
test.options,
|
|
||||||
{}
|
|
||||||
),
|
|
||||||
test.output
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('"Cycle options"', () => {
|
|
||||||
const testcases = [
|
|
||||||
{
|
|
||||||
input: 'hey c',
|
|
||||||
position: 5,
|
|
||||||
options: ['COBOL', 'Carlos', 'carl', 'john', 'joseph', ''],
|
|
||||||
outputs: [
|
|
||||||
{
|
|
||||||
text: 'hey carl ',
|
|
||||||
newPosition: 9
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: 'hey Carlos ',
|
|
||||||
newPosition: 11
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: 'hey COBOL ',
|
|
||||||
newPosition: 10
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: 'hey carl ',
|
|
||||||
newPosition: 9
|
|
||||||
}
|
|
||||||
],
|
|
||||||
description: 'cycles through options correctly'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: 'c',
|
|
||||||
position: 1,
|
|
||||||
options: ['COBOL', 'Carlos', 'carl', 'john', 'joseph', ''],
|
|
||||||
outputs: [
|
|
||||||
{
|
|
||||||
text: 'carl ',
|
|
||||||
newPosition: 5
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: 'Carlos ',
|
|
||||||
newPosition: 7
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: 'COBOL ',
|
|
||||||
newPosition: 6
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: 'carl ',
|
|
||||||
newPosition: 5
|
|
||||||
}
|
|
||||||
],
|
|
||||||
description: 'cycles through options correctly at the beginning of the string'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: 'hey ',
|
|
||||||
position: 5,
|
|
||||||
options: ['COBOL', 'Carlos', 'carl', 'john'],
|
|
||||||
outputs: [
|
|
||||||
{
|
|
||||||
text: 'hey ',
|
|
||||||
newPosition: 5
|
|
||||||
}
|
|
||||||
],
|
|
||||||
description: 'does not complete when there is an empty prefix'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: 'hey c',
|
|
||||||
position: 6,
|
|
||||||
options: [],
|
|
||||||
outputs: [
|
|
||||||
{
|
|
||||||
text: 'hey c',
|
|
||||||
newPosition: 6
|
|
||||||
}
|
|
||||||
],
|
|
||||||
description: 'does not complete when there are no options'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: '',
|
|
||||||
position: 0,
|
|
||||||
options: ['COBOL', 'Carlos', 'carl', 'john'],
|
|
||||||
outputs: [
|
|
||||||
{
|
|
||||||
text: '',
|
|
||||||
newPosition: 0
|
|
||||||
}
|
|
||||||
],
|
|
||||||
description: 'does not complete when the input is empty'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
const complete = CyTube.tabCompleteMethods['Cycle options'];
|
|
||||||
testcases.forEach(test => {
|
|
||||||
it(test.description, () => {
|
|
||||||
var context = {};
|
|
||||||
var currentText = test.input;
|
|
||||||
var currentPosition = test.position;
|
|
||||||
for (var i = 0; i < test.outputs.length; i++) {
|
|
||||||
var output = complete(currentText, currentPosition, test.options, context);
|
|
||||||
assert.deepEqual(output, test.outputs[i]);
|
|
||||||
currentText = output.text;
|
|
||||||
currentPosition = output.newPosition;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('updates the context when the input changes to reduce the # of matches', () => {
|
|
||||||
var test = testcases[0];
|
|
||||||
var context = {};
|
|
||||||
var currentText = test.input;
|
|
||||||
var currentPosition = test.position;
|
|
||||||
|
|
||||||
var output = complete(currentText, currentPosition, test.options, context);
|
|
||||||
assert.deepEqual(context, {
|
|
||||||
start: 4,
|
|
||||||
matches: ['carl', 'Carlos', 'COBOL'],
|
|
||||||
tabIndex: 0
|
|
||||||
});
|
|
||||||
currentText = output.text;
|
|
||||||
currentPosition = output.newPosition;
|
|
||||||
|
|
||||||
output = complete(currentText, currentPosition, test.options, context);
|
|
||||||
assert.deepEqual(context, {
|
|
||||||
start: 4,
|
|
||||||
matches: ['carl', 'Carlos', 'COBOL'],
|
|
||||||
tabIndex: 1
|
|
||||||
});
|
|
||||||
currentText = output.text.replace(context.matches[1], 'jo').trim();
|
|
||||||
currentPosition = 6;
|
|
||||||
|
|
||||||
output = complete(currentText, currentPosition, test.options, context);
|
|
||||||
assert.deepEqual(context, {
|
|
||||||
start: 4,
|
|
||||||
matches: ['john', 'joseph'],
|
|
||||||
tabIndex: 0
|
|
||||||
});
|
|
||||||
assert.deepEqual(output, {
|
|
||||||
text: 'hey john ',
|
|
||||||
newPosition: 9
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('clears the context when the input changes to a non-match', () => {
|
|
||||||
var test = testcases[0];
|
|
||||||
var context = {};
|
|
||||||
var currentText = test.input;
|
|
||||||
var currentPosition = test.position;
|
|
||||||
|
|
||||||
var output = complete(currentText, currentPosition, test.options, context);
|
|
||||||
assert.deepEqual(context, {
|
|
||||||
start: 4,
|
|
||||||
matches: ['carl', 'Carlos', 'COBOL'],
|
|
||||||
tabIndex: 0
|
|
||||||
});
|
|
||||||
currentText = output.text;
|
|
||||||
currentPosition = output.newPosition;
|
|
||||||
|
|
||||||
output = complete(currentText, currentPosition, test.options, context);
|
|
||||||
assert.deepEqual(context, {
|
|
||||||
start: 4,
|
|
||||||
matches: ['carl', 'Carlos', 'COBOL'],
|
|
||||||
tabIndex: 1
|
|
||||||
});
|
|
||||||
currentText = output.text.replace(context.matches[1], 'asdf').trim();
|
|
||||||
currentPosition = 8;
|
|
||||||
|
|
||||||
output = complete(currentText, currentPosition, test.options, context);
|
|
||||||
assert.deepEqual(context, {});
|
|
||||||
assert.deepEqual(output, {
|
|
||||||
text: 'hey asdf',
|
|
||||||
newPosition: 8
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
18
test/xss.js
18
test/xss.js
|
|
@ -1,18 +0,0 @@
|
||||||
const assert = require('assert');
|
|
||||||
const XSS = require('../lib/xss');
|
|
||||||
|
|
||||||
describe('XSS', () => {
|
|
||||||
describe('sanitizeHTML', () => {
|
|
||||||
it('behaves consistently w.r.t. special chars used in emotes', () => {
|
|
||||||
const input = '`^~=| _-,;:!?/."()[]{}@$*\\&#%+á\t';
|
|
||||||
const expected = '`^~=| _-,;:!?/."()[]{}@$*\\&#%+á\t';
|
|
||||||
assert.strictEqual(XSS.sanitizeHTML(input), expected);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('disallows iframes', () => {
|
|
||||||
const input = '<iframe src="https://example.com"></iframe>';
|
|
||||||
const expected = '';
|
|
||||||
assert.strictEqual(XSS.sanitizeHTML(input), expected);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
Loading…
Reference in a new issue