tests: Include the filename in the test output

Also some minor consistency cleanups.
This commit is contained in:
Richard Hansen 2020-10-09 18:19:46 -04:00 committed by John McLear
parent 50e402193b
commit 3e14016214
13 changed files with 2296 additions and 2276 deletions

View file

@ -18,44 +18,46 @@ var apiVersion = 1;
var testPadId = makeid(); var testPadId = makeid();
describe('API Versioning', function() { describe(__filename, function() {
it('errors if can not connect', function(done) { describe('API Versioning', function() {
api it('errors if can not connect', function(done) {
.get('/api/') api
.expect(function(res) { .get('/api/')
apiVersion = res.body.currentVersion; .expect(function(res) {
if (!res.body.currentVersion) throw new Error('No version set in API'); apiVersion = res.body.currentVersion;
return; if (!res.body.currentVersion) throw new Error('No version set in API');
}) return;
.expect(200, done); })
.expect(200, done);
});
}); });
});
describe('OpenAPI definition', function() { describe('OpenAPI definition', function() {
it('generates valid openapi definition document', function(done) { it('generates valid openapi definition document', function(done) {
api api
.get('/api/openapi.json') .get('/api/openapi.json')
.expect(function(res) { .expect(function(res) {
const { valid, errors } = validateOpenAPI(res.body, 3); const { valid, errors } = validateOpenAPI(res.body, 3);
if (!valid) { if (!valid) {
const prettyErrors = JSON.stringify(errors, null, 2); const prettyErrors = JSON.stringify(errors, null, 2);
throw new Error(`Document is not valid OpenAPI. ${errors.length} validation errors:\n${prettyErrors}`); throw new Error(`Document is not valid OpenAPI. ${errors.length} validation errors:\n${prettyErrors}`);
} }
return; return;
}) })
.expect(200, done); .expect(200, done);
});
}); });
});
describe('jsonp support', function() { describe('jsonp support', function() {
it('supports jsonp calls', function(done) { it('supports jsonp calls', function(done) {
api api
.get(endPoint('createPad') + '&jsonp=jsonp_1&padID=' + testPadId) .get(endPoint('createPad') + '&jsonp=jsonp_1&padID=' + testPadId)
.expect(function(res) { .expect(function(res) {
if (!res.text.match('jsonp_1')) throw new Error('no jsonp call seen'); if (!res.text.match('jsonp_1')) throw new Error('no jsonp call seen');
}) })
.expect('Content-Type', /javascript/) .expect('Content-Type', /javascript/)
.expect(200, done); .expect(200, done);
});
}); });
}); });

View file

