diff --git a/src/jio.storage/xwikistorage.js b/src/jio.storage/xwikistorage.js index cf74821873acf7cf589ba529a1c10825195f1dd0..bb2428c75a666703366e4dee13c674dc983a5d97 100644 --- a/src/jio.storage/xwikistorage.js +++ b/src/jio.storage/xwikistorage.js @@ -1,341 +1,641 @@ -/*jslint indent: 2, maxlen: 80, sloppy: true, nomen: true */ -/*global toSend: true, jIO: true, jQuery: true, btoa: true */ +/*jslint indent: 2, maxlen: 80, sloppy: true, nomen: true, vars: true */ +/*global + jIO: true, + $: true, + XMLHttpRequest: true, + Blob: true, + FormData: true, + window: true +*/ +/** + * JIO XWiki Storage. Type = 'xwiki'. + * XWiki Document/Attachment storage. + */ +jIO.addStorageType('xwiki', function (spec, my) { + + spec = spec || {}; + var that, priv, xwikistorage; + that = my.basicStorage(spec, my); + priv = {}; /** - * JIO XWiki based storage. Type = 'xwiki'. - * Edits XWiki documents as html using html editor. - * Test this code using the following inputs: + * Get the Space and Page components of a documkent ID. * -{"type":"xwiki","username":"Admin","password":"admin","xwikiurl":"http://127.0.0 -.1:8080/xwiki","space":"OfficeJS"} + * @param id the document id. + * @return a map of { 'space':<Space>, 'page':<Page> } */ - -(function ($) { - - var newXWikiStorage = function (spec, my) { - var that, priv, escapeDocId, restoreDocId, - doWithFormToken, getDates, super_serialized; - - /** The input configuration. */ - spec = spec || {}; - - /** The "public" object which will have methods called on it. */ - that = my.basicStorage(spec, my); - - /** "private" fields. */ - priv = { - username: spec.username || '', - password: spec.password || '', - xwikiurl: spec.xwikiurl || '', - space: spec.space || '' + var getParts = function (id) { + if (id.indexOf('/') === -1) { + return { + space: 'Main', + page: id + }; + } + return { + space: id.substring(0, id.indexOf('/')), + page: id.substring(id.indexOf('/') + 1) }; + }; - //--------------------- Private Functions ---------------------// - /** Escape a document ID by URL escaping all '/' characters. */ - escapeDocId = function (docId) { - // jslint: replaced "." with [\w\W] - return docId.replace(/[\w\W]html$/, '').split('/').join('%2F'); - }; + /** + * Get the Anti-CSRF token and do something with it. + * + * @param andThen function which is called with (formToken, err) + * as parameters. + */ + var doWithFormToken = function (andThen) { + $.ajax({ + url: priv.formTokenPath, + type: "GET", + async: true, + dataType: 'text', + success: function (html) { + // this is unreliable + //var token = $('meta[name=form_token]', html).attr("content"); + var m = html.match(/<meta name="form_token" content="(\w*)"\/>/); + var token = (m && m[1]) || null; + if (!token) { + andThen(null, { + "status": 404, + "statusText": "Not Found", + "error": "err_form_token_not_found", + "message": "Anti-CSRF form token was not found in page", + "reason": "XWiki main page did not contain expected " + + "Anti-CSRF form token" + }); + } else { + andThen(token, null); + } + }, + error: function (jqxhr, err, cause) { + andThen(null, { + "status": jqxhr.status, + "statusText": jqxhr.statusText, + "error": err, + "message": "Could not get Anti-CSRF form token from [" + + priv.xwikiurl + "]", + "reason": cause + }); + }, + }); + }; - /** Restore a document id from the escaped form. */ - restoreDocId = function (escapedDocId) { - return escapedDocId.split('%2F').join('/') + '.html'; - }; + /** + * Get the REST read URL for a document. + * + * @param docId the id of the document. + * @return the REST URL for accessing this document. + */ + var getDocRestURL = function (docId) { + var parts = getParts(docId); + return priv.xwikiurl + '/rest/wikis/' + + priv.wiki + '/spaces/' + parts.space + '/pages/' + parts.page; + }; + /* + * Wrapper for the xwikistorage based on localstorage JiO store. + */ + xwikistorage = { /** - * Get the Anti-CSRF token and do something with it. + * Get content of an XWikiDocument. * - * @param docId document id of document which you have permission to edit. - * @param whatToDo function which is called with form token as parameter. + * @param docId the document ID. + * @param andThen a callback taking (doc, err), doc being the document + * json object and err being the error if any. */ - doWithFormToken = function (docId, whatToDo) { - var url = priv.xwikiurl + '/bin/edit/' + priv.space + '/' + - escapeDocId(docId) + '?editor=wiki&cachebuster=' + Date.now(); + getItem: function (docId, andThen) { $.ajax({ - url: url, + url: getDocRestURL(docId), type: "GET", async: true, - dataType: 'text', - headers: { - 'Authorization': 'Basic ' + btoa(priv.username + ':' + - priv.password) + dataType: 'xml', + success: function (xmlData) { + var out = {}; + var xd = $(xmlData); + xd.find('modified').each(function () { + out._last_modified = Date.parse($(this).text()); + }); + xd.find('created').each(function () { + out._creation_date = Date.parse($(this).text()); + }); + xd.find('title').each(function () { out.title = $(this).text(); }); + xd.find('parent').each(function () { out.parent = $(this).text(); }); + xd.find('syntax').each(function () { out.syntax = $(this).text(); }); + xd.find('content').each(function () { + out.content = $(this).text(); + }); + out._id = docId; + + andThen(out, null); }, - success: function (html) { - whatToDo($(html).find('input[name=form_token]').attr('value')); + error: function (jqxhr, err, cause) { + if (jqxhr.status === 404) { + andThen(null, null); + return; + } + andThen(null, { + "status": jqxhr.status, + "statusText": jqxhr.statusText, + "error": err, + "message": "Failed to get document [" + docId + "]", + "reason": cause + }); } }); - }; + }, + /** - * Get the creation and modification dates for a page. + * Get content of an XWikiAttachment. * - * @param docId the ID of the document. - * @param callWhenDone callback, will be called when function finishes. + * @param attachId the attachment ID. + * @param andThen a callback taking (attach, err), attach being the + * attachment blob and err being the error if any. */ - getDates = function (docId, callWhenDone) { - // http://127.0.0.1:8080/xwiki/rest/wikis/xwiki/ - // spaces/Main/pages/<pageName> - var map = {}; - $.ajax({ - url: priv.xwikiurl + '/rest/wikis/' + 'xwiki' + '/spaces/' + priv.space - + '/pages/' + escapeDocId(docId) + '?cachebuster=' + Date.now(), - type: "GET", - async: true, - dataType: 'xml', - headers: { - 'Authorization': 'Basic ' + btoa(priv.username + ':' + - priv.password) - }, - success: function (xmlData) { - $(xmlData).find('modified').each(function () { - map._last_modified = Date.parse($(this).text()); - }); - $(xmlData).find('created').each(function () { - map._creation_date = Date.parse($(this).text()); + getAttachment: function (docId, fileName, andThen) { + // need to do this manually, jquery doesn't support returning blobs. + var xhr = new XMLHttpRequest(); + var parts = getParts(docId); + var url = priv.xwikiurl + '/bin/download/' + parts.space + + "/" + parts.page + "/" + fileName; + xhr.open('GET', url, true); + xhr.responseType = 'blob'; + + xhr.onload = function (e) { + if (xhr.status === 200) { + var contentType = xhr.getResponseHeader("Content-Type"); + if (contentType.indexOf(';') > -1) { + contentType = contentType.substring(0, contentType.indexOf(';')); + } + var blob = new Blob([xhr.response], {type: contentType}); + andThen(blob); + } else { + andThen(null, { + "status": xhr.status, + "statusText": xhr.statusText, + "error": "err_network_error", + "message": "Failed to get attachment [" + + docId + "/" + fileName + "]", + "reason": "Error getting data from network" }); - callWhenDone(); } - }); - return map; - }; + }; + + xhr.send(); + }, - //--------------------- Public Functions ---------------------// - /** Get a serialized form of the module state. */ - super_serialized = that.serialized; - that.serialized = function () { - var o = super_serialized(), key; - for (key in priv) { - if (priv.hasOwnProperty(key)) { - o[key] = priv[key]; - } - } - return o; - }; - /** Check that the storage module is properly setup. */ - that.validateState = function () { - var key; - for (key in priv) { - if (priv.hasOwnProperty(key) && !priv[key]) { - return 'Must specify "' + key + '".'; - } - } - return ''; - }; - /** Alias to put() */ - that.post = function (command) { - that.put(command); - }; /** - * Saves a document as an XWikiDocument. + * Store an XWikiDocument. * - * @param command must contain document ID and document content. + * @param id the document identifier. + * @param doc the document JSON object containing + * "parent", "title", "content", and/or "syntax" keys. + * @param andThen a callback taking (err), err being the error if any. */ - that.put = function (command) { - doWithFormToken(command.getDocId(), function (formToken) { - if (!formToken) { - throw new Error("missing form token"); + setItem: function (id, doc, andThen) { + doWithFormToken(function (formToken, err) { + if (err) { + that.error(err); + return; } + var parts = getParts(id); $.ajax({ - url: priv.xwikiurl + '/bin/preview/' + priv.space + '/' + - escapeDocId(command.getDocId()), + url: priv.xwikiurl + "/bin/preview/" + parts.space + '/' + parts.page, type: "POST", async: true, dataType: 'text', - headers: { - 'Authorization': 'Basic ' + btoa(priv.username + ':' + - priv.password) - }, data: { - parent: '', - title: '', + parent: doc.parent || '', + title: doc.title || '', xredirect: '', language: 'en', - RequiresHTMLConversion: 'content', - content_syntax: 'xwiki/2.1', - content: command.getDocContent(), +// RequiresHTMLConversion: 'content', +// content_syntax: doc.syntax || 'xwiki/2.1', + content: doc.content || '', xeditaction: 'edit', - comment: 'Saved by OfficeJS', + comment: 'Saved by JiO', action_saveandcontinue: 'Save & Continue', - syntaxId: 'xwiki/2.1', + syntaxId: doc.syntax || 'xwiki/2.1', xhidden: 0, minorEdit: 0, ajax: true, form_token: formToken }, success: function () { - that.success({ - ok: true, - id: command.getDocId() + andThen(null); + }, + error: function (jqxhr, err, cause) { + andThen({ + "status": jqxhr.status, + "statusText": jqxhr.statusText, + "error": err, + "message": "Failed to store document [" + id + "]", + "reason": cause }); } }); }); - }; // end put + }, + /** - * Loads a document from the XWiki storage. + * Store an XWikiAttachment. + * + * @param docId the ID of the document to attach to. + * @param fileName the attachment file name. + * @param mimeType the MIME type of the attachment content. + * @param content the attachment content. + * @param andThen a callback taking one parameter which is the error if any. */ - that.get = function (command) { - // /bin/view/Main/WebHomee?xpage=plain - /** - * Protocol specification: - * { - * "_id": "somePage", - * "content": "aoeu", - * "_creation_date": 1348154789478, - * "_last_modified": 1348154789478 - * } - */ - var doc, - pendingRequests = 2, - finishedRequest = function () { - pendingRequests -= 1; - if (pendingRequests < 1) { - that.success(doc); + setAttachment: function (docId, fileName, mimeType, content, andThen) { + doWithFormToken(function (formToken, err) { + if (err) { + that.error(err); + return; + } + var parts = getParts(docId); + var blob = (content.constructor === "function Blob() { [native code] }") + ? content : new Blob([content], {type: mimeType}); + var fd = new FormData(); + fd.append("filepath", blob, fileName); + fd.append("form_token", formToken); + var xhr = new XMLHttpRequest(); + xhr.open('POST', priv.xwikiurl + "/bin/upload/" + + parts.space + '/' + parts.page, true); + xhr.onload = function (e) { + if (xhr.status === 302 || xhr.status === 200) { + andThen(null); + } else { + andThen({ + "status": xhr.status, + "statusText": xhr.statusText, + "error": "err_network_error", + "message": "Failed to store attachment [" + + docId + "/" + fileName + "]", + "reason": "Error posting data" + }); } }; - doc = (function () { - var resultMap = getDates(command.getDocId(), finishedRequest); + xhr.send(fd); + }); + }, + + removeItem: function (id, andThen) { + doWithFormToken(function (formToken, err) { + if (err) { + that.error(err); + return; + } + var parts = getParts(id); $.ajax({ - url: priv.xwikiurl + '/bin/get/' + priv.space + '/' + - escapeDocId(command.getDocId()) + '?xpage=plain&cachebuster=' + - Date.now(), - type: "GET", + url: priv.xwikiurl + "/bin/delete/" + parts.space + '/' + parts.page, + type: "POST", async: true, dataType: 'text', - headers: { - 'Authorization': 'Basic ' + btoa(priv.username + ':' + - priv.password) + data: { + confirm: '1', + form_token: formToken + }, + success: function () { + andThen(null); }, - success: function (html) { - resultMap.content = html; - finishedRequest(); + error: function (jqxhr, err, cause) { + andThen({ + "status": jqxhr.status, + "statusText": jqxhr.statusText, + "error": err, + "message": "Failed to delete document [" + id + "]", + "reason": cause + }); } }); - return resultMap; - }()); - doc._id = command.getDocId(); - }; // end get + }); + }, - /** - * Gets a document list from the xwiki storage. - * It will retreive an array containing files meta data owned by - * the user. - * @method allDocs - */ - that.allDocs = function (command) { - // http://127.0.0.1:8080/xwiki/rest/wikis/xwiki/spaces/Main/pages - $.ajax({ - url: priv.xwikiurl + '/rest/wikis/' + 'xwiki' + '/spaces/' + - priv.space + '/pages?cachebuster=' + Date.now(), - type: "GET", - async: true, - dataType: 'xml', - headers: { - 'Authorization': 'Basic ' + btoa(priv.username + ':' + - priv.password) - }, - success: function (xmlData) { - /** Protocol definition: - * { - * "total_rows":2, - * "rows":[{ - * "id":"b", - * "key":"b", - * "value":{ - * "content":"aoeu", - * "_creation_date":1348154789478, - * "_last_modified":1348154789478 - * } - * }, - * { - * "id":"oeau", - * "key":"oeau", - * "value"{ - * "content":"oeu", - * "_creation_date":1348154834680, - * "_last_modified":1348154834680 - * } - * } - * ] - * } - */ - var totalRows = 0, - data = [], - // The number of async calls which are waiting to return. - outstandingCalls = 0, - toSend; - $(xmlData).find('name').each(function () { - outstandingCalls += 1; - var id = restoreDocId($(this).text()), - entry = { - 'id': id, - 'key': id, - 'value': getDates(id, function () { - outstandingCalls -= 1; - if (outstandingCalls < 1) { - that.success(toSend); - } - }) - }; - data[totalRows += 1] = entry; - }); - toSend = { - 'total_rows': totalRows, - 'rows': data - }; - /* TODO: Include the content if requested. - if (!command.getOption('metadata_only')) { - getContent(); - } else { - that.success(toSend); - } - */ - }, - error: function (type) { - if (type.status === 404) { - type.message = 'Cannot find "' + command.getDocId() + - '"informations.'; - type.reason = 'missing'; - that.error(type); - } else { - type.reason = 'Cannot get "' + command.getDocId() + - '"informations'; - type.message = type.reason + '.'; - that.retry(type); - } + removeAttachment: function (docId, fileName, andThen) { + var parts = getParts(docId); + doWithFormToken(function (formToken, err) { + if (err) { + that.error(err); + return; } - }); - }; - /** - * Removes a document from the XWiki storage. - */ - that.remove = function (command) { - // http://127.0.0.1:8080/xwiki/bin/delete/Main/WebHomee? - // confirm=1&form_token= //r7x0oGBSk2EFm2fxVULfFA - doWithFormToken(command.getDocId(), function (formToken) { $.ajax({ - url: priv.xwikiurl + '/bin/delete/' + priv.space + '/' + - escapeDocId(command.getDocId()), + url: priv.xwikiurl + "/bin/delattachment/" + parts.space + '/' + + parts.page + '/' + fileName, type: "POST", async: true, dataType: 'text', - headers: { - 'Authorization': 'Basic ' + btoa(priv.username + ':' + - priv.password) - }, data: { - confirm: 1, + ajax: '1', form_token: formToken }, success: function () { - that.success({ - ok: true, - id: command.getDocId() + andThen(null); + }, + error: function (jqxhr, err, cause) { + andThen({ + "status": jqxhr.status, + "statusText": jqxhr.statusText, + "error": err, + "message": "Failed to delete attachment [" + + docId + '/' + fileName + "]", + "reason": cause }); } }); }); - }; // end remove - return that; + } + }; + + // ==================== Tools ==================== + /** + * Update [doc] the document object and remove [doc] keys + * which are not in [new_doc]. It only changes [doc] keys not starting + * with an underscore. + * ex: doc: {key:value1,_key:value2} with + * new_doc: {key:value3,_key:value4} updates + * doc: {key:value3,_key:value2}. + * @param {object} doc The original document object. + * @param {object} new_doc The new document object + */ + priv.documentObjectUpdate = function (doc, new_doc) { + var k; + for (k in doc) { + if (doc.hasOwnProperty(k)) { + if (k[0] !== '_') { + delete doc[k]; + } + } + } + for (k in new_doc) { + if (new_doc.hasOwnProperty(k)) { + if (k[0] !== '_') { + doc[k] = new_doc[k]; + } + } + } + }; + + /** + * Checks if an object has no enumerable keys + * @method objectIsEmpty + * @param {object} obj The object + * @return {boolean} true if empty, else false + */ + priv.objectIsEmpty = function (obj) { + var k; + for (k in obj) { + if (obj.hasOwnProperty(k)) { + return false; + } + } + return true; + }; + + // ==================== attributes ==================== + // the wiki to store stuff in + priv.wiki = spec.wiki || 'xwiki'; + + // unused + priv.username = spec.username; + priv.language = spec.language; + + // URL location of the wiki, unused since + // XWiki doesn't currently allow cross-domain requests. + priv.xwikiurl = spec.xwikiurl || + window.location.href.replace(/\/xwiki\/bin\//, '/xwiki\n').split('\n')[0]; + // should be: s@/xwiki/bin/.*$@/xwiki@ + // but jslint gets in the way. + + // Which URL to load for getting the Anti-CSRF form token, used for testing. + priv.formTokenPath = spec.formTokenPath || priv.xwikiurl; + + + that.specToStore = function () { + return { + "username": priv.username, + "language": priv.language, + "xwikiurl": priv.xwikiurl, + }; + }; + + // can't fo wrong since no parameters are required. + that.validateState = function () { + return ''; + }; + + // ==================== commands ==================== + /** + * Create a document in local storage. + * @method post + * @param {object} command The JIO command + */ + that.post = function (command) { + var docId = command.getDocId(); + if (!(typeof docId === "string" && docId !== "")) { + setTimeout(function () { + that.error({ + "status": 405, + "statusText": "Method Not Allowed", + "error": "method_not_allowed", + "message": "Cannot create document which id is undefined", + "reason": "Document id is undefined" + }); + }); + return; + } + xwikistorage.getItem(docId, function (doc, err) { + if (err) { + that.error(err); + } else if (doc === null) { + // the document does not exist + xwikistorage.setItem(command.getDocId(), + command.cloneDoc(), + function (err) { + if (err) { + that.error(err); + } else { + that.success({ + "ok": true, + "id": command.getDocId() + }); + } + }); + } else { + // the document already exists + that.error({ + "status": 409, + "statusText": "Conflicts", + "error": "conflicts", + "message": "Cannot create a new document", + "reason": "Document already exists (use 'put' to modify it)" + }); + } + }); }; - jIO.addStorageType('xwiki', newXWikiStorage); -}(jQuery)); + + /** + * Create or update a document in local storage. + * @method put + * @param {object} command The JIO command + */ + that.put = function (command) { + xwikistorage.getItem(command.getDocId(), function (doc, err) { + if (err) { + that.error(err); + } else if (doc === null) { + doc = command.cloneDoc(); + } else { + priv.documentObjectUpdate(doc, command.cloneDoc()); + } + // write + xwikistorage.setItem(command.getDocId(), doc, function (err) { + if (err) { + that.error(err); + } else { + that.success({ + "ok": true, + "id": command.getDocId() + }); + } + }); + }); + }; + + /** + * Add an attachment to a document + * @method putAttachment + * @param {object} command The JIO command + */ + that.putAttachment = function (command) { + xwikistorage.getItem(command.getDocId(), function (doc, err) { + if (err) { + that.error(err); + } else if (doc === null) { + // the document does not exist + that.error({ + "status": 404, + "statusText": "Not Found", + "error": "not_found", + "message": "Impossible to add attachment", + "reason": "Document not found" + }); + } else { + // Document exists, upload attachment. + xwikistorage.setAttachment(command.getDocId(), + command.getAttachmentId(), + command.getAttachmentMimeType(), + command.getAttachmentData(), + function (err) { + if (err) { + that.error(err); + } else { + that.success({ + "ok": true, + "id": command.getDocId() + "/" + command.getAttachmentId() + }); + } + }); + } + }); + }; + + /** + * Get a document or attachment + * @method get + * @param {object} command The JIO command + */ + that.get = that.getAttachment = function (command) { + if (typeof command.getAttachmentId() === "string") { + // seeking for an attachment + xwikistorage.getAttachment(command.getDocId(), + command.getAttachmentId(), + function (attach, err) { + if (err) { + that.error(err); + } else if (attach !== null) { + that.success(attach); + } else { + that.error({ + "status": 404, + "statusText": "Not Found", + "error": "not_found", + "message": "Cannot find the attachment", + "reason": "Attachment does not exist" + }); + } + }); + } else { + // seeking for a document + xwikistorage.getItem(command.getDocId(), function (doc, err) { + if (err) { + that.error(err); + } else if (doc !== null) { + that.success(doc); + } else { + that.error({ + "status": 404, + "statusText": "Not Found", + "error": "not_found", + "message": "Cannot find the document", + "reason": "Document does not exist" + }); + } + }); + } + }; + + /** + * Remove a document or attachment + * @method remove + * @param {object} command The JIO command + */ + that.remove = that.removeAttachment = function (command) { + var notFoundError = function (word) { + that.error({ + "status": 404, + "statusText": "Not Found", + "error": "not_found", + "message": word + " not found", + "reason": "missing" + }); + }; + + var objId = command.getDocId(); + var complete = function (err) { + if (err) { + that.error(err); + } else { + that.success({ + "ok": true, + "id": objId + }); + } + }; + if (typeof command.getAttachmentId() === "string") { + objId += '/' + command.getAttachmentId(); + xwikistorage.removeAttachment(command.getDocId(), + command.getAttachmentId(), + complete); + } else { + xwikistorage.removeItem(objId, complete); + } + }; + + /** + * Get all filenames belonging to a user from the document index + * @method allDocs + * @param {object} command The JIO command + */ + that.allDocs = function () { + setTimeout(function () { + that.error({ + "status": 405, + "statusText": "Method Not Allowed", + "error": "method_not_allowed", + "message": "Your are not allowed to use this command", + "reason": "xwikistorage forbids AllDocs command executions" + }); + }); + }; + + return that; +}); diff --git a/test/jiotests.js b/test/jiotests.js index 47d6a5117bdbd4c37eff39037d0e7a7f55d41445..f7ca0d051b0f25eb77327e21f9792cc23ae2a328 100644 --- a/test/jiotests.js +++ b/test/jiotests.js @@ -164,7 +164,8 @@ generateTools = function (test_namespace) { var o = {}; o.t = test_namespace; - o.clock = sinon.useFakeTimers(); + o.server = o.t.sandbox.server; + o.clock = o.t.sandbox.clock; o.clock.tick(base_tick); o.spy = basicSpyFunction; o.tick = basicTickFunction; @@ -239,10 +240,8 @@ generateTools = function (test_namespace) { switch (type) { case "dav": return 'https:\\/\\/ca-davstorage:8080\\/' + path + '(\\?.*|$)'; - break; - case "s3": + default: return path; - break; } }; o.addFakeServerResponse = function (type, method, path, status, response) { @@ -6281,6 +6280,442 @@ test ('Get revision List', function () { o.jio.stop(); }); */ + +;(function() { +// These tests will only run if we are running the suite inside of XWiki. +module ('Jio XWikiStorage'); +var setUp = function(that, liveTest) { + var o = generateTools(that); + o.server = sinon.fakeServer.create(); + o.jio = JIO.newJio({type:'xwiki',formTokenPath:'form_token'}); + o.addFakeServerResponse("xwiki", "GET", "form_token", 200, + '<meta name="form_token" content="OMGHAX"/>'); + o._addFakeServerResponse = o.addFakeServerResponse; + o.expectedRequests = []; + o.addFakeServerResponse = function(a,b,c,d,e) { + o._addFakeServerResponse(a,b,c,d,e); + o.expectedRequests.push([b,c]); + }; + o.assertReqs = function(count, message) { + o.requests = (o.requests || 0) + count; + ok(o.server.requests.length === o.requests, + message + "[expected [" + count + "] got [" + + (o.server.requests.length - (o.requests - count)) + "]]"); + for (var i = 1; i <= count; i++) { + var req = o.server.requests[o.server.requests.length - i]; + if (!req) { + break; + } + for (var j = o.expectedRequests.length - 1; j >= 0; --j) { + var expected = o.expectedRequests[j]; + if (req.method === expected[0] && + req.url.indexOf(expected[1]) !== 0) + { + o.expectedRequests.splice(j, 1); + } + } + } + var ex = o.expectedRequests.pop(); + if (ex) { + ok(0, "expected [" + ex[0] + "] request for [" + ex[1] + "]"); + } + }; + return o; +}; + +test ("Post", function () { + + var o = setUp(this); + + // post without id + o.spy (o, "status", 405, "Post without id"); + o.jio.post({}, o.f); + o.clock.tick(5000); + o.assertReqs(0, "no id -> no request"); + + // post non empty document + o.addFakeServerResponse("xwiki", "POST", "myFile", 201, "HTML RESPONSE"); + o.spy(o, "value", {"id": "myFile", "ok": true}, + "Create = POST non empty document"); + o.jio.post({"_id": "myFile", "title": "hello there"}, o.f); + o.clock.tick(5000); + o.server.respond(); + o.assertReqs(3, "put -> 1 request to get csrf token, 1 to get doc and 1 to post data"); + + // post but document already exists (post = error!, put = ok) + o.answer = JSON.stringify({"_id": "myFile", "title": "hello there"}); + o.addFakeServerResponse("xwiki", "GET", "myFile", 200, o.answer); + o.spy (o, "status", 409, "Post but document already exists"); + o.jio.post({"_id": "myFile", "title": "hello again"}, o.f); + o.clock.tick(5000); + o.server.respond(); + o.assertReqs(1, "post w/ existing doc -> 1 request to get doc then fail"); + + o.jio.stop(); +}); + +test ("Put", function(){ + + var o = setUp(this); + + // put without id => id required + o.spy (o, "status", 20, "Put without id"); + o.jio.put({}, o.f); + o.clock.tick(5000); + o.assertReqs(0, "put w/o id -> 0 requests"); + + // put non empty document + o.addFakeServerResponse("xwiki", "POST", "put1", 201, "HTML RESPONSE"); + o.spy (o, "value", {"ok": true, "id": "put1"}, + "Create = PUT non empty document"); + o.jio.put({"_id": "put1", "title": "myPut1"}, o.f); + o.clock.tick(5000); + o.server.respond(); + o.assertReqs(3, "put normal doc -> 1 req to get doc, 1 for csrf token, 1 to post"); + + // put but document already exists = update + o.answer = JSON.stringify({"_id": "put1", "title": "myPut1"}); + o.addFakeServerResponse("xwiki", "GET", "put1", 200, o.answer); + o.addFakeServerResponse("xwiki", "POST", "put1", 201, "HTML RESPONSE"); + o.spy (o, "value", {"ok": true, "id": "put1"}, "Updated the document"); + o.jio.put({"_id": "put1", "title": "myPut2abcdedg"}, o.f); + o.clock.tick(5000); + o.server.respond(); + o.assertReqs(3, "put normal doc -> 1 req to get doc, 1 for csrf token, 1 to post"); + + o.jio.stop(); +}); + +test ("PutAttachment", function(){ + + var o = setUp(this); + + // putAttachment without doc id => id required + o.spy(o, "status", 20, "PutAttachment without doc id"); + o.jio.putAttachment({}, o.f); + o.clock.tick(5000); + o.assertReqs(0, "put attach w/o doc id -> 0 requests"); + + // putAttachment without attachment id => attachment id required + o.spy(o, "status", 22, "PutAttachment without attachment id"); + o.jio.putAttachment({"_id": "putattmt1"}, o.f); + o.clock.tick(5000); + o.assertReqs(0, "put attach w/o attach id -> 0 requests"); + + // putAttachment without underlying document => not found + o.addFakeServerResponse("xwiki", "GET", "putattmtx", 404, "HTML RESPONSE"); + o.spy(o, "status", 404, "PutAttachment without document"); + o.jio.putAttachment({"_id": "putattmtx", "_attachment": "putattmt2"}, o.f); + o.clock.tick(5000); + o.server.respond(); + o.assertReqs(1, "put attach w/o existing document -> 1 request to get doc"); + + // putAttachment with document without data + o.answer = JSON.stringify({"_id": "putattmt1", "title": "myPutAttm1"}); + o.addFakeServerResponse("xwiki", "GET", "putattmt1", 200, o.answer); + o.addFakeServerResponse("xwiki", "POST", "putattmt1/putattmt2", 201,"HTML"+ + + "RESPONSE"); + o.spy(o, "value", {"ok": true, "id": "putattmt1/putattmt2"}, + "PutAttachment with document, without data"); + o.jio.putAttachment({"_id": "putattmt1", "_attachment": "putattmt2"}, o.f); + o.clock.tick(5000); + o.server.respond(); + o.assertReqs(3, "put attach -> 1 request to get document, 1 to put " + + "attach, 1 to get csrf token"); + + o.jio.stop(); +}); + +test ("Get", function(){ + + var o = setUp(this); + + // get inexistent document + o.spy(o, "status", 404, "Get non existing document"); + o.jio.get("get1", o.f); + o.clock.tick(5000); + o.server.respond(); + o.assertReqs(1, "try to get nonexistent doc -> 1 request"); + + // get inexistent attachment + o.spy(o, "status", 404, "Get non existing attachment"); + o.jio.get("get1/get2", o.f); + o.clock.tick(5000); + o.server.respond(); + o.assertReqs(1, "try to get nonexistent attach -> 1 request"); + + // get document + o.answer = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' + + '<page xmlns="http://www.xwiki.org"><title>some title</title></page>'; + o.addFakeServerResponse("xwiki", "GET", "get3", 200, o.answer); + o.spy(o, "value", {"_id": "get3", "title": "some title"}, "Get document"); + o.jio.get("get3", o.f); + o.clock.tick(5000); + o.server.respond(); + o.assertReqs(1, "get document -> 1 request"); + + // get inexistent attachment (document exists) + o.spy(o, "status", 404, "Get non existing attachment (doc exists)"); + o.jio.get({"_id": "get3", "_attachment": "getx"}, o.f); + o.clock.tick(5000); + o.server.respond(); + o.assertReqs(1, "get nonexistant attachment -> 1 request"); + + // get attachment + o.answer = JSON.stringify({"_id": "get4", "title": "some attachment"}); + o.addFakeServerResponse("xwiki", "GET", "get3/get4", 200, o.answer); + o.spy(o, "value", {"_id": "get4", "title": "some attachment"}, + "Get attachment"); + o.jio.get({"_id": "get3", "_attachment": "get4"}, o.f); + o.clock.tick(5000); + o.server.respond(); + o.assertReqs(1, "get attachment -> 1 request"); + + o.jio.stop(); +}); + +test ("Remove", function(){ + + var o = setUp(this); + + // remove inexistent document + o.addFakeServerResponse("xwiki", "GET", "remove1", 404, "HTML RESPONSE"); + o.spy(o, "status", 404, "Remove non existening document"); + o.jio.remove({"_id": "remove1"}, o.f); + o.clock.tick(5000); + o.server.respond(); + o.assertReqs(2, "remove nonexistent doc -> 1 request for csrf and 1 for doc"); + + // remove inexistent document/attachment + o.addFakeServerResponse("xwiki", "GET", "remove1/remove2", 404, "HTML" + + "RESPONSE"); + o.spy(o, "status", 404, "Remove inexistent document/attachment"); + o.jio.removeAttachment({"_id": "remove1", "_attachment": "remove2"}, o.f); + o.clock.tick(5000); + o.server.respond(); + o.assertReqs(2, "remove nonexistant attach -> 1 request for csrf and 1 for doc"); + + // remove document + o.answer = JSON.stringify({"_id": "remove3", "title": "some doc"}); + //o.addFakeServerResponse("xwiki", "GET", "remove3", 200, o.answer); + o.addFakeServerResponse("xwiki", "POST", "bin/delete/Main/remove3", + 200, "HTML RESPONSE"); + o.spy(o, "value", {"ok": true, "id": "remove3"}, "Remove document"); + o.jio.remove({"_id": "remove3"}, o.f); + o.clock.tick(5000); + o.server.respond(); + o.assertReqs(2, "remove document -> 1 request for csrf and 1 for deleting doc"); + + o.answer = JSON.stringify({ + "_id": "remove4", + "title": "some doc", + "_attachments": { + "remove5": { + "length": 4, + "digest": "md5-d41d8cd98f00b204e9800998ecf8427e" + } + } + }); + // remove attachment + o.addFakeServerResponse("xwiki", "POST", "delattachment/Main/remove4/remove5", + 200, "HTML RESPONSE"); + o.spy(o, "value", {"ok": true, "id": "remove4/remove5"}, + "Remove attachment"); + o.jio.removeAttachment({"_id": "remove4", "_attachment": "remove5"}, o.f); + o.clock.tick(5000); + o.server.respond(); + o.assertReqs(2, "remove attach -> 1 request for csrf and 1 for deletion"); + + o.jio.stop(); +}); +/* +test ("AllDocs", function () { + + // need to make server requests before activating fakeServer + var davlist = getXML('responsexml/davlist'), + o = setUp(this); + + // get allDocs, no content + addFakeServerResponse("xwiki", "PROPFIND", "", 200, davlist); + o.thisShouldBeTheAnswer = { + "rows": [ + {"id": "alldocs1", "key": "alldocs1", "value": {}}, + {"id": "alldocs2", "key": "alldocs2", "value": {}} + ], + "total_rows": 2 + } + o.spy(o, "value", o.thisShouldBeTheAnswer, "allDocs (no content)"); + o.jio.allDocs(o.f); + o.clock.tick(5000); + respond(); + + // allDocs with option include + o.all1 = {"_id": "allDocs1", "title": "a doc title"}; + o.all2 = {"_id": "allDocs2", "title": "another doc title"}; + o.thisShouldBeTheAnswer = { + "rows": [ + {"id": "alldocs1", "key": "alldocs1", "value": {}, "doc": o.all1}, + {"id": "alldocs2", "key": "alldocs2", "value": {}, "doc": o.all2} + ], + "total_rows": 2 + } + addFakeServerResponse("xwiki", "GET", "alldocs1", 200, + JSON.stringify(o.all1)); + addFakeServerResponse("xwiki", "GET", "alldocs2", 200, + JSON.stringify(o.all2)); + o.spy(o, "value", o.thisShouldBeTheAnswer, "allDocs (include_docs)"); + o.jio.allDocs({"include_docs":true}, o.f); + o.clock.tick(5000); + respond(); + + o.jio.stop(); +}); +*/ + +var nThen = function(next) { + var funcs = []; + var calls = 0; + var waitFor = function(func) { + calls++; + return function() { + if (func) { + func.apply(null, arguments); + } + calls = (calls || 1) - 1; + while (!calls && funcs.length) { + funcs.shift()(waitFor); + } + }; + }; + next(waitFor); + var ret = { + nThen: function(next) { + funcs.push(next); + return ret; + }, + orTimeout: function(func, milliseconds) { + var cto; + var timeout = setTimeout(function() { + while (funcs.shift() !== cto) ; + func(waitFor); + calls = (calls || 1) - 1; + while (!calls && funcs.length) { console.log("call"); funcs.shift()(waitFor); } + }, milliseconds); + funcs.push(cto = function() { clearTimeout(timeout); }); + return ret; + } + }; + return ret; +}; + + +if (window.location.href.match(/xwiki\/bin\/view/)) (function() { +test ("XWiki Live Server setup", function () { + + var o = setUp(this); + o.jio.stop(); + this.sandbox.restore(); + o.jio.start(); + QUnit.stop(); + + nThen(function(waitFor) { + + // Remove the document if it exists. + o.jio.remove({"_id": "one.json"}, waitFor()); + + }).nThen(function(waitFor) { + + // post a new document + o.spy(o, "value", {"id": "one.json", "ok": true}, "Live post document"); + o.jio.post({"_id": "one.json", "title": "hello"}, waitFor(o.f)); + + }).nThen(function(waitFor) { + + o.jio.get("one.json", waitFor(function(err, ret) { + ok(!err); + ok(ret._id == "one.json"); + ok(ret.title == "hello"); + })); + + }).nThen(function(waitFor) { + + // modify document + o.spy(o, "value", {"id": "one.json", "ok": true}, "Live modify document"); + o.jio.put({"_id": "one.json", "title": "hello modified"}, waitFor(o.f)); + + }).nThen(function(waitFor) { + + o.jio.get("one.json", waitFor(function(err, ret) { + ok(!err); + ok(ret.title == "hello modified"); + })); + + }).nThen(function(waitFor) { + + // add attachment + o.spy(o, "value", {"id": "one.json/att.txt", "ok": true}, "Put attachment"); + o.jio.putAttachment({ + "_id": "one.json", + "_attachment": "att.txt", + "_mimetype": "text/plain", + "_data": "there2" + }, waitFor(o.f)); + + }).nThen(function(waitFor) { + + // test allDocs + /*o.jio.allDocs({"include_docs":true}, + function(s){console.log(s);}, + function ( e ) {console.log(e); + }, o.f);*/ + + }).nThen(function(waitFor) { + + // get Attachment + o.jio.getAttachment({"_id":"one.json", "_attachment":"att.txt"}, waitFor(function(err, ret) { + ok(!err); + var fr = new FileReader(); + fr.onload = waitFor(function(dat) { + ok(dat.target.result == "there2"); + }); + fr.readAsText(ret); + })); + + }).nThen(function(waitFor) { + + // remove Attachment + o.spy(o, "value", {"id": "one.json/att.txt", "ok": true}, "Remove attachment"); + o.jio.removeAttachment({"_id":"one.json","_attachment":"att.txt"}, waitFor(o.f)); + + }).nThen(function(waitFor) { + + // remove Document + o.spy(o, "value", {"id": "one.json", "ok": true}, "Remove document"); + o.jio.remove("one.json", waitFor(o.f)); + + }).nThen(function(waitFor) { + + //console.log("success"); + + }).orTimeout(function() { + + //console.log("failed"); + ok(0); + + }, 15000).nThen(function() { + + //console.log("complete"); + o.jio.stop(); + QUnit.start(); + + }); + +}); +})(); // Live XWiki + +})(); // xwiki + + }; // end thisfun if (window.requirejs) { diff --git a/test/jiotests_withoutrequirejs.html b/test/jiotests_withoutrequirejs.html index 860508f38a3b24ab400e682a8b6cab45fb361eb4..d656af868c79cc82193aebdcb91b0c6fc3b93582 100644 --- a/test/jiotests_withoutrequirejs.html +++ b/test/jiotests_withoutrequirejs.html @@ -33,6 +33,8 @@ </script> <script type="text/javascript" src="../complex_queries.js"></script> </script> + <script type="text/javascript" src="../src/jio.storage/xwikistorage.js"> + </script> <script type="text/javascript" src="./jiotests.js"></script> </body> </html>