diff --git a/bin/backendTests.sh b/bin/backendTests.sh new file mode 100755 index 000000000..ec12775ba --- /dev/null +++ b/bin/backendTests.sh @@ -0,0 +1 @@ +src/node_modules/mocha/bin/mocha --timeout 5000 --reporter nyan tests/backend/specs/api diff --git a/bin/updatePlugins.sh b/bin/updatePlugins.sh new file mode 100755 index 000000000..d696eca79 --- /dev/null +++ b/bin/updatePlugins.sh @@ -0,0 +1,12 @@ +#!/bin/sh + +#Move to the folder where ep-lite is installed +cd `dirname $0` + +#Was this script started in the bin folder? if yes move out +if [ -d "../bin" ]; then + cd "../" +fi + +npm outdated --depth=0 | grep -v "^Package" | awk '{print $1}' | xargs npm install $1 --save-dev + diff --git a/src/node/handler/APIHandler.js b/src/node/handler/APIHandler.js index 9adc24180..a26dd2cfb 100644 --- a/src/node/handler/APIHandler.js +++ b/src/node/handler/APIHandler.js @@ -450,6 +450,7 @@ exports.handle = function(apiVersion, functionName, fields, req, res) if(fields["apikey"] != apikey.trim()) { + res.statusCode = 401; res.send({code: 4, message: "no or wrong API Key", data: null}); return; } diff --git a/src/package.json b/src/package.json index c6e07255b..7a8cd74ef 100644 --- a/src/package.json +++ b/src/package.json @@ -40,7 +40,9 @@ "swagger-node-express" : ">=2.1.0", "channels" : "0.0.x", "jsonminify" : "0.2.2", - "measured" : "0.1.3" + "measured" : "0.1.3", + "mocha" : ">=2.0.1", + "supertest" : ">=0.15.0" }, "bin": { "etherpad-lite": "./node/server.js" }, "devDependencies": { diff --git a/src/static/css/pad.css b/src/static/css/pad.css index 5045f2995..8b7e82581 100644 --- a/src/static/css/pad.css +++ b/src/static/css/pad.css @@ -1057,6 +1057,7 @@ input[type=checkbox] { right:20px; width:301px; z-index:9999; + background-color:#666; } #gritter-notice-wrapper.bottom-right { top: auto; @@ -1070,14 +1071,12 @@ input[type=checkbox] { } .gritter-top { - background:url(../../static/img/gritter.png) no-repeat left -30px; height:10px; } .hover .gritter-top { background-position:right -30px; } .gritter-bottom { - background:url(../../static/img/gritter.png) no-repeat left bottom; height:8px; margin:0; } @@ -1086,7 +1085,6 @@ input[type=checkbox] { } .gritter-item { display:block; - background:url(../../static/img/gritter.png) no-repeat left -40px; color:#eee; padding:2px 11px 8px 11px; font-size: 11px; @@ -1104,7 +1102,6 @@ input[type=checkbox] { position:absolute; top:5px; left:3px; - background:url('../../static/img/gritter.png') no-repeat left top; cursor:pointer; width:30px; height:30px; diff --git a/src/static/js/pad_editbar.js b/src/static/js/pad_editbar.js index bdf2d5569..76a79a629 100644 --- a/src/static/js/pad_editbar.js +++ b/src/static/js/pad_editbar.js @@ -183,7 +183,11 @@ var padeditbar = (function() var editbarHeight = $('.menu_left').height() + 2 + "px"; var containerTop = $('.menu_left').height() + 7 + "px"; $('#editbar').css("height", editbarHeight); + $('#editorcontainer').css("top", containerTop); + if($('#options-stickychat').is(":checked")){ + $('#chatbox').css("top", $('#editorcontainer').offset().top + "px"); + }; }, registerDropdownCommand: function (cmd, dropdown) { dropdown = dropdown || cmd; diff --git a/tests/backend/specs/api/pad.js b/tests/backend/specs/api/pad.js new file mode 100644 index 000000000..80f77220f --- /dev/null +++ b/tests/backend/specs/api/pad.js @@ -0,0 +1,269 @@ +var assert = require('assert') + supertest = require(__dirname+'/../../../../src/node_modules/supertest'), + fs = require('fs'), + api = supertest('http://localhost:9001'); + path = require('path'); + +var filePath = path.join(__dirname, '../../../../APIKEY.txt'); + +var apiKey = fs.readFileSync(filePath, {encoding: 'utf-8'}); +var apiVersion = 1; +var testPadId = makeid(); +var lastEdited = ""; + +describe('Connectivity', function(){ + it('errors if can not connect', function(done) { + api.get('/api/') + .expect('Content-Type', /json/) + .expect(200, done) + }); +}) + +describe('API Versioning', function(){ + it('errors if can not connect', 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 if can connect without correct 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) + }); +}) + +/* Pad Tests Order of execution +-> deletePad -- This gives us a guaranteed clear environment + -> createPad + -> getRevisions -- Should be 0 + -> getHTML -- Should be the default pad text in HTML format + -> deletePad -- Should just delete a pad + -> getHTML -- Should return an error + -> createPad(withText) + -> getText -- Should have the text specified above as the pad text + -> setText + -> getText -- Should be the text set before + -> getRevisions -- Should be 0 still? + -> padUsersCount -- Should be 0 + -> getReadOnlyId -- Should be a value + -> listAuthorsOfPad(padID) -- should be empty array? + -> getLastEdited(padID) -- Should be when pad was made + -> setText(padId) + -> getLastEdited(padID) -- Should be when setText was performed +*/ + +describe('deletePad', function(){ + it('deletes a Pad', function(done) { + api.get(endPoint('deletePad')+"&padID="+testPadId) + .expect('Content-Type', /json/) + .expect(200, 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('getRevisionsCount', function(){ + it('gets revision count of Pad', function(done) { + api.get(endPoint('getRevisionsCount')+"&padID="+testPadId) + .expect(function(res){ + if(res.body.code !== 0) throw new Error("Unable to get Revision Count"); + if(res.body.data.revisions !== 0) throw new Error("Incorrect Revision Count"); + }) + .expect('Content-Type', /json/) + .expect(200, done) + }); +}) + +describe('getHTML', function(){ + it('get the HTML of Pad', function(done) { + api.get(endPoint('getHTML')+"&padID="+testPadId) + .expect(function(res){ + if(res.body.data.html.length <= 1) throw new Error("Unable to get Revision Count"); + }) + .expect('Content-Type', /json/) + .expect(200, done) + }); +}) + +describe('deletePad', function(){ + it('deletes a Pad', function(done) { + api.get(endPoint('deletePad')+"&padID="+testPadId) + .expect(function(res){ + if(res.body.code !== 0) throw new Error("Pad Deletion failed") + }) + .expect('Content-Type', /json/) + .expect(200, done) + }); +}) + +describe('getHTML', function(){ + it('get the HTML of a Pad -- Should return a failure', function(done) { + api.get(endPoint('getHTML')+"&padID="+testPadId) + .expect(function(res){ + if(res.body.code !== 1) throw new Error("Pad deletion failed") + }) + .expect('Content-Type', /json/) + .expect(200, done) + }); +}) + +describe('createPad', function(){ + it('creates a new Pad with text', function(done) { + api.get(endPoint('createPad')+"&padID="+testPadId+"&text=testText") + .expect(function(res){ + if(res.body.code !== 0) throw new Error("Pad Creation failed") + }) + .expect('Content-Type', /json/) + .expect(200, done) + }); +}) + +describe('getText', function(){ + it('gets the Pad text and expect it to be testText with \n which is a line break', function(done) { + api.get(endPoint('getText')+"&padID="+testPadId) + .expect(function(res){ + if(res.body.data.text !== "testText\n") throw new Error("Pad Creation with text") + }) + .expect('Content-Type', /json/) + .expect(200, done) + }); +}) + +describe('setText', function(){ + it('creates a new Pad with text', function(done) { + api.get(endPoint('setText')+"&padID="+testPadId+"&text=testTextTwo") + .expect(function(res){ + if(res.body.code !== 0) throw new Error("Pad setting text failed"); + }) + .expect('Content-Type', /json/) + .expect(200, done) + }); +}) + +describe('getText', function(){ + it('gets the Pad text', function(done) { + api.get(endPoint('getText')+"&padID="+testPadId) + .expect(function(res){ + if(res.body.data.text !== "testTextTwo\n") throw new Error("Setting Text") + }) + .expect('Content-Type', /json/) + .expect(200, done) + }); +}) + +describe('getRevisionsCount', function(){ + it('gets Revision Coutn of a Pad', function(done) { + api.get(endPoint('getRevisionsCount')+"&padID="+testPadId) + .expect(function(res){ + if(res.body.data.revisions !== 1) throw new Error("Unable to set text revision count") + }) + .expect('Content-Type', /json/) + .expect(200, done) + }); +}) + +describe('padUsersCount', function(){ + it('gets User Count of a Pad', function(done) { + api.get(endPoint('padUsersCount')+"&padID="+testPadId) + .expect(function(res){ + if(res.body.data.padUsersCount !== 0) throw new Error("Incorrect Pad User count") + }) + .expect('Content-Type', /json/) + .expect(200, done) + }); +}) + +describe('getReadOnlyID', function(){ + it('Gets the Read Only ID of a Pad', function(done) { + api.get(endPoint('getReadOnlyID')+"&padID="+testPadId) + .expect(function(res){ + if(!res.body.data.readOnlyID) throw new Error("No Read Only ID for Pad") + }) + .expect('Content-Type', /json/) + .expect(200, done) + }); +}) + +describe('listAuthorsOfPad', function(){ + it('Get Authors of the Pad', function(done) { + api.get(endPoint('listAuthorsOfPad')+"&padID="+testPadId) + .expect(function(res){ + if(res.body.data.authorIDs.length !== 0) throw new Error("# of Authors of pad is not 0") + }) + .expect('Content-Type', /json/) + .expect(200, done) + }); +}) + +describe('getLastEdited', function(){ + it('Get When Pad was left Edited', function(done) { + api.get(endPoint('getLastEdited')+"&padID="+testPadId) + .expect(function(res){ + if(!res.body.data.lastEdited){ + throw new Error("# of Authors of pad is not 0") + }else{ + lastEdited = res.body.data.lastEdited; + } + }) + .expect('Content-Type', /json/) + .expect(200, done) + }); +}) + +describe('setText', function(){ + it('creates a new Pad with text', function(done) { + api.get(endPoint('setText')+"&padID="+testPadId+"&text=testTextTwo") + .expect(function(res){ + if(res.body.code !== 0) throw new Error("Pad setting text failed"); + }) + .expect('Content-Type', /json/) + .expect(200, done) + }); +}) + +describe('getLastEdited', function(){ + it('Get When Pad was left Edited', function(done) { + api.get(endPoint('getLastEdited')+"&padID="+testPadId) + .expect(function(res){ + if(res.body.data.lastEdited <= lastEdited){ + throw new Error("Editing A Pad is not updating when it was last edited") + } + }) + .expect('Content-Type', /json/) + .expect(200, done) + }); +}) + + +var endPoint = function(point){ + return '/api/'+apiVersion+'/'+point+'?apikey='+apiKey; +} + +function makeid() +{ + var text = ""; + var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + + for( var i=0; i < 5; i++ ){ + text += possible.charAt(Math.floor(Math.random() * possible.length)); + } + return text; +} diff --git a/tests/backend/specs/api/sessionsAndGroups.js b/tests/backend/specs/api/sessionsAndGroups.js new file mode 100644 index 000000000..86ba454a2 --- /dev/null +++ b/tests/backend/specs/api/sessionsAndGroups.js @@ -0,0 +1,363 @@ +var assert = require('assert') + supertest = require(__dirname+'/../../../../src/node_modules/supertest'), + fs = require('fs'), + api = supertest('http://localhost:9001'); + path = require('path'); + +var filePath = path.join(__dirname, '../../../../APIKEY.txt'); + +var apiKey = fs.readFileSync(filePath, {encoding: 'utf-8'}); +var apiVersion = 1; +var testPadId = makeid(); +var groupID = ""; +var authorID = ""; +var sessionID = ""; +var padID = makeid(); + +describe('API Versioning', function(){ + it('errors if can not connect', 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) + }); +}) + +// BEGIN GROUP AND AUTHOR TESTS +///////////////////////////////////// +///////////////////////////////////// + +/* Tests performed +-> createGroup() -- should return a groupID + -> listSessionsOfGroup(groupID) -- should be 0 + -> deleteGroup(groupID) + -> createGroupIfNotExistsFor(groupMapper) -- should return a groupID + + -> createAuthor([name]) -- should return an authorID + -> createAuthorIfNotExistsFor(authorMapper [, name]) -- should return an authorID + -> getAuthorName(authorID) -- should return a name IE "john" + +-> createSession(groupID, authorID, validUntil) + -> getSessionInfo(sessionID) + -> listSessionsOfGroup(groupID) -- should be 1 + -> deleteSession(sessionID) + -> getSessionInfo(sessionID) -- should have author id etc in + +-> listPads(groupID) -- should be empty array + -> createGroupPad(groupID, padName [, text]) + -> listPads(groupID) -- should be empty array + -> getPublicStatus(padId) + -> setPublicStatus(padId, status) + -> getPublicStatus(padId) + -> isPasswordProtected(padID) -- should be false + -> setPassword(padID, password) + -> isPasswordProtected(padID) -- should be true + +-> listPadsOfAuthor(authorID) +*/ + +describe('createGroup', function(){ + it('creates a new group', function(done) { + api.get(endPoint('createGroup')) + .expect(function(res){ + if(res.body.code !== 0 || !res.body.data.groupID) throw new Error("Unable to create new Pad"); + groupID = res.body.data.groupID; + }) + .expect('Content-Type', /json/) + .expect(200, done) + }); +}) + +describe('listSessionsOfGroup', function(){ + it('Lists the session of a group', function(done) { + api.get(endPoint('listSessionsOfGroup')+"&groupID="+groupID) + .expect(function(res){ + if(res.body.code !== 0 || res.body.data !== null) throw new Error("Sessions show as existing for this group"); + }) + .expect('Content-Type', /json/) + .expect(200, done) + }); +}) + +describe('deleteGroup', function(){ + it('Deletes a group', function(done) { + api.get(endPoint('deleteGroup')+"&groupID="+groupID) + .expect(function(res){ + if(res.body.code !== 0) throw new Error("Group failed to be deleted"); + }) + .expect('Content-Type', /json/) + .expect(200, done) + }); +}) + +describe('createGroupIfNotExistsFor', function(){ + it('Creates a group if one doesnt exist for mapper 0', function(done) { + api.get(endPoint('createGroupIfNotExistsFor')+"&groupMapper=management") + .expect(function(res){ + if(res.body.code !== 0 || !res.body.data.groupID) throw new Error("Sessions show as existing for this group"); + }) + .expect('Content-Type', /json/) + .expect(200, done) + }); +}) + +describe('createGroup', function(){ + it('creates a new group', function(done) { + api.get(endPoint('createGroup')) + .expect(function(res){ + if(res.body.code !== 0 || !res.body.data.groupID) throw new Error("Unable to create new Pad"); + groupID = res.body.data.groupID; + }) + .expect('Content-Type', /json/) + .expect(200, done) + }); +}) + +describe('createAuthor', function(){ + it('Creates an author with a name set', function(done) { + api.get(endPoint('createAuthor')) + .expect(function(res){ + if(res.body.code !== 0 || !res.body.data.authorID) throw new Error("Unable to create author"); + }) + .expect('Content-Type', /json/) + .expect(200, done) + }); +}) + +describe('createAuthor', function(){ + it('Creates an author with a name set', function(done) { + api.get(endPoint('createAuthor')+"&name=john") + .expect(function(res){ + if(res.body.code !== 0 || !res.body.data.authorID) throw new Error("Unable to create user with name set"); + authorID = res.body.data.authorID; // we will be this author for the rest of the tests + }) + .expect('Content-Type', /json/) + .expect(200, done) + }); +}) + +describe('createAuthorIfNotExistsFor', function(){ + it('Creates an author if it doesnt exist already and provides mapping', function(done) { + api.get(endPoint('createAuthorIfNotExistsFor')+"&authorMapper=chris") + .expect(function(res){ + if(res.body.code !== 0 || !res.body.data.authorID) throw new Error("Unable to create author with mapper"); + }) + .expect('Content-Type', /json/) + .expect(200, done) + }); +}) + +describe('getAuthorName', function(){ + it('Gets the author name', function(done) { + api.get(endPoint('getAuthorName')+"&authorID="+authorID) + .expect(function(res){ + if(res.body.code !== 0 || !res.body.data === "john") throw new Error("Unable to get Author Name from Author ID"); + }) + .expect('Content-Type', /json/) + .expect(200, done) + }); +}) + +// BEGIN SESSION TESTS +/////////////////////////////////////// +/////////////////////////////////////// + +describe('createSession', function(){ + it('Creates a session for an Author', function(done) { + api.get(endPoint('createSession')+"&authorID="+authorID+"&groupID="+groupID+"&validUntil=999999999999") + .expect(function(res){ + if(res.body.code !== 0 || !res.body.data.sessionID) throw new Error("Unable to create Session"); + sessionID = res.body.data.sessionID; + }) + .expect('Content-Type', /json/) + .expect(200, done) + }); +}) + +describe('getSessionInfo', function(){ + it('Gets session inf', function(done) { + api.get(endPoint('getSessionInfo')+"&sessionID="+sessionID) + .expect(function(res){ + if(res.body.code !== 0 || !res.body.data.groupID || !res.body.data.authorID || !res.body.data.validUntil) throw new Error("Unable to get Session info"); + }) + .expect('Content-Type', /json/) + .expect(200, done) + }); +}) + +describe('listSessionsOfGroup', function(){ + it('Gets sessions of a group', function(done) { + api.get(endPoint('listSessionsOfGroup')+"&groupID="+groupID) + .expect(function(res){ + if(res.body.code !== 0 || typeof res.body.data !== "object") throw new Error("Unable to get sessions of a group"); + }) + .expect('Content-Type', /json/) + .expect(200, done) + }); +}) + +describe('deleteSession', function(){ + it('Deletes a session', function(done) { + api.get(endPoint('deleteSession')+"&sessionID="+sessionID) + .expect(function(res){ + if(res.body.code !== 0) throw new Error("Unable to delete a session"); + }) + .expect('Content-Type', /json/) + .expect(200, done) + }); +}) + +describe('getSessionInfo', function(){ + it('Gets session info', function(done) { + api.get(endPoint('getSessionInfo')+"&sessionID="+sessionID) + .expect(function(res){ + if(res.body.code !== 1) throw new Error("Session was not properly deleted"); + }) + .expect('Content-Type', /json/) + .expect(200, done) + }); +}) + +// GROUP PAD MANAGEMENT +/////////////////////////////////////// +/////////////////////////////////////// + +describe('listPads', function(){ + it('Lists Pads of a Group', function(done) { + api.get(endPoint('listPads')+"&groupID="+groupID) + .expect(function(res){ + if(res.body.code !== 0 || res.body.data.padIDs.length !== 0) throw new Error("Group already had pads for some reason"+res.body.data.padIDs); + }) + .expect('Content-Type', /json/) + .expect(200, done) + }); +}) + +describe('createGroupPad', function(){ + it('Creates a Group Pad', function(done) { + api.get(endPoint('createGroupPad')+"&groupID="+groupID+"&padName="+padID) + .expect(function(res){ + if(res.body.code !== 0) throw new Error("Unable to create group pad"); + padID = res.body.data.padID; + }) + .expect('Content-Type', /json/) + .expect(200, done) + }); +}) + +describe('listPads', function(){ + it('Lists Pads of a Group', function(done) { + api.get(endPoint('listPads')+"&groupID="+groupID) + .expect(function(res){ + if(res.body.code !== 0 || res.body.data.padIDs.length !== 1) throw new Error("Group isnt listing this pad"); + }) + .expect('Content-Type', /json/) + .expect(200, done) + }); +}) + +// PAD SECURITY /-_-\ +/////////////////////////////////////// +/////////////////////////////////////// + +describe('getPublicStatus', function(){ + it('Gets the public status of a pad', function(done) { + api.get(endPoint('getPublicStatus')+"&padID="+padID) + .expect(function(res){ + if(res.body.code !== 0 || res.body.data.publicstatus) throw new Error("Unable to get public status of this pad"); + }) + .expect('Content-Type', /json/) + .expect(200, done) + }); +}) + +describe('setPublicStatus', function(){ + it('Sets the public status of a pad', function(done) { + api.get(endPoint('setPublicStatus')+"&padID="+padID+"&publicStatus=true") + .expect(function(res){ + if(res.body.code !== 0) throw new Error("Setting status did not work"); + }) + .expect('Content-Type', /json/) + .expect(200, done) + }); +}) + +describe('getPublicStatus', function(){ + it('Gets the public status of a pad', function(done) { + api.get(endPoint('getPublicStatus')+"&padID="+padID) + .expect(function(res){ + if(res.body.code !== 0 || !res.body.data.publicStatus) throw new Error("Setting public status of this pad did not work"); + }) + .expect('Content-Type', /json/) + .expect(200, done) + }); +}) + +describe('isPasswordProtected', function(){ + it('Gets the public status of a pad', function(done) { + api.get(endPoint('isPasswordProtected')+"&padID="+padID) + .expect(function(res){ + if(res.body.code !== 0 || res.body.data.isPasswordProtected) throw new Error("Pad is password protected by default"); + }) + .expect('Content-Type', /json/) + .expect(200, done) + }); +}) + +describe('setPassword', function(){ + it('Gets the public status of a pad', function(done) { + api.get(endPoint('setPassword')+"&padID="+padID+"&password=test") + .expect(function(res){ + if(res.body.code !== 0) throw new Error("Unabe to set password"); + }) + .expect('Content-Type', /json/) + .expect(200, done) + }); +}) + +describe('isPasswordProtected', function(){ + it('Gets the public status of a pad', function(done) { + api.get(endPoint('isPasswordProtected')+"&padID="+padID) + .expect(function(res){ + if(res.body.code !== 0 || !res.body.data.isPasswordProtected) throw new Error("Pad password protection has not applied"); + }) + .expect('Content-Type', /json/) + .expect(200, done) + }); +}) + + +// NOT SURE HOW TO POPULAT THIS /-_-\ +/////////////////////////////////////// +/////////////////////////////////////// + +describe('listPadsOfAuthor', function(){ + it('Gets the Pads of an Author', function(done) { + api.get(endPoint('listPadsOfAuthor')+"&authorID="+authorID) + .expect(function(res){ + if(res.body.code !== 0 || res.body.data.padIDs.length !== 0) throw new Error("Pad password protection has not applied"); + }) + .expect('Content-Type', /json/) + .expect(200, done) + }); +}) + + + +var endPoint = function(point){ + return '/api/'+apiVersion+'/'+point+'?apikey='+apiKey; +} + +function makeid() +{ + var text = ""; + var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + + for( var i=0; i < 5; i++ ){ + text += possible.charAt(Math.floor(Math.random() * possible.length)); + } + return text; +}