@ -14,76 +14,78 @@ const apiKey = common.apiKey;
var apiVersion = 1; var apiVersion = 1;
var testPadId = makeid(); var testPadId = makeid();
describe('Connectivity For Character Encoding', function(){ describe(__filename, function() {
it('can connect', function(done) { describe('Connectivity For Character Encoding', function() {
api.get('/api/') it('can connect', function(done) {
.expect('Content-Type', /json/) api.get('/api/')
.expect(200, done) .expect('Content-Type', /json/)
}); .expect(200, done)
})
describe('API Versioning', function(){
it('finds the version tag', function(done) {
api.get('/api/')
.expect(function(res){
apiVersion = res.body.currentVersion;
if (!res.body.currentVersion) throw new Error("No version set in API");
return;
})
.expect(200, done)
});
})
describe('Permission', function(){
it('errors with invalid APIKey', function(done) {
// 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
var permErrorURL = '/api/'+apiVersion+'/createPad?apikey=password&padID=test';
api.get(permErrorURL)
.expect(401, done)
});
})
describe('createPad', function(){
it('creates a new Pad', function(done) {
api.get(endPoint('createPad')+"&padID="+testPadId)
.expect(function(res){
if(res.body.code !== 0) throw new Error("Unable to create new Pad");
})
.expect('Content-Type', /json/)
.expect(200, done)
});
})
describe('setHTML', function(){
it('Sets the HTML of a Pad attempting to weird utf8 encoded content', function(done) {
fs.readFile('../tests/backend/specs/api/emojis.html', 'utf8', function(err, html) {
api.post(endPoint('setHTML'))
.send({
"padID": testPadId,
"html": html,
})
.expect(function(res){
if(res.body.code !== 0) throw new Error("Can't set HTML properly");
})
.expect('Content-Type', /json/)
.expect(200, done);
}); });
}); })
})
describe('getHTML', function(){ describe('API Versioning', function() {
it('get the HTML of Pad with emojis', function(done) { it('finds the version tag', function(done) {
api.get(endPoint('getHTML')+"&padID="+testPadId) api.get('/api/')
.expect(function(res){ .expect(function(res){
if (res.body.data.html.indexOf("&#127484") === -1) { apiVersion = res.body.currentVersion;
throw new Error("Unable to get the HTML"); if (!res.body.currentVersion) throw new Error("No version set in API");
} return;
}) })
.expect('Content-Type', /json/) .expect(200, done)
.expect(200, done) });
}); })
})
describe('Permission', function() {
it('errors with invalid APIKey', function(done) {
// 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
var permErrorURL = '/api/'+apiVersion+'/createPad?apikey=password&padID=test';
api.get(permErrorURL)
.expect(401, done)
});
})
describe('createPad', function() {
it('creates a new Pad', function(done) {
api.get(endPoint('createPad')+"&padID="+testPadId)
.expect(function(res){
if(res.body.code !== 0) throw new Error("Unable to create new Pad");
})
.expect('Content-Type', /json/)
.expect(200, done)
});
})
describe('setHTML', function() {
it('Sets the HTML of a Pad attempting to weird utf8 encoded content', function(done) {
fs.readFile('../tests/backend/specs/api/emojis.html', 'utf8', function(err, html) {
api.post(endPoint('setHTML'))
.send({
"padID": testPadId,
"html": html,
})
.expect(function(res){
if(res.body.code !== 0) throw new Error("Can't set HTML properly");
})
.expect('Content-Type', /json/)
.expect(200, done);
});
});
})
describe('getHTML', function() {
it('get the HTML of Pad with emojis', function(done) {
api.get(endPoint('getHTML')+"&padID="+testPadId)
.expect(function(res){
if (res.body.data.html.indexOf("&#127484") === -1) {
throw new Error("Unable to get the HTML");
}
})
.expect('Content-Type', /json/)
.expect(200, done)
});
})
});
/* /*

View file

@ -11,89 +11,91 @@ var authorID = "";
var padID = makeid(); var padID = makeid();
var timestamp = Date.now(); var timestamp = Date.now();
describe('API Versioning', function(){ describe(__filename, function() {
it('errors if can not connect', function(done) { describe('API Versioning', function(){
api.get('/api/') it('errors if can not connect', function(done) {
.expect(function(res){ api.get('/api/')
apiVersion = res.body.currentVersion; .expect(function(res){
if (!res.body.currentVersion) throw new Error("No version set in API"); apiVersion = res.body.currentVersion;
return; if (!res.body.currentVersion) throw new Error("No version set in API");
}) return;
.expect(200, done) })
}); .expect(200, done)
}) });
})
// BEGIN GROUP AND AUTHOR TESTS // BEGIN GROUP AND AUTHOR TESTS
///////////////////////////////////// /////////////////////////////////////
///////////////////////////////////// /////////////////////////////////////
/* Tests performed /* Tests performed
-> createPad(padID) -> createPad(padID)
-> createAuthor([name]) -- should return an authorID -> createAuthor([name]) -- should return an authorID
-> appendChatMessage(padID, text, authorID, time) -> appendChatMessage(padID, text, authorID, time)
-> getChatHead(padID) -> getChatHead(padID)
-> getChatHistory(padID) -> getChatHistory(padID)
*/ */
describe('createPad', function(){ describe('createPad', function(){
it('creates a new Pad', function(done) { it('creates a new Pad', function(done) {
api.get(endPoint('createPad')+"&padID="+padID) api.get(endPoint('createPad')+"&padID="+padID)
.expect(function(res){ .expect(function(res){
if(res.body.code !== 0) throw new Error("Unable to create new Pad"); if(res.body.code !== 0) throw new Error("Unable to create new Pad");
}) })
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
.expect(200, done) .expect(200, done)
}); });
}) })
describe('createAuthor', function(){ describe('createAuthor', function(){
it('Creates an author with a name set', function(done) { it('Creates an author with a name set', function(done) {
api.get(endPoint('createAuthor')) api.get(endPoint('createAuthor'))
.expect(function(res){ .expect(function(res){
if(res.body.code !== 0 || !res.body.data.authorID) throw new Error("Unable to create author"); 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 authorID = res.body.data.authorID; // we will be this author for the rest of the tests
}) })
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
.expect(200, done) .expect(200, done)
}); });
}) })
describe('appendChatMessage', function(){ describe('appendChatMessage', function(){
it('Adds a chat message to the pad', function(done) { it('Adds a chat message to the pad', function(done) {
api.get(endPoint('appendChatMessage')+"&padID="+padID+"&text=blalblalbha&authorID="+authorID+"&time="+timestamp) api.get(endPoint('appendChatMessage')+"&padID="+padID+"&text=blalblalbha&authorID="+authorID+"&time="+timestamp)
.expect(function(res){ .expect(function(res){
if(res.body.code !== 0) throw new Error("Unable to create chat message"); if(res.body.code !== 0) throw new Error("Unable to create chat message");
}) })
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
.expect(200, done) .expect(200, done)
}); });
}) })
describe('getChatHead', function(){ describe('getChatHead', function(){
it('Gets the head of chat', function(done) { it('Gets the head of chat', function(done) {
api.get(endPoint('getChatHead')+"&padID="+padID) api.get(endPoint('getChatHead')+"&padID="+padID)
.expect(function(res){ .expect(function(res){
if(res.body.data.chatHead !== 0) throw new Error("Chat Head Length is wrong"); 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"); if(res.body.code !== 0) throw new Error("Unable to get chat head");
}) })
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
.expect(200, done) .expect(200, done)
}); });
}) })
describe('getChatHistory', function(){ describe('getChatHistory', function(){
it('Gets Chat History of a Pad', function(done) { it('Gets Chat History of a Pad', function(done) {
api.get(endPoint('getChatHistory')+"&padID="+padID) api.get(endPoint('getChatHistory')+"&padID="+padID)
.expect(function(res){ .expect(function(res){
if(res.body.data.messages.length !== 1) throw new Error("Chat History Length is wrong"); 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"); if(res.body.code !== 0) throw new Error("Unable to get chat history");
}) })
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
.expect(200, done) .expect(200, done)
}); });
}) })
});
var endPoint = function(point){ var endPoint = function(point){
return '/api/'+apiVersion+'/'+point+'?apikey='+apiKey; return '/api/'+apiVersion+'/'+point+'?apikey='+apiKey;

View file

@ -56,38 +56,39 @@ var testImports = {
*/ */
} }
Object.keys(testImports).forEach(function (testName) { describe(__filename, function() {
var testPadId = makeid(); Object.keys(testImports).forEach(function(testName) {
test = testImports[testName]; var testPadId = makeid();
describe('createPad', function(){ test = testImports[testName];
it('creates a new Pad', function(done) { describe('createPad', function() {
api.get(endPoint('createPad')+"&padID="+testPadId) it('creates a new Pad', function(done) {
.expect(function(res){ api.get(endPoint('createPad') + "&padID=" + testPadId)
if(res.body.code !== 0) throw new Error("Unable to create new Pad"); .expect(function(res) {
}) if(res.body.code !== 0) throw new Error("Unable to create new Pad");
.expect('Content-Type', /json/) })
.expect(200, done) .expect('Content-Type', /json/)
}); .expect(200, done)
}) });
})
describe('setHTML', function(){ describe('setHTML', function() {
it('Sets the HTML', function(done) { it('Sets the HTML', function(done) {
api.get(endPoint('setHTML')+"&padID="+testPadId+"&html="+test.input) api.get(endPoint('setHTML') + "&padID=" + testPadId + "&html=" + test.input)
.expect(function(res){ .expect(function(res) {
if(res.body.code !== 0) throw new Error("Error:"+testName) if(res.body.code !== 0) throw new Error("Error:" + testName)
}) })
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
.expect(200, done) .expect(200, done)
}); });
}) })
describe('getHTML', function(){ describe('getHTML', function() {
it('Gets back the HTML of a Pad', function(done) { it('Gets back the HTML of a Pad', function(done) {
api.get(endPoint('getHTML')+"&padID="+testPadId) api.get(endPoint('getHTML') + "&padID=" + testPadId)
.expect(function(res){ .expect(function(res) {
var receivedHtml = res.body.data.html; var receivedHtml = res.body.data.html;
if (receivedHtml !== test.expectedHTML) { if (receivedHtml !== test.expectedHTML) {
throw new Error(`HTML received from export is not the one we were expecting. throw new Error(`HTML received from export is not the one we were expecting.
Test Name: Test Name:
${testName} ${testName}
@ -99,20 +100,20 @@ Object.keys(testImports).forEach(function (testName) {
Which is a different version of the originally imported one: Which is a different version of the originally imported one:
${test.input}`); ${test.input}`);
} }
}) })
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
.expect(200, done) .expect(200, done)
}); });
}) })
describe('getText', function(){ describe('getText', function() {
it('Gets back the Text of a Pad', function(done) { it('Gets back the Text of a Pad', function(done) {
api.get(endPoint('getText')+"&padID="+testPadId) api.get(endPoint('getText') + "&padID=" + testPadId)
.expect(function(res){ .expect(function(res) {
var receivedText = res.body.data.text; var receivedText = res.body.data.text;
if (receivedText !== test.expectedText) { if (receivedText !== test.expectedText) {
throw new Error(`Text received from export is not the one we were expecting. throw new Error(`Text received from export is not the one we were expecting.
Test Name: Test Name:
${testName} ${testName}
@ -124,12 +125,13 @@ Object.keys(testImports).forEach(function (testName) {
Which is a different version of the originally imported one: Which is a different version of the originally imported one:
${test.input}`); ${test.input}`);
} }
}) })
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
.expect(200, done) .expect(200, done)
}); });
}) })
});
}); });

View file

@ -23,326 +23,327 @@ var apiVersion = 1;
const testPadId = makeid(); const testPadId = makeid();
const testPadIdEnc = encodeURIComponent(testPadId); const testPadIdEnc = encodeURIComponent(testPadId);
before(async function() { agent = await common.init(); }); describe(__filename, function() {
before(async function() { agent = await common.init(); });
describe('Connectivity', function(){ describe('Connectivity', function(){
it('can connect', async function() { it('can connect', async function() {
await agent.get('/api/') await agent.get('/api/')
.expect(200)
.expect('Content-Type', /json/);
});
})
describe('API Versioning', function(){
it('finds the version tag', async function() {
await agent.get('/api/')
.expect(200)
.expect((res) => assert(res.body.currentVersion));
});
})
/*
Tests
-----
Test.
/ Create a pad
/ Set pad contents
/ Try export pad in various formats
/ Get pad contents and ensure it matches imported contents
Test.
/ Try to export a pad that doesn't exist // Expect failure
Test.
/ Try to import an unsupported file to a pad that exists
-- TODO: Test.
Try to import to a file and abort it half way through
Test.
Try to import to files of varying size.
Example Curl command for testing import URI:
curl -s -v --form file=@/home/jose/test.txt http://127.0.0.1:9001/p/foo/import
*/
describe('Imports and Exports', function(){
const backups = {};
beforeEach(async function() {
// Note: This is a shallow copy.
backups.settings = Object.assign({}, settings);
});
afterEach(async function() {
// Note: This does not unset settings that were added.
Object.assign(settings, backups.settings);
});
it('creates a new Pad, imports content to it, checks that content', async function() {
await agent.get(endPoint('createPad') + `&padID=${testPadId}`)
.expect(200)
.expect('Content-Type', /json/)
.expect((res) => assert.equal(res.body.code, 0));
await agent.post(`/p/${testPadId}/import`)
.attach('file', padText, {filename: '/test.txt', contentType: 'text/plain'})
.expect(200);
await agent.get(endPoint('getText') + `&padID=${testPadId}`)
.expect(200)
.expect((res) => assert.equal(res.body.data.text, padText.toString()));
});
it('gets read only pad Id and exports the html and text for this pad', async function(){
let ro = await agent.get(endPoint('getReadOnlyID')+"&padID="+testPadId)
.expect(200)
.expect((res) => assert.ok(JSON.parse(res.text).data.readOnlyID));
let readOnlyId = JSON.parse(ro.text).data.readOnlyID;
await agent.get(`/p/${readOnlyId}/export/html`)
.expect(200)
.expect((res) => assert(res.text.indexOf("This is the") !== -1));
await agent.get(`/p/${readOnlyId}/export/txt`)
.expect(200)
.expect((res) => assert(res.text.indexOf("This is the") !== -1));
});
describe('Import/Export tests requiring AbiWord/LibreOffice', function() {
before(function() {
if ((!settings.abiword || settings.abiword.indexOf('/') === -1) &&
(!settings.soffice || settings.soffice.indexOf('/') === -1)) {
this.skip();
}
});
// For some reason word import does not work in testing..
// TODO: fix support for .doc files..
it('Tries to import .doc that uses soffice or abiword', async function() {
await agent.post(`/p/${testPadId}/import`)
.attach('file', wordDoc, {filename: '/test.doc', contentType: 'application/msword'})
.expect(200) .expect(200)
.expect(/FrameCall\('undefined', 'ok'\);/); .expect('Content-Type', /json/);
}); });
})
it('exports DOC', async function() { describe('API Versioning', function(){
await agent.get(`/p/${testPadId}/export/doc`) it('finds the version tag', async function() {
.buffer(true).parse(superagent.parse['application/octet-stream']) await agent.get('/api/')
.expect(200) .expect(200)
.expect((res) => assert(res.body.length >= 9000)); .expect((res) => assert(res.body.currentVersion));
}); });
})
it('Tries to import .docx that uses soffice or abiword', async function() { /*
await agent.post(`/p/${testPadId}/import`) Tests
.attach('file', wordXDoc, { -----
filename: '/test.docx',
contentType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
})
.expect(200)
.expect(/FrameCall\('undefined', 'ok'\);/);
});
it('exports DOC from imported DOCX', async function() { Test.
await agent.get(`/p/${testPadId}/export/doc`) / Create a pad
.buffer(true).parse(superagent.parse['application/octet-stream']) / Set pad contents
.expect(200) / Try export pad in various formats
.expect((res) => assert(res.body.length >= 9100)); / Get pad contents and ensure it matches imported contents
});
it('Tries to import .pdf that uses soffice or abiword', async function() { Test.
await agent.post(`/p/${testPadId}/import`) / Try to export a pad that doesn't exist // Expect failure
.attach('file', pdfDoc, {filename: '/test.pdf', contentType: 'application/pdf'})
.expect(200)
.expect(/FrameCall\('undefined', 'ok'\);/);
});
it('exports PDF', async function() { Test.
await agent.get(`/p/${testPadId}/export/pdf`) / Try to import an unsupported file to a pad that exists
.buffer(true).parse(superagent.parse['application/octet-stream'])
.expect(200)
.expect((res) => assert(res.body.length >= 1000));
});
it('Tries to import .odt that uses soffice or abiword', async function() { -- TODO: Test.
await agent.post(`/p/${testPadId}/import`) Try to import to a file and abort it half way through
.attach('file', odtDoc, {filename: '/test.odt', contentType: 'application/odt'})
.expect(200)
.expect(/FrameCall\('undefined', 'ok'\);/);
});
it('exports ODT', async function() { Test.
await agent.get(`/p/${testPadId}/export/odt`) Try to import to files of varying size.
.buffer(true).parse(superagent.parse['application/octet-stream'])
.expect(200)
.expect((res) => assert(res.body.length >= 7000));
});
}); // End of AbiWord/LibreOffice tests. Example Curl command for testing import URI:
curl -s -v --form file=@/home/jose/test.txt http://127.0.0.1:9001/p/foo/import
*/
it('Tries to import .etherpad', async function() { describe('Imports and Exports', function(){
await agent.post(`/p/${testPadId}/import`) const backups = {};
.attach('file', etherpadDoc, {
filename: '/test.etherpad',
contentType: 'application/etherpad',
})
.expect(200)
.expect(/FrameCall\('true', 'ok'\);/);
});
it('exports Etherpad', async function() {
await agent.get(`/p/${testPadId}/export/etherpad`)
.buffer(true).parse(superagent.parse.text)
.expect(200)
.expect(/hello/);
});
it('exports HTML for this Etherpad file', async function() {
await agent.get(`/p/${testPadId}/export/html`)
.expect(200)
.expect('content-type', 'text/html; charset=utf-8')
.expect(/<ul class="bullet"><li><ul class="bullet"><li>hello<\/ul><\/li><\/ul>/);
});
it('Tries to import unsupported file type', async function() {
settings.allowUnknownFileEnds = false;
await agent.post(`/p/${testPadId}/import`)
.attach('file', padText, {filename: '/test.xasdasdxx', contentType: 'weirdness/jobby'})
.expect(200)
.expect((res) => assert.doesNotMatch(res.text, /FrameCall\('undefined', 'ok'\);/));
});
describe('Import authorization checks', function() {
let authorize;
const deleteTestPad = async () => {
if (await padManager.doesPadExist(testPadId)) {
const pad = await padManager.getPad(testPadId);
await pad.remove();
}
};
const createTestPad = async (text) => {
const pad = await padManager.getPad(testPadId);
if (text) await pad.setText(text);
return pad;
};
beforeEach(async function() { beforeEach(async function() {
await deleteTestPad(); // Note: This is a shallow copy.
settings.requireAuthentication = false; backups.settings = Object.assign({}, settings);
settings.requireAuthorization = true;
settings.users = {user: {password: 'user-password'}};
authorize = () => true;
backups.hooks = {};
backups.hooks.authorize = plugins.hooks.authorize || [];
plugins.hooks.authorize = [{hook_fn: (hookName, {req}, cb) => cb([authorize(req)])}];
}); });
afterEach(async function() { afterEach(async function() {
await deleteTestPad(); // Note: This does not unset settings that were added.
Object.assign(plugins.hooks, backups.hooks); Object.assign(settings, backups.settings);
}); });
it('!authn !exist -> create', async function() { it('creates a new Pad, imports content to it, checks that content', async function() {
await agent.post(`/p/${testPadIdEnc}/import`) await agent.get(endPoint('createPad') + `&padID=${testPadId}`)
.attach('file', padText, {filename: '/test.txt', contentType: 'text/plain'}) .expect(200)
.expect(200); .expect('Content-Type', /json/)
assert(await padManager.doesPadExist(testPadId)); .expect((res) => assert.equal(res.body.code, 0));
const pad = await padManager.getPad(testPadId); await agent.post(`/p/${testPadId}/import`)
assert.equal(pad.text(), padText.toString()); .attach('file', padText, {filename: '/test.txt', contentType: 'text/plain'})
.expect(200);
await agent.get(endPoint('getText') + `&padID=${testPadId}`)
.expect(200)
.expect((res) => assert.equal(res.body.data.text, padText.toString()));
}); });
it('!authn exist -> replace', async function() { it('gets read only pad Id and exports the html and text for this pad', async function(){
const pad = await createTestPad('before import'); let ro = await agent.get(endPoint('getReadOnlyID')+"&padID="+testPadId)
await agent.post(`/p/${testPadIdEnc}/import`) .expect(200)
.attach('file', padText, {filename: '/test.txt', contentType: 'text/plain'}) .expect((res) => assert.ok(JSON.parse(res.text).data.readOnlyID));
.expect(200); let readOnlyId = JSON.parse(ro.text).data.readOnlyID;
assert(await padManager.doesPadExist(testPadId));
assert.equal(pad.text(), padText.toString()); await agent.get(`/p/${readOnlyId}/export/html`)
.expect(200)
.expect((res) => assert(res.text.indexOf("This is the") !== -1));
await agent.get(`/p/${readOnlyId}/export/txt`)
.expect(200)
.expect((res) => assert(res.text.indexOf("This is the") !== -1));
}); });
it('authn anonymous !exist -> fail', async function() {
settings.requireAuthentication = true; describe('Import/Export tests requiring AbiWord/LibreOffice', function() {
await agent.post(`/p/${testPadIdEnc}/import`) before(function() {
.attach('file', padText, {filename: '/test.txt', contentType: 'text/plain'}) if ((!settings.abiword || settings.abiword.indexOf('/') === -1) &&
.expect(401); (!settings.soffice || settings.soffice.indexOf('/') === -1)) {
assert(!(await padManager.doesPadExist(testPadId))); this.skip();
}
});
// For some reason word import does not work in testing..
// TODO: fix support for .doc files..
it('Tries to import .doc that uses soffice or abiword', async function() {
await agent.post(`/p/${testPadId}/import`)
.attach('file', wordDoc, {filename: '/test.doc', contentType: 'application/msword'})
.expect(200)
.expect(/FrameCall\('undefined', 'ok'\);/);
});
it('exports DOC', async function() {
await agent.get(`/p/${testPadId}/export/doc`)
.buffer(true).parse(superagent.parse['application/octet-stream'])
.expect(200)
.expect((res) => assert(res.body.length >= 9000));
});
it('Tries to import .docx that uses soffice or abiword', async function() {
await agent.post(`/p/${testPadId}/import`)
.attach('file', wordXDoc, {
filename: '/test.docx',
contentType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
})
.expect(200)
.expect(/FrameCall\('undefined', 'ok'\);/);
});
it('exports DOC from imported DOCX', async function() {
await agent.get(`/p/${testPadId}/export/doc`)
.buffer(true).parse(superagent.parse['application/octet-stream'])
.expect(200)
.expect((res) => assert(res.body.length >= 9100));
});
it('Tries to import .pdf that uses soffice or abiword', async function() {
await agent.post(`/p/${testPadId}/import`)
.attach('file', pdfDoc, {filename: '/test.pdf', contentType: 'application/pdf'})
.expect(200)
.expect(/FrameCall\('undefined', 'ok'\);/);
});
it('exports PDF', async function() {
await agent.get(`/p/${testPadId}/export/pdf`)
.buffer(true).parse(superagent.parse['application/octet-stream'])
.expect(200)
.expect((res) => assert(res.body.length >= 1000));
});
it('Tries to import .odt that uses soffice or abiword', async function() {
await agent.post(`/p/${testPadId}/import`)
.attach('file', odtDoc, {filename: '/test.odt', contentType: 'application/odt'})
.expect(200)
.expect(/FrameCall\('undefined', 'ok'\);/);
});
it('exports ODT', async function() {
await agent.get(`/p/${testPadId}/export/odt`)
.buffer(true).parse(superagent.parse['application/octet-stream'])
.expect(200)
.expect((res) => assert(res.body.length >= 7000));
});
}); // End of AbiWord/LibreOffice tests.
it('Tries to import .etherpad', async function() {
await agent.post(`/p/${testPadId}/import`)
.attach('file', etherpadDoc, {
filename: '/test.etherpad',
contentType: 'application/etherpad',
})
.expect(200)
.expect(/FrameCall\('true', 'ok'\);/);
}); });
it('authn anonymous exist -> fail', async function() { it('exports Etherpad', async function() {
settings.requireAuthentication = true; await agent.get(`/p/${testPadId}/export/etherpad`)
const pad = await createTestPad('before import\n'); .buffer(true).parse(superagent.parse.text)
await agent.post(`/p/${testPadIdEnc}/import`) .expect(200)
.attach('file', padText, {filename: '/test.txt', contentType: 'text/plain'}) .expect(/hello/);
.expect(401);
assert.equal(pad.text(), 'before import\n');
}); });
it('authn user create !exist -> create', async function() { it('exports HTML for this Etherpad file', async function() {
settings.requireAuthentication = true; await agent.get(`/p/${testPadId}/export/html`)
await agent.post(`/p/${testPadIdEnc}/import`) .expect(200)
.auth('user', 'user-password') .expect('content-type', 'text/html; charset=utf-8')
.attach('file', padText, {filename: '/test.txt', contentType: 'text/plain'}) .expect(/<ul class="bullet"><li><ul class="bullet"><li>hello<\/ul><\/li><\/ul>/);
.expect(200);
assert(await padManager.doesPadExist(testPadId));
const pad = await padManager.getPad(testPadId);
assert.equal(pad.text(), padText.toString());
}); });
it('authn user modify !exist -> fail', async function() { it('Tries to import unsupported file type', async function() {
settings.requireAuthentication = true; settings.allowUnknownFileEnds = false;
authorize = () => 'modify'; await agent.post(`/p/${testPadId}/import`)
await agent.post(`/p/${testPadIdEnc}/import`) .attach('file', padText, {filename: '/test.xasdasdxx', contentType: 'weirdness/jobby'})
.auth('user', 'user-password') .expect(200)
.attach('file', padText, {filename: '/test.txt', contentType: 'text/plain'}) .expect((res) => assert.doesNotMatch(res.text, /FrameCall\('undefined', 'ok'\);/));
.expect(403);
assert(!(await padManager.doesPadExist(testPadId)));
}); });
it('authn user readonly !exist -> fail', async function() { describe('Import authorization checks', function() {
settings.requireAuthentication = true; let authorize;
authorize = () => 'readOnly';
await agent.post(`/p/${testPadIdEnc}/import`)
.auth('user', 'user-password')
.attach('file', padText, {filename: '/test.txt', contentType: 'text/plain'})
.expect(403);
assert(!(await padManager.doesPadExist(testPadId)));
});
it('authn user create exist -> replace', async function() { const deleteTestPad = async () => {
settings.requireAuthentication = true; if (await padManager.doesPadExist(testPadId)) {
const pad = await createTestPad('before import\n'); const pad = await padManager.getPad(testPadId);
await agent.post(`/p/${testPadIdEnc}/import`) await pad.remove();
.auth('user', 'user-password') }
.attach('file', padText, {filename: '/test.txt', contentType: 'text/plain'}) };
.expect(200);
assert.equal(pad.text(), padText.toString());
});
it('authn user modify exist -> replace', async function() { const createTestPad = async (text) => {
settings.requireAuthentication = true; const pad = await padManager.getPad(testPadId);
authorize = () => 'modify'; if (text) await pad.setText(text);
const pad = await createTestPad('before import\n'); return pad;
await agent.post(`/p/${testPadIdEnc}/import`) };
.auth('user', 'user-password')
.attach('file', padText, {filename: '/test.txt', contentType: 'text/plain'})
.expect(200);
assert.equal(pad.text(), padText.toString());
});
it('authn user readonly exist -> fail', async function() { beforeEach(async function() {
const pad = await createTestPad('before import\n'); await deleteTestPad();
settings.requireAuthentication = true; settings.requireAuthentication = false;
authorize = () => 'readOnly'; settings.requireAuthorization = true;
await agent.post(`/p/${testPadIdEnc}/import`) settings.users = {user: {password: 'user-password'}};
.auth('user', 'user-password') authorize = () => true;
.attach('file', padText, {filename: '/test.txt', contentType: 'text/plain'}) backups.hooks = {};
.expect(403); backups.hooks.authorize = plugins.hooks.authorize || [];
assert.equal(pad.text(), 'before import\n'); plugins.hooks.authorize = [{hook_fn: (hookName, {req}, cb) => cb([authorize(req)])}];
});
afterEach(async function() {
await deleteTestPad();
Object.assign(plugins.hooks, backups.hooks);
});
it('!authn !exist -> create', async function() {
await agent.post(`/p/${testPadIdEnc}/import`)
.attach('file', padText, {filename: '/test.txt', contentType: 'text/plain'})
.expect(200);
assert(await padManager.doesPadExist(testPadId));
const pad = await padManager.getPad(testPadId);
assert.equal(pad.text(), padText.toString());
});
it('!authn exist -> replace', async function() {
const pad = await createTestPad('before import');
await agent.post(`/p/${testPadIdEnc}/import`)
.attach('file', padText, {filename: '/test.txt', contentType: 'text/plain'})
.expect(200);
assert(await padManager.doesPadExist(testPadId));
assert.equal(pad.text(), padText.toString());
});
it('authn anonymous !exist -> fail', async function() {
settings.requireAuthentication = true;
await agent.post(`/p/${testPadIdEnc}/import`)
.attach('file', padText, {filename: '/test.txt', contentType: 'text/plain'})
.expect(401);
assert(!(await padManager.doesPadExist(testPadId)));
});
it('authn anonymous exist -> fail', async function() {
settings.requireAuthentication = true;
const pad = await createTestPad('before import\n');
await agent.post(`/p/${testPadIdEnc}/import`)
.attach('file', padText, {filename: '/test.txt', contentType: 'text/plain'})
.expect(401);
assert.equal(pad.text(), 'before import\n');
});
it('authn user create !exist -> create', async function() {
settings.requireAuthentication = true;
await agent.post(`/p/${testPadIdEnc}/import`)
.auth('user', 'user-password')
.attach('file', padText, {filename: '/test.txt', contentType: 'text/plain'})
.expect(200);
assert(await padManager.doesPadExist(testPadId));
const pad = await padManager.getPad(testPadId);
assert.equal(pad.text(), padText.toString());
});
it('authn user modify !exist -> fail', async function() {
settings.requireAuthentication = true;
authorize = () => 'modify';
await agent.post(`/p/${testPadIdEnc}/import`)
.auth('user', 'user-password')
.attach('file', padText, {filename: '/test.txt', contentType: 'text/plain'})
.expect(403);
assert(!(await padManager.doesPadExist(testPadId)));
});
it('authn user readonly !exist -> fail', async function() {
settings.requireAuthentication = true;
authorize = () => 'readOnly';
await agent.post(`/p/${testPadIdEnc}/import`)
.auth('user', 'user-password')
.attach('file', padText, {filename: '/test.txt', contentType: 'text/plain'})
.expect(403);
assert(!(await padManager.doesPadExist(testPadId)));
});
it('authn user create exist -> replace', async function() {
settings.requireAuthentication = true;
const pad = await createTestPad('before import\n');
await agent.post(`/p/${testPadIdEnc}/import`)
.auth('user', 'user-password')
.attach('file', padText, {filename: '/test.txt', contentType: 'text/plain'})
.expect(200);
assert.equal(pad.text(), padText.toString());
});
it('authn user modify exist -> replace', async function() {
settings.requireAuthentication = true;
authorize = () => 'modify';
const pad = await createTestPad('before import\n');
await agent.post(`/p/${testPadIdEnc}/import`)
.auth('user', 'user-password')
.attach('file', padText, {filename: '/test.txt', contentType: 'text/plain'})
.expect(200);
assert.equal(pad.text(), padText.toString());
});
it('authn user readonly exist -> fail', async function() {
const pad = await createTestPad('before import\n');
settings.requireAuthentication = true;
authorize = () => 'readOnly';
await agent.post(`/p/${testPadIdEnc}/import`)
.auth('user', 'user-password')
.attach('file', padText, {filename: '/test.txt', contentType: 'text/plain'})
.expect(403);
assert.equal(pad.text(), 'before import\n');
});
}); });
}); });
}); // End of tests. }); // End of tests.

View file

@ -11,34 +11,36 @@ const api = supertest('http://'+settings.ip+":"+settings.port);
const apiKey = common.apiKey; const apiKey = common.apiKey;
var apiVersion = '1.2.14'; var apiVersion = '1.2.14';
describe('Connectivity for instance-level API tests', function() { describe(__filename, function() {
it('can connect', function(done) { describe('Connectivity for instance-level API tests', function() {
api.get('/api/') it('can connect', function(done) {
.expect('Content-Type', /json/) api.get('/api/')
.expect(200, done) .expect('Content-Type', /json/)
.expect(200, done)
});
}); });
});
describe('getStats', function(){ describe('getStats', function() {
it('Gets the stats of a running instance', function(done) { it('Gets the stats of a running instance', function(done) {
api.get(endPoint('getStats')) api.get(endPoint('getStats'))
.expect(function(res){ .expect(function(res) {
if (res.body.code !== 0) throw new Error("getStats() failed"); if (res.body.code !== 0) throw new Error("getStats() failed");
if (!(('totalPads' in res.body.data) && (typeof res.body.data.totalPads === 'number'))) { if (!(('totalPads' in res.body.data) && (typeof res.body.data.totalPads === 'number'))) {
throw new Error(`Response to getStats() does not contain field totalPads, or it's not a number: ${JSON.stringify(res.body.data)}`); throw new Error(`Response to getStats() does not contain field totalPads, or it's not a number: ${JSON.stringify(res.body.data)}`);
} }
if (!(('totalSessions' in res.body.data) && (typeof res.body.data.totalSessions === 'number'))) { if (!(('totalSessions' in res.body.data) && (typeof res.body.data.totalSessions === 'number'))) {
throw new Error(`Response to getStats() does not contain field totalSessions, or it's not a number: ${JSON.stringify(res.body.data)}`); throw new Error(`Response to getStats() does not contain field totalSessions, or it's not a number: ${JSON.stringify(res.body.data)}`);
} }
if (!(('totalActivePads' in res.body.data) && (typeof res.body.data.totalActivePads === 'number'))) { if (!(('totalActivePads' in res.body.data) && (typeof res.body.data.totalActivePads === 'number'))) {
throw new Error(`Response to getStats() does not contain field totalActivePads, or it's not a number: ${JSON.stringify(res.body.data)}`); throw new Error(`Response to getStats() does not contain field totalActivePads, or it's not a number: ${JSON.stringify(res.body.data)}`);
} }
}) })
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
.expect(200, done); .expect(200, done);
});
}); });
}); });

File diff suppressed because it is too large Load diff

View file

@ -11,273 +11,275 @@ let authorID = '';
let sessionID = ''; let sessionID = '';
let padID = makeid(); let padID = makeid();
describe('API Versioning', function() { describe(__filename, function() {
it('errors if can not connect', async function() { describe('API Versioning', function() {
await api.get('/api/') it('errors if can not connect', async function() {
.expect(200) await api.get('/api/')
.expect((res) => { .expect(200)
assert(res.body.currentVersion); .expect((res) => {
apiVersion = res.body.currentVersion; assert(res.body.currentVersion);
}); apiVersion = res.body.currentVersion;
});
});
}); });
});
// BEGIN GROUP AND AUTHOR TESTS // BEGIN GROUP AND AUTHOR TESTS
///////////////////////////////////// /////////////////////////////////////
///////////////////////////////////// /////////////////////////////////////
/* Tests performed /* Tests performed
-> createGroup() -- should return a groupID -> createGroup() -- should return a groupID
-> listSessionsOfGroup(groupID) -- should be 0 -> listSessionsOfGroup(groupID) -- should be 0
-> deleteGroup(groupID) -> deleteGroup(groupID)
-> createGroupIfNotExistsFor(groupMapper) -- should return a groupID -> createGroupIfNotExistsFor(groupMapper) -- should return a groupID
-> createAuthor([name]) -- should return an authorID -> createAuthor([name]) -- should return an authorID
-> createAuthorIfNotExistsFor(authorMapper [, name]) -- should return an authorID -> createAuthorIfNotExistsFor(authorMapper [, name]) -- should return an authorID
-> getAuthorName(authorID) -- should return a name IE "john" -> getAuthorName(authorID) -- should return a name IE "john"
-> createSession(groupID, authorID, validUntil) -> createSession(groupID, authorID, validUntil)
-> getSessionInfo(sessionID) -> getSessionInfo(sessionID)
-> listSessionsOfGroup(groupID) -- should be 1 -> listSessionsOfGroup(groupID) -- should be 1
-> deleteSession(sessionID) -> deleteSession(sessionID)
-> getSessionInfo(sessionID) -- should have author id etc in -> getSessionInfo(sessionID) -- should have author id etc in
-> listPads(groupID) -- should be empty array
-> createGroupPad(groupID, padName [, text])
-> listPads(groupID) -- should be empty array -> listPads(groupID) -- should be empty array
-> getPublicStatus(padId) -> createGroupPad(groupID, padName [, text])
-> setPublicStatus(padId, status) -> listPads(groupID) -- should be empty array
-> getPublicStatus(padId) -> getPublicStatus(padId)
-> setPublicStatus(padId, status)
-> getPublicStatus(padId)
-> listPadsOfAuthor(authorID) -> listPadsOfAuthor(authorID)
*/ */
describe('API: Group creation and deletion', function() { describe('API: Group creation and deletion', function() {
it('createGroup', async function() { it('createGroup', async function() {
await api.get(endPoint('createGroup')) await api.get(endPoint('createGroup'))
.expect(200) .expect(200)
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
.expect((res) => { .expect((res) => {
assert.equal(res.body.code, 0); assert.equal(res.body.code, 0);
assert(res.body.data.groupID); assert(res.body.data.groupID);
groupID = res.body.data.groupID; groupID = res.body.data.groupID;
}); });
});
it('listSessionsOfGroup for empty group', async function() {
await api.get(endPoint('listSessionsOfGroup') + `&groupID=${groupID}`)
.expect(200)
.expect('Content-Type', /json/)
.expect((res) => {
assert.equal(res.body.code, 0);
assert.equal(res.body.data, null);
});
});
it('deleteGroup', async function() {
await api.get(endPoint('deleteGroup') + `&groupID=${groupID}`)
.expect(200)
.expect('Content-Type', /json/)
.expect((res) => {
assert.equal(res.body.code, 0);
});
});
it('createGroupIfNotExistsFor', async function() {
await api.get(endPoint('createGroupIfNotExistsFor') + '&groupMapper=management')
.expect(200)
.expect('Content-Type', /json/)
.expect((res) => {
assert.equal(res.body.code, 0);
assert(res.body.data.groupID);
});
});
}); });
it('listSessionsOfGroup for empty group', async function() { describe('API: Author creation', function() {
await api.get(endPoint('listSessionsOfGroup') + `&groupID=${groupID}`) it('createGroup', async function() {
.expect(200) await api.get(endPoint('createGroup'))
.expect('Content-Type', /json/) .expect(200)
.expect((res) => { .expect('Content-Type', /json/)
assert.equal(res.body.code, 0); .expect((res) => {
assert.equal(res.body.data, null); assert.equal(res.body.code, 0);
}); assert(res.body.data.groupID);
groupID = res.body.data.groupID;
});
});
it('createAuthor', async function() {
await api.get(endPoint('createAuthor'))
.expect(200)
.expect('Content-Type', /json/)
.expect((res) => {
assert.equal(res.body.code, 0);
assert(res.body.data.authorID);
});
});
it('createAuthor with name', async function() {
await api.get(endPoint('createAuthor') + '&name=john')
.expect(200)
.expect('Content-Type', /json/)
.expect((res) => {
assert.equal(res.body.code, 0);
assert(res.body.data.authorID);
authorID = res.body.data.authorID; // we will be this author for the rest of the tests
});
});
it('createAuthorIfNotExistsFor', async function() {
await api.get(endPoint('createAuthorIfNotExistsFor') + '&authorMapper=chris')
.expect(200)
.expect('Content-Type', /json/)
.expect((res) => {
assert.equal(res.body.code, 0);
assert(res.body.data.authorID);
});
});
it('getAuthorName', async function() {
await api.get(endPoint('getAuthorName') + `&authorID=${authorID}`)
.expect(200)
.expect('Content-Type', /json/)
.expect((res) => {
assert.equal(res.body.code, 0);
assert.equal(res.body.data, 'john');
});
});
}); });
it('deleteGroup', async function() { describe('API: Sessions', function() {
await api.get(endPoint('deleteGroup') + `&groupID=${groupID}`) it('createSession', async function() {
.expect(200) await api.get(endPoint('createSession') +
.expect('Content-Type', /json/) `&authorID=${authorID}&groupID=${groupID}&validUntil=999999999999`)
.expect((res) => { .expect(200)
assert.equal(res.body.code, 0); .expect('Content-Type', /json/)
}); .expect((res) => {
assert.equal(res.body.code, 0);
assert(res.body.data.sessionID);
sessionID = res.body.data.sessionID;
});
});
it('getSessionInfo', async function() {
await api.get(endPoint('getSessionInfo') + `&sessionID=${sessionID}`)
.expect(200)
.expect('Content-Type', /json/)
.expect((res) => {
assert.equal(res.body.code, 0);
assert(res.body.data.groupID);
assert(res.body.data.authorID);
assert(res.body.data.validUntil);
});
});
it('listSessionsOfGroup', async function() {
await api.get(endPoint('listSessionsOfGroup') + `&groupID=${groupID}`)
.expect(200)
.expect('Content-Type', /json/)
.expect((res) => {
assert.equal(res.body.code, 0);
assert.equal(typeof res.body.data, 'object');
});
});
it('deleteSession', async function() {
await api.get(endPoint('deleteSession') + `&sessionID=${sessionID}`)
.expect(200)
.expect('Content-Type', /json/)
.expect((res) => {
assert.equal(res.body.code, 0);
});
});
it('getSessionInfo of deleted session', async function() {
await api.get(endPoint('getSessionInfo') + `&sessionID=${sessionID}`)
.expect(200)
.expect('Content-Type', /json/)
.expect((res) => {
assert.equal(res.body.code, 1);
});
});
}); });
it('createGroupIfNotExistsFor', async function() { describe('API: Group pad management', function() {
await api.get(endPoint('createGroupIfNotExistsFor') + '&groupMapper=management') it('listPads', async function() {
.expect(200) await api.get(endPoint('listPads') + `&groupID=${groupID}`)
.expect('Content-Type', /json/) .expect(200)
.expect((res) => { .expect('Content-Type', /json/)
assert.equal(res.body.code, 0); .expect((res) => {
assert(res.body.data.groupID); assert.equal(res.body.code, 0);
}); assert.equal(res.body.data.padIDs.length, 0);
}); });
}); });
describe('API: Author creation', function() { it('createGroupPad', async function() {
it('createGroup', async function() { await api.get(endPoint('createGroupPad') + `&groupID=${groupID}&padName=${padID}`)
await api.get(endPoint('createGroup')) .expect(200)
.expect(200) .expect('Content-Type', /json/)
.expect('Content-Type', /json/) .expect((res) => {
.expect((res) => { assert.equal(res.body.code, 0);
assert.equal(res.body.code, 0); padID = res.body.data.padID;
assert(res.body.data.groupID); });
groupID = res.body.data.groupID; });
});
it('listPads after creating a group pad', async function() {
await api.get(endPoint('listPads') + `&groupID=${groupID}`)
.expect(200)
.expect('Content-Type', /json/)
.expect((res) => {
assert.equal(res.body.code, 0);
assert.equal(res.body.data.padIDs.length, 1);
});
});
}); });
it('createAuthor', async function() { describe('API: Pad security', function() {
await api.get(endPoint('createAuthor')) it('getPublicStatus', async function() {
.expect(200) await api.get(endPoint('getPublicStatus') + `&padID=${padID}`)
.expect('Content-Type', /json/) .expect(200)
.expect((res) => { .expect('Content-Type', /json/)
assert.equal(res.body.code, 0); .expect((res) => {
assert(res.body.data.authorID); assert.equal(res.body.code, 0);
}); assert.equal(res.body.data.publicStatus, false);
});
});
it('setPublicStatus', async function() {
await api.get(endPoint('setPublicStatus') + `&padID=${padID}&publicStatus=true`)
.expect(200)
.expect('Content-Type', /json/)
.expect((res) => {
assert.equal(res.body.code, 0);
});
});
it('getPublicStatus after changing public status', async function() {
await api.get(endPoint('getPublicStatus') + `&padID=${padID}`)
.expect(200)
.expect('Content-Type', /json/)
.expect((res) => {
assert.equal(res.body.code, 0);
assert.equal(res.body.data.publicStatus, true);
});
});
}); });
it('createAuthor with name', async function() { // NOT SURE HOW TO POPULAT THIS /-_-\
await api.get(endPoint('createAuthor') + '&name=john') ///////////////////////////////////////
.expect(200) ///////////////////////////////////////
.expect('Content-Type', /json/)
.expect((res) => {
assert.equal(res.body.code, 0);
assert(res.body.data.authorID);
authorID = res.body.data.authorID; // we will be this author for the rest of the tests
});
});
it('createAuthorIfNotExistsFor', async function() { describe('API: Misc', function() {
await api.get(endPoint('createAuthorIfNotExistsFor') + '&authorMapper=chris') it('listPadsOfAuthor', async function() {
.expect(200) await api.get(endPoint('listPadsOfAuthor') + `&authorID=${authorID}`)
.expect('Content-Type', /json/) .expect(200)
.expect((res) => { .expect('Content-Type', /json/)
assert.equal(res.body.code, 0); .expect((res) => {
assert(res.body.data.authorID); assert.equal(res.body.code, 0);
}); assert.equal(res.body.data.padIDs.length, 0);
}); });
});
it('getAuthorName', async function() {
await api.get(endPoint('getAuthorName') + `&authorID=${authorID}`)
.expect(200)
.expect('Content-Type', /json/)
.expect((res) => {
assert.equal(res.body.code, 0);
assert.equal(res.body.data, 'john');
});
});
});
describe('API: Sessions', function() {
it('createSession', async function() {
await api.get(endPoint('createSession') +
`&authorID=${authorID}&groupID=${groupID}&validUntil=999999999999`)
.expect(200)
.expect('Content-Type', /json/)
.expect((res) => {
assert.equal(res.body.code, 0);
assert(res.body.data.sessionID);
sessionID = res.body.data.sessionID;
});
});
it('getSessionInfo', async function() {
await api.get(endPoint('getSessionInfo') + `&sessionID=${sessionID}`)
.expect(200)
.expect('Content-Type', /json/)
.expect((res) => {
assert.equal(res.body.code, 0);
assert(res.body.data.groupID);
assert(res.body.data.authorID);
assert(res.body.data.validUntil);
});
});
it('listSessionsOfGroup', async function() {
await api.get(endPoint('listSessionsOfGroup') + `&groupID=${groupID}`)
.expect(200)
.expect('Content-Type', /json/)
.expect((res) => {
assert.equal(res.body.code, 0);
assert.equal(typeof res.body.data, 'object');
});
});
it('deleteSession', async function() {
await api.get(endPoint('deleteSession') + `&sessionID=${sessionID}`)
.expect(200)
.expect('Content-Type', /json/)
.expect((res) => {
assert.equal(res.body.code, 0);
});
});
it('getSessionInfo of deleted session', async function() {
await api.get(endPoint('getSessionInfo') + `&sessionID=${sessionID}`)
.expect(200)
.expect('Content-Type', /json/)
.expect((res) => {
assert.equal(res.body.code, 1);
});
});
});
describe('API: Group pad management', function() {
it('listPads', async function() {
await api.get(endPoint('listPads') + `&groupID=${groupID}`)
.expect(200)
.expect('Content-Type', /json/)
.expect((res) => {
assert.equal(res.body.code, 0);
assert.equal(res.body.data.padIDs.length, 0);
});
});
it('createGroupPad', async function() {
await api.get(endPoint('createGroupPad') + `&groupID=${groupID}&padName=${padID}`)
.expect(200)
.expect('Content-Type', /json/)
.expect((res) => {
assert.equal(res.body.code, 0);
padID = res.body.data.padID;
});
});
it('listPads after creating a group pad', async function() {
await api.get(endPoint('listPads') + `&groupID=${groupID}`)
.expect(200)
.expect('Content-Type', /json/)
.expect((res) => {
assert.equal(res.body.code, 0);
assert.equal(res.body.data.padIDs.length, 1);
});
});
});
describe('API: Pad security', function() {
it('getPublicStatus', async function() {
await api.get(endPoint('getPublicStatus') + `&padID=${padID}`)
.expect(200)
.expect('Content-Type', /json/)
.expect((res) => {
assert.equal(res.body.code, 0);
assert.equal(res.body.data.publicStatus, false);
});
});
it('setPublicStatus', async function() {
await api.get(endPoint('setPublicStatus') + `&padID=${padID}&publicStatus=true`)
.expect(200)
.expect('Content-Type', /json/)
.expect((res) => {
assert.equal(res.body.code, 0);
});
});
it('getPublicStatus after changing public status', async function() {
await api.get(endPoint('getPublicStatus') + `&padID=${padID}`)
.expect(200)
.expect('Content-Type', /json/)
.expect((res) => {
assert.equal(res.body.code, 0);
assert.equal(res.body.data.publicStatus, true);
});
});
});
// NOT SURE HOW TO POPULAT THIS /-_-\
///////////////////////////////////////
///////////////////////////////////////
describe('API: Misc', function() {
it('listPadsOfAuthor', async function() {
await api.get(endPoint('listPadsOfAuthor') + `&authorID=${authorID}`)
.expect(200)
.expect('Content-Type', /json/)
.expect((res) => {
assert.equal(res.body.code, 0);
assert.equal(res.body.data.padIDs.length, 0);
});
}); });
}); });

View file

@ -8,63 +8,65 @@ var assert = require('assert')
var npm = require("../../../../src/node_modules/npm/lib/npm.js"); var npm = require("../../../../src/node_modules/npm/lib/npm.js");
var nodeify = require('../../../../src/node_modules/nodeify'); var nodeify = require('../../../../src/node_modules/nodeify');
describe('tidyHtml', function() { describe(__filename, function() {
before(function(done) { describe('tidyHtml', function() {
npm.load({}, function(err) { before(function(done) {
assert.ok(!err); npm.load({}, function(err) {
TidyHtml = require('../../../../src/node/utils/TidyHtml'); assert.ok(!err);
Settings = require('../../../../src/node/utils/Settings'); TidyHtml = require('../../../../src/node/utils/TidyHtml');
return done() Settings = require('../../../../src/node/utils/Settings');
return done()
});
}); });
});
function tidy(file, callback) { function tidy(file, callback) {
return nodeify(TidyHtml.tidy(file), callback); return nodeify(TidyHtml.tidy(file), callback);
}
it('Tidies HTML', function(done) {
// If the user hasn't configured Tidy, we skip this tests as it's required for this test
if (!Settings.tidyHtml) {
this.skip();
} }
// Try to tidy up a bad HTML file it('Tidies HTML', function(done) {
const tmpDir = os.tmpdir(); // If the user hasn't configured Tidy, we skip this tests as it's required for this test
if (!Settings.tidyHtml) {
this.skip();
}
var tmpFile = path.join(tmpDir, 'tmp_' + (Math.floor(Math.random() * 1000000)) + '.html') // Try to tidy up a bad HTML file
fs.writeFileSync(tmpFile, '<html><body><p>a paragraph</p><li>List without outer UL</li>trailing closing p</p></body></html>'); const tmpDir = os.tmpdir();
tidy(tmpFile, function(err){
assert.ok(!err);
// Read the file again var tmpFile = path.join(tmpDir, 'tmp_' + (Math.floor(Math.random() * 1000000)) + '.html')
var cleanedHtml = fs.readFileSync(tmpFile).toString(); fs.writeFileSync(tmpFile, '<html><body><p>a paragraph</p><li>List without outer UL</li>trailing closing p</p></body></html>');
tidy(tmpFile, function(err){
assert.ok(!err);
var expectedHtml = [ // Read the file again
'<title></title>', var cleanedHtml = fs.readFileSync(tmpFile).toString();
'</head>',
'<body>', var expectedHtml = [
'<p>a paragraph</p>', '<title></title>',
'<ul>', '</head>',
'<li>List without outer UL</li>', '<body>',
'<li style="list-style: none">trailing closing p</li>', '<p>a paragraph</p>',
'</ul>', '<ul>',
'</body>', '<li>List without outer UL</li>',
'</html>', '<li style="list-style: none">trailing closing p</li>',
].join('\n'); '</ul>',
assert.notStrictEqual(cleanedHtml.indexOf(expectedHtml), -1); '</body>',
return done(); '</html>',
].join('\n');
assert.notStrictEqual(cleanedHtml.indexOf(expectedHtml), -1);
return done();
});
}); });
});
it('can deal with errors', function(done) { it('can deal with errors', function(done) {
// If the user hasn't configured Tidy, we skip this tests as it's required for this test // If the user hasn't configured Tidy, we skip this tests as it's required for this test
if (!Settings.tidyHtml) { if (!Settings.tidyHtml) {
this.skip(); this.skip();
} }
tidy('/some/none/existing/file.html', function(err) { tidy('/some/none/existing/file.html', function(err) {
assert.ok(err); assert.ok(err);
return done(); return done();
});
}); });
}); });
}); });

View file

@ -105,55 +105,53 @@ const tests = {
} }
// For each test.. describe(__filename, function() {
for (let test in tests){ for (let test in tests) {
let testObj = tests[test]; let testObj = tests[test];
describe(test, function() {
describe(test, function() { if (testObj.disabled) {
if(testObj.disabled){ return xit("DISABLED:", test, function(done){
return xit("DISABLED:", test, function(done){ done();
done(); })
})
}
it(testObj.description, function(done) {
var $ = cheerio.load(testObj.html); // Load HTML into Cheerio
var doc = $('html')[0]; // Creates a dom-like representation of HTML
// Create an empty attribute pool
var apool = new AttributePool();
// Convert a dom tree into a list of lines and attribute liens
// using the content collector object
var cc = contentcollector.makeContentCollector(true, null, apool);
cc.collectContent(doc);
var result = cc.finish();
var recievedAttributes = result.lineAttribs;
var expectedAttributes = testObj.expectedLineAttribs;
var recievedText = new Array(result.lines)
var expectedText = testObj.expectedText;
// Check recieved text matches the expected text
if(arraysEqual(recievedText[0], expectedText)){
// console.log("PASS: Recieved Text did match Expected Text\nRecieved:", recievedText[0], "\nExpected:", testObj.expectedText)
}else{
console.error("FAIL: Recieved Text did not match Expected Text\nRecieved:", recievedText[0], "\nExpected:", testObj.expectedText)
throw new Error();
} }
// Check recieved attributes matches the expected attributes it(testObj.description, function(done) {
if(arraysEqual(recievedAttributes, expectedAttributes)){ var $ = cheerio.load(testObj.html); // Load HTML into Cheerio
// console.log("PASS: Recieved Attributes matched Expected Attributes"); var doc = $('html')[0]; // Creates a dom-like representation of HTML
done(); // Create an empty attribute pool
}else{ var apool = new AttributePool();
console.error("FAIL", test, testObj.description); // Convert a dom tree into a list of lines and attribute liens
console.error("FAIL: Recieved Attributes did not match Expected Attributes\nRecieved: ", recievedAttributes, "\nExpected: ", expectedAttributes) // using the content collector object
console.error("FAILING HTML", testObj.html); var cc = contentcollector.makeContentCollector(true, null, apool);
throw new Error(); cc.collectContent(doc);
} var result = cc.finish();
var recievedAttributes = result.lineAttribs;
var expectedAttributes = testObj.expectedLineAttribs;
var recievedText = new Array(result.lines)
var expectedText = testObj.expectedText;
// Check recieved text matches the expected text
if (arraysEqual(recievedText[0], expectedText)) {
// console.log("PASS: Recieved Text did match Expected Text\nRecieved:", recievedText[0], "\nExpected:", testObj.expectedText)
} else {
console.error("FAIL: Recieved Text did not match Expected Text\nRecieved:", recievedText[0], "\nExpected:", testObj.expectedText)
throw new Error();
}
// Check recieved attributes matches the expected attributes
if (arraysEqual(recievedAttributes, expectedAttributes)) {
// console.log("PASS: Recieved Attributes matched Expected Attributes");
done();
} else {
console.error("FAIL", test, testObj.description);
console.error("FAIL: Recieved Attributes did not match Expected Attributes\nRecieved: ", recievedAttributes, "\nExpected: ", expectedAttributes)
console.error("FAILING HTML", testObj.html);
throw new Error();
}
});
}); });
}
}); });
};

View file

@ -3,83 +3,85 @@ function m(mod) { return __dirname + '/../../../src/' + mod; }
const assert = require('assert').strict; const assert = require('assert').strict;
const promises = require(m('node/utils/promises')); const promises = require(m('node/utils/promises'));
describe('promises.timesLimit', async () => { describe(__filename, function() {
let wantIndex = 0; describe('promises.timesLimit', function() {
const testPromises = []; let wantIndex = 0;
const makePromise = (index) => { const testPromises = [];
// Make sure index increases by one each time. const makePromise = (index) => {
assert.equal(index, wantIndex++); // Make sure index increases by one each time.
// Save the resolve callback (so the test can trigger resolution) assert.equal(index, wantIndex++);
// and the promise itself (to wait for resolve to take effect). // Save the resolve callback (so the test can trigger resolution)
const p = {}; // and the promise itself (to wait for resolve to take effect).
const promise = new Promise((resolve) => { const p = {};
p.resolve = resolve; const promise = new Promise((resolve) => {
}); p.resolve = resolve;
p.promise = promise; });
testPromises.push(p); p.promise = promise;
return p.promise; testPromises.push(p);
}; return p.promise;
};
const total = 11; const total = 11;
const concurrency = 7; const concurrency = 7;
const timesLimitPromise = promises.timesLimit(total, concurrency, makePromise);
it('honors concurrency', async () => {
assert.equal(wantIndex, concurrency);
});
it('creates another when one completes', async () => {
const {promise, resolve} = testPromises.shift();
resolve();
await promise;
assert.equal(wantIndex, concurrency + 1);
});
it('creates the expected total number of promises', async () => {
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 () => {
await timesLimitPromise;
});
it('does not create too many promises if total < concurrency', async () => {
wantIndex = 0;
assert.equal(testPromises.length, 0);
const total = 7;
const concurrency = 11;
const timesLimitPromise = promises.timesLimit(total, concurrency, makePromise); const timesLimitPromise = promises.timesLimit(total, concurrency, makePromise);
while (testPromises.length > 0) {
const {promise, resolve} = testPromises.pop(); it('honors concurrency', async function() {
assert.equal(wantIndex, concurrency);
});
it('creates another when one completes', async function() {
const {promise, resolve} = testPromises.shift();
resolve(); resolve();
await promise; await promise;
} assert.equal(wantIndex, concurrency + 1);
await timesLimitPromise; });
assert.equal(wantIndex, total);
});
it('accepts total === 0, concurrency > 0', async () => { it('creates the expected total number of promises', async function() {
wantIndex = 0; while (testPromises.length > 0) {
assert.equal(testPromises.length, 0); // Resolve them in random order to ensure that the resolution order doesn't matter.
await promises.timesLimit(0, concurrency, makePromise); const i = Math.floor(Math.random() * Math.floor(testPromises.length));
assert.equal(wantIndex, 0); const {promise, resolve} = testPromises.splice(i, 1)[0];
}); resolve();
await promise;
}
assert.equal(wantIndex, total);
});
it('accepts total === 0, concurrency === 0', async () => { it('resolves', async function() {
wantIndex = 0; await timesLimitPromise;
assert.equal(testPromises.length, 0); });
await promises.timesLimit(0, 0, makePromise);
assert.equal(wantIndex, 0);
});
it('rejects total > 0, concurrency === 0', async () => { it('does not create too many promises if total < concurrency', async function() {
await assert.rejects(promises.timesLimit(total, 0, makePromise), RangeError); 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);
});
}); });
}); });

View file

@ -9,9 +9,6 @@ const setCookieParser = require(m('node_modules/set-cookie-parser'));
const settings = require(m('node/utils/Settings')); const settings = require(m('node/utils/Settings'));
const logger = common.logger; const logger = common.logger;
let agent;
before(async function() { agent = await common.init(); });
// Waits for and returns the next named socket.io event. Rejects if there is any error while waiting // Waits for and returns the next named socket.io event. Rejects if there is any error while waiting
// (unless waiting for that error event). // (unless waiting for that error event).
@ -92,264 +89,269 @@ const handshake = async (socket, padID) => {
return msg; return msg;
}; };
describe('socket.io access checks', function() { describe(__filename, function() {
let authorize; let agent;
let authorizeHooksBackup; before(async function() { agent = await common.init(); });
const cleanUpPads = async () => {
const padIds = ['pad', 'other-pad', 'päd'];
await Promise.all(padIds.map(async (padId) => {
if (await padManager.doesPadExist(padId)) {
const pad = await padManager.getPad(padId);
await pad.remove();
}
}));
};
const settingsBackup = {};
let socket;
beforeEach(async function() { describe('socket.io access checks', function() {
Object.assign(settingsBackup, settings); let authorize;
assert(socket == null); let authorizeHooksBackup;
settings.editOnly = false; const cleanUpPads = async () => {
settings.requireAuthentication = false; const padIds = ['pad', 'other-pad', 'päd'];
settings.requireAuthorization = false; await Promise.all(padIds.map(async (padId) => {
settings.users = { if (await padManager.doesPadExist(padId)) {
admin: {password: 'admin-password', is_admin: true}, const pad = await padManager.getPad(padId);
user: {password: 'user-password'}, await pad.remove();
}
}));
}; };
authorize = () => true; const settingsBackup = {};
authorizeHooksBackup = plugins.hooks.authorize; let socket;
plugins.hooks.authorize = [{hook_fn: (hookName, {req}, cb) => {
return cb([authorize(req)]);
}}];
await cleanUpPads();
});
afterEach(async function() {
Object.assign(settings, settingsBackup);
if (socket) socket.close();
socket = null;
plugins.hooks.authorize = authorizeHooksBackup;
await cleanUpPads();
});
describe('Normal accesses', function() {
it('!authn anonymous cookie /p/pad -> 200, ok', async function() {
const res = await agent.get('/p/pad').expect(200);
socket = await connect(res);
const clientVars = await handshake(socket, 'pad');
assert.equal(clientVars.type, 'CLIENT_VARS');
});
it('!authn !cookie -> ok', async function() {
socket = await connect(null);
const clientVars = await handshake(socket, 'pad');
assert.equal(clientVars.type, 'CLIENT_VARS');
});
it('!authn user /p/pad -> 200, ok', async function() {
const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200);
socket = await connect(res);
const clientVars = await handshake(socket, 'pad');
assert.equal(clientVars.type, 'CLIENT_VARS');
});
it('authn user /p/pad -> 200, ok', async function() {
settings.requireAuthentication = true;
const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200);
socket = await connect(res);
const clientVars = await handshake(socket, 'pad');
assert.equal(clientVars.type, 'CLIENT_VARS');
});
it('authz user /p/pad -> 200, ok', async function() {
settings.requireAuthentication = true;
settings.requireAuthorization = true;
const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200);
socket = await connect(res);
const clientVars = await handshake(socket, 'pad');
assert.equal(clientVars.type, 'CLIENT_VARS');
});
it('supports pad names with characters that must be percent-encoded', async function() {
settings.requireAuthentication = true;
// requireAuthorization is set to true here to guarantee that the user's padAuthorizations
// object is populated. Technically this isn't necessary because the user's padAuthorizations
// is currently populated even if requireAuthorization is false, but setting this to true
// ensures the test remains useful if the implementation ever changes.
settings.requireAuthorization = true;
const encodedPadId = encodeURIComponent('päd');
const res = await agent.get(`/p/${encodedPadId}`).auth('user', 'user-password').expect(200);
socket = await connect(res);
const clientVars = await handshake(socket, 'päd');
assert.equal(clientVars.type, 'CLIENT_VARS');
});
});
describe('Abnormal access attempts', function() {
it('authn anonymous /p/pad -> 401, error', async function() {
settings.requireAuthentication = true;
const res = await agent.get('/p/pad').expect(401);
// Despite the 401, try to create the pad via a socket.io connection anyway.
socket = await connect(res);
const message = await handshake(socket, 'pad');
assert.equal(message.accessStatus, 'deny');
});
it('authn !cookie -> error', async function() {
settings.requireAuthentication = true;
socket = await connect(null);
const message = await handshake(socket, 'pad');
assert.equal(message.accessStatus, 'deny');
});
it('authorization bypass attempt -> error', async function() {
// Only allowed to access /p/pad.
authorize = (req) => req.path === '/p/pad';
settings.requireAuthentication = true;
settings.requireAuthorization = true;
// First authenticate and establish a session.
const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200);
socket = await connect(res);
// Accessing /p/other-pad should fail, despite the successful fetch of /p/pad.
const message = await handshake(socket, 'other-pad');
assert.equal(message.accessStatus, 'deny');
});
});
describe('Authorization levels via authorize hook', function() {
beforeEach(async function() { beforeEach(async function() {
settings.requireAuthentication = true; Object.assign(settingsBackup, settings);
settings.requireAuthorization = true; assert(socket == null);
});
it("level='create' -> can create", async function() {
authorize = () => 'create';
const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200);
socket = await connect(res);
const clientVars = await handshake(socket, 'pad');
assert.equal(clientVars.type, 'CLIENT_VARS');
assert.equal(clientVars.data.readonly, false);
});
it('level=true -> can create', async function() {
authorize = () => true;
const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200);
socket = await connect(res);
const clientVars = await handshake(socket, 'pad');
assert.equal(clientVars.type, 'CLIENT_VARS');
assert.equal(clientVars.data.readonly, false);
});
it("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 connect(res);
const clientVars = await handshake(socket, 'pad');
assert.equal(clientVars.type, 'CLIENT_VARS');
assert.equal(clientVars.data.readonly, false);
});
it("level='create' settings.editOnly=true -> unable to create", async function() {
authorize = () => 'create';
settings.editOnly = true;
const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200);
socket = await connect(res);
const message = await handshake(socket, 'pad');
assert.equal(message.accessStatus, 'deny');
});
it("level='modify' settings.editOnly=false -> unable to create", async function() {
authorize = () => 'modify';
settings.editOnly = false; settings.editOnly = false;
const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); settings.requireAuthentication = false;
socket = await connect(res); settings.requireAuthorization = false;
const message = await handshake(socket, 'pad'); settings.users = {
assert.equal(message.accessStatus, 'deny'); admin: {password: 'admin-password', is_admin: true},
user: {password: 'user-password'},
};
authorize = () => true;
authorizeHooksBackup = plugins.hooks.authorize;
plugins.hooks.authorize = [{hook_fn: (hookName, {req}, cb) => {
return cb([authorize(req)]);
}}];
await cleanUpPads();
}); });
it("level='readOnly' -> unable to create", async function() { afterEach(async function() {
authorize = () => 'readOnly'; Object.assign(settings, settingsBackup);
const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); if (socket) socket.close();
socket = await connect(res); socket = null;
const message = await handshake(socket, 'pad'); plugins.hooks.authorize = authorizeHooksBackup;
assert.equal(message.accessStatus, 'deny'); await cleanUpPads();
});
it("level='readOnly' -> unable to modify", async function() {
await padManager.getPad('pad'); // Create the pad.
authorize = () => 'readOnly';
const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200);
socket = await connect(res);
const clientVars = await handshake(socket, 'pad');
assert.equal(clientVars.type, 'CLIENT_VARS');
assert.equal(clientVars.data.readonly, true);
});
});
describe('Authorization levels via user settings', function() {
beforeEach(async function() {
settings.requireAuthentication = true;
}); });
it('user.canCreate = true -> can create and modify', async function() { describe('Normal accesses', function() {
settings.users.user.canCreate = true; it('!authn anonymous cookie /p/pad -> 200, ok', async function() {
const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); const res = await agent.get('/p/pad').expect(200);
socket = await connect(res); socket = await connect(res);
const clientVars = await handshake(socket, 'pad'); const clientVars = await handshake(socket, 'pad');
assert.equal(clientVars.type, 'CLIENT_VARS'); assert.equal(clientVars.type, 'CLIENT_VARS');
assert.equal(clientVars.data.readonly, false); });
}); it('!authn !cookie -> ok', async function() {
it('user.canCreate = false -> unable to create', async function() { socket = await connect(null);
settings.users.user.canCreate = false; const clientVars = await handshake(socket, 'pad');
const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); assert.equal(clientVars.type, 'CLIENT_VARS');
socket = await connect(res); });
const message = await handshake(socket, 'pad'); it('!authn user /p/pad -> 200, ok', async function() {
assert.equal(message.accessStatus, 'deny'); const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200);
}); socket = await connect(res);
it('user.readOnly = true -> unable to create', async function() { const clientVars = await handshake(socket, 'pad');
settings.users.user.readOnly = true; assert.equal(clientVars.type, 'CLIENT_VARS');
const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); });
socket = await connect(res); it('authn user /p/pad -> 200, ok', async function() {
const message = await handshake(socket, 'pad'); settings.requireAuthentication = true;
assert.equal(message.accessStatus, 'deny'); const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200);
}); socket = await connect(res);
it('user.readOnly = true -> unable to modify', async function() { const clientVars = await handshake(socket, 'pad');
await padManager.getPad('pad'); // Create the pad. assert.equal(clientVars.type, 'CLIENT_VARS');
settings.users.user.readOnly = true; });
const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); it('authz user /p/pad -> 200, ok', async function() {
socket = await connect(res); settings.requireAuthentication = true;
const clientVars = await handshake(socket, 'pad'); settings.requireAuthorization = true;
assert.equal(clientVars.type, 'CLIENT_VARS'); const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200);
assert.equal(clientVars.data.readonly, true); socket = await connect(res);
}); const clientVars = await handshake(socket, 'pad');
it('user.readOnly = false -> can create and modify', async function() { assert.equal(clientVars.type, 'CLIENT_VARS');
settings.users.user.readOnly = false; });
const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); it('supports pad names with characters that must be percent-encoded', async function() {
socket = await connect(res); settings.requireAuthentication = true;
const clientVars = await handshake(socket, 'pad'); // requireAuthorization is set to true here to guarantee that the user's padAuthorizations
assert.equal(clientVars.type, 'CLIENT_VARS'); // object is populated. Technically this isn't necessary because the user's padAuthorizations
assert.equal(clientVars.data.readonly, false); // is currently populated even if requireAuthorization is false, but setting this to true
}); // ensures the test remains useful if the implementation ever changes.
it('user.readOnly = true, user.canCreate = true -> unable to create', async function() { settings.requireAuthorization = true;
settings.users.user.canCreate = true; const encodedPadId = encodeURIComponent('päd');
settings.users.user.readOnly = true; const res = await agent.get(`/p/${encodedPadId}`).auth('user', 'user-password').expect(200);
const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); socket = await connect(res);
socket = await connect(res); const clientVars = await handshake(socket, 'päd');
const message = await handshake(socket, 'pad'); assert.equal(clientVars.type, 'CLIENT_VARS');
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() { describe('Abnormal access attempts', function() {
settings.users.user.readOnly = true; it('authn anonymous /p/pad -> 401, error', async function() {
authorize = () => 'create'; settings.requireAuthentication = true;
const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); const res = await agent.get('/p/pad').expect(401);
socket = await connect(res); // Despite the 401, try to create the pad via a socket.io connection anyway.
const message = await handshake(socket, 'pad'); socket = await connect(res);
assert.equal(message.accessStatus, 'deny'); const message = await handshake(socket, 'pad');
assert.equal(message.accessStatus, 'deny');
});
it('authn !cookie -> error', async function() {
settings.requireAuthentication = true;
socket = await connect(null);
const message = await handshake(socket, 'pad');
assert.equal(message.accessStatus, 'deny');
});
it('authorization bypass attempt -> error', async function() {
// Only allowed to access /p/pad.
authorize = (req) => req.path === '/p/pad';
settings.requireAuthentication = true;
settings.requireAuthorization = true;
// First authenticate and establish a session.
const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200);
socket = await connect(res);
// Accessing /p/other-pad should fail, despite the successful fetch of /p/pad.
const message = await handshake(socket, 'other-pad');
assert.equal(message.accessStatus, 'deny');
});
}); });
it('user settings does not elevate level from authorize hook', async function() {
settings.users.user.readOnly = false; describe('Authorization levels via authorize hook', function() {
settings.users.user.canCreate = true; beforeEach(async function() {
authorize = () => 'readOnly'; settings.requireAuthentication = true;
const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); settings.requireAuthorization = true;
socket = await connect(res); });
const message = await handshake(socket, 'pad');
assert.equal(message.accessStatus, 'deny'); it("level='create' -> can create", async function() {
authorize = () => 'create';
const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200);
socket = await connect(res);
const clientVars = await handshake(socket, 'pad');
assert.equal(clientVars.type, 'CLIENT_VARS');
assert.equal(clientVars.data.readonly, false);
});
it('level=true -> can create', async function() {
authorize = () => true;
const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200);
socket = await connect(res);
const clientVars = await handshake(socket, 'pad');
assert.equal(clientVars.type, 'CLIENT_VARS');
assert.equal(clientVars.data.readonly, false);
});
it("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 connect(res);
const clientVars = await handshake(socket, 'pad');
assert.equal(clientVars.type, 'CLIENT_VARS');
assert.equal(clientVars.data.readonly, false);
});
it("level='create' settings.editOnly=true -> unable to create", async function() {
authorize = () => 'create';
settings.editOnly = true;
const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200);
socket = await connect(res);
const message = await handshake(socket, 'pad');
assert.equal(message.accessStatus, 'deny');
});
it("level='modify' settings.editOnly=false -> unable to create", async function() {
authorize = () => 'modify';
settings.editOnly = false;
const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200);
socket = await connect(res);
const message = await handshake(socket, 'pad');
assert.equal(message.accessStatus, 'deny');
});
it("level='readOnly' -> unable to create", async function() {
authorize = () => 'readOnly';
const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200);
socket = await connect(res);
const message = await handshake(socket, 'pad');
assert.equal(message.accessStatus, 'deny');
});
it("level='readOnly' -> unable to modify", async function() {
await padManager.getPad('pad'); // Create the pad.
authorize = () => 'readOnly';
const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200);
socket = await connect(res);
const clientVars = await handshake(socket, 'pad');
assert.equal(clientVars.type, 'CLIENT_VARS');
assert.equal(clientVars.data.readonly, true);
});
});
describe('Authorization levels via user settings', function() {
beforeEach(async function() {
settings.requireAuthentication = true;
});
it('user.canCreate = true -> can create and modify', async function() {
settings.users.user.canCreate = true;
const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200);
socket = await connect(res);
const clientVars = await handshake(socket, 'pad');
assert.equal(clientVars.type, 'CLIENT_VARS');
assert.equal(clientVars.data.readonly, false);
});
it('user.canCreate = false -> unable to create', async function() {
settings.users.user.canCreate = false;
const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200);
socket = await connect(res);
const message = await handshake(socket, 'pad');
assert.equal(message.accessStatus, 'deny');
});
it('user.readOnly = true -> unable to create', async function() {
settings.users.user.readOnly = true;
const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200);
socket = await connect(res);
const message = await handshake(socket, 'pad');
assert.equal(message.accessStatus, 'deny');
});
it('user.readOnly = true -> unable to modify', async function() {
await padManager.getPad('pad'); // Create the pad.
settings.users.user.readOnly = true;
const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200);
socket = await connect(res);
const clientVars = await handshake(socket, 'pad');
assert.equal(clientVars.type, 'CLIENT_VARS');
assert.equal(clientVars.data.readonly, true);
});
it('user.readOnly = false -> can create and modify', async function() {
settings.users.user.readOnly = false;
const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200);
socket = await connect(res);
const clientVars = await handshake(socket, 'pad');
assert.equal(clientVars.type, 'CLIENT_VARS');
assert.equal(clientVars.data.readonly, false);
});
it('user.readOnly = true, user.canCreate = true -> unable to create', async function() {
settings.users.user.canCreate = true;
settings.users.user.readOnly = true;
const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200);
socket = await connect(res);
const message = await handshake(socket, 'pad');
assert.equal(message.accessStatus, 'deny');
});
});
describe('Authorization level interaction between authorize hook and user settings', function() {
beforeEach(async function() {
settings.requireAuthentication = true;
settings.requireAuthorization = true;
});
it('authorize hook does not elevate level from user settings', async function() {
settings.users.user.readOnly = true;
authorize = () => 'create';
const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200);
socket = await connect(res);
const message = await handshake(socket, 'pad');
assert.equal(message.accessStatus, 'deny');
});
it('user settings does not elevate level from authorize hook', async function() {
settings.users.user.readOnly = false;
settings.users.user.canCreate = true;
authorize = () => 'readOnly';
const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200);
socket = await connect(res);
const message = await handshake(socket, 'pad');
assert.equal(message.accessStatus, 'deny');
});
}); });
}); });
}); });

View file

@ -5,440 +5,441 @@ const common = require('../common');
const plugins = require(m('static/js/pluginfw/plugin_defs')); const plugins = require(m('static/js/pluginfw/plugin_defs'));
const settings = require(m('node/utils/Settings')); const settings = require(m('node/utils/Settings'));
let agent; describe(__filename, function() {
let agent;
before(async function() { agent = await common.init(); });
before(async function() { agent = await common.init(); }); describe('webaccess: without plugins', function() {
const backup = {};
describe('webaccess: without plugins', function() { before(async function() {
const backup = {}; Object.assign(backup, settings);
settings.users = {
before(async function() { admin: {password: 'admin-password', is_admin: true},
Object.assign(backup, settings); user: {password: 'user-password'},
settings.users = { };
admin: {password: 'admin-password', is_admin: true},
user: {password: 'user-password'},
};
});
after(async function() {
Object.assign(settings, backup);
});
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 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('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.req));
}
};
const handlers = {};
const hookNames = ['preAuthorize', 'authenticate', 'authorize'];
const hooksBackup = {};
const settingsBackup = {};
beforeEach(async function() {
callOrder = [];
hookNames.forEach((hookName) => {
// 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];
hooksBackup[hookName] = plugins.hooks[hookName] || [];
plugins.hooks[hookName] = [{hook_fn: h0.handle.bind(h0)}, {hook_fn: h1.handle.bind(h1)}];
}); });
hooksBackup.preAuthzFailure = plugins.hooks.preAuthzFailure || [];
Object.assign(settingsBackup, settings);
settings.users = {
admin: {password: 'admin-password', is_admin: true},
user: {password: 'user-password'},
};
});
afterEach(async function() {
Object.assign(plugins.hooks, hooksBackup);
Object.assign(settings, settingsBackup);
});
describe('preAuthorize', function() { after(async function() {
beforeEach(async function() { Object.assign(settings, backup);
});
it('!authn !authz anonymous / -> 200', async function() {
settings.requireAuthentication = false; settings.requireAuthentication = false;
settings.requireAuthorization = false; settings.requireAuthorization = false;
});
it('defers if it returns []', async function() {
await agent.get('/').expect(200); 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() { it('!authn !authz anonymous /admin/ -> 401', async function() {
settings.requireAuthentication = true; settings.requireAuthentication = false;
settings.requireAuthorization = true; settings.requireAuthorization = false;
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 for static content, defers', async function() {
settings.requireAuthentication = true;
settings.requireAuthorization = true;
await agent.get('/static/robots.txt').expect(200);
assert.deepEqual(callOrder, ['preAuthorize_0', 'preAuthorize_1']);
});
it('cannot grant access to /admin', async function() {
handlers.preAuthorize[0].innerHandle = () => [true];
await agent.get('/admin/').expect(401); 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() { it('authn !authz anonymous / -> 401', 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 = [{hook_fn: (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.requireAuthentication = true;
settings.requireAuthorization = false; 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); 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() { it('authn !authz user / -> 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 = (req) => [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.requireAuthentication = true;
settings.requireAuthorization = true;
});
it('is not called if !requireAuthorization (non-/admin)', async function() {
settings.requireAuthorization = false; settings.requireAuthorization = false;
await agent.get('/').auth('user', 'user-password').expect(200); 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() { 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; settings.requireAuthorization = false;
await agent.get('/admin/').auth('admin', 'admin-password').expect(200); 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() { it('authn authz user / -> 403', async function() {
settings.requireAuthentication = true;
settings.requireAuthorization = true;
await agent.get('/').auth('user', 'user-password').expect(403); 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() { it('authn authz user /admin/ -> 403', async function() {
handlers.authorize[0].innerHandle = () => [true]; settings.requireAuthentication = true;
await agent.get('/').auth('user', 'user-password').expect(200); settings.requireAuthorization = true;
// Note: authorize_1 was not called because authorize_0 handled it. await agent.get('/admin/').auth('user', 'user-password').expect(403);
assert.deepEqual(callOrder, ['preAuthorize_0', 'preAuthorize_1',
'authenticate_0', 'authenticate_1',
'authorize_0']);
}); });
it('does not defer if return [false], 403', async function() { it('authn authz admin / -> 200', async function() {
handlers.authorize[0].innerHandle = (req) => [false]; settings.requireAuthentication = true;
await agent.get('/').auth('user', 'user-password').expect(403); settings.requireAuthorization = true;
// Note: authorize_1 was not called because authorize_0 handled it. await agent.get('/').auth('admin', 'admin-password').expect(200);
assert.deepEqual(callOrder, ['preAuthorize_0', 'preAuthorize_1',
'authenticate_0', 'authenticate_1',
'authorize_0']);
}); });
it('passes req.path in context', async function() { it('authn authz admin /admin/ -> 200', async function() {
handlers.authorize[0].checkContext = ({resource}) => { settings.requireAuthentication = true;
assert.equal(resource, '/'); settings.requireAuthorization = true;
}; await agent.get('/admin/').auth('admin', 'admin-password').expect(200);
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() { describe('webaccess: preAuthorize, authenticate, and authorize hooks', function() {
const Handler = class { let callOrder;
constructor(hookName) { const Handler = class {
this.hookName = hookName; constructor(hookName, suffix) {
this.shouldHandle = false; this.called = false;
this.called = false; this.hookName = hookName;
} this.innerHandle = () => [];
handle(hookName, context, cb) { this.id = hookName + suffix;
assert.equal(hookName, this.hookName); this.checkContext = () => {};
assert(context != null); }
assert(context.req != null); handle(hookName, context, cb) {
assert(context.res != null); assert.equal(hookName, this.hookName);
assert(!this.called); assert(context != null);
this.called = true; assert(context.req != null);
if (this.shouldHandle) { assert(context.res != null);
context.res.status(200).send(this.hookName); assert(context.next != null);
return cb([true]); this.checkContext(context);
assert(!this.called);
this.called = true;
callOrder.push(this.id);
return cb(this.innerHandle(context.req));
} }
return cb([]);
}
};
const handlers = {};
const hookNames = ['authnFailure', 'authzFailure', 'authFailure'];
const settingsBackup = {};
const hooksBackup = {};
beforeEach(function() {
Object.assign(settingsBackup, settings);
hookNames.forEach((hookName) => {
if (plugins.hooks[hookName] == null) plugins.hooks[hookName] = [];
});
Object.assign(hooksBackup, plugins.hooks);
hookNames.forEach((hookName) => {
const handler = new Handler(hookName);
handlers[hookName] = handler;
plugins.hooks[hookName] = [{hook_fn: handler.handle.bind(handler)}];
});
settings.requireAuthentication = true;
settings.requireAuthorization = true;
settings.users = {
admin: {password: 'admin-password', is_admin: true},
user: {password: 'user-password'},
}; };
}); const handlers = {};
afterEach(function() { const hookNames = ['preAuthorize', 'authenticate', 'authorize'];
Object.assign(settings, settingsBackup); const hooksBackup = {};
Object.assign(plugins.hooks, hooksBackup); const settingsBackup = {};
beforeEach(async function() {
callOrder = [];
hookNames.forEach((hookName) => {
// 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];
hooksBackup[hookName] = plugins.hooks[hookName] || [];
plugins.hooks[hookName] = [{hook_fn: h0.handle.bind(h0)}, {hook_fn: h1.handle.bind(h1)}];
});
hooksBackup.preAuthzFailure = plugins.hooks.preAuthzFailure || [];
Object.assign(settingsBackup, settings);
settings.users = {
admin: {password: 'admin-password', is_admin: true},
user: {password: 'user-password'},
};
});
afterEach(async function() {
Object.assign(plugins.hooks, hooksBackup);
Object.assign(settings, settingsBackup);
});
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 for static content, defers', async function() {
settings.requireAuthentication = true;
settings.requireAuthorization = true;
await agent.get('/static/robots.txt').expect(200);
assert.deepEqual(callOrder, ['preAuthorize_0', 'preAuthorize_1']);
});
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 = [{hook_fn: (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 = (req) => [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 = (req) => [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']);
});
});
}); });
// authn failure tests describe('webaccess: authnFailure, authzFailure, authFailure hooks', function() {
it('authn fail, no hooks handle -> 401', async function() { const Handler = class {
await agent.get('/').expect(401); constructor(hookName) {
assert(handlers['authnFailure'].called); this.hookName = hookName;
assert(!handlers['authzFailure'].called); this.shouldHandle = false;
assert(handlers['authFailure'].called); this.called = false;
}); }
it('authn fail, authnFailure handles', async function() { handle(hookName, context, cb) {
handlers['authnFailure'].shouldHandle = true; assert.equal(hookName, this.hookName);
await agent.get('/').expect(200, 'authnFailure'); assert(context != null);
assert(handlers['authnFailure'].called); assert(context.req != null);
assert(!handlers['authzFailure'].called); assert(context.res != null);
assert(!handlers['authFailure'].called); assert(!this.called);
}); this.called = true;
it('authn fail, authFailure handles', async function() { if (this.shouldHandle) {
handlers['authFailure'].shouldHandle = true; context.res.status(200).send(this.hookName);
await agent.get('/').expect(200, 'authFailure'); return cb([true]);
assert(handlers['authnFailure'].called); }
assert(!handlers['authzFailure'].called); return cb([]);
assert(handlers['authFailure'].called); }
}); };
it('authnFailure trumps authFailure', async function() { const handlers = {};
handlers['authnFailure'].shouldHandle = true; const hookNames = ['authnFailure', 'authzFailure', 'authFailure'];
handlers['authFailure'].shouldHandle = true; const settingsBackup = {};
await agent.get('/').expect(200, 'authnFailure'); const hooksBackup = {};
assert(handlers['authnFailure'].called);
assert(!handlers['authFailure'].called);
});
// authz failure tests beforeEach(function() {
it('authz fail, no hooks handle -> 403', async function() { Object.assign(settingsBackup, settings);
await agent.get('/').auth('user', 'user-password').expect(403); hookNames.forEach((hookName) => {
assert(!handlers['authnFailure'].called); if (plugins.hooks[hookName] == null) plugins.hooks[hookName] = [];
assert(handlers['authzFailure'].called); });
assert(handlers['authFailure'].called); Object.assign(hooksBackup, plugins.hooks);
}); hookNames.forEach((hookName) => {
it('authz fail, authzFailure handles', async function() { const handler = new Handler(hookName);
handlers['authzFailure'].shouldHandle = true; handlers[hookName] = handler;
await agent.get('/').auth('user', 'user-password').expect(200, 'authzFailure'); plugins.hooks[hookName] = [{hook_fn: handler.handle.bind(handler)}];
assert(!handlers['authnFailure'].called); });
assert(handlers['authzFailure'].called); settings.requireAuthentication = true;
assert(!handlers['authFailure'].called); settings.requireAuthorization = true;
}); settings.users = {
it('authz fail, authFailure handles', async function() { admin: {password: 'admin-password', is_admin: true},
handlers['authFailure'].shouldHandle = true; user: {password: 'user-password'},
await agent.get('/').auth('user', 'user-password').expect(200, 'authFailure'); };
assert(!handlers['authnFailure'].called); });
assert(handlers['authzFailure'].called); afterEach(function() {
assert(handlers['authFailure'].called); Object.assign(settings, settingsBackup);
}); Object.assign(plugins.hooks, hooksBackup);
it('authzFailure trumps authFailure', async function() { });
handlers['authzFailure'].shouldHandle = true;
handlers['authFailure'].shouldHandle = true; // authn failure tests
await agent.get('/').auth('user', 'user-password').expect(200, 'authzFailure'); it('authn fail, no hooks handle -> 401', async function() {
assert(handlers['authzFailure'].called); await agent.get('/').expect(401);
assert(!handlers['authFailure'].called); 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);
});
}); });
}); });