From cfc82280faef70fc83ca9dfd18b1beebe34329b3 Mon Sep 17 00:00:00 2001 From: Bogdan Abaev Date: Thu, 18 May 2023 23:41:35 -0400 Subject: [PATCH 01/33] phpunit test v1 and v2 converted to mocha/chai --- tests/remote_js/api2.js | 764 +++++++ tests/remote_js/api3.js | 144 ++ tests/remote_js/config.js | 9 + tests/remote_js/config.json | 35 + tests/remote_js/groupsSetup.js | 104 + tests/remote_js/helpers.js | 105 + tests/remote_js/httpHandler.js | 68 + tests/remote_js/package-lock.json | 2378 +++++++++++++++++++++ tests/remote_js/package.json | 16 + tests/remote_js/test/1/collectionTest.js | 119 ++ tests/remote_js/test/1/itemsTest.js | 36 + tests/remote_js/test/2/atomTest.js | 124 ++ tests/remote_js/test/2/bibTest.js | 19 + tests/remote_js/test/2/cacheTest.js | 47 + tests/remote_js/test/2/collectionTest.js | 300 +++ tests/remote_js/test/2/creatorTest.js | 66 + tests/remote_js/test/2/fileTest.js | 19 + tests/remote_js/test/2/fullText.js | 238 +++ tests/remote_js/test/2/generalTest.js | 66 + tests/remote_js/test/2/groupTest.js | 123 ++ tests/remote_js/test/2/itemsTest.js | 1093 ++++++++++ tests/remote_js/test/2/mappingsTest.js | 64 + tests/remote_js/test/2/noteTest.js | 104 + tests/remote_js/test/2/objectTest.js | 454 ++++ tests/remote_js/test/2/paramTest.js | 311 +++ tests/remote_js/test/2/permissionsTest.js | 246 +++ tests/remote_js/test/2/relationsTest.js | 279 +++ tests/remote_js/test/2/searchTest.js | 171 ++ tests/remote_js/test/2/settingsTest.js | 403 ++++ tests/remote_js/test/2/sortTest.js | 128 ++ tests/remote_js/test/2/storageAdmin.js | 50 + tests/remote_js/test/2/tagTest.js | 257 +++ tests/remote_js/test/2/template.js | 18 + tests/remote_js/test/2/versionTest.js | 581 +++++ tests/remote_js/test/shared.js | 28 + 35 files changed, 8967 insertions(+) create mode 100644 tests/remote_js/api2.js create mode 100644 tests/remote_js/api3.js create mode 100644 tests/remote_js/config.js create mode 100644 tests/remote_js/config.json create mode 100644 tests/remote_js/groupsSetup.js create mode 100644 tests/remote_js/helpers.js create mode 100644 tests/remote_js/httpHandler.js create mode 100644 tests/remote_js/package-lock.json create mode 100644 tests/remote_js/package.json create mode 100644 tests/remote_js/test/1/collectionTest.js create mode 100644 tests/remote_js/test/1/itemsTest.js create mode 100644 tests/remote_js/test/2/atomTest.js create mode 100644 tests/remote_js/test/2/bibTest.js create mode 100644 tests/remote_js/test/2/cacheTest.js create mode 100644 tests/remote_js/test/2/collectionTest.js create mode 100644 tests/remote_js/test/2/creatorTest.js create mode 100644 tests/remote_js/test/2/fileTest.js create mode 100644 tests/remote_js/test/2/fullText.js create mode 100644 tests/remote_js/test/2/generalTest.js create mode 100644 tests/remote_js/test/2/groupTest.js create mode 100644 tests/remote_js/test/2/itemsTest.js create mode 100644 tests/remote_js/test/2/mappingsTest.js create mode 100644 tests/remote_js/test/2/noteTest.js create mode 100644 tests/remote_js/test/2/objectTest.js create mode 100644 tests/remote_js/test/2/paramTest.js create mode 100644 tests/remote_js/test/2/permissionsTest.js create mode 100644 tests/remote_js/test/2/relationsTest.js create mode 100644 tests/remote_js/test/2/searchTest.js create mode 100644 tests/remote_js/test/2/settingsTest.js create mode 100644 tests/remote_js/test/2/sortTest.js create mode 100644 tests/remote_js/test/2/storageAdmin.js create mode 100644 tests/remote_js/test/2/tagTest.js create mode 100644 tests/remote_js/test/2/template.js create mode 100644 tests/remote_js/test/2/versionTest.js create mode 100644 tests/remote_js/test/shared.js diff --git a/tests/remote_js/api2.js b/tests/remote_js/api2.js new file mode 100644 index 00000000..3be7e481 --- /dev/null +++ b/tests/remote_js/api2.js @@ -0,0 +1,764 @@ + +const HTTP = require("./httpHandler"); +const { JSDOM } = require("jsdom"); +const wgxpath = require('wgxpath'); + +class API2 { + static apiVersion = null; + + static config = require("./config"); + + static useAPIVersion(version) { + this.apiVersion = version; + } + + static async login() { + const response = await HTTP.post( + `${this.config.apiURLPrefix}test/setup?u=${this.config.userID}&u2=${this.config.userID2}`, + " ", + {}, { + username: this.config.rootUsername, + password: this.config.rootPassword + }); + if (!response.data) { + throw new Error("Could not fetch credentials!"); + } + return JSON.parse(response.data); + } + + static async getItemTemplate(itemType) { + let response = await this.get(`items/new?itemType=${itemType}`); + return JSON.parse(response.data); + } + + static async createItem(itemType, data = {}, context = null, responseFormat = 'atom') { + let json = await this.getItemTemplate(itemType); + + if (data) { + for (let field in data) { + json[field] = data[field]; + } + } + + let response = await this.userPost( + this.config.userID, + `items?key=${this.config.apiKey}`, + JSON.stringify({ + items: [json] + }), + { "Content-Type": "application/json" } + ); + + return this.handleCreateResponse('item', response, responseFormat, context); + } + + static async postItems(json) { + return this.userPost( + this.config.userID, + `items?key=${this.config.apiKey}`, + JSON.stringify({ + items: json + }), + { "Content-Type": "application/json" } + ); + } + + static async postItem(json) { + return this.postItems([json]); + } + + static async groupCreateItem(groupID, itemType, context = null, responseFormat = 'atom') { + let response = await this.get(`items/new?itemType=${itemType}`); + let json = this.getJSONFromResponse(response); + + response = await this.groupPost( + groupID, + `items?key=${this.config.apiKey}`, + JSON.stringify({ + items: [json] + }), + { "Content-Type": "application/json" } + ); + + if (context && response.status != 200) { + throw new Error("Group post resurned status != 200"); + } + + json = this.getJSONFromResponse(response); + + if (responseFormat !== 'json' && Object.keys(json.success).length !== 1) { + console.log(json); + throw new Error("Item creation failed"); + } + + switch (responseFormat) { + case 'json': + return json; + + case 'key': + return Object.keys(json.success).shift(); + + case 'atom': { + let itemKey = Object.keys(json.success).shift(); + return this.groupGetItemXML(groupID, itemKey, context); + } + + default: + throw new Error(`Invalid response format '${responseFormat}'`); + } + } + + static async createAttachmentItem(linkMode, data = {}, parentKey = false, context = false, responseFormat = 'atom') { + let response = await this.get(`items/new?itemType=attachment&linkMode=${linkMode}`); + let json = JSON.parse(response.data); + + Object.keys(data).forEach((key) => { + json[key] = data[key]; + }); + + if (parentKey) { + json.parentItem = parentKey; + } + + response = await this.userPost( + this.config.userID, + `items?key=${this.config.apiKey}`, + JSON.stringify({ + items: [json] + }), + { "Content-Type": "application/json" } + ); + + if (context && response.status !== 200) { + throw new Error("Response status is not 200"); + } + + json = this.getJSONFromResponse(response); + + if (responseFormat !== 'json' && Object.keys(json.success).length !== 1) { + console.log(json); + throw new Error("Item creation failed"); + } + + switch (responseFormat) { + case 'json': + return json; + + case 'key': + return json.success[0]; + + case 'atom': { + const itemKey = json.success[0]; + let xml = await this.getItemXML(itemKey, context); + + if (context) { + const data = this.parseDataFromAtomEntry(xml); + json = JSON.parse(data.content); + if (linkMode !== json.linkMode) { + throw new Error("Link mode does not match"); + } + } + return xml; + } + + + default: + throw new Error(`Invalid response format '${responseFormat}'`); + } + } + + static async groupCreateAttachmentItem(groupID, linkMode, data = {}, parentKey = false, context = false, responseFormat = 'atom') { + let response = await this.get(`items/new?itemType=attachment&linkMode=${linkMode}`); + let json = JSON.parse(response.data); + + Object.keys(data).forEach((key) => { + json[key] = data[key]; + }); + + if (parentKey) { + json.parentItem = parentKey; + } + + response = await this.groupPost( + groupID, + `items?key=${this.config.apiKey}`, + JSON.stringify({ + items: [json] + }), + { "Content-Type": "application/json" } + ); + + if (context && response.status !== 200) { + throw new Error("Response status is not 200"); + } + + json = this.getJSONFromResponse(response); + + if (responseFormat !== 'json' && Object.keys(json.success).length !== 1) { + console.log(json); + throw new Error("Item creation failed"); + } + + switch (responseFormat) { + case 'json': + return json; + + case 'key': + return json.success[0]; + + case 'atom': { + const itemKey = json.success[0]; + let xml = await this.groupGetItemXML(groupID, itemKey, context); + + if (context) { + const data = this.parseDataFromAtomEntry(xml); + json = JSON.parse(data.content); + + if (linkMode !== json.linkMode) { + throw new Error("Link mode does not match"); + } + } + return xml; + } + + + default: + throw new Error(`Invalid response format '${responseFormat}'`); + } + } + + static async createNoteItem(text = "", parentKey = false, context = false, responseFormat = 'atom') { + let response = await this.get(`items/new?itemType=note`); + let json = JSON.parse(response.data); + + json.note = text; + + if (parentKey) { + json.parentItem = parentKey; + } + + response = await this.userPost( + this.config.userID, + `items?key=${this.config.apiKey}`, + JSON.stringify({ + items: [json] + }), + { "Content-Type": "application/json" } + ); + + if (context && response.status !== 200) { + throw new Error("Response status is not 200"); + } + + json = this.getJSONFromResponse(response); + + if (responseFormat !== 'json' && Object.keys(json.success).length !== 1) { + console.log(json); + throw new Error("Item creation failed"); + } + + switch (responseFormat) { + case 'json': + return json; + + case 'key': + return json.success[0]; + + case 'atom': { + const itemKey = json.success[0]; + let xml = await this.getItemXML(itemKey, context); + + if (context) { + const data = this.parseDataFromAtomEntry(xml); + json = JSON.parse(data.content); + + if (text !== json.note) { + throw new Error("Text does not match"); + } + } + + return xml; + } + + default: + throw new Error(`Invalid response format '${responseFormat}'`); + } + } + + static async createCollection(name, data = {}, context = null, responseFormat = 'atom') { + let parent, relations; + + if (typeof data == 'object') { + parent = data.parentCollection ? data.parentCollection : false; + relations = data.relations ? data.relations : {}; + } + else { + parent = data || false; + relations = {}; + } + + const json = { + collections: [ + { + name: name, + parentCollection: parent, + relations: relations + } + ] + }; + + const response = await this.userPost( + this.config.userID, + `collections?key=${this.config.apiKey}`, + JSON.stringify(json), + { "Content-Type": "application/json" } + ); + + return this.handleCreateResponse('collection', response, responseFormat, context); + } + + static async createSearch(name, conditions = [], context = null, responseFormat = 'atom') { + if (conditions === 'default') { + conditions = [ + { + condition: 'title', + operator: 'contains', + value: 'test' + } + ]; + } + + const json = { + searches: [ + { + name: name, + conditions: conditions + } + ] + }; + + const response = await this.userPost( + this.config.userID, + `searches?key=${this.config.apiKey}`, + JSON.stringify(json), + { "Content-Type": "application/json" } + ); + + return this.handleCreateResponse('search', response, responseFormat, context); + } + + static async getLibraryVersion() { + const response = await this.userGet( + this.config.userID, + `items?key=${this.config.apiKey}&format=keys&limit=1` + ); + return response.headers["last-modified-version"][0]; + } + + static async getGroupLibraryVersion(groupID) { + const response = await this.groupGet( + groupID, + `items?key=${this.config.apiKey}&format=keys&limit=1` + ); + return response.headers["last-modified-version"][0]; + } + + static async getItemXML(keys, context = null) { + return this.getObjectXML('item', keys, context); + } + + static async groupGetItemXML(groupID, keys, context = null) { + if (typeof keys === 'string' || typeof keys === 'number') { + keys = [keys]; + } + + const response = await this.groupGet( + groupID, + `items?key=${this.config.apiKey}&itemKey=${keys.join(',')}&order=itemKeyList&content=json` + ); + if (context && response.status != 200) { + throw new Error("Group set request failed."); + } + return this.getXMLFromResponse(response); + } + + static async getXMLFromFirstSuccessItem(response) { + const key = this.getFirstSuccessKeyFromResponse(response); + return this.getItemXML(key); + } + + static async getCollectionXML(keys, context = null) { + return this.getObjectXML('collection', keys, context); + } + + static async getSearchXML(keys, context = null) { + return this.getObjectXML('search', keys, context); + } + + // Simple http requests with no dependencies + static async get(url, headers = {}, auth = null) { + url = this.config.apiURLPrefix + url; + if (this.apiVersion) { + headers["Zotero-API-Version"] = this.apiVersion; + } + + const response = await HTTP.get(url, headers, auth); + + if (this.config.verbose >= 2) { + console.log("\n\n" + response.data + "\n"); + } + + return response; + } + + static async superGet(url, headers = {}) { + return this.get(url, headers, { + username: this.config.username, + password: this.config.password + }); + } + + static async userGet(userID, suffix, headers = {}, auth = null) { + return this.get(`users/${userID}/${suffix}`, headers, auth); + } + + static async groupGet(groupID, suffix, headers = {}, auth = null) { + return this.get(`groups/${groupID}/${suffix}`, headers, auth); + } + + static async post(url, data, headers = {}, auth = null) { + url = this.config.apiURLPrefix + url; + if (this.apiVersion) { + headers["Zotero-API-Version"] = this.apiVersion; + } + + return HTTP.post(url, data, headers, auth); + } + + static async userPost(userID, suffix, data, headers = {}, auth = null) { + return this.post(`users/${userID}/${suffix}`, data, headers, auth); + } + + static async groupPost(groupID, suffix, data, headers = {}, auth = null) { + return this.post(`groups/${groupID}/${suffix}`, data, headers, auth); + } + + static async put(url, data, headers = {}, auth = null) { + url = this.config.apiURLPrefix + url; + if (this.apiVersion) { + headers["Zotero-API-Version"] = this.apiVersion; + } + + return HTTP.put(url, data, headers, auth); + } + + static async userPut(userID, suffix, data, headers = {}, auth = null) { + return this.put(`users/${userID}/${suffix}`, data, headers, auth); + } + + static async groupPut(groupID, suffix, data, headers = {}, auth = null) { + return this.put(`groups/${groupID}/${suffix}`, data, headers, auth); + } + + static async patch(url, data, headers = {}, auth = null) { + url = this.config.apiURLPrefix + url; + if (this.apiVersion) { + headers["Zotero-API-Version"] = this.apiVersion; + } + + return HTTP.patch(url, data, headers, auth); + } + + static async userPatch(userID, suffix, data, headers = {}) { + return this.patch(`users/${userID}/${suffix}`, data, headers); + } + + static async delete(url, data, headers = {}, auth = null) { + url = this.config.apiURLPrefix + url; + if (this.apiVersion) { + headers["Zotero-API-Version"] = this.apiVersion; + } + return HTTP.delete(url, data, headers, auth); + } + + static async userDelete(userID, suffix, data, headers = {}) { + return this.delete(`users/${userID}/${suffix}`, data, headers); + } + + static async head(url, headers = {}, auth = null) { + url = this.config.apiURLPrefix + url; + if (this.apiVersion) { + headers["Zotero-API-Version"] = this.apiVersion; + } + + return HTTP.head(url, headers, auth); + } + + static async userClear(userID) { + const response = await this.userPost( + userID, + "clear", + "", + {}, + { + username: this.config.rootUsername, + password: this.config.rootPassword + } + ); + if (response.status !== 204) { + console.log(response.data); + throw new Error(`Error clearing user ${userID}`); + } + } + + static async groupClear(groupID) { + const response = await this.groupPost( + groupID, + "clear", + "", + {}, + { + username: this.config.rootUsername, + password: this.config.rootPassword + } + ); + + if (response.status !== 204) { + console.log(response.data); + throw new Error(`Error clearing group ${groupID}`); + } + } + + + // Response parsing + static arrayGetFirst(arr) { + try { + return arr[0]; + } + catch (e) { + return null; + } + } + + + static getXMLFromResponse(response) { + var result; + try { + const jsdom = new JSDOM(response.data, { contentType: "application/xml", url: "http://localhost/" }); + wgxpath.install(jsdom.window, true); + result = jsdom.window._document; + } + catch (e) { + console.log(response.data); + throw e; + } + return result; + } + + static getJSONFromResponse(response) { + const json = JSON.parse(response.data); + if (json === null) { + console.log(response.data); + throw new Error("JSON response could not be parsed"); + } + return json; + } + + static getFirstSuccessKeyFromResponse(response) { + const json = this.getJSONFromResponse(response); + if (!json.success || json.success.length === 0) { + console.log(response.data); + throw new Error("No success keys found in response"); + } + return json.success[0]; + } + + static parseDataFromAtomEntry(entryXML) { + const key = this.arrayGetFirst(entryXML.getElementsByTagName('zapi:key')); + const version = this.arrayGetFirst(entryXML.getElementsByTagName('zapi:version')); + const content = this.arrayGetFirst(entryXML.getElementsByTagName('content')); + if (content === null) { + throw new Error("Atom response does not contain "); + } + + return { + key: key ? key.textContent : null, + version: version ? version.textContent : null, + content: content ? content.textContent : null + }; + } + + static getContentFromResponse(response) { + const xml = this.getXMLFromResponse(response); + const data = this.parseDataFromAtomEntry(xml); + return data.content; + } + + // + static getPluralObjectType(objectType) { + if (objectType === 'search') { + return objectType + "es"; + } + return objectType + "s"; + } + + static async getObjectXML(objectType, keys, context = null) { + let objectTypePlural = this.getPluralObjectType(objectType); + + if (!Array.isArray(keys)) { + keys = [keys]; + } + + let response = await this.userGet( + this.config.userID, + `${objectTypePlural}?key=${this.config.apiKey}&${objectType}Key=${keys.join(',')}&order=${objectType}KeyList&content=json` + ); + + // Checking the response status + if (context && response.status !== 200) { + throw new Error("Response status is not 200"); + } + + return this.getXMLFromResponse(response); + } + + static async handleCreateResponse(objectType, response, responseFormat, context = null) { + let uctype = objectType.charAt(0).toUpperCase() + objectType.slice(1); + + // Checking the response status + if (response.status !== 200) { + throw new Error("Response status is not 200"); + } + + let json = JSON.parse(response.data); + + if (responseFormat !== 'responsejson' && (!json.success || Object.keys(json.success).length !== 1)) { + console.log(json); + return response; + //throw new Error(`${uctype} creation failed`); + } + + if (responseFormat === 'responsejson') { + return json; + } + + let key = json.success[0]; + + if (responseFormat === 'key') { + return key; + } + + // Calling the corresponding function based on the uctype + let xml; + switch (uctype) { + case 'Search': + xml = await this.getSearchXML(key, context); + break; + case 'Item': + xml = await this.getItemXML(key, context); + break; + case 'Collection': + xml = await this.getCollectionXML(key, context); + break; + } + + if (responseFormat === 'atom') { + return xml; + } + + let data = this.parseDataFromAtomEntry(xml); + + if (responseFormat === 'data') { + return data; + } + if (responseFormat === 'content') { + return data.content; + } + if (responseFormat === 'json') { + return JSON.parse(data.content); + } + + throw new Error(`Invalid response format '${responseFormat}'`); + } + + static async setKeyOption(userID, key, option, val) { + let response = await this.get( + `users/${userID}/keys/${key}`, + {}, + { + username: this.config.rootUsername, + password: this.config.rootPassword + } + ); + + // Checking the response status + if (response.status !== 200) { + console.log(response.data); + throw new Error(`GET returned ${response.status}`); + } + + let xml; + try { + xml = this.getXMLFromResponse(response); + } + catch (e) { + console.log(response.data); + throw e; + } + + for (let access of xml.getElementsByTagName('access')) { + switch (option) { + case 'libraryNotes': { + if (!access.hasAttribute('library')) { + break; + } + let current = parseInt(access.getAttribute('notes')); + if (current !== val) { + access.setAttribute('notes', val); + response = await this.put( + `users/${this.config.userID}/keys/${this.config.apiKey}`, + xml.documentElement.outerHTML, + {}, + { + username: this.config.rootUsername, + password: this.config.rootPassword + } + ); + if (response.status !== 200) { + console.log(response.data); + throw new Error(`PUT returned ${response.status}`); + } + } + break; + } + + + case 'libraryWrite': { + if (!access.hasAttribute('library')) { + continue; + } + let current = parseInt(access.getAttribute('write')); + if (current !== val) { + access.setAttribute('write', val); + response = await this.put( + `users/${this.config.userID}/keys/${this.config.apiKey}`, + xml.documentElement.outerHTML, + {}, + { + username: this.config.rootUsername, + password: this.config.rootPassword + } + ); + if (response.status !== 200) { + console.log(response.data); + throw new Error(`PUT returned ${response.status}`); + } + } + break; + } + } + } + } +} + +module.exports = API2; diff --git a/tests/remote_js/api3.js b/tests/remote_js/api3.js new file mode 100644 index 00000000..c0c394d5 --- /dev/null +++ b/tests/remote_js/api3.js @@ -0,0 +1,144 @@ +const HTTP = require("./httpHandler"); +const { JSDOM } = require("jsdom"); + +class API3 { + static config = require("./config"); + + static apiVersion = null; + + static useAPIVersion(version) { + this.apiVersion = version; + } + + static async get(url, headers = [], auth = false) { + url = this.config.apiURLPrefix + url; + if (this.apiVersion) { + headers.push("Zotero-API-Version: " + this.apiVersion); + } + if (this.schemaVersion) { + headers.push("Zotero-Schema-Version: " + this.schemaVersion); + } + if (!auth && this.apiKey) { + headers.push("Authorization: Bearer " + this.apiKey); + } + let response = await HTTP.get(url, headers, auth); + if (this.config.verbose >= 2) { + console.log("\n\n" + response.getBody() + "\n"); + } + return response; + } + + + static async delete(url, data, headers = [], auth = false) { + url = this.config.apiURLPrefix + url; + if (this.apiVersion) { + headers.push("Zotero-API-Version: " + this.apiVersion); + } + if (this.schemaVersion) { + headers.push("Zotero-Schema-Version: " + this.schemaVersion); + } + if (!auth && this.config.apiKey) { + headers.push("Authorization: Bearer " + this.config.apiKey); + } + let response = await HTTP.delete(url, data, headers, auth); + return response; + } + + static async post(url, data, headers = [], auth = false) { + url = this.config.apiURLPrefix + url; + if (this.apiVersion) { + headers.push("Zotero-API-Version: " + this.apiVersion); + } + if (this.schemaVersion) { + headers.push("Zotero-Schema-Version: " + this.schemaVersion); + } + if (!auth && this.config.apiKey) { + headers.push("Authorization: Bearer " + this.config.apiKey); + } + let response = await HTTP.post(url, data, headers, auth); + return response; + } + + + static async superGet(url, headers = {}) { + return this.get(url, headers, { + username: this.config.rootUsername, + password: this.config.rootPassword + }); + } + + + static async superPost(url, data, headers = {}) { + return this.post(url, data, headers, { + username: this.config.rootUsername, + password: this.config.rootPassword + }); + } + + static async superDelete(url, data, headers = {}) { + return this.delete(url, data, headers, { + username: this.config.rootUsername, + password: this.config.rootPassword + }); + } + + static async createGroup(fields, returnFormat = 'id') { + const xmlDoc = new JSDOM(""); + const groupXML = xmlDoc.window.document.getElementsByTagName("group")[0]; + groupXML.setAttributeNS(null, "owner", fields.owner); + groupXML.setAttributeNS(null, "name", fields.name || "Test Group " + Math.random().toString(36).substring(2, 15)); + groupXML.setAttributeNS(null, "type", fields.type); + groupXML.setAttributeNS(null, "libraryEditing", fields.libraryEditing || 'members'); + groupXML.setAttributeNS(null, "libraryReading", fields.libraryReading || 'members'); + groupXML.setAttributeNS(null, "fileEditing", fields.fileEditing || 'none'); + groupXML.setAttributeNS(null, "description", ""); + groupXML.setAttributeNS(null, "url", ""); + groupXML.setAttributeNS(null, "hasImage", false); + + + let response = await this.superPost( + "groups", + xmlDoc.window.document.getElementsByTagName("body")[0].innerHTML + ); + if (response.status != 201) { + console.log(response.data); + throw new Error("Unexpected response code " + response.status); + } + + let url = response.headers.location[0]; + let groupID = parseInt(url.match(/\d+$/)[0]); + + // Add members + if (fields.members && fields.members.length) { + let xml = ''; + for (let member of fields.members) { + xml += ''; + } + let usersResponse = await this.superPost(`groups/${groupID}/users`, xml); + if (usersResponse.status != 200) { + console.log(usersResponse.data); + throw new Error("Unexpected response code " + usersResponse.status); + } + } + + if (returnFormat == 'response') { + return response; + } + if (returnFormat == 'id') { + return groupID; + } + throw new Error(`Unknown response format '${returnFormat}'`); + } + + static async deleteGroup(groupID) { + let response = await this.superDelete( + `groups/${groupID}` + ); + if (response.status != 204) { + console.log(response.data); + throw new Error("Unexpected response code " + response.status); + } + } +} + +module.exports = API3; diff --git a/tests/remote_js/config.js b/tests/remote_js/config.js new file mode 100644 index 00000000..aa50ac70 --- /dev/null +++ b/tests/remote_js/config.js @@ -0,0 +1,9 @@ +const fs = require('fs'); + +var config = {}; + +const data = fs.readFileSync('config.json'); +config = JSON.parse(data); +config.timeout = 60000; + +module.exports = config; diff --git a/tests/remote_js/config.json b/tests/remote_js/config.json new file mode 100644 index 00000000..f47a8a0d --- /dev/null +++ b/tests/remote_js/config.json @@ -0,0 +1,35 @@ +{ + "verbose": 1, + "syncURLPrefix": "http://dataserver/", + "apiURLPrefix": "http://localhost/", + "rootUsername": "YtTnrHcWUC0FqP27xuaa", + "rootPassword": "esFEIngwxnyp1kuTrUpKpH72gEftHbkiWneoeimV", + "awsRegion": "us-east-1", + "s3Bucket": "zotero", + "awsAccessKey": "", + "awsSecretKey": "", + "filesystemStorage": 1, + "syncVersion": 9, + + + "userID": 1, + "libraryID": 1, + "username": "testuser", + "password": "letmein", + "displayName": "testuser", + "emailPrimary": "test@test.com", + "emailSecondary": "test@test.com", + "ownedPrivateGroupID": 1, + "ownedPrivateGroupLibraryID": 1, + "ownedPrivateGroupName": "Test Group", + + + "userID2": 2, + "username2": "testuser2", + "password2": "letmein2", + "displayName2": "testuser2", + "ownedPrivateGroupID2": 0, + "ownedPrivateGroupLibraryID2": 0 + + +} diff --git a/tests/remote_js/groupsSetup.js b/tests/remote_js/groupsSetup.js new file mode 100644 index 00000000..691291d1 --- /dev/null +++ b/tests/remote_js/groupsSetup.js @@ -0,0 +1,104 @@ + +const config = require('./config'); +const API3 = require('./api3.js'); +const API2 = require('./api2.js'); + +const resetGroups = async () => { + let resetGroups = true; + + let response = await API3.superGet( + `users/${config.userID}/groups` + ); + let groups = await API2.getJSONFromResponse(response); + config.ownedPublicGroupID = null; + config.ownedPublicNoAnonymousGroupID = null; + config.ownedPrivateGroupID = null; + config.ownedPrivateGroupName = 'Private Test Group'; + config.ownedPrivateGroupID2 = null; + let toDelete = []; + for (let group of groups) { + let data = group.data; + let id = data.id; + let type = data.type; + let owner = data.owner; + let libraryReading = data.libraryReading; + + if (resetGroups) { + toDelete.push(id); + continue; + } + + if (!config.ownedPublicGroupID + && type == 'PublicOpen' + && owner == config.userID + && libraryReading == 'all') { + config.ownedPublicGroupID = id; + } + else if (!config.ownedPublicNoAnonymousGroupID + && type == 'PublicClosed' + && owner == config.userID + && libraryReading == 'members') { + config.ownedPublicNoAnonymousGroupID = id; + } + else if (type == 'Private' && owner == config.userID && data.name == config.ownedPrivateGroupName) { + config.ownedPrivateGroupID = id; + } + else if (type == 'Private' && owner == config.userID2) { + config.ownedPrivateGroupID2 = id; + } + else { + toDelete.push(id); + } + } + + if (!config.ownedPublicGroupID) { + config.ownedPublicGroupID = await API3.createGroup({ + owner: config.userID, + type: 'PublicOpen', + libraryReading: 'all' + }); + } + if (!config.ownedPublicNoAnonymousGroupID) { + config.ownedPublicNoAnonymousGroupID = await API3.createGroup({ + owner: config.userID, + type: 'PublicClosed', + libraryReading: 'members' + }); + } + if (!config.ownedPrivateGroupID) { + config.ownedPrivateGroupID = await API3.createGroup({ + owner: config.userID, + name: "Private Test Group", + type: 'Private', + libraryReading: 'members', + fileEditing: 'members', + members: [ + config.userID2 + ] + }); + } + if (!config.ownedPrivateGroupID2) { + config.ownedPrivateGroupID2 = await API3.createGroup({ + owner: config.userID2, + type: 'Private', + libraryReading: 'members', + fileEditing: 'members' + }); + } + for (let groupID of toDelete) { + await API3.deleteGroup(groupID); + } + + config.numOwnedGroups = 3; + config.numPublicGroups = 2; + + for (let group of groups) { + if (!toDelete.includes(group.id)) { + await API2.groupClear(group.id); + } + } +}; + +module.exports = { + resetGroups +}; diff --git a/tests/remote_js/helpers.js b/tests/remote_js/helpers.js new file mode 100644 index 00000000..6f2864a0 --- /dev/null +++ b/tests/remote_js/helpers.js @@ -0,0 +1,105 @@ +const { JSDOM } = require("jsdom"); +const chai = require('chai'); +const assert = chai.assert; +const crypto = require('crypto'); + +class Helpers { + static uniqueToken = () => { + const id = crypto.randomBytes(16).toString("hex"); + const hash = crypto.createHash('md5').update(id).digest('hex'); + return hash; + }; + + static uniqueID = () => { + const chars = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Z']; + let result = ""; + for (let i = 0; i < 8; i++) { + result += chars[crypto.randomInt(chars.length)]; + } + return result; + }; + + static namespaceResolver = (prefix) => { + let ns = { + atom: 'http://www.w3.org/2005/Atom', + zapi: 'http://zotero.org/ns/api', + zxfer: 'http://zotero.org/ns/transfer' + }; + return ns[prefix] || null; + }; + + static xpathEval = (document, xpath, returnHtml = false, multiple = false) => { + const xpathData = document.evaluate(xpath, document, this.namespaceResolver, 5, null); + if (!multiple && xpathData.snapshotLength != 1) { + throw new Error("No single xpath value fetched"); + } + var node; + var result = []; + do { + node = xpathData.iterateNext(); + if (node) { + result.push(node); + } + } while (node); + + if (returnHtml) { + return multiple ? result : result[0]; + } + + return multiple ? result.map(el => el.innerHTML) : result[0].innerHTML; + }; + + static assertStatusCode = (response, expectedCode, message) => { + try { + assert.equal(response.status, expectedCode); + if (message) { + assert.equal(response.data, message); + } + } + catch (e) { + console.log(response.data); + throw e; + } + }; + + static assertStatusForObject = (response, status, recordId, httpCode, message) => { + let body = response; + if (response.data) { + body = response.data; + } + try { + body = JSON.parse(body); + } + catch (e) { } + assert.include(['unchanged', 'failed', 'success'], status); + + try { + //Make sure the recordId is in the right category - unchanged, failed, success + assert.property(body[status], recordId); + if (httpCode) { + assert.equal(body[status][recordId].code, httpCode); + } + if (message) { + assert.equal(body[status][recordId].message, message); + } + } + catch (e) { + console.log(response.data); + throw e; + } + }; + + static assertNumResults = (response, expectedResults) => { + const doc = new JSDOM(response.data, { url: "http://localhost/" }); + const entries = this.xpathEval(doc.window.document, "//entry", false, true); + assert.equal(entries.length, expectedResults); + }; + + static assertTotalResults = (response, expectedResults) => { + const doc = new JSDOM(response.data, { url: "http://localhost/" }); + const totalResults = this.xpathEval(doc.window.document, "//zapi:totalResults", false, true); + assert.equal(parseInt(totalResults[0]), expectedResults); + }; +} + +module.exports = Helpers; diff --git a/tests/remote_js/httpHandler.js b/tests/remote_js/httpHandler.js new file mode 100644 index 00000000..e729c6ff --- /dev/null +++ b/tests/remote_js/httpHandler.js @@ -0,0 +1,68 @@ +const fetch = require('node-fetch'); +const config = require('./config'); + +class HTTP { + static verbose = config.verbose; + + static async request(method, url, headers = {}, data = {}, auth = false) { + let options = { + method: method, + headers: headers, + body: ["POST", "PUT", "PATCH", "DELETE"].includes(method) ? data : null + }; + + if (auth) { + options.headers.Authorization = 'Basic ' + Buffer.from(auth.username + ':' + auth.password).toString('base64'); + } + + if (config.verbose >= 1) { + console.log(`\n${method} ${url}\n`); + } + + let response = await fetch(url, options); + + // Fetch doesn't automatically parse the response body, so we have to do that manually + let responseData = await response.text(); + + if (HTTP.verbose >= 2) { + console.log(`\n\n${responseData}\n`); + } + + // Return the response status, headers, and data in a format similar to Axios + return { + status: response.status, + headers: response.headers.raw(), + data: responseData + }; + } + + static get(url, headers = {}, auth = false) { + return this.request('GET', url, headers, {}, auth); + } + + static post(url, data = {}, headers = {}, auth = false) { + return this.request('POST', url, headers, data, auth); + } + + static put(url, data = {}, headers = {}, auth = false) { + return this.request('PUT', url, headers, data, auth); + } + + static patch(url, data = {}, headers = {}, auth = false) { + return this.request('PATCH', url, headers, data, auth); + } + + static head(url, headers = {}, auth = false) { + return this.request('HEAD', url, headers, {}, auth); + } + + static options(url, headers = {}, auth = false) { + return this.request('OPTIONS', url, headers, {}, auth); + } + + static delete(url, data = {}, headers = {}, auth = false) { + return this.request('DELETE', url, headers, data, auth); + } +} + +module.exports = HTTP; diff --git a/tests/remote_js/package-lock.json b/tests/remote_js/package-lock.json new file mode 100644 index 00000000..7057205f --- /dev/null +++ b/tests/remote_js/package-lock.json @@ -0,0 +1,2378 @@ +{ + "name": "remote_js", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "axios": "^1.4.0", + "jsdom": "^22.0.0", + "node-fetch": "^2.6.7", + "wgxpath": "^1.2.0" + }, + "devDependencies": { + "@zotero/eslint-config": "^1.0.7", + "chai": "^4.3.7", + "mocha": "^10.2.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "peer": true, + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.5.1.tgz", + "integrity": "sha512-Z5ba73P98O1KUYCCJTUeVpja9RcGoMdncZ6T49FCUl2lN38JtCJ+3WgIDBv0AuY4WChU5PmtJmOCTlN6FZTFKQ==", + "dev": true, + "peer": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.0.3.tgz", + "integrity": "sha512-+5gy6OQfk+xx3q0d6jGZZC3f3KzAkXc/IanVxd1is/VIIziRqqt3ongQz0FiTUXqTk0c7aDB3OaFuKnuSoJicQ==", + "dev": true, + "peer": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.5.2", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "peer": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "peer": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "8.40.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.40.0.tgz", + "integrity": "sha512-ElyB54bJIhXQYVKjDSvCkPO1iU1tSAeVQJbllWJq1XQSmmA4dgFk8CbiBGpiOPxleE48vDogxCtmMYku4HSVLA==", + "dev": true, + "peer": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.11.8", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz", + "integrity": "sha512-UybHIJzJnR5Qc/MsD9Kr+RpO2h+/P1GhOwdiLPXK5TWk5sgTdu88bTD9UP+CKbPPh5Rni1u0GjAdYQLemG8g+g==", + "dev": true, + "peer": true, + "dependencies": { + "@humanwhocodes/object-schema": "^1.2.1", + "debug": "^4.1.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "peer": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "peer": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "peer": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", + "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "dev": true, + "peer": true + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "peer": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "peer": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "peer": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "engines": { + "node": ">= 10" + } + }, + "node_modules/@zotero/eslint-config": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@zotero/eslint-config/-/eslint-config-1.0.7.tgz", + "integrity": "sha512-g29IksYEUk8xJ4Se6dG5KXGGocYPawkNTSNh/ysDBM99YA1Xic+E9YhTDniVwhrfPrVDTC81UvI4cVnm9luZYg==", + "dev": true, + "peerDependencies": { + "eslint": ">= 3" + } + }, + "node_modules/abab": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", + "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==" + }, + "node_modules/acorn": { + "version": "8.8.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", + "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==", + "dev": true, + "peer": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peer": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", + "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/axios": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.4.0.tgz", + "integrity": "sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==", + "dependencies": { + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/chai": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.7.tgz", + "integrity": "sha512-HLnAzZ2iupm25PlN0xFreAlBA5zaBSv3og0DdeGA4Ar6h6rJ3A0rolRUKJhSF2V10GZKDgWF/VmAEsNWjCRB+A==", + "dev": true, + "dependencies": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.2", + "deep-eql": "^4.1.2", + "get-func-name": "^2.0.0", + "loupe": "^2.3.1", + "pathval": "^1.1.1", + "type-detect": "^4.0.5" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/check-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", + "integrity": "sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "peer": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cssstyle": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-3.0.0.tgz", + "integrity": "sha512-N4u2ABATi3Qplzf0hWbVCdjenim8F3ojEXpBDF5hBpjzW182MjNGLqfmQ0SkSPeQ+V86ZXgeH8aXj6kayd4jgg==", + "dependencies": { + "rrweb-cssom": "^0.6.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/data-urls": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-4.0.0.tgz", + "integrity": "sha512-/mMTei/JXPqvFqQtfyTowxmJVwr2PVAeCcDxyFf6LhoOu/09TX2OX3kb2wzi4DMXcfj4OItwDOnhl5oziPnT6g==", + "dependencies": { + "abab": "^2.0.6", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^12.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/debug/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/decamelize": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decimal.js": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz", + "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==" + }, + "node_modules/deep-eql": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz", + "integrity": "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==", + "dev": true, + "dependencies": { + "type-detect": "^4.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "peer": true + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/diff": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", + "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "peer": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/domexception": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz", + "integrity": "sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==", + "dependencies": { + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.40.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.40.0.tgz", + "integrity": "sha512-bvR+TsP9EHL3TqNtj9sCNJVAFK3fBN8Q7g5waghxyRsPLIMwL73XSKnZFK0hk/O2ANC+iAoq6PWMQ+IfBAJIiQ==", + "dev": true, + "peer": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.4.0", + "@eslint/eslintrc": "^2.0.3", + "@eslint/js": "8.40.0", + "@humanwhocodes/config-array": "^0.11.8", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "ajv": "^6.10.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.0", + "eslint-visitor-keys": "^3.4.1", + "espree": "^9.5.2", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "grapheme-splitter": "^1.0.4", + "ignore": "^5.2.0", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-sdsl": "^4.1.4", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.1", + "strip-ansi": "^6.0.1", + "strip-json-comments": "^3.1.0", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-scope": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.0.tgz", + "integrity": "sha512-DYj5deGlHBfMt15J7rdtyKNq/Nqlv5KfU4iodrQ019XESsRnwXH9KAE0y3cwtUHDo2ob7CypAnCqefh6vioWRw==", + "dev": true, + "peer": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.1.tgz", + "integrity": "sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA==", + "dev": true, + "peer": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "peer": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "peer": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "peer": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/espree": { + "version": "9.5.2", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.5.2.tgz", + "integrity": "sha512-7OASN1Wma5fum5SrNhFMAMJxOUAbhyfQ8dQ//PJaJbNw0URTPWqIghHWt1MmAANKhHZIYOHruW4Kw4ruUWOdGw==", + "dev": true, + "peer": true, + "dependencies": { + "acorn": "^8.8.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "dev": true, + "peer": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "peer": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "peer": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "peer": true + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "peer": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "peer": true + }, + "node_modules/fastq": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", + "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", + "dev": true, + "peer": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "peer": true, + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "bin": { + "flat": "cli.js" + } + }, + "node_modules/flat-cache": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", + "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", + "dev": true, + "peer": true, + "dependencies": { + "flatted": "^3.1.0", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz", + "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", + "dev": true, + "peer": true + }, + "node_modules/follow-redirects": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", + "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-func-name": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", + "integrity": "sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/globals": { + "version": "13.20.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz", + "integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==", + "dev": true, + "peer": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/grapheme-splitter": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz", + "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", + "dev": true, + "peer": true + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "bin": { + "he": "bin/he" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", + "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", + "dependencies": { + "whatwg-encoding": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", + "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", + "dev": true, + "peer": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "peer": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "peer": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==" + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "peer": true + }, + "node_modules/js-sdsl": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.4.0.tgz", + "integrity": "sha512-FfVSdx6pJ41Oa+CF7RDaFmTnCaFhua+SNYQX74riGOpl96x+2jQCqEfQ2bnXu/5DPCqlRuiqyvTJM0Qjz26IVg==", + "dev": true, + "peer": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsdom": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-22.0.0.tgz", + "integrity": "sha512-p5ZTEb5h+O+iU02t0GfEjAnkdYPrQSkfuTSMkMYyIoMvUNEHsbG0bHHbfXIcfTqD2UfvjQX7mmgiFsyRwGscVw==", + "dependencies": { + "abab": "^2.0.6", + "cssstyle": "^3.0.0", + "data-urls": "^4.0.0", + "decimal.js": "^10.4.3", + "domexception": "^4.0.0", + "form-data": "^4.0.0", + "html-encoding-sniffer": "^3.0.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.1", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.4", + "parse5": "^7.1.2", + "rrweb-cssom": "^0.6.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^4.1.2", + "w3c-xmlserializer": "^4.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^2.0.0", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^12.0.1", + "ws": "^8.13.0", + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "canvas": "^2.5.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "peer": true + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "peer": true + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "peer": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "peer": true + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/loupe": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.6.tgz", + "integrity": "sha512-RaPMZKiMy8/JruncMU5Bt6na1eftNoo++R4Y+N2FrxkDVTrGvcyzFTsaGif4QTeKESheMGegbhw6iUAq+5A8zA==", + "dev": true, + "dependencies": { + "get-func-name": "^2.0.0" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz", + "integrity": "sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mocha": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.2.0.tgz", + "integrity": "sha512-IDY7fl/BecMwFHzoqF2sg/SHHANeBoMMXFlS9r0OXKDssYE1M5O43wUY/9BVPeIvfH2zmEbBfseqN9gBQZzXkg==", + "dev": true, + "dependencies": { + "ansi-colors": "4.1.1", + "browser-stdout": "1.3.1", + "chokidar": "3.5.3", + "debug": "4.3.4", + "diff": "5.0.0", + "escape-string-regexp": "4.0.0", + "find-up": "5.0.0", + "glob": "7.2.0", + "he": "1.2.0", + "js-yaml": "4.1.0", + "log-symbols": "4.1.0", + "minimatch": "5.0.1", + "ms": "2.1.3", + "nanoid": "3.3.3", + "serialize-javascript": "6.0.0", + "strip-json-comments": "3.1.1", + "supports-color": "8.1.1", + "workerpool": "6.2.1", + "yargs": "16.2.0", + "yargs-parser": "20.2.4", + "yargs-unparser": "2.0.0" + }, + "bin": { + "_mocha": "bin/_mocha", + "mocha": "bin/mocha.js" + }, + "engines": { + "node": ">= 14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mochajs" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/nanoid": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.3.tgz", + "integrity": "sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==", + "dev": true, + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "peer": true + }, + "node_modules/node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-fetch/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "node_modules/node-fetch/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/node-fetch/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nwsapi": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.4.tgz", + "integrity": "sha512-NHj4rzRo0tQdijE9ZqAx6kYDcoRwYwSYzCA8MY3JzfxlrvEU0jhnhJT9BhqhJs7I/dKcrDm6TyulaRqZPIhN5g==" + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optionator": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", + "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", + "dev": true, + "peer": true, + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.3" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "peer": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse5": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", + "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", + "dependencies": { + "entities": "^4.4.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "peer": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, + "node_modules/psl": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", + "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==" + }, + "node_modules/punycode": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", + "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==" + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "peer": true + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "peer": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "peer": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "peer": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rrweb-cssom": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.6.0.tgz", + "integrity": "sha512-APM0Gt1KoXBz0iIkkdB/kfvGOwC4UuJFeG/c+yV7wSc7q96cG/kJ0HiYCnzivD9SB53cLV1MlHFNfOuPaadYSw==" + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "peer": true, + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", + "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", + "dev": true, + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "peer": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==" + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true, + "peer": true + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tough-cookie": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.2.tgz", + "integrity": "sha512-G9fqXWoYFZgTc2z8Q5zaHy/vJMjm+WV0AkAeHxVCQiEB1b+dGvWzFW6QV07cY5jQ5gRkeid2qIkzkxUnmoQZUQ==", + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tr46": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-4.1.1.tgz", + "integrity": "sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==", + "dependencies": { + "punycode": "^2.3.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "peer": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "peer": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "node_modules/w3c-xmlserializer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", + "integrity": "sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==", + "dependencies": { + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "engines": { + "node": ">=12" + } + }, + "node_modules/wgxpath": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/wgxpath/-/wgxpath-1.2.0.tgz", + "integrity": "sha512-C1Whl8ylgNBOBA4Tg0APpjDxEDXzDtiPvEKC4l0HjHTFwYn+/WhisBGWwQXg4bwiNZ7N5xNXlXpm13NcMm/ykA==" + }, + "node_modules/whatwg-encoding": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", + "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-mimetype": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", + "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-url": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-12.0.1.tgz", + "integrity": "sha512-Ed/LrqB8EPlGxjS+TrsXcpUond1mhccS3pchLhzSgPCnTimUCKj3IZE75pAs5m6heB2U2TMerKFUXheyHY+VDQ==", + "dependencies": { + "tr46": "^4.1.1", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "peer": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "dev": true, + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/workerpool": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz", + "integrity": "sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==", + "dev": true + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/ws": { + "version": "8.13.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", + "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-name-validator": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", + "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", + "engines": { + "node": ">=12" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==" + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-parser": { + "version": "20.2.4", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", + "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-unparser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", + "dev": true, + "dependencies": { + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/tests/remote_js/package.json b/tests/remote_js/package.json new file mode 100644 index 00000000..08992196 --- /dev/null +++ b/tests/remote_js/package.json @@ -0,0 +1,16 @@ +{ + "dependencies": { + "axios": "^1.4.0", + "jsdom": "^22.0.0", + "node-fetch": "^2.6.7", + "wgxpath": "^1.2.0" + }, + "devDependencies": { + "@zotero/eslint-config": "^1.0.7", + "chai": "^4.3.7", + "mocha": "^10.2.0" + }, + "scripts": { + "test": "mocha \"test/**/*.js\"" + } +} diff --git a/tests/remote_js/test/1/collectionTest.js b/tests/remote_js/test/1/collectionTest.js new file mode 100644 index 00000000..a7dd6cfe --- /dev/null +++ b/tests/remote_js/test/1/collectionTest.js @@ -0,0 +1,119 @@ +const chai = require('chai'); +const assert = chai.assert; +const config = require("../../config.js"); +const API = require('../../api2.js'); +const Helpers = require('../../helpers.js'); +const { API1Setup, API1WrapUp } = require("../shared.js"); + +describe('CollectionTests', function () { + this.timeout(config.timeout); + + before(async function () { + await API1Setup(); + }); + + after(async function () { + await API1WrapUp(); + }); + + const testNewSingleCollection = async () => { + const collectionName = "Test Collection"; + const json = { name: "Test Collection", parent: false }; + + const response = await API.userPost( + config.userID, + `collections?key=${config.apiKey}`, + JSON.stringify(json), + { "Content-Type": "application/json" } + ); + + const xml = await API.getXMLFromResponse(response); + Helpers.assertStatusCode(response, 200); + const totalResults = Helpers.xpathEval(xml, '//feed/zapi:totalResults'); + const numCollections = Helpers.xpathEval(xml, '//feed//entry/zapi:numCollections'); + assert.equal(parseInt(totalResults), 1); + assert.equal(parseInt(numCollections), 0); + const data = await API.parseDataFromAtomEntry(xml); + const jsonResponse = JSON.parse(data.content); + assert.equal(jsonResponse.name, collectionName); + return jsonResponse; + }; + + it('testNewSingleSubcollection', async function () { + let parent = await testNewSingleCollection(); + parent = parent.collectionKey; + const name = "Test Subcollection"; + const json = { name: name, parent: parent }; + + let response = await API.userPost( + config.userID, + `collections?key=${config.apiKey}`, + JSON.stringify(json), + { "Content-Type": "application/json" } + ); + Helpers.assertStatusCode(response, 200); + let xml = API.getXMLFromResponse(response); + assert.equal(parseInt(Helpers.xpathEval(xml, '//feed/zapi:totalResults')), 1); + + const dataSub = API.parseDataFromAtomEntry(xml); + + const jsonResponse = JSON.parse(dataSub.content); + assert.equal(jsonResponse.name, name); + assert.equal(jsonResponse.parent, parent); + response = await API.userGet( + config.userID, + `collections/${parent}?key=${config.apiKey}` + ); + Helpers.assertStatusCode(response, 200); + xml = await API.getXMLFromResponse(response); + assert.equal(parseInt(Helpers.xpathEval(xml, '/atom:entry/zapi:numCollections')), 1); + }); + + it('testNewSingleCollectionWithoutParentProperty', async function () { + const name = "Test Collection"; + const json = { name: name }; + + const response = await API.userPost( + config.userID, + `collections?key=${config.apiKey}`, + JSON.stringify(json), + { "Content-Type": "application/json" } + ); + Helpers.assertStatusCode(response, 200); + const xml = await API.getXMLFromResponse(response); + assert.equal(parseInt(Helpers.xpathEval(xml, '//feed/zapi:totalResults')), 1); + const data = await API.parseDataFromAtomEntry(xml); + const jsonResponse = JSON.parse(data.content); + assert.equal(jsonResponse.name, name); + }); + + it('testEditSingleCollection', async function () { + API.useAPIVersion(2); + const xml = await API.createCollection("Test", false); + const data = await API.parseDataFromAtomEntry(xml); + const key = data.key; + API.useAPIVersion(1); + + const xmlCollection = await API.getCollectionXML(data.key); + const contentElement = Helpers.xpathEval(xmlCollection, '//atom:entry/atom:content', true); + const etag = contentElement.getAttribute("etag"); + assert.isString(etag); + const newName = "Test 2"; + const json = { name: newName, parent: false }; + + const response = await API.userPut( + config.userID, + `collections/${key}?key=${config.apiKey}`, + JSON.stringify(json), + { + "Content-Type": "application/json", + "If-Match": etag + } + ); + Helpers.assertStatusCode(response, 200); + const xmlResponse = await API.getXMLFromResponse(response); + const dataResponse = await API.parseDataFromAtomEntry(xmlResponse); + const jsonResponse = JSON.parse(dataResponse.content); + assert.equal(jsonResponse.name, newName); + }); +}); diff --git a/tests/remote_js/test/1/itemsTest.js b/tests/remote_js/test/1/itemsTest.js new file mode 100644 index 00000000..56dee3c1 --- /dev/null +++ b/tests/remote_js/test/1/itemsTest.js @@ -0,0 +1,36 @@ +const { assert } = require('chai'); +const API = require('../../api2.js'); +const config = require("../../config.js"); +const Helpers = require('../../helpers.js'); +const { API1Setup, API1WrapUp } = require("../shared.js"); + +describe('ItemTests', function () { + this.timeout(config.timeout); // setting timeout if operations are async and take some time + + before(async function () { + await API1Setup(); + await API.setKeyOption(config.userID, config.apiKey, 'libraryNotes', 1); + }); + + after(async function () { + await API1WrapUp(); + }); + + it('testCreateItemWithChildren', async function () { + let json = await API.getItemTemplate("newspaperArticle"); + let noteJSON = await API.getItemTemplate("note"); + noteJSON.note = "

Here's a test note

"; + json.notes = [noteJSON]; + let response = await API.userPost( + config.userID, + "items?key=" + config.apiKey, + JSON.stringify({ items: [json] }), + { "Content-Type": "application/json" } + ); + Helpers.assertStatusCode(response, 201); + let xml = API.getXMLFromResponse(response); + Helpers.assertNumResults(response, 1); + const numChildren = Helpers.xpathEval(xml, '//atom:entry/zapi:numChildren'); + assert.equal(parseInt(numChildren), 1); + }); +}); diff --git a/tests/remote_js/test/2/atomTest.js b/tests/remote_js/test/2/atomTest.js new file mode 100644 index 00000000..5bebcc2e --- /dev/null +++ b/tests/remote_js/test/2/atomTest.js @@ -0,0 +1,124 @@ +const chai = require('chai'); +const assert = chai.assert; +const config = require("../../config.js"); +const API = require('../../api2.js'); +const Helpers = require('../../helpers.js'); +const { API2Setup, API2WrapUp } = require("../shared.js"); + +describe('CollectionTests', function () { + this.timeout(config.timeout); + + before(async function () { + await API2Setup(); + }); + + after(async function () { + await API2WrapUp(); + }); + + it('testFeedURIs', async function () { + const userID = config.userID; + + const response = await API.userGet( + userID, + "items?key=" + config.apiKey + ); + Helpers.assertStatusCode(response, 200); + const xml = await API.getXMLFromResponse(response); + const links = Helpers.xpathEval(xml, '/atom:feed/atom:link', true, true); + assert.equal(config.apiURLPrefix + "users/" + userID + "/items", links[0].getAttribute('href')); + + // 'order'/'sort' should stay as-is, not turn into 'sort'/'direction' + const response2 = await API.userGet( + userID, + "items?key=" + config.apiKey + "&order=dateModified&sort=asc" + ); + Helpers.assertStatusCode(response2, 200); + const xml2 = await API.getXMLFromResponse(response2); + const links2 = Helpers.xpathEval(xml2, '/atom:feed/atom:link', true, true); + assert.equal(config.apiURLPrefix + "users/" + userID + "/items?order=dateModified&sort=asc", links2[0].getAttribute('href')); + }); + + + //Requires citation server to run + it('testMultiContent', async function () { + this.skip(); + const keyObj = {}; + const item1 = { + title: 'Title', + creators: [ + { + creatorType: 'author', + firstName: 'First', + lastName: 'Last', + }, + ], + }; + + const key1 = await API.createItem('book', item1, null, 'key'); + const itemXml1 + = '' + + '' + + '
' + + '
Last, First. Title, n.d.
' + + '
' + + '{"itemKey":"","itemVersion":0,"itemType":"book","title":"Title","creators":[{"creatorType":"author","firstName":"First","lastName":"Last"}],"abstractNote":"","series":"","seriesNumber":"","volume":"","numberOfVolumes":"","edition":"","place":"","publisher":"","date":"","numPages":"","language":"","ISBN":"","shortTitle":"","url":"","accessDate":"","archive":"","archiveLocation":"","libraryCatalog":"","callNumber":"","rights":"","extra":"","tags":[],"collections":[],"relations":{}}' + + '
'; + keyObj[key1] = itemXml1; + + const item2 = { + title: 'Title 2', + creators: [ + { + creatorType: 'author', + firstName: 'First', + lastName: 'Last', + }, + { + creatorType: 'editor', + firstName: 'Ed', + lastName: 'McEditor', + }, + ], + }; + + const key2 = await API.createItem('book', item2, null, 'key'); + const itemXml2 + = '' + + '' + + '
' + + '
Last, First. Title 2. Edited by Ed McEditor, n.d.
' + + '
' + + '{"itemKey":"","itemVersion":0,"itemType":"book","title":"Title 2","creators":[{"creatorType":"author","firstName":"First","lastName":"Last"},{"creatorType":"editor","firstName":"Ed","lastName":"McEditor"}],"abstractNote":"","series":"","seriesNumber":"","volume":"","numberOfVolumes":"","edition":"","place":"","publisher":"","date":"","numPages":"","language":"","ISBN":"","shortTitle":"","url":"","accessDate":"","archive":"","archiveLocation":"","libraryCatalog":"","callNumber":"","rights":"","extra":"","tags":[],"collections":[],"relations":{}}' + + '
'; + keyObj[key2] = itemXml2; + + const keys = Object.keys(keyObj); + const keyStr = keys.join(','); + + const response = await API.userGet( + config.userID, + `items?key=${config.apiKey}&itemKey=${keyStr}&content=bib,json`, + ); + Helpers.assertStatusCode(response, 200); + const xml = await API.getXMLFromResponse(response); + assert.equal(Helpers.xpathEval(xml, '/atom:feed/zapi:totalResults'), keys.length); + + const entries = Helpers.xpathEval(xml, '//atom:entry', true, true); + for (const entry of entries) { + const key = entry.children('http://zotero.org/ns/api',).key.textContent; + let content = entry.content.asXML(); + + content = content.replace( + ' jsonResponse.success[key])); + assert.equal(parseInt(Helpers.xpathEval(xmlResponse, '/atom:feed/zapi:totalResults')), 2); + + const contents = Helpers.xpathEval(xmlResponse, '/atom:feed/atom:entry/atom:content', false, true); + let content = JSON.parse(contents.shift()); + assert.equal(name1, content.name); + assert.notOk(content.parentCollection); + content = JSON.parse(contents.shift()); + assert.equal(name2, content.name); + assert.equal(parent2, content.parentCollection); + }); + + it('testEditMultipleCollections', async function () { + let xml = await API.createCollection("Test 1", false, true, 'atom'); + let data = await API.parseDataFromAtomEntry(xml); + let key1 = data.key; + xml = await API.createCollection("Test 2", false, true, 'atom'); + data = await API.parseDataFromAtomEntry(xml); + let key2 = data.key; + + let newName1 = "Test 1 Modified"; + let newName2 = "Test 2 Modified"; + let response = await API.userPost( + config.userID, + "collections?key=" + config.apiKey, + JSON.stringify({ + collections: [ + { + collectionKey: key1, + name: newName1 + }, + { + collectionKey: key2, + name: newName2 + } + ] + }), + { + "Content-Type": "application/json", + "If-Unmodified-Since-Version": data.version + } + ); + Helpers.assertStatusCode(response, 200); + let json = await API.getJSONFromResponse(response); + + assert.lengthOf(Object.keys(json.success), 2); + xml = await API.getCollectionXML(Object.keys(json.success).map(key => json.success[key])); + assert.equal(parseInt(Helpers.xpathEval(xml, '/atom:feed/zapi:totalResults')), 2); + + let contents = Helpers.xpathEval(xml, '/atom:feed/atom:entry/atom:content', false, true); + let content = JSON.parse(contents[0]); + assert.equal(content.name, newName1); + assert.notOk(content.parentCollection); + content = JSON.parse(contents[1]); + assert.equal(content.name, newName2); + assert.notOk(content.parentCollection); + }); + + it('testCollectionItemChange', async function () { + const collectionKey1 = await API.createCollection('Test', false, true, 'key'); + const collectionKey2 = await API.createCollection('Test', false, true, 'key'); + + let xml = await API.createItem('book', { + collections: [collectionKey1], + }, true, 'atom'); + let data = API.parseDataFromAtomEntry(xml); + const itemKey1 = data.key; + const itemVersion1 = data.version; + let json = JSON.parse(data.content); + assert.equal(json.collections[0], collectionKey1); + + xml = await API.createItem('journalArticle', { + collections: [collectionKey2], + }, true, 'atom'); + data = API.parseDataFromAtomEntry(xml); + const itemKey2 = data.key; + const itemVersion2 = data.version; + json = JSON.parse(data.content); + assert.equal(json.collections[0], collectionKey2); + + xml = await API.getCollectionXML(collectionKey1); + + assert.equal(parseInt(Helpers.xpathEval(xml, '//atom:entry/zapi:numItems')), 1); + + xml = await API.getCollectionXML(collectionKey2); + let collectionData2 = API.parseDataFromAtomEntry(xml); + assert.equal(parseInt(Helpers.xpathEval(xml, '//atom:entry/zapi:numItems')), 1); + + var libraryVersion = await API.getLibraryVersion(); + + // Add items to collection + var response = await API.userPatch( + config.userID, + `items/${itemKey1}?key=${config.apiKey}`, + JSON.stringify({ + collections: [collectionKey1, collectionKey2], + }), + { + "Content-Type": "application/json", + "If-Unmodified-Since-Version": itemVersion1, + } + ); + Helpers.assertStatusCode(response, 204); + + // Item version should change + xml = await API.getItemXML(itemKey1); + data = API.parseDataFromAtomEntry(xml); + assert.equal(parseInt(data.version), parseInt(libraryVersion) + 1); + + // Collection timestamp shouldn't change, but numItems should + xml = await API.getCollectionXML(collectionKey2); + data = API.parseDataFromAtomEntry(xml); + assert.equal(parseInt(Helpers.xpathEval(xml, '//atom:entry/zapi:numItems')), 2); + assert.equal(data.version, collectionData2.version); + collectionData2 = data; + + libraryVersion = await API.getLibraryVersion(); + + // Remove collections + response = await API.userPatch( + config.userID, + `items/${itemKey2}?key=${config.apiKey}`, + JSON.stringify({ collections: [] }), + { + "Content-Type": "application/json", + "If-Unmodified-Since-Version": itemVersion2, + } + ); + Helpers.assertStatusCode(response, 204); + + // Item version should change + xml = await API.getItemXML(itemKey2); + data = API.parseDataFromAtomEntry(xml); + assert.equal(parseInt(data.version), parseInt(libraryVersion) + 1); + + // Collection timestamp shouldn't change, but numItems should + xml = await API.getCollectionXML(collectionKey2); + data = API.parseDataFromAtomEntry(xml); + assert.equal(parseInt(Helpers.xpathEval(xml, '//atom:entry/zapi:numItems')), 1); + assert.equal(data.version, collectionData2.version); + + // Check collections arrays and numItems + xml = await API.getItemXML(itemKey1); + data = API.parseDataFromAtomEntry(xml); + json = JSON.parse(data.content); + assert.lengthOf(json.collections, 2); + assert.include(json.collections, collectionKey1); + assert.include(json.collections, collectionKey2); + + xml = await API.getItemXML(itemKey2); + data = API.parseDataFromAtomEntry(xml); + json = JSON.parse(data.content); + assert.lengthOf(json.collections, 0); + + xml = await API.getCollectionXML(collectionKey1); + assert.equal(parseInt(Helpers.xpathEval(xml, '//atom:entry/zapi:numItems')), 1); + + xml = await API.getCollectionXML(collectionKey2); + assert.equal(parseInt(Helpers.xpathEval(xml, '//atom:entry/zapi:numItems')), 1); + }); + + it('testCollectionChildItemError', async function () { + const collectionKey = await API.createCollection('Test', false, this, 'key'); + + const key = await API.createItem('book', {}, true, 'key'); + const xml = await API.createNoteItem('

Test Note

', key, true, 'atom'); + const data = API.parseDataFromAtomEntry(xml); + const json = JSON.parse(data.content); + json.collections = [collectionKey]; + json.relations = {}; + + const response = await API.userPut( + config.userID, + `items/${data.key}?key=${config.apiKey}`, + JSON.stringify(json), + { 'Content-Type': 'application/json' } + ); + Helpers.assertStatusCode(response, 400, 'Child items cannot be assigned to collections'); + }); + + it('testCollectionItems', async function () { + const collectionKey = await API.createCollection('Test', false, true, 'key'); + + let xml = await API.createItem("book", { collections: [collectionKey] }, this); + let data = await API.parseDataFromAtomEntry(xml); + let itemKey1 = data.key; + let json = JSON.parse(data.content); + assert.deepEqual([collectionKey], json.collections); + + xml = await API.createItem("journalArticle", { collections: [collectionKey] }, true); + data = await API.parseDataFromAtomEntry(xml); + let itemKey2 = data.key; + json = JSON.parse(data.content); + assert.deepEqual([collectionKey], json.collections); + + let childItemKey1 = await API.createAttachmentItem("linked_url", [], itemKey1, true, 'key'); + let childItemKey2 = await API.createAttachmentItem("linked_url", [], itemKey2, true, 'key'); + + const response1 = await API.userGet( + config.userID, + `collections/${collectionKey}/items?key=${config.apiKey}&format=keys` + ); + Helpers.assertStatusCode(response1, 200); + let keys = response1.data.split("\n").map(key => key.trim()).filter(key => key.length != 0); + assert.lengthOf(keys, 4); + assert.include(keys, itemKey1); + assert.include(keys, itemKey2); + assert.include(keys, childItemKey1); + assert.include(keys, childItemKey2); + + const response2 = await API.userGet( + config.userID, + `collections/${collectionKey}/items/top?key=${config.apiKey}&format=keys` + ); + Helpers.assertStatusCode(response2, 200); + keys = response2.data.split("\n").map(key => key.trim()).filter(key => key.length != 0); + assert.lengthOf(keys, 2); + assert.include(keys, itemKey1); + assert.include(keys, itemKey2); + }); +}); diff --git a/tests/remote_js/test/2/creatorTest.js b/tests/remote_js/test/2/creatorTest.js new file mode 100644 index 00000000..3c57f758 --- /dev/null +++ b/tests/remote_js/test/2/creatorTest.js @@ -0,0 +1,66 @@ +const chai = require('chai'); +const assert = chai.assert; +const config = require("../../config.js"); +const API = require('../../api2.js'); +const Helpers = require('../../helpers.js'); +const { API2Setup, API2WrapUp } = require("../shared.js"); + +describe('CreatorTests', function () { + this.timeout(config.timeout); + + before(async function () { + await API2Setup(); + }); + + after(async function () { + await API2WrapUp(); + }); + + it('testCreatorSummary', async function () { + const xml = await API.createItem('book', + { + creators: [ + { + creatorType: 'author', + name: 'Test' + } + ] + }, true); + + const data = API.parseDataFromAtomEntry(xml); + const itemKey = data.key; + const json = JSON.parse(data.content); + + const creatorSummary = Helpers.xpathEval(xml, '//atom:entry/zapi:creatorSummary'); + assert.equal('Test', creatorSummary); + + json.creators.push({ + creatorType: 'author', + firstName: 'Alice', + lastName: 'Foo' + }); + + const response = await API.userPut(config.userID, `items/${itemKey}?key=${config.apiKey}`, JSON.stringify(json)); + Helpers.assertStatusCode(response, 204); + + const updatedXml = await API.getItemXML(itemKey); + const updatedCreatorSummary = Helpers.xpathEval(updatedXml, '//atom:entry/zapi:creatorSummary'); + assert.equal('Test and Foo', updatedCreatorSummary); + + const updatedData = API.parseDataFromAtomEntry(updatedXml); + const updatedJson = JSON.parse(updatedData.content); + + updatedJson.creators.push({ + creatorType: 'author', + firstName: 'Bob', + lastName: 'Bar' + }); + + const response2 = await API.userPut(config.userID, `items/${itemKey}?key=${config.apiKey}`, JSON.stringify(updatedJson)); + Helpers.assertStatusCode(response2, 204); + + const finalXml = await API.getItemXML(itemKey); + const finalCreatorSummary = Helpers.xpathEval(finalXml, '//atom:entry/zapi:creatorSummary'); + assert.equal('Test et al.', finalCreatorSummary); + }); +}); diff --git a/tests/remote_js/test/2/fileTest.js b/tests/remote_js/test/2/fileTest.js new file mode 100644 index 00000000..7542bf9d --- /dev/null +++ b/tests/remote_js/test/2/fileTest.js @@ -0,0 +1,19 @@ +//const chai = require('chai'); +// const assert = chai.assert; +const config = require("../../config.js"); +// const API = require('../../api2.js'); +// const Helpers = require('../../helpers.js'); +const { API2Setup, API2WrapUp } = require("../shared.js"); + +//Skipped - uses S3 +describe('FileTestTests', function () { + this.timeout(config.timeout); + + before(async function () { + await API2Setup(); + }); + + after(async function () { + await API2WrapUp(); + }); +}); diff --git a/tests/remote_js/test/2/fullText.js b/tests/remote_js/test/2/fullText.js new file mode 100644 index 00000000..2e07deb3 --- /dev/null +++ b/tests/remote_js/test/2/fullText.js @@ -0,0 +1,238 @@ +const chai = require('chai'); +const assert = chai.assert; +const config = require("../../config.js"); +const API = require('../../api2.js'); +const Helpers = require('../../helpers.js'); +const { API2Setup, API2WrapUp } = require("../shared.js"); + +describe('FullTextTests', function () { + this.timeout(config.timeout); + + before(async function () { + await API2Setup(); + }); + + after(async function () { + await API2WrapUp(); + }); + + it('testSetItemContent', async function () { + const key = await API.createItem("book", false, this, 'key'); + const xml = await API.createAttachmentItem("imported_url", [], key, this, 'atom'); + const data = API.parseDataFromAtomEntry(xml); + + let response = await API.userGet( + config.userID, + "items/" + data.key + "/fulltext?key=" + config.apiKey + ); + Helpers.assertStatusCode(response, 404); + assert.isUndefined(response.headers["last-modified-version"]); + + const libraryVersion = await API.getLibraryVersion(); + + const content = "Here is some full-text content"; + const pages = 50; + + // No Content-Type + response = await API.userPut( + config.userID, + "items/" + data.key + "/fulltext?key=" + config.apiKey, + content + ); + Helpers.assertStatusCode(response, 400, "Content-Type must be application/json"); + + // Store content + response = await API.userPut( + config.userID, + "items/" + data.key + "/fulltext?key=" + config.apiKey, + JSON.stringify({ + content: content, + indexedPages: pages, + totalPages: pages, + invalidParam: "shouldBeIgnored" + }), + { "Content-Type": "application/json" } + ); + + Helpers.assertStatusCode(response, 204); + const contentVersion = response.headers["last-modified-version"][0]; + assert.isAbove(parseInt(contentVersion), parseInt(libraryVersion)); + + // Retrieve it + response = await API.userGet( + config.userID, + "items/" + data.key + "/fulltext?key=" + config.apiKey + ); + Helpers.assertStatusCode(response, 200); + assert.equal(response.headers['content-type'][0], "application/json"); + const json = JSON.parse(response.data); + assert.equal(content, json.content); + assert.include(Object.keys(json), 'indexedPages'); + assert.include(Object.keys(json), 'totalPages'); + assert.equal(pages, json.indexedPages); + assert.equal(pages, json.totalPages); + assert.notInclude(Object.keys(json), "indexedChars"); + assert.notInclude(Object.keys(json), "invalidParam"); + assert.equal(contentVersion, response.headers['last-modified-version'][0]); + }); + + it('testModifyAttachmentWithFulltext', async function () { + const key = await API.createItem("book", false, true, 'key'); + const xml = await API.createAttachmentItem("imported_url", [], key, true, 'atom'); + const data = API.parseDataFromAtomEntry(xml); + const content = "Here is some full-text content"; + const pages = 50; + + // Store content + const response = await API.userPut( + config.userID, + "items/" + data.key + "/fulltext?key=" + config.apiKey, + JSON.stringify({ + content: content, + indexedPages: pages, + totalPages: pages + }), + { "Content-Type": "application/json" } + ); + Helpers.assertStatusCode(response, 204); + + const json = JSON.parse(data.content); + json.title = "This is a new attachment title"; + json.contentType = 'text/plain'; + + // Modify attachment item + const response2 = await API.userPut( + config.userID, + "items/" + data.key + "?key=" + config.apiKey, + JSON.stringify(json), + { "If-Unmodified-Since-Version": data.version } + ); + Helpers.assertStatusCode(response2, 204); + }); + + it('testNewerContent', async function () { + await API.userClear(config.userID); + // Store content for one item + let key = await API.createItem("book", false, true, 'key'); + let xml = await API.createAttachmentItem("imported_url", [], key, true, 'atom'); + let data = await API.parseDataFromAtomEntry(xml); + let key1 = data.key; + + let content = "Here is some full-text content"; + + let response = await API.userPut( + config.userID, + `items/${key1}/fulltext?key=${config.apiKey}`, + JSON.stringify({ + content: content + }), + { "Content-Type": "application/json" } + ); + Helpers.assertStatusCode(response, 204); + let contentVersion1 = response.headers["last-modified-version"][0]; + assert.isAbove(parseInt(contentVersion1), 0); + + // And another + key = await API.createItem("book", false, true, 'key'); + xml = await API.createAttachmentItem("imported_url", [], key, true, 'atom'); + data = await API.parseDataFromAtomEntry(xml); + let key2 = data.key; + + response = await API.userPut( + config.userID, + `items/${key2}/fulltext?key=${config.apiKey}`, + JSON.stringify({ + content: content + }), + { "Content-Type": "application/json" } + ); + Helpers.assertStatusCode(response, 204); + let contentVersion2 = response.headers["last-modified-version"][0]; + assert.isAbove(parseInt(contentVersion2), 0); + + // Get newer one + response = await API.userGet( + config.userID, + `fulltext?key=${config.apiKey}&newer=${contentVersion1}` + ); + Helpers.assertStatusCode(response, 200); + assert.equal("application/json", response.headers["content-type"][0]); + assert.equal(contentVersion2, response.headers["last-modified-version"][0]); + let json = API.getJSONFromResponse(response); + assert.lengthOf(Object.keys(json), 1); + assert.property(json, key2); + assert.equal(contentVersion2, json[key2]); + + // Get both with newer=0 + response = await API.userGet( + config.userID, + `fulltext?key=${config.apiKey}&newer=0` + ); + Helpers.assertStatusCode(response, 200); + assert.equal("application/json", response.headers["content-type"][0]); + json = API.getJSONFromResponse(response); + assert.lengthOf(Object.keys(json), 2); + assert.property(json, key1); + assert.equal(contentVersion1, json[key1]); + assert.property(json, key2); + assert.equal(contentVersion2, json[key2]); + }); + + //Requires s3 setup + it('testSearchItemContent', async function() { + this.skip(); + }); + + it('testDeleteItemContent', async function () { + const key = await API.createItem('book', false, true, 'key'); + const xml = await API.createAttachmentItem('imported_file', [], key, true, 'atom'); + const data = API.parseDataFromAtomEntry(xml); + + const content = 'Ыюм мютат дэбетиз конвынёры эю, ку мэль жкрипта трактатоз.\nПро ут чтэт эрепюят граэкйж, дуо нэ выро рыкючабо пырикюлёз.'; + + // Store content + let response = await API.userPut( + config.userID, + `items/${data.key}/fulltext?key=${config.apiKey}`, + JSON.stringify({ + content: content, + indexedPages: 50 + }), + { "Content-Type": "application/json" } + ); + Helpers.assertStatusCode(response, 204); + const contentVersion = response.headers['last-modified-version'][0]; + + // Retrieve it + response = await API.userGet( + config.userID, + `items/${data.key}/fulltext?key=${config.apiKey}` + ); + Helpers.assertStatusCode(response, 200); + let json = JSON.parse(response.data); + assert.equal(json.content, content); + assert.equal(json.indexedPages, 50); + + // Set to empty string + response = await API.userPut( + config.userID, + `items/${data.key}/fulltext?key=${config.apiKey}`, + JSON.stringify({ + content: "" + }), + { "Content-Type": "application/json" } + ); + Helpers.assertStatusCode(response, 204); + assert.isAbove(parseInt(response.headers['last-modified-version'][0]), parseInt(contentVersion)); + + // Make sure it's gone + response = await API.userGet( + config.userID, + `items/${data.key}/fulltext?key=${config.apiKey}` + ); + Helpers.assertStatusCode(response, 200); + json = JSON.parse(response.data); + assert.equal(json.content, ""); + assert.notProperty(json, "indexedPages"); + }); +}); diff --git a/tests/remote_js/test/2/generalTest.js b/tests/remote_js/test/2/generalTest.js new file mode 100644 index 00000000..7fbbca4b --- /dev/null +++ b/tests/remote_js/test/2/generalTest.js @@ -0,0 +1,66 @@ +const chai = require('chai'); +const assert = chai.assert; +const config = require("../../config.js"); +const API = require('../../api2.js'); +const Helpers = require('../../helpers.js'); +const { API2Setup, API2WrapUp } = require("../shared.js"); + + +describe('GeneralTests', function () { + this.timeout(config.timeout); + + before(async function () { + await API2Setup(); + }); + + after(async function () { + await API2WrapUp(); + }); + + it('testInvalidCharacters', async function () { + const data = { + title: "A" + String.fromCharCode(0) + "A", + creators: [ + { + creatorType: "author", + name: "B" + String.fromCharCode(1) + "B" + } + ], + tags: [ + { + tag: "C" + String.fromCharCode(2) + "C" + } + ] + }; + const xml = await API.createItem("book", data, this, 'atom'); + const parsedData = await API.parseDataFromAtomEntry(xml); + const json = JSON.parse(parsedData.content); + assert.equal("AA", json.title); + assert.equal("BB", json.creators[0].name); + assert.equal("CC", json.tags[0].tag); + }); + + it('testZoteroWriteToken', async function () { + const json = await API.getItemTemplate('book'); + const token = Helpers.uniqueToken(); + + let response = await API.userPost( + config.userID, + `items?key=${config.apiKey}`, + JSON.stringify({ items: [json] }), + { 'Content-Type': 'application/json', 'Zotero-Write-Token': token } + ); + + Helpers.assertStatusCode(response, 200); + Helpers.assertStatusForObject(response, 'success', 0); + + response = await API.userPost( + config.userID, + `items?key=${config.apiKey}`, + JSON.stringify({ items: [json] }), + { 'Content-Type': 'application/json', 'Zotero-Write-Token': token } + ); + + Helpers.assertStatusCode(response, 412); + }); +}); diff --git a/tests/remote_js/test/2/groupTest.js b/tests/remote_js/test/2/groupTest.js new file mode 100644 index 00000000..31f288c6 --- /dev/null +++ b/tests/remote_js/test/2/groupTest.js @@ -0,0 +1,123 @@ +const chai = require('chai'); +const assert = chai.assert; +const config = require("../../config.js"); +const API = require('../../api2.js'); +const Helpers = require('../../helpers.js'); +const { API2Setup, API2WrapUp } = require("../shared.js"); +const { JSDOM } = require("jsdom"); +const { resetGroups } = require("../../groupsSetup.js"); + +describe('GroupTests', function () { + this.timeout(config.timeout); + + before(async function () { + await API2Setup(); + await resetGroups(); + }); + + after(async function () { + await API2WrapUp(); + }); + + it('testUpdateMetadata', async function () { + const response = await API.userGet( + config.userID, + "groups?fq=GroupType:PublicOpen&content=json&key=" + config.apiKey + ); + Helpers.assertStatusCode(response, 200); + + // Get group API URI and ETag + const xml = API.getXMLFromResponse(response); + const groupID = Helpers.xpathEval(xml, "//atom:entry/zapi:groupID"); + let urlComponent = Helpers.xpathEval(xml, "//atom:entry/atom:link[@rel='self']", true); + let url = urlComponent.getAttribute("href"); + url = url.replace(config.apiURLPrefix, ''); + const etagComponent = Helpers.xpathEval(xml, "//atom:entry/atom:content", true); + const etag = etagComponent.getAttribute("etag"); + + // Make sure format=etags returns the same ETag + const response2 = await API.userGet( + config.userID, + "groups?format=etags&key=" + config.apiKey + ); + Helpers.assertStatusCode(response2, 200); + const json = JSON.parse(response2.data); + assert.equal(etag, json[groupID]); + + // Update group metadata + const jsonBody = JSON.parse(Helpers.xpathEval(xml, "//atom:entry/atom:content")); + const xmlDoc = new JSDOM(""); + const groupXML = xmlDoc.window.document.getElementsByTagName("group")[0]; + var name, description, urlField; + for (const [key, value] of Object.entries(jsonBody)) { + switch (key) { + case 'id': + case 'members': + continue; + + case 'name': { + name = "My Test Group " + Math.floor(Math.random() * 10001); + groupXML.setAttribute("name", name); + break; + } + + + case 'description': { + description = "This is a test description " + Math.floor(Math.random() * 10001); + const newNode = xmlDoc.window.document.createElement(key); + newNode.innerHTML = description; + groupXML.appendChild(newNode); + break; + } + + + case 'url': { + urlField = "http://example.com/" + Math.floor(Math.random() * 10001); + const newNode = xmlDoc.window.document.createElement(key); + newNode.innerHTML = urlField; + groupXML.appendChild(newNode); + break; + } + + + default: + groupXML.setAttributeNS(null, key, value); + } + } + + const response3 = await API.put( + url, + groupXML.outerHTML, + { "Content-Type": "text/xml" }, + { + username: config.rootUsername, + password: config.rootPassword + } + ); + Helpers.assertStatusCode(response3, 200); + const xml2 = API.getXMLFromResponse(response3); + const nameFromGroup = xml2.documentElement.getElementsByTagName("title")[0].innerHTML; + assert.equal(name, nameFromGroup); + + const response4 = await API.userGet( + config.userID, + `groups?format=etags&key=${config.apiKey}` + ); + Helpers.assertStatusCode(response4, 200); + const json2 = JSON.parse(response4.data); + const newETag = json2[groupID]; + assert.notEqual(etag, newETag); + + // Check ETag header on individual group request + const response5 = await API.groupGet( + groupID, + "?content=json&key=" + config.apiKey + ); + Helpers.assertStatusCode(response5, 200); + assert.equal(newETag, response5.headers.etag[0]); + const json3 = JSON.parse(API.getContentFromResponse(response5)); + assert.equal(name, json3.name); + assert.equal(description, json3.description); + assert.equal(urlField, json3.url); + }); +}); diff --git a/tests/remote_js/test/2/itemsTest.js b/tests/remote_js/test/2/itemsTest.js new file mode 100644 index 00000000..1ba7b094 --- /dev/null +++ b/tests/remote_js/test/2/itemsTest.js @@ -0,0 +1,1093 @@ +const chai = require('chai'); +const assert = chai.assert; +const config = require("../../config.js"); +const API = require('../../api2.js'); +const Helpers = require('../../helpers.js'); +const { API2Setup, API2WrapUp } = require("../shared.js"); + +describe('ItemsTests', function () { + this.timeout(config.timeout * 2); + + before(async function () { + await API2Setup(); + }); + + after(async function () { + await API2WrapUp(); + }); + + const testNewEmptyBookItem = async () => { + const xml = await API.createItem("book", false, true); + const data = API.parseDataFromAtomEntry(xml); + const json = JSON.parse(data.content); + assert.equal(json.itemType, "book"); + return json; + }; + + it('testNewEmptyBookItemMultiple', async function () { + const json = await API.getItemTemplate("book"); + + const data = []; + json.title = "A"; + data.push(json); + const json2 = Object.assign({}, json); + json2.title = "B"; + data.push(json2); + const json3 = Object.assign({}, json); + json3.title = "C"; + data.push(json3); + + const response = await API.postItems(data); + Helpers.assertStatusCode(response, 200); + const jsonResponse = await API.getJSONFromResponse(response); + const successArray = Object.keys(jsonResponse.success).map(key => jsonResponse.success[key]); + const xml = await API.getItemXML(successArray, true); + const contents = Helpers.xpathEval(xml, '/atom:feed/atom:entry/atom:content', false, true); + + let content = JSON.parse(contents[0]); + assert.equal(content.title, "A"); + content = JSON.parse(contents[1]); + assert.equal(content.title, "B"); + content = JSON.parse(contents[2]); + assert.equal(content.title, "C"); + }); + + it('testEditBookItem', async function () { + const newBookItem = await testNewEmptyBookItem(); + const key = newBookItem.itemKey; + const version = newBookItem.itemVersion; + + const newTitle = 'New Title'; + const numPages = 100; + const creatorType = 'author'; + const firstName = 'Firstname'; + const lastName = 'Lastname'; + + newBookItem.title = newTitle; + newBookItem.numPages = numPages; + newBookItem.creators.push({ + creatorType: creatorType, + firstName: firstName, + lastName: lastName + }); + + const response = await API.userPut( + config.userID, + `items/${key}?key=${config.apiKey}`, + JSON.stringify(newBookItem), + { + headers: { + 'Content-Type': 'application/json', + 'If-Unmodified-Since-Version': version + } + } + ); + Helpers.assertStatusCode(response, 204); + + const xml = await API.getItemXML(key); + const data = API.parseDataFromAtomEntry(xml); + const updatedJson = JSON.parse(data.content); + + assert.equal(newTitle, updatedJson.title); + assert.equal(numPages, updatedJson.numPages); + assert.equal(creatorType, updatedJson.creators[0].creatorType); + assert.equal(firstName, updatedJson.creators[0].firstName); + assert.equal(lastName, updatedJson.creators[0].lastName); + }); + + it('testDateModified', async function () { + const objectType = 'item'; + const objectTypePlural = API.getPluralObjectType(objectType); + // In case this is ever extended to other objects + let xml; + let itemData; + switch (objectType) { + case 'item': + itemData = { + title: "Test" + }; + xml = await API.createItem("videoRecording", itemData, this, 'atom'); + break; + } + + const data = API.parseDataFromAtomEntry(xml); + const objectKey = data.key; + let json = JSON.parse(data.content); + const dateModified1 = Helpers.xpathEval(xml, '//atom:entry/atom:updated'); + + // Make sure we're in the next second + await new Promise(resolve => setTimeout(resolve, 1000)); + + // + // If no explicit dateModified, use current timestamp + // + json.title = 'Test 2'; + let response = await API.userPut( + config.userID, + `${objectTypePlural}/${objectKey}?key=${config.apiKey}`, + JSON.stringify(json) + ); + Helpers.assertStatusCode(response, 204); + + switch (objectType) { + case 'item': + xml = await API.getItemXML(objectKey); + break; + } + + const dateModified2 = Helpers.xpathEval(xml, '//atom:entry/atom:updated'); + assert.notEqual(dateModified1, dateModified2); + json = JSON.parse(API.parseDataFromAtomEntry(xml).content); + + // Make sure we're in the next second + await new Promise(resolve => setTimeout(resolve, 1000)); + + // + // If existing dateModified, use current timestamp + // + json.title = 'Test 3'; + json.dateModified = dateModified2; + response = await API.userPut( + config.userID, + `${objectTypePlural}/${objectKey}? key=${config.apiKey}`, + JSON.stringify(json) + ); + Helpers.assertStatusCode(response, 204); + + switch (objectType) { + case 'item': + xml = await API.getItemXML(objectKey); + break; + } + + const dateModified3 = Helpers.xpathEval(xml, '//atom:entry/atom:updated'); + assert.notEqual(dateModified2, dateModified3); + json = JSON.parse(API.parseDataFromAtomEntry(xml).content); + + // + // If explicit dateModified, use that + // + const newDateModified = "2013-03-03T21:33:53Z"; + json.title = 'Test 4'; + json.dateModified = newDateModified; + response = await API.userPut( + config.userID, + `${objectTypePlural}/${objectKey}? key=${config.apiKey}`, + JSON.stringify(json) + ); + Helpers.assertStatusCode(response, 204); + + switch (objectType) { + case 'item': + xml = await API.getItemXML(objectKey); + break; + } + const dateModified4 = Helpers.xpathEval(xml, '//atom:entry/atom:updated'); + assert.equal(newDateModified, dateModified4); + }); + + it('testDateAccessedInvalid', async function () { + const date = 'February 1, 2014'; + const xml = await API.createItem("book", { accessDate: date }, true, 'atom'); + const data = API.parseDataFromAtomEntry(xml); + const json = JSON.parse(data.content); + // Invalid dates should be ignored + assert.equal(json.accessDate, ''); + }); + + it('testChangeItemType', async function () { + const json = await API.getItemTemplate("book"); + json.title = "Foo"; + json.numPages = 100; + + const response = await API.userPost( + config.userID, + "items?key=" + config.apiKey, + JSON.stringify({ + items: [json], + }), + { "Content-Type": "application/json" } + ); + + const key = API.getFirstSuccessKeyFromResponse(response); + const xml = await API.getItemXML(key, true); + const data = await API.parseDataFromAtomEntry(xml); + const version = data.version; + const json1 = JSON.parse(data.content); + + const json2 = await API.getItemTemplate("bookSection"); + delete json2.attachments; + delete json2.notes; + + Object.entries(json2).forEach(([field, _]) => { + if (field !== "itemType" && json1[field]) { + json2[field] = json1[field]; + } + }); + + const response2 = await API.userPut( + config.userID, + "items/" + key + "?key=" + config.apiKey, + JSON.stringify(json2), + { "Content-Type": "application/json", "If-Unmodified-Since-Version": version } + ); + + Helpers.assertStatusCode(response2, 204); + + const xml2 = await API.getItemXML(key); + const data2 = await API.parseDataFromAtomEntry(xml2); + const json3 = JSON.parse(data2.content); + + assert.equal(json3.itemType, "bookSection"); + assert.equal(json3.title, "Foo"); + assert.notProperty(json3, "numPages"); + }); + + it('testModifyItemPartial', async function () { + const itemData = { + title: "Test" + }; + const xml = await API.createItem("book", itemData, this, 'atom'); + const data = API.parseDataFromAtomEntry(xml); + const json = JSON.parse(data.content); + let itemVersion = json.itemVersion; + + const patch = async (itemKey, itemVersion, itemData, newData) => { + for (const field in newData) { + itemData[field] = newData[field]; + } + const response = await API.userPatch( + config.userID, + "items/" + itemKey + "?key=" + config.apiKey, + JSON.stringify(newData), + { + "Content-Type": "application/json", + "If-Unmodified-Since-Version": itemVersion + } + ); + Helpers.assertStatusCode(response, 204); + const xml = await API.getItemXML(itemKey); + const data = API.parseDataFromAtomEntry(xml); + const json = JSON.parse(data.content); + + for (const field in itemData) { + assert.deepEqual(itemData[field], json[field]); + } + const headerVersion = parseInt(response.headers["last-modified-version"][0]); + assert.isAbove(headerVersion, itemVersion); + assert.equal(json.itemVersion, headerVersion); + + return headerVersion; + }; + + let newData = { + date: "2013" + }; + itemVersion = await patch(data.key, itemVersion, itemData, newData); + + newData = { + title: "" + }; + itemVersion = await patch(data.key, itemVersion, itemData, newData); + + newData = { + tags: [ + { tag: "Foo" } + ] + }; + itemVersion = await patch(data.key, itemVersion, itemData, newData); + + newData = { + tags: [] + }; + itemVersion = await patch(data.key, itemVersion, itemData, newData); + + const key = await API.createCollection('Test', false, this, 'key'); + newData = { + collections: [key] + }; + itemVersion = await patch(data.key, itemVersion, itemData, newData); + + newData = { + collections: [] + }; + await patch(data.key, itemVersion, itemData, newData); + }); + + it('testNewComputerProgramItem', async function () { + const xml = await API.createItem('computerProgram', false, true); + const data = await API.parseDataFromAtomEntry(xml); + const key = data.key; + const json = JSON.parse(data.content); + assert.equal(json.itemType, 'computerProgram'); + + const version = '1.0'; + json.version = version; + + const response = await API.userPut( + config.userID, + `items/${key}?key=${config.apiKey}`, + JSON.stringify(json), + { "Content-Type": "application/json", "If-Unmodified-Since-Version": data.version } + ); + Helpers.assertStatusCode(response, 204); + + const xml2 = await API.getItemXML(key); + const data2 = await API.parseDataFromAtomEntry(xml2); + const json2 = JSON.parse(data2.content); + assert.equal(json2.version, version); + + delete json2.version; + const version2 = '1.1'; + json2.versionNumber = version2; + const response2 = await API.userPut( + config.userID, + `items/${key}?key=${config.apiKey}`, + JSON.stringify(json2), + { "Content-Type": "application/json" } + ); + Helpers.assertStatusCode(response2, 204); + + const xml3 = await API.getItemXML(key); + const data3 = await API.parseDataFromAtomEntry(xml3); + const json3 = JSON.parse(data3.content); + assert.equal(json3.version, version2); + }); + + it('testNewInvalidBookItem', async function () { + const json = await API.getItemTemplate("book"); + + // Missing item type + const json2 = { ...json }; + delete json2.itemType; + let response = await API.userPost( + config.userID, + `items?key=${config.apiKey}`, + JSON.stringify({ + items: [json2] + }), + { "Content-Type": "application/json" } + ); + Helpers.assertStatusForObject(response, 'failed', 0, 400, "'itemType' property not provided"); + + // contentType on non-attachment + const json3 = { ...json }; + json3.contentType = "text/html"; + response = await API.userPost( + config.userID, + `items?key=${config.apiKey}`, + JSON.stringify({ + items: [json3] + }), + { "Content-Type": "application/json" } + ); + Helpers.assertStatusForObject(response, 'failed', 0, 400, "'contentType' is valid only for attachment items"); + }); + + it('testEditTopLevelNote', async function () { + const xml = await API.createNoteItem("

Test

", null, true, 'atom'); + const data = API.parseDataFromAtomEntry(xml); + const json = JSON.parse(data.content); + const noteText = "

Test Test

"; + json.note = noteText; + const response = await API.userPut( + config.userID, + `items/${data.key}?key=` + config.apiKey, + JSON.stringify(json) + ); + Helpers.assertStatusCode(response, 204); + const response2 = await API.userGet( + config.userID, + `items/${data.key}?key=` + config.apiKey + "&content=json" + ); + Helpers.assertStatusCode(response2, 200); + const xml2 = API.getXMLFromResponse(response2); + const data2 = API.parseDataFromAtomEntry(xml2); + const json2 = JSON.parse(data2.content); + assert.equal(json2.note, noteText); + }); + + it('testEditChildNote', async function () { + const key = await API.createItem("book", { title: "Test" }, true, 'key'); + const xml = await API.createNoteItem("

Test

", key, true, 'atom'); + const data = API.parseDataFromAtomEntry(xml); + const json = JSON.parse(data.content); + const noteText = "

Test Test

"; + json.note = noteText; + const response1 = await API.userPut( + config.userID, + "items/" + data.key + "?key=" + config.apiKey, + JSON.stringify(json) + ); + assert.equal(response1.status, 204); + const response2 = await API.userGet( + config.userID, + "items/" + data.key + "?key=" + config.apiKey + "&content=json" + ); + Helpers.assertStatusCode(response2, 200); + const xml2 = API.getXMLFromResponse(response2); + const data2 = API.parseDataFromAtomEntry(xml2); + const json2 = JSON.parse(data2.content); + assert.equal(json2.note, noteText); + }); + + it('testEditTitleWithCollectionInMultipleMode', async function () { + const collectionKey = await API.createCollection('Test', false, true, 'key'); + let xml = await API.createItem('book', { + title: 'A', + collections: [ + collectionKey, + ], + }, true, 'atom'); + let data = API.parseDataFromAtomEntry(xml); + data = JSON.parse(data.content); + const version = data.itemVersion; + data.title = 'B'; + const response = await API.userPost( + config.userID, + `items?key=${config.apiKey}`, JSON.stringify({ + items: [data], + }), + ); + Helpers.assertStatusCode(response, 200); + xml = await API.getItemXML(data.itemKey); + data = API.parseDataFromAtomEntry(xml); + const json = JSON.parse(data.content); + assert.equal(json.title, 'B'); + assert.isAbove(json.itemVersion, version); + }); + + it('testEditTitleWithTagInMultipleMode', async function () { + const tag1 = { + tag: 'foo', + type: 1, + }; + const tag2 = { + tag: 'bar', + }; + + const xml = await API.createItem('book', { + title: 'A', + tags: [tag1], + }, true, 'atom'); + + const data = API.parseDataFromAtomEntry(xml); + const json = JSON.parse(data.content); + assert.equal(json.tags.length, 1); + assert.deepEqual(json.tags[0], tag1); + + const version = json.itemVersion; + json.title = 'B'; + json.tags.push(tag2); + + const response = await API.userPost( + config.userID, + `items?key=${config.apiKey}`, + JSON.stringify({ + items: [json], + }), + ); + Helpers.assertStatusForObject(response, 'success', 0); + const xml2 = await API.getItemXML(json.itemKey); + const data2 = API.parseDataFromAtomEntry(xml2); + const json2 = JSON.parse(data2.content); + assert.equal(json2.title, 'B'); + assert.isAbove(json2.itemVersion, version); + assert.equal(json2.tags.length, 2); + assert.deepEqual(json2.tags, [tag2, tag1]); + }); + + it('testNewTopLevelImportedFileAttachment', async function () { + const response = await API.get("items/new?itemType=attachment&linkMode=imported_file"); + const json = JSON.parse(response.data); + const userPostResponse = await API.userPost( + config.userID, + `items?key=${config.apiKey}`, + JSON.stringify({ + items: [json] + }), { "Content-Type": "application/json" } + ); + Helpers.assertStatusCode(userPostResponse, 200); + }); + + it('testNewInvalidTopLevelAttachment', async function() { + this.skip(); //disabled + }); + + it('testNewEmptyLinkAttachmentItem', async function () { + const key = await API.createItem("book", false, true, 'key'); + const xml = await API.createAttachmentItem("linked_url", [], key, true, 'atom'); + await API.parseDataFromAtomEntry(xml); + }); + + it('testNewEmptyLinkAttachmentItemWithItemKey', async function () { + const key = await API.createItem("book", false, true, 'key'); + await API.createAttachmentItem("linked_url", [], key, true, 'atom'); + + let response = await API.get("items/new?itemType=attachment&linkMode=linked_url"); + let json = JSON.parse(response.data); + json.parentItem = key; + + json.itemKey = Helpers.uniqueID(); + json.itemVersion = 0; + + response = await API.userPost( + config.userID, + "items?key=" + config.apiKey, + JSON.stringify({ + items: [json] + }), + { "Content-Type": "application/json" } + ); + Helpers.assertStatusCode(response, 200); + }); + + const testNewEmptyImportedURLAttachmentItem = async () => { + const key = await API.createItem('book', false, true, 'key'); + const xml = await API.createAttachmentItem('imported_url', [], key, true, 'atom'); + return API.parseDataFromAtomEntry(xml); + }; + + it('testEditEmptyImportedURLAttachmentItem', async function () { + const newItemData = await testNewEmptyImportedURLAttachmentItem(); + const key = newItemData.key; + const version = newItemData.version; + const json = JSON.parse(newItemData.content); + + const response = await API.userPut( + config.userID, + `items/${key}?key=${config.apiKey}`, + JSON.stringify(json), + { + 'Content-Type': 'application/json', + 'If-Unmodified-Since-Version': version + } + ); + Helpers.assertStatusCode(response, 204); + + const xml = await API.getItemXML(key); + const data = await API.parseDataFromAtomEntry(xml); + // Item Shouldn't be changed + assert.equal(version, data.version); + }); + + const testEditEmptyLinkAttachmentItem = async () => { + const key = await API.createItem('book', false, true, 'key'); + const xml = await API.createAttachmentItem('linked_url', [], key, true, 'atom'); + const data = await API.parseDataFromAtomEntry(xml); + + const updatedKey = data.key; + const version = data.version; + const json = JSON.parse(data.content); + + const response = await API.userPut( + config.userID, + `items/${updatedKey}?key=${config.apiKey}`, + JSON.stringify(json), + { + "Content-Type": "application/json", + "If-Unmodified-Since-Version": version + } + ); + Helpers.assertStatusCode(response, 204); + + const newXml = await API.getItemXML(updatedKey); + const newData = await API.parseDataFromAtomEntry(newXml); + // Item shouldn't change + assert.equal(version, newData.version); + return newData; + }; + + it('testEditLinkAttachmentItem', async function () { + const newItemData = await testEditEmptyLinkAttachmentItem(); + const key = newItemData.key; + const version = newItemData.version; + const json = JSON.parse(newItemData.content); + + const contentType = "text/xml"; + const charset = "utf-8"; + + json.contentType = contentType; + json.charset = charset; + + const response = await API.userPut( + config.userID, + `items/${key}?key=${config.apiKey}`, + JSON.stringify(json), + { + "Content-Type": "application/json", + "If-Unmodified-Since-Version": version + } + ); + + Helpers.assertStatusCode(response, 204); + + const xml = await API.getItemXML(key); + const data = API.parseDataFromAtomEntry(xml); + const parsedJson = JSON.parse(data.content); + + assert.equal(parsedJson.contentType, contentType); + assert.equal(parsedJson.charset, charset); + }); + + it('testEditAttachmentUpdatedTimestamp', async function () { + const xml = await API.createAttachmentItem("linked_file", [], false, true); + const data = API.parseDataFromAtomEntry(xml); + const atomUpdated = Helpers.xpathEval(xml, '//atom:entry/atom:updated'); + const json = JSON.parse(data.content); + json.note = "Test"; + + await new Promise(resolve => setTimeout(resolve, 1000)); + + const response = await API.userPut( + config.userID, + `items/${data.key}?key=${config.apiKey}`, + JSON.stringify(json), + { "If-Unmodified-Since-Version": data.version } + ); + Helpers.assertStatusCode(response, 204); + + const xml2 = await API.getItemXML(data.key); + const atomUpdated2 = Helpers.xpathEval(xml2, '//atom:entry/atom:updated'); + assert.notEqual(atomUpdated2, atomUpdated); + }); + + it('testNewAttachmentItemInvalidLinkMode', async function () { + const response = await API.get("items/new?itemType=attachment&linkMode=linked_url"); + const json = JSON.parse(response.data); + + // Invalid linkMode + json.linkMode = "invalidName"; + const newResponse = await API.userPost( + config.userID, + "items?key=" + config.apiKey, + JSON.stringify({ + items: [json] + }), + { "Content-Type": "application/json" } + ); + Helpers.assertStatusForObject(newResponse, 'failed', 0, 400, "'invalidName' is not a valid linkMode"); + + // Missing linkMode + delete json.linkMode; + const missingResponse = await API.userPost( + config.userID, + "items?key=" + config.apiKey, + JSON.stringify({ + items: [json] + }), + { "Content-Type": "application/json" } + ); + Helpers.assertStatusForObject(missingResponse, 'failed', 0, 400, "'linkMode' property not provided"); + }); + it('testNewAttachmentItemMD5OnLinkedURL', async function () { + const newItemData = await testNewEmptyBookItem(); + const parentKey = newItemData.key; + + const response = await API.get("items/new?itemType=attachment&linkMode=linked_url"); + const json = JSON.parse(response.data); + json.parentItem = parentKey; + + json.md5 = "c7487a750a97722ae1878ed46b215ebe"; + const postResponse = await API.userPost( + config.userID, + "items?key=" + config.apiKey, + JSON.stringify({ + items: [json] + }), + { "Content-Type": "application/json" } + ); + Helpers.assertStatusForObject(postResponse, 'failed', 0, 400, "'md5' is valid only for imported and embedded-image attachments"); + }); + it('testNewAttachmentItemModTimeOnLinkedURL', async function () { + const newItemData = await testNewEmptyBookItem(); + const parentKey = newItemData.key; + + const response = await API.get("items/new?itemType=attachment&linkMode=linked_url"); + const json = JSON.parse(response.data); + json.parentItem = parentKey; + + json.mtime = "1332807793000"; + const postResponse = await API.userPost( + config.userID, + "items?key=" + config.apiKey, + JSON.stringify({ + items: [json] + }), + { "Content-Type": "application/json" } + ); + Helpers.assertStatusForObject(postResponse, 'failed', 0, 400, "'mtime' is valid only for imported and embedded-image attachments"); + }); + it('testMappedCreatorTypes', async function () { + const json = { + items: [ + { + itemType: 'presentation', + title: 'Test', + creators: [ + { + creatorType: "author", + name: "Foo" + } + ] + }, + { + itemType: 'presentation', + title: 'Test', + creators: [ + { + creatorType: "editor", + name: "Foo" + } + ] + } + ] + }; + const response = await API.userPost( + config.userID, + "items?key=" + config.apiKey, + JSON.stringify(json) + ); + // 'author' gets mapped automatically, others dont + Helpers.assertStatusForObject(response, 'failed', 1, 400); + Helpers.assertStatusForObject(response, 'success', 0); + }); + + it('testNumChildren', async function () { + let xml = await API.createItem("book", false, true); + assert.equal(Helpers.xpathEval(xml, '//atom:entry/zapi:numChildren'), 0); + const data = API.parseDataFromAtomEntry(xml); + const key = data.key; + + await API.createAttachmentItem("linked_url", [], key, true, 'key'); + + let response = await API.userGet( + config.userID, + `items/${key}?key=${config.apiKey}&content=json` + ); + xml = API.getXMLFromResponse(response); + assert.equal(Helpers.xpathEval(xml, '//atom:entry/zapi:numChildren'), 1); + + await API.createNoteItem("Test", key, true, 'key'); + + response = await API.userGet( + config.userID, + `items/${key}?key=${config.apiKey}&content=json` + ); + xml = API.getXMLFromResponse(response); + assert.equal(Helpers.xpathEval(xml, '//atom:entry/zapi:numChildren'), 2); + }); + + it('testTop', async function () { + await API.userClear(config.userID); + + const collectionKey = await API.createCollection('Test', false, this, 'key'); + + const parentTitle1 = "Parent Title"; + const childTitle1 = "This is a Test Title"; + const parentTitle2 = "Another Parent Title"; + const parentTitle3 = "Yet Another Parent Title"; + const noteText = "This is a sample note."; + const parentTitleSearch = "title"; + const childTitleSearch = "test"; + const dates = ["2013", "January 3, 2010", ""]; + const orderedDates = [dates[2], dates[1], dates[0]]; + const itemTypes = ["journalArticle", "newspaperArticle", "book"]; + + const parentKeys = []; + const childKeys = []; + + parentKeys.push(await API.createItem(itemTypes[0], { + title: parentTitle1, + date: dates[0], + collections: [ + collectionKey + ] + }, this, 'key')); + + childKeys.push(await API.createAttachmentItem("linked_url", { + title: childTitle1 + }, parentKeys[0], this, 'key')); + + parentKeys.push(await API.createItem(itemTypes[1], { + title: parentTitle2, + date: dates[1] + }, this, 'key')); + + childKeys.push(await API.createNoteItem(noteText, parentKeys[1], this, 'key')); + + parentKeys.push(await API.createItem(itemTypes[2], { + title: parentTitle3 + }, this, 'key')); + + childKeys.push(await API.createAttachmentItem("linked_url", { + title: childTitle1, + deleted: true + }, parentKeys[parentKeys.length - 1], this, 'key')); + + const deletedKey = await API.createItem("book", { + title: "This is a deleted item", + deleted: true, + }, this, 'key'); + + await API.createNoteItem("This is a child note of a deleted item.", deletedKey, this, 'key'); + + const top = async (url, expectedResults = -1) => { + const response = await API.userGet(config.userID, url); + Helpers.assertStatusCode(response, 200); + if (expectedResults !== -1) { + Helpers.assertNumResults(response, expectedResults); + } + return response; + }; + + const checkXml = (response, expectedCount = -1, path = '//atom:entry/zapi:key') => { + const xml = API.getXMLFromResponse(response); + const xpath = Helpers.xpathEval(xml, path, false, true); + if (expectedCount !== -1) { + assert.equal(xpath.length, expectedCount); + } + return xpath; + }; + + // /top, Atom + let response = await top(`items/top?key=${config.apiKey}&content=json`, parentKeys.length); + let xpath = await checkXml(response, parentKeys.length); + for (let parentKey of parentKeys) { + assert.include(xpath, parentKey); + } + + // /top, Atom, in collection + response = await top(`collections/${collectionKey}/items/top?key=${config.apiKey}&content=json`, 1); + xpath = await checkXml(response, 1); + assert.include(xpath, parentKeys[0]); + + // /top, keys + response = await top(`items/top?key=${config.apiKey}&format=keys`); + let keys = response.data.trim().split("\n"); + assert.equal(keys.length, parentKeys.length); + for (let parentKey of parentKeys) { + assert.include(keys, parentKey); + } + + // /top, keys, in collection + response = await top(`collections/${collectionKey}/items/top?key=${config.apiKey}&format=keys`); + assert.equal(response.data.trim(), parentKeys[0]); + + // /top with itemKey for parent, Atom + response = await top(`items/top?key=${config.apiKey}&content=json&itemKey=${parentKeys[0]}`, 1); + xpath = await checkXml(response); + assert.equal(parentKeys[0], xpath.shift()); + + // /top with itemKey for parent, Atom, in collection + response = await top(`collections/${collectionKey}/items/top?key=${config.apiKey}&content=json&itemKey=${parentKeys[0]}`, 1); + xpath = await checkXml(response); + assert.equal(parentKeys[0], xpath.shift()); + + // /top with itemKey for parent, keys + response = await top(`items/top?key=${config.apiKey}&format=keys&itemKey=${parentKeys[0]}`); + assert.equal(parentKeys[0], response.data.trim()); + + // /top with itemKey for parent, keys, in collection + response = await top(`collections/${collectionKey}/items/top?key=${config.apiKey}&format=keys&itemKey=${parentKeys[0]}`); + assert.equal(parentKeys[0], response.data.trim()); + + // /top with itemKey for child, Atom + response = await top(`items/top?key=${config.apiKey}&content=json&itemKey=${childKeys[0]}`, 1); + xpath = await checkXml(response); + assert.equal(parentKeys[0], xpath.shift()); + + // /top with itemKey for child, keys + response = await top(`items/top?key=${config.apiKey}&format=keys&itemKey=${childKeys[0]}`); + assert.equal(parentKeys[0], response.data.trim()); + + // /top, Atom, with q for all items + response = await top(`items/top?key=${config.apiKey}&content=json&q=${parentTitleSearch}`, parentKeys.length); + xpath = await checkXml(response, parentKeys.length); + for (let parentKey of parentKeys) { + assert.include(xpath, parentKey); + } + + // /top, Atom, in collection, with q for all items + response = await top(`collections/${collectionKey}/items/top?key=${config.apiKey}&content=json&q=${parentTitleSearch}`, 1); + xpath = await checkXml(response, 1); + assert.include(xpath, parentKeys[0]); + + // /top, Atom, with q for child item + response = await top(`items/top?key=${config.apiKey}&content=json&q=${childTitleSearch}`, 1); + xpath = checkXml(response, 1); + assert.include(xpath, parentKeys[0]); + + // /top, Atom, in collection, with q for child item + response = await top(`collections/${collectionKey}/items/top?key=${config.apiKey}&content=json&q=${childTitleSearch}`, 1); + xpath = checkXml(response, 1); + assert.include(xpath, parentKeys[0]); + + // /top, Atom, with q for all items, ordered by title + response = await top(`items/top?key=${config.apiKey}&content=json&q=${parentTitleSearch}&order=title`, parentKeys.length); + xpath = checkXml(response, parentKeys.length, '//atom:entry/atom:title'); + + let orderedTitles = [parentTitle1, parentTitle2, parentTitle3].sort(); + let orderedResults = xpath.map(val => String(val)); + assert.deepEqual(orderedTitles, orderedResults); + + // /top, Atom, with q for all items, ordered by date asc + response = await top(`items/top?key=${config.apiKey}&content=json&q=${parentTitleSearch}&order=date&sort=asc`, parentKeys.length); + xpath = checkXml(response, parentKeys.length, '//atom:entry/atom:content'); + orderedResults = xpath.map(val => JSON.parse(val).date); + assert.deepEqual(orderedDates, orderedResults); + + // /top, Atom, with q for all items, ordered by date desc + response = await top(`items/top?key=${config.apiKey}&content=json&q=${parentTitleSearch}&order=date&sort=desc`, parentKeys.length); + xpath = checkXml(response, parentKeys.length, '//atom:entry/atom:content'); + let orderedDatesReverse = [...orderedDates].reverse(); + orderedResults = xpath.map(val => JSON.parse(val).date); + assert.deepEqual(orderedDatesReverse, orderedResults); + + // /top, Atom, with q for all items, ordered by item type asc + response = await top(`items/top?key=${config.apiKey}&content=json&q=${parentTitleSearch}&order=itemType`, parentKeys.length); + xpath = checkXml(response, parentKeys.length, '//atom:entry/zapi:itemType'); + let orderedItemTypes = [...itemTypes].sort(); + orderedResults = xpath.map(val => String(val)); + assert.deepEqual(orderedItemTypes, orderedResults); + + // /top, Atom, with q for all items, ordered by item type desc + response = await top(`items/top?key=${config.apiKey}&content=json&q=${parentTitleSearch}&order=itemType&sort=desc`, parentKeys.length); + xpath = checkXml(response, parentKeys.length, '//atom:entry/zapi:itemType'); + orderedItemTypes = [...itemTypes].sort().reverse(); + orderedResults = xpath.map(val => String(val)); + assert.deepEqual(orderedItemTypes, orderedResults); + }); + + it('testParentItem', async function () { + let xml = await API.createItem("book", false, true); + let data = API.parseDataFromAtomEntry(xml); + let json = JSON.parse(data.content); + let parentKey = data.key; + let parentVersion = data.version; + + xml = await API.createAttachmentItem("linked_url", [], parentKey, true); + data = API.parseDataFromAtomEntry(xml); + json = JSON.parse(data.content); + let childKey = data.key; + let childVersion = data.version; + + assert.ok(json.parentItem); + assert.equal(parentKey, json.parentItem); + + // Remove the parent, making the child a standalone attachment + delete json.parentItem; + + // Remove version property, to test header + delete json.itemVersion; + + // The parent item version should have been updated when a child + // was added, so this should fail + let response = await API.userPut( + config.userID, + `items/${childKey}?key=${config.apiKey}`, + JSON.stringify(json), + { "If-Unmodified-Since-Version": parentVersion } + ); + Helpers.assertStatusCode(response, 412); + + response = await API.userPut( + config.userID, + `items/${childKey}?key=${config.apiKey}`, + JSON.stringify(json), + { "If-Unmodified-Since-Version": childVersion } + ); + Helpers.assertStatusCode(response, 204); + + xml = await API.getItemXML(childKey); + data = API.parseDataFromAtomEntry(xml); + json = JSON.parse(data.content); + assert.notExists(json.parentItem); + }); + + it('testParentItemPatch', async function () { + let xml = await API.createItem("book", false, true); + let data = API.parseDataFromAtomEntry(xml); + let json = JSON.parse(data.content); + const parentKey = data.key; + + xml = await API.createAttachmentItem("linked_url", [], parentKey, true); + data = API.parseDataFromAtomEntry(xml); + json = JSON.parse(data.content); + const childKey = data.key; + const childVersion = data.version; + + assert.ok(json.parentItem); + assert.equal(parentKey, json.parentItem); + + const json3 = { + title: 'Test' + }; + + // With PATCH, parent shouldn't be removed even though unspecified + const response = await API.userPatch( + config.userID, + `items/${childKey}?key=${config.apiKey}`, + JSON.stringify(json3), + { "If-Unmodified-Since-Version": childVersion }, + ); + + Helpers.assertStatusCode(response, 204); + + xml = await API.getItemXML(childKey); + data = API.parseDataFromAtomEntry(xml); + json = JSON.parse(data.content); + + assert.ok(json.parentItem); + }); + + it('testDate', async function () { + const date = "Sept 18, 2012"; + + const xml = await API.createItem("book", { date: date }, true); + const data = API.parseDataFromAtomEntry(xml); + const key = data.key; + + const response = await API.userGet( + config.userID, + `items/${key}?key=${config.apiKey}&content=json` + ); + const xmlResponse = await API.getXMLFromResponse(response); + const dataResponse = API.parseDataFromAtomEntry(xmlResponse); + const json = JSON.parse(dataResponse.content); + assert.equal(date, json.date); + + assert.equal(Helpers.xpathEval(xmlResponse, '//atom:entry/zapi:year'), '2012'); + }); + + it('testUnicodeTitle', async function () { + const title = "Tést"; + + const xml = await API.createItem("book", { title }, true); + const data = await API.parseDataFromAtomEntry(xml); + const key = data.key; + + // Test entry + let response = await API.userGet( + config.userID, + `items/${key}?key=${config.apiKey}&content=json` + ); + let xmlResponse = await API.getXMLFromResponse(response); + assert.equal(xmlResponse.getElementsByTagName("title")[0].innerHTML, "Tést"); + + // Test feed + response = await API.userGet( + config.userID, + `items?key=${config.apiKey}&content=json` + ); + xmlResponse = await API.getXMLFromResponse(response); + + let titleFound = false; + for (var node of xmlResponse.getElementsByTagName("title")) { + if (node.innerHTML == title) { + titleFound = true; + } + } + assert.ok(titleFound); + }); +}); diff --git a/tests/remote_js/test/2/mappingsTest.js b/tests/remote_js/test/2/mappingsTest.js new file mode 100644 index 00000000..471b469f --- /dev/null +++ b/tests/remote_js/test/2/mappingsTest.js @@ -0,0 +1,64 @@ +const chai = require('chai'); +const assert = chai.assert; +const config = require("../../config.js"); +const API = require('../../api2.js'); +const Helpers = require('../../helpers.js'); +const { API2Setup, API2WrapUp } = require("../shared.js"); + +describe('MappingsTests', function () { + this.timeout(config.timeout); + + before(async function () { + await API2Setup(); + }); + + after(async function () { + await API2WrapUp(); + }); + + it('testNewItem', async function () { + let response = await API.get("items/new?itemType=invalidItemType"); + Helpers.assertStatusCode(response, 400); + + response = await API.get("items/new?itemType=book"); + Helpers.assertStatusCode(response, 200); + assert.equal(response.headers['content-type'][0], 'application/json'); + const json = JSON.parse(response.data); + assert.equal(json.itemType, 'book'); + }); + + it('testNewItemAttachment', async function () { + let response = await API.get('items/new?itemType=attachment'); + Helpers.assertStatusCode(response, 400); + + response = await API.get('items/new?itemType=attachment&linkMode=invalidLinkMode'); + Helpers.assertStatusCode(response, 400); + + response = await API.get('items/new?itemType=attachment&linkMode=linked_url'); + Helpers.assertStatusCode(response, 200); + const json1 = JSON.parse(response.data); + assert.isNotNull(json1); + assert.property(json1, 'url'); + + response = await API.get('items/new?itemType=attachment&linkMode=linked_file'); + Helpers.assertStatusCode(response, 200); + const json2 = JSON.parse(response.data); + assert.isNotNull(json2); + assert.notProperty(json2, 'url'); + }); + + it('testComputerProgramVersion', async function () { + let response = await API.get("items/new?itemType=computerProgram"); + Helpers.assertStatusCode(response, 200); + let json = JSON.parse(response.data); + assert.property(json, 'version'); + assert.notProperty(json, 'versionNumber'); + + response = await API.get("itemTypeFields?itemType=computerProgram"); + Helpers.assertStatusCode(response, 200); + json = JSON.parse(response.data); + let fields = json.map(val => val.field); + assert.include(fields, 'version'); + assert.notInclude(fields, 'versionNumber'); + }); +}); diff --git a/tests/remote_js/test/2/noteTest.js b/tests/remote_js/test/2/noteTest.js new file mode 100644 index 00000000..c802c52e --- /dev/null +++ b/tests/remote_js/test/2/noteTest.js @@ -0,0 +1,104 @@ +const chai = require('chai'); +const assert = chai.assert; +const config = require("../../config.js"); +const API = require('../../api2.js'); +const Helpers = require("../../helpers.js"); +const { API2Setup, API2WrapUp } = require("../shared.js"); + +describe('NoteTests', function () { + this.timeout(config.timeout); + let content; + let json; + + before(async function () { + await API2Setup(); + }); + + after(async function () { + await API2WrapUp(); + }); + + beforeEach(async function () { + content = "1234567890".repeat(50001); + json = await API.getItemTemplate("note"); + json.note = content; + }); + + it('testNoteTooLong', async function () { + const response = await API.userPost( + config.userID, + "items?key=" + config.apiKey, + JSON.stringify({ + items: [json] + }), + { + headers: { "Content-Type": "application/json" } + } + ); + const expectedMessage = "Note '1234567890123456789012345678901234567890123456789012345678901234567890123456789...' too long"; + Helpers.assertStatusForObject(response, 'failed', 0, 413, expectedMessage); + }); + + it('testNoteTooLongBlankFirstLines', async function () { + json.note = " \n \n" + content; + + const response = await API.userPost( + config.userID, + "items?key=" + config.apiKey, + JSON.stringify({ + items: [json] + }), + { "Content-Type": "application/json" } + ); + const expectedMessage = "Note '1234567890123456789012345678901234567890123456789012345678901234567890123456789...' too long"; + Helpers.assertStatusForObject(response, 'failed', 0, 413, expectedMessage); + }); + + it('testNoteTooLongBlankFirstLinesHTML', async function () { + json.note = '\n

 

\n

 

\n' + content; + + const response = await API.userPost( + config.userID, + `items?key=${config.apiKey}`, + JSON.stringify({ + items: [json] + }), + { "Content-Type": "application/json" } + ); + + const expectedMessage = "Note '1234567890123456789012345678901234567890123456789012345678901234567890123...' too long"; + Helpers.assertStatusForObject(response, 'failed', 0, 413, expectedMessage); + }); + + it('testNoteTooLongTitlePlusNewlines', async function () { + json.note = "Full Text:\n\n" + content; + + const response = await API.userPost( + config.userID, + "items?key=" + config.apiKey, + JSON.stringify({ + items: [json] + }), + { "Content-Type": "application/json" } + ); + + const expectedMessage = "Note 'Full Text: 1234567890123456789012345678901234567890123456789012345678901234567...' too long"; + Helpers.assertStatusForObject(response, 'failed', 0, 413, expectedMessage); + }); + + it('testNoteTooLongWithinHTMLTags', async function () { + json.note = " \n

"; + + const response = await API.userPost( + config.userID, + "items?key=" + config.apiKey, + JSON.stringify({ + items: [json] + }), + { "Content-Type": "application/json" } + ); + + const expectedMessage = "Note '<p><!-- 1234567890123456789012345678901234567890123456789012345678901234...' too long"; + Helpers.assertStatusForObject(response, 'failed', 0, 413, expectedMessage); + }); +}); diff --git a/tests/remote_js/test/2/objectTest.js b/tests/remote_js/test/2/objectTest.js new file mode 100644 index 00000000..c591b718 --- /dev/null +++ b/tests/remote_js/test/2/objectTest.js @@ -0,0 +1,454 @@ +const chai = require('chai'); +const assert = chai.assert; +const config = require("../../config.js"); +const API = require('../../api2.js'); +const Helpers = require('../../helpers.js'); +const { API2Setup, API2WrapUp } = require("../shared.js"); + +describe('ObjectTests', function () { + this.timeout(config.timeout); + + before(async function () { + await API2Setup(); + }); + + after(async function () { + await API2WrapUp(); + }); + + const _testMultiObjectGet = async (objectType = 'collection') => { + const objectNamePlural = API.getPluralObjectType(objectType); + const keyProp = `${objectType}Key`; + + const keys = []; + switch (objectType) { + case 'collection': + keys.push(await API.createCollection("Name", false, true, 'key')); + keys.push(await API.createCollection("Name", false, true, 'key')); + await API.createCollection("Name", false, true, 'key'); + break; + + case 'item': + keys.push(await API.createItem("book", { title: "Title" }, true, 'key')); + keys.push(await API.createItem("book", { title: "Title" }, true, 'key')); + await API.createItem("book", { title: "Title" }, true, 'key'); + break; + + case 'search': + keys.push(await API.createSearch("Name", 'default', true, 'key')); + keys.push(await API.createSearch("Name", 'default', true, 'key')); + await API.createSearch("Name", 'default', true, 'key'); + break; + } + + let response = await API.userGet( + config.userID, + `${objectNamePlural}?key=${config.apiKey}&${keyProp}=${keys.join(',')}` + ); + Helpers.assertStatusCode(response, 200); + Helpers.assertNumResults(response, keys.length); + + // Trailing comma in itemKey parameter + response = await API.userGet( + config.userID, + `${objectNamePlural}?key=${config.apiKey}&${keyProp}=${keys.join(',')},` + ); + Helpers.assertStatusCode(response, 200); + Helpers.assertNumResults(response, keys.length); + }; + + const _testSingleObjectDelete = async (objectType) => { + const objectTypePlural = API.getPluralObjectType(objectType); + + let xml; + switch (objectType) { + case 'collection': + xml = await API.createCollection('Name', false, true); + break; + case 'item': + xml = await API.createItem('book', { title: 'Title' }, true); + break; + case 'search': + xml = await API.createSearch('Name', 'default', true); + break; + } + + const data = API.parseDataFromAtomEntry(xml); + const objectKey = data.key; + const objectVersion = data.version; + + const responseDelete = await API.userDelete( + config.userID, + `${objectTypePlural}/${objectKey}?key=${config.apiKey}`, + JSON.stringify({}), + { 'If-Unmodified-Since-Version': objectVersion } + ); + Helpers.assertStatusCode(responseDelete, 204); + + const responseGet = await API.userGet( + config.userID, + `${objectTypePlural}/${objectKey}?key=${config.apiKey}` + ); + Helpers.assertStatusCode(responseGet, 404); + }; + + const _testMultiObjectDelete = async (objectType) => { + await API.userClear(config.userID); + const objectTypePlural = await API.getPluralObjectType(objectType); + const keyProp = `${objectType}Key`; + + const deleteKeys = []; + const keepKeys = []; + switch (objectType) { + case 'collection': + deleteKeys.push(await API.createCollection("Name", false, true, 'key')); + deleteKeys.push(await API.createCollection("Name", false, true, 'key')); + keepKeys.push(await API.createCollection("Name", false, true, 'key')); + break; + + case 'item': + deleteKeys.push(await API.createItem("book", { title: "Title" }, true, 'key')); + deleteKeys.push(await API.createItem("book", { title: "Title" }, true, 'key')); + keepKeys.push(await API.createItem("book", { title: "Title" }, true, 'key')); + break; + + case 'search': + deleteKeys.push(await API.createSearch("Name", 'default', true, 'key')); + deleteKeys.push(await API.createSearch("Name", 'default', true, 'key')); + keepKeys.push(await API.createSearch("Name", 'default', true, 'key')); + break; + } + + let response = await API.userGet(config.userID, `${objectTypePlural}?key=${config.apiKey}`); + Helpers.assertNumResults(response, deleteKeys.length + keepKeys.length); + + let libraryVersion = response.headers["last-modified-version"]; + + response = await API.userDelete(config.userID, + `${objectTypePlural}?key=${config.apiKey}&${keyProp}=${deleteKeys.join(',')}`, + JSON.stringify({}), + { "If-Unmodified-Since-Version": libraryVersion } + ); + Helpers.assertStatusCode(response, 204); + libraryVersion = response.headers["last-modified-version"]; + response = await API.userGet(config.userID, `${objectTypePlural}?key=${config.apiKey}`); + Helpers.assertNumResults(response, keepKeys.length); + + response = await API.userGet(config.userID, `${objectTypePlural}?key=${config.apiKey}&${keyProp}=${keepKeys.join(',')}`); + Helpers.assertNumResults(response, keepKeys.length); + + response = await API.userDelete(config.userID, + `${objectTypePlural}?key=${config.apiKey}&${keyProp}=${keepKeys.join(',')},`, + JSON.stringify({}), + { "If-Unmodified-Since-Version": libraryVersion }); + Helpers.assertStatusCode(response, 204); + + response = await API.userGet(config.userID, `${objectTypePlural}?key=${config.apiKey}`); + Helpers.assertNumResults(response, 0); + }; + + const _testPartialWriteFailure = async () => { + await API.userClear(config.userID); + let json; + let conditions = []; + const objectType = 'collection'; + let json1 = { name: "Test" }; + let json2 = { name: "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123" }; + let json3 = { name: "Test" }; + switch (objectType) { + case 'collection': + json1 = { name: "Test" }; + json2 = { name: "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123" }; + json3 = { name: "Test" }; + break; + case 'item': + json1 = await API.getItemTemplate('book'); + json2 = { ...json1 }; + json3 = { ...json1 }; + json2.title = "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123"; + break; + case 'search': + conditions = [ + { + condition: 'title', + operator: 'contains', + value: 'value' + } + ]; + json1 = { name: "Test", conditions: conditions }; + json2 = { name: "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123", conditions: conditions }; + json3 = { name: "Test", conditions: conditions }; + break; + } + + const response = await API.userPost( + config.userID, + `${API.getPluralObjectType(objectType)}?key=${config.apiKey}`, + JSON.stringify({ + objectTypePlural: [json1, json2, json3] + }), + { "Content-Type": "application/json" }); + + Helpers.assertStatusCode(response, 200); + json = await API.getJSONFromResponse(response); + + Helpers.assertStatusForObject(response, 'success', 0, 200); + Helpers.assertStatusForObject(response, 'success', 1, 413); + Helpers.assertStatusForObject(response, 'success', 2, 200); + + const responseKeys = await API.userGet( + config.userID, + `${API.getPluralObjectType(objectType)}?format=keys&key=${config.apiKey}` + ); + + Helpers.assertStatusCode(responseKeys, 200); + const keys = responseKeys.data.trim().split("\n"); + + assert.lengthOf(keys, 2); + json.success.forEach((key) => { + assert.include(keys, key); + }); + }; + + const _testPartialWriteFailureWithUnchanged = async (objectType) => { + await API.userClear(config.userID); + + let objectTypePlural = API.getPluralObjectType(objectType); + let objectData; + let objectDataContent; + let json1; + let json2; + let json3; + let conditions = []; + + switch (objectType) { + case 'collection': + objectData = await API.createCollection('Test', false, true, 'data'); + objectDataContent = objectData.content; + json1 = JSON.parse(objectDataContent); + json2 = { name: "1234567890".repeat(6554) }; + json3 = { name: 'Test' }; + break; + + case 'item': + objectData = await API.createItem('book', { title: 'Title' }, true, 'data'); + objectDataContent = objectData.content; + json1 = JSON.parse(objectDataContent); + json2 = await API.getItemTemplate('book'); + json3 = { ...json2 }; + json2.title = "1234567890".repeat(6554); + break; + + case 'search': + conditions = [ + { + condition: 'title', + operator: 'contains', + value: 'value' + } + ]; + objectData = await API.createSearch('Name', conditions, true, 'data'); + objectDataContent = objectData.content; + json1 = JSON.parse(objectDataContent); + json2 = { + name: "1234567890".repeat(6554), + conditions + }; + json3 = { + name: 'Test', + conditions + }; + break; + } + + let response = await API.userPost( + config.userID, + `${objectTypePlural}?key=${config.apiKey}`, + JSON.stringify({ [objectTypePlural]: [json1, json2, json3] }), + { 'Content-Type': 'application/json' } + ); + + Helpers.assertStatusCode(response, 200); + let json = API.getJSONFromResponse(response); + + Helpers.assertStatusForObject(response, 'unchanged', 0); + Helpers.assertStatusForObject(response, 'failed', 1); + Helpers.assertStatusForObject(response, 'success', 2); + + + response = await API.userGet(config.userID, + `${objectTypePlural}?format=keys&key=${config.apiKey}`); + Helpers.assertStatusCode(response, 200); + let keys = response.data.trim().split('\n'); + assert.lengthOf(keys, 2); + + for (const [_, value] of Object.entries(json.success)) { + assert.include(keys, value); + } + }; + + const _testMultiObjectWriteInvalidObject = async (objectType) => { + const objectTypePlural = API.getPluralObjectType(objectType); + + let response = await API.userPost( + config.userID, + `${objectTypePlural}?key=${config.apiKey}`, + JSON.stringify([{}]), + { "Content-Type": "application/json" } + ); + + Helpers.assertStatusCode(response, 400); + assert.equal(response.data, "Uploaded data must be a JSON object"); + + response = await API.userPost( + config.userID, + `${objectTypePlural}?key=${config.apiKey}`, + JSON.stringify({ + [objectTypePlural]: { + foo: "bar" + } + }), + { "Content-Type": "application/json" } + ); + + Helpers.assertStatusCode(response, 400); + assert.equal(response.data, `'${objectTypePlural}' must be an array`); + }; + + it('testMultiObjectGet', async function () { + await _testMultiObjectGet('collection'); + await _testMultiObjectGet('item'); + await _testMultiObjectGet('search'); + }); + it('testSingleObjectDelete', async function () { + await _testSingleObjectDelete('collection'); + await _testSingleObjectDelete('item'); + await _testSingleObjectDelete('search'); + }); + it('testMultiObjectDelete', async function () { + await _testMultiObjectDelete('collection'); + await _testMultiObjectDelete('item'); + await _testMultiObjectDelete('search'); + }); + it('testPartialWriteFailure', async function () { + _testPartialWriteFailure('collection'); + _testPartialWriteFailure('item'); + _testPartialWriteFailure('search'); + }); + it('testPartialWriteFailureWithUnchanged', async function () { + await _testPartialWriteFailureWithUnchanged('collection'); + await _testPartialWriteFailureWithUnchanged('item'); + await _testPartialWriteFailureWithUnchanged('search'); + }); + + it('testMultiObjectWriteInvalidObject', async function () { + await _testMultiObjectWriteInvalidObject('collection'); + await _testMultiObjectWriteInvalidObject('item'); + await _testMultiObjectWriteInvalidObject('search'); + }); + + it('testDeleted', async function () { + await API.userClear(config.userID); + + // Create objects + const objectKeys = {}; + objectKeys.tag = ["foo", "bar"]; + + objectKeys.collection = []; + objectKeys.collection.push(await API.createCollection("Name", false, true, 'key')); + objectKeys.collection.push(await API.createCollection("Name", false, true, 'key')); + objectKeys.collection.push(await API.createCollection("Name", false, true, 'key')); + + objectKeys.item = []; + objectKeys.item.push(await API.createItem("book", { title: "Title", tags: objectKeys.tag.map(tag => ({ tag })) }, true, 'key')); + objectKeys.item.push(await API.createItem("book", { title: "Title" }, true, 'key')); + objectKeys.item.push(await API.createItem("book", { title: "Title" }, true, 'key')); + + objectKeys.search = []; + objectKeys.search.push(await API.createSearch("Name", 'default', true, 'key')); + objectKeys.search.push(await API.createSearch("Name", 'default', true, 'key')); + objectKeys.search.push(await API.createSearch("Name", 'default', true, 'key')); + + // Get library version + let response = await API.userGet(config.userID, "items?key=" + config.apiKey + "&format=keys&limit=1"); + let libraryVersion1 = response.headers["last-modified-version"][0]; + + const func = async (objectType, libraryVersion, url) => { + const objectTypePlural = await API.getPluralObjectType(objectType); + const response = await API.userDelete(config.userID, + `${objectTypePlural}?key=${config.apiKey}${url}`, + JSON.stringify({}), + { "If-Unmodified-Since-Version": libraryVersion }); + Helpers.assertStatusCode(response, 204); + return response.headers["last-modified-version"][0]; + }; + + let tempLibraryVersion = await func('collection', libraryVersion1, "&collectionKey=" + objectKeys.collection[0]); + tempLibraryVersion = await func('item', tempLibraryVersion, "&itemKey=" + objectKeys.item[0]); + tempLibraryVersion = await func('search', tempLibraryVersion, "&searchKey=" + objectKeys.search[0]); + let libraryVersion2 = tempLibraryVersion; + + // Delete second and third objects + tempLibraryVersion = await func('collection', tempLibraryVersion, "&collectionKey=" + objectKeys.collection.slice(1).join(',')); + tempLibraryVersion = await func('item', tempLibraryVersion, "&itemKey=" + objectKeys.item.slice(1).join(',')); + let libraryVersion3 = await func('search', tempLibraryVersion, "&searchKey=" + objectKeys.search.slice(1).join(',')); + + // Request all deleted objects + response = await API.userGet(config.userID, "deleted?key=" + config.apiKey + "&newer=" + libraryVersion1); + Helpers.assertStatusCode(response, 200); + let json = JSON.parse(response.data); + let version = response.headers["last-modified-version"][0]; + assert.isNotNull(version); + assert.equal(response.headers["content-type"][0], "application/json"); + + // Verify keys + const verifyKeys = async (json, objectType, objectKeys) => { + const objectTypePlural = await API.getPluralObjectType(objectType); + assert.containsAllKeys(json, [objectTypePlural]); + assert.lengthOf(json[objectTypePlural], objectKeys.length); + for (let key of objectKeys) { + assert.include(json[objectTypePlural], key); + } + }; + await verifyKeys(json, 'collection', objectKeys.collection); + await verifyKeys(json, 'item', objectKeys.item); + await verifyKeys(json, 'search', objectKeys.search); + // Tags aren't deleted by removing from items + await verifyKeys(json, 'tag', []); + + // Request second and third deleted objects + response = await API.userGet( + config.userID, + `deleted?key=${config.apiKey}&newer=${libraryVersion2}` + ); + Helpers.assertStatusCode(response, 200); + json = JSON.parse(response.data); + version = response.headers["last-modified-version"][0]; + assert.isNotNull(version); + assert.equal(response.headers["content-type"][0], "application/json"); + + await verifyKeys(json, 'collection', objectKeys.collection.slice(1)); + await verifyKeys(json, 'item', objectKeys.item.slice(1)); + await verifyKeys(json, 'search', objectKeys.search.slice(1)); + // Tags aren't deleted by removing from items + await verifyKeys(json, 'tag', []); + + // Explicit tag deletion + response = await API.userDelete( + config.userID, + `tags?key=${config.apiKey}&tag=${objectKeys.tag.join('%20||%20')}`, + JSON.stringify({}), + { "If-Unmodified-Since-Version": libraryVersion3 } + ); + Helpers.assertStatusCode(response, 204); + + // Verify deleted tags + response = await API.userGet( + config.userID, + `deleted?key=${config.apiKey}&newer=${libraryVersion3}` + ); + Helpers.assertStatusCode(response, 200); + json = JSON.parse(response.data); + await verifyKeys(json, 'tag', objectKeys.tag); + }); +}); diff --git a/tests/remote_js/test/2/paramTest.js b/tests/remote_js/test/2/paramTest.js new file mode 100644 index 00000000..f098b449 --- /dev/null +++ b/tests/remote_js/test/2/paramTest.js @@ -0,0 +1,311 @@ +const chai = require('chai'); +const assert = chai.assert; +const config = require("../../config.js"); +const API = require('../../api2.js'); +const Helpers = require('../../helpers.js'); +const { API2Setup, API2WrapUp } = require("../shared.js"); + +describe('ParametersTests', function () { + this.timeout(config.timeout * 2); + let collectionKeys = []; + let itemKeys = []; + let searchKeys = []; + + before(async function () { + await API2Setup(); + }); + + after(async function () { + await API2WrapUp(); + }); + + const _testFormatKeys = async (objectType, sorted = false) => { + const objectTypePlural = API.getPluralObjectType(objectType); + const response = await API.userGet( + config.userID, + `${objectTypePlural}?key=${config.apiKey}&format=keys${sorted ? '&order=title' : ''}`, + { 'Content-Type': 'application/json' } + ); + Helpers.assertStatusCode(response, 200); + + const keys = response.data.trim().split('\n'); + keys.sort(); + + switch (objectType) { + case "item": + assert.equal(keys.length, itemKeys.length); + if (sorted) { + assert.deepEqual(keys, itemKeys); + } + else { + keys.forEach((key) => { + assert.include(itemKeys, key); + }); + } + break; + case "collection": + assert.equal(keys.length, collectionKeys.length); + if (sorted) { + assert.deepEqual(keys, collectionKeys); + } + else { + keys.forEach((key) => { + assert.include(collectionKeys, key); + }); + } + break; + case "search": + assert.equal(keys.length, searchKeys.length); + + if (sorted) { + assert.deepEqual(keys, searchKeys); + } + else { + keys.forEach((key) => { + assert.include(searchKeys, key); + }); + } + break; + default: + throw new Error("Unknown object type"); + } + }; + + const _testObjectKeyParameter = async (objectType) => { + const objectTypePlural = API.getPluralObjectType(objectType); + const xmlArray = []; + let response; + switch (objectType) { + case 'collection': + xmlArray.push(await API.createCollection("Name", false, true)); + xmlArray.push(await API.createCollection("Name", false, true)); + break; + + case 'item': + xmlArray.push(await API.createItem("book", false, true)); + xmlArray.push(await API.createItem("book", false, true)); + break; + + case 'search': + xmlArray.push(await API.createSearch( + "Name", + [{ + condition: "title", + operator: "contains", + value: "test" + }], + true + )); + xmlArray.push(await API.createSearch( + "Name", + [{ + condition: "title", + operator: "contains", + value: "test" + }], + true + )); + break; + } + + const keys = []; + xmlArray.forEach((xml) => { + const data = API.parseDataFromAtomEntry(xml); + keys.push(data.key); + }); + + response = await API.userGet( + config.userID, + `${objectTypePlural}?key=${config.apiKey}&content=json&${objectType}Key=${keys[0]}` + ); + + Helpers.assertStatusCode(response, 200); + Helpers.assertNumResults(response, 1); + + let xml = API.getXMLFromResponse(response); + const data = API.parseDataFromAtomEntry(xml); + assert.equal(keys[0], data.key); + + response = await API.userGet( + config.userID, + `${objectTypePlural}?key=${config.apiKey}&content=json&${objectType}Key=${keys[0]},${keys[1]}&order=${objectType}KeyList` + ); + + Helpers.assertStatusCode(response, 200); + Helpers.assertNumResults(response, 2); + + xml = API.getXMLFromResponse(response); + const xpath = Helpers.xpathEval(xml, '//atom:entry/zapi:key', false, true); + const key1 = xpath[0]; + assert.equal(keys[0], key1); + const key2 = xpath[1]; + assert.equal(keys[1], key2); + }; + + it('testFormatKeys', async function () { + await API.userClear(config.userID); + for (let i = 0; i < 5; i++) { + const collectionKey = await API.createCollection('Test', false, null, 'key'); + collectionKeys.push(collectionKey); + } + + for (let i = 0; i < 5; i++) { + const itemKey = await API.createItem('book', false, null, 'key'); + itemKeys.push(itemKey); + } + const attachmentItemKey = await API.createAttachmentItem('imported_file', [], false, null, 'key'); + itemKeys.push(attachmentItemKey); + + for (let i = 0; i < 5; i++) { + const searchKey = await API.createSearch('Test', 'default', null, 'key'); + searchKeys.push(searchKey); + } + + await _testFormatKeys('collection'); + await _testFormatKeys('item'); + await _testFormatKeys('search'); + + + itemKeys.sort(); + collectionKeys.sort(); + searchKeys.sort(); + + await _testFormatKeys('collection', true); + await _testFormatKeys('item', true); + await _testFormatKeys('search', true); + }); + + it('testObjectKeyParameter', async function () { + await _testObjectKeyParameter('collection'); + await _testObjectKeyParameter('item'); + await _testObjectKeyParameter('search'); + }); + it('testCollectionQuickSearch', async function () { + const title1 = 'Test Title'; + const title2 = 'Another Title'; + + const keys = []; + keys.push(await API.createCollection(title1, [], true, 'key')); + keys.push(await API.createCollection(title2, [], true, 'key')); + + // Search by title + let response = await API.userGet( + config.userID, + `collections?key=${config.apiKey}&content=json&q=another` + ); + Helpers.assertStatusCode(response, 200); + Helpers.assertNumResults(response, 1); + const xml = API.getXMLFromResponse(response); + const xpath = Helpers.xpathEval(xml, '//atom:entry/zapi:key', false, true); + const key = xpath[0]; + assert.equal(keys[1], key); + + // No results + response = await API.userGet( + config.userID, + `collections?key=${config.apiKey}&content=json&q=nothing` + ); + Helpers.assertStatusCode(response, 200); + Helpers.assertNumResults(response, 0); + }); + + it('testItemQuickSearch', async function () { + const title1 = "Test Title"; + const title2 = "Another Title"; + const year2 = "2013"; + + const keys = []; + keys.push(await API.createItem("book", { + title: title1 + }, true, 'key')); + keys.push(await API.createItem("journalArticle", { + title: title2, + date: "November 25, " + year2 + }, true, 'key')); + + // Search by title + let response = await API.userGet( + config.userID, + "items?key=" + config.apiKey + "&content=json&q=" + encodeURIComponent(title1) + ); + Helpers.assertStatusCode(response, 200); + Helpers.assertNumResults(response, 1); + + let xml = API.getXMLFromResponse(response); + let xpath = Helpers.xpathEval(xml, '//atom:entry/zapi:key', false, true); + let key = xpath[0]; + assert.equal(keys[0], key); + + // TODO: Search by creator + + // Search by year + response = await API.userGet( + config.userID, + "items?key=" + config.apiKey + "&content=json&q=" + year2 + ); + Helpers.assertStatusCode(response, 200); + Helpers.assertNumResults(response, 1); + xml = API.getXMLFromResponse(response); + key = Helpers.xpathEval(xml, '//atom:entry/zapi:key'); + assert.equal(keys[1], key); + + // Search by year + 1 + response = await API.userGet( + config.userID, + "items?key=" + config.apiKey + "&content=json&q=" + (year2 + 1) + ); + Helpers.assertStatusCode(response, 200); + Helpers.assertNumResults(response, 0); + }); + + it('testItemQuickSearchOrderByDate', async function () { + await API.userClear(config.userID); + const title1 = 'Test Title'; + const title2 = 'Another Title'; + let response, xpath, xml; + const keys = []; + keys.push(await API.createItem('book', { + title: title1, + date: 'February 12, 2013' + }, true, 'key')); + keys.push(await API.createItem('journalArticle', { + title: title2, + date: 'November 25, 2012' + }, true, 'key')); + + response = await API.userGet( + config.userID, + `items?key=${config.apiKey}&content=json&q=${encodeURIComponent(title1)}` + ); + Helpers.assertStatusCode(response, 200); + Helpers.assertNumResults(response, 1); + + xml = API.getXMLFromResponse(response); + xpath = Helpers.xpathEval(xml, '//atom:entry/zapi:key', false, true); + assert.equal(keys[0], xpath[0]); + + response = await API.userGet( + config.userID, + `items?key=${config.apiKey}&content=json&q=title&order=date&sort=asc` + ); + Helpers.assertStatusCode(response, 200); + Helpers.assertNumResults(response, 2); + xml = API.getXMLFromResponse(response); + xpath = Helpers.xpathEval(xml, '//atom:entry/zapi:key', false, true); + + assert.equal(keys[1], xpath[0]); + assert.equal(keys[0], xpath[1]); + + response = await API.userGet( + config.userID, + `items?key=${config.apiKey}&content=json&q=title&order=date&sort=desc` + ); + Helpers.assertStatusCode(response, 200); + Helpers.assertNumResults(response, 2); + xml = API.getXMLFromResponse(response); + xpath = Helpers.xpathEval(xml, '//atom:entry/zapi:key', false, true); + + assert.equal(keys[0], xpath[0]); + assert.equal(keys[1], xpath[1]); + }); +}); diff --git a/tests/remote_js/test/2/permissionsTest.js b/tests/remote_js/test/2/permissionsTest.js new file mode 100644 index 00000000..1dd1aea2 --- /dev/null +++ b/tests/remote_js/test/2/permissionsTest.js @@ -0,0 +1,246 @@ +const chai = require('chai'); +const assert = chai.assert; +const config = require("../../config.js"); +const API = require('../../api2.js'); +const Helpers = require('../../helpers.js'); +const { API2Setup, API2WrapUp } = require("../shared.js"); +const { resetGroups } = require("../../groupsSetup.js"); + +describe('PermissionsTests', function () { + this.timeout(config.timeout); + + before(async function () { + await API2Setup(); + await resetGroups(); + }); + + after(async function () { + await API2WrapUp(); + }); + + it('testUserGroupsAnonymous', async function () { + const response = await API.get(`users/${config.userID}/groups?content=json`); + Helpers.assertStatusCode(response, 200); + + const xml = API.getXMLFromResponse(response); + const groupIDs = Helpers.xpathEval(xml, '//atom:entry/zapi:groupID', false, true); + assert.include(groupIDs, String(config.ownedPublicGroupID), `Owned public group ID ${config.ownedPublicGroupID} not found`); + assert.include(groupIDs, String(config.ownedPublicNoAnonymousGroupID), `Owned public no-anonymous group ID ${config.ownedPublicNoAnonymousGroupID} not found`); + Helpers.assertTotalResults(response, config.numPublicGroups); + }); + + it('testKeyNoteAccessWriteError', async function() { + this.skip(); //disabled + }); + + it('testUserGroupsOwned', async function () { + const response = await API.get( + "users/" + config.userID + "/groups?content=json" + + "&key=" + config.apiKey + ); + Helpers.assertStatusCode(response, 200); + + Helpers.assertTotalResults(response, config.numOwnedGroups); + Helpers.assertNumResults(response, config.numOwnedGroups); + }); + + it('testTagDeletePermissions', async function () { + await API.userClear(config.userID); + + await API.createItem('book', { + tags: [{ tag: 'A' }] + }, true); + + const libraryVersion = await API.getLibraryVersion(); + + await API.setKeyOption( + config.userID, config.apiKey, 'libraryWrite', 0 + ); + + let response = await API.userDelete( + config.userID, + `tags?tag=A&key=${config.apiKey}`, + ); + Helpers.assertStatusCode(response, 403); + + await API.setKeyOption( + config.userID, config.apiKey, 'libraryWrite', 1 + ); + + response = await API.userDelete( + config.userID, + `tags?tag=A&key=${config.apiKey}`, + JSON.stringify({}), + { 'If-Unmodified-Since-Version': libraryVersion } + ); + Helpers.assertStatusCode(response, 204); + }); + + it("testKeyNoteAccess", async function () { + await API.userClear(config.userID); + + await API.setKeyOption( + config.userID, config.apiKey, 'libraryNotes', 1 + ); + + let keys = []; + let topLevelKeys = []; + let bookKeys = []; + + const makeNoteItem = async (text) => { + const xml = await API.createNoteItem(text, false, true); + const data = await API.parseDataFromAtomEntry(xml); + keys.push(data.key); + topLevelKeys.push(data.key); + }; + + const makeBookItem = async (title) => { + let xml = await API.createItem('book', { title: title }, true); + let data = await API.parseDataFromAtomEntry(xml); + keys.push(data.key); + topLevelKeys.push(data.key); + bookKeys.push(data.key); + return data.key; + }; + + await makeBookItem("A"); + + await makeNoteItem("

B

"); + await makeNoteItem("

C

"); + await makeNoteItem("

D

"); + await makeNoteItem("

E

"); + + const lastKey = await makeBookItem("F"); + + let xml = await API.createNoteItem("

G

", lastKey, true); + let data = await API.parseDataFromAtomEntry(xml); + keys.push(data.key); + + // Create collection and add items to it + let response = await API.userPost( + config.userID, + "collections?key=" + config.apiKey, + JSON.stringify({ + collections: [ + { + name: "Test", + parentCollection: false + } + ] + }), + { "Content-Type": "application/json" } + ); + Helpers.assertStatusCode(response, 200); + let collectionKey = API.getFirstSuccessKeyFromResponse(response); + + response = await API.userPost( + config.userID, + `collections/${collectionKey}/items?key=` + config.apiKey, + topLevelKeys.join(" ") + ); + Helpers.assertStatusCode(response, 204); + + // + // format=atom + // + // Root + response = await API.userGet( + config.userID, "items?key=" + config.apiKey + ); + Helpers.assertNumResults(response, keys.length); + Helpers.assertTotalResults(response, keys.length); + + // Top + response = await API.userGet( + config.userID, "items/top?key=" + config.apiKey + ); + Helpers.assertNumResults(response, topLevelKeys.length); + Helpers.assertTotalResults(response, topLevelKeys.length); + + // Collection + response = await API.userGet( + config.userID, + "collections/" + collectionKey + "/items/top?key=" + config.apiKey + ); + Helpers.assertNumResults(response, topLevelKeys.length); + Helpers.assertTotalResults(response, topLevelKeys.length); + + // + // format=keys + // + // Root + response = await API.userGet( + config.userID, + "items?key=" + config.apiKey + "&format=keys" + ); + Helpers.assertStatusCode(response, 200); + assert.equal(response.data.trim().split("\n").length, keys.length); + + // Top + response = await API.userGet( + config.userID, + "items/top?key=" + config.apiKey + "&format=keys" + ); + Helpers.assertStatusCode(response, 200); + assert.equal(response.data.trim().split("\n").length, topLevelKeys.length); + + // Collection + response = await API.userGet( + config.userID, + "collections/" + collectionKey + "/items/top?key=" + config.apiKey + "&format=keys" + ); + Helpers.assertStatusCode(response, 200); + assert.equal(response.data.trim().split("\n").length, topLevelKeys.length); + + // Remove notes privilege from key + await API.setKeyOption( + config.userID, config.apiKey, 'libraryNotes', 0 + ); + // + // format=atom + // + // totalResults with limit + response = await API.userGet( + config.userID, + "items?key=" + config.apiKey + "&limit=1" + ); + Helpers.assertNumResults(response, 1); + Helpers.assertTotalResults(response, bookKeys.length); + + // And without limit + response = await API.userGet( + config.userID, + "items?key=" + config.apiKey + ); + Helpers.assertNumResults(response, bookKeys.length); + Helpers.assertTotalResults(response, bookKeys.length); + + // Top + response = await API.userGet( + config.userID, + "items/top?key=" + config.apiKey + ); + Helpers.assertNumResults(response, bookKeys.length); + Helpers.assertTotalResults(response, bookKeys.length); + + // Collection + response = await API.userGet( + config.userID, + "collections/" + collectionKey + "/items?key=" + config.apiKey + ); + Helpers.assertNumResults(response, bookKeys.length); + Helpers.assertTotalResults(response, bookKeys.length); + + // + // format=keys + // + response = await API.userGet( + config.userID, + "items?key=" + config.apiKey + "&format=keys" + ); + keys = response.data.trim().split("\n"); + keys.sort(); + bookKeys.sort(); + assert.deepEqual(bookKeys, keys); + }); +}); diff --git a/tests/remote_js/test/2/relationsTest.js b/tests/remote_js/test/2/relationsTest.js new file mode 100644 index 00000000..1b8f7a41 --- /dev/null +++ b/tests/remote_js/test/2/relationsTest.js @@ -0,0 +1,279 @@ +const chai = require('chai'); +const assert = chai.assert; +const config = require("../../config.js"); +const API = require('../../api2.js'); +const Helpers = require('../../helpers.js'); +const { API2Setup, API2WrapUp } = require("../shared.js"); + +describe('RelationsTests', function () { + this.timeout(config.timeout); + + before(async function () { + await API2Setup(); + }); + + after(async function () { + await API2WrapUp(); + }); + + it('testNewItemRelations', async function () { + const relations = { + "owl:sameAs": "http://zotero.org/groups/1/items/AAAAAAAA", + "dc:relation": [ + "http://zotero.org/users/" + config.userID + "/items/AAAAAAAA", + "http://zotero.org/users/" + config.userID + "/items/BBBBBBBB", + ] + }; + const xml = await API.createItem("book", { relations }, true); + const data = API.parseDataFromAtomEntry(xml); + const json = JSON.parse(data.content); + assert.equal(Object.keys(relations).length, Object.keys(json.relations).length); + + for (const [predicate, object] of Object.entries(relations)) { + if (typeof object === "string") { + assert.equal(object, json.relations[predicate]); + } + else { + for (const rel of object) { + assert.include(json.relations[predicate], rel); + } + } + } + }); + + it('testRelatedItemRelations', async function () { + const relations = { + "owl:sameAs": "http://zotero.org/groups/1/items/AAAAAAAA" + }; + + const item1JSON = await API.createItem("book", { relations: relations }, true, 'json'); + const item2JSON = await API.createItem("book", null, this, 'json'); + + const uriPrefix = "http://zotero.org/users/" + config.userID + "/items/"; + const item1URI = uriPrefix + item1JSON.itemKey; + const item2URI = uriPrefix + item2JSON.itemKey; + + // Add item 2 as related item of item 1 + relations["dc:relation"] = item2URI; + item1JSON.relations = relations; + const response = await API.userPut( + config.userID, + "items/" + item1JSON.itemKey + "?key=" + config.apiKey, + JSON.stringify(item1JSON) + ); + Helpers.assertStatusCode(response, 204); + + // Make sure it exists on item 1 + const xml = await API.getItemXML(item1JSON.itemKey); + const data = API.parseDataFromAtomEntry(xml); + const json = JSON.parse(data.content); + assert.equal(Object.keys(relations).length, Object.keys(json.relations).length); + for (const [predicate, object] of Object.entries(relations)) { + assert.equal(object, json.relations[predicate]); + } + + // And item 2, since related items are bidirectional + const xml2 = await API.getItemXML(item2JSON.itemKey); + const data2 = API.parseDataFromAtomEntry(xml2); + const item2JSON2 = JSON.parse(data2.content); + assert.equal(1, Object.keys(item2JSON2.relations).length); + assert.equal(item1URI, item2JSON2.relations["dc:relation"]); + + // Sending item 2's unmodified JSON back up shouldn't cause the item to be updated. + // Even though we're sending a relation that's technically not part of the item, + // when it loads the item it will load the reverse relations too and therefore not + // add a relation that it thinks already exists. + const response2 = await API.userPut( + config.userID, + "items/" + item2JSON.itemKey + "?key=" + config.apiKey, + JSON.stringify(item2JSON2) + ); + Helpers.assertStatusCode(response2, 204); + assert.equal(parseInt(item2JSON2.itemVersion), response2.headers["last-modified-version"][0]); + }); + + it('testRelatedItemRelationsSingleRequest', async function () { + const uriPrefix = "http://zotero.org/users/" + config.userID + "/items/"; + const item1Key = Helpers.uniqueID(); + const item2Key = Helpers.uniqueID(); + const item1URI = uriPrefix + item1Key; + const item2URI = uriPrefix + item2Key; + + const item1JSON = await API.getItemTemplate('book'); + item1JSON.itemKey = item1Key; + item1JSON.itemVersion = 0; + item1JSON.relations['dc:relation'] = item2URI; + const item2JSON = await API.getItemTemplate('book'); + item2JSON.itemKey = item2Key; + item2JSON.itemVersion = 0; + + const response = await API.postItems([item1JSON, item2JSON]); + Helpers.assertStatusCode(response, 200); + + // Make sure it exists on item 1 + const xml = await API.getItemXML(item1JSON.itemKey); + const data = API.parseDataFromAtomEntry(xml); + const parsedJson = JSON.parse(data.content); + + assert.lengthOf(Object.keys(parsedJson.relations), 1); + assert.equal(parsedJson.relations['dc:relation'], item2URI); + + // And item 2, since related items are bidirectional + const xml2 = await API.getItemXML(item2JSON.itemKey); + const data2 = API.parseDataFromAtomEntry(xml2); + const parsedJson2 = JSON.parse(data2.content); + assert.lengthOf(Object.keys(parsedJson2.relations), 1); + assert.equal(parsedJson2.relations['dc:relation'], item1URI); + }); + + it('testInvalidItemRelation', async function () { + let response = await API.createItem('book', { + relations: { + 'foo:unknown': 'http://zotero.org/groups/1/items/AAAAAAAA' + } + }, true, 'response'); + + Helpers.assertStatusForObject(response, 'failed', 0, 400, "Unsupported predicate 'foo:unknown'"); + + response = await API.createItem('book', { + relations: { + 'owl:sameAs': 'Not a URI' + } + }, this, 'response'); + + Helpers.assertStatusForObject(response, 'failed', 0, 400, "'relations' values currently must be Zotero item URIs"); + + response = await API.createItem('book', { + relations: { + 'owl:sameAs': ['Not a URI'] + } + }, this, 'response'); + + Helpers.assertStatusForObject(response, 'failed', 0, 400, "'relations' values currently must be Zotero item URIs"); + }); + + it('testDeleteItemRelation', async function () { + const relations = { + "owl:sameAs": [ + "http://zotero.org/groups/1/items/AAAAAAAA", + "http://zotero.org/groups/1/items/BBBBBBBB" + ], + "dc:relation": "http://zotero.org/users/" + config.userID + + "/items/AAAAAAAA" + }; + + const data = await API.createItem("book", { + relations: relations + }, true, 'data'); + + let json = JSON.parse(data.content); + + // Remove a relation + json.relations['owl:sameAs'] = relations['owl:sameAs'] = relations['owl:sameAs'][0]; + const response = await API.userPut( + config.userID, + "items/" + data.key + "?key=" + config.apiKey, + JSON.stringify(json) + ); + Helpers.assertStatusCode(response, 204); + + // Make sure it's gone + const xml = await API.getItemXML(data.key); + const itemData = await API.parseDataFromAtomEntry(xml); + json = JSON.parse(itemData.content); + assert.equal(Object.keys(relations).length, Object.keys(json.relations).length); + for (const [predicate, object] of Object.entries(relations)) { + assert.deepEqual(object, json.relations[predicate]); + } + + // Delete all + json.relations = {}; + const deleteResponse = await API.userPut( + config.userID, + "items/" + data.key + "?key=" + config.apiKey, + JSON.stringify(json) + ); + Helpers.assertStatusCode(deleteResponse, 204); + + // Make sure they're gone + const xmlAfterDelete = await API.getItemXML(data.key); + const itemDataAfterDelete = await API.parseDataFromAtomEntry(xmlAfterDelete); + const responseDataAfterDelete = JSON.parse(itemDataAfterDelete.content); + assert.lengthOf(Object.keys(responseDataAfterDelete.relations), 0); + }); + + it('testNewCollectionRelations', async function () { + const relationsObj = { + "owl:sameAs": "http://zotero.org/groups/1/collections/AAAAAAAA" + }; + const data = await API.createCollection("Test", + { relations: relationsObj }, true, 'data'); + const json = JSON.parse(data.content); + assert.equal(Object.keys(json.relations).length, Object.keys(relationsObj).length); + for (const [predicate, object] of Object.entries(relationsObj)) { + assert.equal(object, json.relations[predicate]); + } + }); + + it('testInvalidCollectionRelation', async function () { + const json = { + name: "Test", + relations: { + "foo:unknown": "http://zotero.org/groups/1/collections/AAAAAAAA" + } + }; + const response = await API.userPost( + config.userID, + "collections?key=" + config.apiKey, + JSON.stringify({ collections: [json] }) + ); + Helpers.assertStatusForObject(response, 'failed', 0, null, "Unsupported predicate 'foo:unknown'"); + + json.relations = { + "owl:sameAs": "Not a URI" + }; + const response2 = await API.userPost( + config.userID, + "collections?key=" + config.apiKey, + JSON.stringify({ collections: [json] }) + ); + Helpers.assertStatusForObject(response2, 'failed', 0, null, "'relations' values currently must be Zotero collection URIs"); + + json.relations = ["http://zotero.org/groups/1/collections/AAAAAAAA"]; + const response3 = await API.userPost( + config.userID, + "collections?key=" + config.apiKey, + JSON.stringify({ collections: [json] }) + ); + Helpers.assertStatusForObject(response3, 'failed', 0, null, "'relations' property must be an object"); + }); + + it('testDeleteCollectionRelation', async function () { + const relations = { + "owl:sameAs": "http://zotero.org/groups/1/collections/AAAAAAAA" + }; + const data = await API.createCollection("Test", { + relations: relations + }, true, 'data'); + const json = JSON.parse(data.content); + + // Remove all relations + json.relations = {}; + delete relations['owl:sameAs']; + const response = await API.userPut( + config.userID, + `collections/${data.key}?key=${config.apiKey}`, + JSON.stringify(json) + ); + Helpers.assertStatusCode(response, 204); + + // Make sure it's gone + const xml = await API.getCollectionXML(data.key); + const parsedData = API.parseDataFromAtomEntry(xml); + const jsonData = JSON.parse(parsedData.content); + assert.equal(Object.keys(jsonData.relations).length, Object.keys(relations).length); + for (const key in relations) { + assert.equal(jsonData.relations[key], relations[key]); + } + }); +}); diff --git a/tests/remote_js/test/2/searchTest.js b/tests/remote_js/test/2/searchTest.js new file mode 100644 index 00000000..fb64aa3c --- /dev/null +++ b/tests/remote_js/test/2/searchTest.js @@ -0,0 +1,171 @@ +const chai = require('chai'); +const assert = chai.assert; +const config = require("../../config.js"); +const API = require('../../api2.js'); +const Helpers = require('../../helpers.js'); +const { API2Setup, API2WrapUp } = require("../shared.js"); + +describe('SearchTests', function () { + this.timeout(config.timeout); + + before(async function () { + await API2Setup(); + }); + + after(async function () { + await API2WrapUp(); + }); + const testNewSearch = async () => { + let name = "Test Search"; + let conditions = [ + { + condition: "title", + operator: "contains", + value: "test" + }, + { + condition: "noChildren", + operator: "false", + value: "" + }, + { + condition: "fulltextContent/regexp", + operator: "contains", + value: "/test/" + } + ]; + + let xml = await API.createSearch(name, conditions, true); + assert.equal(parseInt(Helpers.xpathEval(xml, '/atom:feed/zapi:totalResults')), 1); + + let data = API.parseDataFromAtomEntry(xml); + let json = JSON.parse(data.content); + assert.equal(name, json.name); + assert.isArray(json.conditions); + assert.equal(conditions.length, json.conditions.length); + for (let i = 0; i < conditions.length; i++) { + for (let key in conditions[i]) { + assert.equal(conditions[i][key], json.conditions[i][key]); + } + } + + return data; + }; + + it('testModifySearch', async function () { + const newSearchData = await testNewSearch(); + let json = JSON.parse(newSearchData.content); + + // Remove one search condition + json.conditions.shift(); + + const name = json.name; + const conditions = json.conditions; + + let response = await API.userPut( + config.userID, + `searches/${newSearchData.key}?key=${config.apiKey}`, + JSON.stringify(json), + { + "Content-Type": "application/json", + "If-Unmodified-Since-Version": newSearchData.version + } + ); + + Helpers.assertStatusCode(response, 204); + + const xml = await API.getSearchXML(newSearchData.key); + const data = API.parseDataFromAtomEntry(xml); + json = JSON.parse(data.content); + + assert.equal(name, json.name); + assert.isArray(json.conditions); + assert.equal(conditions.length, json.conditions.length); + + for (let i = 0; i < conditions.length; i++) { + const condition = conditions[i]; + assert.equal(condition.field, json.conditions[i].field); + assert.equal(condition.operator, json.conditions[i].operator); + assert.equal(condition.value, json.conditions[i].value); + } + }); + + it('testNewSearchNoName', async function () { + const conditions = [ + { + condition: 'title', + operator: 'contains', + value: 'test', + }, + ]; + const headers = { + 'Content-Type': 'application/json', + }; + const response = await API.createSearch('', conditions, headers, 'responsejson'); + Helpers.assertStatusForObject(response, 'failed', 0, 400, 'Search name cannot be empty'); + }); + + it('testNewSearchNoConditions', async function () { + const json = await API.createSearch("Test", [], true, 'responsejson'); + Helpers.assertStatusForObject(json, 'failed', 0, 400, "'conditions' cannot be empty"); + }); + + it('testNewSearchConditionErrors', async function () { + let json = await API.createSearch( + 'Test', + [ + { + operator: 'contains', + value: 'test' + } + ], + true, + 'responsejson' + ); + Helpers.assertStatusForObject(json, 'failed', 0, 400, "'condition' property not provided for search condition"); + + + json = await API.createSearch( + 'Test', + [ + { + condition: '', + operator: 'contains', + value: 'test' + } + ], + true, + 'responsejson' + ); + Helpers.assertStatusForObject(json, 'failed', 0, 400, 'Search condition cannot be empty'); + + + json = await API.createSearch( + 'Test', + [ + { + condition: 'title', + value: 'test' + } + ], + true, + 'responsejson' + ); + Helpers.assertStatusForObject(json, 'failed', 0, 400, "'operator' property not provided for search condition"); + + + json = await API.createSearch( + 'Test', + [ + { + condition: 'title', + operator: '', + value: 'test' + } + ], + true, + 'responsejson' + ); + Helpers.assertStatusForObject(json, 'failed', 0, 400, 'Search operator cannot be empty'); + }); +}); diff --git a/tests/remote_js/test/2/settingsTest.js b/tests/remote_js/test/2/settingsTest.js new file mode 100644 index 00000000..bac4ed87 --- /dev/null +++ b/tests/remote_js/test/2/settingsTest.js @@ -0,0 +1,403 @@ +const chai = require('chai'); +const assert = chai.assert; +const config = require("../../config.js"); +const API = require('../../api2.js'); +const Helpers = require('../../helpers.js'); +const { API2Setup, API2WrapUp } = require("../shared.js"); +const { resetGroups } = require("../../groupsSetup.js"); + +describe('SettingsTests', function () { + this.timeout(config.timeout); + + before(async function () { + await API2Setup(); + await resetGroups(); + }); + + after(async function () { + await API2WrapUp(); + await resetGroups(); + }); + + beforeEach(async function() { + await API.userClear(config.userID); + await API.groupClear(config.ownedPrivateGroupID); + }); + + it('testAddUserSetting', async function () { + const settingKey = "tagColors"; + const value = [ + { + name: "_READ", + color: "#990000" + } + ]; + + const libraryVersion = await API.getLibraryVersion(); + + const json = { + value: value + }; + + // No version + let response = await API.userPut( + config.userID, + `settings/${settingKey}?key=${config.apiKey}`, + JSON.stringify(json), + { "Content-Type": "application/json" } + ); + Helpers.assertStatusCode(response, 428); + + // Version must be 0 for non-existent setting + response = await API.userPut( + config.userID, + `settings/${settingKey}?key=${config.apiKey}`, + JSON.stringify(json), + { + "Content-Type": "application/json", + "If-Unmodified-Since-Version": "1" + } + ); + Helpers.assertStatusCode(response, 412); + + // Create + response = await API.userPut( + config.userID, + `settings/${settingKey}?key=${config.apiKey}`, + JSON.stringify(json), + { + "Content-Type": "application/json", + "If-Unmodified-Since-Version": "0" + } + ); + Helpers.assertStatusCode(response, 204); + + // Multi-object GET + response = await API.userGet( + config.userID, + `settings?key=${config.apiKey}` + ); + Helpers.assertStatusCode(response, 200); + assert.equal(response.headers['content-type'][0], "application/json"); + let jsonResponse = JSON.parse(response.data); + + assert.property(jsonResponse, settingKey); + assert.deepEqual(value, jsonResponse[settingKey].value); + assert.equal(parseInt(libraryVersion) + 1, jsonResponse[settingKey].version); + + // Single-object GET + response = await API.userGet( + config.userID, + `settings/${settingKey}?key=${config.apiKey}` + ); + Helpers.assertStatusCode(response, 200); + assert.equal(response.headers['content-type'][0], "application/json"); + jsonResponse = JSON.parse(response.data); + + assert.deepEqual(value, jsonResponse.value); + assert.equal(parseInt(libraryVersion) + 1, jsonResponse.version); + }); + + it('testAddUserSettingMultiple', async function () { + await API.userClear(config.userID); + const settingKey = 'tagColors'; + const val = [ + { + name: '_READ', + color: '#990000', + }, + ]; + + const libraryVersion = await API.getLibraryVersion(); + + const json = { + [settingKey]: { + value: val + }, + }; + const response = await API.userPost( + config.userID, + `settings?key=${config.apiKey}`, + JSON.stringify(json), + { 'Content-Type': 'application/json' } + ); + Helpers.assertStatusCode(response, 204); + + // Multi-object GET + const multiObjResponse = await API.userGet( + config.userID, + `settings?key=${config.apiKey}` + ); + Helpers.assertStatusCode(multiObjResponse, 200); + + assert.equal(multiObjResponse.headers['content-type'][0], 'application/json'); + const multiObjJson = JSON.parse(multiObjResponse.data); + assert.property(multiObjJson, settingKey); + assert.deepEqual(multiObjJson[settingKey].value, val); + assert.equal(multiObjJson[settingKey].version, parseInt(libraryVersion) + 1); + + // Single-object GET + const singleObjResponse = await API.userGet( + config.userID, + `settings/${settingKey}?key=${config.apiKey}` + ); + + Helpers.assertStatusCode(singleObjResponse, 200); + assert.equal(singleObjResponse.headers['content-type'][0], 'application/json'); + const singleObjJson = JSON.parse(singleObjResponse.data); + assert.exists(singleObjJson); + assert.deepEqual(singleObjJson.value, val); + assert.equal(singleObjJson.version, parseInt(libraryVersion) + 1); + }); + + it('testAddGroupSettingMultiple', async function () { + const settingKey = "tagColors"; + const value = [ + { + name: "_READ", + color: "#990000" + } + ]; + + // TODO: multiple, once more settings are supported + + const groupID = config.ownedPrivateGroupID; + const libraryVersion = await API.getGroupLibraryVersion(groupID); + + const json = { + [settingKey]: { + value: value + } + }; + + const response = await API.groupPost( + groupID, + `settings?key=${config.apiKey}`, + JSON.stringify(json), + { "Content-Type": "application/json" } + ); + + Helpers.assertStatusCode(response, 204); + + // Multi-object GET + const response2 = await API.groupGet( + groupID, + `settings?key=${config.apiKey}` + ); + + Helpers.assertStatusCode(response2, 200); + assert.equal(response2.headers['content-type'][0], "application/json"); + const json2 = JSON.parse(response2.data); + assert.exists(json2); + assert.property(json2, settingKey); + assert.deepEqual(value, json2[settingKey].value); + assert.equal(parseInt(libraryVersion) + 1, json2[settingKey].version); + + // Single-object GET + const response3 = await API.groupGet( + groupID, + `settings/${settingKey}?key=${config.apiKey}` + ); + + Helpers.assertStatusCode(response3, 200); + assert.equal(response3.headers['content-type'][0], "application/json"); + const json3 = JSON.parse(response3.data); + assert.exists(json3); + assert.deepEqual(value, json3.value); + assert.equal(parseInt(libraryVersion) + 1, json3.version); + }); + + it('testAddGroupSettingMultiple', async function () { + const settingKey = "tagColors"; + const value = [ + { + name: "_READ", + color: "#990000" + } + ]; + + // TODO: multiple, once more settings are supported + + const groupID = config.ownedPrivateGroupID; + const libraryVersion = await API.getGroupLibraryVersion(groupID); + + const json = { + [settingKey]: { + value: value + } + }; + + const response = await API.groupPost( + groupID, + `settings?key=${config.apiKey}`, + JSON.stringify(json), + { "Content-Type": "application/json" } + ); + + Helpers.assertStatusCode(response, 204); + + // Multi-object GET + const response2 = await API.groupGet( + groupID, + `settings?key=${config.apiKey}` + ); + + Helpers.assertStatusCode(response2, 200); + assert.equal(response2.headers['content-type'][0], "application/json"); + const json2 = JSON.parse(response2.data); + assert.isNotNull(json2); + assert.property(json2, settingKey); + assert.deepEqual(value, json2[settingKey].value); + assert.equal(parseInt(libraryVersion) + 1, json2[settingKey].version); + + // Single-object GET + const response3 = await API.groupGet( + groupID, + `settings/${settingKey}?key=${config.apiKey}` + ); + + assert.equal(response3.status, 200); + assert.equal(response3.headers['content-type'][0], "application/json"); + const json3 = JSON.parse(response3.data); + assert.deepEqual(value, json3.value); + assert.equal(parseInt(libraryVersion) + 1, json3.version); + }); + + it('testAddGroupSettingMultiple', async function () { + const settingKey = "tagColors"; + const value = [ + { + name: "_READ", + color: "#990000" + } + ]; + + // TODO: multiple, once more settings are supported + + const groupID = config.ownedPrivateGroupID; + const libraryVersion = await API.getGroupLibraryVersion(groupID); + + const json = { + [settingKey]: { + value: value + } + }; + + const response = await API.groupPost( + groupID, + `settings?key=${config.apiKey}`, + JSON.stringify(json), + { "Content-Type": "application/json" } + ); + + Helpers.assertStatusCode(response, 204); + + // Multi-object GET + const response2 = await API.groupGet( + groupID, + `settings?key=${config.apiKey}` + ); + + Helpers.assertStatusCode(response2, 200); + assert.equal(response2.headers['content-type'][0], "application/json"); + const json2 = JSON.parse(response2.data); + assert.property(json2, settingKey); + assert.deepEqual(value, json2[settingKey].value); + assert.equal(parseInt(libraryVersion) + 1, json2[settingKey].version); + + // Single-object GET + const response3 = await API.groupGet( + groupID, + `settings/${settingKey}?key=${config.apiKey}` + ); + + Helpers.assertStatusCode(response3, 200); + assert.equal(response3.headers['content-type'][0], "application/json"); + const json3 = JSON.parse(response3.data); + assert.deepEqual(value, json3.value); + assert.equal(parseInt(libraryVersion) + 1, json3.version); + }); + + it('testDeleteNonexistentSetting', async function () { + const response = await API.userDelete(config.userID, + `settings/nonexistentSetting?key=${config.apiKey}`, + { "If-Unmodified-Since-Version": "0" }); + Helpers.assertStatusCode(response, 404); + }); + + it('testUnsupportedSetting', async function () { + const settingKey = "unsupportedSetting"; + let value = true; + + const json = { + value: value, + version: 0 + }; + + const response = await API.userPut( + config.userID, + `settings/${settingKey}?key=${config.apiKey}`, + JSON.stringify(json), + { "Content-Type": "application/json" } + ); + Helpers.assertStatusCode(response, 400, `Invalid setting '${settingKey}'`); + }); + + it('testUnsupportedSettingMultiple', async function () { + const settingKey = 'unsupportedSetting'; + const json = { + tagColors: { + value: { + name: '_READ', + color: '#990000' + }, + version: 0 + }, + [settingKey]: { + value: false, + version: 0 + } + }; + + const libraryVersion = await API.getLibraryVersion(); + + let response = await API.userPut( + config.userID, + `settings/${settingKey}?key=${config.apiKey}`, + JSON.stringify(json), + { 'Content-Type': 'application/json' } + ); + Helpers.assertStatusCode(response, 400); + + // Valid setting shouldn't exist, and library version should be unchanged + response = await API.userGet( + config.userID, + `settings/${settingKey}?key=${config.apiKey}` + ); + Helpers.assertStatusCode(response, 404); + assert.equal(libraryVersion, await API.getLibraryVersion()); + }); + + it('testOverlongSetting', async function () { + const settingKey = "tagColors"; + const value = [ + { + name: "abcdefghij".repeat(3001), + color: "#990000" + } + ]; + + const json = { + value: value, + version: 0 + }; + + const response = await API.userPut( + config.userID, + `settings/${settingKey}?key=${config.apiKey}`, + JSON.stringify(json), + { "Content-Type": "application/json" } + ); + Helpers.assertStatusCode(response, 400, "'value' cannot be longer than 30000 characters"); + }); +}); diff --git a/tests/remote_js/test/2/sortTest.js b/tests/remote_js/test/2/sortTest.js new file mode 100644 index 00000000..82e80cca --- /dev/null +++ b/tests/remote_js/test/2/sortTest.js @@ -0,0 +1,128 @@ +const chai = require('chai'); +const assert = chai.assert; +const config = require("../../config.js"); +const API = require('../../api2.js'); +const Helpers = require('../../helpers.js'); +const { API2Setup, API2WrapUp } = require("../shared.js"); + +describe('SortTests', function () { + this.timeout(config.timeout); + //let collectionKeys = []; + let itemKeys = []; + let childAttachmentKeys = []; + let childNoteKeys = []; + //let searchKeys = []; + + let titles = ['q', 'c', 'a', 'j', 'e', 'h', 'i']; + let names = ['m', 's', 'a', 'bb', 'ba', '', '']; + let attachmentTitles = ['v', 'x', null, 'a', null]; + let notes = [null, 'aaa', null, null, 'taf']; + + before(async function () { + await API2Setup(); + await setup(); + }); + + after(async function () { + await API2WrapUp(); + }); + + const setup = async () => { + let titleIndex = 0; + for (let i = 0; i < titles.length - 2; i++) { + const key = await API.createItem("book", { + title: titles[titleIndex], + creators: [ + { + creatorType: "author", + name: names[i] + } + ] + }, true, 'key'); + titleIndex += 1; + // Child attachments + if (attachmentTitles[i]) { + childAttachmentKeys.push(await API.createAttachmentItem( + "imported_file", { + title: attachmentTitles[i] + }, key, true, 'key')); + } + // Child notes + if (notes[i]) { + childNoteKeys.push(await API.createNoteItem(notes[i], key, true, 'key')); + } + + itemKeys.push(key); + } + // Top-level attachment + itemKeys.push(await API.createAttachmentItem("imported_file", { + title: titles[titleIndex] + }, false, null, 'key')); + titleIndex += 1; + // Top-level note + itemKeys.push(await API.createNoteItem(titles[titleIndex], false, null, 'key')); + // + // Collections + // + /*for (let i=0; i<5; i++) { + collectionKeys.push(await API.createCollection("Test", false, true, 'key')); + }*/ + + // + // Searches + // + /*for (let i=0; i<5; i++) { + searchKeys.push(await API.createSearch("Test", 'default', null, 'key')); + }*/ + }; + + it('testSortTopItemsTitle', async function () { + let response = await API.userGet( + config.userID, + "items/top?key=" + config.apiKey + "&format=keys&order=title" + ); + Helpers.assertStatusCode(response, 200); + + let keys = response.data.trim().split("\n"); + + let titlesToIndex = {}; + titles.forEach((v, i) => { + titlesToIndex[v] = i; + }); + let titlesSorted = [...titles]; + titlesSorted.sort(); + let correct = {}; + titlesSorted.forEach((title) => { + let index = titlesToIndex[title]; + correct[index] = keys[index]; + }); + correct = Object.keys(correct).map(key => correct[key]); + assert.deepEqual(correct, keys); + }); + + it('testSortTopItemsCreator', async function () { + let response = await API.userGet( + config.userID, + "items/top?key=" + config.apiKey + "&format=keys&order=creator" + ); + Helpers.assertStatusCode(response, 200); + let keys = response.data.trim().split("\n"); + let namesCopy = { ...names }; + let sortFunction = function (a, b) { + if (a === '' && b !== '') return 1; + if (b === '' && a !== '') return -1; + if (a < b) return -1; + if (a > b) return 11; + return 0; + }; + let namesEntries = Object.entries(namesCopy); + namesEntries.sort((a, b) => sortFunction(a[1], b[1])); + assert.equal(Object.keys(namesEntries).length, keys.length); + let correct = {}; + namesEntries.forEach((entry, i) => { + correct[i] = itemKeys[parseInt(entry[0])]; + }); + correct = Object.keys(correct).map(key => correct[key]); + assert.deepEqual(correct, keys); + }); +}); diff --git a/tests/remote_js/test/2/storageAdmin.js b/tests/remote_js/test/2/storageAdmin.js new file mode 100644 index 00000000..134898f2 --- /dev/null +++ b/tests/remote_js/test/2/storageAdmin.js @@ -0,0 +1,50 @@ +const chai = require('chai'); +const assert = chai.assert; +const config = require("../../config.js"); +const API = require('../../api2.js'); +const Helpers = require('../../helpers.js'); +const { API2Setup, API2WrapUp } = require("../shared.js"); + +describe('StorageAdminTests', function () { + this.timeout(config.timeout); + const DEFAULT_QUOTA = 300; + + before(async function () { + await API2Setup(); + await setQuota(0, 0, DEFAULT_QUOTA); + }); + + after(async function () { + await API2WrapUp(); + }); + + const setQuota = async (quota, expiration, expectedQuota) => { + let response = await API.post('users/' + config.userID + '/storageadmin', + `quota=${quota}&expiration=${expiration}`, + { "content-type": 'application/x-www-form-urlencoded' }, + { + username: config.rootUsername, + password: config.rootPassword + }); + Helpers.assertStatusCode(response, 200); + let xml = API.getXMLFromResponse(response); + console.log(xml.documentElement.innerHTML); + let xmlQuota = xml.getElementsByTagName("quota")[0].innerHTML; + assert.equal(xmlQuota, expectedQuota); + if (expiration != 0) { + const xmlExpiration = xml.getElementsByTagName("expiration")[0].innerHTML; + assert.equal(xmlExpiration, expiration); + } + }; + it('test2GB', async function () { + const quota = 2000; + const expiration = Math.floor(Date.now() / 1000) + (86400 * 365); + await setQuota(quota, expiration, quota); + }); + + it('testUnlimited', async function () { + const quota = 'unlimited'; + const expiration = Math.floor(Date.now() / 1000) + (86400 * 365); + await setQuota(quota, expiration, quota); + }); +}); diff --git a/tests/remote_js/test/2/tagTest.js b/tests/remote_js/test/2/tagTest.js new file mode 100644 index 00000000..578c36e8 --- /dev/null +++ b/tests/remote_js/test/2/tagTest.js @@ -0,0 +1,257 @@ +const chai = require('chai'); +const assert = chai.assert; +const config = require("../../config.js"); +const API = require('../../api2.js'); +const Helpers = require('../../helpers.js'); +const { API2Setup, API2WrapUp } = require("../shared.js"); + +describe('TagTests', function () { + this.timeout(config.timeout); + + before(async function () { + await API2Setup(); + API.useAPIVersion(2); + }); + + after(async function () { + await API2WrapUp(); + }); + it('test_empty_tag_should_be_ignored', async function () { + let json = await API.getItemTemplate("book"); + json.tags.push({ tag: "", type: 1 }); + + let response = await API.postItem(json); + Helpers.assertStatusCode(response, 200); + }); + + it('testInvalidTagObject', async function () { + let json = await API.getItemTemplate("book"); + json.tags.push(["invalid"]); + + let headers = { "Content-Type": "application/json" }; + let response = await API.postItem(json, headers); + + Helpers.assertStatusForObject(response, 'failed', 0, 400, "Tag must be an object"); + }); + + it('testTagSearch', async function () { + const tags1 = ["a", "aa", "b"]; + const tags2 = ["b", "c", "cc"]; + + await API.createItem("book", { + tags: tags1.map((tag) => { + return { tag: tag }; + }) + }, true, 'key'); + + await API.createItem("book", { + tags: tags2.map((tag) => { + return { tag: tag }; + }) + }, true, 'key'); + + let response = await API.userGet( + config.userID, + "tags?key=" + config.apiKey + + "&content=json&tag=" + tags1.join("%20||%20"), + { "Content-Type": "application/json" } + ); + Helpers.assertStatusCode(response, 200); + Helpers.assertNumResults(response, tags1.length); + }); + + it('testTagNewer', async function () { + await API.userClear(config.userID); + + // Create items with tags + await API.createItem("book", { + tags: [ + { tag: "a" }, + { tag: "b" } + ] + }, true); + + const version = await API.getLibraryVersion(); + + // 'newer' shouldn't return any results + let response = await API.userGet( + config.userID, + `tags?key=${config.apiKey}&content=json&newer=${version}` + ); + Helpers.assertStatusCode(response, 200); + Helpers.assertNumResults(response, 0); + + // Create another item with tags + await API.createItem("book", { + tags: [ + { tag: "a" }, + { tag: "c" } + ] + }, true); + + // 'newer' should return new tag + response = await API.userGet( + config.userID, + `tags?key=${config.apiKey}&content=json&newer=${version}` + ); + Helpers.assertStatusCode(response, 200); + Helpers.assertNumResults(response, 1); + + assert.isAbove(parseInt(response.headers['last-modified-version']), parseInt(version)); + + const content = await API.getContentFromResponse(response); + const json = JSON.parse(content); + assert.strictEqual(json.tag, 'c'); + assert.strictEqual(json.type, 0); + }); + + it('testMultiTagDelete', async function () { + const tags1 = ["a", "aa", "b"]; + const tags2 = ["b", "c", "cc"]; + const tags3 = ["Foo"]; + + await API.createItem("book", { + tags: tags1.map(tag => ({ tag: tag })) + }, true, 'key'); + + await API.createItem("book", { + tags: tags2.map(tag => ({ tag: tag, type: 1 })) + }, true, 'key'); + + await API.createItem("book", { + tags: tags3.map(tag => ({ tag: tag })) + }, true, 'key'); + + let libraryVersion = await API.getLibraryVersion(); + libraryVersion = parseInt(libraryVersion); + + // Missing version header + let response = await API.userDelete( + config.userID, + `tags?key=${config.apiKey}&content=json&tag=${tags1.concat(tags2).map(tag => encodeURIComponent(tag)).join("%20||%20")}` + ); + Helpers.assertStatusCode(response, 428); + + // Outdated version header + response = await API.userDelete( + config.userID, + `tags?key=${config.apiKey}&content=json&tag=${tags1.concat(tags2).map(tag => encodeURIComponent(tag)).join("%20||%20")}`, + JSON.stringify({}), + { "If-Unmodified-Since-Version": `${libraryVersion - 1}` } + ); + Helpers.assertStatusCode(response, 412); + + // Delete + response = await API.userDelete( + config.userID, + `tags?key=${config.apiKey}&content=json&tag=${tags1.concat(tags2).map(tag => encodeURIComponent(tag)).join("%20||%20")}`, + JSON.stringify({}), + { "If-Unmodified-Since-Version": `${libraryVersion}` } + ); + Helpers.assertStatusCode(response, 204); + + // Make sure they're gone + response = await API.userGet( + config.userID, + `tags?key=${config.apiKey}&content=json&tag=${tags1.concat(tags2, tags3).map(tag => encodeURIComponent(tag)).join("%20||%20")}` + ); + Helpers.assertStatusCode(response, 200); + Helpers.assertNumResults(response, 1); + }); + + it('testTagAddItemVersionChange', async function () { + let data1 = await API.createItem("book", { + tags: [{ + tag: "a" + }, + { + tag: "b" + }] + }, true, 'data'); + let json1 = JSON.parse(data1.content); + //let version1 = data1.version; + + let data2 = await API.createItem("book", { + tags: [{ + tag: "a" + }, + { + tag: "c" + }] + }, true, 'data'); + let json2 = JSON.parse(data2.content); + let version2 = data2.version; + version2 = parseInt(version2); + // Remove tag 'a' from item 1 + json1.tags = [{ + tag: "d" + }, + { + tag: "c" + }]; + + let response = await API.postItem(json1); + Helpers.assertStatusCode(response, 200); + + // Item 1 version should be one greater than last update + let xml1 = await API.getItemXML(json1.itemKey); + data1 = await API.parseDataFromAtomEntry(xml1); + assert.equal(parseInt(data1.version), version2 + 1); + + // Item 2 version shouldn't have changed + let xml2 = await API.getItemXML(json2.itemKey); + data2 = await API.parseDataFromAtomEntry(xml2); + assert.equal(parseInt(data2.version), version2); + }); + + it('testItemTagSearch', async function () { + await API.userClear(config.userID); + + // Create items with tags + let key1 = await API.createItem("book", { + tags: [ + { tag: "a" }, + { tag: "b" } + ] + }, true, 'key'); + + let key2 = await API.createItem("book", { + tags: [ + { tag: "a" }, + { tag: "c" } + ] + }, true, 'key'); + + let checkTags = async function (tagComponent, assertingKeys = []) { + let response = await API.userGet( + config.userID, + `items?key=${config.apiKey}&format=keys&${tagComponent}` + ); + Helpers.assertStatusCode(response, 200); + if (assertingKeys.length != 0) { + let keys = response.data.trim().split("\n"); + + assert.equal(keys.length, assertingKeys.length); + for (let assertingKey of assertingKeys) { + assert.include(keys, assertingKey); + } + } + else { + assert.isEmpty(response.data.trim()); + } + return response; + }; + + // Searches + await checkTags("tag=a", [key2, key1]); + await checkTags("tag=a&tag=c", [key2]); + await checkTags("tag=b&tag=c", []); + await checkTags("tag=b%20||%20c", [key1, key2]); + await checkTags("tag=a%20||%20b%20||%20c", [key1, key2]); + await checkTags("tag=-a"); + await checkTags("tag=-b", [key2]); + await checkTags("tag=b%20||%20c&tag=a", [key1, key2]); + await checkTags("tag=-z", [key1, key2]); + await checkTags("tag=B", [key1]); + }); +}); diff --git a/tests/remote_js/test/2/template.js b/tests/remote_js/test/2/template.js new file mode 100644 index 00000000..f27a25b0 --- /dev/null +++ b/tests/remote_js/test/2/template.js @@ -0,0 +1,18 @@ +const chai = require('chai'); +const assert = chai.assert; +const config = require("../../config.js"); +const API = require('../../api2.js'); +const Helpers = require('../../helpers.js'); +const { API2Setup, API2WrapUp } = require("../shared.js"); + +describe('Tests', function () { + this.timeout(config.timeout); + + before(async function () { + await API2Setup(); + }); + + after(async function () { + await API2WrapUp(); + }); +}); diff --git a/tests/remote_js/test/2/versionTest.js b/tests/remote_js/test/2/versionTest.js new file mode 100644 index 00000000..f1714ff5 --- /dev/null +++ b/tests/remote_js/test/2/versionTest.js @@ -0,0 +1,581 @@ +const chai = require('chai'); +const assert = chai.assert; +const config = require("../../config.js"); +const API = require('../../api2.js'); +const Helpers = require('../../helpers.js'); +const { API2Setup, API2WrapUp } = require("../shared.js"); + +describe('VersionsTests', function () { + this.timeout(config.timeout * 2); + + before(async function () { + await API2Setup(); + }); + + after(async function () { + await API2WrapUp(); + }); + + const _capitalizeFirstLetter = (string) => { + return string.charAt(0).toUpperCase() + string.slice(1); + }; + + const _modifyJSONObject = async (objectType, json) => { + switch (objectType) { + case "collection": + json.name = "New Name " + Helpers.uniqueID(); + return json; + case "item": + json.title = "New Title " + Helpers.uniqueID(); + return json; + case "search": + json.name = "New Name " + Helpers.uniqueID(); + return json; + default: + throw new Error("Unknown object type"); + } + }; + + const _testSingleObjectLastModifiedVersion = async (objectType) => { + const objectTypePlural = API.getPluralObjectType(objectType); + const versionProp = objectType + 'Version'; + let objectKey; + switch (objectType) { + case 'collection': + objectKey = await API.createCollection('Name', false, true, 'key'); + break; + case 'item': + objectKey = await API.createItem( + 'book', + { title: 'Title' }, + true, + 'key' + ); + break; + case 'search': + objectKey = await API.createSearch( + 'Name', + [ + { + condition: 'title', + operator: 'contains', + value: 'test' + } + ], + this, + 'key' + ); + break; + } + let response = await API.userGet( + config.userID, + `${objectTypePlural}/${objectKey}?key=${config.apiKey}&content=json` + ); + + Helpers.assertStatusCode(response, 200); + const objectVersion = response.headers['last-modified-version'][0]; + const xml = API.getXMLFromResponse(response); + const data = API.parseDataFromAtomEntry(xml); + const json = JSON.parse(data.content); + assert.equal(objectVersion, json[versionProp]); + assert.equal(objectVersion, data.version); + response = await API.userGet( + config.userID, + `${objectTypePlural}?key=${config.apiKey}&limit=1` + ); + Helpers.assertStatusCode(response, 200); + const libraryVersion = response.headers['last-modified-version'][0]; + assert.equal(libraryVersion, objectVersion); + _modifyJSONObject(objectType, json); + delete json[versionProp]; + response = await API.userPut( + config.userID, + `${objectTypePlural}/${objectKey}?key=${config.apiKey}`, + JSON.stringify(json), + { 'Content-Type': 'application/json' } + ); + Helpers.assertStatusCode(response, 428); + response = await API.userPut( + config.userID, + `${objectTypePlural}/${objectKey}?key=${config.apiKey}`, + JSON.stringify(json), + { + 'Content-Type': 'application/json', + 'If-Unmodified-Since-Version': objectVersion - 1 + } + ); + Helpers.assertStatusCode(response, 412); + response = await API.userPut( + config.userID, + `${objectTypePlural}/${objectKey}?key=${config.apiKey}`, + JSON.stringify(json), + { + 'Content-Type': 'application/json', + 'If-Unmodified-Since-Version': objectVersion + } + ); + Helpers.assertStatusCode(response, 204); + const newObjectVersion = response.headers['last-modified-version'][0]; + assert.isAbove(parseInt(newObjectVersion), parseInt(objectVersion)); + _modifyJSONObject(objectType, json); + json[versionProp] = newObjectVersion; + response = await API.userPut( + config.userID, + `${objectTypePlural}/${objectKey}?key=${config.apiKey}`, + JSON.stringify(json), + { 'Content-Type': 'application/json' } + ); + Helpers.assertStatusCode(response, 204); + const newObjectVersion2 = response.headers['last-modified-version'][0]; + assert.isAbove(parseInt(newObjectVersion2), parseInt(newObjectVersion)); + response = await API.userGet( + config.userID, + `${objectTypePlural}?key=${config.apiKey}&limit=1` + ); + Helpers.assertStatusCode(response, 200); + const newLibraryVersion = response.headers['last-modified-version'][0]; + assert.equal(parseInt(newObjectVersion2), parseInt(newLibraryVersion)); + await API.createItem('book', { title: 'Title' }, this, 'key'); + response = await API.userGet( + config.userID, + `${objectTypePlural}/${objectKey}?key=${config.apiKey}&limit=1` + ); + Helpers.assertStatusCode(response, 200); + const newObjectVersion3 = response.headers['last-modified-version'][0]; + assert.equal(parseInt(newLibraryVersion), parseInt(newObjectVersion3)); + response = await API.userDelete( + config.userID, + `${objectTypePlural}/${objectKey}?key=${config.apiKey}`, + JSON.stringify({}) + ); + Helpers.assertStatusCode(response, 428); + response = await API.userDelete( + config.userID, + `${objectTypePlural}/${objectKey}?key=${config.apiKey}`, + JSON.stringify({}), + { 'If-Unmodified-Since-Version': objectVersion } + ); + Helpers.assertStatusCode(response, 412); + response = await API.userDelete( + config.userID, + `${objectTypePlural}/${objectKey}?key=${config.apiKey}`, + JSON.stringify({}), + { 'If-Unmodified-Since-Version': newObjectVersion2 } + ); + Helpers.assertStatusCode(response, 204); + }; + + const _testMultiObjectLastModifiedVersion = async (objectType) => { + await API.userClear(config.userID); + const objectTypePlural = API.getPluralObjectType(objectType); + const objectKeyProp = objectType + "Key"; + const objectVersionProp = objectType + "Version"; + + let response = await API.userGet( + config.userID, + `${objectTypePlural}?key=${config.apiKey}&limit=1` + ); + + let version = parseInt(response.headers['last-modified-version'][0]); + assert.isNumber(version); + + let json; + switch (objectType) { + case 'collection': + json = {}; + json.name = "Name"; + break; + + case 'item': + json = await API.getItemTemplate("book"); + json.creators[0].firstName = "Test"; + json.creators[0].lastName = "Test"; + break; + + case 'search': + json = {}; + json.name = "Name"; + json.conditions = []; + json.conditions.push({ + condition: "title", + operator: "contains", + value: "test" + }); + break; + } + + // Outdated library version + const headers1 = { + "Content-Type": "application/json", + "If-Unmodified-Since-Version": version - 1 + }; + response = await API.userPost( + config.userID, + `${objectTypePlural}?key=${config.apiKey}`, + JSON.stringify({ + [objectTypePlural]: [json] + }), + headers1 + ); + + Helpers.assertStatusCode(response, 412); + + // Make sure version didn't change during failure + response = await API.userGet( + config.userID, + `${objectTypePlural}?key=${config.apiKey}&limit=1` + ); + + assert.equal(version, parseInt(response.headers['last-modified-version'][0])); + + // Create a new object, using library timestamp + const headers2 = { + "Content-Type": "application/json", + "If-Unmodified-Since-Version": version + }; + response = await API.userPost( + config.userID, + `${objectTypePlural}?key=${config.apiKey}`, + JSON.stringify({ + [objectTypePlural]: [json] + }), + headers2 + ); + Helpers.assertStatusCode(response, 200); + Helpers.assertStatusForObject(response, 'success', 0); + const version2 = parseInt(response.headers['last-modified-version'][0]); + assert.isNumber(version2); + // Version should be incremented on new object + assert.isAbove(version2, version); + + const objectKey = API.getFirstSuccessKeyFromResponse(response); + + // Check single-object request + response = await API.userGet( + config.userID, + `${objectTypePlural}/${objectKey}?key=${config.apiKey}&content=json` + ); + Helpers.assertStatusCode(response, 200); + + version = parseInt(response.headers['last-modified-version'][0]); + assert.isNumber(version); + assert.equal(version2, version); + + json[objectKeyProp] = objectKey; + // Modify object + switch (objectType) { + case 'collection': + json.name = "New Name"; + break; + + case 'item': + json.title = "New Title"; + break; + + case 'search': + json.name = "New Name"; + break; + } + + delete json[objectVersionProp]; + + // No If-Unmodified-Since-Version or object version property + response = await API.userPost( + config.userID, + `${objectTypePlural}?key=${config.apiKey}`, + JSON.stringify({ + [objectTypePlural]: [json] + }), + { + "Content-Type": "application/json" + } + ); + Helpers.assertStatusForObject(response, 'failed', 0, 428); + + json[objectVersionProp] = version - 1; + + response = await API.userPost( + config.userID, + `${objectTypePlural}?key=${config.apiKey}`, + JSON.stringify({ + [objectTypePlural]: [json] + }), + { + "Content-Type": "application/json", + } + ); + // Outdated object version property + const message = `${_capitalizeFirstLetter(objectType)} has been modified since specified version (expected ${json[objectVersionProp]}, found ${version2})`; + Helpers.assertStatusForObject(response, 'failed', 0, 412, message); + // Modify object, using object version property + json[objectVersionProp] = version; + + response = await API.userPost( + config.userID, + `${objectTypePlural}?key=${config.apiKey}`, + JSON.stringify({ + [objectTypePlural]: [json] + }), + { + "Content-Type": "application/json", + } + ); + Helpers.assertStatusCode(response, 200); + Helpers.assertStatusForObject(response, 'success', 0); + // Version should be incremented on modified object + const version3 = parseInt(response.headers['last-modified-version'][0]); + assert.isNumber(version3); + assert.isAbove(version3, version2); + // Check library version + response = await API.userGet( + config.userID, + `${objectTypePlural}?key=${config.apiKey}` + ); + version = parseInt(response.headers['last-modified-version'][0]); + assert.isNumber(version); + assert.equal(version, version3); + // Check single-object request + response = await API.userGet( + config.userID, + `${objectTypePlural}/${objectKey}?key=${config.apiKey}` + ); + version = parseInt(response.headers['last-modified-version'][0]); + assert.isNumber(version); + assert.equal(version, version3); + }; + + const _testMultiObject304NotModified = async (objectType) => { + const objectTypePlural = API.getPluralObjectType(objectType); + + let response = await API.userGet( + config.userID, + `${objectTypePlural}?key=${config.apiKey}` + ); + + const version = parseInt(response.headers['last-modified-version'][0]); + assert.isNumber(version); + + response = await API.userGet( + config.userID, + `${objectTypePlural}?key=${config.apiKey}`, + { 'If-Modified-Since-Version': version } + ); + Helpers.assertStatusCode(response, 304); + }; + + const _testNewerAndVersionsFormat = async (objectType) => { + const objectTypePlural = API.getPluralObjectType(objectType); + + const xmlArray = []; + + switch (objectType) { + case 'collection': + xmlArray.push(await API.createCollection("Name", false, true)); + xmlArray.push(await API.createCollection("Name", false, true)); + xmlArray.push(await API.createCollection("Name", false, true)); + break; + + case 'item': + xmlArray.push(await API.createItem("book", { + title: "Title" + }, true)); + xmlArray.push(await API.createItem("book", { + title: "Title" + }, true)); + xmlArray.push(await API.createItem("book", { + title: "Title" + }, true)); + break; + + + case 'search': + xmlArray.push(await API.createSearch( + "Name", [{ + condition: "title", + operator: "contains", + value: "test" + }], + true + )); + xmlArray.push(await API.createSearch( + "Name", [{ + condition: "title", + operator: "contains", + value: "test" + }], + true + )); + xmlArray.push(await API.createSearch( + "Name", [{ + condition: "title", + operator: "contains", + value: "test" + }], + true + )); + } + + const objects = []; + while (xmlArray.length > 0) { + const xml = xmlArray.shift(); + const data = await API.parseDataFromAtomEntry(xml); + objects.push({ + key: data.key, + version: data.version + }); + } + + const firstVersion = objects[0].version; + + let response = await API.userGet( + config.userID, + `${objectTypePlural}?key=${config.apiKey}&format=versions&newer=${firstVersion}`, { + "Content-Type": "application/json" + } + ); + Helpers.assertStatusCode(response, 200); + const json = JSON.parse(response.data); + assert.ok(json); + assert.lengthOf(Object.keys(json), 2); + const keys = Object.keys(json); + + assert.equal(objects[2].key, keys.shift()); + assert.equal(objects[2].version, json[objects[2].key]); + assert.equal(objects[1].key, keys.shift()); + assert.equal(objects[1].version, json[objects[1].key]); + }; + + const _testUploadUnmodified = async (objectType) => { + let objectTypePlural = API.getPluralObjectType(objectType); + let xml, version, response, data, json; + + switch (objectType) { + case "collection": + xml = await API.createCollection("Name", false, true); + break; + + case "item": + xml = await API.createItem("book", { title: "Title" }, true); + break; + + case "search": + xml = await API.createSearch("Name", "default", true); + break; + } + + version = parseInt(Helpers.xpathEval(xml, "//atom:entry/zapi:version")); + assert.notEqual(0, version); + + data = API.parseDataFromAtomEntry(xml); + json = JSON.parse(data.content); + + response = await API.userPut( + config.userID, + `${objectTypePlural}/${data.key}?key=${config.apiKey}`, + JSON.stringify(json), + { "Content-Type": "application/json" } + ); + + Helpers.assertStatusCode(response, 204); + assert.equal(version, response.headers["last-modified-version"][0]); + + switch (objectType) { + case "collection": + xml = await API.getCollectionXML(data.key); + break; + + case "item": + xml = await API.getItemXML(data.key); + break; + + case "search": + xml = await API.getSearchXML(data.key); + break; + } + + data = API.parseDataFromAtomEntry(xml); + assert.equal(version, data.version); + }; + + it('testSingleObjectLastModifiedVersion', async function () { + await _testSingleObjectLastModifiedVersion('collection'); + await _testSingleObjectLastModifiedVersion('item'); + await _testSingleObjectLastModifiedVersion('search'); + }); + it('testMultiObjectLastModifiedVersion', async function () { + await _testMultiObjectLastModifiedVersion('collection'); + await _testMultiObjectLastModifiedVersion('item'); + await _testMultiObjectLastModifiedVersion('search'); + }); + + it('testMultiObject304NotModified', async function () { + await _testMultiObject304NotModified('collection'); + await _testMultiObject304NotModified('item'); + await _testMultiObject304NotModified('search'); + await _testMultiObject304NotModified('tag'); + }); + + it('testNewerAndVersionsFormat', async function () { + await _testNewerAndVersionsFormat('collection'); + await _testNewerAndVersionsFormat('item'); + await _testNewerAndVersionsFormat('search'); + }); + + it('testUploadUnmodified', async function () { + await _testUploadUnmodified('collection'); + await _testUploadUnmodified('item'); + await _testUploadUnmodified('search'); + }); + + it('testNewerTags', async function () { + const tags1 = ["a", "aa", "b"]; + const tags2 = ["b", "c", "cc"]; + + const data1 = await API.createItem("book", { + tags: tags1.map((tag) => { + return { tag: tag }; + }) + }, true, 'data'); + + await API.createItem("book", { + tags: tags2.map((tag) => { + return { tag: tag }; + }) + }, true, 'data'); + + // Only newly added tags should be included in newer, + // not previously added tags or tags added to items + let response = await API.userGet( + config.userID, + "tags?key=" + config.apiKey + + "&newer=" + data1.version + ); + Helpers.assertNumResults(response, 2); + + // Deleting an item shouldn't update associated tag versions + response = await API.userDelete( + config.userID, + `items/${data1.key}?key=${config.apiKey}`, + JSON.stringify({}), + { + "If-Unmodified-Since-Version": data1.version + } + ); + Helpers.assertStatusCode(response, 204); + + response = await API.userGet( + config.userID, + "tags?key=" + config.apiKey + + "&newer=" + data1.version + ); + Helpers.assertNumResults(response, 2); + let libraryVersion = response.headers["last-modified-version"][0]; + + response = await API.userGet( + config.userID, + "tags?key=" + config.apiKey + + "&newer=" + libraryVersion + ); + Helpers.assertNumResults(response, 0); + }); +}); diff --git a/tests/remote_js/test/shared.js b/tests/remote_js/test/shared.js new file mode 100644 index 00000000..389fc54c --- /dev/null +++ b/tests/remote_js/test/shared.js @@ -0,0 +1,28 @@ +const config = require("../config.js"); +const API = require('../api2.js'); + +module.exports = { + + API1Setup: async () => { + const credentials = await API.login(); + config.apiKey = credentials.user1.apiKey; + config.user2APIKey = credentials.user2.apiKey; + await API.useAPIVersion(1); + await API.userClear(config.userID); + }, + API1WrapUp: async () => { + await API.userClear(config.userID); + }, + + API2Setup: async () => { + const credentials = await API.login(); + config.apiKey = credentials.user1.apiKey; + config.user2APIKey = credentials.user2.apiKey; + await API.useAPIVersion(2); + await API.setKeyOption(config.userID, config.apiKey, 'libraryNotes', 1); + await API.userClear(config.userID); + }, + API2WrapUp: async () => { + await API.userClear(config.userID); + } +}; From d58206e0a355daab26fe9479d6e759c0dd2ecefb Mon Sep 17 00:00:00 2001 From: Bogdan Abaev Date: Mon, 22 May 2023 09:41:19 -0400 Subject: [PATCH 02/33] helper functions for api v3, annotation, atom and creator tests for v3 --- tests/remote_js/api2.js | 1 + tests/remote_js/api3.js | 813 +++++++++++++++++++++- tests/remote_js/groupsSetup.js | 5 +- tests/remote_js/helpers.js | 41 +- tests/remote_js/helpers3.js | 37 + tests/remote_js/test/2/atomTest.js | 70 +- tests/remote_js/test/3/annotationsTest.js | 560 +++++++++++++++ tests/remote_js/test/3/atomTest.js | 147 ++++ tests/remote_js/test/3/creatorTest.js | 194 ++++++ tests/remote_js/test/{2 => 3}/template.js | 10 +- tests/remote_js/test/shared.js | 15 + 11 files changed, 1828 insertions(+), 65 deletions(-) create mode 100644 tests/remote_js/helpers3.js create mode 100644 tests/remote_js/test/3/annotationsTest.js create mode 100644 tests/remote_js/test/3/atomTest.js create mode 100644 tests/remote_js/test/3/creatorTest.js rename tests/remote_js/test/{2 => 3}/template.js (55%) diff --git a/tests/remote_js/api2.js b/tests/remote_js/api2.js index 3be7e481..7d540054 100644 --- a/tests/remote_js/api2.js +++ b/tests/remote_js/api2.js @@ -578,6 +578,7 @@ class API2 { const version = this.arrayGetFirst(entryXML.getElementsByTagName('zapi:version')); const content = this.arrayGetFirst(entryXML.getElementsByTagName('content')); if (content === null) { + console.log(entryXML.outerHTML); throw new Error("Atom response does not contain "); } diff --git a/tests/remote_js/api3.js b/tests/remote_js/api3.js index c0c394d5..e58d7523 100644 --- a/tests/remote_js/api3.js +++ b/tests/remote_js/api3.js @@ -1,64 +1,95 @@ const HTTP = require("./httpHandler"); const { JSDOM } = require("jsdom"); +const API2 = require("./api2.js"); +const Helpers = require("./helpers"); +const fs = require("fs"); -class API3 { - static config = require("./config"); - - static apiVersion = null; +class API3 extends API2 { + static schemaVersion; + + static apiVersion = 3; - static useAPIVersion(version) { - this.apiVersion = version; - } static async get(url, headers = [], auth = false) { url = this.config.apiURLPrefix + url; if (this.apiVersion) { - headers.push("Zotero-API-Version: " + this.apiVersion); + headers["Zotero-API-Version"] = this.apiVersion; } if (this.schemaVersion) { - headers.push("Zotero-Schema-Version: " + this.schemaVersion); + headers["Zotero-Schema-Version"] = this.schemaVersion; } - if (!auth && this.apiKey) { - headers.push("Authorization: Bearer " + this.apiKey); + if (!auth && this.config.apiKey) { + headers.Authorization = "Bearer " + this.config.apiKey; } let response = await HTTP.get(url, headers, auth); if (this.config.verbose >= 2) { - console.log("\n\n" + response.getBody() + "\n"); + console.log("\n\n" + response.data + "\n"); + } + return response; + } + + static async head(url, headers = [], auth = false) { + url = this.config.apiURLPrefix + url; + if (this.apiVersion) { + headers["Zotero-API-Version"] = this.apiVersion; + } + if (this.schemaVersion) { + headers["Zotero-Schema-Version"] = this.schemaVersion; + } + if (!auth && this.config.apiKey) { + headers.Authorization = "Bearer " + this.config.apiKey; + } + let response = await HTTP.head(url, headers, auth); + if (this.config.verbose >= 2) { + console.log("\n\n" + response.data + "\n"); } return response; } + + static async userHead(userID, suffix, headers = {}, auth = null) { + return this.head(`users/${userID}/${suffix}`, headers, auth); + } + static useSchemaVersion(version) { + this.schemaVersion = version; + } + + static async resetSchemaVersion() { + const schema = JSON.parse(fs.readFileSync("../../htdocs/zotero-schema/schema.json")); + this.schemaVersion = schema; + } + static async delete(url, data, headers = [], auth = false) { url = this.config.apiURLPrefix + url; if (this.apiVersion) { - headers.push("Zotero-API-Version: " + this.apiVersion); + headers["Zotero-API-Version"] = this.apiVersion; } if (this.schemaVersion) { - headers.push("Zotero-Schema-Version: " + this.schemaVersion); + headers["Zotero-Schema-Version"] = this.schemaVersion; } if (!auth && this.config.apiKey) { - headers.push("Authorization: Bearer " + this.config.apiKey); + headers.Authorization = "Bearer " + this.config.apiKey; } let response = await HTTP.delete(url, data, headers, auth); return response; } - + static async post(url, data, headers = [], auth = false) { url = this.config.apiURLPrefix + url; if (this.apiVersion) { - headers.push("Zotero-API-Version: " + this.apiVersion); + headers["Zotero-API-Version"] = this.apiVersion; } if (this.schemaVersion) { - headers.push("Zotero-Schema-Version: " + this.schemaVersion); + headers["Zotero-Schema-Version"] = this.schemaVersion; } if (!auth && this.config.apiKey) { - headers.push("Authorization: Bearer " + this.config.apiKey); + headers.Authorization = "Bearer " + this.config.apiKey; } let response = await HTTP.post(url, data, headers, auth); return response; } - + static async superGet(url, headers = {}) { return this.get(url, headers, { @@ -139,6 +170,748 @@ class API3 { throw new Error("Unexpected response code " + response.status); } } + + static getSearchXML = async (keys, context = null) => { + return this.getObject('search', keys, context, 'atom'); + }; + + static async getContentFromAtomResponse(response, type = null) { + let xml = this.getXMLFromResponse(response); + let content = Helpers.xpathEval(xml, '//atom:entry/atom:content'); + if (!content) { + console.log(xml.asXML()); + throw new Error("Atom response does not contain "); + } + + let subcontent = Helpers.xpathEval(content, '//zapi:subcontent'); + if (subcontent) { + if (!type) { + throw new Error('$type not provided for multi-content response'); + } + let html; + switch (type) { + case 'json': + return JSON.parse(subcontent[0].xpath('//zapi:subcontent[@zapi:type="json"]')[0]); + + case 'html': + html = Helpers.xpathEval(subcontent[0], '//zapi:subcontent[@zapi:type="html"]'); + html.registerXPathNamespace('html', 'http://www.w3.org/1999/xhtml'); + return html; + + default: + throw new Error("Unknown data type '$type'"); + } + } + else { + throw new Error("Unimplemented"); + } + } + + static async groupCreateItem(groupID, itemType, data = [], context = null, returnFormat = 'responseJSON') { + let response = await this.get(`items/new?itemType=${itemType}`); + let json = JSON.parse(await response.data); + + if (data) { + for (let field in data) { + json[field] = data[field]; + } + } + + response = await this.groupPost( + groupID, + `items?key=${this.config.apiKey}`, + JSON.stringify([json]), + { "Content-Type": "application/json" } + ); + return this.handleCreateResponse('item', response, returnFormat, context, groupID); + } + + static async resetKey(key) { + let response; + response = await this.get( + `keys/${key}`, + [], + { + username: `${this.config.rootUsername}`, + password: `${this.config.rootPassword}` + } + ); + if (response.status != 200) { + console.log(response.data); + throw new Error(`GET returned ${response.status}`); + } + let json = this.getJSONFromResponse(response, true); + + + const resetLibrary = (lib) => { + for (const [permission, _] of Object.entries(lib)) { + lib[permission] = false; + } + }; + if (json.access.user) { + resetLibrary(json.access.user); + } + delete json.access.groups; + response = await this.put( + `users/${this.config.userID}/keys/${this.config.apiKey}`, + JSON.stringify(json), + [], + { + username: `${this.config.rootUsername}`, + password: `${this.config.rootPassword}` + } + ); + if (response.status != 200) { + console.log(response.data); + throw new Error(`PUT returned ${response.status}`); + } + } + + static getItemXML = async (keys, context = null) => { + return this.getObject('item', keys, context, 'atom'); + }; + + static async parseLinkHeader(response) { + let header = response.getHeader('Link'); + let links = {}; + header.split(',').forEach(function (val) { + let matches = val.match(/<([^>]+)>; rel="([^"]+)"/); + links[matches[2]] = matches[1]; + }); + return links; + } + + static async getItem(keys, context = null, format = false, groupID = false) { + const mainObject = this || context; + return mainObject.getObject('item', keys, context, format, groupID); + } + + static async createAttachmentItem(linkMode, data = [], parentKey = false, context = false, returnFormat = 'responseJSON') { + let response = await this.get(`items/new?itemType=attachment&linkMode=${linkMode}`); + let json = JSON.parse(response.data); + + for (let key in data) { + json[key] = data[key]; + } + + if (parentKey) { + json.parentItem = parentKey; + } + + response = await this.userPost( + this.config.userID, + `items?key=${this.config.apiKey}`, + JSON.stringify([json]), + { "Content-Type": "application/json" } + ); + + return this.handleCreateResponse('item', response, returnFormat, context); + } + + static async getItemResponse(keys, context = null, format = false, groupID = false) { + const mainObject = this || context; + return mainObject.getObjectResponse('item', keys, context, format, groupID); + } + + static async postObjects(objectType, json) { + let objectTypPlural = this.getPluralObjectType(objectType); + return this.userPost( + this.config.userID, + objectTypPlural, + JSON.stringify(json), + { "Content-Type": "application/json" } + ); + } + + static getXMLFromFirstSuccessItem = async (response) => { + let key = await this.getFirstSuccessKeyFromResponse(response); + await this.getItemXML(key); + }; + + + static async getCollection(keys, context = null, format = false, groupID = false) { + return this.getObject("collection", keys, context, format, groupID); + } + + static async patch(url, data, headers = {}, auth = false) { + let apiUrl = this.config.apiURLPrefix + url; + if (this.apiVersion) { + headers["Zotero-API-Version"] = this.apiVersion; + } + if (this.schemaVersion) { + headers["Zotero-Schema-Version"] = this.schemaVersion; + } + if (!auth && this.config.apiKey) { + headers.Authorization = "Bearer " + this.config.apiKey; + } + let response = await HTTP.patch(apiUrl, data, headers, auth); + return response; + } + + static createDataObject = async (objectType, data = false, context = false, format = 'json') => { + let template = this.createUnsavedDataObject(objectType); + if (data) { + for (let key in data) { + template[key] = data[key]; + } + } + data = template; + let response; + switch (objectType) { + case 'collection': + return this.createCollection("Test", data, context, format); + + case 'item': + return this.createItem("book", data, context, format); + + case 'search': + response = await this.postObjects(objectType, [data]); + return this.handleCreateResponse('search', response, format, context); + } + return null; + }; + + static async put(url, data, headers = [], auth = false) { + url = this.config.apiURLPrefix + url; + if (this.apiVersion) { + headers["Zotero-API-Version"] = this.apiVersion; + } + if (this.schemaVersion) { + headers["Zotero-Schema-Version"] = this.schemaVersion; + } + if (!auth && this.config.apiKey) { + headers.Authorization = "Bearer " + this.config.apiKey; + } + let response = await HTTP.put(url, data, headers, auth); + return response; + } + + static userPut = async (userID, suffix, data, headers = {}, auth = false) => { + return this.put(`users/${userID}/${suffix}`, data, headers, auth); + }; + + static userPatch = async (userID, suffix, data, headers = {}, auth = false) => { + return this.patch(`users/${userID}/${suffix}`, data, headers, auth); + }; + + static groupCreateAttachmentItem = async (groupID, linkMode, data = [], parentKey = false, context = false, returnFormat = 'responseJSON') => { + let response = await this.get(`items/new?itemType=attachment&linkMode=${linkMode}`); + let json = await response.json(); + for (let key in data) { + json[key] = data[key]; + } + if (parentKey) { + json.parentItem = parentKey; + } + + response = await this.groupPost( + groupID, + `items?key=${this.config.apiKey}`, + JSON.stringify([json]), + { "Content-Type": "application/json" } + ); + + return this.handleCreateResponse('item', response, returnFormat, context, groupID); + }; + + static async getFirstSuccessKeyFromResponse(response) { + let json = this.getJSONFromResponse(response); + if (!json.success) { + console.log(response.body); + throw new Error("No success keys found in response"); + } + return json.success.shift(); + } + + static async groupGet(groupID, suffix, headers = {}, auth = false) { + return this.get(`groups/${groupID}/${suffix}`, headers, auth); + } + + static async getCollectionXML(keys, context = null) { + return this.getObject('collection', keys, context, 'atom'); + } + + static async postItems(json) { + return this.postObjects('item', json); + } + + static async groupPut(groupID, suffix, data, headers = {}, auth = false) { + return this.put(`groups/${groupID}/${suffix}`, data, headers, auth); + } + + static async userDelete(userID, suffix, headers = {}, auth = false) { + let url = `users/${userID}/${suffix}`; + return this.delete(url, headers, auth); + } + + static async getSuccessfulKeysFromResponse(response) { + let json = this.getJSONFromResponse(response); + return json.successful.map((o) => { + return o.key; + }); + } + + static async getItemTemplate(itemType) { + let response = await this.get(`items/new?itemType=${itemType}`); + if (response.status != 200) { + console.log(response.status); + console.log(response.data); + throw new Error("Invalid response from template request"); + } + return JSON.parse(response.data); + } + + static async groupPost(groupID, suffix, data, headers = {}, auth = false) { + return this.post(`groups/${groupID}/${suffix}`, data, headers, auth); + } + + static async superPut(url, data, headers) { + let postData = { + username: this.config.rootUsername, + password: this.config.rootPassword + }; + Object.assign(postData, data); + return this.put(url, postData, headers); + } + + static async getSearchResponse(keys, context = null, format = false, groupID = false) { + return this.getObjectResponse('search', keys, context, format, groupID); + } + + // Atom + + static async getSearch(keys, context = null, format = false, groupID = false) { + return this.getObject('search', keys, context, format, groupID); + } + + static async createCollection(name, data = {}, context = null, returnFormat = 'responseJSON') { + let parent, relations; + + if (Array.isArray(data)) { + parent = data.parentCollection ? data.parentCollection : false; + relations = data.relations ? data.relations : {}; + } + else { + parent = data ? data : false; + relations = {}; + } + + let json = [ + { + name: name, + parentCollection: parent, + relations: relations + } + ]; + + if (data.deleted) { + json[0].deleted = data.deleted; + } + + let response = this.postObjects('collection', json); + return this.handleCreateResponse('collection', response, returnFormat, context); + } + + static async setKeyGroupPermission(key, groupID, permission, _) { + let response = await this.get( + "keys/" + key, + [], + { + username: this.config.rootUsername, + password: this.config.rootPassword + } + ); + if (response.status != 200) { + console.log(response.data); + throw new Error("GET returned " + response.status); + } + + let json = this.getJSONFromResponse(response); + if (!json.access) { + json.access = {}; + } + if (!json.access.groups) { + json.access.groups = {}; + } + json.access.groups[groupID][permission] = true; + response = await this.put( + "keys/" + key, + JSON.stringify(json), + [], + { + username: this.config.rootUsername, + password: this.config.rootPassword + } + ); + if (response.status != 200) { + console.log(response.data); + throw new Error("PUT returned " + response.status); + } + } + + static async setKeyOption(userID, key, option, val) { + console.log("setKeyOption() is deprecated -- use setKeyUserPermission()"); + + switch (option) { + case 'libraryNotes': + option = 'notes'; + break; + + case 'libraryWrite': + option = 'write'; + break; + } + + await this.setKeyUserPermission(key, option, val); + } + + + static async createNoteItem(text = "", parentKey = false, context = false, returnFormat = 'responseJSON') { + let response = await this.get("items/new?itemType=note"); + let json = JSON.parse(response.data); + json.note = text; + if (parentKey) { + json.parentItem = parentKey; + } + + response = await this.postObjects('item', [json]); + return this.handleCreateResponse('item', response, returnFormat, context); + } + + static async createItem(itemType, data = {}, context = null, returnFormat = 'responseJSON') { + let json = await this.getItemTemplate(itemType); + + if (data) { + for (let field in data) { + json[field] = data[field]; + } + } + + let headers = { + "Content-Type": "application/json", + "Zotero-API-Key": this.config.apiKey + }; + + let requestBody = JSON.stringify([json]); + + let response = await this.userPost(this.config.userID, "items", requestBody, headers); + + return this.handleCreateResponse('item', response, returnFormat, context); + } + + static async setKeyUserPermission(key, permission, value) { + let response = await this.get( + "keys/" + key, + {}, + { + username: this.config.rootUsername, + password: this.config.rootPassword + } + ); + if (response.status != 200) { + console.log(response.data); + throw new Error("GET returned " + response.status); + } + + if (this.apiVersion >= 3) { + let json = this.getJSONFromResponse(response); + + switch (permission) { + case 'library': + if (json.access.user && value == !json.access.user.library) { + break; + } + json.access.user.library = value; + break; + + case 'write': + if (json.access.user && value == !json.access.user.write) { + break; + } + json.access.user.write = value; + break; + + case 'notes': + if (json.access.user && value == !json.access.user.notes) { + break; + } + json.access.user.notes = value; + break; + } + + response = await this.put( + "keys/" + this.config.apiKey, + JSON.stringify(json), + {}, + { + username: this.config.rootUsername, + password: this.config.rootPassword + } + ); + } + else { + let xml; + try { + xml = this.getXMLFromResponse(response); + } + catch (e) { + console.log(response.data); + throw e; + } + let current; + for (let access of xml.getElementsByTagName("access")) { + switch (permission) { + case 'library': + current = parseInt(access.getAttribute('library')); + if (current != value) { + access.setAttribute('library', parseInt(value)); + } + break; + + case 'write': + if (!access.library) { + continue; + } + current = parseInt(access.getAttribute('write')); + if (current != value) { + access.setAttribute('write', parseInt(value)); + } + break; + + case 'notes': + if (!access.library) { + break; + } + current = parseInt(access.getAttribute('notes')); + if (current != value) { + access.setAttribute('notes', parseInt(value)); + } + break; + } + } + + response = await this.put( + "keys/" + this.config.apiKey, + xml.outterHTML, + {}, + { + username: this.config.rootUsername, + password: this.config.rootPassword + } + ); + } + if (response.status != 200) { + console.log(response.data); + throw new Error("PUT returned " + response.status); + } + } + + static async createAnnotationItem(annotationType, data = [], parentKey, context = false, returnFormat = 'responseJSON') { + let response = await this.get(`items/new?itemType=annotation&annotationType=${annotationType}`); + let json = await response.json(); + json.parentItem = parentKey; + if (annotationType === 'highlight') { + json.annotationText = 'This is highlighted text.'; + } + if (data.annotationComment !== undefined) { + json.annotationComment = data.annotationComment; + } + json.annotationColor = '#ff8c19'; + json.annotationSortIndex = '00015|002431|00000'; + json.annotationPosition = JSON.stringify({ + pageIndex: 123, + rects: [ + [314.4, 412.8, 556.2, 609.6] + ] + }); + + response = await this.postObjects('item', [json]); + return this.handleCreateResponse('item', response, returnFormat, context); + } + + static async createSearch(name, conditions = [], context = null, returnFormat = 'responseJSON') { + if (!conditions || conditions === 'default') { + conditions = [ + { + condition: 'title', + operator: 'contains', + value: 'test', + }, + ]; + } + + const json = [ + { + name, + conditions, + }, + ]; + + let response = await this.postObjects('search', json); + return this.handleCreateResponse('search', response, returnFormat, context); + } + + static async getCollectionResponse(keys, context = null, format = false, groupID = false) { + return this.getObjectResponse('collection', keys, context, format, groupID); + } + + static createUnsavedDataObject = async (objectType) => { + let json; + switch (objectType) { + case "collection": + json = { + name: "Test", + }; + break; + + case "item": + // Convert to array + json = JSON.parse(JSON.stringify(this.getItemTemplate("book"))); + break; + + case "search": + json = { + name: "Test", + conditions: [ + { + condition: "title", + operator: "contains", + value: "test", + }, + ], + }; + break; + } + return json; + }; + + static async handleCreateResponse(objectType, response, returnFormat, context = null, groupID = false) { + let uctype = objectType.charAt(0).toUpperCase() + objectType.slice(1); + + if (context) { + Helpers.assert200(response); + } + + if (returnFormat == 'response') { + return response; + } + + let json = this.getJSONFromResponse(response); + + if (returnFormat != 'responseJSON' && Object.keys(json.success).length != 1) { + console.log(json); + throw new Error(uctype + " creation failed"); + } + + if (returnFormat == 'responseJSON') { + return json; + } + + let key = json.success[0]; + + if (returnFormat == 'key') { + return key; + } + + let asResponse = false; + if (/response$/i.test(returnFormat)) { + returnFormat = returnFormat.substring(0, returnFormat.length - 8); + asResponse = true; + } + let responseFunc; + switch (uctype) { + case 'Item': + responseFunc = asResponse ? this.getItemResponse : this.getItem; + break; + case 'Collection': + responseFunc = asResponse ? this.getCollectionResponse : this.getCollection; + break; + case 'Search': + responseFunc = asResponse ? this.getSearchResponse : this.getSearch; + break; + default: + throw Error("Unknown object type"); + } + + if (returnFormat.substring(0, 4) == 'json') { + response = await responseFunc(key, this, 'json', groupID); + if (returnFormat == 'json' || returnFormat == 'jsonResponse') { + return response; + } + if (returnFormat == 'jsonData') { + return response.data; + } + } + + response = await responseFunc(key, this, 'atom', groupID); + + if (returnFormat == 'atom' || returnFormat == 'atomResponse') { + return response; + } + + let xml = this.getXMLFromResponse(response); + let data = this.parseDataFromAtomEntry(xml); + + if (returnFormat == 'data') { + return data; + } + if (returnFormat == 'content') { + return data.content; + } + if (returnFormat == 'atomJSON') { + return JSON.parse(data.content); + } + + throw new Error("Invalid result format '" + returnFormat + "'"); + } + + static async getObjectResponse(objectType, keys, context = null, format = false, groupID = false) { + let objectTypePlural = this.getPluralObjectType(objectType); + + let single = typeof keys === "string"; + + let url = `${objectTypePlural}`; + if (single) { + url += `/${keys}`; + } + url += `?key=${this.config.apiKey}`; + if (!single) { + url += `&${objectType}Key=${keys.join(',')}&order=${objectType}KeyList`; + } + if (format !== false) { + url += `&format=${format}`; + if (format == 'atom') { + url += '&content=json'; + } + } + let response; + if (groupID) { + response = await this.groupGet(groupID, url); + } + else { + response = await this.userGet(this.config.userID, url); + } + if (context) { + Helpers.assert200(response); + } + return response; + } + + static async getObject(objectType, keys, context = null, format = false, groupID = false) { + let response = await this.getObjectResponse(objectType, keys, context, format, groupID); + let contentType = response.headers['content-type'][0]; + switch (contentType) { + case 'application/json': + return this.getJSONFromResponse(response); + + case 'application/atom+xml': + return this.getXMLFromResponse(response); + + default: + console.log(response.body); + throw new Error(`Unknown content type '${contentType}'`); + } + } } module.exports = API3; diff --git a/tests/remote_js/groupsSetup.js b/tests/remote_js/groupsSetup.js index 691291d1..d7abc3e7 100644 --- a/tests/remote_js/groupsSetup.js +++ b/tests/remote_js/groupsSetup.js @@ -1,7 +1,6 @@ const config = require('./config'); const API3 = require('./api3.js'); -const API2 = require('./api2.js'); const resetGroups = async () => { let resetGroups = true; @@ -9,7 +8,7 @@ const resetGroups = async () => { let response = await API3.superGet( `users/${config.userID}/groups` ); - let groups = await API2.getJSONFromResponse(response); + let groups = await API3.getJSONFromResponse(response); config.ownedPublicGroupID = null; config.ownedPublicNoAnonymousGroupID = null; config.ownedPrivateGroupID = null; @@ -94,7 +93,7 @@ const resetGroups = async () => { for (let group of groups) { if (!toDelete.includes(group.id)) { - await API2.groupClear(group.id); + await API3.groupClear(group.id); } } }; diff --git a/tests/remote_js/helpers.js b/tests/remote_js/helpers.js index 6f2864a0..657c2d0d 100644 --- a/tests/remote_js/helpers.js +++ b/tests/remote_js/helpers.js @@ -4,16 +4,22 @@ const assert = chai.assert; const crypto = require('crypto'); class Helpers { + static isV3 = false; + + static useV3 = () => { + this.isV3 = true; + }; + static uniqueToken = () => { const id = crypto.randomBytes(16).toString("hex"); const hash = crypto.createHash('md5').update(id).digest('hex'); return hash; }; - static uniqueID = () => { + static uniqueID = (count = 8) => { const chars = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Z']; let result = ""; - for (let i = 0; i < 8; i++) { + for (let i = 0; i < count; i++) { result += chars[crypto.randomInt(chars.length)]; } return result; @@ -100,6 +106,37 @@ class Helpers { const totalResults = this.xpathEval(doc.window.document, "//zapi:totalResults", false, true); assert.equal(parseInt(totalResults[0]), expectedResults); }; + + static assertContentType = (response, contentType) => { + assert.include(response.headers['content-type'], contentType.toLowerCase()); + }; + + + //Assert codes + static assert200 = (response) => { + this.assertStatusCode(response, 200); + }; + + static assert204 = (response) => { + this.assertStatusCode(response, 204); + }; + + static assert400 = (response) => { + this.assertStatusCode(response, 400); + }; + + static assert400ForObject = (response, message) => { + this.assertStatusForObject(response, 'failed', 0, 400, message); + }; + + static assert200ForObject = (response) => { + this.assertStatusForObject(response, 'success', 0); + }; + + // Methods to help during conversion + static assertEquals = (one, two) => { + assert.equal(two, one); + }; } module.exports = Helpers; diff --git a/tests/remote_js/helpers3.js b/tests/remote_js/helpers3.js new file mode 100644 index 00000000..447a31c2 --- /dev/null +++ b/tests/remote_js/helpers3.js @@ -0,0 +1,37 @@ +const { JSDOM } = require("jsdom"); +const chai = require('chai'); +const assert = chai.assert; +const Helpers = require('./helpers'); + +class Helpers3 extends Helpers { + static assertTotalResults(response, expectedCount) { + const totalResults = parseInt(response.headers['total-results'][0]); + assert.isNumber(totalResults); + assert.equal(totalResults, expectedCount); + } + + static assertNumResults = (response, expectedResults) => { + const contentType = response.headers['content-type'][0]; + if (contentType == 'application/json') { + const json = JSON.parse(response.data); + assert.lengthOf(Object.keys(json), expectedResults); + } + else if (contentType == 'text/plain') { + const rows = response.data.split("\n").trim(); + assert.lengthOf(rows, expectedResults); + } + else if (contentType == 'application/x-bibtex') { + let matched = response.getBody().match(/^@[a-z]+{/gm); + assert.equal(matched, expectedResults); + } + else if (contentType == 'application/atom+xml') { + const doc = new JSDOM(response.data, { url: "http://localhost/" }); + const entries = this.xpathEval(doc.window.document, "//entry", false, true); + assert.equal(entries.length, expectedResults); + } + else { + throw new Error(`Unknonw content type" ${contentType}`); + } + }; +} +module.exports = Helpers3; diff --git a/tests/remote_js/test/2/atomTest.js b/tests/remote_js/test/2/atomTest.js index 5bebcc2e..513bf11e 100644 --- a/tests/remote_js/test/2/atomTest.js +++ b/tests/remote_js/test/2/atomTest.js @@ -7,43 +7,9 @@ const { API2Setup, API2WrapUp } = require("../shared.js"); describe('CollectionTests', function () { this.timeout(config.timeout); - + let keyObj = {}; before(async function () { await API2Setup(); - }); - - after(async function () { - await API2WrapUp(); - }); - - it('testFeedURIs', async function () { - const userID = config.userID; - - const response = await API.userGet( - userID, - "items?key=" + config.apiKey - ); - Helpers.assertStatusCode(response, 200); - const xml = await API.getXMLFromResponse(response); - const links = Helpers.xpathEval(xml, '/atom:feed/atom:link', true, true); - assert.equal(config.apiURLPrefix + "users/" + userID + "/items", links[0].getAttribute('href')); - - // 'order'/'sort' should stay as-is, not turn into 'sort'/'direction' - const response2 = await API.userGet( - userID, - "items?key=" + config.apiKey + "&order=dateModified&sort=asc" - ); - Helpers.assertStatusCode(response2, 200); - const xml2 = await API.getXMLFromResponse(response2); - const links2 = Helpers.xpathEval(xml2, '/atom:feed/atom:link', true, true); - assert.equal(config.apiURLPrefix + "users/" + userID + "/items?order=dateModified&sort=asc", links2[0].getAttribute('href')); - }); - - - //Requires citation server to run - it('testMultiContent', async function () { - this.skip(); - const keyObj = {}; const item1 = { title: 'Title', creators: [ @@ -92,6 +58,40 @@ describe('CollectionTests', function () { + '{"itemKey":"","itemVersion":0,"itemType":"book","title":"Title 2","creators":[{"creatorType":"author","firstName":"First","lastName":"Last"},{"creatorType":"editor","firstName":"Ed","lastName":"McEditor"}],"abstractNote":"","series":"","seriesNumber":"","volume":"","numberOfVolumes":"","edition":"","place":"","publisher":"","date":"","numPages":"","language":"","ISBN":"","shortTitle":"","url":"","accessDate":"","archive":"","archiveLocation":"","libraryCatalog":"","callNumber":"","rights":"","extra":"","tags":[],"collections":[],"relations":{}}' + ''; keyObj[key2] = itemXml2; + }); + + after(async function () { + await API2WrapUp(); + }); + + it('testFeedURIs', async function () { + const userID = config.userID; + + const response = await API.userGet( + userID, + "items?key=" + config.apiKey + ); + Helpers.assertStatusCode(response, 200); + const xml = await API.getXMLFromResponse(response); + const links = Helpers.xpathEval(xml, '/atom:feed/atom:link', true, true); + assert.equal(config.apiURLPrefix + "users/" + userID + "/items", links[0].getAttribute('href')); + + // 'order'/'sort' should stay as-is, not turn into 'sort'/'direction' + const response2 = await API.userGet( + userID, + "items?key=" + config.apiKey + "&order=dateModified&sort=asc" + ); + Helpers.assertStatusCode(response2, 200); + const xml2 = await API.getXMLFromResponse(response2); + const links2 = Helpers.xpathEval(xml2, '/atom:feed/atom:link', true, true); + assert.equal(config.apiURLPrefix + "users/" + userID + "/items?order=dateModified&sort=asc", links2[0].getAttribute('href')); + }); + + + //Requires citation server to run + it('testMultiContent', async function () { + this.skip(); + const keys = Object.keys(keyObj); const keyStr = keys.join(','); diff --git a/tests/remote_js/test/3/annotationsTest.js b/tests/remote_js/test/3/annotationsTest.js new file mode 100644 index 00000000..c59ca3e5 --- /dev/null +++ b/tests/remote_js/test/3/annotationsTest.js @@ -0,0 +1,560 @@ +const chai = require('chai'); +const assert = chai.assert; +const config = require("../../config.js"); +const API = require('../../api3.js'); +const Helpers = require('../../helpers3.js'); +const { resetGroups } = require('../../groupsSetup.js'); +const { API3Setup, API3WrapUp } = require("../shared.js"); + +describe('AnnotationsTests', function () { + this.timeout(config.timeout); + let attachmentKey, attachmentJSON; + + before(async function () { + await API3Setup(); + await resetGroups(); + await API.groupClear(config.ownedPrivateGroupID); + + let key = await API.createItem("book", {}, null, 'key'); + attachmentJSON = await API.createAttachmentItem( + "imported_url", + { contentType: 'application/pdf' }, + key, + null, + 'jsonData' + ); + + attachmentKey = attachmentJSON.key; + }); + + after(async function () { + await API3WrapUp(); + }); + + + it('test_should_reject_non_empty_annotationText_for_image_annotation', async function () { + let json = { + itemType: 'annotation', + parentItem: attachmentKey, + annotationType: 'image', + annotationText: 'test', + annotationSortIndex: '00015|002431|00000', + annotationPosition: JSON.stringify({ + pageIndex: 123, + rects: [ + [314.4, 412.8, 556.2, 609.6] + ] + }) + }; + let response = await API.userPost( + config.userID, + "items", + JSON.stringify([json]), + { "Content-Type": "application/json" } + ); + Helpers.assert400ForObject(response, "'annotationText' can only be set for highlight annotations"); + }); + + it('test_should_not_allow_changing_annotation_type', async function () { + let json = { + itemType: 'annotation', + parentItem: attachmentKey, + annotationType: 'highlight', + annotationText: 'This is highlighted text.', + annotationSortIndex: '00015|002431|00000', + annotationPosition: JSON.stringify({ + pageIndex: 123, + rects: [ + [314.4, 412.8, 556.2, 609.6] + ] + }) + }; + let response = await API.userPost( + config.userID, + "items", + JSON.stringify([json]), + { 'Content-Type': 'application/json' } + ); + Helpers.assert200ForObject(response); + json = API.getJSONFromResponse(response).successful[0]; + let annotationKey = json.key; + let version = json.version; + + json = { + version: version, + annotationType: 'note' + }; + response = await API.userPatch( + config.userID, + `items/${annotationKey}`, + JSON.stringify(json), + { 'Content-Type': 'application/json' } + ); + Helpers.assert400(response); + }); + + it('test_should_reject_invalid_color_value', async function () { + const json = { + itemType: 'annotation', + parentItem: attachmentKey, + annotationType: 'highlight', + annotationText: '', + annotationSortIndex: '00015|002431|00000', + annotationColor: 'ff8c19', + annotationPosition: JSON.stringify({ + pageIndex: 123, + rects: [ + [314.4, 412.8, 556.2, 609.6], + ], + }), + }; + const response = await API.userPost( + config.userID, + 'items', + JSON.stringify([json]), + { 'Content-Type': 'application/json' } + ); + Helpers.assert400ForObject( + response, + 'annotationColor must be a hex color (e.g., \'#FF0000\')' + ); + }); + + it('test_should_not_include_authorName_if_empty', async function () { + let json = { + itemType: 'annotation', + parentItem: attachmentKey, + annotationType: 'highlight', + annotationText: 'This is highlighted text.', + annotationColor: '#ff8c19', + annotationPageLabel: '10', + annotationSortIndex: '00015|002431|00000', + annotationPosition: JSON.stringify({ + pageIndex: 123, + rects: [ + [314.4, 412.8, 556.2, 609.6] + ] + }) + }; + let response = await API.userPost(config.userID, 'items', JSON.stringify([json]), { 'Content-Type': 'application/json' }); + Helpers.assert200ForObject(response); + let jsonResponse = await API.getJSONFromResponse(response); + let jsonData = jsonResponse.successful[0].data; + assert.notProperty(jsonData, 'annotationAuthorName'); + }); + + it('test_should_use_default_yellow_if_color_not_specified', async function () { + let json = { + itemType: 'annotation', + parentItem: attachmentKey, + annotationType: 'highlight', + annotationText: '', + annotationSortIndex: '00015|002431|00000', + annotationPosition: JSON.stringify({ + pageIndex: 123, + rects: [ + [314.4, 412.8, 556.2, 609.6] + ] + }) + }; + let response = await API.userPost( + config.userID, + "items", + JSON.stringify([json]), + { "Content-Type": "application/json" } + ); + Helpers.assert200ForObject(response); + json = API.getJSONFromResponse(response); + let jsonData = json.successful[0].data; + Helpers.assertEquals('#ffd400', jsonData.annotationColor); + }); + + it('test_should_clear_annotation_fields', async function () { + const json = { + itemType: 'annotation', + parentItem: attachmentKey, + annotationType: 'highlight', + annotationText: 'This is highlighted text.', + annotationComment: 'This is a comment.', + annotationSortIndex: '00015|002431|00000', + annotationPageLabel: "5", + annotationPosition: JSON.stringify({ + pageIndex: 123, + rects: [ + [314.4, 412.8, 556.2, 609.6] + ] + }) + }; + const response = await API.userPost(config.userID, 'items', JSON.stringify([json]), { "Content-Type": "application/json" }); + Helpers.assert200ForObject(response); + const result = await API.getJSONFromResponse(response); + const { key: annotationKey, version } = result.successful[0]; + const patchJson = { + key: annotationKey, + version: version, + annotationComment: '', + annotationPageLabel: '' + }; + const patchResponse = await API.userPatch(config.userID, `items/${annotationKey}`, JSON.stringify(patchJson), { "Content-Type": "application/json" }); + Helpers.assert204(patchResponse); + const itemJson = await API.getItem(annotationKey, this, 'json'); + Helpers.assertEquals('', itemJson.data.annotationComment); + Helpers.assertEquals('', itemJson.data.annotationPageLabel); + }); + + it('test_should_reject_empty_annotationText_for_image_annotation', async function () { + let json = { + itemType: 'annotation', + parentItem: attachmentKey, + annotationType: 'image', + annotationText: '', + annotationSortIndex: '00015|002431|00000', + annotationPosition: JSON.stringify({ + pageIndex: 123, + rects: [ + [314.4, 412.8, 556.2, 609.6] + ] + }) + }; + + let response = await API.userPost( + config.userID, + 'items', + JSON.stringify([json]), + { 'Content-Type': 'application/json' }, + ); + + Helpers.assert400ForObject(response, "'annotationText' can only be set for highlight annotations"); + }); + + it('test_should_save_an_ink_annotation', async function () { + const paths = [ + [173.54, 647.25, 175.88, 647.25, 181.32, 647.25, 184.44, 647.25, 191.44, 647.25, 197.67, 647.25, 203.89, 645.7, 206.23, 645.7, 210.12, 644.92, 216.34, 643.36, 218.68], + [92.4075, 245.284, 92.4075, 245.284, 92.4075, 246.034, 91.6575, 248.284, 91.6575, 253.534, 91.6575, 255.034, 91.6575, 261.034, 91.6575, 263.284, 95.4076, 271.535, 99.9077] + ]; + const json = { + itemType: 'annotation', + parentItem: attachmentKey, + annotationType: 'ink', + annotationColor: '#ff8c19', + annotationPageLabel: '10', + annotationSortIndex: '00015|002431|00000', + annotationPosition: JSON.stringify({ + pageIndex: 123, + paths, + width: 2 + }) + }; + const response = await API.userPost(config.userID, "items", JSON.stringify([json]), { "Content-Type": "application/json" }); + Helpers.assert200ForObject(response); + const jsonResponse = API.getJSONFromResponse(response); + const jsonData = jsonResponse.successful[0].data; + Helpers.assertEquals('annotation', jsonData.itemType); + Helpers.assertEquals('ink', jsonData.annotationType); + Helpers.assertEquals('#ff8c19', jsonData.annotationColor); + Helpers.assertEquals('10', jsonData.annotationPageLabel); + Helpers.assertEquals('00015|002431|00000', jsonData.annotationSortIndex); + const position = JSON.parse(jsonData.annotationPosition); + Helpers.assertEquals(123, position.pageIndex); + assert.deepEqual(paths, position.paths); + }); + + it('test_should_save_a_note_annotation', async function () { + let json = { + itemType: 'annotation', + parentItem: attachmentKey, + annotationType: 'note', + annotationComment: 'This is a comment.', + annotationSortIndex: '00015|002431|00000', + annotationPosition: JSON.stringify({ + pageIndex: 123, + rects: [ + [314.4, 412.8, 556.2, 609.6] + ] + }) + }; + let response = await API.userPost( + config.userID, + "items", + JSON.stringify([json]), + { "Content-Type": "application/json" } + ); + Helpers.assert200ForObject(response); + let jsonResponse = await API.getJSONFromResponse(response); + let jsonData = jsonResponse.successful[0].data; + Helpers.assertEquals('annotation', jsonData.itemType.toString()); + Helpers.assertEquals('note', jsonData.annotationType); + Helpers.assertEquals('This is a comment.', jsonData.annotationComment); + Helpers.assertEquals('00015|002431|00000', jsonData.annotationSortIndex); + let position = JSON.parse(jsonData.annotationPosition); + Helpers.assertEquals(123, position.pageIndex); + assert.deepEqual([[314.4, 412.8, 556.2, 609.6]], position.rects); + assert.notProperty(jsonData, 'annotationText'); + }); + + it('test_should_update_annotation_text', async function () { + const json = { + itemType: 'annotation', + parentItem: attachmentKey, + annotationType: 'highlight', + annotationText: 'This is highlighted text.', + annotationComment: '', + annotationSortIndex: '00015|002431|00000', + annotationPosition: JSON.stringify({ + pageIndex: 123, + rects: [ + [314.4, 412.8, 556.2, 609.6] + ] + }) + }; + const response = await API.userPost( + config.userID, + "items", + JSON.stringify([json]), + { "Content-Type": "application/json" } + ); + Helpers.assert200ForObject(response); + const jsonResponse = API.getJSONFromResponse(response).successful[0]; + const annotationKey = jsonResponse.key; + const version = jsonResponse.version; + + const updateJson = { + key: annotationKey, + version: version, + annotationText: 'New text' + }; + const updateResponse = await API.userPatch( + config.userID, + `items/${annotationKey}`, + JSON.stringify(updateJson), + { "Content-Type": "application/json" } + ); + Helpers.assert204(updateResponse); + + const getItemResponse = await API.getItem(annotationKey, this, 'json'); + const jsonItemText = getItemResponse.data.annotationText; + Helpers.assertEquals('New text', jsonItemText); + }); + + it('test_should_reject_long_position', async function () { + let rects = []; + for (let i = 0; i <= 13000; i++) { + rects.push(i); + } + let positionJSON = JSON.stringify({ + pageIndex: 123, + rects: [rects], + }); + let json = { + itemType: "annotation", + parentItem: attachmentKey, + annotationType: "ink", + annotationSortIndex: "00015|002431|00000", + annotationColor: "#ff8c19", + annotationPosition: positionJSON, + }; + let response = await API.userPost( + config.userID, + "items", + JSON.stringify([json]), + { "Content-Type": "application/json" } + ); + // TEMP: See note in Item.inc.php + //assert413ForObject( + Helpers.assert400ForObject( + // TODO: Restore once output isn't HTML-encoded + //response, "Annotation position '" . mb_substr(positionJSON, 0, 50) . "…' is too long", 0 + response, + "Annotation position is too long for attachment " + attachmentKey, + 0 + ); + }); + + it('test_should_truncate_long_text', async function () { + const json = { + itemType: 'annotation', + parentItem: attachmentKey, + annotationType: 'highlight', + annotationText: '这是一个测试。'.repeat(5000), + annotationSortIndex: '00015|002431|00000', + annotationColor: '#ff8c19', + annotationPosition: JSON.stringify({ + pageIndex: 123, + rects: [ + [314.4, 412.8, 556.2, 609.6] + ] + }) + }; + const response = await API.userPost( + config.userID, + 'items', + JSON.stringify([json]), + { 'Content-Type': 'application/json' } + ); + Helpers.assert200ForObject(response); + const jsonResponse = API.getJSONFromResponse(response); + const jsonData = jsonResponse.successful[0].data; + Helpers.assertEquals(7500, jsonData.annotationText.length); + }); + + it('test_should_update_annotation_comment', async function () { + let json = { + itemType: 'annotation', + parentItem: attachmentKey, + annotationType: 'highlight', + annotationText: 'This is highlighted text.', + annotationComment: '', + annotationSortIndex: '00015|002431|00000', + annotationPosition: JSON.stringify({ + pageIndex: 123, + rects: [ + [314.4, 412.8, 556.2, 609.6] + ] + }) + }; + let response = await API.userPost( + config.userID, + "items", + JSON.stringify([json]), + { "Content-Type": "application/json" } + ); + Helpers.assert200ForObject(response); + json = API.getJSONFromResponse(response).successful[0]; + let annotationKey = json.key, version = json.version; + json = { + key: annotationKey, + version: version, + annotationComment: 'What a highlight!' + }; + response = await API.userPatch( + config.userID, + `items/${annotationKey}`, + JSON.stringify(json), + { "Content-Type": "application/json" } + ); + Helpers.assert204(response); + json = await API.getItem(annotationKey, this, 'json'); + Helpers.assertEquals('What a highlight!', json.data.annotationComment); + }); + + it('test_should_save_a_highlight_annotation', async function () { + let json = { + itemType: 'annotation', + parentItem: attachmentKey, + annotationType: 'highlight', + annotationAuthorName: 'First Last', + annotationText: 'This is highlighted text.', + annotationColor: '#ff8c19', + annotationPageLabel: '10', + annotationSortIndex: '00015|002431|00000', + annotationPosition: JSON.stringify({ + pageIndex: 123, + rects: [ + [314.4, 412.8, 556.2, 609.6] + ] + }) + }; + + let response = await API.userPost( + config.userID, + "items", + JSON.stringify([json]), + { "Content-Type": "application/json" } + ); + Helpers.assert200ForObject(response); + let jsonResponse = await API.getJSONFromResponse(response); + let jsonData = jsonResponse.successful[0].data; + Helpers.assertEquals('annotation', String(jsonData.itemType)); + Helpers.assertEquals('highlight', jsonData.annotationType); + Helpers.assertEquals('First Last', jsonData.annotationAuthorName); + Helpers.assertEquals('This is highlighted text.', jsonData.annotationText); + Helpers.assertEquals('#ff8c19', jsonData.annotationColor); + Helpers.assertEquals('10', jsonData.annotationPageLabel); + Helpers.assertEquals('00015|002431|00000', jsonData.annotationSortIndex); + let position = JSON.parse(jsonData.annotationPosition); + Helpers.assertEquals(123, position.pageIndex); + assert.deepEqual([[314.4, 412.8, 556.2, 609.6]], position.rects); + }); + + it('test_should_save_an_image_annotation', async function () { + // Create annotation + let json = { + itemType: 'annotation', + parentItem: attachmentKey, + annotationType: 'image', + annotationSortIndex: '00015|002431|00000', + annotationPosition: JSON.stringify({ + pageIndex: 123, + rects: [ + [314.4, 412.8, 556.2, 609.6] + ] + }) + }; + let response = await API.userPost( + config.userID, + "items", + JSON.stringify([json]), + { "Content-Type": "application/json" } + ); + Helpers.assert200ForObject(response); + let jsonResponse = await API.getJSONFromResponse(response); + jsonResponse = jsonResponse.successful[0]; + let jsonData = jsonResponse.data; + Helpers.assertEquals('annotation', jsonData.itemType); + Helpers.assertEquals('image', jsonData.annotationType); + Helpers.assertEquals('00015|002431|00000', jsonData.annotationSortIndex); + let position = JSON.parse(jsonData.annotationPosition); + Helpers.assertEquals(123, position.pageIndex); + assert.deepEqual([[314.4, 412.8, 556.2, 609.6]], position.rects); + assert.notProperty(jsonData, 'annotationText'); + + // Image uploading tested in FileTest + }); + + it('test_should_reject_invalid_sortIndex', async function () { + let json = { + itemType: 'annotation', + parentItem: attachmentKey, + annotationType: 'highlight', + annotationText: '', + annotationSortIndex: '0000', + annotationColor: '#ff8c19', + annotationPosition: JSON.stringify({ + pageIndex: 123, + rects: [ + [314.4, 412.8, 556.2, 609.6], + ] + }) + }; + let response = await API.userPost(config.userID, 'items', JSON.stringify([json]), { 'Content-Type': 'application/json' }); + Helpers.assert400ForObject(response, "Invalid sortIndex '0000'", 0); + }); + + it('test_should_reject_long_page_label', async function () { + let label = Helpers.uniqueID(52); + let json = { + itemType: 'annotation', + parentItem: attachmentKey, + annotationType: 'ink', + annotationSortIndex: '00015|002431|00000', + annotationColor: '#ff8c19', + annotationPageLabel: label, + annotationPosition: { + paths: [] + } + }; + let response = await API.userPost( + config.userID, + "items", + JSON.stringify([json]), + { "Content-Type": "application/json" } + ); + // TEMP: See note in Item.inc.php + //Helpers.assert413ForObject( + Helpers.assert400ForObject( + // TODO: Restore once output isn't HTML-encoded + //response, "Annotation page label '" + label.substr(0, 50) + "…' is too long", 0 + response, "Annotation page label is too long for attachment " + attachmentKey, 0 + ); + }); +}); diff --git a/tests/remote_js/test/3/atomTest.js b/tests/remote_js/test/3/atomTest.js new file mode 100644 index 00000000..eb10f78a --- /dev/null +++ b/tests/remote_js/test/3/atomTest.js @@ -0,0 +1,147 @@ +const chai = require('chai'); +const assert = chai.assert; +const config = require("../../config.js"); +const API = require('../../api3.js'); +const Helpers = require('../../helpers3.js'); +const { API3Setup, API3WrapUp } = require("../shared.js"); + +describe('AtomTests', function () { + this.timeout(config.timeout); + let items = []; + + before(async function () { + await API3Setup(); + Helpers.useV3(); + let key = await API.createItem("book", { + title: "Title", + creators: [{ + creatorType: "author", + firstName: "First", + lastName: "Last" + }] + }, false, "key"); + items[key] = '
Last, First. Title, n.d.
' + + '{"key":"","version":0,"itemType":"book","title":"Title","creators":[{"creatorType":"author","firstName":"First","lastName":"Last"}],"abstractNote":"","series":"","seriesNumber":"","volume":"","numberOfVolumes":"","edition":"","place":"","publisher":"","date":"","numPages":"","language":"","ISBN":"","shortTitle":"","url":"","accessDate":"","archive":"","archiveLocation":"","libraryCatalog":"","callNumber":"","rights":"","extra":"","tags":[],"collections":[],"relations":{},"dateAdded":"","dateModified":""}' + + '
'; + key = await API.createItem("book", { + title: "Title 2", + creators: [ + { + creatorType: "author", + firstName: "First", + lastName: "Last" + }, + { + creatorType: "editor", + firstName: "Ed", + lastName: "McEditor" + } + ] + }, false, "key"); + items[key] = '
Last, First. Title 2. Edited by Ed McEditor, n.d.
' + + '{"key":"","version":0,"itemType":"book","title":"Title 2","creators":[{"creatorType":"author","firstName":"First","lastName":"Last"},{"creatorType":"editor","firstName":"Ed","lastName":"McEditor"}],"abstractNote":"","series":"","seriesNumber":"","volume":"","numberOfVolumes":"","edition":"","place":"","publisher":"","date":"","numPages":"","language":"","ISBN":"","shortTitle":"","url":"","accessDate":"","archive":"","archiveLocation":"","libraryCatalog":"","callNumber":"","rights":"","extra":"","tags":[],"collections":[],"relations":{},"dateAdded":"","dateModified":""}' + + '
'; + }); + + after(async function () { + await API3WrapUp(); + }); + + + it('testFeedURIs', async function () { + let userID = config.userID; + + let response = await API.userGet(userID, "items?format=atom"); + Helpers.assert200(response); + let xml = await API.getXMLFromResponse(response); + let links = Helpers.xpathEval(xml, "//atom:feed/atom:link", true, true); + Helpers.assertEquals( + config.apiURLPrefix + "users/" + userID + "/items?format=atom", + links[0].getAttribute("href") + ); + + response = await API.userGet(userID, "items?format=atom&order=dateModified&sort=asc"); + Helpers.assert200(response); + xml = await API.getXMLFromResponse(response); + links = Helpers.xpathEval(xml, "//atom:feed/atom:link", true, true); + Helpers.assertEquals( + config.apiURLPrefix + "users/" + userID + "/items?direction=asc&format=atom&sort=dateModified", + links[0].getAttribute("href") + ); + }); + + //Requires citation server to run + it('testMultiContent', async function () { + this.skip(); + let keys = Object.keys(items); + let keyStr = keys.join(','); + + let response = await API.userGet( + config.userID, + { params: { itemKey: keyStr, content: 'bib,json' } } + ); + Helpers.assert200(response); + let xml = await API.getXMLFromResponse(response); + Helpers.assertTotalResults(response, keys.length); + + let entries = xml.xpath('//atom:entry'); + for (let i = 0; i < entries.length; i++) { + let entry = entries[i]; + let key = entry.children("http://zotero.org/ns/api").key.toString(); + let content = entry.content.asXML(); + + // Add namespace prefix (from ) + content = content.replace(' { await API.userClear(config.userID); + }, + + API3Setup: async () => { + const credentials = await API.login(); + config.apiKey = credentials.user1.apiKey; + config.user2APIKey = credentials.user2.apiKey; + await API3.useAPIVersion(3); + await API3.resetSchemaVersion(); + await API3.setKeyUserPermission(config.apiKey, 'notes', true); + await API3.setKeyUserPermission(config.apiKey, 'write', true); + await API.userClear(config.userID); + }, + API3WrapUp: async () => { + await API.userClear(config.userID); } }; From 0244f16b26438132cfde7434ef6640a77a3fb42c Mon Sep 17 00:00:00 2001 From: Bogdan Abaev Date: Mon, 22 May 2023 16:59:45 -0400 Subject: [PATCH 03/33] v3 collection, group, fullText and note test --- tests/remote_js/api2.js | 8 +- tests/remote_js/api3.js | 86 ++-- tests/remote_js/helpers.js | 34 +- tests/remote_js/httpHandler.js | 4 +- tests/remote_js/test/2/objectTest.js | 13 +- tests/remote_js/test/2/permissionsTest.js | 1 - tests/remote_js/test/2/tagTest.js | 2 - tests/remote_js/test/2/versionTest.js | 6 +- tests/remote_js/test/3/collectionTest.js | 490 +++++++++++++++++++++ tests/remote_js/test/3/fullTextTest.js | 502 ++++++++++++++++++++++ tests/remote_js/test/3/groupTest.js | 280 ++++++++++++ tests/remote_js/test/3/noteTest.js | 153 +++++++ tests/remote_js/test/shared.js | 2 + 13 files changed, 1516 insertions(+), 65 deletions(-) create mode 100644 tests/remote_js/test/3/collectionTest.js create mode 100644 tests/remote_js/test/3/fullTextTest.js create mode 100644 tests/remote_js/test/3/groupTest.js create mode 100644 tests/remote_js/test/3/noteTest.js diff --git a/tests/remote_js/api2.js b/tests/remote_js/api2.js index 7d540054..2253537d 100644 --- a/tests/remote_js/api2.js +++ b/tests/remote_js/api2.js @@ -473,16 +473,16 @@ class API2 { return this.patch(`users/${userID}/${suffix}`, data, headers); } - static async delete(url, data, headers = {}, auth = null) { + static async delete(url, headers = {}, auth = null) { url = this.config.apiURLPrefix + url; if (this.apiVersion) { headers["Zotero-API-Version"] = this.apiVersion; } - return HTTP.delete(url, data, headers, auth); + return HTTP.delete(url, headers, auth); } - static async userDelete(userID, suffix, data, headers = {}) { - return this.delete(`users/${userID}/${suffix}`, data, headers); + static async userDelete(userID, suffix, headers = {}) { + return this.delete(`users/${userID}/${suffix}`, headers); } static async head(url, headers = {}, auth = null) { diff --git a/tests/remote_js/api3.js b/tests/remote_js/api3.js index e58d7523..efa5ce3f 100644 --- a/tests/remote_js/api3.js +++ b/tests/remote_js/api3.js @@ -6,11 +6,16 @@ const fs = require("fs"); class API3 extends API2 { static schemaVersion; - + static apiVersion = 3; + static apiKey = this.config.apiKey; + + static useAPIKey(key) { + this.apiKey = key; + } - static async get(url, headers = [], auth = false) { + static async get(url, headers = {}, auth = false) { url = this.config.apiURLPrefix + url; if (this.apiVersion) { headers["Zotero-API-Version"] = this.apiVersion; @@ -18,8 +23,8 @@ class API3 extends API2 { if (this.schemaVersion) { headers["Zotero-Schema-Version"] = this.schemaVersion; } - if (!auth && this.config.apiKey) { - headers.Authorization = "Bearer " + this.config.apiKey; + if (!auth && this.apiKey) { + headers.Authorization = "Bearer " + this.apiKey; } let response = await HTTP.get(url, headers, auth); if (this.config.verbose >= 2) { @@ -28,7 +33,11 @@ class API3 extends API2 { return response; } - static async head(url, headers = [], auth = false) { + static async userGet(userID, suffix, headers = {}, auth = null) { + return this.get(`users/${userID}/${suffix}`, headers, auth); + } + + static async head(url, headers = {}, auth = false) { url = this.config.apiURLPrefix + url; if (this.apiVersion) { headers["Zotero-API-Version"] = this.apiVersion; @@ -36,8 +45,8 @@ class API3 extends API2 { if (this.schemaVersion) { headers["Zotero-Schema-Version"] = this.schemaVersion; } - if (!auth && this.config.apiKey) { - headers.Authorization = "Bearer " + this.config.apiKey; + if (!auth && this.apiKey) { + headers.Authorization = "Bearer " + this.apiKey; } let response = await HTTP.head(url, headers, auth); if (this.config.verbose >= 2) { @@ -60,7 +69,7 @@ class API3 extends API2 { } - static async delete(url, data, headers = [], auth = false) { + static async delete(url, headers = {}, auth = false) { url = this.config.apiURLPrefix + url; if (this.apiVersion) { headers["Zotero-API-Version"] = this.apiVersion; @@ -68,14 +77,14 @@ class API3 extends API2 { if (this.schemaVersion) { headers["Zotero-Schema-Version"] = this.schemaVersion; } - if (!auth && this.config.apiKey) { - headers.Authorization = "Bearer " + this.config.apiKey; + if (!auth && this.apiKey) { + headers.Authorization = "Bearer " + this.apiKey; } - let response = await HTTP.delete(url, data, headers, auth); + let response = await HTTP.delete(url, headers, auth); return response; } - static async post(url, data, headers = [], auth = false) { + static async post(url, data, headers = {}, auth = false) { url = this.config.apiURLPrefix + url; if (this.apiVersion) { headers["Zotero-API-Version"] = this.apiVersion; @@ -83,8 +92,8 @@ class API3 extends API2 { if (this.schemaVersion) { headers["Zotero-Schema-Version"] = this.schemaVersion; } - if (!auth && this.config.apiKey) { - headers.Authorization = "Bearer " + this.config.apiKey; + if (!auth && this.apiKey) { + headers.Authorization = "Bearer " + this.apiKey; } let response = await HTTP.post(url, data, headers, auth); return response; @@ -106,8 +115,8 @@ class API3 extends API2 { }); } - static async superDelete(url, data, headers = {}) { - return this.delete(url, data, headers, { + static async superDelete(url, headers = {}) { + return this.delete(url, headers, { username: this.config.rootUsername, password: this.config.rootPassword }); @@ -219,7 +228,7 @@ class API3 extends API2 { response = await this.groupPost( groupID, - `items?key=${this.config.apiKey}`, + `items?key=${this.apiKey}`, JSON.stringify([json]), { "Content-Type": "application/json" } ); @@ -253,7 +262,7 @@ class API3 extends API2 { } delete json.access.groups; response = await this.put( - `users/${this.config.userID}/keys/${this.config.apiKey}`, + `users/${this.config.userID}/keys/${this.apiKey}`, JSON.stringify(json), [], { @@ -300,7 +309,7 @@ class API3 extends API2 { response = await this.userPost( this.config.userID, - `items?key=${this.config.apiKey}`, + `items?key=${this.apiKey}`, JSON.stringify([json]), { "Content-Type": "application/json" } ); @@ -330,7 +339,8 @@ class API3 extends API2 { static async getCollection(keys, context = null, format = false, groupID = false) { - return this.getObject("collection", keys, context, format, groupID); + const module = this || context; + return module.getObject("collection", keys, context, format, groupID); } static async patch(url, data, headers = {}, auth = false) { @@ -341,8 +351,8 @@ class API3 extends API2 { if (this.schemaVersion) { headers["Zotero-Schema-Version"] = this.schemaVersion; } - if (!auth && this.config.apiKey) { - headers.Authorization = "Bearer " + this.config.apiKey; + if (!auth && this.apiKey) { + headers.Authorization = "Bearer " + this.apiKey; } let response = await HTTP.patch(apiUrl, data, headers, auth); return response; @@ -371,7 +381,7 @@ class API3 extends API2 { return null; }; - static async put(url, data, headers = [], auth = false) { + static async put(url, data, headers = {}, auth = false) { url = this.config.apiURLPrefix + url; if (this.apiVersion) { headers["Zotero-API-Version"] = this.apiVersion; @@ -379,8 +389,8 @@ class API3 extends API2 { if (this.schemaVersion) { headers["Zotero-Schema-Version"] = this.schemaVersion; } - if (!auth && this.config.apiKey) { - headers.Authorization = "Bearer " + this.config.apiKey; + if (!auth && this.apiKey) { + headers.Authorization = "Bearer " + this.apiKey; } let response = await HTTP.put(url, data, headers, auth); return response; @@ -406,7 +416,7 @@ class API3 extends API2 { response = await this.groupPost( groupID, - `items?key=${this.config.apiKey}`, + `items?key=${this.apiKey}`, JSON.stringify([json]), { "Content-Type": "application/json" } ); @@ -444,10 +454,10 @@ class API3 extends API2 { return this.delete(url, headers, auth); } - static async getSuccessfulKeysFromResponse(response) { + static getSuccessfulKeysFromResponse(response) { let json = this.getJSONFromResponse(response); - return json.successful.map((o) => { - return o.key; + return Object.keys(json.successful).map((o) => { + return json.successful[o].key; }); } @@ -475,19 +485,21 @@ class API3 extends API2 { } static async getSearchResponse(keys, context = null, format = false, groupID = false) { - return this.getObjectResponse('search', keys, context, format, groupID); + const module = this || context; + return module.getObjectResponse('search', keys, context, format, groupID); } // Atom static async getSearch(keys, context = null, format = false, groupID = false) { - return this.getObject('search', keys, context, format, groupID); + const module = this || context; + return module.getObject('search', keys, context, format, groupID); } static async createCollection(name, data = {}, context = null, returnFormat = 'responseJSON') { let parent, relations; - if (Array.isArray(data)) { + if (typeof data == 'object') { parent = data.parentCollection ? data.parentCollection : false; relations = data.relations ? data.relations : {}; } @@ -508,7 +520,7 @@ class API3 extends API2 { json[0].deleted = data.deleted; } - let response = this.postObjects('collection', json); + let response = await this.postObjects('collection', json); return this.handleCreateResponse('collection', response, returnFormat, context); } @@ -589,7 +601,7 @@ class API3 extends API2 { let headers = { "Content-Type": "application/json", - "Zotero-API-Key": this.config.apiKey + "Zotero-API-Key": this.apiKey }; let requestBody = JSON.stringify([json]); @@ -640,7 +652,7 @@ class API3 extends API2 { } response = await this.put( - "keys/" + this.config.apiKey, + "keys/" + this.apiKey, JSON.stringify(json), {}, { @@ -691,7 +703,7 @@ class API3 extends API2 { } response = await this.put( - "keys/" + this.config.apiKey, + "keys/" + this.apiKey, xml.outterHTML, {}, { @@ -874,7 +886,7 @@ class API3 extends API2 { if (single) { url += `/${keys}`; } - url += `?key=${this.config.apiKey}`; + url += `?key=${this.apiKey}`; if (!single) { url += `&${objectType}Key=${keys.join(',')}&order=${objectType}KeyList`; } diff --git a/tests/remote_js/helpers.js b/tests/remote_js/helpers.js index 657c2d0d..3234f9e2 100644 --- a/tests/remote_js/helpers.js +++ b/tests/remote_js/helpers.js @@ -108,7 +108,7 @@ class Helpers { }; static assertContentType = (response, contentType) => { - assert.include(response.headers['content-type'], contentType.toLowerCase()); + assert.include(response?.headers['content-type'], contentType.toLowerCase()); }; @@ -125,18 +125,42 @@ class Helpers { this.assertStatusCode(response, 400); }; - static assert400ForObject = (response, message) => { - this.assertStatusForObject(response, 'failed', 0, 400, message); + static assert403 = (response) => { + this.assertStatusCode(response, 403); }; - static assert200ForObject = (response) => { - this.assertStatusForObject(response, 'success', 0); + static assert428 = (response) => { + this.assertStatusCode(response, 428); + }; + + static assert404 = (response) => { + this.assertStatusCode(response, 404); + }; + + static assert400ForObject = (response, { index = 0, message = null } = {}) => { + this.assertStatusForObject(response, 'failed', index, 400, message); + }; + + static assert200ForObject = (response, { index = 0, message = null } = {}) => { + this.assertStatusForObject(response, 'success', index, message); + }; + + static assert409ForObject = (response, { index = 0, message = null } = {}) => { + this.assertStatusForObject(response, 'failed', index, 409, message); + }; + + static assert413ForObject = (response, { index = 0, message = null } = {}) => { + this.assertStatusForObject(response, 'failed', index, 413, message); }; // Methods to help during conversion static assertEquals = (one, two) => { assert.equal(two, one); }; + + static assertCount = (count, object) => { + assert.lengthOf(Object.keys(object), count); + }; } module.exports = Helpers; diff --git a/tests/remote_js/httpHandler.js b/tests/remote_js/httpHandler.js index e729c6ff..09a2da82 100644 --- a/tests/remote_js/httpHandler.js +++ b/tests/remote_js/httpHandler.js @@ -60,8 +60,8 @@ class HTTP { return this.request('OPTIONS', url, headers, {}, auth); } - static delete(url, data = {}, headers = {}, auth = false) { - return this.request('DELETE', url, headers, data, auth); + static delete(url, headers = {}, auth = false) { + return this.request('DELETE', url, headers, "", auth); } } diff --git a/tests/remote_js/test/2/objectTest.js b/tests/remote_js/test/2/objectTest.js index c591b718..ffd1cd85 100644 --- a/tests/remote_js/test/2/objectTest.js +++ b/tests/remote_js/test/2/objectTest.js @@ -80,7 +80,6 @@ describe('ObjectTests', function () { const responseDelete = await API.userDelete( config.userID, `${objectTypePlural}/${objectKey}?key=${config.apiKey}`, - JSON.stringify({}), { 'If-Unmodified-Since-Version': objectVersion } ); Helpers.assertStatusCode(responseDelete, 204); @@ -126,7 +125,6 @@ describe('ObjectTests', function () { response = await API.userDelete(config.userID, `${objectTypePlural}?key=${config.apiKey}&${keyProp}=${deleteKeys.join(',')}`, - JSON.stringify({}), { "If-Unmodified-Since-Version": libraryVersion } ); Helpers.assertStatusCode(response, 204); @@ -139,7 +137,6 @@ describe('ObjectTests', function () { response = await API.userDelete(config.userID, `${objectTypePlural}?key=${config.apiKey}&${keyProp}=${keepKeys.join(',')},`, - JSON.stringify({}), { "If-Unmodified-Since-Version": libraryVersion }); Helpers.assertStatusCode(response, 204); @@ -153,19 +150,19 @@ describe('ObjectTests', function () { let conditions = []; const objectType = 'collection'; let json1 = { name: "Test" }; - let json2 = { name: "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123" }; + let json2 = { name: "1234567890".repeat(6554) }; let json3 = { name: "Test" }; switch (objectType) { case 'collection': json1 = { name: "Test" }; - json2 = { name: "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123" }; + json2 = { name: "1234567890".repeat(6554) }; json3 = { name: "Test" }; break; case 'item': json1 = await API.getItemTemplate('book'); json2 = { ...json1 }; json3 = { ...json1 }; - json2.title = "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123"; + json2.title = "1234567890".repeat(6554); break; case 'search': conditions = [ @@ -176,7 +173,7 @@ describe('ObjectTests', function () { } ]; json1 = { name: "Test", conditions: conditions }; - json2 = { name: "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123", conditions: conditions }; + json2 = { name: "1234567890".repeat(6554), conditions: conditions }; json3 = { name: "Test", conditions: conditions }; break; } @@ -377,7 +374,6 @@ describe('ObjectTests', function () { const objectTypePlural = await API.getPluralObjectType(objectType); const response = await API.userDelete(config.userID, `${objectTypePlural}?key=${config.apiKey}${url}`, - JSON.stringify({}), { "If-Unmodified-Since-Version": libraryVersion }); Helpers.assertStatusCode(response, 204); return response.headers["last-modified-version"][0]; @@ -437,7 +433,6 @@ describe('ObjectTests', function () { response = await API.userDelete( config.userID, `tags?key=${config.apiKey}&tag=${objectKeys.tag.join('%20||%20')}`, - JSON.stringify({}), { "If-Unmodified-Since-Version": libraryVersion3 } ); Helpers.assertStatusCode(response, 204); diff --git a/tests/remote_js/test/2/permissionsTest.js b/tests/remote_js/test/2/permissionsTest.js index 1dd1aea2..d26324c0 100644 --- a/tests/remote_js/test/2/permissionsTest.js +++ b/tests/remote_js/test/2/permissionsTest.js @@ -70,7 +70,6 @@ describe('PermissionsTests', function () { response = await API.userDelete( config.userID, `tags?tag=A&key=${config.apiKey}`, - JSON.stringify({}), { 'If-Unmodified-Since-Version': libraryVersion } ); Helpers.assertStatusCode(response, 204); diff --git a/tests/remote_js/test/2/tagTest.js b/tests/remote_js/test/2/tagTest.js index 578c36e8..d625ceff 100644 --- a/tests/remote_js/test/2/tagTest.js +++ b/tests/remote_js/test/2/tagTest.js @@ -136,7 +136,6 @@ describe('TagTests', function () { response = await API.userDelete( config.userID, `tags?key=${config.apiKey}&content=json&tag=${tags1.concat(tags2).map(tag => encodeURIComponent(tag)).join("%20||%20")}`, - JSON.stringify({}), { "If-Unmodified-Since-Version": `${libraryVersion - 1}` } ); Helpers.assertStatusCode(response, 412); @@ -145,7 +144,6 @@ describe('TagTests', function () { response = await API.userDelete( config.userID, `tags?key=${config.apiKey}&content=json&tag=${tags1.concat(tags2).map(tag => encodeURIComponent(tag)).join("%20||%20")}`, - JSON.stringify({}), { "If-Unmodified-Since-Version": `${libraryVersion}` } ); Helpers.assertStatusCode(response, 204); diff --git a/tests/remote_js/test/2/versionTest.js b/tests/remote_js/test/2/versionTest.js index f1714ff5..8f6eacae 100644 --- a/tests/remote_js/test/2/versionTest.js +++ b/tests/remote_js/test/2/versionTest.js @@ -145,21 +145,18 @@ describe('VersionsTests', function () { assert.equal(parseInt(newLibraryVersion), parseInt(newObjectVersion3)); response = await API.userDelete( config.userID, - `${objectTypePlural}/${objectKey}?key=${config.apiKey}`, - JSON.stringify({}) + `${objectTypePlural}/${objectKey}?key=${config.apiKey}` ); Helpers.assertStatusCode(response, 428); response = await API.userDelete( config.userID, `${objectTypePlural}/${objectKey}?key=${config.apiKey}`, - JSON.stringify({}), { 'If-Unmodified-Since-Version': objectVersion } ); Helpers.assertStatusCode(response, 412); response = await API.userDelete( config.userID, `${objectTypePlural}/${objectKey}?key=${config.apiKey}`, - JSON.stringify({}), { 'If-Unmodified-Since-Version': newObjectVersion2 } ); Helpers.assertStatusCode(response, 204); @@ -556,7 +553,6 @@ describe('VersionsTests', function () { response = await API.userDelete( config.userID, `items/${data1.key}?key=${config.apiKey}`, - JSON.stringify({}), { "If-Unmodified-Since-Version": data1.version } diff --git a/tests/remote_js/test/3/collectionTest.js b/tests/remote_js/test/3/collectionTest.js new file mode 100644 index 00000000..ea7fa05f --- /dev/null +++ b/tests/remote_js/test/3/collectionTest.js @@ -0,0 +1,490 @@ +const chai = require('chai'); +const assert = chai.assert; +const config = require("../../config.js"); +const API = require('../../api3.js'); +const Helpers = require('../../helpers3.js'); +const { API3Setup, API3WrapUp } = require("../shared.js"); + +describe('CollectionTests', function () { + this.timeout(config.timeout); + + before(async function () { + await API3Setup(); + }); + + after(async function () { + await API3WrapUp(); + }); + + this.beforeEach(async function () { + await API.userClear(config.userID); + }); + + const testNewCollection = async () => { + const name = "Test Collection"; + const json = await API.createCollection(name, false, true, 'json'); + assert.equal(json.data.name, name); + return json.key; + }; + + + it('testNewSubcollection', async function () { + let parent = await testNewCollection(); + let name = "Test Subcollection"; + + let json = await API.createCollection(name, parent, this, 'json'); + Helpers.assertEquals(name, json.data.name); + Helpers.assertEquals(parent, json.data.parentCollection); + + let response = await API.userGet( + config.userID, + "collections/" + parent + ); + Helpers.assert200(response); + let jsonResponse = await API.getJSONFromResponse(response); + Helpers.assertEquals(jsonResponse.meta.numCollections, 1); + }); + + it('test_should_delete_collection_with_14_levels_below_it', async function () { + let json = await API.createCollection("0", false, this, 'json'); + let topCollectionKey = json.key; + let parentCollectionKey = topCollectionKey; + for (let i = 0; i < 14; i++) { + json = await API.createCollection(`${i}`, parentCollectionKey, this, 'json'); + parentCollectionKey = json.key; + } + const response = await API.userDelete( + config.userID, + "collections?collectionKey=" + topCollectionKey, + { + "If-Unmodified-Since-Version": `${json.version}` + } + ); + Helpers.assert204(response); + }); + + it('testCollectionChildItemError', async function () { + let collectionKey = await API.createCollection('Test', false, this, 'key'); + + let key = await API.createItem("book", [], this, 'key'); + let json = await API.createNoteItem("Test Note", key, this, 'jsonData'); + json.collections = [collectionKey]; + + let response = await API.userPut( + config.userID, + `items/${json.key}`, + JSON.stringify(json), + { "Content-Type": "application/json" } + ); + Helpers.assert400(response); + Helpers.assertEquals("Child items cannot be assigned to collections", response.data); + }); + + it('test_should_convert_child_attachent_with_embedded_note_in_collection_to_standalone_attachment_while_changing_note', async function () { + let collectionKey = await API.createCollection('Test', false, this, 'key'); + let key = await API.createItem("book", { collections: [collectionKey] }, this, 'key'); + let json = await API.createAttachmentItem("linked_url", { note: "Foo" }, key, this, 'jsonData'); + json = { + key: json.key, + version: json.version, + note: "", + collections: [collectionKey], + parentItem: false + }; + let response = await API.userPost( + config.userID, + "items", + JSON.stringify([json]), + { + "Content-Type": "application/json" + } + ); + Helpers.assert200ForObject(response); + json = API.getJSONFromResponse(response).successful[0]; + assert.equal(json.data.note, ""); + assert.deepEqual([collectionKey], json.data.collections); + }); + + it('testCollectionItems', async function () { + let collectionKey = await API.createCollection('Test', false, this, 'key'); + + let json = await API.createItem("book", { collections: [collectionKey] }, this, 'jsonData'); + let itemKey1 = json.key; + assert.deepEqual([collectionKey], json.collections); + + json = await API.createItem("journalArticle", { collections: [collectionKey] }, this, 'jsonData'); + let itemKey2 = json.key; + assert.deepEqual([collectionKey], json.collections); + + let childItemKey1 = await API.createAttachmentItem("linked_url", {}, itemKey1, this, 'key'); + let childItemKey2 = await API.createAttachmentItem("linked_url", {}, itemKey2, this, 'key'); + + let response = await API.userGet( + API.config.userID, + `collections/${collectionKey}/items?format=keys` + ); + Helpers.assert200(response); + let keys = response.data.trim().split("\n"); + assert.lengthOf(keys, 4); + assert.include(keys, itemKey1); + assert.include(keys, itemKey2); + assert.include(keys, childItemKey1); + assert.include(keys, childItemKey2); + + response = await API.userGet( + API.config.userID, + `collections/${collectionKey}/items/top?format=keys` + ); + Helpers.assert200(response); + keys = response.data.trim().split("\n"); + assert.lengthOf(keys, 2); + assert.include(keys, itemKey1); + assert.include(keys, itemKey2); + }); + + + it('test_should_allow_emoji_in_name', async function () { + let name = "🐶"; + let json = await API.createCollection(name, false, this, 'json'); + assert.equal(name, json.data.name); + }); + + it('testCreateKeyedCollections', async function () { + let key1 = Helpers.uniqueID(); + let name1 = "Test Collection 2"; + let name2 = "Test Subcollection"; + + let json = [ + { + key: key1, + version: 0, + name: name1 + }, + { + name: name2, + parentCollection: key1 + } + ]; + + let response = await API.userPost( + config.userID, + "collections", + JSON.stringify(json), + { "Content-Type": "application/json" } + ); + Helpers.assert200(response); + let libraryVersion = response.headers['last-modified-version'][0]; + json = API.getJSONFromResponse(response); + assert.lengthOf(Object.keys(json.successful), 2); + + // Check data in write response + Helpers.assertEquals(json.successful[0].key, json.successful[0].data.key); + Helpers.assertEquals(json.successful[1].key, json.successful[1].data.key); + Helpers.assertEquals(libraryVersion, json.successful[0].version); + Helpers.assertEquals(libraryVersion, json.successful[1].version); + Helpers.assertEquals(libraryVersion, json.successful[0].data.version); + Helpers.assertEquals(libraryVersion, json.successful[1].data.version); + Helpers.assertEquals(name1, json.successful[0].data.name); + Helpers.assertEquals(name2, json.successful[1].data.name); + assert.notOk(json.successful[0].data.parentCollection); + Helpers.assertEquals(key1, json.successful[1].data.parentCollection); + + // Check in separate request, to be safe + let keys = Object.keys(json.successful).map(k => json.successful[k].key); + response = await API.getCollectionResponse(keys); + Helpers.assertTotalResults(response, 2); + json = API.getJSONFromResponse(response); + Helpers.assertEquals(name1, json[0].data.name); + assert.notOk(json[0].data.parentCollection); + Helpers.assertEquals(name2, json[1].data.name); + Helpers.assertEquals(key1, json[1].data.parentCollection); + }); + + it('testUpdateMultipleCollections', async function () { + let collection1Data = await API.createCollection("Test 1", false, this, 'jsonData'); + let collection2Name = "Test 2"; + let collection2Data = await API.createCollection(collection2Name, false, this, 'jsonData'); + + let libraryVersion = await API.getLibraryVersion(); + + // Update with no change, which should still update library version (for now) + let response = await API.userPost( + config.userID, + "collections", + JSON.stringify([ + collection1Data, + collection2Data + ]), + { + "Content-Type": "application/json" + } + ); + Helpers.assert200(response); + // If this behavior changes, remove the pre-increment + Helpers.assertEquals(++libraryVersion, response.headers['last-modified-version'][0]); + let json = await API.getJSONFromResponse(response); + assert.lengthOf(Object.keys(json.unchanged), 2); + + Helpers.assertEquals(libraryVersion, await API.getLibraryVersion()); + + // Update + let collection1NewName = "Test 1 Modified"; + let collection2NewParentKey = await API.createCollection("Test 3", false, this, 'key'); + + response = await API.userPost( + config.userID, + "collections", + JSON.stringify([ + { + key: collection1Data.key, + version: collection1Data.version, + name: collection1NewName + }, + { + key: collection2Data.key, + version: collection2Data.version, + parentCollection: collection2NewParentKey + } + ]), + { + "Content-Type": "application/json" + } + ); + Helpers.assert200(response); + libraryVersion = response.headers['last-modified-version'][0]; + json = await API.getJSONFromResponse(response); + assert.lengthOf(Object.keys(json.successful), 2); + // Deprecated + assert.lengthOf(Object.keys(json.success), 2); + + // Check data in write response + Helpers.assertEquals(json.successful[0].key, json.successful[0].data.key); + Helpers.assertEquals(json.successful[1].key, json.successful[1].data.key); + Helpers.assertEquals(libraryVersion, json.successful[0].version); + Helpers.assertEquals(libraryVersion, json.successful[1].version); + Helpers.assertEquals(libraryVersion, json.successful[0].data.version); + Helpers.assertEquals(libraryVersion, json.successful[1].data.version); + Helpers.assertEquals(collection1NewName, json.successful[0].data.name); + Helpers.assertEquals(collection2Name, json.successful[1].data.name); + assert.notOk(json.successful[0].data.parentCollection); + Helpers.assertEquals(collection2NewParentKey, json.successful[1].data.parentCollection); + + // Check in separate request, to be safe + let keys = Object.keys(json.successful).map(k => json.successful[k].key); + + response = await API.getCollectionResponse(keys); + Helpers.assertTotalResults(response, 2); + json = await API.getJSONFromResponse(response); + // POST follows PATCH behavior, so unspecified values shouldn't change + Helpers.assertEquals(collection1NewName, json[0].data.name); + assert.notOk(json[0].data.parentCollection); + Helpers.assertEquals(collection2Name, json[1].data.name); + Helpers.assertEquals(collection2NewParentKey, json[1].data.parentCollection); + }); + + it('testCollectionItemChange', async function () { + let collectionKey1 = await API.createCollection('Test', false, this, 'key'); + let collectionKey2 = await API.createCollection('Test', false, this, 'key'); + + let json = await API.createItem("book", { + collections: [collectionKey1] + }, this, 'json'); + let itemKey1 = json.key; + let itemVersion1 = json.version; + assert.deepEqual([collectionKey1], json.data.collections); + + json = await API.createItem("journalArticle", { + collections: [collectionKey2] + }, this, 'json'); + let itemKey2 = json.key; + let itemVersion2 = json.version; + assert.deepEqual([collectionKey2], json.data.collections); + + json = await API.getCollection(collectionKey1, this); + assert.deepEqual(1, json.meta.numItems); + + json = await API.getCollection(collectionKey2, this); + Helpers.assertEquals(1, json.meta.numItems); + let collectionData2 = json.data; + + let libraryVersion = await API.getLibraryVersion(); + + // Add items to collection + let response = await API.userPatch( + config.userID, + `items/${itemKey1}`, + JSON.stringify({ + collections: [collectionKey1, collectionKey2] + }), + { + "Content-Type": "application/json", + "If-Unmodified-Since-Version": itemVersion1 + } + ); + Helpers.assert204(response); + + // Item version should change + json = await API.getItem(itemKey1, this); + Helpers.assertEquals(parseInt(libraryVersion) + 1, parseInt(json.version)); + + // Collection timestamp shouldn't change, but numItems should + json = await API.getCollection(collectionKey2, this); + Helpers.assertEquals(2, json.meta.numItems); + Helpers.assertEquals(collectionData2.version, json.version); + collectionData2 = json.data; + + libraryVersion = await API.getLibraryVersion(); + + // Remove collections + response = await API.userPatch( + config.userID, + `items/${itemKey2}`, + JSON.stringify({ + collections: [] + }), + { + "Content-Type": "application/json", + "If-Unmodified-Since-Version": itemVersion2 + } + ); + Helpers.assert204(response); + + // Item version should change + json = await API.getItem(itemKey2, this); + assert.equal(parseInt(libraryVersion) + 1, json.version); + + // Collection timestamp shouldn't change, but numItems should + json = await API.getCollection(collectionKey2, this); + assert.equal(json.meta.numItems, 1); + assert.equal(collectionData2.version, json.version); + + // Check collections arrays and numItems + json = await API.getItem(itemKey1, this); + assert.lengthOf(json.data.collections, 2); + assert.include(json.data.collections, collectionKey1); + assert.include(json.data.collections, collectionKey2); + + json = await API.getItem(itemKey2, this); + assert.lengthOf(json.data.collections, 0); + + json = await API.getCollection(collectionKey1, this); + assert.equal(json.meta.numItems, 1); + + json = await API.getCollection(collectionKey2, this); + assert.equal(json.meta.numItems, 1); + }); + + it('testNewMultipleCollections', async function () { + let json = await API.createCollection("Test Collection 1", false, this, 'jsonData'); + let name1 = "Test Collection 2"; + let name2 = "Test Subcollection"; + let parent2 = json.key; + + json = [ + { + name: name1 + }, + { + name: name2, + parentCollection: parent2 + } + + ]; + + let response = await API.userPost( + config.userID, + "collections", + JSON.stringify(json), + { + "Content-Type": "application/json" + } + ); + + Helpers.assert200(response); + let libraryVersion = response.headers['last-modified-version'][0]; + let jsonResponse = API.getJSONFromResponse(response); + assert.lengthOf(Object.keys(jsonResponse.successful), 2); + // Deprecated + assert.lengthOf(Object.keys(jsonResponse.success), 2); + + // Check data in write response + Helpers.assertEquals(jsonResponse.successful[0].key, jsonResponse.successful[0].data.key); + Helpers.assertEquals(jsonResponse.successful[1].key, jsonResponse.successful[1].data.key); + Helpers.assertEquals(libraryVersion, jsonResponse.successful[0].version); + Helpers.assertEquals(libraryVersion, jsonResponse.successful[1].version); + Helpers.assertEquals(libraryVersion, jsonResponse.successful[0].data.version); + Helpers.assertEquals(libraryVersion, jsonResponse.successful[1].data.version); + Helpers.assertEquals(name1, jsonResponse.successful[0].data.name); + Helpers.assertEquals(name2, jsonResponse.successful[1].data.name); + assert.notOk(jsonResponse.successful[0].data.parentCollection); + Helpers.assertEquals(parent2, jsonResponse.successful[1].data.parentCollection); + + // Check in separate request, to be safe + let keys = Object.keys(jsonResponse.successful).map(k => jsonResponse.successful[k].key); + + response = await API.getCollectionResponse(keys); + + Helpers.assertTotalResults(response, 2); + jsonResponse = API.getJSONFromResponse(response); + Helpers.assertEquals(name1, jsonResponse[0].data.name); + assert.notOk(jsonResponse[0].data.parentCollection); + Helpers.assertEquals(name2, jsonResponse[1].data.name); + Helpers.assertEquals(parent2, jsonResponse[1].data.parentCollection); + }); + + it('test_should_return_409_on_missing_parent_collection', async function () { + let missingCollectionKey = "GDHRG8AZ"; + let json = await API.createCollection("Test", { parentCollection: missingCollectionKey }, this); + Helpers.assert409ForObject(json, `Parent collection ${missingCollectionKey} not found`); + Helpers.assertEquals(missingCollectionKey, json.failed[0].data.collection); + }); + + it('test_should_return_413_if_collection_name_is_too_long', async function () { + const content = "1".repeat(256); + const json = { + name: content + }; + const response = await API.userPost( + config.userID, + "collections", + JSON.stringify([json]), + { + "Content-Type": "application/json" + } + ); + Helpers.assert413ForObject(response); + }); + + it('testNewCollection', async function () { + let name = "Test Collection"; + let json = await API.createCollection(name, false, this, 'json'); + Helpers.assertEquals(name, json.data.name); + return json.key; + }); + + it('testCollectionItemMissingCollection', async function () { + let response = await API.createItem("book", { collections: ["AAAAAAAA"] }, this, 'response'); + Helpers.assert409ForObject(response, "Collection AAAAAAAA not found"); + }); + + it('test_should_move_parent_collection_to_root_if_descendent_of_collection', async function () { + let jsonA = await API.createCollection('A', false, this, 'jsonData'); + // Set B as a child of A + let keyB = await API.createCollection('B', { parentCollection: jsonA.key }, this, 'key'); + + // Try to set B as parent of A + jsonA.parentCollection = keyB; + let response = await API.userPost( + config.userID, + 'collections', + JSON.stringify([jsonA]), + { "Content-Type": "application/json" } + ); + Helpers.assert200(response); + let json = await API.getJSONFromResponse(response); + assert.equal(json.successful[0].data.parentCollection, keyB); + + let jsonB = await API.getCollection(keyB, this); + assert.notOk(jsonB.data.parentCollection); + }); +}); diff --git a/tests/remote_js/test/3/fullTextTest.js b/tests/remote_js/test/3/fullTextTest.js new file mode 100644 index 00000000..7da5c75d --- /dev/null +++ b/tests/remote_js/test/3/fullTextTest.js @@ -0,0 +1,502 @@ +const chai = require('chai'); +const assert = chai.assert; +const config = require("../../config.js"); +const API = require('../../api3.js'); +const Helpers = require('../../helpers3.js'); +const { API3Setup, API3WrapUp } = require("../shared.js"); + +describe('FullTextTests', function () { + this.timeout(config.timeout); + + before(async function () { + await API3Setup(); + }); + + after(async function () { + await API3WrapUp(); + }); + + this.beforeEach(async function () { + await API.useAPIKey(config.apiKey); + }); + + it('testContentAnonymous', async function () { + API.useAPIKey(false); + const response = await API.userGet( + config.userID, + 'items/AAAAAAAA/fulltext', + { 'Content-Type': 'application/json' } + ); + Helpers.assert403(response); + }); + + it('testModifyAttachmentWithFulltext', async function () { + let key = await API.createItem("book", false, this, 'key'); + let json = await API.createAttachmentItem("imported_url", [], key, this, 'jsonData'); + let attachmentKey = json.key; + let content = "Here is some full-text content"; + let pages = 50; + + // Store content + let response = await API.userPut( + config.userID, + "items/" + attachmentKey + "/fulltext", + JSON.stringify({ + content: content, + indexedPages: pages, + totalPages: pages + }), + { "Content-Type": "application/json" } + ); + Helpers.assert204(response); + + json.title = "This is a new attachment title"; + json.contentType = 'text/plain'; + + // Modify attachment item + response = await API.userPut( + config.userID, + "items/" + attachmentKey, + JSON.stringify(json), + { "If-Unmodified-Since-Version": json.version } + ); + Helpers.assert204(response); + }); + + it('testSetItemContentMultiple', async function () { + let key = await API.createItem("book", false, this, 'key'); + let attachmentKey1 = await API.createAttachmentItem("imported_url", [], key, this, 'key'); + let attachmentKey2 = await API.createAttachmentItem("imported_url", [], key, this, 'key'); + + let libraryVersion = await API.getLibraryVersion(); + + let json = [ + { + key: attachmentKey1, + content: "Here is some full-text content", + indexedPages: 50, + totalPages: 50, + invalidParam: "shouldBeIgnored" + }, + { + content: "This is missing a key and should be skipped", + indexedPages: 20, + totalPages: 40 + }, + { + key: attachmentKey2, + content: "Here is some more full-text content", + indexedPages: 20, + totalPages: 40 + } + ]; + + // No Content-Type + let response = await API.userPost( + config.userID, + "fulltext", + JSON.stringify(json), + { + "If-Unmodified-Since-Version": libraryVersion + } + ); + Helpers.assert400(response, "Content-Type must be application/json"); + + // No If-Unmodified-Since-Version + response = await API.userPost( + config.userID, + "fulltext", + JSON.stringify(json), + { + "Content-Type": "application/json" + } + ); + Helpers.assert428(response, "If-Unmodified-Since-Version not provided"); + + // Store content + response = await API.userPost( + config.userID, + "fulltext", + JSON.stringify(json), + { + "Content-Type": "application/json", + "If-Unmodified-Since-Version": libraryVersion + } + ); + + Helpers.assert200(response); + Helpers.assert200ForObject(response, { index: 0 }); + Helpers.assert400ForObject(response, { index: 1 }); + Helpers.assert200ForObject(response, { index: 2 }); + let newLibraryVersion = response.headers['last-modified-version'][0]; + assert.isAbove(parseInt(newLibraryVersion), parseInt(libraryVersion)); + libraryVersion = newLibraryVersion; + + let originalJSON = json; + + // Retrieve content + response = await API.userGet( + config.userID, + "items/" + attachmentKey1 + "/fulltext" + ); + Helpers.assert200(response); + Helpers.assertContentType(response, "application/json"); + json = JSON.parse(response.data); + Helpers.assertEquals(originalJSON[0].content, json.content); + Helpers.assertEquals(originalJSON[0].indexedPages, json.indexedPages); + Helpers.assertEquals(originalJSON[0].totalPages, json.totalPages); + assert.notProperty(json, "indexedChars"); + assert.notProperty(json, "invalidParam"); + Helpers.assertEquals(libraryVersion, response.headers['last-modified-version'][0]); + + response = await API.userGet( + config.userID, + "items/" + attachmentKey2 + "/fulltext" + ); + Helpers.assert200(response); + Helpers.assertContentType(response, "application/json"); + json = JSON.parse(response.data); + Helpers.assertEquals(originalJSON[2].content, json.content); + Helpers.assertEquals(originalJSON[2].indexedPages, json.indexedPages); + Helpers.assertEquals(originalJSON[2].totalPages, json.totalPages); + assert.notProperty(json, "indexedChars"); + assert.notProperty(json, "invalidParam"); + Helpers.assertEquals(libraryVersion, response.headers['last-modified-version'][0]); + }); + + it('testSetItemContent', async function () { + let response = await API.createItem("book", false, this, 'key'); + let attachmentKey = await API.createAttachmentItem("imported_url", [], response, this, 'key'); + + response = await API.userGet( + config.userID, + "items/" + attachmentKey + "/fulltext" + ); + Helpers.assert404(response); + assert.notOk(response.headers['last-modified-version']); + + let libraryVersion = await API.getLibraryVersion(); + + let content = "Here is some full-text content"; + let pages = 50; + + // No Content-Type + response = await API.userPut( + config.userID, + "items/" + attachmentKey + "/fulltext", + content + ); + Helpers.assert400(response, "Content-Type must be application/json"); + + // Store content + response = await API.userPut( + config.userID, + "items/" + attachmentKey + "/fulltext", + JSON.stringify({ + content: content, + indexedPages: pages, + totalPages: pages, + invalidParam: "shouldBeIgnored" + }), + { "Content-Type": "application/json" } + ); + + Helpers.assert204(response); + let contentVersion = response.headers['last-modified-version'][0]; + assert.isAbove(parseInt(contentVersion), parseInt(libraryVersion)); + + // Retrieve it + response = await API.userGet( + config.userID, + "items/" + attachmentKey + "/fulltext" + ); + Helpers.assert200(response); + Helpers.assertContentType(response, "application/json"); + let json = JSON.parse(await response.data); + Helpers.assertEquals(content, json.content); + assert.property(json, 'indexedPages'); + assert.property(json, 'totalPages'); + Helpers.assertEquals(pages, json.indexedPages); + Helpers.assertEquals(pages, json.totalPages); + assert.notProperty(json, "indexedChars"); + assert.notProperty(json, "invalidParam"); + Helpers.assertEquals(contentVersion, response.headers['last-modified-version'][0]); + }); + + // Requires ES + it('testSearchItemContent', async function () { + this.skip(); + let collectionKey = await API.createCollection('Test', false, this, 'key'); + let parentKey = await API.createItem("book", { collections: [collectionKey] }, this, 'key'); + let json = await API.createAttachmentItem("imported_url", [], parentKey, this, 'jsonData'); + let attachmentKey = json.key; + + let response = await API.userGet( + config.userID, + "items/" + attachmentKey + "/fulltext" + ); + Helpers.assert404(response); + + let content = "Here is some unique full-text content"; + let pages = 50; + + // Store content + response = await API.userPut( + config.userID, + "items/" + attachmentKey + "/fulltext", + JSON.stringify({ + content: content, + indexedPages: pages, + totalPages: pages + }), + { "Content-Type": "application/json" } + ); + + Helpers.assert204(response); + + // Wait for indexing via Lambda + await new Promise(resolve => setTimeout(resolve, 6000)); + + // Search for nonexistent word + response = await API.userGet( + config.userID, + "items?q=nothing&qmode=everything&format=keys" + ); + Helpers.assert200(response); + Helpers.assertEquals("", response.data.trim()); + + // Search for a word + response = await API.userGet( + config.userID, + "items?q=unique&qmode=everything&format=keys" + ); + Helpers.assert200(response); + Helpers.assertEquals(attachmentKey, response.data.trim()); + + // Search for a phrase + response = await API.userGet( + config.userID, + "items?q=unique%20full-text&qmode=everything&format=keys" + ); + Helpers.assert200(response); + Helpers.assertEquals(attachmentKey, response.data.trim()); + + // Search for a phrase in /top + response = await API.userGet( + config.userID, + "items/top?q=unique%20full-text&qmode=everything&format=keys" + ); + Helpers.assert200(response); + Helpers.assertEquals(parentKey, response.data.trim()); + + // Search for a phrase in a collection + response = await API.userGet( + config.userID, + "collections/" + collectionKey + "/items?q=unique%20full-text&qmode=everything&format=keys" + ); + Helpers.assert200(response); + Helpers.assertEquals(attachmentKey, response.data.trim()); + + // Search for a phrase in a collection + response = await API.userGet( + config.userID, + "collections/" + collectionKey + "/items/top?q=unique%20full-text&qmode=everything&format=keys" + ); + Helpers.assert200(response); + Helpers.assertEquals(parentKey, response.data.trim()); + }); + + it('testSinceContent', async function () { + await _testSinceContent('since'); + await _testSinceContent('newer'); + }); + + const _testSinceContent = async (param) => { + await API.userClear(config.userID); + + // Store content for one item + let key = await API.createItem("book", false, true, 'key'); + let json = await API.createAttachmentItem("imported_url", [], key, true, 'jsonData'); + let key1 = json.key; + + let content = "Here is some full-text content"; + + let response = await API.userPut( + config.userID, + `items/${key1}/fulltext`, + JSON.stringify([{ content: content }]), + { "Content-Type": "application/json" } + ); + Helpers.assert204(response); + let contentVersion1 = response.headers['last-modified-version'][0]; + assert.isAbove(parseInt(contentVersion1), 0); + + // And another + key = await API.createItem("book", false, true, 'key'); + json = await API.createAttachmentItem("imported_url", [], key, true, 'jsonData'); + let key2 = json.key; + + response = await API.userPut( + config.userID, + `items/${key2}/fulltext`, + JSON.stringify({ content: content }), + { "Content-Type": "application/json" } + ); + Helpers.assert204(response); + let contentVersion2 = response.headers['last-modified-version'][0]; + assert.isAbove(parseInt(contentVersion2), 0); + + // Get newer one + response = await API.userGet( + config.userID, + `fulltext?${param}=${contentVersion1}` + ); + Helpers.assert200(response); + Helpers.assertContentType(response, "application/json"); + Helpers.assertEquals(contentVersion2, response.headers['last-modified-version'][0]); + json = API.getJSONFromResponse(response); + assert.lengthOf(Object.keys(json), 1); + assert.property(json, key2); + Helpers.assertEquals(contentVersion2, json[key2]); + + // Get both with since=0 + response = await API.userGet( + config.userID, + `fulltext?${param}=0` + ); + Helpers.assert200(response); + Helpers.assertContentType(response, "application/json"); + json = API.getJSONFromResponse(response); + assert.lengthOf(Object.keys(json), 2); + assert.property(json, key1); + Helpers.assertEquals(contentVersion1, json[key1]); + assert.property(json, key1); + Helpers.assertEquals(contentVersion2, json[key2]); + }; + + it('testDeleteItemContent', async function () { + let key = await API.createItem("book", false, this, 'key'); + let attachmentKey = await API.createAttachmentItem("imported_file", [], key, this, 'key'); + + let content = "Ыюм мютат дэбетиз конвынёры эю, ку мэль жкрипта трактатоз.\nПро ут чтэт эрепюят граэкйж, дуо нэ выро рыкючабо пырикюлёз."; + + // Store content + let response = await API.userPut( + config.userID, + "items/" + attachmentKey + "/fulltext", + JSON.stringify({ + content: content, + indexedPages: 50 + }), + { "Content-Type": "application/json" } + ); + Helpers.assert204(response); + let contentVersion = response.headers['last-modified-version'][0]; + + // Retrieve it + response = await API.userGet( + config.userID, + "items/" + attachmentKey + "/fulltext" + ); + Helpers.assert200(response); + let json = JSON.parse(response.data); + Helpers.assertEquals(content, json.content); + Helpers.assertEquals(50, json.indexedPages); + + // Set to empty string + response = await API.userPut( + config.userID, + "items/" + attachmentKey + "/fulltext", + JSON.stringify({ + content: "" + }), + { "Content-Type": "application/json" } + ); + Helpers.assert204(response); + assert.isAbove(parseInt(response.headers['last-modified-version'][0]), parseInt(contentVersion)); + + // Make sure it's gone + response = await API.userGet( + config.userID, + "items/" + attachmentKey + "/fulltext" + ); + Helpers.assert200(response); + json = JSON.parse(response.data); + Helpers.assertEquals("", json.content); + assert.notProperty(json, "indexedPages"); + }); + + it('_testSinceContent', async function () { + await API.userClear(config.userID); + // Store content for one item + let key = await API.createItem("book", false, this, 'key'); + let json = await API.createAttachmentItem("imported_url", [], key, this, 'jsonData'); + let key1 = json.key; + + let content = "Here is some full-text content"; + + let response = await API.userPut( + config.userID, + "items/" + key1 + "/fulltext", + JSON.stringify({ + content: content + }), + { "Content-Type": "application/json" } + ); + Helpers.assert204(response); + let contentVersion1 = response.headers['last-modified-version'][0]; + assert.isAbove(parseInt(contentVersion1), 0); + + // And another + key = await API.createItem("book", false, this, 'key'); + json = await API.createAttachmentItem("imported_url", [], key, this, 'jsonData'); + let key2 = json.key; + + response = await API.userPut( + config.userID, + "items/" + key2 + "/fulltext", + JSON.stringify({ + content: content + }), + { "Content-Type": "application/json" } + ); + Helpers.assert204(response); + let contentVersion2 = response.headers['last-modified-version'][0]; + assert.isAbove(parseInt(contentVersion2), 0); + + // Get newer one + response = await API.userGet( + config.userID, + "fulltext?param=" + contentVersion1 + ); + Helpers.assert200(response); + Helpers.assertContentType(response, "application/json"); + Helpers.assertEquals(contentVersion2, response.headers['last-modified-version'][0]); + json = API.getJSONFromResponse(response); + assert.lengthOf(Object.keys(json), 2); + assert.property(json, key2); + Helpers.assertEquals(contentVersion2, json[key2]); + + // Get both with since=0 + response = await API.userGet( + config.userID, + "fulltext?param=0" + ); + Helpers.assert200(response); + Helpers.assertContentType(response, "application/json"); + json = API.getJSONFromResponse(response); + assert.lengthOf(Object.keys(json), 2); + assert.property(json, key1); + Helpers.assertEquals(contentVersion1, json[key1]); + Helpers.assertEquals(contentVersion2, json[key2]); + }); + + it('testVersionsAnonymous', async function () { + API.useAPIKey(false); + const response = await API.userGet( + config.userID, + "fulltext" + ); + Helpers.assert403(response); + }); +}); diff --git a/tests/remote_js/test/3/groupTest.js b/tests/remote_js/test/3/groupTest.js new file mode 100644 index 00000000..73e9cf72 --- /dev/null +++ b/tests/remote_js/test/3/groupTest.js @@ -0,0 +1,280 @@ +const chai = require('chai'); +const assert = chai.assert; +const config = require("../../config.js"); +const API = require('../../api3.js'); +const Helpers = require('../../helpers3.js'); +const { JSDOM } = require('jsdom'); +const { API3Setup, API3WrapUp } = require("../shared.js"); + +describe('Tests', function () { + this.timeout(config.timeout); + + before(async function () { + await API3Setup(); + }); + + after(async function () { + await API3WrapUp(); + }); + + + it('testDeleteGroup', async function () { + let groupID = await API.createGroup({ + owner: config.userID, + type: 'Private', + libraryReading: 'all', + }); + await API.groupCreateItem(groupID, 'book', false, this, 'key'); + await API.groupCreateItem(groupID, 'book', false, this, 'key'); + await API.groupCreateItem(groupID, 'book', false, this, 'key'); + await API.deleteGroup(groupID); + + const response = await API.groupGet(groupID, ''); + Helpers.assert404(response); + }); + + it('testUpdateMemberJSON', async function () { + let groupID = await API.createGroup({ + owner: config.userID, + type: 'Private', + libraryReading: 'all' + }); + + let response = await API.userGet(config.userID, `groups?format=versions&key=${config.apiKey}`); + Helpers.assert200(response); + let version = JSON.parse(response.data)[groupID]; + + response = await API.superPost(`groups/${groupID}/users`, '', { 'Content-Type': 'text/xml' }); + Helpers.assert200(response); + + response = await API.userGet(config.userID, `groups?format=versions&key=${config.apiKey}`); + Helpers.assert200(response); + let json = JSON.parse(response.data); + let newVersion = json[groupID]; + assert.notEqual(version, newVersion); + + response = await API.groupGet(groupID, ''); + Helpers.assert200(response); + Helpers.assertEquals(newVersion, response.headers['last-modified-version'][0]); + + await API.deleteGroup(groupID); + }); + + it('testUpdateMetadataAtom', async function () { + let response = await API.userGet( + config.userID, + `groups?fq=GroupType:PublicOpen&content=json&key=${config.apiKey}` + ); + Helpers.assert200(response); + + // Get group API URI and version + let xml = API.getXMLFromResponse(response); + + let groupID = await Helpers.xpathEval(xml, '//atom:entry/zapi:groupID'); + let urlComponent = await Helpers.xpathEval(xml, "//atom:entry/atom:link[@rel='self']", true, false); + let url = urlComponent.getAttribute('href'); + url = url.replace(config.apiURLPrefix, ''); + let version = JSON.parse(API.parseDataFromAtomEntry(xml).content).version; + + // Make sure format=versions returns the same version + response = await API.userGet( + config.userID, + `groups?format=versions&key=${config.apiKey}` + ); + Helpers.assert200(response); + let json = JSON.parse(response.data); + assert.equal(version, json[groupID]); + + // Update group metadata + json = JSON.parse(await Helpers.xpathEval(xml, "//atom:entry/atom:content")); + + const xmlDoc = new JSDOM(""); + const groupXML = xmlDoc.window.document.getElementsByTagName("group")[0]; + let name, description, urlField, newNode; + for (let [key, val] of Object.entries(json)) { + switch (key) { + case 'id': + case 'members': + continue; + + case 'name': + name = "My Test Group " + Math.random(); + groupXML.setAttribute('name', name); + break; + + case 'description': + description = "This is a test description " + Math.random(); + newNode = xmlDoc.window.document.createElement(key); + newNode.innerHTML = description; + groupXML.appendChild(newNode); + break; + + case 'url': + urlField = "http://example.com/" + Math.random(); + newNode = xmlDoc.window.document.createElement(key); + newNode.innerHTML = urlField; + groupXML.appendChild(newNode); + break; + + default: + groupXML.setAttributeNS(null, key, val); + } + } + const payload = groupXML.outerHTML; + response = await API.put( + url, + payload, + { "Content-Type": "text/xml" }, + { + username: config.rootUsername, + password: config.rootPassword + } + ); + Helpers.assert200(response); + xml = API.getXMLFromResponse(response); + let group = await Helpers.xpathEval(xml, '//atom:entry/atom:content/zxfer:group', true, true); + Helpers.assertCount(1, group); + assert.equal(name, group[0].getAttribute('name')); + + response = await API.userGet( + config.userID, + `groups?format=versions&key=${config.apiKey}` + ); + Helpers.assert200(response); + json = JSON.parse(response.data); + let newVersion = json[groupID]; + assert.notEqual(version, newVersion); + + // Check version header on individual group request + response = await API.groupGet( + groupID, + `?content=json&key=${config.apiKey}` + ); + Helpers.assert200(response); + assert.equal(newVersion, response.headers['last-modified-version'][0]); + json = JSON.parse(API.getContentFromResponse(response)); + assert.equal(name, json.name); + assert.equal(description, json.description); + assert.equal(urlField, json.url); + }); + + it('testUpdateMetadataJSON', async function () { + const response = await API.userGet( + config.userID, + "groups?fq=GroupType:PublicOpen" + ); + + Helpers.assert200(response); + + const json = API.getJSONFromResponse(response)[0]; + const groupID = json.id; + let url = json.links.self.href; + url = url.replace(config.apiURLPrefix, ''); + const version = json.version; + + const response2 = await API.userGet( + config.userID, + "groups?format=versions&key=" + config.apiKey + ); + + Helpers.assert200(response2); + + Helpers.assertEquals(version, JSON.parse(response2.data)[groupID]); + + const xmlDoc = new JSDOM(""); + const groupXML = xmlDoc.window.document.getElementsByTagName("group")[0]; + let name, description, urlField, newNode; + for (const [key, val] of Object.entries(json.data)) { + switch (key) { + case 'id': + case 'version': + case 'members': + continue; + case 'name': { + name = "My Test Group " + Helpers.uniqueID(); + groupXML.setAttributeNS(null, key, name); + break; + } + case 'description': { + description = "This is a test description " + Helpers.uniqueID(); + newNode = xmlDoc.window.document.createElement(key); + newNode.innerHTML = description; + groupXML.appendChild(newNode); + break; + } + case 'url': { + urlField = "http://example.com/" + Helpers.uniqueID(); + newNode = xmlDoc.window.document.createElement(key); + newNode.innerHTML = urlField; + groupXML.appendChild(newNode); + break; + } + default: + groupXML.setAttributeNS(null, key, val); + } + } + + const response3 = await API.put( + url, + groupXML.outerHTML, + { "Content-Type": "text/xml" }, + { + username: config.rootUsername, + password: config.rootPassword + } + ); + + Helpers.assert200(response3); + + const xmlResponse = API.getXMLFromResponse(response3); + const group = Helpers.xpathEval(xmlResponse, '//atom:entry/atom:content/zxfer:group', true, true); + + Helpers.assertCount(1, group); + Helpers.assertEquals(name, group[0].getAttribute('name')); + + const response4 = await API.userGet( + config.userID, + "groups?format=versions&key=" + config.apiKey + ); + + Helpers.assert200(response4); + + const json2 = JSON.parse(response4.data); + const newVersion = json2[groupID]; + + assert.notEqual(version, newVersion); + + const response5 = await API.groupGet( + groupID, + "" + ); + + Helpers.assert200(response5); + Helpers.assertEquals(newVersion, response5.headers['last-modified-version'][0]); + const json3 = API.getJSONFromResponse(response5).data; + + Helpers.assertEquals(name, json3.name); + Helpers.assertEquals(description, json3.description); + Helpers.assertEquals(urlField, json3.url); + }); + + it('test_group_should_not_appear_in_search_until_first_populated', async function () { + const name = Helpers.uniqueID(14); + const groupID = await API.createGroup({ + owner: config.userID, + type: 'PublicClosed', + name, + libraryReading: 'all' + }); + + let response = await API.superGet(`groups?q=${name}`); + Helpers.assertNumResults(response, 0); + + await API.groupCreateItem(groupID, 'book', false, this); + + response = await API.superGet(`groups?q=${name}`); + Helpers.assertNumResults(response, 1); + + await API.deleteGroup(groupID); + }); +}); diff --git a/tests/remote_js/test/3/noteTest.js b/tests/remote_js/test/3/noteTest.js new file mode 100644 index 00000000..176805f4 --- /dev/null +++ b/tests/remote_js/test/3/noteTest.js @@ -0,0 +1,153 @@ +const chai = require('chai'); +const assert = chai.assert; +const config = require("../../config.js"); +const API = require('../../api3.js'); +const Helpers = require('../../helpers3.js'); +const { API3Setup, API3WrapUp } = require("../shared.js"); + +describe('NoteTests', function () { + //this.timeout(config.timeout); + this.timeout(0); + + let content, json; + + before(async function () { + await API3Setup(); + }); + + after(async function () { + await API3WrapUp(); + }); + + this.beforeEach(async function() { + content = "1234567890".repeat(50001); + json = await API.getItemTemplate('note'); + json.note = content; + }); + + it('testSaveHTML', async function () { + const content = '

Foo & Bar

'; + const json = await API.createNoteItem(content, false, this, 'json'); + Helpers.assertEquals(content, json.data.note); + }); + + it('testNoteTooLong', async function () { + let response = await API.userPost( + config.userID, + "items", + JSON.stringify([json]), + { "Content-Type": "application/json" } + ); + Helpers.assert413ForObject( + response, + "Note '1234567890123456789012345678901234567890123456789012345678901234567890123456789...' too long" + ); + }); + + it('testNoteTooLongBlankFirstLines', async function () { + json.note = " \n \n" + content; + + let response = await API.userPost( + config.userID, + "items", + JSON.stringify([json]), + { "Content-Type": "application/json" } + ); + + Helpers.assert413ForObject( + response, + "Note '1234567890123456789012345678901234567890123456789012345678901234567890123456789...' too long" + ); + }); + + it('testSaveUnchangedSanitizedNote', async function () { + let json = await API.createNoteItem('Foo', false, this, 'json'); + let response = await API.postItem(json.data, { "Content-Type": "application/json" }); + json = await API.getJSONFromResponse(response); + let unchanged = json.unchanged; + assert.property(unchanged, 0); + }); + + it('testNoteTooLongBlankFirstLinesHTML', async function () { + json.note = "\n

 

\n

 

\n" + content; + + let response = await API.userPost( + config.userID, + "items", + JSON.stringify([json]), + { "Content-Type": "application/json" } + ); + + Helpers.assert413ForObject( + response, + "Note '1234567890123456789012345678901234567890123456789012345678901234567890123...' too long" + ); + }); + + it('test_utf8mb4_note', async function () { + let note = "

🐻

"; + json.note = note; + + let response = await API.userPost( + config.userID, + "items", + JSON.stringify([json]), + { "Content-Type": "application/json" } + ); + + Helpers.assert200ForObject(response); + + let jsonResponse = await API.getJSONFromResponse(response); + let data = jsonResponse.successful[0].data; + assert.equal(note, data.note); + }); + + it('testNoteTooLongWithinHTMLTags', async function () { + json.note = " \n

"; + let response = await API.userPost( + config.userID, + "items", + JSON.stringify([json]), + { "Content-Type": "application/json" } + ); + Helpers.assert413ForObject( + response, + "Note '<p><!-- 1234567890123456789012345678901234567890123456789012345678901234...' too long" + ); + }); + + it('testNoteTooLongTitlePlusNewlines', async function () { + json.note = `Full Text:\n\n${content}`; + let response = await API.userPost( + config.userID, + 'items', + JSON.stringify([json]), + { 'Content-Type': 'application/json' } + ); + Helpers.assert413ForObject( + response, + "Note 'Full Text: 1234567890123456789012345678901234567890123456789012345678901234567...' too long" + ); + }); + + it('test_should_allow_zotero_links_in_notes', async function () { + let json = await API.createNoteItem('

Test

', false, this, 'json'); + + const val = '

Test

'; + json.data.note = val; + + let response = await API.postItem(json.data); + let jsonResp = await API.getJSONFromResponse(response); + Helpers.assertEquals(val, jsonResp.successful[0].data.note); + }); + + it('testSaveHTMLAtom', async function () { + let content = '

Foo & Bar

'; + let xml = await API.createNoteItem(content, false, this, 'atom'); + let contentXml = xml.getElementsByTagName('content')[0]; + const tempNode = xml.createElement("textarea"); + const htmlNote = JSON.parse(contentXml.innerHTML).note; + tempNode.innerHTML = htmlNote; + assert.equal(tempNode.textContent, content); + }); +}); diff --git a/tests/remote_js/test/shared.js b/tests/remote_js/test/shared.js index 0625619a..aa40a1f5 100644 --- a/tests/remote_js/test/shared.js +++ b/tests/remote_js/test/shared.js @@ -32,12 +32,14 @@ module.exports = { config.apiKey = credentials.user1.apiKey; config.user2APIKey = credentials.user2.apiKey; await API3.useAPIVersion(3); + await API3.useAPIKey(config.apiKey); await API3.resetSchemaVersion(); await API3.setKeyUserPermission(config.apiKey, 'notes', true); await API3.setKeyUserPermission(config.apiKey, 'write', true); await API.userClear(config.userID); }, API3WrapUp: async () => { + await API3.useAPIKey(config.apiKey); await API.userClear(config.userID); } }; From f4dfc108359dbda5895c15e02224010ea405c394 Mon Sep 17 00:00:00 2001 From: Bogdan Abaev Date: Mon, 22 May 2023 18:49:34 -0400 Subject: [PATCH 04/33] v3 object test --- tests/remote_js/test/3/objectTest.js | 485 +++++++++++++++++++++++++++ 1 file changed, 485 insertions(+) create mode 100644 tests/remote_js/test/3/objectTest.js diff --git a/tests/remote_js/test/3/objectTest.js b/tests/remote_js/test/3/objectTest.js new file mode 100644 index 00000000..7651d10d --- /dev/null +++ b/tests/remote_js/test/3/objectTest.js @@ -0,0 +1,485 @@ +const chai = require('chai'); +const assert = chai.assert; +const config = require("../../config.js"); +const API = require('../../api3.js'); +const Helpers = require('../../helpers3.js'); +const { API3Setup, API3WrapUp } = require("../shared.js"); + +describe('ObjectTests', function () { + this.timeout(0); + + before(async function () { + await API3Setup(); + }); + + after(async function () { + await API3WrapUp(); + }); + + beforeEach(async function() { + await API.userClear(config.userID); + }); + + afterEach(async function() { + await API.userClear(config.userID); + }); + + const _testMultiObjectGet = async (objectType = 'collection') => { + const objectNamePlural = API.getPluralObjectType(objectType); + const keyProp = `${objectType}Key`; + + const keys = []; + switch (objectType) { + case 'collection': + keys.push(await API.createCollection("Name", false, true, 'key')); + keys.push(await API.createCollection("Name", false, true, 'key')); + await API.createCollection("Name", false, true, 'key'); + break; + + case 'item': + keys.push(await API.createItem("book", { title: "Title" }, true, 'key')); + keys.push(await API.createItem("book", { title: "Title" }, true, 'key')); + await API.createItem("book", { title: "Title" }, true, 'key'); + break; + + case 'search': + keys.push(await API.createSearch("Name", 'default', true, 'key')); + keys.push(await API.createSearch("Name", 'default', true, 'key')); + await API.createSearch("Name", 'default', true, 'key'); + break; + } + + let response = await API.userHead( + config.userID, + `${objectNamePlural}?key=${config.apiKey}&${keyProp}=${keys.join(',')}` + ); + Helpers.assert200(response); + Helpers.assertTotalResults(response, keys.length); + + + response = await API.userGet( + config.userID, + `${objectNamePlural}?${keyProp}=${keys.join(',')}` + ); + Helpers.assertStatusCode(response, 200); + Helpers.assertNumResults(response, keys.length); + + // Trailing comma in itemKey parameter + response = await API.userGet( + config.userID, + `${objectNamePlural}?${keyProp}=${keys.join(',')},` + ); + Helpers.assertStatusCode(response, 200); + Helpers.assertNumResults(response, keys.length); + }; + + const _testSingleObjectDelete = async (objectType) => { + const objectTypePlural = API.getPluralObjectType(objectType); + + let json; + switch (objectType) { + case 'collection': + json = await API.createCollection('Name', false, true, 'json'); + break; + case 'item': + json = await API.createItem('book', { title: 'Title' }, true, 'json'); + break; + case 'search': + json = await API.createSearch('Name', 'default', true, 'json'); + break; + } + + const objectKey = json.key; + const objectVersion = json.version; + + const responseDelete = await API.userDelete( + config.userID, + `${objectTypePlural}/${objectKey}`, + { 'If-Unmodified-Since-Version': objectVersion } + ); + Helpers.assertStatusCode(responseDelete, 204); + + const responseGet = await API.userGet( + config.userID, + `${objectTypePlural}/${objectKey}` + ); + Helpers.assertStatusCode(responseGet, 404); + }; + + const _testMultiObjectDelete = async (objectType) => { + const objectTypePlural = await API.getPluralObjectType(objectType); + const keyProp = `${objectType}Key`; + + const deleteKeys = []; + const keepKeys = []; + switch (objectType) { + case 'collection': + deleteKeys.push(await API.createCollection("Name", false, true, 'key')); + deleteKeys.push(await API.createCollection("Name", false, true, 'key')); + keepKeys.push(await API.createCollection("Name", false, true, 'key')); + break; + + case 'item': + deleteKeys.push(await API.createItem("book", { title: "Title" }, true, 'key')); + deleteKeys.push(await API.createItem("book", { title: "Title" }, true, 'key')); + keepKeys.push(await API.createItem("book", { title: "Title" }, true, 'key')); + break; + + case 'search': + deleteKeys.push(await API.createSearch("Name", 'default', true, 'key')); + deleteKeys.push(await API.createSearch("Name", 'default', true, 'key')); + keepKeys.push(await API.createSearch("Name", 'default', true, 'key')); + break; + } + + let response = await API.userGet(config.userID, `${objectTypePlural}`); + Helpers.assertNumResults(response, deleteKeys.length + keepKeys.length); + + let libraryVersion = response.headers["last-modified-version"]; + + response = await API.userDelete(config.userID, + `${objectTypePlural}?${keyProp}=${deleteKeys.join(',')}`, + { "If-Unmodified-Since-Version": libraryVersion } + ); + Helpers.assertStatusCode(response, 204); + libraryVersion = response.headers["last-modified-version"]; + response = await API.userGet(config.userID, `${objectTypePlural}`); + Helpers.assertNumResults(response, keepKeys.length); + + response = await API.userGet(config.userID, `${objectTypePlural}?${keyProp}=${keepKeys.join(',')}`); + Helpers.assertNumResults(response, keepKeys.length); + + response = await API.userDelete(config.userID, + `${objectTypePlural}?${keyProp}=${keepKeys.join(',')},`, + { "If-Unmodified-Since-Version": libraryVersion }); + Helpers.assertStatusCode(response, 204); + + response = await API.userGet(config.userID, `${objectTypePlural}?`); + Helpers.assertNumResults(response, 0); + }; + + const _testPartialWriteFailure = async () => { + let conditions = []; + const objectType = 'collection'; + let json1 = { name: "Test" }; + let json2 = { name: "1234567890".repeat(6554) }; + let json3 = { name: "Test" }; + switch (objectType) { + case 'collection': + json1 = { name: "Test" }; + json2 = { name: "1234567890".repeat(6554) }; + json3 = { name: "Test" }; + break; + case 'item': + json1 = await API.getItemTemplate('book'); + json2 = { ...json1 }; + json3 = { ...json1 }; + json2.title = "1234567890".repeat(6554); + break; + case 'search': + conditions = [ + { + condition: 'title', + operator: 'contains', + value: 'value' + } + ]; + json1 = { name: "Test", conditions: conditions }; + json2 = { name: "1234567890".repeat(6554), conditions: conditions }; + json3 = { name: "Test", conditions: conditions }; + break; + } + + const response = await API.userPost( + config.userID, + `${API.getPluralObjectType(objectType)}?`, + JSON.stringify([json1, json2, json3]), + { "Content-Type": "application/json" }); + + Helpers.assertStatusCode(response, 200); + let successKeys = await API.getSuccessKeysFrom(response); + + Helpers.assertStatusForObject(response, 'success', 0, 200); + Helpers.assertStatusForObject(response, 'success', 1, 413); + Helpers.assertStatusForObject(response, 'success', 2, 200); + + const responseKeys = await API.userGet( + config.userID, + `${API.getPluralObjectType(objectType)}?format=keys&key=${config.apiKey}` + ); + + Helpers.assertStatusCode(responseKeys, 200); + const keys = responseKeys.data.trim().split("\n"); + + assert.lengthOf(keys, 2); + successKeys.forEach((key) => { + assert.include(keys, key); + }); + }; + + const _testPartialWriteFailureWithUnchanged = async (objectType) => { + let objectTypePlural = API.getPluralObjectType(objectType); + + let json1; + let json2; + let json3; + let conditions = []; + + switch (objectType) { + case 'collection': + json1 = await API.createCollection('Test', false, true, 'jsonData'); + json2 = { name: "1234567890".repeat(6554) }; + json3 = { name: 'Test' }; + break; + + case 'item': + json1 = await API.createItem('book', { title: 'Title' }, true, 'jsonData'); + json2 = await API.getItemTemplate('book'); + json3 = { ...json2 }; + json2.title = "1234567890".repeat(6554); + break; + + case 'search': + conditions = [ + { + condition: 'title', + operator: 'contains', + value: 'value' + } + ]; + json1 = await API.createSearch('Name', conditions, true, 'jsonData'); + json2 = { + name: "1234567890".repeat(6554), + conditions + }; + json3 = { + name: 'Test', + conditions + }; + break; + } + + let response = await API.userPost( + config.userID, + `${objectTypePlural}`, + JSON.stringify([json1, json2, json3]), + { 'Content-Type': 'application/json' } + ); + + Helpers.assertStatusCode(response, 200); + let successKeys = API.getSuccessfulKeysFromResponse(response); + + Helpers.assertStatusForObject(response, 'unchanged', 0); + Helpers.assertStatusForObject(response, 'failed', 1); + Helpers.assertStatusForObject(response, 'success', 2); + + + response = await API.userGet(config.userID, + `${objectTypePlural}?format=keys&key=${config.apiKey}`); + Helpers.assertStatusCode(response, 200); + let keys = response.data.trim().split('\n'); + + assert.lengthOf(keys, 2); + + for (let key of successKeys) { + assert.include(keys, key); + } + }; + + const _testMultiObjectWriteInvalidObject = async (objectType) => { + const objectTypePlural = API.getPluralObjectType(objectType); + + let response = await API.userPost( + config.userID, + `${objectTypePlural}`, + JSON.stringify({ foo: "bar" }), + { "Content-Type": "application/json" } + ); + + Helpers.assertStatusCode(response, 400, "Uploaded data must be a JSON array"); + + response = await API.userPost( + config.userID, + `${objectTypePlural}`, + JSON.stringify([[], ""]), + { "Content-Type": "application/json" } + ); + Helpers.assert400ForObject(response, { message: `Invalid value for index 0 in uploaded data; expected JSON ${objectType} object`, index: 0 }); + Helpers.assert400ForObject(response, { message: `Invalid value for index 1 in uploaded data; expected JSON ${objectType} object`, index: 1 }); + }; + + it('testMultiObjectGet', async function () { + await _testMultiObjectGet('collection'); + await _testMultiObjectGet('item'); + await _testMultiObjectGet('search'); + }); + it('testSingleObjectDelete', async function () { + await _testSingleObjectDelete('collection'); + await _testSingleObjectDelete('item'); + await _testSingleObjectDelete('search'); + }); + it('testMultiObjectDelete', async function () { + await _testMultiObjectDelete('collection'); + await _testMultiObjectDelete('item'); + await _testMultiObjectDelete('search'); + }); + it('testPartialWriteFailure', async function () { + _testPartialWriteFailure('collection'); + _testPartialWriteFailure('item'); + _testPartialWriteFailure('search'); + }); + it('testPartialWriteFailureWithUnchanged', async function () { + await _testPartialWriteFailureWithUnchanged('collection'); + await _testPartialWriteFailureWithUnchanged('item'); + await _testPartialWriteFailureWithUnchanged('search'); + }); + + it('testMultiObjectWriteInvalidObject', async function () { + await _testMultiObjectWriteInvalidObject('collection'); + await _testMultiObjectWriteInvalidObject('item'); + await _testMultiObjectWriteInvalidObject('search'); + }); + + it('testDeleted', async function () { + // Create objects + const objectKeys = {}; + objectKeys.tag = ["foo", "bar"]; + + objectKeys.collection = []; + objectKeys.collection.push(await API.createCollection("Name", false, true, 'key')); + objectKeys.collection.push(await API.createCollection("Name", false, true, 'key')); + objectKeys.collection.push(await API.createCollection("Name", false, true, 'key')); + + objectKeys.item = []; + objectKeys.item.push(await API.createItem("book", { title: "Title", tags: objectKeys.tag.map(tag => ({ tag })) }, true, 'key')); + objectKeys.item.push(await API.createItem("book", { title: "Title" }, true, 'key')); + objectKeys.item.push(await API.createItem("book", { title: "Title" }, true, 'key')); + + objectKeys.search = []; + objectKeys.search.push(await API.createSearch("Name", 'default', true, 'key')); + objectKeys.search.push(await API.createSearch("Name", 'default', true, 'key')); + objectKeys.search.push(await API.createSearch("Name", 'default', true, 'key')); + + // Get library version + let response = await API.userGet(config.userID, "items?key=" + config.apiKey + "&format=keys&limit=1"); + let libraryVersion1 = response.headers["last-modified-version"][0]; + + const func = async (objectType, libraryVersion, url) => { + const objectTypePlural = await API.getPluralObjectType(objectType); + const response = await API.userDelete(config.userID, + `${objectTypePlural}?key=${config.apiKey}${url}`, + { "If-Unmodified-Since-Version": libraryVersion }); + Helpers.assertStatusCode(response, 204); + return response.headers["last-modified-version"][0]; + }; + + let tempLibraryVersion = await func('collection', libraryVersion1, "&collectionKey=" + objectKeys.collection[0]); + tempLibraryVersion = await func('item', tempLibraryVersion, "&itemKey=" + objectKeys.item[0]); + tempLibraryVersion = await func('search', tempLibraryVersion, "&searchKey=" + objectKeys.search[0]); + let libraryVersion2 = tempLibraryVersion; + + // /deleted without 'since' should be an error + response = await API.userGet( + config.userID, + "deleted?key=" + config.apiKey + ); + Helpers.assert400(response); + + // Delete second and third objects + tempLibraryVersion = await func('collection', tempLibraryVersion, "&collectionKey=" + objectKeys.collection.slice(1).join(',')); + tempLibraryVersion = await func('item', tempLibraryVersion, "&itemKey=" + objectKeys.item.slice(1).join(',')); + let libraryVersion3 = await func('search', tempLibraryVersion, "&searchKey=" + objectKeys.search.slice(1).join(',')); + + // Request all deleted objects + response = await API.userGet(config.userID, "deleted?key=" + config.apiKey + "&since=" + libraryVersion1); + Helpers.assertStatusCode(response, 200); + let json = JSON.parse(response.data); + let version = response.headers["last-modified-version"][0]; + assert.isNotNull(version); + assert.equal(response.headers["content-type"][0], "application/json"); + + const assertEquivalent = (response, equivalentTo) => { + Helpers.assert200(response); + assert.equal(response.data, equivalentTo.data); + assert.deepEqual(response.headers['last-modified-version'], equivalentTo.headers['last-modified-version']); + assert.deepEqual(response.headers['content-type'], equivalentTo.headers['content-type']); + }; + + // Make sure 'newer' is equivalent + let responseAlt = await API.userGet( + config.userID, + "deleted?key=" + config.apiKey + "&newer=" + libraryVersion1 + ); + assertEquivalent(responseAlt, response); + + // Make sure 'since=0' is equivalent + responseAlt = await API.userGet( + config.userID, + "deleted?key=" + config.apiKey + "&since=0" + ); + assertEquivalent(responseAlt, response); + + // Make sure 'newer=0' is equivalent + responseAlt = await API.userGet( + config.userID, + "deleted?key=" + config.apiKey + "&newer=0" + ); + assertEquivalent(responseAlt, response); + + // Verify keys + const verifyKeys = async (json, objectType, objectKeys) => { + const objectTypePlural = await API.getPluralObjectType(objectType); + assert.containsAllKeys(json, [objectTypePlural]); + assert.lengthOf(json[objectTypePlural], objectKeys.length); + for (let key of objectKeys) { + assert.include(json[objectTypePlural], key); + } + }; + await verifyKeys(json, 'collection', objectKeys.collection); + await verifyKeys(json, 'item', objectKeys.item); + await verifyKeys(json, 'search', objectKeys.search); + // Tags aren't deleted by removing from items + await verifyKeys(json, 'tag', []); + + // Request second and third deleted objects + response = await API.userGet( + config.userID, + `deleted?key=${config.apiKey}&since=${libraryVersion2}` + ); + Helpers.assertStatusCode(response, 200); + json = JSON.parse(response.data); + version = response.headers["last-modified-version"][0]; + assert.isNotNull(version); + assert.equal(response.headers["content-type"][0], "application/json"); + + responseAlt = await API.userGet( + config.userID, + `deleted?key=${config.apiKey}&newer=${libraryVersion2}` + ); + assertEquivalent(responseAlt, response); + + await verifyKeys(json, 'collection', objectKeys.collection.slice(1)); + await verifyKeys(json, 'item', objectKeys.item.slice(1)); + await verifyKeys(json, 'search', objectKeys.search.slice(1)); + // Tags aren't deleted by removing from items + await verifyKeys(json, 'tag', []); + + // Explicit tag deletion + response = await API.userDelete( + config.userID, + `tags?key=${config.apiKey}&tag=${objectKeys.tag.join('%20||%20')}`, + { "If-Unmodified-Since-Version": libraryVersion3 } + ); + Helpers.assertStatusCode(response, 204); + + // Verify deleted tags + response = await API.userGet( + config.userID, + `deleted?key=${config.apiKey}&newer=${libraryVersion3}` + ); + Helpers.assertStatusCode(response, 200); + json = JSON.parse(response.data); + await verifyKeys(json, 'tag', objectKeys.tag); + }); +}); + From b30737fb8eb323987fb33810dfc3f66f704c52c2 Mon Sep 17 00:00:00 2001 From: Bogdan Abaev Date: Tue, 23 May 2023 09:37:09 -0400 Subject: [PATCH 05/33] leftover object tests, mappings, params tests --- tests/remote_js/api3.js | 4 +- tests/remote_js/config.js | 3 +- tests/remote_js/helpers3.js | 12 +- tests/remote_js/test/3/mappingsTest.js | 159 +++++++ tests/remote_js/test/3/objectTest.js | 308 +++++++++++++- tests/remote_js/test/3/paramsTest.js | 554 +++++++++++++++++++++++++ tests/remote_js/test/3/template.js | 7 + 7 files changed, 1039 insertions(+), 8 deletions(-) create mode 100644 tests/remote_js/test/3/mappingsTest.js create mode 100644 tests/remote_js/test/3/paramsTest.js diff --git a/tests/remote_js/api3.js b/tests/remote_js/api3.js index efa5ce3f..c68ea28e 100644 --- a/tests/remote_js/api3.js +++ b/tests/remote_js/api3.js @@ -359,7 +359,7 @@ class API3 extends API2 { } static createDataObject = async (objectType, data = false, context = false, format = 'json') => { - let template = this.createUnsavedDataObject(objectType); + let template = await this.createUnsavedDataObject(objectType); if (data) { for (let key in data) { template[key] = data[key]; @@ -778,7 +778,7 @@ class API3 extends API2 { case "item": // Convert to array - json = JSON.parse(JSON.stringify(this.getItemTemplate("book"))); + json = JSON.parse(JSON.stringify(await this.getItemTemplate("book"))); break; case "search": diff --git a/tests/remote_js/config.js b/tests/remote_js/config.js index aa50ac70..3b1b7e05 100644 --- a/tests/remote_js/config.js +++ b/tests/remote_js/config.js @@ -5,5 +5,6 @@ var config = {}; const data = fs.readFileSync('config.json'); config = JSON.parse(data); config.timeout = 60000; - +config.numOwnedGroups = 3; +config.numPublicGroups = 2; module.exports = config; diff --git a/tests/remote_js/helpers3.js b/tests/remote_js/helpers3.js index 447a31c2..aa7e0a0f 100644 --- a/tests/remote_js/helpers3.js +++ b/tests/remote_js/helpers3.js @@ -16,12 +16,12 @@ class Helpers3 extends Helpers { const json = JSON.parse(response.data); assert.lengthOf(Object.keys(json), expectedResults); } - else if (contentType == 'text/plain') { - const rows = response.data.split("\n").trim(); + else if (contentType.includes('text/plain')) { + const rows = response.data.trim().split("\n"); assert.lengthOf(rows, expectedResults); } else if (contentType == 'application/x-bibtex') { - let matched = response.getBody().match(/^@[a-z]+{/gm); + let matched = response.data.match(/^@[a-z]+{/gm); assert.equal(matched, expectedResults); } else if (contentType == 'application/atom+xml') { @@ -33,5 +33,11 @@ class Helpers3 extends Helpers { throw new Error(`Unknonw content type" ${contentType}`); } }; + + static assertRegExp(exp, val) { + if (!exp.test(val)) { + throw new Error(`${val} does not match regular expression`) + } + } } module.exports = Helpers3; diff --git a/tests/remote_js/test/3/mappingsTest.js b/tests/remote_js/test/3/mappingsTest.js new file mode 100644 index 00000000..4c1199bf --- /dev/null +++ b/tests/remote_js/test/3/mappingsTest.js @@ -0,0 +1,159 @@ +const chai = require('chai'); +const assert = chai.assert; +const config = require("../../config.js"); +const API = require('../../api3.js'); +const Helpers = require('../../helpers3.js'); +const { API3Setup, API3WrapUp } = require("../shared.js"); + +describe('MappingsTests', function () { + this.timeout(config.timeout); + + before(async function () { + await API3Setup(); + }); + + after(async function () { + await API3WrapUp(); + }); + + it('testLocale', async function () { + let response = await API.get("itemTypes?locale=fr-FR"); + Helpers.assert200(response); + let json = JSON.parse(response.data); + let o; + for (let i = 0; i < json.length; i++) { + if (json[i].itemType == 'book') { + o = json[i]; + break; + } + } + Helpers.assertEquals('Livre', o.localized); + }); + + it('test_should_return_fields_for_note_annotations', async function () { + let response = await API.get("items/new?itemType=annotation&annotationType=highlight"); + let json = await API.getJSONFromResponse(response); + assert.property(json, 'annotationText'); + Helpers.assertEquals(json.annotationText, ''); + }); + + it('test_should_reject_unknown_annotation_type', async function () { + let response = await API.get("items/new?itemType=annotation&annotationType=foo", { "Content-Type": "application/json" }); + Helpers.assert400(response); + }); + + it('testNewItem', async function () { + let response = await API.get("items/new?itemType=invalidItemType"); + Helpers.assert400(response); + + response = await API.get("items/new?itemType=book"); + Helpers.assert200(response); + Helpers.assertContentType(response, 'application/json'); + let json = JSON.parse(response.data); + Helpers.assertEquals('book', json.itemType); + }); + + it('testComputerProgramVersion', async function () { + let response = await API.get("items/new?itemType=computerProgram"); + Helpers.assert200(response); + let json = JSON.parse(response.data); + + assert.property(json, 'versionNumber'); + assert.notProperty(json, 'version'); + + response = await API.get("itemTypeFields?itemType=computerProgram"); + Helpers.assert200(response); + json = JSON.parse(response.data); + + let fields = json.map((val) => { + return val.field; + }); + + assert.include(fields, 'versionNumber'); + assert.notInclude(fields, 'version'); + }); + + it('test_should_return_fields_for_highlight_annotations', async function () { + const response = await API.get("items/new?itemType=annotation&annotationType=highlight"); + const json = await API.getJSONFromResponse(response); + assert.property(json, 'annotationText'); + assert.equal(json.annotationText, ''); + }); + + it('test_should_return_fields_for_all_annotation_types', async function () { + for (let type of ['highlight', 'note', 'image']) { + const response = await API.get(`items/new?itemType=annotation&annotationType=${type}`); + const json = await API.getJSONFromResponse(response); + + assert.property(json, 'annotationComment'); + Helpers.assertEquals('', json.annotationComment); + Helpers.assertEquals('', json.annotationColor); + Helpers.assertEquals('', json.annotationPageLabel); + Helpers.assertEquals('00000|000000|00000', json.annotationSortIndex); + assert.property(json, 'annotationPosition'); + Helpers.assertEquals(0, json.annotationPosition.pageIndex); + assert.isArray(json.annotationPosition.rects); + assert.notProperty(json, 'collections'); + assert.notProperty(json, 'relations'); + } + }); + + it('test_should_reject_missing_annotation_type', async function () { + let response = await API.get("items/new?itemType=annotation"); + Helpers.assert400(response); + }); + + it('test_should_return_attachment_fields', async function () { + let response = await API.get("items/new?itemType=attachment&linkMode=linked_url"); + let json = JSON.parse(response.data); + assert.equal(json.url, ''); + assert.notProperty(json, 'filename'); + assert.notProperty(json, 'path'); + + response = await API.get("items/new?itemType=attachment&linkMode=linked_file"); + json = JSON.parse(response.data); + assert.equal(json.path, ''); + assert.notProperty(json, 'filename'); + assert.notProperty(json, 'url'); + + response = await API.get("items/new?itemType=attachment&linkMode=imported_url"); + json = JSON.parse(response.data); + assert.equal(json.filename, ''); + assert.equal(json.url, ''); + assert.notProperty(json, 'path'); + + response = await API.get("items/new?itemType=attachment&linkMode=imported_file"); + json = JSON.parse(response.data); + assert.equal(json.filename, ''); + assert.notProperty(json, 'path'); + assert.notProperty(json, 'url'); + + response = await API.get("items/new?itemType=attachment&linkMode=embedded_image"); + json = JSON.parse(response.data); + assert.notProperty(json, 'title'); + assert.notProperty(json, 'url'); + assert.notProperty(json, 'accessDate'); + assert.notProperty(json, 'tags'); + assert.notProperty(json, 'collections'); + assert.notProperty(json, 'relations'); + assert.notProperty(json, 'note'); + assert.notProperty(json, 'charset'); + assert.notProperty(json, 'path'); + }); + + it('test_should_return_fields_for_image_annotations', async function () { + let response = await API.get('items/new?itemType=annotation&annotationType=image'); + let json = await API.getJSONFromResponse(response); + Helpers.assertEquals(0, json.annotationPosition.width); + Helpers.assertEquals(0, json.annotationPosition.height); + }); + + it('test_should_return_a_note_template', async function () { + let response = await API.get("items/new?itemType=note"); + Helpers.assert200(response); + Helpers.assertContentType(response, 'application/json'); + let json = API.getJSONFromResponse(response); + Helpers.assertEquals('note', json.itemType); + assert.property(json, 'note'); + }); +}); diff --git a/tests/remote_js/test/3/objectTest.js b/tests/remote_js/test/3/objectTest.js index 7651d10d..c0b742ea 100644 --- a/tests/remote_js/test/3/objectTest.js +++ b/tests/remote_js/test/3/objectTest.js @@ -7,6 +7,7 @@ const { API3Setup, API3WrapUp } = require("../shared.js"); describe('ObjectTests', function () { this.timeout(0); + let types = ['collection', 'search', 'item']; before(async function () { await API3Setup(); @@ -16,11 +17,11 @@ describe('ObjectTests', function () { await API3WrapUp(); }); - beforeEach(async function() { + beforeEach(async function () { await API.userClear(config.userID); }); - afterEach(async function() { + afterEach(async function () { await API.userClear(config.userID); }); @@ -481,5 +482,308 @@ describe('ObjectTests', function () { json = JSON.parse(response.data); await verifyKeys(json, 'tag', objectKeys.tag); }); + + + it('test_patch_with_deleted_should_clear_trash_state', async function () { + for (let type of types) { + const dataObj = { + deleted: true, + }; + const json = await API.createDataObject(type, dataObj, this); + // TODO: Change to true in APIv4 + if (type === 'item') { + assert.equal(json.data.deleted, 1); + } + else { + assert.ok(json.data.deleted); + } + const data = [ + { + key: json.key, + version: json.version, + deleted: false + } + ]; + const response = await API.postObjects(type, data); + const jsonResponse = await API.getJSONFromResponse(response); + assert.notProperty(jsonResponse.successful[0].data, 'deleted'); + } + }); + + const _testResponseJSONPut = async (objectType) => { + const objectPlural = API.getPluralObjectType(objectType); + let json1, conditions; + + switch (objectType) { + case 'collection': + json1 = { name: 'Test 1' }; + break; + + case 'item': + json1 = await API.getItemTemplate('book'); + json1.title = 'Test 1'; + break; + + case 'search': + conditions = [ + { + condition: 'title', + operator: 'contains', + value: 'value' + } + ]; + json1 = { name: 'Test 1', conditions }; + break; + } + + let response = await API.userPost( + config.userID, + `${objectPlural}`, + JSON.stringify([json1]), + { 'Content-Type': 'application/json' } + ); + + Helpers.assert200(response); + + let json = await API.getJSONFromResponse(response); + Helpers.assert200ForObject(response); + const objectKey = json.successful[0].key; + + response = await API.userGet( + config.userID, + `${objectPlural}/${objectKey}` + ); + + Helpers.assert200(response); + json = API.getJSONFromResponse(response); + + switch (objectType) { + case 'item': + json.data.title = 'Test 2'; + break; + + case 'collection': + case 'search': + json.data.name = 'Test 2'; + break; + } + + response = await API.userPut( + config.userID, + `${objectPlural}/${objectKey}`, + JSON.stringify(json), + { 'Content-Type': 'application/json' } + ); + + Helpers.assert204(response); + //check + response = await API.userGet( + config.userID, + `${objectPlural}/${objectKey}` + ); + + Helpers.assert200(response); + json = await API.getJSONFromResponse(response); + + switch (objectType) { + case 'item': + assert.equal(json.data.title, 'Test 2'); + break; + + case 'collection': + case 'search': + assert.equal(json.data.name, 'Test 2'); + break; + } + }; + + it('testResponseJSONPut', async function () { + await _testResponseJSONPut('collection'); + await _testResponseJSONPut('item'); + await _testResponseJSONPut('search'); + }); + + it('testCreateByPut', async function () { + await _testCreateByPut('collection'); + await _testCreateByPut('item'); + await _testCreateByPut('search'); + }); + + const _testCreateByPut = async (objectType) => { + const objectTypePlural = API.getPluralObjectType(objectType); + const json = await API.createUnsavedDataObject(objectType); + const key = Helpers.uniqueID(); + const response = await API.userPut( + config.userID, + `${objectTypePlural}/${key}`, + JSON.stringify(json), + { + 'Content-Type': 'application/json', + 'If-Unmodified-Since-Version': '0' + } + ); + Helpers.assert204(response); + }; + + const _testEmptyVersionsResponse = async (objectType) => { + const objectTypePlural = API.getPluralObjectType(objectType); + const keyProp = objectType + 'Key'; + + const response = await API.userGet( + config.userID, + `${objectTypePlural}?format=versions&${keyProp}=NNNNNNNN`, + { 'Content-Type': 'application/json' } + ); + + Helpers.assert200(response); + + const json = JSON.parse(response.data); + + assert.isObject(json); + assert.lengthOf(Object.keys(json), 0); + }; + + const _testResponseJSONPost = async (objectType) => { + await API.userClear(config.userID); + + let objectTypePlural = await API.getPluralObjectType(objectType); + let json1, json2, conditions; + switch (objectType) { + case "collection": + json1 = { name: "Test 1" }; + json2 = { name: "Test 2" }; + break; + + case "item": + json1 = await API.getItemTemplate("book"); + json2 = { ...json1 }; + json1.title = "Test 1"; + json2.title = "Test 2"; + break; + + case "search": + conditions = [ + { condition: "title", operator: "contains", value: "value" }, + ]; + json1 = { name: "Test 1", conditions: conditions }; + json2 = { name: "Test 2", conditions: conditions }; + break; + } + + let response = await API.userPost( + config.userID, + objectTypePlural, + JSON.stringify([json1, json2]), + { "Content-Type": "application/json" } + ); + Helpers.assert200(response); + let json = await API.getJSONFromResponse(response); + Helpers.assert200ForObject(response, false, 0); + Helpers.assert200ForObject(response, false, 1); + + response = await API.userGet(config.userID, objectTypePlural); + Helpers.assert200(response); + json = await API.getJSONFromResponse(response); + switch (objectType) { + case "item": + json[0].data.title + = json[0].data.title === "Test 1" ? "Test A" : "Test B"; + json[1].data.title + = json[1].data.title === "Test 2" ? "Test B" : "Test A"; + break; + + case "collection": + case "search": + json[0].data.name + = json[0].data.name === "Test 1" ? "Test A" : "Test B"; + json[1].data.name + = json[1].data.name === "Test 2" ? "Test B" : "Test A"; + break; + } + + response = await API.userPost( + config.userID, + objectTypePlural, + JSON.stringify(json), + { "Content-Type": "application/json" } + ); + Helpers.assert200(response); + json = await API.getJSONFromResponse(response); + Helpers.assert200ForObject(response, false, 0); + Helpers.assert200ForObject(response, false, 1); + + // Check + response = await API.userGet(config.userID, objectTypePlural); + Helpers.assert200(response); + json = await API.getJSONFromResponse(response); + + switch (objectTypePlural) { + case "item": + Helpers.assertEquals("Test A", json[0].data.title); + Helpers.assertEquals("Test B", json[1].data.title); + break; + + case "collection": + case "search": + Helpers.assertEquals("Test A", json[0].data.name); + Helpers.assertEquals("Test B", json[1].data.name); + break; + } + }; + + it('test_patch_of_object_should_set_trash_state', async function () { + for (let type of types) { + let json = await API.createDataObject(type); + const data = [ + { + key: json.key, + version: json.version, + deleted: true + } + ]; + const response = await API.postObjects(type, data); + Helpers.assert200ForObject(response); + json = API.getJSONFromResponse(response); + assert.property(json.successful[0].data, 'deleted'); + if (type == 'item') { + assert.equal(json.successful[0].data.deleted, 1); + } + else { + assert.property(json.successful[0].data, 'deleted'); + } + } + }); + + it('testResponseJSONPost', async function () { + await _testResponseJSONPost('collection'); + await _testResponseJSONPost('item'); + await _testResponseJSONPost('search'); + }); + + it('testEmptyVersionsResponse', async function () { + await _testEmptyVersionsResponse('collection'); + await _testEmptyVersionsResponse('item'); + await _testEmptyVersionsResponse('search'); + }); + + it('test_patch_of_object_in_trash_without_deleted_should_not_remove_it_from_trash', async function () { + for (let i = 0; i < types.length; i++) { + const json = await API.createItem("book", { + deleted: true + }, this, 'json'); + const data = [ + { + key: json.key, + version: json.version, + title: "A" + } + ]; + const response = await API.postItems(data); + const jsonResponse = await API.getJSONFromResponse(response); + + assert.property(jsonResponse.successful[0].data, 'deleted'); + assert.equal(jsonResponse.successful[0].data.deleted, 1); + } + }); }); diff --git a/tests/remote_js/test/3/paramsTest.js b/tests/remote_js/test/3/paramsTest.js new file mode 100644 index 00000000..785e1fec --- /dev/null +++ b/tests/remote_js/test/3/paramsTest.js @@ -0,0 +1,554 @@ +const chai = require('chai'); +const assert = chai.assert; +const config = require("../../config.js"); +const API = require('../../api3.js'); +const Helpers = require('../../helpers3.js'); +const { API3Setup, API3WrapUp } = require("../shared.js"); + +describe('ParamsTests', function () { + this.timeout(0); + + let collectionKeys = []; + let itemKeys = []; + let searchKeys = []; + + let keysByName = { + collectionKeys: collectionKeys, + itemKeys: itemKeys, + searchKeys: searchKeys + }; + + before(async function () { + await API3Setup(); + }); + + after(async function () { + await API3WrapUp(); + }); + beforeEach(async function () { + await API.userClear(config.userID); + }); + + afterEach(async function () { + await API.userClear(config.userID); + }); + + const parseLinkHeader = (links) => { + assert.isNotNull(links); + const parsedLinks = []; + for (let link of links.split(',')) { + link = link.trim(); + let [uri, rel] = link.split('; '); + Helpers.assertRegExp(/^$/, uri); + Helpers.assertRegExp(/^rel="[a-z]+"$/, rel); + uri = uri.slice(1, -1); + rel = rel.slice(5, -1); + const params = {}; + new URLSearchParams(new URL(uri).search.slice(1)).forEach((value, key) => { + params[key] = value; + }); + parsedLinks[rel] = { + uri: uri, + params: params + }; + } + return parsedLinks; + }; + + it('testPaginationWithItemKey', async function () { + let totalResults = 27; + + for (let i = 0; i < totalResults; i++) { + await API.createItem("book", false, this, 'key'); + } + + let response = await API.userGet( + config.userID, + "items?format=keys&limit=50", + { "Content-Type": "application/json" } + ); + let keys = response.data.trim().split("\n"); + + response = await API.userGet( + config.userID, + "items?format=json&itemKey=" + keys.join(","), + { "Content-Type": "application/json" } + ); + let json = API.getJSONFromResponse(response); + Helpers.assertCount(totalResults, json); + }); + + + const _testPagination = async (objectType) => { + await API.userClear(config.userID); + const objectTypePlural = await API.getPluralObjectType(objectType); + + let limit = 2; + let totalResults = 5; + let formats = ['json', 'atom', 'keys']; + + // Create sample data + switch (objectType) { + case 'collection': + case 'item': + case 'search': + case 'tag': + await _createPaginationData(objectType, totalResults); + break; + } + let filteredFormats; + switch (objectType) { + case 'item': + formats.push('bibtex'); + break; + + case 'tag': + filteredFormats = formats.filter(val => !['keys'].includes(val)); + formats = filteredFormats; + break; + + case 'group': + // Change if the config changes + limit = 1; + totalResults = config.numOwnedGroups; + formats = formats.filter(val => !['keys'].includes(val)); + break; + } + + const func = async (start, format) => { + const response = await API.userGet( + config.userID, + `${objectTypePlural}?start=${start}&limit=${limit}&format=${format}` + ); + + Helpers.assert200(response); + Helpers.assertNumResults(response, limit); + Helpers.assertTotalResults(response, totalResults); + + const linksString = response.headers.link[0]; + const links = parseLinkHeader(linksString); + assert.property(links, 'first'); + assert.notProperty(links.first.params, 'start'); + Helpers.assertEquals(limit, links.first.params.limit); + assert.property(links, 'prev'); + + Helpers.assertEquals(limit, links.prev.params.limit); + + + assert.property(links, 'last'); + if (start < 3) { + Helpers.assertEquals(start + limit, links.next.params.start); + Helpers.assertEquals(limit, links.next.params.limit); + assert.notProperty(links.prev.params, 'start'); + assert.property(links, 'next'); + } + else { + assert.equal(Math.max(start - limit, 0), parseInt(links.prev.params.start)); + assert.notProperty(links, 'next'); + } + + let lastStart = totalResults - (totalResults % limit); + + if (lastStart == totalResults) { + lastStart -= limit; + } + + Helpers.assertEquals(lastStart, links.last.params.start); + Helpers.assertEquals(limit, links.last.params.limit); + }; + + for (const format of formats) { + const response = await API.userGet( + config.userID, + `${objectTypePlural}?limit=${limit}&format=${format}` + ); + + Helpers.assert200(response); + Helpers.assertNumResults(response, limit); + Helpers.assertTotalResults(response, totalResults); + + const linksString = response.headers.link[0]; + const links = parseLinkHeader(linksString); + assert.notProperty(links, 'first'); + assert.notProperty(links, 'prev'); + assert.property(links, 'next'); + Helpers.assertEquals(limit, links.next.params.start); + Helpers.assertEquals(limit, links.next.params.limit); + assert.property(links, 'last'); + + let lastStart = totalResults - (totalResults % limit); + + if (lastStart == totalResults) { + lastStart -= limit; + } + + Helpers.assertEquals(lastStart, links.last.params.start); + Helpers.assertEquals(limit, links.last.params.limit); + + // TODO: Test with more groups + if (objectType == 'group') { + continue; + } + + await func(1, format); + await func(2, format); + await func(3, format); + } + }; + + + it('test_should_perform_quicksearch_with_multiple_words', async function () { + let title1 = "This Is a Great Title"; + let title2 = "Great, But Is It Better Than This Title?"; + + let keys = []; + keys.push(await API.createItem("book", { + title: title1 + }, this, 'key')); + keys.push(await API.createItem("journalArticle", { + title: title2, + }, this, 'key')); + + // Search by multiple independent words + let q = "better title"; + let response = await API.userGet( + config.userID, + "items?q=" + encodeURIComponent(q) + ); + Helpers.assert200(response); + Helpers.assertNumResults(response, 1); + let json = await API.getJSONFromResponse(response); + Helpers.assertEquals(keys[1], json[0].key); + + // Search by phrase + q = '"great title"'; + response = await API.userGet( + config.userID, + "items?q=" + encodeURIComponent(q) + ); + Helpers.assert200(response); + Helpers.assertNumResults(response, 1); + json = await API.getJSONFromResponse(response); + Helpers.assertEquals(keys[0], json[0].key); + + // Search by non-matching phrase + q = '"better title"'; + response = await API.userGet( + config.userID, + "items?q=" + encodeURIComponent(q) + ); + Helpers.assert200(response); + Helpers.assertNumResults(response, 0); + }); + + it('testFormatKeys', async function () { + for (let i = 0; i < 5; i++) { + collectionKeys.push(await API.createCollection("Test", false, null, 'key')); + itemKeys.push(await API.createItem("book", false, null, 'key')); + searchKeys.push(await API.createSearch("Test", 'default', null, 'key')); + } + itemKeys.push(await API.createAttachmentItem("imported_file", [], false, null, 'key')); + + await _testFormatKeys('collection'); + await _testFormatKeys('item'); + await _testFormatKeys('search'); + + await _testFormatKeys('collection', true); + await _testFormatKeys('item', true); + await _testFormatKeys('search', true); + }); + + const _testFormatKeys = async (objectType, sorted = false) => { + let objectTypePlural = await API.getPluralObjectType(objectType); + let keysVar = objectType + "Keys"; + + let response = await API.userGet( + config.userID, + `${objectTypePlural}?format=keys${sorted ? "&order=title" : ""}` + ); + Helpers.assert200(response); + + let keys = response.data.trim().split("\n"); + keys.sort(); + const keysVarCopy = keysByName[keysVar]; + keysVarCopy.sort(); + assert.deepEqual(keys, keysVarCopy); + }; + + const _createPaginationData = async (objectType, num) => { + switch (objectType) { + case 'collection': + for (let i = 0; i < num; i++) { + await API.createCollection("Test", false, true, 'key'); + } + break; + + case 'item': + for (let i = 0; i < num; i++) { + await API.createItem("book", false, true, 'key'); + } + break; + + case 'search': + for (let i = 0; i < num; i++) { + await API.createSearch("Test", 'default', true, 'key'); + } + break; + + case 'tag': + await API.createItem("book", { + tags: [ + { tag: 'a' }, + { tag: 'b' } + ] + }, true); + await API.createItem("book", { + tags: [ + { tag: 'c' }, + { tag: 'd' }, + { tag: 'e' } + ] + }, true); + break; + } + }; + + // items requires translation to run + it('testPagination', async function () { + this.skip(); + await _testPagination('collection'); + await _testPagination('group'); + + await _testPagination('item'); + await _testPagination('search'); + await _testPagination('tag'); + }); + + it('testCollectionQuickSearch', async function () { + let title1 = "Test Title"; + let title2 = "Another Title"; + + let keys = []; + keys.push(await API.createCollection(title1, [], this, 'key')); + keys.push(await API.createCollection(title2, [], this, 'key')); + + // Search by title + let response = await API.userGet( + config.userID, + "collections?q=another" + ); + Helpers.assert200(response); + Helpers.assertNumResults(response, 1); + let json = await API.getJSONFromResponse(response); + Helpers.assertEquals(keys[1], json[0].key); + + // No results + response = await API.userGet( + config.userID, + "collections?q=nothing" + ); + Helpers.assert200(response); + Helpers.assertNumResults(response, 0); + }); + + it('test_should_include_since_parameter_in_next_link', async function () { + let totalResults = 6; + let item = await API.createItem("book", false, true, 'json'); + let since = item.version; + + for (let i = 0; i < totalResults; i++) { + await API.createItem("book", false, 'key'); + } + + let response = await API.userGet( + config.userID, + `items?limit=5&since=${since}` + ); + + let json = API.getJSONFromResponse(response); + let linkParams = parseLinkHeader(response.headers.link[0]).next.params; + + assert.equal(linkParams.limit, 5); + assert.property(linkParams, 'since'); + + assert.lengthOf(json, 5); + Helpers.assertNumResults(response, 5); + Helpers.assertTotalResults(response, totalResults); + }); + + + it('testItemQuickSearchOrderByDate', async function () { + let title1 = "Test Title"; + let title2 = "Another Title"; + let keys = []; + keys.push(await API.createItem("book", { + title: title1, + date: "February 12, 2013" + }, this, 'key')); + keys.push(await API.createItem("journalArticle", { + title: title2, + date: "November 25, 2012" + }, this, 'key')); + let response = await API.userGet( + config.userID, + "items?q=" + encodeURIComponent(title1) + ); + Helpers.assert200(response); + Helpers.assertNumResults(response, 1); + let json = API.getJSONFromResponse(response); + Helpers.assertEquals(keys[0], json[0].key); + response = await API.userGet( + config.userID, + "items?q=title&sort=date&direction=asc" + ); + Helpers.assert200(response); + Helpers.assertNumResults(response, 2); + json = API.getJSONFromResponse(response); + Helpers.assertEquals(keys[1], json[0].key); + Helpers.assertEquals(keys[0], json[1].key); + response = await API.userGet( + config.userID, + "items?q=title&order=date&sort=asc" + ); + Helpers.assert200(response); + Helpers.assertNumResults(response, 2); + json = API.getJSONFromResponse(response); + Helpers.assertEquals(keys[1], json[0].key); + Helpers.assertEquals(keys[0], json[1].key); + response = await API.userGet( + config.userID, + "items?q=title&sort=date&direction=desc" + ); + Helpers.assert200(response); + Helpers.assertNumResults(response, 2); + json = API.getJSONFromResponse(response); + Helpers.assertEquals(keys[0], json[0].key); + Helpers.assertEquals(keys[1], json[1].key); + response = await API.userGet( + config.userID, + "items?q=title&order=date&sort=desc" + ); + Helpers.assert200(response); + Helpers.assertNumResults(response, 2); + json = API.getJSONFromResponse(response); + Helpers.assertEquals(keys[0], json[0].key); + Helpers.assertEquals(keys[1], json[1].key); + }); + + it('testObjectKeyParameter', async function () { + await _testObjectKeyParameter('collection'); + await _testObjectKeyParameter('item'); + await _testObjectKeyParameter('search'); + }); + + it('testItemQuickSearch', async function () { + let title1 = "Test Title"; + let title2 = "Another Title"; + let year2 = "2013"; + + let keys = []; + keys.push(await API.createItem("book", { + title: title1 + }, this, 'key')); + keys.push(await API.createItem("journalArticle", { + title: title2, + date: "November 25, " + year2 + }, this, 'key')); + + // Search by title + let response = await API.userGet( + config.userID, + "items?q=" + encodeURIComponent(title1) + ); + Helpers.assert200(response); + Helpers.assertNumResults(response, 1); + let json = API.getJSONFromResponse(response); + Helpers.assertEquals(keys[0], json[0].key); + + // TODO: Search by creator + + // Search by year + response = await API.userGet( + config.userID, + "items?q=" + year2 + ); + Helpers.assert200(response); + Helpers.assertNumResults(response, 1); + json = API.getJSONFromResponse(response); + Helpers.assertEquals(keys[1], json[0].key); + + // Search by year + 1 + response = await API.userGet( + config.userID, + "items?q=" + (parseInt(year2) + 1) + ); + Helpers.assert200(response); + Helpers.assertNumResults(response, 0); + }); + + const _testObjectKeyParameter = async (objectType) => { + const objectTypePlural = API.getPluralObjectType(objectType); + let jsonArray = []; + switch (objectType) { + case 'collection': + jsonArray.push(await API.createCollection("Name", false, this, 'jsonData')); + jsonArray.push(await API.createCollection("Name", false, this, 'jsonData')); + break; + case 'item': + jsonArray.push(await API.createItem("book", false, this, 'jsonData')); + jsonArray.push(await API.createItem("book", false, this, 'jsonData')); + break; + case 'search': + jsonArray.push(await API.createSearch( + "Name", + [ + { + condition: "title", + operator: "contains", + value: "test", + }, + ], + this, + 'jsonData' + )); + jsonArray.push(await API.createSearch( + "Name", + [ + { + condition: "title", + operator: "contains", + value: "test", + }, + ], + this, + 'jsonData' + )); + break; + } + let keys = []; + jsonArray.forEach((json) => { + keys.push(json.key); + }); + + let response = await API.userGet( + config.userID, + `${objectTypePlural}?${objectType}Key=${keys[0]}` + ); + Helpers.assert200(response); + Helpers.assertNumResults(response, 1); + Helpers.assertTotalResults(response, 1); + let json = await API.getJSONFromResponse(response); + Helpers.assertEquals(keys[0], json[0].key); + + response = await API.userGet( + config.userID, + `${objectTypePlural}?${objectType}Key=${keys[0]},${keys[1]}&order=${objectType}KeyList` + ); + Helpers.assert200(response); + Helpers.assertNumResults(response, 2); + Helpers.assertTotalResults(response, 2); + json = API.getJSONFromResponse(response); + Helpers.assertEquals(keys[0], json[0].key); + Helpers.assertEquals(keys[1], json[1].key); + }; +}); diff --git a/tests/remote_js/test/3/template.js b/tests/remote_js/test/3/template.js index 9f0f9733..cca3f068 100644 --- a/tests/remote_js/test/3/template.js +++ b/tests/remote_js/test/3/template.js @@ -15,4 +15,11 @@ describe('Tests', function () { after(async function () { await API3WrapUp(); }); + beforeEach(async function () { + await API.userClear(config.userID); + }); + + afterEach(async function () { + await API.userClear(config.userID); + }); }); From aae33b188239413118fdd7c49ccef53bbee13286 Mon Sep 17 00:00:00 2001 From: Bogdan Abaev Date: Tue, 23 May 2023 15:42:27 -0400 Subject: [PATCH 06/33] previously skipped tests requiring citation or translation server --- tests/remote_js/helpers.js | 6 + tests/remote_js/helpers3.js | 2 +- tests/remote_js/test/2/atomTest.js | 15 +- tests/remote_js/test/2/bibTest.js | 155 +++++++++++++- tests/remote_js/test/3/atomTest.js | 57 +++-- tests/remote_js/test/3/bibTest.js | 303 +++++++++++++++++++++++++++ tests/remote_js/test/3/paramsTest.js | 2 - 7 files changed, 493 insertions(+), 47 deletions(-) create mode 100644 tests/remote_js/test/3/bibTest.js diff --git a/tests/remote_js/helpers.js b/tests/remote_js/helpers.js index 3234f9e2..e0d67f69 100644 --- a/tests/remote_js/helpers.js +++ b/tests/remote_js/helpers.js @@ -55,6 +55,12 @@ class Helpers { return multiple ? result.map(el => el.innerHTML) : result[0].innerHTML; }; + static assertXMLEqual = (one, two) => { + const contentDom = new JSDOM(one); + const expectedDom = new JSDOM(two); + assert.equal(contentDom.window.document.innerHTML, expectedDom.window.document.innerHTML); + }; + static assertStatusCode = (response, expectedCode, message) => { try { assert.equal(response.status, expectedCode); diff --git a/tests/remote_js/helpers3.js b/tests/remote_js/helpers3.js index aa7e0a0f..de013ebd 100644 --- a/tests/remote_js/helpers3.js +++ b/tests/remote_js/helpers3.js @@ -22,7 +22,7 @@ class Helpers3 extends Helpers { } else if (contentType == 'application/x-bibtex') { let matched = response.data.match(/^@[a-z]+{/gm); - assert.equal(matched, expectedResults); + assert.lengthOf(matched, expectedResults); } else if (contentType == 'application/atom+xml') { const doc = new JSDOM(response.data, { url: "http://localhost/" }); diff --git a/tests/remote_js/test/2/atomTest.js b/tests/remote_js/test/2/atomTest.js index 513bf11e..e475baec 100644 --- a/tests/remote_js/test/2/atomTest.js +++ b/tests/remote_js/test/2/atomTest.js @@ -3,6 +3,7 @@ const assert = chai.assert; const config = require("../../config.js"); const API = require('../../api2.js'); const Helpers = require('../../helpers.js'); +const { JSDOM } = require('jsdom'); const { API2Setup, API2WrapUp } = require("../shared.js"); describe('CollectionTests', function () { @@ -90,9 +91,6 @@ describe('CollectionTests', function () { //Requires citation server to run it('testMultiContent', async function () { - this.skip(); - - const keys = Object.keys(keyObj); const keyStr = keys.join(','); @@ -106,9 +104,9 @@ describe('CollectionTests', function () { const entries = Helpers.xpathEval(xml, '//atom:entry', true, true); for (const entry of entries) { - const key = entry.children('http://zotero.org/ns/api',).key.textContent; - let content = entry.content.asXML(); - + const key = entry.getElementsByTagName("zapi:key")[0].innerHTML; + let content = entry.getElementsByTagName("content")[0].outerHTML; + content = content.replace( 'Last, Title.', + apa: '(Last, n.d.)' + }, + bib: { + default: '
Last, First. Title, n.d.
', + apa: '
Last, F. (n.d.). Title.
' + } + + }; + + key = await API.createItem("book", { + title: "Title 2", + creators: [ + { + creatorType: "author", + firstName: "First", + lastName: "Last" + } + ] + }, null, 'key'); + + + items[key] = { + + citation: { + default: 'Last, Title 2.', + apa: '(Last, n.d.)' + }, + bib: { + default: '
Last, First. Title 2. Edited by Ed McEditor, n.d.
', + apa: '
Last, F. (n.d.). Title 2 (E. McEditor, Ed.).
' + } + + }; }); after(async function () { await API2WrapUp(); }); + + it('testContentCitationMulti', async function () { + let keys = Object.keys(items); + let keyStr = keys.join(','); + for (let style of styles) { + let response = await API.userGet( + config.userID, + `items?key=${config.apiKey}&itemKey=${keyStr}&content=citation${style == "default" ? "" : "&style=" + encodeURIComponent(style)}` + ); + Helpers.assert200(response); + Helpers.assertTotalResults(response, keys.length); + let xml = API.getXMLFromResponse(response); + assert.equal(Helpers.xpathEval(xml, '/atom:feed/zapi:totalResults'), keys.length); + + let entries = Helpers.xpathEval(xml, '//atom:entry', true, true); + for (let entry of entries) { + const key = entry.getElementsByTagName("zapi:key")[0].innerHTML; + let content = entry.getElementsByTagName("content")[0].outerHTML; + content = content.replace(''; key = await API.createItem("book", { @@ -38,7 +39,7 @@ describe('AtomTests', function () { } ] }, false, "key"); - items[key] = '
Last, First. Title 2. Edited by Ed McEditor, n.d.
' + keyObj[key] = '
Last, First. Title 2. Edited by Ed McEditor, n.d.
' + '{"key":"","version":0,"itemType":"book","title":"Title 2","creators":[{"creatorType":"author","firstName":"First","lastName":"Last"},{"creatorType":"editor","firstName":"Ed","lastName":"McEditor"}],"abstractNote":"","series":"","seriesNumber":"","volume":"","numberOfVolumes":"","edition":"","place":"","publisher":"","date":"","numPages":"","language":"","ISBN":"","shortTitle":"","url":"","accessDate":"","archive":"","archiveLocation":"","libraryCatalog":"","callNumber":"","rights":"","extra":"","tags":[],"collections":[],"relations":{},"dateAdded":"","dateModified":""}' + '
'; }); @@ -72,40 +73,38 @@ describe('AtomTests', function () { //Requires citation server to run it('testMultiContent', async function () { - this.skip(); - let keys = Object.keys(items); - let keyStr = keys.join(','); - - let response = await API.userGet( + const keys = Object.keys(keyObj); + const keyStr = keys.join(','); + + const response = await API.userGet( config.userID, - { params: { itemKey: keyStr, content: 'bib,json' } } + `items?itemKey=${keyStr}&content=bib,json`, ); - Helpers.assert200(response); - let xml = await API.getXMLFromResponse(response); + Helpers.assertStatusCode(response, 200); + const xml = await API.getXMLFromResponse(response); Helpers.assertTotalResults(response, keys.length); + + const entries = Helpers.xpathEval(xml, '//atom:entry', true, true); + for (const entry of entries) { + const key = entry.getElementsByTagName("zapi:key")[0].innerHTML; + let content = entry.getElementsByTagName("content")[0].outerHTML; - let entries = xml.xpath('//atom:entry'); - for (let i = 0; i < entries.length; i++) { - let entry = entries[i]; - let key = entry.children("http://zotero.org/ns/api").key.toString(); - let content = entry.content.asXML(); - - // Add namespace prefix (from ) - content = content.replace('Doe and Smith, Title.', + apa: '(Doe & Smith, 2014)', + "https://www.zotero.org/styles/apa": '(Doe & Smith, 2014)', + "https://raw.githubusercontent.com/citation-style-language/styles/master/ieee.csl": '[1]' + }, + bib: { + default: '
Doe, Alice, and Bob Smith. Title, 2014.
', + apa: '
Doe, A., & Smith, B. (2014). Title.
', + "https://www.zotero.org/styles/apa": '
Doe, A., & Smith, B. (2014). Title.
', + "https://raw.githubusercontent.com/citation-style-language/styles/master/ieee.csl": '
[1]
A. Doe and B. Smith, Title. 2014.
' + } + }, + atom: { + citation: { + default: 'Doe and Smith, Title.', + apa: '(Doe & Smith, 2014)', + "https://www.zotero.org/styles/apa": '(Doe & Smith, 2014)', + "https://raw.githubusercontent.com/citation-style-language/styles/master/ieee.csl": '[1]' + }, + bib: { + default: '
Doe, Alice, and Bob Smith. Title, 2014.
', + apa: '
Doe, A., & Smith, B. (2014). Title.
', + "https://www.zotero.org/styles/apa": '
Doe, A., & Smith, B. (2014). Title.
', + "https://raw.githubusercontent.com/citation-style-language/styles/master/ieee.csl": '
[1]
A. Doe and B. Smith, Title. 2014.
' + } + } + }; + + key = await API.createItem("book", { + title: "Title 2", + date: "June 24, 2014", + creators: [ + { + creatorType: "author", + firstName: "Jane", + lastName: "Smith" + }, + { + creatorType: "editor", + firstName: "Ed", + lastName: "McEditor" + } + ] + }, null, 'key'); + + + items[key] = { + json: { + citation: { + default: 'Smith, Title 2.', + apa: '(Smith, 2014)', + "https://www.zotero.org/styles/apa": '(Smith, 2014)', + "https://raw.githubusercontent.com/citation-style-language/styles/master/ieee.csl": '[1]' + }, + bib: { + default: '
Smith, Jane. Title 2. Edited by Ed McEditor, 2014.
', + apa: '
Smith, J. (2014). Title 2 (E. McEditor, Ed.).
', + "https://www.zotero.org/styles/apa": '
Smith, J. (2014). Title 2 (E. McEditor, Ed.).
', + "https://raw.githubusercontent.com/citation-style-language/styles/master/ieee.csl": '
[1]
J. Smith, Title 2. 2014.
' + } + }, + atom: { + citation: { + default: 'Smith, Title 2.', + apa: '(Smith, 2014)', + "https://www.zotero.org/styles/apa": '(Smith, 2014)', + "https://raw.githubusercontent.com/citation-style-language/styles/master/ieee.csl": '[1]' + }, + bib: { + default: '
Smith, Jane. Title 2. Edited by Ed McEditor, 2014.
', + apa: '
Smith, J. (2014). Title 2 (E. McEditor, Ed.).
', + "https://www.zotero.org/styles/apa": '
Smith, J. (2014). Title 2 (E. McEditor, Ed.).
', + "https://raw.githubusercontent.com/citation-style-language/styles/master/ieee.csl": '
[1]
J. Smith, Title 2. 2014.
' + } + } + }; + + + multiResponses = { + default: '
Doe, Alice, and Bob Smith. Title, 2014.
Smith, Jane. Title 2. Edited by Ed McEditor, 2014.
', + apa: '
Doe, A., & Smith, B. (2014). Title.
Smith, J. (2014). Title 2 (E. McEditor, Ed.).
', + "https://www.zotero.org/styles/apa": '
Doe, A., & Smith, B. (2014). Title.
Smith, J. (2014). Title 2 (E. McEditor, Ed.).
', + "https://raw.githubusercontent.com/citation-style-language/styles/master/ieee.csl": '
[1]
J. Smith, Title 2. 2014.
[2]
A. Doe and B. Smith, Title. 2014.
' + }; + + multiResponsesLocales = { + fr: '
Doe, Alice, et Bob Smith. Title, 2014.
Smith, Jane. Title 2. Édité par Ed McEditor, 2014.
' + }; + }); + + after(async function () { + await API3WrapUp(); + }); + + it('testContentCitationMulti', async function () { + let keys = Object.keys(items); + let keyStr = keys.join(','); + for (let style of styles) { + let response = await API.userGet( + config.userID, + `items?itemKey=${keyStr}&content=citation${style == "default" ? "" : "&style=" + encodeURIComponent(style)}` + ); + Helpers.assert200(response); + Helpers.assertTotalResults(response, keys.length); + let xml = API.getXMLFromResponse(response); + let entries = Helpers.xpathEval(xml, '//atom:entry', true, true); + for (let entry of entries) { + const key = entry.getElementsByTagName("zapi:key")[0].innerHTML; + let content = entry.getElementsByTagName("content")[0].outerHTML; + content = content.replace(' Date: Wed, 24 May 2023 17:26:57 -0400 Subject: [PATCH 07/33] file tests v2 and half of v3 --- tests/remote_js/helpers.js | 18 + tests/remote_js/helpers3.js | 6 - tests/remote_js/httpHandler.js | 7 + tests/remote_js/test/2/fileTest.js | 696 +++++++++++++++++++++++++++- tests/remote_js/test/2/fullText.js | 2 +- tests/remote_js/test/3/fileTest.js | 711 +++++++++++++++++++++++++++++ 6 files changed, 1427 insertions(+), 13 deletions(-) create mode 100644 tests/remote_js/test/3/fileTest.js diff --git a/tests/remote_js/helpers.js b/tests/remote_js/helpers.js index e0d67f69..3f8c6f56 100644 --- a/tests/remote_js/helpers.js +++ b/tests/remote_js/helpers.js @@ -55,6 +55,12 @@ class Helpers { return multiple ? result.map(el => el.innerHTML) : result[0].innerHTML; }; + static assertRegExp(exp, val) { + if (!exp.test(val)) { + throw new Error(`${val} does not match regular expression`) + } + } + static assertXMLEqual = (one, two) => { const contentDom = new JSDOM(one); const expectedDom = new JSDOM(two); @@ -122,11 +128,19 @@ class Helpers { static assert200 = (response) => { this.assertStatusCode(response, 200); }; + + static assert201 = (response) => { + this.assertStatusCode(response, 201); + }; static assert204 = (response) => { this.assertStatusCode(response, 204); }; + static assert302 = (response) => { + this.assertStatusCode(response, 302); + }; + static assert400 = (response) => { this.assertStatusCode(response, 400); }; @@ -135,6 +149,10 @@ class Helpers { this.assertStatusCode(response, 403); }; + static assert412 = (response) => { + this.assertStatusCode(response, 412); + }; + static assert428 = (response) => { this.assertStatusCode(response, 428); }; diff --git a/tests/remote_js/helpers3.js b/tests/remote_js/helpers3.js index de013ebd..24f645b5 100644 --- a/tests/remote_js/helpers3.js +++ b/tests/remote_js/helpers3.js @@ -33,11 +33,5 @@ class Helpers3 extends Helpers { throw new Error(`Unknonw content type" ${contentType}`); } }; - - static assertRegExp(exp, val) { - if (!exp.test(val)) { - throw new Error(`${val} does not match regular expression`) - } - } } module.exports = Helpers3; diff --git a/tests/remote_js/httpHandler.js b/tests/remote_js/httpHandler.js index 09a2da82..d8d432dc 100644 --- a/tests/remote_js/httpHandler.js +++ b/tests/remote_js/httpHandler.js @@ -8,6 +8,8 @@ class HTTP { let options = { method: method, headers: headers, + follow: 0, + redirect: 'manual', body: ["POST", "PUT", "PATCH", "DELETE"].includes(method) ? data : null }; @@ -19,6 +21,11 @@ class HTTP { console.log(`\n${method} ${url}\n`); } + //Hardcoded for running tests against containers + if (url.includes("172.16.0.11")) { + url = url.replace('172.16.0.11', 'localhost'); + } + let response = await fetch(url, options); // Fetch doesn't automatically parse the response body, so we have to do that manually diff --git a/tests/remote_js/test/2/fileTest.js b/tests/remote_js/test/2/fileTest.js index 7542bf9d..c5498370 100644 --- a/tests/remote_js/test/2/fileTest.js +++ b/tests/remote_js/test/2/fileTest.js @@ -1,19 +1,703 @@ -//const chai = require('chai'); -// const assert = chai.assert; +const chai = require('chai'); +const assert = chai.assert; const config = require("../../config.js"); -// const API = require('../../api2.js'); -// const Helpers = require('../../helpers.js'); +const API = require('../../api2.js'); +const Helpers = require('../../helpers.js'); const { API2Setup, API2WrapUp } = require("../shared.js"); +const { S3Client, DeleteObjectsCommand, PutObjectCommand } = require("@aws-sdk/client-s3"); +const fs = require('fs'); +const HTTP = require('../../httpHandler.js'); +const crypto = require('crypto'); +const util = require('util'); +const exec = util.promisify(require('child_process').exec); -//Skipped - uses S3 describe('FileTestTests', function () { - this.timeout(config.timeout); + this.timeout(0); + let toDelete = []; + const s3Client = new S3Client({ region: "us-east-1" }); before(async function () { await API2Setup(); + try { + fs.mkdirSync("./work"); + } + catch {} }); after(async function () { await API2WrapUp(); + fs.rmdirSync("./work", { recursive: true, force: true }); + if (toDelete.length > 0) { + const commandInput = { + Bucket: config.s3Bucket, + Delete: { + Objects: toDelete.map((x) => { + return { Key: x }; + }) + } + }; + const command = new DeleteObjectsCommand(commandInput); + await s3Client.send(command); + } }); + + const md5 = (str) => { + return crypto.createHash('md5').update(str).digest('hex'); + }; + + const md5File = (fileName) => { + const data = fs.readFileSync(fileName); + return crypto.createHash('md5').update(data).digest('hex'); + }; + + const testNewEmptyImportedFileAttachmentItem = async () => { + let xml = await API.createAttachmentItem("imported_file", [], false, this); + let data = await API.parseDataFromAtomEntry(xml); + return data; + }; + + const testGetFile = async () => { + const addFileData = await testAddFileExisting(); + const userGetViewModeResponse = await API.userGet(config.userID, `items/${addFileData.key}/file/view?key=${config.apiKey}`); + Helpers.assert302(userGetViewModeResponse); + const location = userGetViewModeResponse.headers.location[0]; + Helpers.assertRegExp(/^https?:\/\/[^/]+\/[a-zA-Z0-9%]+\/[a-f0-9]{64}\/test_/, location); + const filenameEncoded = encodeURIComponent(addFileData.filename); + assert.equal(filenameEncoded, location.substring(location.length - filenameEncoded.length)); + const viewModeResponse = await HTTP.get(location); + Helpers.assert200(viewModeResponse); + assert.equal(addFileData.md5, md5(viewModeResponse.data)); + const userGetDownloadModeResponse = await API.userGet(config.userID, `items/${addFileData.key}/file?key=${config.apiKey}`); + Helpers.assert302(userGetDownloadModeResponse); + const downloadModeLocation = userGetDownloadModeResponse.headers.location; + const s3Response = await HTTP.get(downloadModeLocation); + Helpers.assert200(s3Response); + assert.equal(addFileData.md5, md5(s3Response.data)); + return { + key: addFileData.key, + response: s3Response + }; + }; + + it('testAddFileLinkedAttachment', async function () { + let xml = await API.createAttachmentItem("linked_file", [], false, this); + let data = await API.parseDataFromAtomEntry(xml); + + let file = "./work/file"; + let fileContents = getRandomUnicodeString(); + fs.writeFileSync(file, fileContents); + let hash = md5File(file); + let filename = "test_" + fileContents; + let mtime = fs.statSync(file).mtimeMs; + let size = fs.statSync(file).size; + let contentType = "text/plain"; + let charset = "utf-8"; + + // Get upload authorization + let response = await API.userPost( + config.userID, + `items/${data.key}/file?key=${config.apiKey}`, + implodeParams({ + md5: hash, + filename: filename, + filesize: size, + mtime: mtime, + contentType: contentType, + charset: charset + }), + { + "Content-Type": "application/x-www-form-urlencoded", + "If-None-Match": "*" + } + ); + Helpers.assert400(response); + }); + + // Skipped as there may or may not be an error + it('testAddFileFullParams', async function () { + this.skip(); + let xml = await API.createAttachmentItem("imported_file", [], false, this); + + let data = await API.parseDataFromAtomEntry(xml); + let serverDateModified = Helpers.xpathEval(xml, '//atom:entry/atom:updated'); + await new Promise(r => setTimeout(r, 2000)); + let originalVersion = data.version; + let file = "./work/file"; + let fileContents = getRandomUnicodeString(); + await fs.promises.writeFile(file, fileContents); + let hash = md5File(file); + let filename = "test_" + fileContents; + let mtime = parseInt((await fs.promises.stat(file)).mtimeMs); + let size = parseInt((await fs.promises.stat(file)).size); + let contentType = "text/plain"; + let charset = "utf-8"; + + let response = await API.userPost( + config.userID, + `items/${data.key}/file?key=${config.apiKey}`, + implodeParams({ + md5: hash, + filename: filename, + filesize: size, + mtime: mtime, + contentType: contentType, + charset: charset, + params: 1 + }), + { + "Content-Type": "application/x-www-form-urlencoded", + "If-None-Match": "*" + } + ); + Helpers.assert200(response); + Helpers.assertContentType(response, "application/json"); + let json = JSON.parse(response.data); + assert.ok(json); + toDelete.push(hash); + + let boundary = "---------------------------" + md5(Helpers.uniqueID()); + let prefix = ""; + for (let key in json.params) { + prefix += "--" + boundary + "\r\nContent-Disposition: form-data; name=\"" + key + "\"\r\n\r\n" + json.params[key] + "\r\n"; + } + prefix += "--" + boundary + "\r\nContent-Disposition: form-data; name=\"file\"\r\n\r\n"; + let suffix = "\r\n--" + boundary + "--"; + response = await HTTP.post( + json.url, + prefix + fileContents + suffix, + { + "Content-Type": "multipart/form-data; boundary=" + boundary + } + ); + Helpers.assert201(response); + + response = await API.userPost( + config.userID, + `items/${data.key}/file?key=${config.apiKey}`, + "upload=" + json.uploadKey, + { + "Content-Type": "application/x-www-form-urlencoded", + "If-None-Match": "*" + } + ); + Helpers.assert204(response); + response = await API.userGet( + config.userID, + `items/${data.key}?key=${config.apiKey}&content=json` + ); + xml = API.getXMLFromResponse(response); + data = await API.parseDataFromAtomEntry(xml); + json = JSON.parse(data.content); + assert.equal(hash, json.md5); + assert.equal(filename, json.filename); + assert.equal(mtime, json.mtime); + assert.equal(contentType, json.contentType); + assert.equal(charset, json.charset); + const updated = Helpers.xpathEval(xml, '/atom:entry/atom:updated'); + assert.notEqual(serverDateModified, updated); + assert.notEqual(originalVersion, data.version); + }); + + const getRandomUnicodeString = function () { + return "Âéìøü 这是一个测试。 " + Helpers.uniqueID(); + }; + + it('testExistingFileWithOldStyleFilename', async function () { + let fileContents = getRandomUnicodeString(); + let hash = md5(fileContents); + let filename = 'test.txt'; + let size = fileContents.length; + + let parentKey = await API.createItem("book", false, this, 'key'); + let xml = await API.createAttachmentItem("imported_file", [], parentKey, this); + let data = await API.parseDataFromAtomEntry(xml); + let key = data.key; + let mtime = Date.now(); + let contentType = 'text/plain'; + let charset = 'utf-8'; + + let response = await API.userPost( + config.userID, + `items/${data.key}/file?key=${config.apiKey}`, + implodeParams({ + md5: hash, + filename, + filesize: size, + mtime, + contentType, + charset + }), + { + "Content-Type": "application/x-www-form-urlencoded", + "If-None-Match": "*" + } + ); + Helpers.assert200(response); + Helpers.assertContentType(response, "application/json"); + let json = JSON.parse(response.data); + assert.isOk(json); + + toDelete.push(`${hash}/${filename}`); + toDelete.push(hash); + const putCommand = new PutObjectCommand({ + Bucket: config.s3Bucket, + Key: `${hash}/${filename}`, + Body: fileContents + }); + await s3Client.send(putCommand); + response = await API.userPost( + config.userID, + `items/${key}/file?key=${config.apiKey}`, + `upload=${json.uploadKey}`, + { + "Content-Type": "application/x-www-form-urlencoded", + "If-None-Match": "*" + } + ); + Helpers.assert204(response); + + response = await API.userGet( + config.userID, + `items/${key}/file?key=${config.apiKey}` + ); + Helpers.assert302(response); + let location = response.headers.location[0]; + let matches = location.match(/^https:\/\/(?:[^/]+|.+config.s3Bucket)\/([a-f0-9]{32})\/test.txt\?/); + Helpers.assertEquals(2, matches.length); + Helpers.assertEquals(hash, matches[1]); + + parentKey = await API.createItem("book", false, this, 'key'); + xml = await API.createAttachmentItem("imported_file", [], parentKey, this); + data = await API.parseDataFromAtomEntry(xml); + key = data.key; + response = await API.userPost( + config.userID, + `items/${key}/file?key=${config.apiKey}`, + implodeParams({ + md5: hash, + filename, + filesize: size, + mtime, + contentType, + charset + }), + { + "Content-Type": "application/x-www-form-urlencoded", + "If-None-Match": "*" + } + ); + Helpers.assert200(response); + let postJSON = JSON.parse(response.data); + assert.isOk(postJSON); + Helpers.assertEquals(1, postJSON.exists); + + response = await API.userGet( + config.userID, + `items/${key}/file?key=${config.apiKey}` + ); + Helpers.assert302(response); + location = response.headers.location[0]; + matches = location.match(/^https:\/\/(?:[^/]+|.+config.s3Bucket)\/([a-f0-9]{32})\/test.txt\?/); + Helpers.assertEquals(2, matches.length); + Helpers.assertEquals(hash, matches[1]); + + response = await HTTP.get(location); + Helpers.assert200(response); + Helpers.assertEquals(fileContents, response.data); + Helpers.assertEquals(`${contentType}; charset=${charset}`, response.headers['content-type'][0]); + + parentKey = await API.createItem("book", false, this, 'key'); + xml = await API.createAttachmentItem("imported_file", [], parentKey, this); + data = await API.parseDataFromAtomEntry(xml); + key = data.key; + contentType = 'application/x-custom'; + response = await API.userPost( + config.userID, + `items/${key}/file?key=${config.apiKey}`, + implodeParams({ + md5: hash, + filename: "test2.txt", + filesize: size, + mtime, + contentType + }), + { + "Content-Type": "application/x-www-form-urlencoded", + "If-None-Match": "*" + } + ); + Helpers.assert200(response); + postJSON = JSON.parse(response.data); + assert.isOk(postJSON); + Helpers.assertEquals(1, postJSON.exists); + + response = await API.userGet( + config.userID, + `items/${key}/file?key=${config.apiKey}` + ); + Helpers.assert302(response); + location = response.headers.location[0]; + matches = location.match(/^https:\/\/(?:[^/]+|.+config.s3Bucket)\/([a-f0-9]{32})\?/); + Helpers.assertEquals(2, matches.length); + Helpers.assertEquals(hash, matches[1]); + + response = await HTTP.get(location); + Helpers.assert200(response); + Helpers.assertEquals(fileContents, response.data); + Helpers.assertEquals(contentType, response.headers['content-type'][0]); + }); + + const testAddFileFull = async () => { + let xml = await API.createItem("book", false, this); + let data = await API.parseDataFromAtomEntry(xml); + let parentKey = data.key; + xml = await API.createAttachmentItem("imported_file", [], parentKey, this); + data = await API.parseDataFromAtomEntry(xml); + let file = "./work/file"; + let fileContents = getRandomUnicodeString(); + fs.writeFileSync(file, fileContents); + let hash = md5File(file); + let filename = "test_" + fileContents; + let mtime = fs.statSync(file).mtime * 1000; + let size = fs.statSync(file).size; + let contentType = "text/plain"; + let charset = "utf-8"; + + let response = await API.userPost( + config.userID, + `items/${data.key}/file?key=${config.apiKey}`, + implodeParams({ + md5: hash, + filename: filename, + filesize: size, + mtime: mtime, + contentType: contentType, + charset: charset + }), + { + "Content-Type": "application/x-www-form-urlencoded", + "If-None-Match": "*" + } + ); + Helpers.assert200(response); + Helpers.assertContentType(response, "application/json"); + let json = JSON.parse(response.data); + assert.isOk(json); + toDelete.push(`${hash}`); + + response = await HTTP.post( + json.url, + json.prefix + fileContents + json.suffix, + { + "Content-Type": `${json.contentType}` + } + ); + Helpers.assert201(response); + + response = await API.userPost( + config.userID, + `items/${data.key}/file?key=${config.apiKey}`, + `upload=${json.uploadKey}`, + { + "Content-Type": "application/x-www-form-urlencoded", + } + ); + Helpers.assert428(response); + + response = await API.userPost( + config.userID, + `items/${data.key}/file?key=${config.apiKey}`, + `upload=invalidUploadKey`, + { + "Content-Type": "application/x-www-form-urlencoded", + "If-None-Match": "*" + } + ); + Helpers.assert400(response); + + response = await API.userPost( + config.userID, + `items/${data.key}/file?key=${config.apiKey}`, + `upload=${json.uploadKey}`, + { + "Content-Type": "application/x-www-form-urlencoded", + "If-None-Match": "*" + } + ); + Helpers.assert204(response); + + response = await API.userGet( + config.userID, + `items/${data.key}?key=${config.apiKey}&content=json` + ); + xml = await API.getXMLFromResponse(response); + data = await API.parseDataFromAtomEntry(xml); + json = JSON.parse(data.content); + + assert.equal(hash, json.md5); + assert.equal(filename, json.filename); + assert.equal(mtime, json.mtime); + assert.equal(contentType, json.contentType); + assert.equal(charset, json.charset); + + return { + key: data.key, + json: json, + size: size + }; + }; + + it('testAddFileAuthorizationErrors', async function () { + const data = await testNewEmptyImportedFileAttachmentItem(); + const fileContents = getRandomUnicodeString(); + const hash = md5(fileContents); + const mtime = Date.now(); + const size = fileContents.length; + const filename = `test_${fileContents}`; + + const fileParams = { + md5: hash, + filename, + filesize: size, + mtime, + contentType: "text/plain", + charset: "utf-8" + }; + + // Check required params + const requiredParams = ["md5", "filename", "filesize", "mtime"]; + for (let i = 0; i < requiredParams.length; i++) { + const exclude = requiredParams[i]; + const response = await API.userPost( + config.userID, + `items/${data.key}/file?key=${config.apiKey}`, + implodeParams(fileParams, [exclude]), + { + "Content-Type": "application/x-www-form-urlencoded", + "If-None-Match": "*" + }); + Helpers.assert400(response); + } + + // Seconds-based mtime + const fileParams2 = { ...fileParams, mtime: Math.round(mtime / 1000) }; + const _ = await API.userPost( + config.userID, + `items/${data.key}/file?key=${config.apiKey}`, + implodeParams(fileParams2), + { + "Content-Type": "application/x-www-form-urlencoded", + "If-None-Match": "*" + }); + // TODO: Enable this test when the dataserver enforces it + //Helpers.assert400(response2); + //assert.equal('mtime must be specified in milliseconds', response2.data); + + // Invalid If-Match + const response3 = await API.userPost( + config.userID, + `items/${data.key}/file?key=${config.apiKey}`, + implodeParams(fileParams), + { + "Content-Type": "application/x-www-form-urlencoded", + "If-Match": md5("invalidETag") + }); + Helpers.assert412(response3); + + // Missing If-None-Match + const response4 = await API.userPost( + config.userID, + `items/${data.key}/file?key=${config.apiKey}`, + implodeParams(fileParams), + { + "Content-Type": "application/x-www-form-urlencoded" + }); + Helpers.assert428(response4); + + // Invalid If-None-Match + const response5 = await API.userPost( + config.userID, + `items/${data.key}/file?key=${config.apiKey}`, + implodeParams(fileParams), + { + "Content-Type": "application/x-www-form-urlencoded", + "If-None-Match": "invalidETag" + }); + Helpers.assert400(response5); + }); + + const implodeParams = (params, exclude = []) => { + let parts = []; + for (const [key, value] of Object.entries(params)) { + if (!exclude.includes(key)) { + parts.push(key + "=" + encodeURIComponent(value)); + } + } + return parts.join("&"); + }; + + it('testAddFilePartial', async function () { + const getFileData = await testGetFile(); + const response = await API.userGet( + config.userID, + `items/${getFileData.key}?key=${config.apiKey}&content=json` + ); + const xml = await API.getXMLFromResponse(response); + + await new Promise(resolve => setTimeout(resolve, 1000)); + + const data = API.parseDataFromAtomEntry(xml); + const originalVersion = data.version; + + const oldFilename = "./work/old"; + const fileContents = getFileData.response.data; + fs.writeFileSync(oldFilename, fileContents); + + const newFilename = "./work/new"; + const patchFilename = "./work/patch"; + + const algorithms = { + bsdiff: `bsdiff ${oldFilename} ${newFilename} ${patchFilename}`, + xdelta: `xdelta -f -e -9 -S djw -s ${oldFilename} ${newFilename} ${patchFilename}`, + vcdiff: `vcdiff encode -dictionary ${oldFilename} -target ${newFilename} -delta ${patchFilename}`, + }; + + for (let [algo, cmd] of Object.entries(algorithms)) { + fs.writeFileSync(newFilename, getRandomUnicodeString() + Helpers.uniqueID()); + const newHash = md5File(newFilename); + const fileParams = { + md5: newHash, + filename: `test_${fileContents}`, + filesize: fs.statSync(newFilename).size, + mtime: parseInt(fs.statSync(newFilename).mtimeMs), + contentType: "text/plain", + charset: "utf-8", + }; + + const postResponse = await API.userPost( + config.userID, + `items/${getFileData.key}/file?key=${config.apiKey}`, + implodeParams(fileParams), + { + "Content-Type": "application/x-www-form-urlencoded", + "If-Match": md5File(oldFilename), + } + ); + Helpers.assert200(postResponse); + let json = JSON.parse(postResponse.data); + assert.isOk(json); + try { + await exec(cmd); + } + catch { + console.log("Warning: Could not run " + algo); + continue; + } + + const patch = fs.readFileSync(patchFilename); + assert.notEqual("", patch.toString()); + + toDelete.push(newHash); + + let response = await API.userPatch( + config.userID, + `items/${getFileData.key}/file?key=${config.apiKey}&algorithm=${algo}&upload=${json.uploadKey}`, + patch, + { + "If-Match": md5File(oldFilename), + } + ); + Helpers.assert204(response); + + fs.unlinkSync(patchFilename); + fs.renameSync(newFilename, oldFilename); + response = await API.userGet( + config.userID, + `items/${getFileData.key}?key=${config.apiKey}&content=json` + ); + const xml = API.getXMLFromResponse(response); + const data = API.parseDataFromAtomEntry(xml); + json = JSON.parse(data.content); + Helpers.assertEquals(fileParams.md5, json.md5); + Helpers.assertEquals(fileParams.mtime, json.mtime); + Helpers.assertEquals(fileParams.contentType, json.contentType); + Helpers.assertEquals(fileParams.charset, json.charset); + assert.notEqual(originalVersion, data.version); + + const fileResponse = await API.userGet( + config.userID, + `items/${getFileData.key}/file?key=${config.apiKey}` + ); + Helpers.assert302(fileResponse); + const location = fileResponse.headers.location[0]; + + const getFileResponse = await HTTP.get(location); + Helpers.assert200(getFileResponse); + Helpers.assertEquals(fileParams.md5, md5(getFileResponse.data)); + Helpers.assertEquals( + `${fileParams.contentType}${fileParams.contentType && fileParams.charset ? `; charset=${fileParams.charset}` : "" + }`, + getFileResponse.headers["content-type"][0] + ); + } + }); + + const testAddFileExisting = async () => { + const addFileData = await testAddFileFull(); + const key = addFileData.key; + const json = addFileData.json; + const md5 = json.md5; + const size = addFileData.size; + + // Get upload authorization + let response = await API.userPost( + config.userID, + `items/${key}/file?key=${config.apiKey}`, + implodeParams({ + md5: json.md5, + filename: json.filename, + filesize: size, + mtime: json.mtime, + contentType: json.contentType, + charset: json.charset + }), + { + "Content-Type": "application/x-www-form-urlencoded", + "If-Match": json.md5 + } + ); + Helpers.assert200(response); + let postJSON = JSON.parse(response.data); + assert.isOk(postJSON); + assert.equal(1, postJSON.exists); + + // Get upload authorization for existing file with different filename + response = await API.userPost( + config.userID, + `items/${key}/file?key=${config.apiKey}`, + implodeParams({ + md5: json.md5, + filename: json.filename + "等", // Unicode 1.1 character, to test signature generation + filesize: size, + mtime: json.mtime, + contentType: json.contentType, + charset: json.charset + }), + { + "Content-Type": "application/x-www-form-urlencoded", + "If-Match": json.md5 + } + ); + Helpers.assert200(response); + postJSON = JSON.parse(response.data); + assert.isOk(postJSON); + assert.equal(1, postJSON.exists); + + const testResult = { + key: key, + md5: md5, + filename: json.filename + "等" + }; + return testResult; + }; }); diff --git a/tests/remote_js/test/2/fullText.js b/tests/remote_js/test/2/fullText.js index 2e07deb3..c558e062 100644 --- a/tests/remote_js/test/2/fullText.js +++ b/tests/remote_js/test/2/fullText.js @@ -178,7 +178,7 @@ describe('FullTextTests', function () { assert.equal(contentVersion2, json[key2]); }); - //Requires s3 setup + //Requires ES it('testSearchItemContent', async function() { this.skip(); }); diff --git a/tests/remote_js/test/3/fileTest.js b/tests/remote_js/test/3/fileTest.js new file mode 100644 index 00000000..82cd46cb --- /dev/null +++ b/tests/remote_js/test/3/fileTest.js @@ -0,0 +1,711 @@ +const chai = require('chai'); +const assert = chai.assert; +const config = require("../../config.js"); +const API = require('../../api3.js'); +const Helpers = require('../../helpers3.js'); +const { API3Setup, API3WrapUp } = require("../shared.js"); +const { S3Client, DeleteObjectsCommand, PutObjectCommand } = require("@aws-sdk/client-s3"); +const fs = require('fs'); +const HTTP = require('../../httpHandler.js'); +const crypto = require('crypto'); +const util = require('util'); +const exec = util.promisify(require('child_process').exec); + +describe('FileTestTests', function () { + this.timeout(0); + let toDelete = []; + const s3Client = new S3Client({ region: "us-east-1" }); + + before(async function () { + await API3Setup(); + try { + fs.mkdirSync("./work"); + } + catch {} + }); + + after(async function () { + await API3WrapUp(); + fs.rmdirSync("./work", { recursive: true, force: true }); + if (toDelete.length > 0) { + const commandInput = { + Bucket: config.s3Bucket, + Delete: { + Objects: toDelete.map((x) => { + return { Key: x }; + }) + } + }; + const command = new DeleteObjectsCommand(commandInput); + await s3Client.send(command); + } + }); + + const md5 = (str) => { + return crypto.createHash('md5').update(str).digest('hex'); + }; + + const md5File = (fileName) => { + const data = fs.readFileSync(fileName); + return crypto.createHash('md5').update(data).digest('hex'); + }; + + const testNewEmptyImportedFileAttachmentItem = async () => { + return API.createAttachmentItem("imported_file", [], false, this, 'key'); + }; + + const testGetFile = async () => { + const addFileData = await testAddFileExisting(); + const userGetViewModeResponse = await API.userGet( + config.userID, + `items/${addFileData.key}/file/view` + ); + Helpers.assert302(userGetViewModeResponse); + const location = userGetViewModeResponse.headers.location[0]; + Helpers.assertRegExp(/^https?:\/\/[^/]+\/[a-zA-Z0-9%]+\/[a-f0-9]{64}\/test_/, location); + const filenameEncoded = encodeURIComponent(addFileData.filename); + assert.equal(filenameEncoded, location.substring(location.length - filenameEncoded.length)); + const viewModeResponse = await HTTP.get(location); + Helpers.assert200(viewModeResponse); + assert.equal(addFileData.md5, md5(viewModeResponse.data)); + const userGetDownloadModeResponse = await API.userGet( + config.userID, + `items/${addFileData.key}/file` + ); + Helpers.assert302(userGetDownloadModeResponse); + const downloadModeLocation = userGetDownloadModeResponse.headers.location; + const s3Response = await HTTP.get(downloadModeLocation); + Helpers.assert200(s3Response); + assert.equal(addFileData.md5, md5(s3Response.data)); + return { + key: addFileData.key, + response: s3Response + }; + }; + + it('testAddFileLinkedAttachment', async function () { + let key = await API.createAttachmentItem("linked_file", [], false, this, 'key'); + + let file = "./work/file"; + let fileContents = getRandomUnicodeString(); + fs.writeFileSync(file, fileContents); + let hash = md5File(file); + let filename = "test_" + fileContents; + let mtime = fs.statSync(file).mtimeMs; + let size = fs.statSync(file).size; + let contentType = "text/plain"; + let charset = "utf-8"; + + // Get upload authorization + let response = await API.userPost( + config.userID, + `items/${key}/file`, + implodeParams({ + md5: hash, + filename: filename, + filesize: size, + mtime: mtime, + contentType: contentType, + charset: charset + }), + { + "Content-Type": "application/x-www-form-urlencoded", + "If-None-Match": "*" + } + ); + Helpers.assert400(response); + }); + + it('testAddFileFormDataFullParams', async function () { + let json = await API.createAttachmentItem("imported_file", [], false, this, 'json'); + let attachmentKey = json.key; + + await new Promise(r => setTimeout(r, 2000)); + + let originalVersion = json.version; + let file = "./work/file"; + let fileContents = getRandomUnicodeString(); + await fs.promises.writeFile(file, fileContents); + let hash = md5File(file); + let filename = "test_" + fileContents; + let mtime = parseInt((await fs.promises.stat(file)).mtimeMs); + let size = parseInt((await fs.promises.stat(file)).size); + let contentType = "text/plain"; + let charset = "utf-8"; + + let response = await API.userPost( + config.userID, + `items/${attachmentKey}/file`, + implodeParams({ + md5: hash, + filename: filename, + filesize: size, + mtime: mtime, + contentType: contentType, + charset: charset, + params: 1 + }), + { + "Content-Type": "application/x-www-form-urlencoded", + "If-None-Match": "*" + } + ); + Helpers.assert200(response); + Helpers.assertContentType(response, "application/json"); + json = JSON.parse(response.data); + assert.ok(json); + toDelete.push(hash); + + let boundary = "---------------------------" + md5(Helpers.uniqueID()); + let prefix = ""; + for (let key in json.params) { + prefix += "--" + boundary + "\r\nContent-Disposition: form-data; name=\"" + key + "\"\r\n\r\n" + json.params[key] + "\r\n"; + } + prefix += "--" + boundary + "\r\nContent-Disposition: form-data; name=\"file\"\r\n\r\n"; + let suffix = "\r\n--" + boundary + "--"; + response = await HTTP.post( + json.url, + prefix + fileContents + suffix, + { + "Content-Type": "multipart/form-data; boundary=" + boundary + } + ); + Helpers.assert201(response); + + response = await API.userPost( + config.userID, + `items/${attachmentKey}/file`, + "upload=" + json.uploadKey, + { + "Content-Type": "application/x-www-form-urlencoded", + "If-None-Match": "*" + } + ); + Helpers.assert204(response); + + response = await API.userGet( + config.userID, + `items/${attachmentKey}` + ); + json = API.getJSONFromResponse(response).data; + assert.equal(hash, json.md5); + assert.equal(filename, json.filename); + assert.equal(mtime, json.mtime); + assert.equal(contentType, json.contentType); + assert.equal(charset, json.charset); + + assert.notEqual(originalVersion, json.version); + }); + + const getRandomUnicodeString = function () { + return "Âéìøü 这是一个测试。 " + Helpers.uniqueID(); + }; + + it('testExistingFileWithOldStyleFilename', async function () { + let fileContents = getRandomUnicodeString(); + let hash = md5(fileContents); + let filename = 'test.txt'; + let size = fileContents.length; + + let parentKey = await API.createItem("book", false, this, 'key'); + let json = await API.createAttachmentItem("imported_file", [], parentKey, this, 'jsonData'); + let key = json.key; + let mtime = Date.now(); + let contentType = 'text/plain'; + let charset = 'utf-8'; + + let response = await API.userPost( + config.userID, + `items/${key}/file`, + implodeParams({ + md5: hash, + filename, + filesize: size, + mtime, + contentType, + charset + }), + { + "Content-Type": "application/x-www-form-urlencoded", + "If-None-Match": "*" + } + ); + Helpers.assert200(response); + Helpers.assertContentType(response, "application/json"); + json = JSON.parse(response.data); + assert.isOk(json); + + toDelete.push(`${hash}/${filename}`); + toDelete.push(hash); + const putCommand = new PutObjectCommand({ + Bucket: config.s3Bucket, + Key: `${hash}/${filename}`, + Body: fileContents + }); + await s3Client.send(putCommand); + response = await API.userPost( + config.userID, + `items/${key}/file`, + `upload=${json.uploadKey}`, + { + "Content-Type": "application/x-www-form-urlencoded", + "If-None-Match": "*" + } + ); + Helpers.assert204(response); + + response = await API.userGet( + config.userID, + `items/${key}/file` + ); + Helpers.assert302(response); + let location = response.headers.location[0]; + let matches = location.match(/^https:\/\/(?:[^/]+|.+config.s3Bucket)\/([a-f0-9]{32})\/test.txt\?/); + Helpers.assertEquals(2, matches.length); + Helpers.assertEquals(hash, matches[1]); + + // Get upload authorization for the same file and filename on another item, which should + // result in 'exists', even though we uploaded to the old-style location + parentKey = await API.createItem("book", false, this, 'key'); + json = await API.createAttachmentItem("imported_file", [], parentKey, this, 'jsonData'); + + key = json.key; + response = await API.userPost( + config.userID, + `items/${key}/file`, + implodeParams({ + md5: hash, + filename, + filesize: size, + mtime, + contentType, + charset + }), + { + "Content-Type": "application/x-www-form-urlencoded", + "If-None-Match": "*" + } + ); + Helpers.assert200(response); + let postJSON = JSON.parse(response.data); + assert.isOk(postJSON); + Helpers.assertEquals(1, postJSON.exists); + + response = await API.userGet( + config.userID, + `items/${key}/file` + ); + Helpers.assert302(response); + location = response.headers.location[0]; + matches = location.match(/^https:\/\/(?:[^/]+|.+config.s3Bucket)\/([a-f0-9]{32})\/test.txt\?/); + Helpers.assertEquals(2, matches.length); + Helpers.assertEquals(hash, matches[1]); + + response = await HTTP.get(location); + Helpers.assert200(response); + Helpers.assertEquals(fileContents, response.data); + Helpers.assertEquals(`${contentType}; charset=${charset}`, response.headers['content-type'][0]); + + // Get upload authorization for the same file and different filename on another item, + // which should result in 'exists' and a copy of the file to the hash-only location + parentKey = await API.createItem("book", false, this, 'key'); + json = await API.createAttachmentItem("imported_file", [], parentKey, this, 'jsonData'); + + key = json.key; + contentType = 'application/x-custom'; + response = await API.userPost( + config.userID, + `items/${key}/file`, + implodeParams({ + md5: hash, + filename: "test2.txt", + filesize: size, + mtime, + contentType + }), + { + "Content-Type": "application/x-www-form-urlencoded", + "If-None-Match": "*" + } + ); + Helpers.assert200(response); + postJSON = JSON.parse(response.data); + assert.isOk(postJSON); + Helpers.assertEquals(1, postJSON.exists); + + response = await API.userGet( + config.userID, + `items/${key}/file` + ); + Helpers.assert302(response); + location = response.headers.location[0]; + matches = location.match(/^https:\/\/(?:[^/]+|.+config.s3Bucket)\/([a-f0-9]{32})\?/); + Helpers.assertEquals(2, matches.length); + Helpers.assertEquals(hash, matches[1]); + + response = await HTTP.get(location); + Helpers.assert200(response); + Helpers.assertEquals(fileContents, response.data); + Helpers.assertEquals(contentType, response.headers['content-type'][0]); + }); + + const testAddFileFormDataFull = async () => { + let parentKey = await API.createItem("book", false, this, 'key'); + let json = await API.createAttachmentItem("imported_file", [], parentKey, this, 'json'); + let attachmentKey = json.key; + + let file = "./work/file"; + let fileContents = getRandomUnicodeString(); + fs.writeFileSync(file, fileContents); + let hash = md5File(file); + let filename = "test_" + fileContents; + let mtime = fs.statSync(file).mtime * 1000; + let size = fs.statSync(file).size; + let contentType = "text/plain"; + let charset = "utf-8"; + + let response = await API.userPost( + config.userID, + `items/${attachmentKey}/file`, + implodeParams({ + md5: hash, + filename: filename, + filesize: size, + mtime: mtime, + contentType: contentType, + charset: charset + }), + { + "Content-Type": "application/x-www-form-urlencoded", + "If-None-Match": "*" + } + ); + Helpers.assert200(response); + Helpers.assertContentType(response, "application/json"); + json = JSON.parse(response.data); + assert.isOk(json); + toDelete.push(`${hash}`); + + const wrongContent = fileContents.split('').reverse().join(""); + response = await HTTP.post( + json.url, + json.prefix + wrongContent + json.suffix, + { + "Content-Type": `${json.contentType}` + } + ); + Helpers.assert400(response); + assert.include(response.data, "The Content-MD5 you specified did not match what we received."); + + response = await HTTP.post( + json.url, + json.prefix + fileContents + json.suffix, + { + "Content-Type": `${json.contentType}` + } + ); + Helpers.assert201(response); + + response = await API.userPost( + config.userID, + `items/${attachmentKey}/file`, + `upload=${json.uploadKey}`, + { + "Content-Type": "application/x-www-form-urlencoded", + } + ); + Helpers.assert428(response); + + response = await API.userPost( + config.userID, + `items/${attachmentKey}/file`, + `upload=invalidUploadKey`, + { + "Content-Type": "application/x-www-form-urlencoded", + "If-None-Match": "*" + } + ); + Helpers.assert400(response); + + response = await API.userPost( + config.userID, + `items/${attachmentKey}/file`, + `upload=${json.uploadKey}`, + { + "Content-Type": "application/x-www-form-urlencoded", + "If-None-Match": "*" + } + ); + Helpers.assert204(response); + + response = await API.userGet( + config.userID, + `items/${attachmentKey}` + ); + json = API.getJSONFromResponse(response).data; + + assert.equal(hash, json.md5); + assert.equal(filename, json.filename); + assert.equal(mtime, json.mtime); + assert.equal(contentType, json.contentType); + assert.equal(charset, json.charset); + + return { + key: attachmentKey, + json: json, + size: size + }; + }; + + it('testAddFileAuthorizationErrors', async function () { + const parentKey = await testNewEmptyImportedFileAttachmentItem(); + const fileContents = getRandomUnicodeString(); + const hash = md5(fileContents); + const mtime = Date.now(); + const size = fileContents.length; + const filename = `test_${fileContents}`; + + const fileParams = { + md5: hash, + filename, + filesize: size, + mtime, + contentType: "text/plain", + charset: "utf-8" + }; + + // Check required params + const requiredParams = ["md5", "filename", "filesize", "mtime"]; + for (let i = 0; i < requiredParams.length; i++) { + const exclude = requiredParams[i]; + const response = await API.userPost( + config.userID, + `items/${parentKey}/file`, + implodeParams(fileParams, [exclude]), + { + "Content-Type": "application/x-www-form-urlencoded", + "If-None-Match": "*" + }); + Helpers.assert400(response); + } + + // Seconds-based mtime + const fileParams2 = { ...fileParams, mtime: Math.round(mtime / 1000) }; + const _ = await API.userPost( + config.userID, + `items/${parentKey}/file`, + implodeParams(fileParams2), + { + "Content-Type": "application/x-www-form-urlencoded", + "If-None-Match": "*" + }); + // TODO: Enable this test when the dataserver enforces it + //Helpers.assert400(response2); + //assert.equal('mtime must be specified in milliseconds', response2.data); + + // Invalid If-Match + const response3 = await API.userPost( + config.userID, + `items/${parentKey}/file`, + implodeParams(fileParams), + { + "Content-Type": "application/x-www-form-urlencoded", + "If-Match": md5("invalidETag") + }); + Helpers.assert412(response3); + + // Missing If-None-Match + const response4 = await API.userPost( + config.userID, + `items/${parentKey}/file`, + implodeParams(fileParams), + { + "Content-Type": "application/x-www-form-urlencoded" + }); + Helpers.assert428(response4); + + // Invalid If-None-Match + const response5 = await API.userPost( + config.userID, + `items/${parentKey}/file}`, + implodeParams(fileParams), + { + "Content-Type": "application/x-www-form-urlencoded", + "If-None-Match": "invalidETag" + }); + Helpers.assert400(response5); + }); + + const implodeParams = (params, exclude = []) => { + let parts = []; + for (const [key, value] of Object.entries(params)) { + if (!exclude.includes(key)) { + parts.push(key + "=" + encodeURIComponent(value)); + } + } + return parts.join("&"); + }; + + it('testAddFilePartial', async function () { + const getFileData = await testGetFile(); + const response = await API.userGet( + config.userID, + `items/${getFileData.key}` + ); + let json = await API.getJSONFromResponse(response).data; + + await new Promise(resolve => setTimeout(resolve, 1000)); + + const originalVersion = json.version; + + const oldFilename = "./work/old"; + const fileContents = getFileData.response.data; + fs.writeFileSync(oldFilename, fileContents); + + const newFilename = "./work/new"; + const patchFilename = "./work/patch"; + + const algorithms = { + bsdiff: `bsdiff ${oldFilename} ${newFilename} ${patchFilename}`, + xdelta: `xdelta -f -e -9 -S djw -s ${oldFilename} ${newFilename} ${patchFilename}`, + vcdiff: `vcdiff encode -dictionary ${oldFilename} -target ${newFilename} -delta ${patchFilename}`, + }; + + for (let [algo, cmd] of Object.entries(algorithms)) { + fs.writeFileSync(newFilename, getRandomUnicodeString() + Helpers.uniqueID()); + const newHash = md5File(newFilename); + const fileParams = { + md5: newHash, + filename: `test_${fileContents}`, + filesize: fs.statSync(newFilename).size, + mtime: parseInt(fs.statSync(newFilename).mtimeMs), + contentType: "text/plain", + charset: "utf-8", + }; + + const postResponse = await API.userPost( + config.userID, + `items/${getFileData.key}/file`, + implodeParams(fileParams), + { + "Content-Type": "application/x-www-form-urlencoded", + "If-Match": md5File(oldFilename), + } + ); + Helpers.assert200(postResponse); + let json = JSON.parse(postResponse.data); + assert.isOk(json); + try { + await exec(cmd); + } + catch { + console.log("Warning: Could not run " + algo); + continue; + } + + const patch = fs.readFileSync(patchFilename); + assert.notEqual("", patch.toString()); + + toDelete.push(newHash); + + let response = await API.userPatch( + config.userID, + `items/${getFileData.key}/file?algorithm=${algo}&upload=${json.uploadKey}`, + patch, + { + "If-Match": md5File(oldFilename), + } + ); + Helpers.assert204(response); + + fs.unlinkSync(patchFilename); + fs.renameSync(newFilename, oldFilename); + response = await API.userGet( + config.userID, + `items/${getFileData.key}` + ); + json = API.getJSONFromResponse(response).data; + + Helpers.assertEquals(fileParams.md5, json.md5); + Helpers.assertEquals(fileParams.mtime, json.mtime); + Helpers.assertEquals(fileParams.contentType, json.contentType); + Helpers.assertEquals(fileParams.charset, json.charset); + assert.notEqual(originalVersion, json.version); + + const fileResponse = await API.userGet( + config.userID, + `items/${getFileData.key}/file` + ); + Helpers.assert302(fileResponse); + const location = fileResponse.headers.location[0]; + + const getFileResponse = await HTTP.get(location); + Helpers.assert200(getFileResponse); + Helpers.assertEquals(fileParams.md5, md5(getFileResponse.data)); + Helpers.assertEquals( + `${fileParams.contentType}${fileParams.contentType && fileParams.charset ? `; charset=${fileParams.charset}` : "" + }`, + getFileResponse.headers["content-type"][0] + ); + } + }); + + const testAddFileExisting = async () => { + const addFileData = await testAddFileFormDataFull(); + const key = addFileData.key; + const json = addFileData.json; + const md5 = json.md5; + const size = addFileData.size; + + // Get upload authorization + let response = await API.userPost( + config.userID, + `items/${key}/file`, + implodeParams({ + md5: json.md5, + filename: json.filename, + filesize: size, + mtime: json.mtime, + contentType: json.contentType, + charset: json.charset + }), + { + "Content-Type": "application/x-www-form-urlencoded", + "If-Match": json.md5 + } + ); + Helpers.assert200(response); + let postJSON = JSON.parse(response.data); + assert.isOk(postJSON); + assert.equal(1, postJSON.exists); + + // Get upload authorization for existing file with different filename + response = await API.userPost( + config.userID, + `items/${key}/file`, + implodeParams({ + md5: json.md5, + filename: json.filename + "等", // Unicode 1.1 character, to test signature generation + filesize: size, + mtime: json.mtime, + contentType: json.contentType, + charset: json.charset + }), + { + "Content-Type": "application/x-www-form-urlencoded", + "If-Match": json.md5 + } + ); + Helpers.assert200(response); + postJSON = JSON.parse(response.data); + assert.isOk(postJSON); + assert.equal(1, postJSON.exists); + + const testResult = { + key: key, + md5: md5, + filename: json.filename + "等" + }; + return testResult; + }; +}); From e8449dbeed0c813f0c7a64c26538e34802b3bb69 Mon Sep 17 00:00:00 2001 From: Bogdan Abaev Date: Thu, 25 May 2023 10:55:11 -0400 Subject: [PATCH 08/33] the rest of file tests --- tests/remote_js/api3.js | 2 +- tests/remote_js/data/bad_string.xml | 6 + tests/remote_js/data/sync1download.xml | 123 ++ tests/remote_js/data/sync1upload.xml | 120 ++ tests/remote_js/data/test.html.zip | Bin 0 -> 196 bytes tests/remote_js/data/test.pdf | Bin 0 -> 21881 bytes tests/remote_js/helpers.js | 8 + tests/remote_js/test/3/fileTest.js | 1565 +++++++++++++++++++++++- tests/remote_js/test/shared.js | 7 + 9 files changed, 1827 insertions(+), 4 deletions(-) create mode 100644 tests/remote_js/data/bad_string.xml create mode 100644 tests/remote_js/data/sync1download.xml create mode 100644 tests/remote_js/data/sync1upload.xml create mode 100644 tests/remote_js/data/test.html.zip create mode 100644 tests/remote_js/data/test.pdf diff --git a/tests/remote_js/api3.js b/tests/remote_js/api3.js index c68ea28e..bcffc602 100644 --- a/tests/remote_js/api3.js +++ b/tests/remote_js/api3.js @@ -406,7 +406,7 @@ class API3 extends API2 { static groupCreateAttachmentItem = async (groupID, linkMode, data = [], parentKey = false, context = false, returnFormat = 'responseJSON') => { let response = await this.get(`items/new?itemType=attachment&linkMode=${linkMode}`); - let json = await response.json(); + let json = this.getJSONFromResponse(response); for (let key in data) { json[key] = data[key]; } diff --git a/tests/remote_js/data/bad_string.xml b/tests/remote_js/data/bad_string.xml new file mode 100644 index 00000000..5cf31734 --- /dev/null +++ b/tests/remote_js/data/bad_string.xml @@ -0,0 +1,6 @@ +<p>&nbsp;</p> +<p style="margin-top: 0.18cm; margin-bottom: 0.18cm; line-height: 100%;" lang="es-ES"><br /><br /></p> +<p style="margin-top: 0.18cm; margin-bottom: 0.18cm; line-height: 100%;" lang="es-ES"><br /><br /></p> +<table border="1" cellspacing="0" cellpadding="7" width="614"> +<colgroup><col width="598"></col> </colgroup> +<p style="margin-top: 0.18cm; margin-bottom: 0.18cm;" lang="en-US"><span style="font-family: Times New Roman,serif;"><span style="font-size: x-large;"><strong>test</strong></span></span></p> diff --git a/tests/remote_js/data/sync1download.xml b/tests/remote_js/data/sync1download.xml new file mode 100644 index 00000000..97be9141 --- /dev/null +++ b/tests/remote_js/data/sync1download.xml @@ -0,0 +1,123 @@ + + + + + + Døn + Stîllmån + + + 汉字 + 1 + + + Test + Testerman + + + Testish McTester + 1 + + + Testy + Teststein + + + + + <p>Here's a <strong>child</strong> note.</p> + + + 3 + March 6, 2007 + My Book Section + + + DUQPU87V + + + amazon.html + AAAAAAFkAAIAAAxNYWNpbnRvc2ggSEQAAAAAAAAAAAAAAAAAAADC/J9aSCsAAAAOrpkLYW1hem9uLmh0bWwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADnT/bcS9TKEAAAAAAAAAAP////8AAAkgAAAAAAAAAAAAAAAAAAAAB0Rlc2t0b3AAABAACAAAwvzXmgAAABEACAAAxL2E4QAAAAEADAAOrpkADjPnAA4z5QACACpNYWNpbnRvc2ggSEQ6VXNlcnM6ZGFuOkRlc2t0b3A6YW1hem9uLmh0bWwADgAYAAsAYQBtAGEAegBvAG4ALgBoAHQAbQBsAA8AGgAMAE0AYQBjAGkAbgB0AG8AcwBoACAASABEABIAHVVzZXJzL2Rhbi9EZXNrdG9wL2FtYXpvbi5odG1sAAATAAEvAAAVAAIACv//AAA= + <p>Note on a top-level linked file</p> + DUQPU87V + + + http://chnm.gmu.edu/ + 2009-03-07 04:55:59 + Center for History and New Media + storage:chnm.gmu.edu.html + <p>This is a note for a snapshot.</p> + + + <p>Here's a top-level note.</p> + + + http://www.zotero.org/ + 2009-03-07 04:56:47 + Zotero: The Next-Generation Research Tool + storage:www.zotero.org.html + + + My Book + + + 6TKKAABJ 7IMJZ8V6 + + + http://chnm.gmu.edu/ + 2009-03-07 04:56:01 + Center for History and New Media + <p>This is a note for a link.</p> + + + FILE.jpg + storage:FILE.jpg + + + Trashed item + + + + + + DUQPU87V + + + HTHD884W + + + 6TKKAABJ 9P9UVFK3 + + + + + + + + + + + + + + + 6TKKAABJ + + + 6TKKAABJ + + + 6TKKAABJ DUQPU87V + + + DUQPU87V + + + HTHD884W + + + T3K4BNWP + + + + diff --git a/tests/remote_js/data/sync1upload.xml b/tests/remote_js/data/sync1upload.xml new file mode 100644 index 00000000..ca0947eb --- /dev/null +++ b/tests/remote_js/data/sync1upload.xml @@ -0,0 +1,120 @@ + + + + Test + Testerman + + + 汉字 + 1 + + + Testy + Teststein + + + Døn + Stîllmån + + + + + 3 + 2007-03-06 March 6, 2007 + My Book Section + + + + Testish McTester + 1 + + + + + My Book + + + 6TKKAABJ + + + <p>Here's a <strong>child</strong> note.</p> + + + http://chnm.gmu.edu/ + 2009-03-07 04:56:01 + Center for History and New Media + <p>This is a note for a link.</p> + + + http://chnm.gmu.edu/ + 2009-03-07 04:55:59 + Center for History and New Media + storage:chnm.gmu.edu.html + <p>This is a note for a snapshot.</p> + + + <p>Here's a top-level note.</p> + + + http://www.zotero.org/ + 2009-03-07 04:56:47 + Zotero: The Next-Generation Research Tool + storage:www.zotero.org.html + + + FILE.jpg + storage:FILE.jpg + + + Trashed item + + + + amazon.html + AAAAAAFkAAIAAAxNYWNpbnRvc2ggSEQAAAAAAAAAAAAAAAAAAADC/J9aSCsAAAAOrpkLYW1hem9uLmh0bWwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADnT/bcS9TKEAAAAAAAAAAP////8AAAkgAAAAAAAAAAAAAAAAAAAAB0Rlc2t0b3AAABAACAAAwvzXmgAAABEACAAAxL2E4QAAAAEADAAOrpkADjPnAA4z5QACACpNYWNpbnRvc2ggSEQ6VXNlcnM6ZGFuOkRlc2t0b3A6YW1hem9uLmh0bWwADgAYAAsAYQBtAGEAegBvAG4ALgBoAHQAbQBsAA8AGgAMAE0AYQBjAGkAbgB0AG8AcwBoACAASABEABIAHVVzZXJzL2Rhbi9EZXNrdG9wL2FtYXpvbi5odG1sAAATAAEvAAAVAAIACv//AAA= + <p>Note on a top-level linked file</p> + DUQPU87V + + + + + 6TKKAABJ 9P9UVFK3 + + + DUQPU87V + + + + HTHD884W + + + + + + + + + + + + + + DUQPU87V + + + 6TKKAABJ + + + 6TKKAABJ DUQPU87V + + + 6TKKAABJ + + + HTHD884W + + + T3K4BNWP + + + diff --git a/tests/remote_js/data/test.html.zip b/tests/remote_js/data/test.html.zip new file mode 100644 index 0000000000000000000000000000000000000000..678763669f37857e814c07e534ed7d645d78f1a3 GIT binary patch literal 196 zcmWIWW@Zs#U|`^2xVA6MKTA*`Tn5Nf24YSI8HSS7;u5`#lH8oo5KabWmhAjUcr!A|G2^m80%#2b j1JE3XEsY=+l69;Q>(HzU@MdKLDP#mfe;^$X;xGUJ$k;Aj literal 0 HcmV?d00001 diff --git a/tests/remote_js/data/test.pdf b/tests/remote_js/data/test.pdf new file mode 100644 index 0000000000000000000000000000000000000000..2fd6020815ca6286a7c2c22e757a0453c102218f GIT binary patch literal 21881 zcmcG#1yGz@*XN5%uaND-w4p18xtwixUbkN(vYsd+JBuhQ+@fpo zLt?O~;1pY`dk$bGeO1r5O>i~PX-gE%>>~@E%%&yuCr}rXQnYup%P&LXsmw<|fkLyv zSh0%X8c^vp2t|wnX7H%+B!?!Tnr?NtVz z5pK0Z=XpybCn( z?Wry5U4;3GEv2<}qu4txmApdBTE82{Wki&}r`1XK-A6|~26C?+M)xO<-xi9lS1I2B zD-x)Fd8CO2j`yeDF=((soU&7yH>OeEpNow?cz7K{h%C z_O%tY`m|}^&X;D021dTxpRE8hd(;1{@yFF4*ZlFvpED~P`=7!;mn{FqYpPz3WM@jH+gyu0TB?qlkm8gR_dGk%<}b zPY{W?FatUM>YktfBX~YW{L8GW$_(WEV+Js)sOs@XuiX2}Ne+zv?l{ ze|#lIp+E6x{}GHpMo}|&D-$zi3E}^9m}aMXsH!YthL3l0bWWg>0LgD~yZYnapc3IECSvGt!9m9Juvf^6KYEo!#dcStZD7x<*{|0! zUx&2T7x%x z1)F*4fwc>ktkN)u!}1O> ze3L@W62{DenGah~MY26mYkOC^N8A1@J&j@~A6IJ(r-Lmgx?OQClYFx<8X{jeoqWTh z&XFMb2gI|) zVkZ(JLdv)VWj@_%_u!}b(`AFoqcJmWa=+Z>7f;#on@0FpCai#3hg308axJ?I%Y@Tv z{L@pdxjoCr?!|RNKb2R$pWe@EmlP-CIYQAnTI;qaRg9gOAa`WiUkMtzQq({i?VtpA zH>(uRfgo3qAdPET*MwoV`iTQu@RV>-CnvTppt&{-vlocXT*}H>}Gj%2}pu;I#53%h9Lyd=a%>%FnGBAPB86w~Z zusVXT0YO3nB$2^W!5JjMtOOzIh4>dh07A?QpzguejvzCE-V+e=L7yCNtwZ=#$ zIPHK#h36ZkV`Kin*$&r=-ZWM>nlqMWnoT!JSC_@w`(%#H8u%DgY&`p|M3b^gz#aAz z%GVhB0d>PTL!G*`Z{Q0?Cp`FwLp?ZK_>O)p*t#tC>@^VIk>;b@1CIL z&x4S9T`r7$h`Rv(`2K|JaYR45DdSh*N=Nwq&ZvxG?)LuOTRXY)XM;3a%t! z$X)}w#?;?p7lb;b@G0bz(51r2u$0NDGa3U`rL0NxDEKHkiIaYDn@TuiYYFNr)R5%@ z4akua1r(F1D9{z8@x_D}<+-F=1-%5lM2eLW%Is$}E7IGtwG}!=eWiUFC6ZXvuG6-E zRVP&@*rXh%eI{2(o=@{gGhoQ2G$8huek*xt0aNM~>{SUYtW(by-)!vT3zZVfEJ!i_ zVw`ClVH~{0KBz?Akr2UCd z=%Dik){NXtXP0Uhe;0SRVOSZekIC?XRT-x^Iw`t6I_{cnuXTKTeECzYmHvANb;=`+ zJ&jKaNs4(2Xi8j#teV3t`fPTEU4_C7lQo64k@ceW^DIMU%y{XzN6vV*0o(h|OZZV; zTZDJZll5)?5#Lb~ZYFLQE)K35ZW`MUcAQN6%wL(infq)*jh5PQO%U3<+WSrVOeq;> zBdX@)4N+YA)itN3^ppHWtCf7!CS?tp7KQQ+V`zGb3_2V_-*vyMcnB-VFlsUy)GD3l z^DE_6=N5HKbnEyJ_e=nn45Ng;_Lk*G=64(A7!_`skX9sAPSO-=7xMhzG--Bm<|?KE-6@?4{V^Sp7PS_nrd{Jv-BbV4K&FkJiSJOD^|ndFSjp;MQ*}B0x_kGj zFHfdk-703geEawv;vLB|#WOQBU$|#@C*u8rt2@Q@xf-x*k$xKH05W?Z_`qx z>)Bdk)ME%^g>dj#7g#g&*v^wqn=Sax2@IwT(FUH5`7>(EDcjn|Pa|^kG5hWVb_1&m ze2cFw&$LJ~NC^^AL&>E{`?%w9`d`v&H4-#xzbD=spIe`o@^kY?@W=3@b-8y{`Z7Kk zy@I{EKdoNRU+h24KPy0RLR7&X0E!^QA=IJy;aFf%A>APQdqI2KgNPWX49Ik6Fu{ah zMBGDpVcFsJFrHkn8R*!mC{Dau)I9w>Js{|f#Y9C0+9l(DPW_aKiouk@^br3gF(@`G zAt&}(EKN)%O_NTmVMjb#I~rKRhe?Eo@tMrsx%_@=RVIA*5A07o9wM*G}%}3U@f=?>-sed3A5Z`r4f|9o1M%Z z``-k=u75rIv757dxLI_$zlFW@vY*)?R)@~j=q|U)d+7DHZQgbr$JqhxBo{A!i~o)> z!&|qHT$@qKQ>yDSv}`!v`yt#athCulKc<6dJ7|kxd%9}gP;I@OMxsA?Yy5jFa!Y}H zCGmJ&%U9!Zb>uGOw6plG_}OE_9mZp2^|0+)Gs9R*e{rH_L$7*wwoe z?KI@@x^XM=8LjeAk+rb(0fLj5>vC)8f#^I3o_EnJ(zoJ}@-_Fac|tozTElzqLx-^QNyLjAU}=_sqC<*XFw$`<{&l9tHJu#?bZoU4Mn|(0A>161p5oUtq+W^VRaz zm5}-hnQz20OI{PqhUfy|gk~eQS-tfgDF04g_@jd0SR-qqqynLKiyixo}qFQ1+ z`Z0Rd``gXn#qa2}>Fv;N>bK_ml*!6mKab~53*%n{7dDf}83NL4WnElPb9Z+8rUyII z9#-cirIBluT>^K3uldg=H$w@BCkhV=DY-EM9zL)ySDqAh=6mb+A=l6UvjYE1hW@3y zKQ!_`+W3#i{bT#k!%`w5!bUD;rog{6tTIsVpE`ep_&<{NU!Csq|E9eEh-tSE!ORR~ zl(8~(`HtOMB65#S5_5W{+KU(|0PvS3C&Zy#M z?D|*kpz7>q_V?`me2c#eg;bQ~q*Z8Dt?bNP=#?GpjO_mw^P*-hCeBult`5!s=KrJ_ zV)h@op_To|_;N-je^ve0B5vjE;woZk5uO|{pHnv%l@wUZ+$j4wvWC(jvsaZ z9RDt1XJP`faQw++{yqP({rUVoS5{80ziRw#`D6XK0{rRWzq#>$4vjwx|L^ktz1;Ff zb|2}^|I!(@Miw8w{#eI9?SwzFIDig43ll4lo{fbQ$jtFEA7&N~mX8EY>LWU=OoZ$$ zY|TCN}%m3lIL#H>aC*Ew+MJFrnsAXf!-72Y7qjo*H&RX)%yM{0;<;=x7(Z- z*AmN%Nu~nO>4q_yyV9`SJ@V5rZ0kt_0O~X5~l*O z491WTwxeIdxlr35&jZf|;FC>A+p#Y|?jYNp;EV5uz9K~jrw-R_#IgP)GHPD72+vN_ zh%4R6)Nf5SX-S;?vkERn8CSYmQJS_GeIzwVS(-Wmog~(Q$>a6u^UKrMm#4Ig>@ut#4Ql38gwChj z+an(D+v?6It{=L9CIyPI_UeZTwZedqtUUa3U#x+Sdj16%i;X<2U>|H2l}SFh0RPc)P5+e|^; zEVuFQ$3RMvh_D6GW#VInnyB+5jSzL&))OhRlBvoPVlWVdqziO6#T84Q_Rx*9x~G;v zVK9IB?lRRuu->g>mo0ui57Wgt3umt*9$}oCfQTU626Up(FwH zWm(S13McFe5vsxf@?<4(*s-Fc%VHGEF?>)_nM>H{p~0;+SO5{mcqwMoeg&nfqD!-P zQ1H(DCG62e%Y!}ub&>du#4Y3DI@R6@Rov6<(CTt`Ms4d_s<{9A6=S^C#589N(y;br z#0===8n<2=BqSwmrCcYCWDK0}CBAC}Z!_U?;K-cz0} zdSz0~R^p7upjf%xR^OGZ%k}UV7v*u<-%k!&>w9R+%KYD2cf+?%c&kixM-tDrjrF{) zmaNUZ*y*naAm|j_RI7BgNui9S=`Lo>K}0Nmfw9n$h{A2ZG*%Kn;PS1B{t>TW#8ej|9xWA%skmrV4dg1$KC z#-hCq^zrX*C}pbs?f~Y$&}sw*f)@s00ri8yfb@dFy+Kc<-Ol4w-HppHCJS^bgF>g= z2KoJoYV?eXL#CH%f=&T->V!?2dQ^GFs(2Aes^MGb<=BfRK8aYYfLDhYeGljuv?RssN2w z1YJES0c9uwunk6%%XgVo$7g~0<3%-t*ZRf{G8b;2*mEz#)(wHjeL3X$JDW&*e; zIVNN#Ot6;V?b;_alt8@s47?$aa3_e_0>^r}PDDn_ki=Wz_%t&` zFtj_=o)Dz-Q38}5pTdnsVl3fm3fz#!zu4hT*{XI1mrMy5m6^LQpfT?J_J&W%v~Vg2=rsjC_7M1O+@$4oc~RP9k7cpP-s z!FES0BzF{(7Nd^P=Tpqo^biRfNW>Kk{gj@7g(;mx*ALQ|K-JTjO0$;P&+kMjqbMYF zrM_7Ru@6Y$4bT^H5PTpX!R<%z#90fHxS-+}YMREx_)SN(3a{xbcy0~@2l{>*aLDh< zIB{WhLjY4-@D4I;QzzP_L^^}BB>KBTY-DUouDHKAO)3ua4E1xpR?Bhz=RT)G?v zJ*BqqH{yqkb>4uJ7^V{+(h!RDLJWFg8ASnYxW+E!%{b^uBet+u(9<>H^Md;cHkvJf zrUj|}5bfpd!>#+yVa*D(O7YbXNIQc|Q0}PPatwDEDc8Zt?kvw8{HPq-ljpL?)Yz z>XB|HlcTuB&O!FUrorq%yhz4mZ|VEAE52Y{;3@5oM-dog#pw}zYcE#)ki zxZ%_Yi+Sr(`B;TV1(AlpZ~b3>;l5Zg;4$wQb2sYV!ik)DKeK#2~LC zGC0~b&$=?LE6Q(G%GV9jm9MT-Uiist!+n4t z1TWcRw_g6BNKZU)dOY)S?udJpi4W=UGuxGkJ@|qM^f)a6++pthP66DKQDZ?m!ycHr zs5_wI;XL7UnBG{oZhL%zg8pjt0X%b(X;|?y`b-q&ZApH4&HYi&hG!K!P@Ic!2lA`h zR;Sr$ENwpVJ7El`v=bt)kk;`dM$+i`%3T{RZ{YjukytL-9wKFgF{21JHHAPZTormEVgWQ)@gHb_V^H)g@eA^_KVe7 zK**V8j3kZm5wGnN5aeA=H61lPDGTTy&tSJ6}~aNdoMVuX-a`I@h;^C?b01CgIrE^2@0Lv z`=k<_o##Ssll|nJfXs}$>xaR!5D!o-mzEPLG_4QEKkf95dIFGoDF}_-gG&Vr#@^py z#DaU=-iy#`Zo`+vpRH|V^|sJ@vR`3EA@<^FP-w&v8T%>Z*sql#iZlh5%sM08z zo_s=iRhR=M(fVl6Jd3cNgh?QIi-ZA^pn{bGNGipFoecJjDawl)}m z`B(n}$5#Q7%`pGSzL;F2CnR)xuq-^-(1v#x^qgA{baVMU5Xd&zHIFij4gRaxlJ zp8%~`{EBrF#+|sHuchbWd>17fF+yBW_e88Ip+1INY&xoSb_b=p?3q} zE|V4o;LtyvsGcWjoOPc1!oi-w+TPHRu3JAHji>aQfwPTa5F@tVm-s93)E@xxaeC27l~Y%m{R-n4wt z`YJ;o>@w;jVK!fNH`mnpjvK+gqG`3U@S|;^gWWekhVc#H25T1J_H0JTKD{ss632x0 zaM2K`lkBAcM^rHLQl8mJZU}d?C4j}AY$U2RR6Zr~U{+_*L`9;LE?upTl32fss}rXv9`d^MK36iq)3u9Ejg#k$vI<}*AqKL{H3t2@?iQ9^$} zZGYB}@$y+IR<@Z7pA~q%S%xLV62$;!!?bLT$}B(l*tVj6uE4Zk+%fnQj*aVmsCmth<_ zB6i*AQ5}zM`kKJ)^Hydk9Q0*AHiLQQzU;e~S@vv)?Ng!tw<$ho32JaP4qxXd1*6fn z^$C^z+SSRQ;r&^K!<@BOP3QOR$5q^J*qiFY9L2hijv} zwuHux;iUCPAo^~U=P7V5vhyDH6MZ-tuqmm$=`tj<$!Iah5VA5zWGAwmX(GwyxgmFL ztqq;M@5vTDK?HqxNXgvcxFED1%!GbU2+FmYq}ev}s98&-N0go3uv9tM|8@ieU%Wd3 z)fWUW`SG6mXo)yOYH(9ZJ)!W`1FJnDGO(AY11&d(2mk9qEh)TVSup%MyQu! z>a}{7kB^C_Y!b_QIGbvHv3uy21$V$9f4c$esDY=+V2>$RDTnkG@vACD8Lk9htO?L^ zY$k)%0xF=!VhmZ!hTMB8nH*L8XPEr%#SCQx1Yv+g`cnuMN0cf81g%QImq=L2L22gM z>ZQzyy-T|uy>R=~?|j$f-%<~$`QIKguZd9R0N1DAD<*ao;@JtQyCkav*>ROh@r?}t zo2x=+sz2*=jJd$YgL-YjUVl0(V8{F!$^10P5=u zfYlA(=p7-5_%j<0Z|<7=2gPI+f~faoUyPDG!k4uZQ&0XNB{h8*Q|#(is6|5 zU~IQQ!<;s6dMs#gpZpfp70eIDLSFc(Drx?e-faT4Mcbi?{qcd@Vbd(a+q zCWo>^k%E6C~L~6nvF#E6vSFf45c5I13y}uW)T)MixMQPX)Q)W7LQV)=;x=Z z>`a#Hm4v=pS0>xI52~bu)0Pjz0)F{~vmX6|URLwMSxGk`I0Z_oSd=IPlkM_{$mNV> zLm^CYi-Dogl`Uo9I?88v`x< z^1&ob8WF{q#Gy#1ht+^8GunYlB*Z%*x?ufq-B%6JglvL0wYYbP%?v8bibU47FlmEv zGl3)DtgZIap51LU%6uEd?O;*S8x6PXs<`pU z@h)Z^IoytotP@yaqg|IBx6y3!e2nqrP{S!dTW_P;JRQ^LN^Un^Y|{*1PMR`nrD*qa zFFWqV?s-&DBWd8$-fc7w0x2(8K&nzq-4@Ot9kg-!l*)TAKeWA4bZ9rd zJhqk|wyALbY5<=_ZsQf+vuw7!J;$ObSzJUmQ68oD&svk`;c+F*J{m--`ZP_-P+|iGCXuVmKm_eaBhFMxUJ+_ zDZBN(lpKkSIz;QTmTnTrYxk?*=BV@3gT)-mST@bK|FAH+uOw1YIr`K&w@;^h%D*|p zy9YZc`h*3RpRTD?vJk-a+1xtJe8&;^!t0xmO`J`vTihFrt+PPzEfWLr4rl@C&V>U` zAmpBeb%jlmqC_AhD8Wh2a8=va8j=|Zki|x|1yW1S&S$U+N`4ajvMj0hws%wgBE#XZ zFi$t=|2m50_t0(d=04`oqhQU)zFc`=q3?TF$Wco;w|c{R!guG3pUJaQ=V>Rmt~;T` z3@qq5fq36*ayB7F`jHY92hL35N_!=~h6JhIxp_~{-6KlpUV5l+As!`0p|fc+dnB1& zO8df$Pm2fNB^W6O3L4k@UOJgvu&TI&c;B0?zog-ivQ}M#Uo@3YDlM*A4drsz{n&I@w3nBR zu#&dE?d$g4-(3`{KWh}&vAE{c*iCCZukiM&bvMwJCfiC}7OgC_N-<dSXatMpHw0VopU}~62QBBq)IrW)f3D`6|OjxLG1(3p)tI z$~g9r+4#xVInE~Dy82M(e!!emkyR3}f-L)++CIXwNaFhW z4O}N;txNYjBS1WbV`No^m=O>0Qfb%p!sgtppx~XaSCW&gAU!$H30(%HMm~ftNEmgK z?!r%Q>4n3+tQ2EgQi;@-?;t)5UTMN~QDHwZVsB5Bg~Qn_=|}q9k~La5PbdB?r>ZL} z0{tl9rX!-Z8>)MLFR~K8fcSX&+luRHv(`>9Tpykxd~c>b2v3>L-7@lOU&(Xx_>M3`aLhUkdoC zwoL=^M%ycS>X@GTjW_!u`HjM|UW&CFFnL;QoKZW$2$U6fHpNe_8>~$(>ZhJ}ch*bJ z9yo$q?nT}!%4Ym4-S|R++O8wrb{b%WZY;w|sfw%LO0T&?=665^_Yw16j3F$V-L4Zj z2O$WuRMJQtl^i_> z-!WKN_!BKTce8UEyk2`U3jA;m;P;zBf7a?TBYcD)%Nbmwn^% z=<9=L-!C|h)L)%DJg+1ae4*#}U>vL>PwUp0FIY?`idNS1tYB95y_sHQA0iG84;HD0 z!ZR#A(*0OMctkjV!V-8p?5!%gg@3>E{dLGeJ+cp)%Q>nFH+w0YVmWmQueQ0VO*YrB zY_qi_TOIgIB7lv>zyPIMJ|h-R6WFJ1An`MaG;A1MNmIqJu;*2QS#elHP}N-C7#wUd zD<6AwikTv45m~f?3Pb3K2@7AtM=T&P)aI1GN|FgbNwCL~ zOnVQB*Hvs@4T--a|M_W)HqERLr+Tm>Gs86fZYiZwLg8hzwWMWbqN~+%Y4URHGNQ0D zQEc>v*v*f@*GE670GwpZX6F0%=db+ykKUJF!PVuSwk?@S!#;>>_}1_zUpSX&$G=wf z#0b&-U^cpM5kNn>_#rs^s8*eKbO$H&El55@Sjh0DW!B;{?xAiy?M|udv#;<>?+=rg zFL>=}sg`9`XR-KUp&BLfhyj9Gm=w@3DEqnmTum6U5-OM>CO;Ti3O}S>Tn~_z)<0BG zv~o1Q7;?e-hiAHyMND(R-jr@j7*cw}JeT#cmUfr=*iZJ&pFZu4H6D4Eu#W7V53}sQ zvDJFC%fE4YZC<`Ljm}<{)Ks?`Utb{tvZO+W=F&>a&cWkm^R#7ugTmDt@?gwc+#b8( z-LyBhyMA`_$&&eO<40q{c$3j%;EQ>shQ-*Jua;u!gGwO^9Bnpi0MHEZ6N%3 znnrTyg+Xd=JxZ5UJ~M~A`hawn_|@6N$*zftU~sYx=mSd%N7Xj?@a3{4;gfqlw{9Bf z)Tz%jm!r3|Cz|A^CKqSJ$|Ow;U|CPtPAr55S^nZ{#I)~TFerR0^-no!HT(Sxq`D3J zoFG}Ps?KjM0>Jbh;V&*23l0Ev;HMv|c>zEgiu%5yf%-br6EmB1PvVMR9HB-AW>yx= z!-7?ct?pAO+RYeHiN@o*9E_%Fovz011if1hCv%}wQ!$&m-OQS`Nyct=cZD<1;IWYw zC+zXHxeha_&$~f&D}Kur^v}i}%@4UE2sUZsZLVYFJcwrU-cz@wzC2LNsQ#oMtLhfT6QrDrfVnrJ0o3eK$K& zKLHLRA=;%LU5D;nXZC#ME%ZGTAm$BHH_>wEh7kxDF+IM@S}X_FVuaMAQqt!qfLZ9( zyEXy@9$JF0*y7tKwrdp@2f;2SapO_$Q2lq2HClM}{WE5bnVU^>C*(+FH~TOS8NKrU za5rvX`P??Rv9blEUS()6r@R%?i^WSB{fS+z(fP(r{~sub*WX^IYp zCQe?x`Pd|mni*mk6$XJ4q!VPKa598$-U5cGOyDErcbh&Cptl(#qGi7L#ZG_~PBdH6 zv2upuM#-1CaHj_H4vKZm*d6t)*v!xvx6w2Huwf2*olfJopgdp1H}Kb(!vr56{S;cM z(7n=(p2n>43H`#!3RvrzIEKhQ?aX^6+uET4@7`ZmZrMJuIJPyS$6x8VnPM zXtsaZ%dJgk>z--luKYfi`cAmn4AS?+j!6+65zHQ)M|2S>3b+Q-{wyu|}AV-o+tki{wn7ofBdc2TpbL zvwmZWxT`&+XP48+S^07;um$3$FS30UBGTl(s3rOkRurtnXp6GY11Kt_Q)F1@!!KL))Gm!~hi`(NLZr<~J2C___f*pD1JTs7 z^vu^C>W0kAEZ;bedCta)y+5&t zBH}rSJ)?bx@MrBIV%(udDBN^DbNPlh+<5gK0vjrbdc3*(1@j_gXOkA~N)7B0U_z`7 zzb-`=?0}};!@-c&XO_adlNykcmAQholuDPg-bwb&wZCNZR4t0b0k`ex9VE$EZ1xW{ z^WR~5=6`5Tje|HWbce*t9vMOgl?uKou==6~1bZ&2pn8h;}#e^3^7_D_GHn2)~x*#6x8cPQqM zqyiuo6<`P1Y71jPK;y$1g};os%_dwKr@#QZPT{(s5Ce4sG&Y)l*YRJPhXti1S~0n16Oe{1b7^|IlOq zW#0|+U;88e*@pAM!!U8OeNZ|7Cmv?>gNIQSYqOl?>U7}h^k_@sU-g}mX`4Q@%|=d) zPl^wP3aTO|m`+3vf{jA%p^9cg(ITVjV~SiB`w?17A~G#{QpVaJO~tYWA6w?q$>Yk1 zIi=G3$0hSkT6ex^ZY{s<^Xo<0q1@GI$C&{N6di$f0GTCgA~Ap&85bqdkIo-FA}^(} zAFJbexfIJ(%|En>CI983BngaH?={?@1i<*Jtx|jdqWgIN03odiaAf3%|SrcIZnbaznU{;YIm|$|z z9B5wBj5dgBVjB*~STb_5rwQxx$G;8CK?Hs$Wj#EFo)&~%l?fj6@J-03_sFvd zyKGG`Q;Z=ujRBVu70}jxB5p$7l9H_H99y7s;`Qwt2k=k#b>>g+_GVjh>14VD2nhxU9;ow z0Ef23w{YdD^G`GWcjfm_Ge{3JaK1;d{I@Qga0nv)khM7K{!CSOh?S@PM@C1X8VG26mK-Jg`@;+ z5BY%As31CgzEzAKc)n3A{Ajl`WKZ?Zwds%8)|~?CVBV7v(oI04K>5NCQcLl|08(q( z^BcCCK$?+)1QDcLPLvlUmYgIM#4qVf2ncW57dnV|StY1*FhDPoffB0XnDzXbWw?F4$8vOI_Bn4~l6WDfKZ zD6(+~)uBqo2rapkwox2J_k_%N(@y9~@!F2|R?al)2I~(RU`e73pGS|9F*;X%$%~Ex z^)-qFEYw9&^ni^Og%`wfo-AlT;4RhZsgyFPw%*N|7a^1h#Q~xf@`M`pUcZNda9-L{ z7q8a|5loOPpN!}-NWJ4HqwEnB0Crbb9H}P6pCfup_TeiiC+eFINGlGcCQBj*IY|f> zU&7gSx{y$QcOpm6%eq_L$#(*;6OQMxZw4Z~0>aV2-nXv;5vv^;uV3HspIf^_2yU?& zavw2H!i8deZH8PzP;a;mIipXG{pg=dzr^~^`o{7h-rWfg>EEUM6F=iI#f$hThdk2V z<#k8<(i9jYiKc#25S7f%@kM@GAzB=L!+%;mH28to%6g0EMf;f+`Yr=-Gg4Uj@azs1o{I{K#`ry{268fCSqZY8s)bgg1%fYZOQ zGsH_0sZCijL@5%LSjbq(rY#AuXU=^vLr<9XMMc1Nk?Lq(%2I?4qkIbntw_tHRZIwYm~^IOBHt_JrfBvIsD( zWP&V|J^d<5sHGf3m)V%pg4M<998x-qGZ&;%rHYG-$Sqe|Zw-h7VV~7Fvhcbsma>{3 zl3FJ{VSg356i#nFy5JMok*ad3%`Gqqhr)hoj@eLC_1d;oj?-Wl1+{An2)k1<5)>hT z0Kq*Keulwal4yr$w1YUki1m#@2)`EsquTmBi3n;I{-6)KD`-w8APYiz2?3HR8u$X1 zX;1*PG(-m@)Q|MUjt+;Yt8cha4hl^(w}1`K2zgww7mN@@wd28e1n)EQ5VXA0w?gC& z-7C)ccynE(+$ol{vreCd+|d?1zXRu0-u-0;5>73?^DQm(v%5RT@FDYTFGG9NU-pI~ z4sF_5NClJ1f9A$=zein|MHBda%Qo=c2_d$BIn>X8{)NhFZHygXJnqw&9nTBZ`gHul zn1jPk8*YJwIWMp8Jks8hyI0L2()&+cq@>=Gq*im7*Ejnby&^BD3>&P!-cSM}>6PCc zXyUmA4W9) zcp@nxGeMymE#J@ejE-73qzE>_LCfC2#5d!AI<*dSpTv^n&C^0O=tzo5KC>RrS!UL^|}?Tr$07Movjg(@uZxg>6@Y9q|Nt1-v3=i`M2VfT#qzIP&SoDkwr* z@pXP(qa{RsXBjOPfO?`RT(o4Dw8Z0vc<>iLFF=}@Kfv4Q4XREb6l;MGzhqlMG!oz! zot!bZ7I2HlEjGImm}HDpEk~L0hM)n>_oe$WTTcY zh5lFf;@?vk$ZsIL6F@IkN$P0p2{IuU0^q2ABf5DP!WoZ_Pz}$7NZz2)4T<6fdGflT@jV>(Cxc06*pZ(a(CE@D2|H78iIZ2z>7!u5quUKs&nPt*l)E!00OH}Jyd zg~tc%9nwFjdjJcPW-uAG5i{Rh$))Ueh7bn!$Zbi+pSnH@j70yY`5!mX?aR3E1B^esZtf6PpaVEY5R=}V~i`qPXZ57*e4`A&x}mN#zpv5?3^$G+*}%=0Q2py-Oj8fZS{_V@UH%?UcimJJ5;r0lFI* z`TVx!nlo*>cge-YcFs0x(Uk4LO`AfdP@NjXIYX5MbJEe7!Ez^)o4`rILU(7gL>4xY zWgZekPeaZ12Z7@3jYw8Y@Xc{h36bGdK_a6&7HPz? zMU&9m7!Vx33ksC}Mr zE`IQ>xs0vE-A-;Vue2zm9`mq=&QCI?q;^{cEwc=5Vexo1 zTL#~WOJn>Nk^T@Oqj1pvK53vJN5r)RWvOYc6>oR40jRMJVlK9R93*9IWlp2R%o8bc;fHpt}PxEzp+!-S_ zCpjQxaA(ajIiQ7z+!7afHjVVRl2x559_jd z(;zgSty32+Wz`_|Zacj< z-A=kw9eX1mboODa|ZTgG$?Y_r{&@!kzfx{|*WYT^Kp?%*cN>)i#_zD2t40#P?&Wd{Ld zi*!q!4KxR8_<(1V&$0;wE+0RrBdKCUG%>slxY2T`t18YHkp)2%_VC814H@@)DDOmY zUU)Um-*ot0gXlFlFi^9lR&t{;Vgo@Bk&^w2nz&-4M3^68ILPCcHrJh32kJgD=0L{N zX0QDIKz$N^(qao|>ob{IqOWq)O;weUJ37;^MFJ@{$6ON1C2oERI-^ZXU}8O+@)t2W zITiI*(P86c`e2RxKGLyc3vIAKoc&6j9~Bl%SQR?y->g-2_|)Z$os`Pw%5Q~TqXf8| zE}5OqJ??Yz*1L8otK+?#J9_Ys(l>Qler#PA^yHAN?8{AesL)~saJAlWl@LWw>6O*`CgrjN9r=*XbZ zQq|E2fyk>IapOqGCXvrZb&{BAX{$XH^R>iQj&BHSG{ai8a-cdPbxbFXui0%FKE4+M zxn8~lV@wl@ZGhw6INHXQrb$|_kTfiRt`c1{_A^GZ`f6nwj%6@tr5Yf`9(0_uNj=F} zh?UW$CK+kiV>s@Y!tcQ=NUCK^>8+>T>l%E48$Q_gSE>Ka;F zIKoX$g+n$HQm}7D(VRV!;2{lwlSB|ARO(U>+;5}`2GTs25ZuSxWuLcaf?!iXm3|e& zJz%BVP&0Nf2(9j+leUhu)mbC0op*eJ`bJp{T_QMQc8GKkcbH_z!CN81cx^+{rU)5(=|HLE}Nb^mftVYoIs?0X8YqAQ4i*3C{4rH#bJbeBa`PWFxMsna^G znE#d5Q4!KrOTGBO5gUm`DwIu$buxGMGQFC3WIy|G8+TeR^^d{9q}LQJhNba`%RK+m z+$+j*27OkoAJ<$p7%xPR_guHAvZQ$UN5!tUt2iZsW3f5LjStbtC~xHC$#m z)dZn^f>h?;x&Sy2QL~s>y@%YX8cO>4kdN31B~xH0h-oNz0bo(53zjXm2##;FVB%6< zp*ZneY4h6{d6dzY@35L#4V1UDp30TKVw%j1#2REvrYv=&EB89?FBlzkP3FpOpqjar zHNC6X3$ng9*0`UIU;K=5&xVGE50rf0`N4`b-u1ln_*fO0VAJ@x7tthl3oep`7h8gj zM#}+_>FjFsd6svqu`yRYp!)IuDdfzfq2Av(eoM%dN)k~b*%>p#%otg&En@6r>|}p|;W~NjC{MMUr3tV%m7d+cUcM&m29h zX_B7v$Q#DGvv z)}$!KwkSg3Cnz{7{BW_?xnTM9Sa&e)eDH!s#;cP8*VK!y?oqD4$ZcbOHt^x&J@1m( z_&LC;na4}%CcQ$JDB?NJd6fEgUs96s2eCzoS3UAe%^!=_M^wJP3!Y!tpM-x%m5%1w zUr1Dlt&Db{))A#1+0w;H4Hlq9rSpktObbff70-oNbCrdn^^P^4t?quYo43LkRLceJ zUeCEwoUd1U?*r}QQXy?De4citefcJR&r%d!azNvu-cqfnReL!U>pU^ruM7^s&$$-9 zSm2H9;P*Pg=Fmoih83BnK^+S;VJ6~z?aOPjf?wG0j>R%$`JB#q!=Pqbzx&Y9XAdsT zsun3RsbRElc3O(A9-orI)o@quAqf_{@kktW3LRk+mr2xO(|qm>Q;k8+?#Ywpftzyk zB9RYr3;k!vh23xFwK;&nBeT-c2Q%~qO@m&ea0FT+VbrtBcZpeIl{OpXOZe{hvEAyW zA9d~#x&c{Qp)SbDxzrqW8%rY=bV{PDb*S+FF+Y6m$;1z%nznk$6ER%U?Xj9qwQs!Beu-rm@!z*rmRsgyMB zF6u3QM~#;`it7wBq4!F%gmS$BOE`2-4ex#?cIKNn?0#O(dx&F6w=L}jeayZ1g9SzV zIIZP}?~1hbYPNzkllpwK7Dhu;8R-tobLadSINy~Vlg?A~GOTT!5FCJi387r>*-(4Y zGOcL(_WSYhIE8A;4^^&VuHjPE+_lg2xdT-m*rBMh2-$MP!b|A*H@ONV04MOYlG0%p_@PSai4sz4`<%M>m+cTf>SDya)mbU z)!*c`q8OxS)YG<7z!QRSu3k3mI+8?xtEG1Y~OlZ>j@<-q~h1*(tF;gLc3+Gq zLuIS-!+NoU5tG#3HszOM7g*@*x%SWMafr0ouTO>28wktN3mB-K0t%xzcHO>u_>Ss14(g0eNsEW)zA z&PMCS-$yEt6dmOp%W}|S((2AppAuy0S0I|oQq+C<25Q>=Z(~L7?k&3~)zLN36DJ9C zZ9c`Y@gi5d>6B4QvYVbx`{5%Q9XZphi?n{tvQ=>^&kuH|Xu>ReAc|>bF>6BMM53n^ zv^6JaP{3a3D2tqURLpGhE01ph(PiDj)wtV|qc@eWHC;GzozivgnWzC`)`Y>2qUhP{ z;#gz(Ib+SJJ0$8L$5da>9r3{ z8dHgNor*O3#G8-%&%QoBqy;4+7WU5~*dj1sw<1tMU#9EOI&O8b`fi+8C6iUQ)nnQ* zISzKqTj#q|Mh#vR9`&49w7(HQvq)jNP;t~DcB~>ShygkTh9!8Ia2Ou(PU!Fmq&WE| zh|`b0Uz*00MpT-;Ew_r<(keDmic~WOHp40$F$c-Q{tZ2QleI+G{kN#jGvBG znv?LmaoosljE&C53)UiEjx_p+)j!o=5EnKa)E3$k%3g3&ORGvG=(XVFqq%+ol;`x3 z*BMh?8K#wNj7$t?7~E45?w>Dg@^r}D_eAmP+xZrMG!@lCC8(i2jNc!btt?)e&WESB z=kzt)?x|bbC6auy)HKvf}S6&UtU%JtL-J zXe_NfZIz-mHb59;& zSB`f0%@(ht;8bgHAEB{8G>qm|NK7^zhnr_%;RtK5-ukNs_rFp;27h4m@o^#foVzd; zk!Ssq+t{#eDn;qk$~5|l<{SrYy!?i`d|rEf3n3l*9G`u-Ao{tWU!)PwbF7?qVDDf) zxtiYJ8T9KN}!vD)ic%{{c^+_R}n<@Y!K(mU=^Du1+s-1bJ<31z8`#Ih8|9oG$XTC%5RPE zJY&Vinob4d%SwnZtK+|eM$f-LP$bUe*v+s$OdoHJR=BrfxE3ao)n^7`6HI-xJ~Lp8 zHUOOs!Iujpd9E#&pME-Ubeud4BN1nK;VBcT*#3xid*;U$fADj0cu8E!;I*2*)v8 zROz??S{IUMz3f)WblD=AHG2drbF%rmHP_|BMit%NJt`jE{kf0Q0-pHOe6h@Q6A6p! z8z*LkNRs0Fc=Fl+WdF4FZut0Jm)O`l(584j=rcL4ecTp~=VqXYi{p1diC@D=o7l zFBAqx?(mg{BZ0pEZ}AYw9eJgtWp~&~!(lr$l)8QA81`y~gkmrZ`kZyE{?RQIo% zG!*rF4j2T90B*%Ec7V2He*n4zveQm>M-ISllb-ThJlX%qi`uN5-|SHTu@{sqP(a&u z?qpj4KtkTanHo7>0?H4`CuK+?0Zg60tM{A>fdl|{wkr9Y8i3f5C%|Q8VQ?7&9)*J; vq;axP0ty1(e23cF$>3~d6hZ&rl1``~O0 literal 0 HcmV?d00001 diff --git a/tests/remote_js/helpers.js b/tests/remote_js/helpers.js index 3f8c6f56..f96d045e 100644 --- a/tests/remote_js/helpers.js +++ b/tests/remote_js/helpers.js @@ -145,6 +145,10 @@ class Helpers { this.assertStatusCode(response, 400); }; + static assert401 = (response) => { + this.assertStatusCode(response, 401); + }; + static assert403 = (response) => { this.assertStatusCode(response, 403); }; @@ -161,6 +165,10 @@ class Helpers { this.assertStatusCode(response, 404); }; + static assert500 = (response) => { + this.assertStatusCode(response, 500); + }; + static assert400ForObject = (response, { index = 0, message = null } = {}) => { this.assertStatusForObject(response, 'failed', index, 400, message); }; diff --git a/tests/remote_js/test/3/fileTest.js b/tests/remote_js/test/3/fileTest.js index 82cd46cb..70dccf50 100644 --- a/tests/remote_js/test/3/fileTest.js +++ b/tests/remote_js/test/3/fileTest.js @@ -3,13 +3,14 @@ const assert = chai.assert; const config = require("../../config.js"); const API = require('../../api3.js'); const Helpers = require('../../helpers3.js'); -const { API3Setup, API3WrapUp } = require("../shared.js"); +const { API3Setup, API3WrapUp, APISetCredentials } = require("../shared.js"); const { S3Client, DeleteObjectsCommand, PutObjectCommand } = require("@aws-sdk/client-s3"); const fs = require('fs'); const HTTP = require('../../httpHandler.js'); const crypto = require('crypto'); const util = require('util'); const exec = util.promisify(require('child_process').exec); +const JSZIP = require("jszip"); describe('FileTestTests', function () { this.timeout(0); @@ -41,6 +42,10 @@ describe('FileTestTests', function () { } }); + beforeEach(async () => { + await APISetCredentials(); + }); + const md5 = (str) => { return crypto.createHash('md5').update(str).digest('hex'); }; @@ -171,7 +176,7 @@ describe('FileTestTests', function () { } ); Helpers.assert201(response); - + response = await API.userPost( config.userID, `items/${attachmentKey}/file`, @@ -198,7 +203,30 @@ describe('FileTestTests', function () { }); const getRandomUnicodeString = function () { - return "Âéìøü 这是一个测试。 " + Helpers.uniqueID(); + const rand = crypto.randomInt(10, 100); + return "Âéìøü 这是一个测试。 " + Helpers.uniqueID(rand); + }; + + const generateZip = async (file, fileContents, archiveName) => { + const zip = new JSZIP(); + + zip.file(file, fileContents); + zip.file("file.css", getRandomUnicodeString()); + + const content = await zip.generateAsync({ + type: "nodebuffer", + compression: "DEFLATE", + compressionOptions: { level: 1 } + }); + fs.writeFileSync(archiveName, content); + + // Because when the file is sent, the buffer is stringified, we have to hash the stringified + // fileContents and get the size of stringified buffer here, otherwise they wont match. + return { + hash: md5(content.toString()), + zipSize: Buffer.from(content.toString()).byteLength, + fileContent: fs.readFileSync(archiveName) + }; }; it('testExistingFileWithOldStyleFilename', async function () { @@ -708,4 +736,1535 @@ describe('FileTestTests', function () { }; return testResult; }; + + + //////////////// + + + it('testAddFileClientV4Zip', async function () { + await API.userClear(config.userID); + + const auth = { + username: config.username, + password: config.password, + }; + + const response1 = await API.userGet( + config.userID, + 'laststoragesync?auth=1', + {}, + auth + ); + Helpers.assert404(response1); + + const json1 = await API.createItem('book', false, this, 'jsonData'); + let key = json1.key; + + const fileContentType = 'text/html'; + const fileCharset = 'UTF-8'; + const fileFilename = 'file.html'; + const fileModtime = Date.now(); + + const json2 = await API.createAttachmentItem('imported_url', [], key, this, 'jsonData'); + key = json2.key; + //const version = json2.version; + json2.contentType = fileContentType; + json2.charset = fileCharset; + json2.filename = fileFilename; + + const response2 = await API.userPut( + config.userID, + `items/${key}`, + JSON.stringify(json2), + { + 'Content-Type': 'application/json', + } + ); + Helpers.assert204(response2); + const originalVersion = response2.headers['last-modified-version'][0]; + + const response3 = await API.userGet( + config.userID, + `items/${json2.key}/file?auth=1&iskey=1&version=1&info=1`, + {}, + auth + ); + Helpers.assert404(response3); + + + const { hash, zipSize, fileContent } = await generateZip(fileFilename, getRandomUnicodeString(), `work/${key}.zip`); + + const filename = `${key}.zip`; + + const response4 = await API.userPost( + config.userID, + `items/${json2.key}/file?auth=1&iskey=1&version=1`, + implodeParams({ + md5: hash, + filename: filename, + filesize: zipSize, + mtime: fileModtime, + zip: 1, + }), + { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + auth + ); + Helpers.assert200(response4); + Helpers.assertContentType(response4, 'application/xml'); + const xml = API.getXMLFromResponse(response4); + toDelete.push(`${hash}`); + const xmlParams = xml.getElementsByTagName('params')[0]; + const urlComponent = xml.getElementsByTagName('url')[0]; + const keyComponent = xml.getElementsByTagName('key')[0]; + let url = urlComponent.innerHTML; + const boundary = `---------------------------${Helpers.uniqueID()}`; + let postData = ''; + + for (let child of xmlParams.children) { + const key = child.tagName; + const val = child.innerHTML; + postData += `--${boundary}\r\nContent-Disposition: form-data; name="${key}"\r\n\r\n${val}\r\n`; + } + postData += `--${boundary}\r\nContent-Disposition: form-data; name="file"\r\n\r\n${fileContent}\r\n`; + postData += `--${boundary}--`; + + const response5 = await HTTP.post(`${url}`, postData, { + 'Content-Type': `multipart/form-data; boundary=${boundary}`, + }); + Helpers.assert201(response5); + + const response6 = await API.userPost( + config.userID, + `items/${json2.key}/file?auth=1&iskey=1&version=1`, + `update=${keyComponent.innerHTML}&mtime=${fileModtime}`, + { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + auth + ); + Helpers.assert204(response6); + + const response7 = await API.userGet(config.userID, `items/${json2.key}`); + const json3 = API.getJSONFromResponse(response7).data; + Helpers.assertEquals(originalVersion, json3.version); + Helpers.assertEquals(hash, json3.md5); + Helpers.assertEquals(fileFilename, json3.filename); + Helpers.assertEquals(fileModtime, json3.mtime); + + const response8 = await API.userGet( + config.userID, + 'laststoragesync?auth=1', + {}, + { + username: config.username, + password: config.password, + } + ); + Helpers.assert200(response8); + const mtime = response8.data; + Helpers.assertRegExp(/^[0-9]{10}$/, mtime); + + const response9 = await API.userPost( + config.userID, + `items/${json2.key}/file?auth=1&iskey=1&version=1`, + implodeParams({ + md5: hash, + filename, + filesize: zipSize, + mtime: fileModtime + 1000, + zip: 1, + }), + { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + auth + ); + Helpers.assert200(response9); + Helpers.assertContentType(response9, 'application/xml'); + Helpers.assertEquals('', response9.data); + + const response10 = await API.userGet(config.userID, `items/${json2.key}`); + const json4 = API.getJSONFromResponse(response10).data; + Helpers.assertEquals(originalVersion, json4.version); + }); + + it('test_should_not_allow_anonymous_access_to_file_in_public_closed_group_with_library_reading_for_all', async function () { + let file = "work/file"; + let fileContents = getRandomUnicodeString(); + fs.writeFileSync(file, fileContents); + let hash = md5File(file); + let filename = `test_${fileContents}`; + let mtime = parseInt(fs.statSync(file).mtimeMs); + let size = fs.statSync(file).size; + + let groupID = await API.createGroup({ + owner: config.userID, + type: "PublicClosed", + name: Helpers.uniqueID(14), + libraryReading: "all", + fileEditing: "members", + }); + + let parentKey = await API.groupCreateItem(groupID, "book", false, this, "key"); + let attachmentKey = await API.groupCreateAttachmentItem( + groupID, + "imported_file", + { + contentType: "text/plain", + charset: "utf-8", + }, + parentKey, + this, + "key" + ); + + // Get authorization + let response = await API.groupPost( + groupID, + `items/${attachmentKey}/file`, + implodeParams({ + md5: hash, + mtime, + filename, + filesize: size, + }), + { + "Content-Type": "application/x-www-form-urlencoded", + "If-None-Match": "*", + }, + + ); + Helpers.assert200(response); + let json = API.getJSONFromResponse(response); + + toDelete.push(hash); + + // + // Upload to S3 + // + response = await HTTP.post(json.url, `${json.prefix}${fileContents}${json.suffix}`, { + + "Content-Type": `${json.contentType}`, + }, + ); + Helpers.assert201(response); + + // Successful registration + response = await API.groupPost( + groupID, + `items/${attachmentKey}/file`, + `upload=${json.uploadKey}`, + { + + "Content-Type": "application/x-www-form-urlencoded", + "If-None-Match": "*", + }, + + ); + Helpers.assert204(response); + + response = await API.get(`groups/${groupID}/items/${attachmentKey}/file`); + Helpers.assert302(response); + response = await API.get(`groups/${groupID}/items/${attachmentKey}/file/view`); + Helpers.assert302(response); + response = await API.get(`groups/${groupID}/items/${attachmentKey}/file/view/url`); + Helpers.assert200(response); + + API.useAPIKey(""); + response = await API.get(`groups/${groupID}/items/${attachmentKey}/file`); + Helpers.assert404(response); + response = await API.get(`groups/${groupID}/items/${attachmentKey}/file/view`); + Helpers.assert404(response); + response = await API.get(`groups/${groupID}/items/${attachmentKey}/file/view/url`); + Helpers.assert404(response); + + await API.deleteGroup(groupID); + }); + + //TODO: this fails + it('test_should_include_best_attachment_link_on_parent_for_imported_url', async function () { + let json = await API.createItem("book", false, this, 'json'); + assert.equal(0, json.meta.numChildren); + let parentKey = json.key; + + json = await API.createAttachmentItem("imported_url", [], parentKey, this, 'json'); + let attachmentKey = json.key; + let version = json.version; + + let filename = "test.html"; + let mtime = Date.now(); + let size = fs.statSync("data/test.html.zip").size; + let md5 = "af625b88d74e98e33b78f6cc0ad93ed0"; + let zipMD5 = "f56e3080d7abf39019a9445d7aab6b24"; + + let fileContents = fs.readFileSync("data/test.html.zip"); + //let zipMD5 = md5File("data/test.html.zip"); + let zipFilename = attachmentKey + ".zip"; + //let size = Buffer.from(fileContents.toString()).byteLength; + + // Create attachment item + let response = await API.userPost( + config.userID, + "items", + JSON.stringify([ + { + key: attachmentKey, + contentType: "text/html" + } + ]), + { + + "Content-Type": "application/json", + "If-Unmodified-Since-Version": version + } + + ); + Helpers.assert200ForObject(response); + + // 'attachment' link shouldn't appear if no uploaded file + response = await API.userGet( + config.userID, + "items/" + parentKey + ); + json = API.getJSONFromResponse(response); + assert.notProperty(json.links, 'attachment'); + + // Get upload authorization + response = await API.userPost( + config.userID, + "items/" + attachmentKey + "/file", + implodeParams({ + md5: md5, + mtime: mtime, + filename: filename, + filesize: size, + zipMD5: zipMD5, + zipFilename: zipFilename + }), + { + + "Content-Type": "application/x-www-form-urlencoded", + "If-None-Match": '*' + } + + ); + Helpers.assert200(response); + json = API.getJSONFromResponse(response); + + // If file doesn't exist on S3, upload + if (!json.exists) { + response = await HTTP.post( + json.url, + json.prefix + fileContents + json.suffix, + { "Content-Type": json.contentType } + ); + Helpers.assert201(response); + + // Post-upload file registration + response = await API.userPost( + config.userID, + "items/" + attachmentKey + "/file", + "upload=" + json.uploadKey, + { + + "Content-Type": "application/x-www-form-urlencoded", + "If-None-Match": "*" + } + + ); + Helpers.assert204(response); + } + toDelete.push(zipMD5); + + // 'attachment' link should now appear + response = await API.userGet( + config.userID, + "items/" + parentKey + ); + json = API.getJSONFromResponse(response); + assert.property(json.links, 'attachment'); + assert.property(json.links.attachment, 'href'); + assert.equal('application/json', json.links.attachment.type); + assert.equal('text/html', json.links.attachment.attachmentType); + assert.notProperty(json.links.attachment, 'attachmentSize'); + }); + + it('testClientV5ShouldRejectFileSizeMismatch', async function () { + await API.userClear(config.userID); + + const file = 'work/file'; + const fileContents = getRandomUnicodeString(); + const contentType = 'text/plain'; + const charset = 'utf-8'; + fs.writeFileSync(file, fileContents); + const hash = md5File(file); + const filename = `test_${fileContents}`; + const mtime = fs.statSync(file).mtimeMs; + let size = 0; + + const json = await API.createAttachmentItem('imported_file', { + contentType, + charset + }, false, this, 'jsonData'); + const key = json.key; + //const originalVersion = json.version; + + // Get authorization + const response = await API.userPost( + config.userID, + `items/${key}/file`, + implodeParams({ + md5: hash, + mtime, + filename, + filesize: size + }), + { + 'Content-Type': 'application/x-www-form-urlencoded', + 'If-None-Match': '*' + } + ); + Helpers.assert200(response); + const jsonObj = await API.getJSONFromResponse(response); + + // Try to upload to S3, which should fail + const s3Response = await HTTP.post( + jsonObj.url, + jsonObj.prefix + fileContents + jsonObj.suffix, + { + 'Content-Type': jsonObj.contentType + } + ); + Helpers.assert400(s3Response); + assert.include( + s3Response.data, + 'Your proposed upload exceeds the maximum allowed size' + ); + }); + + it('test_updating_attachment_hash_should_clear_associated_storage_file', async function () { + let file = "work/file"; + let fileContents = getRandomUnicodeString(); + let contentType = "text/html"; + let charset = "utf-8"; + + fs.writeFileSync(file, fileContents); + + let hash = md5File(file); + let filename = "test_" + fileContents; + let mtime = parseInt(fs.statSync(file).mtime * 1000); + let size = parseInt(fs.statSync(file).size); + + + let json = await API.createAttachmentItem("imported_file", { + contentType: contentType, + charset: charset + }, false, this, 'jsonData'); + let itemKey = json.key; + + let response = await API.userPost( + config.userID, + "items/" + itemKey + "/file", + implodeParams({ + md5: hash, + mtime: mtime, + filename: filename, + filesize: size + }), + { + "Content-Type": "application/x-www-form-urlencoded", + "If-None-Match": "*" + } + ); + Helpers.assert200(response); + + json = API.getJSONFromResponse(response); + + toDelete.push(hash); + + response = await HTTP.post( + json.url, + json.prefix + fileContents + json.suffix, + { + "Content-Type": json.contentType + } + ); + Helpers.assert201(response); + + response = await API.userPost( + config.userID, + "items/" + itemKey + "/file", + "upload=" + json.uploadKey, + { + "Content-Type": "application/x-www-form-urlencoded", + "If-None-Match": "*" + } + ); + Helpers.assert204(response); + let newVersion = response.headers['last-modified-version'][0]; + + filename = "test.pdf"; + mtime = Date.now(); + hash = md5(Helpers.uniqueID()); + + response = await API.userPatch( + config.userID, + "items/" + itemKey, + JSON.stringify({ + filename: filename, + mtime: mtime, + md5: hash, + }), + { + "Content-Type": "application/json", + "If-Unmodified-Since-Version": newVersion + } + ); + Helpers.assert204(response); + + response = await API.userGet( + config.userID, + "items/" + itemKey + "/file" + ); + Helpers.assert404(response); + }); + + it('test_add_embedded_image_attachment', async function () { + await API.userClear(config.userID); + + const noteKey = await API.createNoteItem("", null, this, 'key'); + + const file = "work/file"; + const fileContents = getRandomUnicodeString(); + const contentType = "image/png"; + fs.writeFileSync(file, fileContents); + const hash = md5(fileContents); + const filename = "image.png"; + const mtime = fs.statSync(file).mtime * 1000; + const size = fs.statSync(file).size; + + let json = await API.createAttachmentItem("embedded_image", { + parentItem: noteKey, + contentType: contentType + }, false, this, 'jsonData'); + + const key = json.key; + const originalVersion = json.version; + + // Get authorization + let response = await API.userPost( + config.userID, + `items/${key}/file`, + implodeParams({ + md5: hash, + mtime: mtime, + filename: filename, + filesize: size + }), + { + + "Content-Type": "application/x-www-form-urlencoded", + "If-None-Match": "*" + } + + ); + Helpers.assert200(response); + json = API.getJSONFromResponse(response); + + toDelete.push(hash); + + // Upload to S3 + response = await HTTP.post( + json.url, + `${json.prefix}${fileContents}${json.suffix}`, + { + + "Content-Type": `${json.contentType}` + } + + ); + Helpers.assert201(response); + + // Register upload + response = await API.userPost( + config.userID, + `items/${key}/file`, + `upload=${json.uploadKey}`, + { + + "Content-Type": "application/x-www-form-urlencoded", + "If-None-Match": "*" + + } + ); + Helpers.assert204(response); + const newVersion = response.headers['last-modified-version']; + assert.isAbove(parseInt(newVersion), parseInt(originalVersion)); + + // Verify attachment item metadata + response = await API.userGet( + config.userID, + `items/${key}` + ); + json = API.getJSONFromResponse(response).data; + assert.equal(hash, json.md5); + assert.equal(mtime, json.mtime); + assert.equal(filename, json.filename); + assert.equal(contentType, json.contentType); + assert.notProperty(json, 'charset'); + }); + + it('testAddFileClientV5Zip', async function () { + await API.userClear(config.userID); + + const fileContents = getRandomUnicodeString(); + const contentType = "text/html"; + const charset = "utf-8"; + const filename = "file.html"; + const mtime = Date.now() / 1000 | 0; + const hash = md5(fileContents); + + // Get last storage sync + let response = await API.userGet(config.userID, "laststoragesync"); + Helpers.assert404(response); + + let json = await API.createItem("book", false, this, 'jsonData'); + let key = json.key; + + json = await API.createAttachmentItem("imported_url", { + contentType, + charset + }, key, this, 'jsonData'); + key = json.key; + + const zipData = await generateZip(filename, getRandomUnicodeString(), `work/${key}.zip`); + + const zipFilename = `${key}.zip`; + + + // + // Get upload authorization + // + response = await API.userPost(config.userID, `items/${key}/file`, implodeParams({ + md5: hash, + mtime: mtime, + filename: filename, + filesize: zipData.zipSize, + zipMD5: zipData.hash, + zipFilename: zipFilename + }), { + + "Content-Type": "application/x-www-form-urlencoded", + "If-None-Match": "*" + } + ); + Helpers.assert200(response); + json = API.getJSONFromResponse(response); + + toDelete.push(zipData.hash); + + // Upload to S3 + response = await HTTP.post(json.url, json.prefix + zipData.fileContent + json.suffix, { + + "Content-Type": json.contentType + + }); + Helpers.assert201(response); + + // + // Register upload + // + + // If-Match with file hash shouldn't match unregistered file + response = await API.userPost(config.userID, `items/${key}/file`, `upload=${json.uploadKey}`, { + + "Content-Type": "application/x-www-form-urlencoded", + "If-Match": hash + + }); + Helpers.assert412(response); + + // If-Match with ZIP hash shouldn't match unregistered file + response = await API.userPost(config.userID, `items/${key}/file`, `upload=${json.uploadKey}`, { + + "Content-Type": "application/x-www-form-urlencoded", + "If-Match": zipData.hash + } + ); + Helpers.assert412(response); + + response = await API.userPost(config.userID, `items/${key}/file`, `upload=${json.uploadKey}`, { + + "Content-Type": "application/x-www-form-urlencoded", + "If-None-Match": "*" + } + ); + Helpers.assert204(response); + const newVersion = response.headers["last-modified-version"]; + + // Verify attachment item metadata + response = await API.userGet(config.userID, `items/${key}`); + json = API.getJSONFromResponse(response).data; + assert.equal(hash, json.md5); + assert.equal(mtime, json.mtime); + assert.equal(filename, json.filename); + assert.equal(contentType, json.contentType); + assert.equal(charset, json.charset); + + response = await API.userGet(config.userID, "laststoragesync"); + Helpers.assert200(response); + Helpers.assertRegExp(/^[0-9]{10}$/, response.data); + + // File exists + response = await API.userPost(config.userID, + `items/${key}/file`, + implodeParams({ + md5: hash, + mtime: mtime + 1000, + filename: filename, + filesize: zipData.zipSize, + zip: 1, + zipMD5: zipData.hash, + zipFilename: zipFilename + }), { + + "Content-Type": "application/x-www-form-urlencoded", + "If-Match": hash + } + ); + Helpers.assert200(response); + json = API.getJSONFromResponse(response); + assert.property(json, "exists"); + const version = response.headers["last-modified-version"]; + assert.isAbove(parseInt(version), parseInt(newVersion)); + }); + + it('test_updating_compressed_attachment_hash_should_clear_associated_storage_file', async function () { + let fileContents = getRandomUnicodeString(); + let contentType = "text/html"; + let charset = "utf-8"; + let filename = "file.html"; + let mtime = Math.floor(Date.now() / 1000); + let hash = md5(fileContents); + + let json = await API.createAttachmentItem("imported_file", { + contentType: contentType, + charset: charset + }, false, this, 'jsonData'); + let itemKey = json.key; + + let file = "work/" + itemKey + ".zip"; + let zipFilename = "work/" + itemKey + ".zip"; + + const zipData = await generateZip(file, fileContents, zipFilename); + let zipHash = zipData.hash; + let zipSize = zipData.zipSize; + let zipFileContents = zipData.fileContent; + + let response = await API.userPost( + config.userID, + "items/" + itemKey + "/file", + implodeParams({ + md5: hash, + mtime: mtime, + filename: filename, + filesize: zipSize, + zipMD5: zipHash, + zipFilename: zipFilename + }), + { + + "Content-Type": "application/x-www-form-urlencoded", + "If-None-Match": "*" + } + + ); + Helpers.assert200(response); + json = API.getJSONFromResponse(response); + + toDelete.push(zipHash); + + response = await HTTP.post( + json.url, + json.prefix + zipFileContents + json.suffix, + { + + "Content-Type": json.contentType + } + + ); + Helpers.assert201(response); + + response = await API.userPost( + config.userID, + "items/" + itemKey + "/file", + "upload=" + json.uploadKey, + { + + "Content-Type": "application/x-www-form-urlencoded", + "If-None-Match": "*" + } + + ); + Helpers.assert204(response); + let newVersion = response.headers['last-modified-version']; + + hash = md5(Helpers.uniqueID()); + mtime = Date.now(); + zipHash = md5(Helpers.uniqueID()); + zipSize += 1; + response = await API.userPatch( + config.userID, + "items/" + itemKey, + JSON.stringify({ + md5: hash, + mtime: mtime, + filename: filename + }), + { + + "Content-Type": "application/json", + "If-Unmodified-Since-Version": newVersion + } + + ); + Helpers.assert204(response); + + response = await API.userGet( + config.userID, + "items/" + itemKey + "/file" + ); + Helpers.assert404(response); + }); + + it('test_replace_file_with_new_file', async function () { + await API.userClear(config.userID); + + const file = "work/file"; + const fileContents = getRandomUnicodeString(); + const contentType = "text/html"; + const charset = "utf-8"; + fs.writeFileSync(file, fileContents); + const hash = md5File(file); + const filename = "test_" + fileContents; + const mtime = fs.statSync(file).mtime * 1000; + const size = fs.statSync(file).size; + + const json = await API.createAttachmentItem("imported_file", { + contentType: contentType, + charset: charset + }, false, this, 'jsonData'); + const key = json.key; + + // Get authorization + const response = await API.userPost( + config.userID, + `items/${key}/file`, + implodeParams({ + md5: hash, + mtime: mtime, + filename: filename, + filesize: size + }), + { + "Content-Type": "application/x-www-form-urlencoded", + "If-None-Match": "*" + } + ); + Helpers.assert200(response); + const data = JSON.parse(response.data); + + toDelete.push(hash); + + const s3FilePath + = data.prefix + fileContents + data.suffix; + // Upload to S3 + const s3response = await HTTP.post( + data.url, + s3FilePath, + { + "Content-Type": data.contentType + } + ); + Helpers.assert201(s3response); + + // Successful registration + const success = await API.userPost( + config.userID, + `items/${key}/file`, + "upload=" + data.uploadKey, + { + "Content-Type": "application/x-www-form-urlencoded", + "If-None-Match": "*" + } + ); + Helpers.assert204(success); + + // Verify attachment item metadata + const metaDataResponse = await API.userGet( + config.userID, + `items/${key}` + ); + const metaDataJson = API.getJSONFromResponse(metaDataResponse); + Helpers.assertEquals(hash, metaDataJson.data.md5); + Helpers.assertEquals(mtime, metaDataJson.data.mtime); + Helpers.assertEquals(filename, metaDataJson.data.filename); + Helpers.assertEquals(contentType, metaDataJson.data.contentType); + Helpers.assertEquals(charset, metaDataJson.data.charset); + // update file + const newFileContents + = getRandomUnicodeString() + getRandomUnicodeString(); + fs.writeFileSync(file, newFileContents); + const newHash = md5File(file); + const newFilename = "test_" + newFileContents; + const newMtime = fs.statSync(file).mtime * 1000; + const newSize = fs.statSync(file).size; + + // Update file + const updateResponse + = await API.userPost( + config.userID, + `items/${key}/file`, + implodeParams({ + md5: newHash, + mtime: newMtime, + filename: newFilename, + filesize: newSize + }), + { + "Content-Type": "application/x-www-form-urlencoded", + "If-Match": hash + } + ); + Helpers.assert200(updateResponse); + const updateData = JSON.parse(updateResponse.data); + + toDelete.push(newHash); + // Upload to S3 + const updateS3response = await HTTP.post( + updateData.url, + `${updateData.prefix}${newFileContents}${updateData.suffix}`, + { + "Content-Type": updateData.contentType + } + ); + Helpers.assert201(updateS3response); + + // Successful registration + const succeeded = await API.userPost( + config.userID, + `items/${key}/file`, + `upload=${updateData.uploadKey}`, + { + "Content-Type": "application/x-www-form-urlencoded", + "If-Match": hash + } + ); + Helpers.assert204(succeeded); + + // Verify new attachment item metadata + const updatedMetaDataResponse = await API.userGet( + config.userID, + `items/${key}` + ); + const updatedMetaDataJson = API.getJSONFromResponse(updatedMetaDataResponse); + Helpers.assertEquals(newHash, updatedMetaDataJson.data.md5); + Helpers.assertEquals(newMtime, updatedMetaDataJson.data.mtime); + Helpers.assertEquals(newFilename, updatedMetaDataJson.data.filename); + Helpers.assertEquals( + contentType, + updatedMetaDataJson.data.contentType + ); + Helpers.assertEquals(charset, updatedMetaDataJson.data.charset); + }); + + it('testClientV5ShouldReturn404GettingAuthorizationForMissingFile', async function () { + let params = { + md5: md5('qzpqBjLddCc6UhfX'), + mtime: 1477002989206, + filename: 'test.pdf', + filesize: 12345, + }; + let headers = { + 'Content-Type': 'application/x-www-form-urlencoded', + 'If-None-Match': '*', + }; + let response = await API.userPost( + config.userID, + 'items/UP24VFQR/file', + implodeParams(params), + headers + ); + Helpers.assert404(response); + }); + + it('testLastStorageSyncNoAuthorization', async function () { + API.useAPIKey(false); + let response = await API.userGet( + config.userID, + "laststoragesync", + { "Content-Type": "application/json" } + ); + Helpers.assert401(response); + }); + + it('testAddFileClientV5', async function () { + await API.userClear(config.userID); + + const file = "work/file"; + const fileContents = getRandomUnicodeString(); + const contentType = "text/html"; + const charset = "utf-8"; + fs.writeFileSync(file, fileContents); + const hash = crypto.createHash('md5').update(fileContents).digest("hex"); + const filename = "test_" + fileContents; + const mtime = fs.statSync(file).mtime * 1000; + const size = fs.statSync(file).size; + + // Get last storage sync + let response = await API.userGet( + config.userID, + "laststoragesync" + ); + Helpers.assert404(response); + + const json = await API.createAttachmentItem("imported_file", { + contentType: contentType, + charset: charset + }, false, this, 'jsonData'); + const key = json.key; + const originalVersion = json.version; + + // File shouldn't exist + response = await API.userGet( + config.userID, + `items/${key}/file` + ); + Helpers.assert404(response); + + // + // Get upload authorization + // + + // Require If-Match/If-None-Match + response = await API.userPost( + config.userID, + `items/${key}/file`, + implodeParams({ + md5: hash, + mtime: mtime, + filename: filename, + filesize: size + }), + { + "Content-Type": "application/x-www-form-urlencoded" + } + ); + Helpers.assert428(response, "If-Match/If-None-Match header not provided"); + + // Get authorization + response = await API.userPost( + config.userID, + `items/${key}/file`, + implodeParams({ + md5: hash, + mtime: mtime, + filename: filename, + filesize: size + }), + { + "Content-Type": "application/x-www-form-urlencoded", + "If-None-Match": "*" + } + ); + Helpers.assert200(response); + const uploadJSON = API.getJSONFromResponse(response); + + toDelete.push(hash); + + // + // Upload to S3 + // + let s3Headers = { + "Content-Type": uploadJSON.contentType + }; + response = await HTTP.post( + uploadJSON.url, + uploadJSON.prefix + fileContents + uploadJSON.suffix, + s3Headers + ); + Helpers.assert201(response); + + // + // Register upload + // + + // Require If-Match/If-None-Match + response = await API.userPost( + config.userID, + `items/${key}/file`, + `upload=${uploadJSON.uploadKey}`, + { + "Content-Type": "application/x-www-form-urlencoded" + } + ); + Helpers.assert428(response, "If-Match/If-None-Match header not provided"); + + // Invalid upload key + response = await API.userPost( + config.userID, + `items/${key}/file`, + "upload=invalidUploadKey", + { + "Content-Type": "application/x-www-form-urlencoded", + "If-None-Match": "*" + } + ); + Helpers.assert400(response); + + // If-Match shouldn't match unregistered file + response = await API.userPost( + config.userID, + `items/${key}/file`, + `upload=${uploadJSON.uploadKey}`, + { + "Content-Type": "application/x-www-form-urlencoded", + "If-Match": hash + } + ); + Helpers.assert412(response); + assert.notOk(response.headers['last-modified-version']); + + // Successful registration + response = await API.userPost( + config.userID, + `items/${key}/file`, + `upload=${uploadJSON.uploadKey}`, + { + "Content-Type": "application/x-www-form-urlencoded", + "If-None-Match": "*" + } + ); + Helpers.assert204(response); + const newVersion = response.headers['last-modified-version'][0]; + assert.isAbove(parseInt(newVersion), parseInt(originalVersion)); + + // Verify attachment item metadata + response = await API.userGet( + config.userID, + `items/${key}` + ); + const jsonResp = API.getJSONFromResponse(response).data; + assert.equal(hash, jsonResp.md5); + assert.equal(mtime, jsonResp.mtime); + assert.equal(filename, jsonResp.filename); + assert.equal(contentType, jsonResp.contentType); + assert.equal(charset, jsonResp.charset); + + response = await API.userGet( + config.userID, + "laststoragesync" + ); + Helpers.assert200(response); + Helpers.assertRegExp(/^[0-9]{10}$/, response.data); + + // + // Update file + // + + // Conflict for If-None-Match when file exists + response = await API.userPost( + config.userID, + `items/${key}/file`, + implodeParams({ + md5: hash, + mtime: mtime + 1000, + filename: filename, + filesize: size + }), + { + "Content-Type": "application/x-www-form-urlencoded", + "If-None-Match": "*" + } + ); + Helpers.assert412(response, "If-None-Match: * set but file exists"); + assert.notEqual(response.headers['last-modified-version'][0], null); + + // Conflict for If-Match when existing file differs + response = await API.userPost( + config.userID, + `items/${key}/file`, + implodeParams({ + md5: hash, + mtime: mtime + 1000, + filename: filename, + filesize: size + }), + { + "Content-Type": "application/x-www-form-urlencoded", + "If-Match": md5("invalid") + } + ); + Helpers.assert412(response, "ETag does not match current version of file"); + assert.notEqual(response.headers['last-modified-version'][0], null); + + // Error if wrong file size given for existing file + response = await API.userPost( + config.userID, + `items/${key}/file`, + implodeParams({ + md5: hash, + mtime: mtime + 1000, + filename: filename, + filesize: size - 1 + }), + { + "Content-Type": "application/x-www-form-urlencoded", + "If-Match": hash + } + ); + Helpers.assert400(response, "Specified file size incorrect for known file"); + + // File exists + response = await API.userPost( + config.userID, + `items/${key}/file`, + implodeParams({ + md5: hash, + mtime: mtime + 1000, + filename: filename, + filesize: size + }), + { + "Content-Type": "application/x-www-form-urlencoded", + "If-Match": hash + } + ); + Helpers.assert200(response); + let existsJSON = API.getJSONFromResponse(response); + assert.property(existsJSON, "exists"); + let version = response.headers['last-modified-version'][0]; + assert.isAbove(parseInt(version), parseInt(newVersion)); + + // File exists with different filename + response = await API.userPost( + config.userID, + `items/${key}/file`, + implodeParams({ + md5: hash, + mtime: mtime + 1000, + filename: filename + '等', + filesize: size + }), + { + "Content-Type": "application/x-www-form-urlencoded", + "If-Match": hash + } + ); + Helpers.assert200(response); + existsJSON = API.getJSONFromResponse(response); + assert.property(existsJSON, "exists"); + version = response.headers['last-modified-version'][0]; + assert.isAbove(parseInt(version), parseInt(newVersion)); + }); + + it('test_should_include_best_attachment_link_on_parent_for_imported_file', async function () { + let json = await API.createItem("book", false, this, 'json'); + assert.equal(0, json.meta.numChildren); + let parentKey = json.key; + + json = await API.createAttachmentItem("imported_file", [], parentKey, this, 'json'); + let attachmentKey = json.key; + let version = json.version; + + let filename = "test.pdf"; + let mtime = Date.now(); + let md5 = "e54589353710950c4b7ff70829a60036"; + let size = fs.statSync("data/test.pdf").size; + let fileContents = fs.readFileSync("data/test.pdf"); + + // Create attachment item + let response = await API.userPost( + config.userID, + "items", + JSON.stringify([ + { + key: attachmentKey, + contentType: "application/pdf", + } + ]), + { + "Content-Type": "application/json", + "If-Unmodified-Since-Version": version + } + ); + Helpers.assert200ForObject(response); + + // 'attachment' link shouldn't appear if no uploaded file + response = await API.userGet( + config.userID, + "items/" + parentKey + ); + json = await API.getJSONFromResponse(response); + assert.notProperty(json.links, 'attachment'); + + // Get upload authorization + response = await API.userPost( + config.userID, + "items/" + attachmentKey + "/file", + implodeParams({ + md5: md5, + mtime: mtime, + filename: filename, + filesize: size + }), + { + "Content-Type": "application/x-www-form-urlencoded", + "If-None-Match": "*" + } + ); + Helpers.assert200(response); + json = await API.getJSONFromResponse(response); + + // If file doesn't exist on S3, upload + if (!json.exists) { + response = await HTTP.post( + json.url, + json.prefix + fileContents + json.suffix, + { + "Content-Type": json.contentType + } + ); + Helpers.assert201(response); + + // Post-upload file registration + response = await API.userPost( + config.userID, + "items/" + attachmentKey + "/file", + "upload=" + json.uploadKey, + { + "Content-Type": "application/x-www-form-urlencoded", + "If-None-Match": "*" + } + ); + Helpers.assert204(response); + } + toDelete.push(md5); + + // 'attachment' link should now appear + response = await API.userGet( + config.userID, + "items/" + parentKey + ); + json = await API.getJSONFromResponse(response); + assert.property(json.links, 'attachment'); + assert.property(json.links.attachment, 'href'); + assert.equal('application/json', json.links.attachment.type); + assert.equal('application/pdf', json.links.attachment.attachmentType); + assert.equal(size, json.links.attachment.attachmentSize); + }); + + it('testAddFileClientV4', async function () { + await API.userClear(config.userID); + + const fileContentType = 'text/html'; + const fileCharset = 'utf-8'; + + const auth = { + username: config.username, + password: config.password, + }; + + // Get last storage sync + let response = await API.userGet( + config.userID, + 'laststoragesync?auth=1', + {}, + auth + ); + Helpers.assert404(response); + + const json = await API.createAttachmentItem( + 'imported_file', + [], + false, + this, + 'jsonData' + ); + let originalVersion = json.version; + json.contentType = fileContentType; + json.charset = fileCharset; + + response = await API.userPut( + config.userID, + `items/${json.key}`, + JSON.stringify(json), + { 'Content-Type': 'application/json' } + ); + Helpers.assert204(response); + originalVersion = response.headers['last-modified-version'][0]; + + // Get file info + response = await API.userGet( + config.userID, + `items/${json.key}/file?auth=1&iskey=1&version=1&info=1`, + {}, + auth + ); + Helpers.assert404(response); + + const file = 'work/file'; + const fileContents = getRandomUnicodeString(); + fs.writeFileSync(file, fileContents); + const hash = crypto.createHash('md5').update(fileContents).digest('hex'); + const filename = `test_${fileContents}`; + const mtime = parseInt(fs.statSync(file).mtimeMs); + const size = parseInt(fs.statSync(file).size); + + // Get upload authorization + response = await API.userPost( + config.userID, + `items/${json.key}/file?auth=1&iskey=1&version=1`, + implodeParams({ + md5: hash, + filename, + filesize: size, + mtime, + }), + { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + auth + ); + Helpers.assert200(response); + Helpers.assertContentType(response, 'application/xml'); + const xml = API.getXMLFromResponse(response); + const xmlParams = xml.getElementsByTagName('params')[0]; + const urlComponent = xml.getElementsByTagName('url')[0]; + const keyComponent = xml.getElementsByTagName('key')[0]; + toDelete.push(hash); + + const boundary = `---------------------------${Helpers.uniqueID()}`; + let postData = ''; + for (let child of xmlParams.children) { + const key = child.tagName; + const val = child.innerHTML; + postData += `--${boundary}\r\nContent-Disposition: form-data; name="${key}"\r\n\r\n${val}\r\n`; + } + postData += `--${boundary}\r\nContent-Disposition: form-data; name="file"\r\n\r\n${fileContents}\r\n`; + postData += `--${boundary}--`; + + // Upload to S3 + response = await HTTP.post(urlComponent.innerHTML, postData, { + 'Content-Type': `multipart/form-data; boundary=${boundary}`, + }); + Helpers.assert201(response); + + // + // Register upload + // + + // Invalid upload key + response = await API.userPost( + config.userID, + `items/${json.key}/file?auth=1&iskey=1&version=1`, + `update=invalidUploadKey&mtime=${mtime}`, + { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + auth + ); + Helpers.assert400(response); + + // No mtime + response = await API.userPost( + config.userID, + `items/${json.key}/file?auth=1&iskey=1&version=1`, + `update=${xml.key}`, + { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + auth + ); + Helpers.assert500(response); + + response = await API.userPost( + config.userID, + `items/${json.key}/file?auth=1&iskey=1&version=1`, + `update=${keyComponent.innerHTML}&mtime=${mtime}`, + { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + auth + ); + Helpers.assert204(response); + + // Verify attachment item metadata + response = await API.userGet(config.userID, `items/${json.key}`); + const { data } = API.getJSONFromResponse(response); + // Make sure attachment item version hasn't changed (or else the client + // will get a conflict when it tries to update the metadata) + assert.equal(originalVersion, data.version); + assert.equal(hash, data.md5); + assert.equal(filename, data.filename); + assert.equal(mtime, data.mtime); + + response = await API.userGet( + config.userID, + 'laststoragesync?auth=1', + {}, + { + username: config.username, + password: config.password, + } + ); + Helpers.assert200(response); + const newMtime = response.data; + assert.match(newMtime, /^[0-9]{10}$/); + + // File exists + response = await API.userPost( + config.userID, + `items/${json.key}/file?auth=1&iskey=1&version=1`, + implodeParams({ + md5: hash, + filename, + filesize: size, + mtime: newMtime + 1000, + }), + { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + auth + ); + Helpers.assert200(response); + Helpers.assertContentType(response, 'application/xml'); + assert.equal('', response.data); + + // File exists with different filename + response = await API.userPost( + config.userID, + `items/${json.key}/file?auth=1&iskey=1&version=1`, + implodeParams({ + md5: hash, + filename: `${filename}等`, // Unicode 1.1 character, to test signature generation + filesize: size, + mtime: newMtime + 1000, + }), + { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + auth + ); + Helpers.assert200(response); + Helpers.assertContentType(response, 'application/xml'); + assert.equal('', response.data); + + // Make sure attachment version still hasn't changed + response = await API.userGet(config.userID, `items/${json.key}`); + const { version } = API.getJSONFromResponse(response).data; + assert.equal(originalVersion, version); + }); }); diff --git a/tests/remote_js/test/shared.js b/tests/remote_js/test/shared.js index aa40a1f5..54772bee 100644 --- a/tests/remote_js/test/shared.js +++ b/tests/remote_js/test/shared.js @@ -4,6 +4,13 @@ const API3 = require('../api3.js'); module.exports = { + APISetCredentials: async () => { + const credentials = await API.login(); + config.apiKey = credentials.user1.apiKey; + config.user2APIKey = credentials.user2.apiKey; + await API3.useAPIKey(config.apiKey); + }, + API1Setup: async () => { const credentials = await API.login(); config.apiKey = credentials.user1.apiKey; From 8b7804b35b41fad1db197dbedd7b5766b7ae9d6a Mon Sep 17 00:00:00 2001 From: Bogdan Abaev Date: Thu, 25 May 2023 16:40:01 -0400 Subject: [PATCH 09/33] publication tests --- tests/remote_js/api3.js | 22 +- tests/remote_js/helpers.js | 14 +- tests/remote_js/helpers3.js | 79 +++ tests/remote_js/package.json | 5 +- tests/remote_js/test/3/fileTest.js | 201 +++--- tests/remote_js/test/3/publicationTest.js | 751 ++++++++++++++++++++++ 6 files changed, 946 insertions(+), 126 deletions(-) create mode 100644 tests/remote_js/test/3/publicationTest.js diff --git a/tests/remote_js/api3.js b/tests/remote_js/api3.js index bcffc602..8e9008e5 100644 --- a/tests/remote_js/api3.js +++ b/tests/remote_js/api3.js @@ -3,6 +3,7 @@ const { JSDOM } = require("jsdom"); const API2 = require("./api2.js"); const Helpers = require("./helpers"); const fs = require("fs"); +const wgxpath = require('wgxpath'); class API3 extends API2 { static schemaVersion; @@ -186,26 +187,25 @@ class API3 extends API2 { static async getContentFromAtomResponse(response, type = null) { let xml = this.getXMLFromResponse(response); - let content = Helpers.xpathEval(xml, '//atom:entry/atom:content'); + let content = Helpers.xpathEval(xml, '//atom:entry/atom:content', true); if (!content) { - console.log(xml.asXML()); + console.log(content.documentElement.outerHTML); throw new Error("Atom response does not contain "); } - - let subcontent = Helpers.xpathEval(content, '//zapi:subcontent'); + let subcontent = Helpers.xpathEval(xml, '//atom:entry/atom:content/zapi:subcontent', true, true); if (subcontent) { if (!type) { throw new Error('$type not provided for multi-content response'); } - let html; + let component; switch (type) { case 'json': - return JSON.parse(subcontent[0].xpath('//zapi:subcontent[@zapi:type="json"]')[0]); + component = subcontent.filter(node => node.getAttribute('zapi:type') == 'json')[0]; + return JSON.parse(component.innerHTML); case 'html': - html = Helpers.xpathEval(subcontent[0], '//zapi:subcontent[@zapi:type="html"]'); - html.registerXPathNamespace('html', 'http://www.w3.org/1999/xhtml'); - return html; + component = subcontent.filter(node => node.getAttribute('zapi:type') == 'html')[0]; + return component; default: throw new Error("Unknown data type '$type'"); @@ -281,9 +281,9 @@ class API3 extends API2 { }; static async parseLinkHeader(response) { - let header = response.getHeader('Link'); + let header = response.headers.link; let links = {}; - header.split(',').forEach(function (val) { + header.forEach(function (val) { let matches = val.match(/<([^>]+)>; rel="([^"]+)"/); links[matches[2]] = matches[1]; }); diff --git a/tests/remote_js/helpers.js b/tests/remote_js/helpers.js index f96d045e..f4f82c01 100644 --- a/tests/remote_js/helpers.js +++ b/tests/remote_js/helpers.js @@ -29,13 +29,14 @@ class Helpers { let ns = { atom: 'http://www.w3.org/2005/Atom', zapi: 'http://zotero.org/ns/api', - zxfer: 'http://zotero.org/ns/transfer' + zxfer: 'http://zotero.org/ns/transfer', + html: 'http://www.w3.org/1999/xhtml' }; return ns[prefix] || null; }; - static xpathEval = (document, xpath, returnHtml = false, multiple = false) => { - const xpathData = document.evaluate(xpath, document, this.namespaceResolver, 5, null); + static xpathEval = (document, xpath, returnHtml = false, multiple = false, element = null) => { + const xpathData = document.evaluate(xpath, (element || document), this.namespaceResolver, 5, null); if (!multiple && xpathData.snapshotLength != 1) { throw new Error("No single xpath value fetched"); } @@ -56,6 +57,9 @@ class Helpers { }; static assertRegExp(exp, val) { + if (typeof exp == "string") { + exp = new RegExp(exp); + } if (!exp.test(val)) { throw new Error(`${val} does not match regular expression`) } @@ -165,6 +169,10 @@ class Helpers { this.assertStatusCode(response, 404); }; + static assert405 = (response) => { + this.assertStatusCode(response, 405); + }; + static assert500 = (response) => { this.assertStatusCode(response, 500); }; diff --git a/tests/remote_js/helpers3.js b/tests/remote_js/helpers3.js index 24f645b5..498852f4 100644 --- a/tests/remote_js/helpers3.js +++ b/tests/remote_js/helpers3.js @@ -2,8 +2,12 @@ const { JSDOM } = require("jsdom"); const chai = require('chai'); const assert = chai.assert; const Helpers = require('./helpers'); +const crypto = require('crypto'); +const fs = require('fs'); class Helpers3 extends Helpers { + static notificationHeader = 'zotero-debug-notifications'; + static assertTotalResults(response, expectedCount) { const totalResults = parseInt(response.headers['total-results'][0]); assert.isNumber(totalResults); @@ -33,5 +37,80 @@ class Helpers3 extends Helpers { throw new Error(`Unknonw content type" ${contentType}`); } }; + + static assertNoResults(response) { + this.assertTotalResults(response, 0); + + const contentType = response.headers['content-type'][0]; + if (contentType == 'application/json') { + const json = JSON.parse(response.data); + assert.lengthOf(Object.keys(json), 0); + } + else if (contentType == 'application/atom+xml') { + const xml = new JSDOM(response.data, { url: "http://localhost/" }); + const entries = xml.window.document.getElementsByTagName('entry'); + assert.equal(entries.length, 0); + } + else { + throw new Error(`Unknown content type ${contentType}`); + } + } + + static md5 = (str) => { + return crypto.createHash('md5').update(str).digest('hex'); + }; + + static md5File = (fileName) => { + const data = fs.readFileSync(fileName); + return crypto.createHash('md5').update(data).digest('hex'); + }; + + static getRandomUnicodeString = function () { + const rand = crypto.randomInt(10, 100); + return "Âéìøü 这是一个测试。 " + Helpers.uniqueID(rand); + }; + + static implodeParams = (params, exclude = []) => { + let parts = []; + for (const [key, value] of Object.entries(params)) { + if (!exclude.includes(key)) { + parts.push(key + "=" + encodeURIComponent(value)); + } + } + return parts.join("&"); + }; + + static assertHasNotification(notification, response) { + let header = response.headers[this.notificationHeader][0]; + assert.ok(header); + + // Header contains a Base64-encode array of encoded JSON notifications + try { + let notifications = JSON.parse(Buffer.from(header, 'base64')).map(n => JSON.parse(n)); + assert.deepInclude(notifications, notification); + } + catch (e) { + console.log("\nHeader: " + Buffer.from(header, 'base64') + "\n"); + throw e; + } + } + + static assertNotificationCount(expected, response) { + let headerArr = response.headers[this.notificationHeader]; + let header = headerArr.length > 0 ? headerArr[0] : ""; + try { + if (expected === 0) { + assert.lengthOf(headerArr, 0); + } + else { + assert.ok(header); + this.assertCount(expected, JSON.parse(Buffer.from(header, 'base64'))); + } + } + catch (e) { + console.log("\nHeader: " + Buffer.from(header, 'base64') + "\n"); + throw e; + } + } } module.exports = Helpers3; diff --git a/tests/remote_js/package.json b/tests/remote_js/package.json index 08992196..abb9882f 100644 --- a/tests/remote_js/package.json +++ b/tests/remote_js/package.json @@ -1,7 +1,9 @@ { "dependencies": { + "@aws-sdk/client-s3": "^3.338.0", "axios": "^1.4.0", "jsdom": "^22.0.0", + "jszip": "^3.10.1", "node-fetch": "^2.6.7", "wgxpath": "^1.2.0" }, @@ -11,6 +13,7 @@ "mocha": "^10.2.0" }, "scripts": { - "test": "mocha \"test/**/*.js\"" + "test": "mocha \"test/**/*.js\"", + "test_file": "mocha \"test/3/publicationTest.js\"" } } diff --git a/tests/remote_js/test/3/fileTest.js b/tests/remote_js/test/3/fileTest.js index 70dccf50..a70daee8 100644 --- a/tests/remote_js/test/3/fileTest.js +++ b/tests/remote_js/test/3/fileTest.js @@ -3,7 +3,7 @@ const assert = chai.assert; const config = require("../../config.js"); const API = require('../../api3.js'); const Helpers = require('../../helpers3.js'); -const { API3Setup, API3WrapUp, APISetCredentials } = require("../shared.js"); +const { API3Setup, API3WrapUp } = require("../shared.js"); const { S3Client, DeleteObjectsCommand, PutObjectCommand } = require("@aws-sdk/client-s3"); const fs = require('fs'); const HTTP = require('../../httpHandler.js'); @@ -43,17 +43,9 @@ describe('FileTestTests', function () { }); beforeEach(async () => { - await APISetCredentials(); + API.useAPIKey(config.apiKey); }); - const md5 = (str) => { - return crypto.createHash('md5').update(str).digest('hex'); - }; - - const md5File = (fileName) => { - const data = fs.readFileSync(fileName); - return crypto.createHash('md5').update(data).digest('hex'); - }; const testNewEmptyImportedFileAttachmentItem = async () => { return API.createAttachmentItem("imported_file", [], false, this, 'key'); @@ -72,7 +64,7 @@ describe('FileTestTests', function () { assert.equal(filenameEncoded, location.substring(location.length - filenameEncoded.length)); const viewModeResponse = await HTTP.get(location); Helpers.assert200(viewModeResponse); - assert.equal(addFileData.md5, md5(viewModeResponse.data)); + assert.equal(addFileData.md5, Helpers.md5(viewModeResponse.data)); const userGetDownloadModeResponse = await API.userGet( config.userID, `items/${addFileData.key}/file` @@ -81,7 +73,7 @@ describe('FileTestTests', function () { const downloadModeLocation = userGetDownloadModeResponse.headers.location; const s3Response = await HTTP.get(downloadModeLocation); Helpers.assert200(s3Response); - assert.equal(addFileData.md5, md5(s3Response.data)); + assert.equal(addFileData.md5, Helpers.md5(s3Response.data)); return { key: addFileData.key, response: s3Response @@ -92,9 +84,9 @@ describe('FileTestTests', function () { let key = await API.createAttachmentItem("linked_file", [], false, this, 'key'); let file = "./work/file"; - let fileContents = getRandomUnicodeString(); + let fileContents = Helpers.getRandomUnicodeString(); fs.writeFileSync(file, fileContents); - let hash = md5File(file); + let hash = Helpers.md5File(file); let filename = "test_" + fileContents; let mtime = fs.statSync(file).mtimeMs; let size = fs.statSync(file).size; @@ -105,7 +97,7 @@ describe('FileTestTests', function () { let response = await API.userPost( config.userID, `items/${key}/file`, - implodeParams({ + Helpers.implodeParams({ md5: hash, filename: filename, filesize: size, @@ -129,9 +121,9 @@ describe('FileTestTests', function () { let originalVersion = json.version; let file = "./work/file"; - let fileContents = getRandomUnicodeString(); + let fileContents = Helpers.getRandomUnicodeString(); await fs.promises.writeFile(file, fileContents); - let hash = md5File(file); + let hash = Helpers.md5File(file); let filename = "test_" + fileContents; let mtime = parseInt((await fs.promises.stat(file)).mtimeMs); let size = parseInt((await fs.promises.stat(file)).size); @@ -141,7 +133,7 @@ describe('FileTestTests', function () { let response = await API.userPost( config.userID, `items/${attachmentKey}/file`, - implodeParams({ + Helpers.implodeParams({ md5: hash, filename: filename, filesize: size, @@ -161,7 +153,7 @@ describe('FileTestTests', function () { assert.ok(json); toDelete.push(hash); - let boundary = "---------------------------" + md5(Helpers.uniqueID()); + let boundary = "---------------------------" + Helpers.md5(Helpers.uniqueID()); let prefix = ""; for (let key in json.params) { prefix += "--" + boundary + "\r\nContent-Disposition: form-data; name=\"" + key + "\"\r\n\r\n" + json.params[key] + "\r\n"; @@ -202,16 +194,12 @@ describe('FileTestTests', function () { assert.notEqual(originalVersion, json.version); }); - const getRandomUnicodeString = function () { - const rand = crypto.randomInt(10, 100); - return "Âéìøü 这是一个测试。 " + Helpers.uniqueID(rand); - }; const generateZip = async (file, fileContents, archiveName) => { const zip = new JSZIP(); zip.file(file, fileContents); - zip.file("file.css", getRandomUnicodeString()); + zip.file("file.css", Helpers.getRandomUnicodeString()); const content = await zip.generateAsync({ type: "nodebuffer", @@ -223,15 +211,15 @@ describe('FileTestTests', function () { // Because when the file is sent, the buffer is stringified, we have to hash the stringified // fileContents and get the size of stringified buffer here, otherwise they wont match. return { - hash: md5(content.toString()), + hash: Helpers.md5(content.toString()), zipSize: Buffer.from(content.toString()).byteLength, fileContent: fs.readFileSync(archiveName) }; }; it('testExistingFileWithOldStyleFilename', async function () { - let fileContents = getRandomUnicodeString(); - let hash = md5(fileContents); + let fileContents = Helpers.getRandomUnicodeString(); + let hash = Helpers.md5(fileContents); let filename = 'test.txt'; let size = fileContents.length; @@ -245,7 +233,7 @@ describe('FileTestTests', function () { let response = await API.userPost( config.userID, `items/${key}/file`, - implodeParams({ + Helpers.implodeParams({ md5: hash, filename, filesize: size, @@ -301,7 +289,7 @@ describe('FileTestTests', function () { response = await API.userPost( config.userID, `items/${key}/file`, - implodeParams({ + Helpers.implodeParams({ md5: hash, filename, filesize: size, @@ -344,7 +332,7 @@ describe('FileTestTests', function () { response = await API.userPost( config.userID, `items/${key}/file`, - implodeParams({ + Helpers.implodeParams({ md5: hash, filename: "test2.txt", filesize: size, @@ -383,9 +371,9 @@ describe('FileTestTests', function () { let attachmentKey = json.key; let file = "./work/file"; - let fileContents = getRandomUnicodeString(); + let fileContents = Helpers.getRandomUnicodeString(); fs.writeFileSync(file, fileContents); - let hash = md5File(file); + let hash = Helpers.md5File(file); let filename = "test_" + fileContents; let mtime = fs.statSync(file).mtime * 1000; let size = fs.statSync(file).size; @@ -395,7 +383,7 @@ describe('FileTestTests', function () { let response = await API.userPost( config.userID, `items/${attachmentKey}/file`, - implodeParams({ + Helpers.implodeParams({ md5: hash, filename: filename, filesize: size, @@ -487,8 +475,8 @@ describe('FileTestTests', function () { it('testAddFileAuthorizationErrors', async function () { const parentKey = await testNewEmptyImportedFileAttachmentItem(); - const fileContents = getRandomUnicodeString(); - const hash = md5(fileContents); + const fileContents = Helpers.getRandomUnicodeString(); + const hash = Helpers.md5(fileContents); const mtime = Date.now(); const size = fileContents.length; const filename = `test_${fileContents}`; @@ -509,7 +497,7 @@ describe('FileTestTests', function () { const response = await API.userPost( config.userID, `items/${parentKey}/file`, - implodeParams(fileParams, [exclude]), + Helpers.implodeParams(fileParams, [exclude]), { "Content-Type": "application/x-www-form-urlencoded", "If-None-Match": "*" @@ -522,7 +510,7 @@ describe('FileTestTests', function () { const _ = await API.userPost( config.userID, `items/${parentKey}/file`, - implodeParams(fileParams2), + Helpers.implodeParams(fileParams2), { "Content-Type": "application/x-www-form-urlencoded", "If-None-Match": "*" @@ -535,10 +523,10 @@ describe('FileTestTests', function () { const response3 = await API.userPost( config.userID, `items/${parentKey}/file`, - implodeParams(fileParams), + Helpers.implodeParams(fileParams), { "Content-Type": "application/x-www-form-urlencoded", - "If-Match": md5("invalidETag") + "If-Match": Helpers.md5("invalidETag") }); Helpers.assert412(response3); @@ -546,7 +534,7 @@ describe('FileTestTests', function () { const response4 = await API.userPost( config.userID, `items/${parentKey}/file`, - implodeParams(fileParams), + Helpers.implodeParams(fileParams), { "Content-Type": "application/x-www-form-urlencoded" }); @@ -556,7 +544,7 @@ describe('FileTestTests', function () { const response5 = await API.userPost( config.userID, `items/${parentKey}/file}`, - implodeParams(fileParams), + Helpers.implodeParams(fileParams), { "Content-Type": "application/x-www-form-urlencoded", "If-None-Match": "invalidETag" @@ -564,15 +552,6 @@ describe('FileTestTests', function () { Helpers.assert400(response5); }); - const implodeParams = (params, exclude = []) => { - let parts = []; - for (const [key, value] of Object.entries(params)) { - if (!exclude.includes(key)) { - parts.push(key + "=" + encodeURIComponent(value)); - } - } - return parts.join("&"); - }; it('testAddFilePartial', async function () { const getFileData = await testGetFile(); @@ -600,8 +579,8 @@ describe('FileTestTests', function () { }; for (let [algo, cmd] of Object.entries(algorithms)) { - fs.writeFileSync(newFilename, getRandomUnicodeString() + Helpers.uniqueID()); - const newHash = md5File(newFilename); + fs.writeFileSync(newFilename, Helpers.getRandomUnicodeString() + Helpers.uniqueID()); + const newHash = Helpers.md5File(newFilename); const fileParams = { md5: newHash, filename: `test_${fileContents}`, @@ -614,10 +593,10 @@ describe('FileTestTests', function () { const postResponse = await API.userPost( config.userID, `items/${getFileData.key}/file`, - implodeParams(fileParams), + Helpers.implodeParams(fileParams), { "Content-Type": "application/x-www-form-urlencoded", - "If-Match": md5File(oldFilename), + "If-Match": Helpers.md5File(oldFilename), } ); Helpers.assert200(postResponse); @@ -641,7 +620,7 @@ describe('FileTestTests', function () { `items/${getFileData.key}/file?algorithm=${algo}&upload=${json.uploadKey}`, patch, { - "If-Match": md5File(oldFilename), + "If-Match": Helpers.md5File(oldFilename), } ); Helpers.assert204(response); @@ -669,7 +648,7 @@ describe('FileTestTests', function () { const getFileResponse = await HTTP.get(location); Helpers.assert200(getFileResponse); - Helpers.assertEquals(fileParams.md5, md5(getFileResponse.data)); + Helpers.assertEquals(fileParams.md5, Helpers.md5(getFileResponse.data)); Helpers.assertEquals( `${fileParams.contentType}${fileParams.contentType && fileParams.charset ? `; charset=${fileParams.charset}` : "" }`, @@ -689,7 +668,7 @@ describe('FileTestTests', function () { let response = await API.userPost( config.userID, `items/${key}/file`, - implodeParams({ + Helpers.implodeParams({ md5: json.md5, filename: json.filename, filesize: size, @@ -711,7 +690,7 @@ describe('FileTestTests', function () { response = await API.userPost( config.userID, `items/${key}/file`, - implodeParams({ + Helpers.implodeParams({ md5: json.md5, filename: json.filename + "等", // Unicode 1.1 character, to test signature generation filesize: size, @@ -792,14 +771,14 @@ describe('FileTestTests', function () { Helpers.assert404(response3); - const { hash, zipSize, fileContent } = await generateZip(fileFilename, getRandomUnicodeString(), `work/${key}.zip`); + const { hash, zipSize, fileContent } = await generateZip(fileFilename, Helpers.getRandomUnicodeString(), `work/${key}.zip`); const filename = `${key}.zip`; const response4 = await API.userPost( config.userID, `items/${json2.key}/file?auth=1&iskey=1&version=1`, - implodeParams({ + Helpers.implodeParams({ md5: hash, filename: filename, filesize: zipSize, @@ -869,7 +848,7 @@ describe('FileTestTests', function () { const response9 = await API.userPost( config.userID, `items/${json2.key}/file?auth=1&iskey=1&version=1`, - implodeParams({ + Helpers.implodeParams({ md5: hash, filename, filesize: zipSize, @@ -892,9 +871,9 @@ describe('FileTestTests', function () { it('test_should_not_allow_anonymous_access_to_file_in_public_closed_group_with_library_reading_for_all', async function () { let file = "work/file"; - let fileContents = getRandomUnicodeString(); + let fileContents = Helpers.getRandomUnicodeString(); fs.writeFileSync(file, fileContents); - let hash = md5File(file); + let hash = Helpers.md5File(file); let filename = `test_${fileContents}`; let mtime = parseInt(fs.statSync(file).mtimeMs); let size = fs.statSync(file).size; @@ -924,7 +903,7 @@ describe('FileTestTests', function () { let response = await API.groupPost( groupID, `items/${attachmentKey}/file`, - implodeParams({ + Helpers.implodeParams({ md5: hash, mtime, filename, @@ -995,14 +974,14 @@ describe('FileTestTests', function () { let filename = "test.html"; let mtime = Date.now(); - let size = fs.statSync("data/test.html.zip").size; + //let size = fs.statSync("data/test.html.zip").size; let md5 = "af625b88d74e98e33b78f6cc0ad93ed0"; - let zipMD5 = "f56e3080d7abf39019a9445d7aab6b24"; + //let zipMD5 = "f56e3080d7abf39019a9445d7aab6b24"; let fileContents = fs.readFileSync("data/test.html.zip"); - //let zipMD5 = md5File("data/test.html.zip"); + let zipMD5 = Helpers.md5File("data/test.html.zip"); let zipFilename = attachmentKey + ".zip"; - //let size = Buffer.from(fileContents.toString()).byteLength; + let size = Buffer.from(fileContents.toString()).byteLength; // Create attachment item let response = await API.userPost( @@ -1035,7 +1014,7 @@ describe('FileTestTests', function () { response = await API.userPost( config.userID, "items/" + attachmentKey + "/file", - implodeParams({ + Helpers.implodeParams({ md5: md5, mtime: mtime, filename: filename, @@ -1095,11 +1074,11 @@ describe('FileTestTests', function () { await API.userClear(config.userID); const file = 'work/file'; - const fileContents = getRandomUnicodeString(); + const fileContents = Helpers.getRandomUnicodeString(); const contentType = 'text/plain'; const charset = 'utf-8'; fs.writeFileSync(file, fileContents); - const hash = md5File(file); + const hash = Helpers.md5File(file); const filename = `test_${fileContents}`; const mtime = fs.statSync(file).mtimeMs; let size = 0; @@ -1115,7 +1094,7 @@ describe('FileTestTests', function () { const response = await API.userPost( config.userID, `items/${key}/file`, - implodeParams({ + Helpers.implodeParams({ md5: hash, mtime, filename, @@ -1146,13 +1125,13 @@ describe('FileTestTests', function () { it('test_updating_attachment_hash_should_clear_associated_storage_file', async function () { let file = "work/file"; - let fileContents = getRandomUnicodeString(); + let fileContents = Helpers.getRandomUnicodeString(); let contentType = "text/html"; let charset = "utf-8"; fs.writeFileSync(file, fileContents); - let hash = md5File(file); + let hash = Helpers.md5File(file); let filename = "test_" + fileContents; let mtime = parseInt(fs.statSync(file).mtime * 1000); let size = parseInt(fs.statSync(file).size); @@ -1167,7 +1146,7 @@ describe('FileTestTests', function () { let response = await API.userPost( config.userID, "items/" + itemKey + "/file", - implodeParams({ + Helpers.implodeParams({ md5: hash, mtime: mtime, filename: filename, @@ -1207,7 +1186,7 @@ describe('FileTestTests', function () { filename = "test.pdf"; mtime = Date.now(); - hash = md5(Helpers.uniqueID()); + hash = Helpers.md5(Helpers.uniqueID()); response = await API.userPatch( config.userID, @@ -1237,10 +1216,10 @@ describe('FileTestTests', function () { const noteKey = await API.createNoteItem("", null, this, 'key'); const file = "work/file"; - const fileContents = getRandomUnicodeString(); + const fileContents = Helpers.getRandomUnicodeString(); const contentType = "image/png"; fs.writeFileSync(file, fileContents); - const hash = md5(fileContents); + const hash = Helpers.md5(fileContents); const filename = "image.png"; const mtime = fs.statSync(file).mtime * 1000; const size = fs.statSync(file).size; @@ -1257,7 +1236,7 @@ describe('FileTestTests', function () { let response = await API.userPost( config.userID, `items/${key}/file`, - implodeParams({ + Helpers.implodeParams({ md5: hash, mtime: mtime, filename: filename, @@ -1319,12 +1298,12 @@ describe('FileTestTests', function () { it('testAddFileClientV5Zip', async function () { await API.userClear(config.userID); - const fileContents = getRandomUnicodeString(); + const fileContents = Helpers.getRandomUnicodeString(); const contentType = "text/html"; const charset = "utf-8"; const filename = "file.html"; const mtime = Date.now() / 1000 | 0; - const hash = md5(fileContents); + const hash = Helpers.md5(fileContents); // Get last storage sync let response = await API.userGet(config.userID, "laststoragesync"); @@ -1339,7 +1318,7 @@ describe('FileTestTests', function () { }, key, this, 'jsonData'); key = json.key; - const zipData = await generateZip(filename, getRandomUnicodeString(), `work/${key}.zip`); + const zipData = await generateZip(filename, Helpers.getRandomUnicodeString(), `work/${key}.zip`); const zipFilename = `${key}.zip`; @@ -1347,7 +1326,7 @@ describe('FileTestTests', function () { // // Get upload authorization // - response = await API.userPost(config.userID, `items/${key}/file`, implodeParams({ + response = await API.userPost(config.userID, `items/${key}/file`, Helpers.implodeParams({ md5: hash, mtime: mtime, filename: filename, @@ -1420,7 +1399,7 @@ describe('FileTestTests', function () { // File exists response = await API.userPost(config.userID, `items/${key}/file`, - implodeParams({ + Helpers.implodeParams({ md5: hash, mtime: mtime + 1000, filename: filename, @@ -1442,12 +1421,12 @@ describe('FileTestTests', function () { }); it('test_updating_compressed_attachment_hash_should_clear_associated_storage_file', async function () { - let fileContents = getRandomUnicodeString(); + let fileContents = Helpers.getRandomUnicodeString(); let contentType = "text/html"; let charset = "utf-8"; let filename = "file.html"; let mtime = Math.floor(Date.now() / 1000); - let hash = md5(fileContents); + let hash = Helpers.md5(fileContents); let json = await API.createAttachmentItem("imported_file", { contentType: contentType, @@ -1466,7 +1445,7 @@ describe('FileTestTests', function () { let response = await API.userPost( config.userID, "items/" + itemKey + "/file", - implodeParams({ + Helpers.implodeParams({ md5: hash, mtime: mtime, filename: filename, @@ -1511,9 +1490,9 @@ describe('FileTestTests', function () { Helpers.assert204(response); let newVersion = response.headers['last-modified-version']; - hash = md5(Helpers.uniqueID()); + hash = Helpers.md5(Helpers.uniqueID()); mtime = Date.now(); - zipHash = md5(Helpers.uniqueID()); + zipHash = Helpers.md5(Helpers.uniqueID()); zipSize += 1; response = await API.userPatch( config.userID, @@ -1543,11 +1522,11 @@ describe('FileTestTests', function () { await API.userClear(config.userID); const file = "work/file"; - const fileContents = getRandomUnicodeString(); + const fileContents = Helpers.getRandomUnicodeString(); const contentType = "text/html"; const charset = "utf-8"; fs.writeFileSync(file, fileContents); - const hash = md5File(file); + const hash = Helpers.md5File(file); const filename = "test_" + fileContents; const mtime = fs.statSync(file).mtime * 1000; const size = fs.statSync(file).size; @@ -1562,7 +1541,7 @@ describe('FileTestTests', function () { const response = await API.userPost( config.userID, `items/${key}/file`, - implodeParams({ + Helpers.implodeParams({ md5: hash, mtime: mtime, filename: filename, @@ -1615,9 +1594,9 @@ describe('FileTestTests', function () { Helpers.assertEquals(charset, metaDataJson.data.charset); // update file const newFileContents - = getRandomUnicodeString() + getRandomUnicodeString(); + = Helpers.getRandomUnicodeString() + Helpers.getRandomUnicodeString(); fs.writeFileSync(file, newFileContents); - const newHash = md5File(file); + const newHash = Helpers.md5File(file); const newFilename = "test_" + newFileContents; const newMtime = fs.statSync(file).mtime * 1000; const newSize = fs.statSync(file).size; @@ -1627,7 +1606,7 @@ describe('FileTestTests', function () { = await API.userPost( config.userID, `items/${key}/file`, - implodeParams({ + Helpers.implodeParams({ md5: newHash, mtime: newMtime, filename: newFilename, @@ -1682,7 +1661,7 @@ describe('FileTestTests', function () { it('testClientV5ShouldReturn404GettingAuthorizationForMissingFile', async function () { let params = { - md5: md5('qzpqBjLddCc6UhfX'), + md5: Helpers.md5('qzpqBjLddCc6UhfX'), mtime: 1477002989206, filename: 'test.pdf', filesize: 12345, @@ -1694,7 +1673,7 @@ describe('FileTestTests', function () { let response = await API.userPost( config.userID, 'items/UP24VFQR/file', - implodeParams(params), + Helpers.implodeParams(params), headers ); Helpers.assert404(response); @@ -1714,7 +1693,7 @@ describe('FileTestTests', function () { await API.userClear(config.userID); const file = "work/file"; - const fileContents = getRandomUnicodeString(); + const fileContents = Helpers.getRandomUnicodeString(); const contentType = "text/html"; const charset = "utf-8"; fs.writeFileSync(file, fileContents); @@ -1752,7 +1731,7 @@ describe('FileTestTests', function () { response = await API.userPost( config.userID, `items/${key}/file`, - implodeParams({ + Helpers.implodeParams({ md5: hash, mtime: mtime, filename: filename, @@ -1768,7 +1747,7 @@ describe('FileTestTests', function () { response = await API.userPost( config.userID, `items/${key}/file`, - implodeParams({ + Helpers.implodeParams({ md5: hash, mtime: mtime, filename: filename, @@ -1878,7 +1857,7 @@ describe('FileTestTests', function () { response = await API.userPost( config.userID, `items/${key}/file`, - implodeParams({ + Helpers.implodeParams({ md5: hash, mtime: mtime + 1000, filename: filename, @@ -1896,7 +1875,7 @@ describe('FileTestTests', function () { response = await API.userPost( config.userID, `items/${key}/file`, - implodeParams({ + Helpers.implodeParams({ md5: hash, mtime: mtime + 1000, filename: filename, @@ -1904,7 +1883,7 @@ describe('FileTestTests', function () { }), { "Content-Type": "application/x-www-form-urlencoded", - "If-Match": md5("invalid") + "If-Match": Helpers.md5("invalid") } ); Helpers.assert412(response, "ETag does not match current version of file"); @@ -1914,7 +1893,7 @@ describe('FileTestTests', function () { response = await API.userPost( config.userID, `items/${key}/file`, - implodeParams({ + Helpers.implodeParams({ md5: hash, mtime: mtime + 1000, filename: filename, @@ -1931,7 +1910,7 @@ describe('FileTestTests', function () { response = await API.userPost( config.userID, `items/${key}/file`, - implodeParams({ + Helpers.implodeParams({ md5: hash, mtime: mtime + 1000, filename: filename, @@ -1952,7 +1931,7 @@ describe('FileTestTests', function () { response = await API.userPost( config.userID, `items/${key}/file`, - implodeParams({ + Helpers.implodeParams({ md5: hash, mtime: mtime + 1000, filename: filename + '等', @@ -2014,7 +1993,7 @@ describe('FileTestTests', function () { response = await API.userPost( config.userID, "items/" + attachmentKey + "/file", - implodeParams({ + Helpers.implodeParams({ md5: md5, mtime: mtime, filename: filename, @@ -2116,7 +2095,7 @@ describe('FileTestTests', function () { Helpers.assert404(response); const file = 'work/file'; - const fileContents = getRandomUnicodeString(); + const fileContents = Helpers.getRandomUnicodeString(); fs.writeFileSync(file, fileContents); const hash = crypto.createHash('md5').update(fileContents).digest('hex'); const filename = `test_${fileContents}`; @@ -2127,7 +2106,7 @@ describe('FileTestTests', function () { response = await API.userPost( config.userID, `items/${json.key}/file?auth=1&iskey=1&version=1`, - implodeParams({ + Helpers.implodeParams({ md5: hash, filename, filesize: size, @@ -2228,7 +2207,7 @@ describe('FileTestTests', function () { response = await API.userPost( config.userID, `items/${json.key}/file?auth=1&iskey=1&version=1`, - implodeParams({ + Helpers.implodeParams({ md5: hash, filename, filesize: size, @@ -2247,7 +2226,7 @@ describe('FileTestTests', function () { response = await API.userPost( config.userID, `items/${json.key}/file?auth=1&iskey=1&version=1`, - implodeParams({ + Helpers.implodeParams({ md5: hash, filename: `${filename}等`, // Unicode 1.1 character, to test signature generation filesize: size, diff --git a/tests/remote_js/test/3/publicationTest.js b/tests/remote_js/test/3/publicationTest.js new file mode 100644 index 00000000..d0c7e88d --- /dev/null +++ b/tests/remote_js/test/3/publicationTest.js @@ -0,0 +1,751 @@ +const chai = require('chai'); +const assert = chai.assert; +const config = require("../../config.js"); +const API = require('../../api3.js'); +const Helpers = require('../../helpers3.js'); +const { API3Setup, API3WrapUp } = require("../shared.js"); +const { S3Client, DeleteObjectsCommand } = require("@aws-sdk/client-s3"); +const HTTP = require('../../httpHandler.js'); +const fs = require('fs'); +const { resetGroups } = require("../../groupsSetup.js"); +const { JSDOM } = require("jsdom"); + + +describe('PublicationTests', function () { + this.timeout(0); + let toDelete = []; + const s3Client = new S3Client({ region: "us-east-1" }); + + before(async function () { + await API3Setup(); + await resetGroups(); + try { + fs.mkdirSync("./work"); + } + catch {} + }); + + after(async function () { + await API3WrapUp(); + if (toDelete.length > 0) { + const commandInput = { + Bucket: config.s3Bucket, + Delete: { + Objects: toDelete.map((x) => { + return { Key: x }; + }) + } + }; + const command = new DeleteObjectsCommand(commandInput); + await s3Client.send(command); + } + }); + beforeEach(async function () { + await API.userClear(config.userID); + API.useAPIKey(""); + }); + + + it('test_should_return_404_for_collections_request', async function () { + let response = await API.get(`users/${config.userID}/publications/collections`, { "Content-Type": "application/json" }); + Helpers.assert404(response); + }); + + it('test_should_show_publications_urls_in_json_response_for_multi_object_request', async function () { + await API.useAPIKey(config.apiKey); + const itemKey1 = await API.createItem("book", { inPublications: true }, this, 'key'); + const itemKey2 = await API.createItem("book", { inPublications: true }, this, 'key'); + + const response = await API.get("users/" + config.userID + "/publications/items?limit=1", { "Content-Type": "application/json" }); + const json = API.getJSONFromResponse(response); + + const links = await API.parseLinkHeader(response); + + Helpers.assertRegExp( + `https?://[^/]+/users/${config.userID}/publications/items/(${itemKey1}|${itemKey2})`, + json[0].links.self.href + ); + + Helpers.assertRegExp( + `https?://[^/]+/users/${config.userID}/publications/items`, + links.next + ); + + // TODO: rel="alternate" (what should this be?) + }); + + it('test_should_trigger_notification_on_publications_topic', async function () { + API.useAPIKey(config.apiKey); + const response = await API.createItem('book', { inPublications: true }, this, 'response'); + const version = API.getJSONFromResponse(response).successful[0].version; + Helpers.assertNotificationCount(2, response); + Helpers.assertHasNotification({ + event: 'topicUpdated', + topic: `/users/${config.userID}`, + version: version + }, response); + Helpers.assertHasNotification({ + event: 'topicUpdated', + topic: `/users/${config.userID}/publications` + }, response); + }); + + it('test_should_show_publications_urls_in_atom_response_for_single_object_request', async function () { + API.useAPIKey(config.apiKey); + const itemKey = await API.createItem('book', { inPublications: true }, this, 'key'); + const response = await API.get(`users/${config.userID}/publications/items/${itemKey}?format=atom`); + const xml = await API.getXMLFromResponse(response); + + // id + Helpers.assertRegExp( + `http://[^/]+/users/${config.userID}/items/${itemKey}`, + Helpers.xpathEval(xml, '//atom:id') + ); + + // rel="self" + const selfRel = Helpers.xpathEval(xml, '//atom:link[@rel="self"]', true, false); + Helpers.assertRegExp( + `https?://[^/]+/users/${config.userID}/publications/items/${itemKey}\\?format=atom`, + selfRel.getAttribute("href") + ); + + // TODO: rel="alternate" + }); + + // Disabled + it('test_should_return_304_for_request_with_etag', async function () { + this.skip(); + let response = await API.get(`users/${config.userID}/publications/items`); + Helpers.assert200(response); + let etag = response.headers.etag[0]; + Helpers.assertNotNull(etag); + + response = await API.get( + `users/${config.userID}/publications/items`, + { + "If-None-Match": etag + } + ); + Helpers.assert304(response); + assert.equal(etag, response.headers.etag[0]); + }); + + it('test_should_show_publications_urls_in_json_response_for_single_object_request', async function () { + await API.useAPIKey(config.apiKey); + const itemKey = await API.createItem("book", { inPublications: true }, this, 'key'); + + const response = await API.get(`users/${config.userID}/publications/items/${itemKey}`); + const json = await API.getJSONFromResponse(response); + + // rel="self" + Helpers.assertRegExp( + `https?://[^/]+/users/${config.userID}/publications/items/${itemKey}`, + json.links.self.href + ); + }); + + it('test_should_return_no_atom_results_for_empty_publications_list', async function () { + let response = await API.get(`users/${config.userID}/publications/items?format=atom`); + Helpers.assert200(response); + Helpers.assertNoResults(response); + assert.isNumber(parseInt(response.headers['last-modified-version'][0])); + }); + + it('test_shouldnt_include_hidden_child_items_in_numChildren', async function () { + API.useAPIKey(config.apiKey); + const parentItemKey = await API.createItem('book', { inPublications: true }, this, 'key'); + + const json1 = await API.getItemTemplate('attachment&linkMode=imported_file'); + json1.title = 'A'; + json1.parentItem = parentItemKey; + json1.inPublications = true; + + const json2 = await API.getItemTemplate('note'); + json2.note = 'B'; + json2.parentItem = parentItemKey; + json2.inPublications = true; + + const json3 = await API.getItemTemplate('attachment&linkMode=imported_file'); + json3.title = 'C'; + json3.parentItem = parentItemKey; + + const json4 = await API.getItemTemplate('note'); + json4.note = 'D'; + json4.parentItem = parentItemKey; + json4.inPublications = true; + json4.deleted = true; + + const json5 = await API.getItemTemplate('attachment&linkMode=imported_file'); + json5.title = 'E'; + json5.parentItem = parentItemKey; + json5.deleted = true; + + let response = await API.userPost(config.userID, 'items', JSON.stringify([json1, json2, json3, json4, json5])); + Helpers.assert200(response); + + API.useAPIKey(''); + + response = await API.userGet(config.userID, `publications/items/${parentItemKey}`); + Helpers.assert200(response); + const json = API.getJSONFromResponse(response); + assert.equal(2, json.meta.numChildren); + + response = await API.userGet(config.userID, `publications/items/${parentItemKey}/children`); + + response = await API.userGet(config.userID, `publications/items/${parentItemKey}?format=atom`); + Helpers.assert200(response); + const xml = API.getXMLFromResponse(response); + assert.equal(2, parseInt(Helpers.xpathEval(xml, '/atom:entry/zapi:numChildren'))); + }); + + it('testLinkedFileAttachment', async function () { + let json = await API.getItemTemplate("book"); + json.inPublications = true; + API.useAPIKey(config.apiKey); + let response = await API.userPost( + config.userID, + "items", + JSON.stringify([json]) + ); + Helpers.assert200(response); + json = API.getJSONFromResponse(response); + let itemKey = json.successful[0].key; + + json = await API.getItemTemplate("attachment&linkMode=linked_file"); + json.inPublications = true; + json.parentItem = itemKey; + await API.useAPIKey(config.apiKey); + response = await API.userPost( + config.userID, + "items", + JSON.stringify([json]), + { "Content-Type": "application/json" } + ); + Helpers.assert400ForObject(response, { message: "Linked-file attachments cannot be added to My Publications" }); + }); + + it('testTopLevelAttachmentAndNote', async function () { + let msg = "Top-level notes and attachments cannot be added to My Publications"; + + // Attachment + API.useAPIKey(config.apiKey); + let json = await API.getItemTemplate("attachment&linkMode=imported_file"); + json.inPublications = true; + let response = await API.userPost( + config.userID, + "items", + JSON.stringify([json]), + { "Content-Type": "application/json" } + ); + Helpers.assert400ForObject(response, msg, 0); + + // Note + API.useAPIKey(config.apiKey); + json = await API.getItemTemplate("note"); + json.inPublications = true; + response = await API.userPost( + config.userID, + "items", + JSON.stringify([json]), + { "Content-Type": "application/json" } + ); + Helpers.assert400ForObject(response, msg, 0); + }); + + it('test_shouldnt_allow_inPublications_in_group_library', async function () { + API.useAPIKey(config.apiKey); + let json = await API.getItemTemplate("book"); + json.inPublications = true; + const response = await API.groupPost(config.ownedPrivateGroupID, "items", JSON.stringify([json]), { "Content-Type": "application/json" }); + Helpers.assert400ForObject(response, { message: "Group items cannot be added to My Publications" }); + }); + + it('test_should_show_item_for_anonymous_single_object_request', async function () { + // Create item + API.useAPIKey(config.apiKey); + const itemKey = await API.createItem('book', { inPublications: true }, this, 'key'); + + // Read item anonymously + API.useAPIKey(''); + + // JSON + let response = await API.userGet(config.userID, `publications/items/${itemKey}`); + Helpers.assert200(response); + let json = await API.getJSONFromResponse(response); + assert.equal(config.displayName, json.library.name); + assert.equal('user', json.library.type); + + // Atom + response = await API.userGet(config.userID, `publications/items/${itemKey}?format=atom`); + Helpers.assert200(response); + const xml = API.getXMLFromResponse(response); + const author = xml.getElementsByTagName("author")[0]; + const name = author.getElementsByTagName("name")[0]; + assert.equal(config.displayName, name.innerHTML); + }); + + it('test_should_remove_inPublications_on_POST_with_false', async function () { + API.useAPIKey(config.apiKey); + let json = await API.getItemTemplate('book'); + json.inPublications = true; + let response = await API.userPost(config.userID, 'items', JSON.stringify([json])); + Helpers.assert200(response); + let key = API.getJSONFromResponse(response).successful[0].key; + let version = response.headers['last-modified-version'][0]; + json = { + key, + version, + title: 'Test', + inPublications: false, + }; + response = await API.userPost(config.userID, 'items', JSON.stringify([json]), { + 'Content-Type': 'application/json', + }); + Helpers.assert200ForObject(response); + json = API.getJSONFromResponse(response); + assert.notProperty(json.successful[0].data, 'inPublications'); + }); + + it('test_should_return_404_for_anonymous_request_for_item_not_in_publications', async function () { + API.useAPIKey(config.apiKey); + const key = await API.createItem("book", [], this, 'key'); + API.useAPIKey(); + const response = await API.get("users/" + config.userID + "/publications/items/" + key, { "Content-Type": "application/json" }); + Helpers.assert404(response); + }); + + it('test_should_return_no_results_for_empty_publications_list_with_key', async function () { + API.useAPIKey(config.apiKey); + let response = await API.get(`users/${config.userID}/publications/items`); + Helpers.assert200(response); + Helpers.assertNoResults(response); + assert.isNumber(parseInt(response.headers['last-modified-version'][0])); + }); + + it('test_should_show_item_for_anonymous_multi_object_request', async function () { + // Create item + API.useAPIKey(config.apiKey); + let itemKey = await API.createItem('book', { inPublications: true }, this, 'key'); + + // Read item anonymously + API.useAPIKey(''); + + // JSON + let response = await API.userGet(config.userID, 'publications/items'); + Helpers.assert200(response); + let json = await API.getJSONFromResponse(response); + assert.include(json.map(item => item.key), itemKey); + + // Atom + response = await API.userGet(config.userID, 'publications/items?format=atom'); + Helpers.assert200(response); + let xml = await API.getXMLFromResponse(response); + let xpath = Helpers.xpathEval(xml, '//atom:entry/zapi:key'); + assert.include(xpath, itemKey); + }); + + it('test_should_show_publications_urls_in_atom_response_for_multi_object_request', async function () { + let response = await API.get(`users/${config.userID}/publications/items?format=atom`); + let xml = await API.getXMLFromResponse(response); + + // id + let id = Helpers.xpathEval(xml, '//atom:id'); + Helpers.assertRegExp(`http://[^/]+/users/${config.userID}/publications/items`, id); + + let link = Helpers.xpathEval(xml, '//atom:link[@rel="self"]', true, false); + let href = link.getAttribute('href'); + Helpers.assertRegExp(`https?://[^/]+/users/${config.userID}/publications/items\\?format=atom`, href); + + // rel="first" + link = Helpers.xpathEval(xml, '//atom:link[@rel="first"]', true, false); + href = link.getAttribute('href'); + Helpers.assertRegExp(`https?://[^/]+/users/${config.userID}/publications/items\\?format=atom`, href); + + // TODO: rel="alternate" (what should this be?) + }); + + it('test_should_return_200_for_deleted_request', async function () { + let response = await API.get(`users/${config.userID}/publications/deleted?since=0`, { 'Content-Type': 'application/json' }); + Helpers.assert200(response); + }); + + // Disabled until after integrated My Publications upgrade + it('test_should_return_404_for_settings_request', async function () { + this.skip(); + let response = await API.get(`users/${config.userID}/publications/settings`); + Helpers.assert404(response); + }); + + it('test_should_return_404_for_authenticated_request_for_item_not_in_publications', async function () { + API.useAPIKey(config.apiKey); + let key = await API.createItem("book", [], this, 'key'); + let response = await API.get("users/" + config.userID + "/publications/items/" + key, { "Content-Type": "application/json" }); + Helpers.assert404(response); + }); + + it('test_shouldnt_show_trashed_item', async function () { + API.useAPIKey(config.apiKey); + const itemKey = await API.createItem("book", { inPublications: true, deleted: true }, this, 'key'); + + const response = await API.userGet( + config.userID, + "publications/items/" + itemKey + ); + Helpers.assert404(response); + }); + + it('test_should_return_400_for_settings_request_with_items', async function () { + API.useAPIKey(config.apiKey); + let response = await API.createItem("book", { inPublications: true }, this, 'response'); + Helpers.assert200ForObject(response); + + response = await API.get(`users/${config.userID}/publications/settings`); + assert.equal(response.status, 400); + }); + + // Disabled until after integrated My Publications upgrade + it('test_should_return_404_for_deleted_request', async function () { + this.skip(); + let response = await API.get(`users/${config.userID}/publications/deleted?since=0`); + Helpers.assert404(response); + }); + + it('test_should_return_no_results_for_empty_publications_list', async function () { + let response = await API.get(`users/${config.userID}/publications/items`); + Helpers.assert200(response); + Helpers.assertNoResults(response); + assert.isNumber(parseInt(response.headers['last-modified-version'][0])); + }); + + it('test_shouldnt_show_restricted_properties', async function () { + API.useAPIKey(config.apiKey); + let itemKey = await API.createItem('book', { inPublications: true }, this, 'key'); + + // JSON + let response = await API.userGet(config.userID, `publications/items/${itemKey}`); + Helpers.assert200(response); + let json = API.getJSONFromResponse(response); + assert.notProperty(json.data, 'inPublications'); + assert.notProperty(json.data, 'collections'); + assert.notProperty(json.data, 'relations'); + assert.notProperty(json.data, 'tags'); + assert.notProperty(json.data, 'dateAdded'); + assert.notProperty(json.data, 'dateModified'); + + // Atom + response = await API.userGet(config.userID, `publications/items/${itemKey}?format=atom&content=html,json`); + Helpers.assert200(response); + + // HTML in Atom + let html = await API.getContentFromAtomResponse(response, 'html'); + let doc = (new JSDOM(html.innerHTML)).window.document; + let trs = Array.from(doc.getElementsByTagName("tr")); + let publications = trs.filter(node => node.getAttribute("class") == "publication"); + assert.equal(publications.length, 0); + + // JSON in Atom + let atomJson = await API.getContentFromAtomResponse(response, 'json'); + assert.notProperty(atomJson, 'inPublications'); + assert.notProperty(atomJson, 'collections'); + assert.notProperty(atomJson, 'relations'); + assert.notProperty(atomJson, 'tags'); + assert.notProperty(atomJson, 'dateAdded'); + assert.notProperty(atomJson, 'dateModified'); + }); + + it('test_shouldnt_remove_inPublications_on_POST_without_property', async function () { + await API.useAPIKey(config.apiKey); + const json = await API.getItemTemplate('book'); + json.inPublications = true; + const response = await API.userPost(config.userID, 'items', JSON.stringify([json])); + + Helpers.assert200(response); + const key = API.getJSONFromResponse(response).successful[0].key; + const version = response.headers['last-modified-version'][0]; + + const newJson = { + key: key, + version: version, + title: 'Test', + inPublications: false + }; + + const newResponse = await API.userPost( + config.userID, + 'items', + JSON.stringify([newJson]), + { 'Content-Type': 'application/json' } + ); + + Helpers.assert200ForObject(newResponse); + + const newJsonResponse = API.getJSONFromResponse(newResponse); + + assert.notProperty(newJsonResponse.successful[0].data, 'inPublications'); + }); + + it('test_should_return_404_for_searches_request', async function () { + let response = await API.get(`users/${config.userID}/publications/searches`); + Helpers.assert404(response); + }); + + it('test_shouldnt_show_child_items_in_top_mode', async function () { + API.useAPIKey(config.apiKey); + + let parentItemKey = await API.createItem("book", { title: 'A', inPublications: true }, this, 'key'); + + let json1 = await API.getItemTemplate("attachment&linkMode=imported_file"); + json1.title = 'B'; + json1.parentItem = parentItemKey; + json1.inPublications = true; + + let json2 = await API.getItemTemplate("attachment&linkMode=imported_file"); + json2.title = 'C'; + json2.parentItem = parentItemKey; + + let response = await API.userPost( + config.userID, + "items", + JSON.stringify([json1, json2]) + ); + + Helpers.assert200(response); + API.useAPIKey(""); + + response = await API.userGet( + config.userID, + "publications/items/top" + ); + + Helpers.assert200(response); + let json = await API.getJSONFromResponse(response); + + assert.equal(json.length, 1); + + let titles = json.map(item => item.data.title); + + assert.include(titles, 'A'); + }); + + it('test_shouldnt_show_child_item_not_in_publications_for_item_children_request', async function () { + API.useAPIKey(config.apiKey); + const parentItemKey = await API.createItem("book", { title: 'A', inPublications: true }, this, 'key'); + + const json1 = await API.getItemTemplate("attachment&linkMode=imported_file"); + json1.title = 'B'; + json1.parentItem = parentItemKey; + json1.inPublications = true; + // Create hidden child attachment + const json2 = await API.getItemTemplate("attachment&linkMode=imported_file"); + json2.title = 'C'; + json2.parentItem = parentItemKey; + const response = await API.userPost(config.userID, "items", JSON.stringify([json1, json2])); + Helpers.assert200(response); + + // Anonymous read + API.useAPIKey(""); + + const response2 = await API.userGet(config.userID, `publications/items/${parentItemKey}/children`); + Helpers.assert200(response2); + const json = API.getJSONFromResponse(response2); + assert.equal(json.length, 1); + const titles = json.map(item => item.data.title); + assert.include(titles, 'B'); + }); + + it('test_shouldnt_show_child_item_not_in_publications', async function () { + API.useAPIKey(config.apiKey); + const parentItemKey = await API.createItem('book', { title: 'A', inPublications: true }, this, 'key'); + + const json1 = await API.getItemTemplate('attachment&linkMode=imported_file'); + json1.title = 'B'; + json1.parentItem = parentItemKey; + json1.inPublications = true; + const json2 = await API.getItemTemplate('attachment&linkMode=imported_file'); + json2.title = 'C'; + json2.parentItem = parentItemKey; + const response = await API.userPost(config.userID, 'items', JSON.stringify([json1, json2])); + Helpers.assert200(response); + API.useAPIKey(''); + const readResponse = await API.userGet(config.userID, 'publications/items'); + Helpers.assert200(readResponse); + const json = API.getJSONFromResponse(readResponse); + Helpers.assertCount(2, json); + const titles = json.map(item => item.data.title); + assert.include(titles, 'A'); + assert.include(titles, 'B'); + assert.notInclude(titles, 'C'); + }); + + it('test_should_return_200_for_settings_request_with_no_items', async function () { + let response = await API.get(`users/${config.userID}/publications/settings`); + Helpers.assert200(response); + Helpers.assertNoResults(response); + }); + + it('test_should_return_403_for_anonymous_write', async function () { + const json = await API.getItemTemplate("book"); + const response = await API.userPost(config.userID, "publications/items", JSON.stringify(json)); + Helpers.assert403(response); + }); + + it('test_should_return_405_for_authenticated_write', async function () { + await API.useAPIKey(config.apiKey); + const json = await API.getItemTemplate('book'); + const response = await API.userPost(config.userID, 'publications/items', JSON.stringify(json), { 'Content-Type': 'application/json' }); + Helpers.assert405(response); + }); + + it('test_shouldnt_show_trashed_item_in_versions_response', async function () { + await API.useAPIKey(config.apiKey); + let itemKey1 = await API.createItem("book", { inPublications: true }, this, 'key'); + let itemKey2 = await API.createItem("book", { inPublications: true, deleted: true }, this, 'key'); + + let response = await API.userGet( + config.userID, + "publications/items?format=versions" + ); + Helpers.assert200(response); + let json = await API.getJSONFromResponse(response); + assert.equal(json.hasOwnProperty(itemKey1), true); + assert.equal(json.hasOwnProperty(itemKey2), false); + + // Shouldn't show with includeTrashed=1 here + response = await API.userGet( + config.userID, + "publications/items?format=versions&includeTrashed=1" + ); + Helpers.assert200(response); + json = await API.getJSONFromResponse(response); + assert.equal(json.hasOwnProperty(itemKey1), true); + assert.equal(json.hasOwnProperty(itemKey2), false); + }); + + it('test_should_include_download_details', async function () { + API.useAPIKey(config.apiKey); + const file = "work/file"; + const fileContents = Helpers.getRandomUnicodeString(); + const contentType = "text/html"; + const charset = "utf-8"; + fs.writeFileSync(file, fileContents); + const hash = Helpers.md5File(file); + const filename = "test_" + fileContents; + const mtime = parseInt(fs.statSync(file).mtimeMs); + const size = fs.statSync(file).size; + + const parentItemKey = await API.createItem("book", { title: 'A', inPublications: true }, this, 'key'); + const json = await API.createAttachmentItem("imported_file", { + parentItem: parentItemKey, + inPublications: true, + contentType: contentType, + charset: charset + }, false, this, 'jsonData'); + const key = json.key; + const originalVersion = json.version; + + // Get upload authorization + API.useAPIKey(config.apiKey); + let response = await API.userPost( + config.userID, + `items/${key}/file`, + Helpers.implodeParams({ + md5: hash, + mtime: mtime, + filename: filename, + filesize: size + }), + { + 'Content-Type': 'application/x-www-form-urlencoded', + 'If-None-Match': '*' + } + ); + Helpers.assert200(response); + let jsonResponse = JSON.parse(response.data); + + toDelete.push(hash); + + // Upload to S3 + response = await HTTP.post( + jsonResponse.url, + jsonResponse.prefix + fileContents + jsonResponse.suffix, + { 'Content-Type': jsonResponse.contentType } + ); + Helpers.assert201(response); + + // Register upload + response = await API.userPost( + config.userID, + `items/${key}/file`, + `upload=${jsonResponse.uploadKey}`, + { 'Content-Type': 'application/x-www-form-urlencoded', + 'If-None-Match': '*' } + ); + Helpers.assert204(response); + const newVersion = response.headers['last-modified-version'][0]; + assert.isAbove(parseInt(newVersion), parseInt(originalVersion)); + + // Anonymous read + API.useAPIKey(''); + + // Verify attachment item metadata (JSON) + response = await API.userGet( + config.userID, + `publications/items/${key}` + ); + const responseData = JSON.parse(response.data); + const jsonData = responseData.data; + assert.equal(hash, jsonData.md5); + assert.equal(mtime, jsonData.mtime); + assert.equal(filename, jsonData.filename); + assert.equal(contentType, jsonData.contentType); + assert.equal(charset, jsonData.charset); + + // Verify download details (JSON) + Helpers.assertRegExp( + `https?://[^/]+/users/${config.userID}/publications/items/${key}/file/view`, + responseData.links.enclosure.href + ); + + // Verify attachment item metadata (Atom) + response = await API.userGet( + config.userID, + `publications/items/${key}?format=atom` + ); + const xml = API.getXMLFromResponse(response); + const hrefComp = Helpers.xpathEval(xml, '//atom:entry/atom:link[@rel="enclosure"]', true, false); + const href = hrefComp.getAttribute('href'); + // Verify download details (JSON) + Helpers.assertRegExp( + `https?://[^/]+/users/${config.userID}/publications/items/${key}/file/view`, + href + ); + + // Check access to file + const r = `https?://[^/]+/(users/${config.userID}/publications/items/${key}/file/view)`; + const exp = new RegExp(r); + const matches = href.match(exp); + const fileURL = matches[1]; + response = await API.get(fileURL); + Helpers.assert302(response); + + // Remove item from My Publications + API.useAPIKey(config.apiKey); + + responseData.data.inPublications = false; + response = await API.userPost( + config.userID, + 'items', + JSON.stringify([responseData]), + { + 'Content-Type': 'application/json' + + } + ); + Helpers.assert200ForObject(response); + + // No more access via publications URL + API.useAPIKey(); + response = await API.get(fileURL); + Helpers.assert404(response); + }); +}); From bf083e1ffb50c46608206b374e5c9e38868db3ec Mon Sep 17 00:00:00 2001 From: Bogdan Abaev Date: Thu, 25 May 2023 22:48:43 -0400 Subject: [PATCH 10/33] minor adjustments to prevent random 500 errors --- tests/remote_js/test/2/objectTest.js | 3 +- tests/remote_js/test/2/storageAdmin.js | 1 - tests/remote_js/test/shared.js | 74 +++++++++++++++++--------- 3 files changed, 51 insertions(+), 27 deletions(-) diff --git a/tests/remote_js/test/2/objectTest.js b/tests/remote_js/test/2/objectTest.js index ffd1cd85..b8cac412 100644 --- a/tests/remote_js/test/2/objectTest.js +++ b/tests/remote_js/test/2/objectTest.js @@ -6,7 +6,7 @@ const Helpers = require('../../helpers.js'); const { API2Setup, API2WrapUp } = require("../shared.js"); describe('ObjectTests', function () { - this.timeout(config.timeout); + this.timeout(0); before(async function () { await API2Setup(); @@ -220,6 +220,7 @@ describe('ObjectTests', function () { switch (objectType) { case 'collection': + await new Promise(r => setTimeout(r, 1000)); objectData = await API.createCollection('Test', false, true, 'data'); objectDataContent = objectData.content; json1 = JSON.parse(objectDataContent); diff --git a/tests/remote_js/test/2/storageAdmin.js b/tests/remote_js/test/2/storageAdmin.js index 134898f2..b85ecaf9 100644 --- a/tests/remote_js/test/2/storageAdmin.js +++ b/tests/remote_js/test/2/storageAdmin.js @@ -28,7 +28,6 @@ describe('StorageAdminTests', function () { }); Helpers.assertStatusCode(response, 200); let xml = API.getXMLFromResponse(response); - console.log(xml.documentElement.innerHTML); let xmlQuota = xml.getElementsByTagName("quota")[0].innerHTML; assert.equal(xmlQuota, expectedQuota); if (expiration != 0) { diff --git a/tests/remote_js/test/shared.js b/tests/remote_js/test/shared.js index 54772bee..12e8f556 100644 --- a/tests/remote_js/test/shared.js +++ b/tests/remote_js/test/shared.js @@ -2,14 +2,30 @@ const config = require("../config.js"); const API = require('../api2.js'); const API3 = require('../api3.js'); -module.exports = { - APISetCredentials: async () => { - const credentials = await API.login(); - config.apiKey = credentials.user1.apiKey; - config.user2APIKey = credentials.user2.apiKey; - await API3.useAPIKey(config.apiKey); - }, +// To fix socket hang up errors +const retryIfNeeded = async (action) => { + let success = false; + let attempts = 3; + let tried = 0; + while (tried < attempts && !success) { + try { + await action(); + success = true; + } + catch (e) { + console.log(e); + console.log("Waiting for 2 seconds and re-trying."); + await new Promise(r => setTimeout(r, 2000)); + tried += 1; + } + } + if (!success) { + throw new Error(`Setup action did not succeed after ${attempts} retried.`); + } +}; + +module.exports = { API1Setup: async () => { const credentials = await API.login(); @@ -23,30 +39,38 @@ module.exports = { }, API2Setup: async () => { - const credentials = await API.login(); - config.apiKey = credentials.user1.apiKey; - config.user2APIKey = credentials.user2.apiKey; - await API.useAPIVersion(2); - await API.setKeyOption(config.userID, config.apiKey, 'libraryNotes', 1); - await API.userClear(config.userID); + await retryIfNeeded(async () => { + const credentials = await API.login(); + config.apiKey = credentials.user1.apiKey; + config.user2APIKey = credentials.user2.apiKey; + await API.useAPIVersion(2); + await API.setKeyOption(config.userID, config.apiKey, 'libraryNotes', 1); + await API.userClear(config.userID); + }); }, API2WrapUp: async () => { - await API.userClear(config.userID); + await retryIfNeeded(async () => { + await API.userClear(config.userID); + }); }, API3Setup: async () => { - const credentials = await API.login(); - config.apiKey = credentials.user1.apiKey; - config.user2APIKey = credentials.user2.apiKey; - await API3.useAPIVersion(3); - await API3.useAPIKey(config.apiKey); - await API3.resetSchemaVersion(); - await API3.setKeyUserPermission(config.apiKey, 'notes', true); - await API3.setKeyUserPermission(config.apiKey, 'write', true); - await API.userClear(config.userID); + await retryIfNeeded(async () => { + const credentials = await API.login(); + config.apiKey = credentials.user1.apiKey; + config.user2APIKey = credentials.user2.apiKey; + await API3.useAPIVersion(3); + await API3.useAPIKey(config.apiKey); + await API3.resetSchemaVersion(); + await API3.setKeyUserPermission(config.apiKey, 'notes', true); + await API3.setKeyUserPermission(config.apiKey, 'write', true); + await API.userClear(config.userID); + }); }, API3WrapUp: async () => { - await API3.useAPIKey(config.apiKey); - await API.userClear(config.userID); + await retryIfNeeded(async () => { + await API3.useAPIKey(config.apiKey); + await API.userClear(config.userID); + }); } }; From 6af736758bdfae449af905bbe822afcc08fcf110 Mon Sep 17 00:00:00 2001 From: Bogdan Abaev Date: Fri, 26 May 2023 16:38:07 -0400 Subject: [PATCH 11/33] more v3 tests --- tests/remote_js/api3.js | 32 +- tests/remote_js/helpers.js | 14 +- tests/remote_js/test/2/fileTest.js | 1 - tests/remote_js/test/2/groupTest.js | 3 +- tests/remote_js/test/2/permissionsTest.js | 3 +- tests/remote_js/test/2/settingsTest.js | 254 +++++---- tests/remote_js/test/3/cacheTest.js | 47 ++ tests/remote_js/test/3/exportTest.js | 181 ++++++ tests/remote_js/test/3/generalTest.js | 64 +++ tests/remote_js/test/3/keysTest.js | 306 ++++++++++ tests/remote_js/test/3/permissionTest.js | 371 ++++++++++++ tests/remote_js/test/3/publicationTest.js | 3 +- tests/remote_js/test/3/relationTest.js | 374 ++++++++++++ tests/remote_js/test/3/searchTest.js | 296 ++++++++++ tests/remote_js/test/3/settingsTest.js | 665 ++++++++++++++++++++++ tests/remote_js/test/3/sortTest.js | 418 ++++++++++++++ tests/remote_js/test/3/storageAdmin.js | 49 ++ tests/remote_js/test/3/translationTest.js | 168 ++++++ tests/remote_js/test/shared.js | 9 +- 19 files changed, 3122 insertions(+), 136 deletions(-) create mode 100644 tests/remote_js/test/3/cacheTest.js create mode 100644 tests/remote_js/test/3/exportTest.js create mode 100644 tests/remote_js/test/3/generalTest.js create mode 100644 tests/remote_js/test/3/keysTest.js create mode 100644 tests/remote_js/test/3/permissionTest.js create mode 100644 tests/remote_js/test/3/relationTest.js create mode 100644 tests/remote_js/test/3/searchTest.js create mode 100644 tests/remote_js/test/3/settingsTest.js create mode 100644 tests/remote_js/test/3/sortTest.js create mode 100644 tests/remote_js/test/3/storageAdmin.js create mode 100644 tests/remote_js/test/3/translationTest.js diff --git a/tests/remote_js/api3.js b/tests/remote_js/api3.js index 8e9008e5..e16c28be 100644 --- a/tests/remote_js/api3.js +++ b/tests/remote_js/api3.js @@ -236,10 +236,9 @@ class API3 extends API2 { } static async resetKey(key) { - let response; - response = await this.get( + let response = await this.get( `keys/${key}`, - [], + {}, { username: `${this.config.rootUsername}`, password: `${this.config.rootPassword}` @@ -249,7 +248,7 @@ class API3 extends API2 { console.log(response.data); throw new Error(`GET returned ${response.status}`); } - let json = this.getJSONFromResponse(response, true); + let json = this.getJSONFromResponse(response); const resetLibrary = (lib) => { @@ -262,9 +261,9 @@ class API3 extends API2 { } delete json.access.groups; response = await this.put( - `users/${this.config.userID}/keys/${this.apiKey}`, + `users/${this.config.userID}/keys/${this.config.apiKey}`, JSON.stringify(json), - [], + {}, { username: `${this.config.rootUsername}`, password: `${this.config.rootPassword}` @@ -424,13 +423,13 @@ class API3 extends API2 { return this.handleCreateResponse('item', response, returnFormat, context, groupID); }; - static async getFirstSuccessKeyFromResponse(response) { + static getFirstSuccessKeyFromResponse(response) { let json = this.getJSONFromResponse(response); if (!json.success) { console.log(response.body); throw new Error("No success keys found in response"); } - return json.success.shift(); + return json.success[0]; } static async groupGet(groupID, suffix, headers = {}, auth = false) { @@ -527,7 +526,7 @@ class API3 extends API2 { static async setKeyGroupPermission(key, groupID, permission, _) { let response = await this.get( "keys/" + key, - [], + {}, { username: this.config.rootUsername, password: this.config.rootPassword @@ -545,11 +544,12 @@ class API3 extends API2 { if (!json.access.groups) { json.access.groups = {}; } + json.access.groups[groupID] = json.access.groups[groupID] || {}; json.access.groups[groupID][permission] = true; response = await this.put( "keys/" + key, JSON.stringify(json), - [], + {}, { username: this.config.rootUsername, password: this.config.rootPassword @@ -630,23 +630,29 @@ class API3 extends API2 { switch (permission) { case 'library': - if (json.access.user && value == !json.access.user.library) { + if (json.access?.user && value == json.access?.user.library) { break; } + json.access = json.access || {}; + json.access.user = json.access.user || {}; json.access.user.library = value; break; case 'write': - if (json.access.user && value == !json.access.user.write) { + if (json.access?.user && value == json.access?.user.write) { break; } + json.access = json.access || {}; + json.access.user = json.access.user || {}; json.access.user.write = value; break; case 'notes': - if (json.access.user && value == !json.access.user.notes) { + if (json.access?.user && value == json.access?.user.notes) { break; } + json.access = json.access || {}; + json.access.user = json.access.user || {}; json.access.user.notes = value; break; } diff --git a/tests/remote_js/helpers.js b/tests/remote_js/helpers.js index f4f82c01..f5b3f4ff 100644 --- a/tests/remote_js/helpers.js +++ b/tests/remote_js/helpers.js @@ -61,7 +61,7 @@ class Helpers { exp = new RegExp(exp); } if (!exp.test(val)) { - throw new Error(`${val} does not match regular expression`) + throw new Error(`${val} does not match regular expression`); } } @@ -141,12 +141,16 @@ class Helpers { this.assertStatusCode(response, 204); }; + static assert300 = (response) => { + this.assertStatusCode(response, 300); + }; + static assert302 = (response) => { this.assertStatusCode(response, 302); }; - static assert400 = (response) => { - this.assertStatusCode(response, 400); + static assert400 = (response, message) => { + this.assertStatusCode(response, 400, message); }; static assert401 = (response) => { @@ -193,6 +197,10 @@ class Helpers { this.assertStatusForObject(response, 'failed', index, 413, message); }; + static assertUnchangedForObject = (response, { index = 0, message = null } = {}) => { + this.assertStatusForObject(response, 'unchanged', index, null, message); + }; + // Methods to help during conversion static assertEquals = (one, two) => { assert.equal(two, one); diff --git a/tests/remote_js/test/2/fileTest.js b/tests/remote_js/test/2/fileTest.js index c5498370..69d17448 100644 --- a/tests/remote_js/test/2/fileTest.js +++ b/tests/remote_js/test/2/fileTest.js @@ -115,7 +115,6 @@ describe('FileTestTests', function () { // Skipped as there may or may not be an error it('testAddFileFullParams', async function () { - this.skip(); let xml = await API.createAttachmentItem("imported_file", [], false, this); let data = await API.parseDataFromAtomEntry(xml); diff --git a/tests/remote_js/test/2/groupTest.js b/tests/remote_js/test/2/groupTest.js index 31f288c6..6113cabe 100644 --- a/tests/remote_js/test/2/groupTest.js +++ b/tests/remote_js/test/2/groupTest.js @@ -3,9 +3,8 @@ const assert = chai.assert; const config = require("../../config.js"); const API = require('../../api2.js'); const Helpers = require('../../helpers.js'); -const { API2Setup, API2WrapUp } = require("../shared.js"); +const { API2Setup, API2WrapUp, resetGroups } = require("../shared.js"); const { JSDOM } = require("jsdom"); -const { resetGroups } = require("../../groupsSetup.js"); describe('GroupTests', function () { this.timeout(config.timeout); diff --git a/tests/remote_js/test/2/permissionsTest.js b/tests/remote_js/test/2/permissionsTest.js index d26324c0..ed4d385b 100644 --- a/tests/remote_js/test/2/permissionsTest.js +++ b/tests/remote_js/test/2/permissionsTest.js @@ -3,8 +3,7 @@ const assert = chai.assert; const config = require("../../config.js"); const API = require('../../api2.js'); const Helpers = require('../../helpers.js'); -const { API2Setup, API2WrapUp } = require("../shared.js"); -const { resetGroups } = require("../../groupsSetup.js"); +const { API2Setup, API2WrapUp, resetGroups } = require("../shared.js"); describe('PermissionsTests', function () { this.timeout(config.timeout); diff --git a/tests/remote_js/test/2/settingsTest.js b/tests/remote_js/test/2/settingsTest.js index bac4ed87..05ec3dc2 100644 --- a/tests/remote_js/test/2/settingsTest.js +++ b/tests/remote_js/test/2/settingsTest.js @@ -3,8 +3,7 @@ const assert = chai.assert; const config = require("../../config.js"); const API = require('../../api2.js'); const Helpers = require('../../helpers.js'); -const { API2Setup, API2WrapUp } = require("../shared.js"); -const { resetGroups } = require("../../groupsSetup.js"); +const { API2Setup, API2WrapUp, resetGroups } = require("../shared.js"); describe('SettingsTests', function () { this.timeout(config.timeout); @@ -19,7 +18,7 @@ describe('SettingsTests', function () { await resetGroups(); }); - beforeEach(async function() { + beforeEach(async function () { await API.userClear(config.userID); await API.groupClear(config.ownedPrivateGroupID); }); @@ -207,141 +206,125 @@ describe('SettingsTests', function () { assert.equal(parseInt(libraryVersion) + 1, json3.version); }); - it('testAddGroupSettingMultiple', async function () { - const settingKey = "tagColors"; - const value = [ - { - name: "_READ", - color: "#990000" - } - ]; - - // TODO: multiple, once more settings are supported + it('testDeleteNonexistentSetting', async function () { + const response = await API.userDelete(config.userID, + `settings/nonexistentSetting?key=${config.apiKey}`, + { "If-Unmodified-Since-Version": "0" }); + Helpers.assertStatusCode(response, 404); + }); - const groupID = config.ownedPrivateGroupID; - const libraryVersion = await API.getGroupLibraryVersion(groupID); + it('testUnsupportedSetting', async function () { + const settingKey = "unsupportedSetting"; + let value = true; const json = { - [settingKey]: { - value: value - } + value: value, + version: 0 }; - const response = await API.groupPost( - groupID, - `settings?key=${config.apiKey}`, + const response = await API.userPut( + config.userID, + `settings/${settingKey}?key=${config.apiKey}`, JSON.stringify(json), { "Content-Type": "application/json" } ); - - Helpers.assertStatusCode(response, 204); - - // Multi-object GET - const response2 = await API.groupGet( - groupID, - `settings?key=${config.apiKey}` - ); - - Helpers.assertStatusCode(response2, 200); - assert.equal(response2.headers['content-type'][0], "application/json"); - const json2 = JSON.parse(response2.data); - assert.isNotNull(json2); - assert.property(json2, settingKey); - assert.deepEqual(value, json2[settingKey].value); - assert.equal(parseInt(libraryVersion) + 1, json2[settingKey].version); - - // Single-object GET - const response3 = await API.groupGet( - groupID, - `settings/${settingKey}?key=${config.apiKey}` - ); - - assert.equal(response3.status, 200); - assert.equal(response3.headers['content-type'][0], "application/json"); - const json3 = JSON.parse(response3.data); - assert.deepEqual(value, json3.value); - assert.equal(parseInt(libraryVersion) + 1, json3.version); + Helpers.assertStatusCode(response, 400, `Invalid setting '${settingKey}'`); }); - it('testAddGroupSettingMultiple', async function () { - const settingKey = "tagColors"; - const value = [ + it('testUpdateUserSetting', async function () { + let settingKey = "tagColors"; + let value = [ { name: "_READ", color: "#990000" } ]; - - // TODO: multiple, once more settings are supported - - const groupID = config.ownedPrivateGroupID; - const libraryVersion = await API.getGroupLibraryVersion(groupID); - - const json = { - [settingKey]: { - value: value - } + + let libraryVersion = await API.getLibraryVersion(); + + let json = { + value: value, + version: 0 }; - - const response = await API.groupPost( - groupID, - `settings?key=${config.apiKey}`, + + // Create + let response = await API.userPut( + config.userID, + `settings/${settingKey}?key=${config.apiKey}`, JSON.stringify(json), - { "Content-Type": "application/json" } + { + "Content-Type": "application/json" + } ); - - Helpers.assertStatusCode(response, 204); - - // Multi-object GET - const response2 = await API.groupGet( - groupID, - `settings?key=${config.apiKey}` + Helpers.assert204(response); + + // Check + response = await API.userGet( + config.userID, + `settings/${settingKey}?key=${config.apiKey}` ); - - Helpers.assertStatusCode(response2, 200); - assert.equal(response2.headers['content-type'][0], "application/json"); - const json2 = JSON.parse(response2.data); - assert.property(json2, settingKey); - assert.deepEqual(value, json2[settingKey].value); - assert.equal(parseInt(libraryVersion) + 1, json2[settingKey].version); - - // Single-object GET - const response3 = await API.groupGet( - groupID, + Helpers.assert200(response); + Helpers.assertContentType(response, "application/json"); + json = JSON.parse(response.data); + assert.isNotNull(json); + assert.deepEqual(json.value, value); + assert.equal(parseInt(json.version), parseInt(libraryVersion) + 1); + + // Update with no change + response = await API.userPut( + config.userID, + `settings/${settingKey}?key=${config.apiKey}`, + JSON.stringify(json), + { + "Content-Type": "application/json" + } + ); + Helpers.assert204(response); + + // Check + response = await API.userGet( + config.userID, `settings/${settingKey}?key=${config.apiKey}` ); - - Helpers.assertStatusCode(response3, 200); - assert.equal(response3.headers['content-type'][0], "application/json"); - const json3 = JSON.parse(response3.data); - assert.deepEqual(value, json3.value); - assert.equal(parseInt(libraryVersion) + 1, json3.version); - }); - - it('testDeleteNonexistentSetting', async function () { - const response = await API.userDelete(config.userID, - `settings/nonexistentSetting?key=${config.apiKey}`, - { "If-Unmodified-Since-Version": "0" }); - Helpers.assertStatusCode(response, 404); - }); - - it('testUnsupportedSetting', async function () { - const settingKey = "unsupportedSetting"; - let value = true; - - const json = { - value: value, - version: 0 - }; - - const response = await API.userPut( + Helpers.assert200(response); + Helpers.assertContentType(response, "application/json"); + json = JSON.parse(response.data); + assert.isNotNull(json); + assert.deepEqual(json.value, value); + assert.equal(parseInt(json.version), parseInt(libraryVersion) + 1); + + let newValue = [ + { + name: "_READ", + color: "#CC9933" + } + ]; + json.value = newValue; + + // Update, no change + response = await API.userPut( config.userID, `settings/${settingKey}?key=${config.apiKey}`, JSON.stringify(json), - { "Content-Type": "application/json" } + { + "Content-Type": "application/json" + } ); - Helpers.assertStatusCode(response, 400, `Invalid setting '${settingKey}'`); + Helpers.assert204(response); + + // Check + response = await API.userGet( + config.userID, + `settings/${settingKey}?key=${config.apiKey}` + ); + Helpers.assert200(response); + Helpers.assertContentType(response, "application/json"); + json = JSON.parse(response.data); + assert.isNotNull(json); + assert.deepEqual(json.value, newValue); + assert.equal(parseInt(json.version), parseInt(libraryVersion) + 2); }); + it('testUnsupportedSettingMultiple', async function () { const settingKey = 'unsupportedSetting'; @@ -400,4 +383,51 @@ describe('SettingsTests', function () { ); Helpers.assertStatusCode(response, 400, "'value' cannot be longer than 30000 characters"); }); + + it('testDeleteUserSetting', async function () { + let settingKey = "tagColors"; + let value = [ + { + name: "_READ", + color: "#990000" + } + ]; + + let json = { + value: value, + version: 0 + }; + + let libraryVersion = parseInt(await API.getLibraryVersion()); + + // Create + let response = await API.userPut( + config.userID, + `settings/${settingKey}?key=${config.apiKey}`, + JSON.stringify(json), + { + "Content-Type": "application/json" + } + ); + Helpers.assert204(response); + + // Delete + response = await API.userDelete( + config.userID, + `settings/${settingKey}?key=${config.apiKey}`, + { + "If-Unmodified-Since-Version": `${libraryVersion + 1}` + } + ); + Helpers.assert204(response); + + // Check + response = await API.userGet( + config.userID, + `settings/${settingKey}?key=${config.apiKey}` + ); + Helpers.assert404(response); + + assert.equal(libraryVersion + 2, await API.getLibraryVersion()); + }); }); diff --git a/tests/remote_js/test/3/cacheTest.js b/tests/remote_js/test/3/cacheTest.js new file mode 100644 index 00000000..4aabeb9b --- /dev/null +++ b/tests/remote_js/test/3/cacheTest.js @@ -0,0 +1,47 @@ +const chai = require('chai'); +const assert = chai.assert; +const config = require("../../config.js"); +const API = require('../../api3.js'); +const { API3Setup, API3WrapUp } = require("../shared.js"); + +describe('CacheTests', function () { + this.timeout(config.timeout); + + before(async function () { + await API3Setup(); + }); + + after(async function () { + await API3WrapUp(); + }); + + it('testCacheCreatorPrimaryData', async function () { + const data = { + title: 'Title', + creators: [ + { + creatorType: 'author', + firstName: 'First', + lastName: 'Last', + }, + { + creatorType: 'editor', + firstName: 'Ed', + lastName: 'McEditor', + }, + ], + }; + + const key = await API.createItem('book', data, true, 'key'); + + const response = await API.userGet( + config.userID, + `items/${key}?content=csljson` + ); + const json = JSON.parse(API.getContentFromResponse(response)); + assert.equal(json.author[0].given, 'First'); + assert.equal(json.author[0].family, 'Last'); + assert.equal(json.editor[0].given, 'Ed'); + assert.equal(json.editor[0].family, 'McEditor'); + }); +}); diff --git a/tests/remote_js/test/3/exportTest.js b/tests/remote_js/test/3/exportTest.js new file mode 100644 index 00000000..f4963228 --- /dev/null +++ b/tests/remote_js/test/3/exportTest.js @@ -0,0 +1,181 @@ +const chai = require('chai'); +const assert = chai.assert; +const config = require("../../config.js"); +const API = require('../../api3.js'); +const Helpers = require('../../helpers3.js'); +const { API3Setup, API3WrapUp } = require("../shared.js"); + +describe('ExportTests', function () { + this.timeout(config.timeout); + let items = {}; + let multiResponses; + let formats = ['bibtex', 'ris', 'csljson']; + + before(async function () { + await API3Setup(); + await API.userClear(config.userID); + + // Create test data + let key = await API.createItem("book", { + title: "Title", + date: "January 1, 2014", + accessDate: "2019-05-23T01:23:45Z", + creators: [ + { + creatorType: "author", + firstName: "First", + lastName: "Last" + } + ] + }, null, 'key'); + items[key] = { + bibtex: "\n@book{last_title_2014,\n title = {Title},\n urldate = {2019-05-23},\n author = {Last, First},\n month = jan,\n year = {2014},\n}\n", + ris: "TY - BOOK\r\nTI - Title\r\nAU - Last, First\r\nDA - 2014/01/01/\r\nPY - 2014\r\nY2 - 2019/05/23/01:23:45\r\nER - \r\n\r\n", + csljson: { + id: config.libraryID + "/" + key, + type: 'book', + title: 'Title', + author: [ + { + family: 'Last', + given: 'First' + } + ], + issued: { + 'date-parts': [ + ["2014", 1, 1] + ] + }, + accessed: { + 'date-parts': [ + [2019, 5, 23] + ] + } + } + }; + + key = await API.createItem("book", { + title: "Title 2", + date: "June 24, 2014", + creators: [ + { + creatorType: "author", + firstName: "First", + lastName: "Last" + }, + { + creatorType: "editor", + firstName: "Ed", + lastName: "McEditor" + } + ] + }, null, 'key'); + items[key] = { + bibtex: "\n@book{last_title_2014,\n title = {Title 2},\n author = {Last, First},\n editor = {McEditor, Ed},\n month = jun,\n year = {2014},\n}\n", + ris: "TY - BOOK\r\nTI - Title 2\r\nAU - Last, First\r\nA3 - McEditor, Ed\r\nDA - 2014/06/24/\r\nPY - 2014\r\nER - \r\n\r\n", + csljson: { + id: config.libraryID + "/" + key, + type: 'book', + title: 'Title 2', + author: [ + { + family: 'Last', + given: 'First' + } + ], + editor: [ + { + family: 'McEditor', + given: 'Ed' + } + ], + issued: { + 'date-parts': [ + ['2014', 6, 24] + ] + } + } + }; + + multiResponses = { + bibtex: { + contentType: "application/x-bibtex", + content: "\n@book{last_title_2014,\n title = {Title 2},\n author = {Last, First},\n editor = {McEditor, Ed},\n month = jun,\n year = {2014},\n}\n\n@book{last_title_2014-1,\n title = {Title},\n urldate = {2019-05-23},\n author = {Last, First},\n month = jan,\n year = {2014},\n}\n" + }, + ris: { + contentType: "application/x-research-info-systems", + content: "TY - BOOK\r\nTI - Title 2\r\nAU - Last, First\r\nA3 - McEditor, Ed\r\nDA - 2014/06/24/\r\nPY - 2014\r\nER - \r\n\r\nTY - BOOK\r\nTI - Title\r\nAU - Last, First\r\nDA - 2014/01/01/\r\nPY - 2014\r\nY2 - 2019/05/23/01:23:45\r\nER - \r\n\r\n" + }, + csljson: { + contentType: "application/vnd.citationstyles.csl+json", + content: { + items: [ + items[Object.keys(items)[1]].csljson, + items[Object.keys(items)[0]].csljson + ] + } + } + }; + }); + + after(async function () { + await API3WrapUp(); + }); + + it('testExportInclude', async function () { + for (let format of formats) { + let response = await API.userGet( + config.userID, + `items?include=${format}`, + { "Content-Type": "application/json" } + ); + Helpers.assert200(response); + let json = API.getJSONFromResponse(response); + for (let obj of json) { + assert.deepEqual(obj[format], items[obj.key][format]); + } + } + }); + + it('testExportFormatSingle', async function () { + for (const format of formats) { + for (const [key, expected] of Object.entries(items)) { + const response = await API.userGet( + config.userID, + `items/${key}?format=${format}` + ); + Helpers.assert200(response); + let body = response.data; + + // TODO: Remove in APIv4 + if (format === 'csljson') { + body = JSON.parse(body); + body = body.items[0]; + } + assert.deepEqual(expected[format], body); + } + } + }); + + it('testExportFormatMultiple', async function () { + for (let format of formats) { + const response = await API.userGet( + config.userID, + `items?format=${format}` + ); + Helpers.assert200(response); + Helpers.assertContentType( + response, + multiResponses[format].contentType + ); + let body = response.data; + if (typeof multiResponses[format].content == 'object') { + body = JSON.parse(body); + } + assert.deepEqual( + multiResponses[format].content, + body + ); + } + }); +}); diff --git a/tests/remote_js/test/3/generalTest.js b/tests/remote_js/test/3/generalTest.js new file mode 100644 index 00000000..3ff19c2a --- /dev/null +++ b/tests/remote_js/test/3/generalTest.js @@ -0,0 +1,64 @@ +const chai = require('chai'); +const assert = chai.assert; +const config = require("../../config.js"); +const API = require('../../api3.js'); +const Helpers = require('../../helpers3.js'); +const { API3Setup, API3WrapUp } = require("../shared.js"); + + +describe('GeneralTests', function () { + this.timeout(config.timeout); + + before(async function () { + await API3Setup(); + }); + + after(async function () { + await API3WrapUp(); + }); + + it('testInvalidCharacters', async function () { + const data = { + title: "A" + String.fromCharCode(0) + "A", + creators: [ + { + creatorType: "author", + name: "B" + String.fromCharCode(1) + "B" + } + ], + tags: [ + { + tag: "C" + String.fromCharCode(2) + "C" + } + ] + }; + const json = await API.createItem("book", data, this, 'jsonData'); + assert.equal("AA", json.title); + assert.equal("BB", json.creators[0].name); + assert.equal("CC", json.tags[0].tag); + }); + + it('testZoteroWriteToken', async function () { + const json = await API.getItemTemplate('book'); + const token = Helpers.uniqueToken(); + + let response = await API.userPost( + config.userID, + `items`, + JSON.stringify([json]), + { 'Content-Type': 'application/json', 'Zotero-Write-Token': token } + ); + + Helpers.assertStatusCode(response, 200); + Helpers.assertStatusForObject(response, 'success', 0); + + response = await API.userPost( + config.userID, + `items?key=${config.apiKey}`, + JSON.stringify({ items: [json] }), + { 'Content-Type': 'application/json', 'Zotero-Write-Token': token } + ); + + Helpers.assertStatusCode(response, 412); + }); +}); diff --git a/tests/remote_js/test/3/keysTest.js b/tests/remote_js/test/3/keysTest.js new file mode 100644 index 00000000..84198d89 --- /dev/null +++ b/tests/remote_js/test/3/keysTest.js @@ -0,0 +1,306 @@ +const chai = require('chai'); +const assert = chai.assert; +const config = require("../../config.js"); +const API = require('../../api3.js'); +const Helpers = require('../../helpers3.js'); +const { API3Setup, API3WrapUp } = require("../shared.js"); + +describe('KeysTests', function () { + this.timeout(config.timeout); + + before(async function () { + await API3Setup(); + }); + + after(async function () { + await API3WrapUp(); + }); + // beforeEach(async function () { + // await API.userClear(config.userID); + // }); + + // afterEach(async function () { + // await API.userClear(config.userID); + // }); + + it('testKeyCreateAndModifyWithCredentials', async function () { + await API.useAPIKey(""); + let name = "Test " + Helpers.uniqueID(); + + let response = await API.userPost( + config.userID, + 'keys', + JSON.stringify({ + username: config.username, + password: config.password, + name: name, + access: { + user: { + library: true + } + } + }) + ); + Helpers.assert403(response); + + response = await API.post( + 'keys', + JSON.stringify({ + username: config.username, + password: config.password, + name: name, + access: { + user: { + library: true + } + } + }), + {}, + {} + ); + Helpers.assert201(response); + let json = await API.getJSONFromResponse(response); + let key = json.key; + assert.equal(json.userID, config.userID); + assert.equal(json.name, name); + assert.deepEqual({ + user: { + library: true, + files: true + } + }, json.access); + + name = "Test " + Helpers.uniqueID(); + + response = await API.userPut( + config.userID, + "keys/" + key, + JSON.stringify({ + username: config.username, + password: config.password, + name: name, + access: { + user: { + library: true + } + } + }) + ); + Helpers.assert403(response); + + response = await API.put( + "keys/" + key, + JSON.stringify({ + username: config.username, + password: config.password, + name: name, + access: { + user: { + library: true + } + } + }) + ); + Helpers.assert200(response); + json = await API.getJSONFromResponse(response); + key = json.key; + assert.equal(json.name, name); + + response = await API.userDelete( + config.userID, + "keys/" + key + ); + Helpers.assert204(response); + }); + + it('testKeyCreateAndDelete', async function () { + await API.useAPIKey(''); + const name = 'Test ' + Helpers.uniqueID(); + + let response = await API.userPost( + config.userID, + 'keys', + JSON.stringify({ + name: name, + access: { + user: { library: true } + } + }) + ); + Helpers.assert403(response); + + response = await API.userPost( + config.userID, + 'keys', + JSON.stringify({ + name: name, + access: { + user: { library: true } + } + }), + {}, + { + username: config.rootUsername, + password: config.rootPassword + } + ); + Helpers.assert201(response); + const json = await API.getJSONFromResponse(response); + const key = json.key; + assert.equal(config.username, json.username); + assert.equal(config.displayName, json.displayName); + assert.equal(name, json.name); + assert.deepEqual({ user: { library: true, files: true } }, json.access); + + response = await API.userDelete(config.userID, 'keys/current', { + 'Zotero-API-Key': key + }); + Helpers.assert204(response); + + response = await API.userGet(config.userID, 'keys/current', { + 'Zotero-API-Key': key + }); + Helpers.assert403(response); + }); + + it('testGetKeyInfoCurrent', async function () { + API.useAPIKey(""); + const response = await API.get( + 'keys/current', + { "Zotero-API-Key": config.apiKey } + ); + Helpers.assert200(response); + const json = await API.getJSONFromResponse(response); + assert.equal(config.apiKey, json.key); + assert.equal(config.userID, json.userID); + assert.equal(config.username, json.username); + assert.equal(config.displayName, json.displayName); + assert.property(json.access, "user"); + assert.property(json.access, "groups"); + assert.isOk(json.access.user.library); + assert.isOk(json.access.user.files); + assert.isOk(json.access.user.notes); + assert.isOk(json.access.user.write); + assert.isOk(json.access.groups.all.library); + assert.isOk(json.access.groups.all.write); + assert.notProperty(json, 'name'); + assert.notProperty(json, 'dateAdded'); + assert.notProperty(json, 'lastUsed'); + assert.notProperty(json, 'recentIPs'); + }); + + it('testGetKeyInfoWithUser', async function () { + API.useAPIKey(""); + const response = await API.userGet( + config.userID, + 'keys/' + config.apiKey + ); + Helpers.assert200(response); + const json = API.getJSONFromResponse(response); + assert.equal(config.apiKey, json.key); + assert.equal(config.userID, json.userID); + assert.property(json.access, "user"); + assert.property(json.access, "groups"); + assert.isOk(json.access.user.library); + assert.isOk(json.access.user.files); + assert.isOk(json.access.user.notes); + assert.isOk(json.access.user.write); + assert.isOk(json.access.groups.all.library); + assert.isOk(json.access.groups.all.write); + }); + + it('testKeyCreateWithEmailAddress', async function () { + API.useAPIKey(""); + let name = "Test " + Helpers.uniqueID(); + let emails = [config.emailPrimary, config.emailSecondary]; + for (let i = 0; i < emails.length; i++) { + let email = emails[i]; + let data = JSON.stringify({ + username: email, + password: config.password, + name: name, + access: { + user: { + library: true + } + } + }); + let headers = { "Content-Type": "application/json" }; + let options = {}; + let response = await API.post('keys', data, headers, options); + Helpers.assert201(response); + let json = API.getJSONFromResponse(response); + assert.equal(config.userID, json.userID); + assert.equal(config.username, json.username); + assert.equal(config.displayName, json.displayName); + assert.equal(name, json.name); + assert.deepEqual({ user: { library: true, files: true } }, json.access); + } + }); + + it('testGetKeyInfoCurrentWithoutHeader', async function () { + API.useAPIKey(''); + const response = await API.get('keys/current'); + + Helpers.assert403(response); + }); + + it('testGetKeys', async function () { + // No anonymous access + API.useAPIKey(''); + let response = await API.userGet( + config.userID, + 'keys' + ); + Helpers.assert403(response); + + // No access with user's API key + API.useAPIKey(config.apiKey); + response = await API.userGet( + config.userID, + 'keys' + ); + Helpers.assert403(response); + + // Root access + response = await API.userGet( + config.userID, + 'keys', + {}, + { + username: config.rootUsername, + password: config.rootPassword, + } + ); + Helpers.assert200(response); + const json = API.getJSONFromResponse(response); + assert.isArray(json, true); + assert.isAbove(json.length, 0); + assert.property(json[0], 'dateAdded'); + assert.property(json[0], 'lastUsed'); + if (config.apiURLPrefix != "http://localhost/") { + assert.property(json[0], 'recentIPs'); + } + }); + + it('testGetKeyInfoByPath', async function () { + API.useAPIKey(""); + const response = await API.get('keys/' + config.apiKey); + Helpers.assert200(response); + const json = await API.getJSONFromResponse(response); + assert.equal(config.apiKey, json.key); + assert.equal(config.userID, json.userID); + assert.property(json.access, 'user'); + assert.property(json.access, 'groups'); + assert.isOk(json.access.user.library); + assert.isOk(json.access.user.files); + assert.isOk(json.access.user.notes); + assert.isOk(json.access.user.write); + assert.isOk(json.access.groups.all.library); + assert.isOk(json.access.groups.all.write); + assert.notProperty(json, 'name'); + assert.notProperty(json, 'dateAdded'); + assert.notProperty(json, 'lastUsed'); + assert.notProperty(json, 'recentIPs'); + }); +}); diff --git a/tests/remote_js/test/3/permissionTest.js b/tests/remote_js/test/3/permissionTest.js new file mode 100644 index 00000000..c98b2d01 --- /dev/null +++ b/tests/remote_js/test/3/permissionTest.js @@ -0,0 +1,371 @@ +const chai = require('chai'); +const assert = chai.assert; +const config = require("../../config.js"); +const API = require('../../api3.js'); +const Helpers = require('../../helpers3.js'); +const { API3Setup, API3WrapUp, resetGroups } = require("../shared.js"); + +describe('PermissionsTests', function () { + this.timeout(config.timeout); + + before(async function () { + await API3Setup(); + }); + + after(async function () { + await API3WrapUp(); + }); + + beforeEach(async function () { + await resetGroups(); + await API.resetKey(config.apiKey); + API.useAPIKey(config.apiKey); + await API.setKeyUserPermission(config.apiKey, 'library', true); + await API.setKeyUserPermission(config.apiKey, 'write', true); + await API.setKeyGroupPermission(config.apiKey, 0, 'write', true); + }); + + it('testUserGroupsAnonymousJSON', async function () { + API.useAPIKey(false); + const response = await API.get(`users/${config.userID}/groups`); + Helpers.assertStatusCode(response, 200); + + const json = API.getJSONFromResponse(response); + const groupIDs = json.map(obj => String(obj.id)); + assert.include(groupIDs, String(config.ownedPublicGroupID), `Owned public group ID ${config.ownedPublicGroupID} not found`); + assert.include(groupIDs, String(config.ownedPublicNoAnonymousGroupID), `Owned public no-anonymous group ID ${config.ownedPublicNoAnonymousGroupID} not found`); + Helpers.assertTotalResults(response, config.numPublicGroups); + }); + + it('testUserGroupsAnonymousAtom', async function () { + API.useAPIKey(false); + const response = await API.get(`users/${config.userID}/groups?content=json`); + Helpers.assertStatusCode(response, 200); + + const xml = API.getXMLFromResponse(response); + const groupIDs = Helpers.xpathEval(xml, '//atom:entry/zapi:groupID', false, true); + assert.include(groupIDs, String(config.ownedPublicGroupID), `Owned public group ID ${config.ownedPublicGroupID} not found`); + assert.include(groupIDs, String(config.ownedPublicNoAnonymousGroupID), `Owned public no-anonymous group ID ${config.ownedPublicNoAnonymousGroupID} not found`); + Helpers.assertTotalResults(response, config.numPublicGroups); + }); + + it('testKeyNoteAccessWriteError', async function () { + this.skip(); //disabled + }); + + it('testUserGroupsOwned', async function () { + API.useAPIKey(config.apiKey); + const response = await API.get( + "users/" + config.userID + "/groups" + ); + Helpers.assertStatusCode(response, 200); + + Helpers.assertTotalResults(response, config.numOwnedGroups); + Helpers.assertNumResults(response, config.numOwnedGroups); + }); + + it('testTagDeletePermissions', async function () { + await API.userClear(config.userID); + + await API.createItem('book', { + tags: [{ tag: 'A' }] + }, true); + + const libraryVersion = await API.getLibraryVersion(); + + await API.setKeyUserPermission( + config.apiKey, 'write', false + ); + + let response = await API.userDelete( + config.userID, + `tags?tag=A&key=${config.apiKey}`, + ); + Helpers.assertStatusCode(response, 403); + + await API.setKeyUserPermission( + config.apiKey, 'write', true + ); + + response = await API.userDelete( + config.userID, + `tags?tag=A&key=${config.apiKey}`, + { 'If-Unmodified-Since-Version': libraryVersion } + ); + Helpers.assertStatusCode(response, 204); + }); + + it('test_should_see_private_group_listed_when_using_key_with_library_read_access', async function () { + await API.resetKey(config.apiKey); + let response = await API.userGet(config.userID, "groups"); + Helpers.assert200(response); + Helpers.assertNumResults(response, config.numPublicGroups); + + // Grant key read permission to library + await API.setKeyGroupPermission( + config.apiKey, + config.ownedPrivateGroupID, + 'library', + true + ); + + response = await API.userGet(config.userID, "groups"); + Helpers.assertNumResults(response, config.numOwnedGroups); + Helpers.assertTotalResults(response, config.numOwnedGroups); + + const json = API.getJSONFromResponse(response); + const groupIDs = json.map(data => data.id); + assert.include(groupIDs, config.ownedPrivateGroupID); + }); + + + it('testGroupLibraryReading', async function () { + const groupID = config.ownedPublicNoAnonymousGroupID; + await API.groupClear(groupID); + + await API.groupCreateItem( + groupID, + 'book', + { + title: "Test" + }, + true + ); + + try { + await API.useAPIKey(config.apiKey); + let response = await API.groupGet(groupID, "items"); + Helpers.assert200(response); + Helpers.assertNumResults(response, 1); + + // An anonymous request should fail, because libraryReading is members + await API.useAPIKey(false); + response = await API.groupGet(groupID, "items"); + Helpers.assert403(response); + } + finally { + await API.groupClear(groupID); + } + }); + + + it('test_shouldnt_be_able_to_write_to_group_using_key_with_library_read_access', async function () { + await API.resetKey(config.apiKey); + + // Grant key read (not write) permission to library + await API.setKeyGroupPermission( + config.apiKey, + config.ownedPrivateGroupID, + 'library', + true + ); + + let response = await API.get("items/new?itemType=book"); + let json = JSON.parse(response.data); + + response = await API.groupPost( + config.ownedPrivateGroupID, + "items", + JSON.stringify({ + items: [json] + }), + { "Content-Type": "application/json" } + ); + Helpers.assert403(response); + }); + + + it('testKeyNoteAccess', async function () { + await API.userClear(config.userID); + + await API.setKeyUserPermission(config.apiKey, 'notes', true); + + let keys = []; + let topLevelKeys = []; + let bookKeys = []; + + const makeNoteItem = async (text) => { + const key = await API.createNoteItem(text, false, true, 'key'); + keys.push(key); + topLevelKeys.push(key); + }; + + const makeBookItem = async (title) => { + let key = await API.createItem('book', { title: title }, true, 'key'); + keys.push(key); + topLevelKeys.push(key); + bookKeys.push(key); + return key; + }; + + await makeBookItem("A"); + + await makeNoteItem("B"); + await makeNoteItem("C"); + await makeNoteItem("D"); + await makeNoteItem("E"); + + const lastKey = await makeBookItem("F"); + + let key = await API.createNoteItem("G", lastKey, true, 'key'); + keys.push(key); + + // Create collection and add items to it + let response = await API.userPost( + config.userID, + "collections", + JSON.stringify([ + { + name: "Test", + parentCollection: false + } + ]), + { "Content-Type": "application/json" } + ); + Helpers.assertStatusCode(response, 200); + let collectionKey = API.getFirstSuccessKeyFromResponse(response); + + response = await API.userPost( + config.userID, + `collections/${collectionKey}/items`, + topLevelKeys.join(" ") + ); + Helpers.assertStatusCode(response, 204); + + // + // format=atom + // + // Root + response = await API.userGet( + config.userID, "items" + ); + Helpers.assertNumResults(response, keys.length); + Helpers.assertTotalResults(response, keys.length); + + // Top + response = await API.userGet( + config.userID, "items/top" + ); + Helpers.assertNumResults(response, topLevelKeys.length); + Helpers.assertTotalResults(response, topLevelKeys.length); + + // Collection + response = await API.userGet( + config.userID, + "collections/" + collectionKey + "/items/top" + ); + Helpers.assertNumResults(response, topLevelKeys.length); + Helpers.assertTotalResults(response, topLevelKeys.length); + + // + // format=keys + // + // Root + response = await API.userGet( + config.userID, + "items?format=keys" + ); + Helpers.assertStatusCode(response, 200); + assert.equal(response.data.trim().split("\n").length, keys.length); + + // Top + response = await API.userGet( + config.userID, + "items/top?format=keys" + ); + Helpers.assertStatusCode(response, 200); + assert.equal(response.data.trim().split("\n").length, topLevelKeys.length); + + // Collection + response = await API.userGet( + config.userID, + "collections/" + collectionKey + "/items/top?format=keys" + ); + Helpers.assertStatusCode(response, 200); + assert.equal(response.data.trim().split("\n").length, topLevelKeys.length); + + // Remove notes privilege from key + await API.setKeyUserPermission(config.apiKey, "notes", false); + // + // format=json + // + // totalResults with limit + response = await API.userGet( + config.userID, + "items?limit=1" + ); + Helpers.assertNumResults(response, 1); + Helpers.assertTotalResults(response, bookKeys.length); + + // And without limit + response = await API.userGet( + config.userID, + "items" + ); + Helpers.assertNumResults(response, bookKeys.length); + Helpers.assertTotalResults(response, bookKeys.length); + + // Top + response = await API.userGet( + config.userID, + "items/top" + ); + Helpers.assertNumResults(response, bookKeys.length); + Helpers.assertTotalResults(response, bookKeys.length); + + // Collection + response = await API.userGet( + config.userID, + `collections/${collectionKey}/items` + ); + Helpers.assertNumResults(response, bookKeys.length); + Helpers.assertTotalResults(response, bookKeys.length); + + // + // format=atom + // + // totalResults with limit + response = await API.userGet( + config.userID, + "items?limit=1" + ); + Helpers.assertNumResults(response, 1); + Helpers.assertTotalResults(response, bookKeys.length); + + // And without limit + response = await API.userGet( + config.userID, + "items" + ); + Helpers.assertNumResults(response, bookKeys.length); + Helpers.assertTotalResults(response, bookKeys.length); + + // Top + response = await API.userGet( + config.userID, + "items/top" + ); + Helpers.assertNumResults(response, bookKeys.length); + Helpers.assertTotalResults(response, bookKeys.length); + + // Collection + response = await API.userGet( + config.userID, + "collections/" + collectionKey + "/items" + ); + Helpers.assertNumResults(response, bookKeys.length); + Helpers.assertTotalResults(response, bookKeys.length); + + // + // format=keys + // + response = await API.userGet( + config.userID, + "items?format=keys" + ); + keys = response.data.trim().split("\n"); + keys.sort(); + bookKeys.sort(); + assert.deepEqual(bookKeys, keys); + }); +}); diff --git a/tests/remote_js/test/3/publicationTest.js b/tests/remote_js/test/3/publicationTest.js index d0c7e88d..55b6f90b 100644 --- a/tests/remote_js/test/3/publicationTest.js +++ b/tests/remote_js/test/3/publicationTest.js @@ -3,11 +3,10 @@ const assert = chai.assert; const config = require("../../config.js"); const API = require('../../api3.js'); const Helpers = require('../../helpers3.js'); -const { API3Setup, API3WrapUp } = require("../shared.js"); +const { API3Setup, API3WrapUp, resetGroups } = require("../shared.js"); const { S3Client, DeleteObjectsCommand } = require("@aws-sdk/client-s3"); const HTTP = require('../../httpHandler.js'); const fs = require('fs'); -const { resetGroups } = require("../../groupsSetup.js"); const { JSDOM } = require("jsdom"); diff --git a/tests/remote_js/test/3/relationTest.js b/tests/remote_js/test/3/relationTest.js new file mode 100644 index 00000000..4b14c91e --- /dev/null +++ b/tests/remote_js/test/3/relationTest.js @@ -0,0 +1,374 @@ +const chai = require('chai'); +const assert = chai.assert; +const config = require("../../config.js"); +const API = require('../../api3.js'); +const Helpers = require('../../helpers3.js'); +const { API3Setup, API3WrapUp } = require("../shared.js"); + +describe('RelationsTests', function () { + this.timeout(config.timeout); + + before(async function () { + await API3Setup(); + }); + + after(async function () { + await API3WrapUp(); + }); + + it('testNewItemRelations', async function () { + const relations = { + "owl:sameAs": "http://zotero.org/groups/1/items/AAAAAAAA", + "dc:relation": [ + "http://zotero.org/users/" + config.userID + "/items/AAAAAAAA", + "http://zotero.org/users/" + config.userID + "/items/BBBBBBBB", + ] + }; + const json = await API.createItem("book", { relations }, true, 'jsonData'); + + assert.equal(Object.keys(relations).length, Object.keys(json.relations).length); + + for (const [predicate, object] of Object.entries(relations)) { + if (typeof object === "string") { + assert.equal(object, json.relations[predicate]); + } + else { + for (const rel of object) { + assert.include(json.relations[predicate], rel); + } + } + } + }); + + it('testRelatedItemRelations', async function () { + const relations = { + "owl:sameAs": "http://zotero.org/groups/1/items/AAAAAAAA" + }; + + const item1JSON = await API.createItem("book", { relations: relations }, true, 'jsonData'); + const item2JSON = await API.createItem("book", null, this, 'jsonData'); + + const uriPrefix = "http://zotero.org/users/" + config.userID + "/items/"; + const item1URI = uriPrefix + item1JSON.key; + const item2URI = uriPrefix + item2JSON.key; + + // Add item 2 as related item of item 1 + relations["dc:relation"] = item2URI; + item1JSON.relations = relations; + const response = await API.userPut( + config.userID, + "items/" + item1JSON.key, + JSON.stringify(item1JSON) + ); + Helpers.assertStatusCode(response, 204); + + // Make sure it exists on item 1 + const json = (await API.getItem(item1JSON.key, true, 'json')).data; + assert.equal(Object.keys(relations).length, Object.keys(json.relations).length); + for (const [predicate, object] of Object.entries(relations)) { + assert.equal(object, json.relations[predicate]); + } + + // And item 2, since related items are bidirectional + const item2JSON2 =(await API.getItem(item2JSON.key, true, 'json')).data; + assert.equal(1, Object.keys(item2JSON2.relations).length); + assert.equal(item1URI, item2JSON2.relations["dc:relation"]); + + // Sending item 2's unmodified JSON back up shouldn't cause the item to be updated. + // Even though we're sending a relation that's technically not part of the item, + // when it loads the item it will load the reverse relations too and therefore not + // add a relation that it thinks already exists. + const response2 = await API.userPut( + config.userID, + "items/" + item2JSON.key, + JSON.stringify(item2JSON2) + ); + Helpers.assertStatusCode(response2, 204); + assert.equal(parseInt(item2JSON2.version), response2.headers["last-modified-version"][0]); + }); + + it('testRelatedItemRelationsSingleRequest', async function () { + const uriPrefix = "http://zotero.org/users/" + config.userID + "/items/"; + const item1Key = Helpers.uniqueID(); + const item2Key = Helpers.uniqueID(); + const item1URI = uriPrefix + item1Key; + const item2URI = uriPrefix + item2Key; + + const item1JSON = await API.getItemTemplate('book'); + item1JSON.key = item1Key; + item1JSON.version = 0; + item1JSON.relations['dc:relation'] = item2URI; + const item2JSON = await API.getItemTemplate('book'); + item2JSON.key = item2Key; + item2JSON.version = 0; + + const response = await API.postItems([item1JSON, item2JSON]); + Helpers.assertStatusCode(response, 200); + + // Make sure it exists on item 1 + const parsedJson = (await API.getItem(item1JSON.key, true, 'json')).data; + + assert.lengthOf(Object.keys(parsedJson.relations), 1); + assert.equal(parsedJson.relations['dc:relation'], item2URI); + + // And item 2, since related items are bidirectional + const parsedJson2 = (await API.getItem(item2JSON.key, true, 'json')).data; + assert.lengthOf(Object.keys(parsedJson2.relations), 1); + assert.equal(parsedJson2.relations['dc:relation'], item1URI); + }); + + it('testInvalidItemRelation', async function () { + let response = await API.createItem('book', { + relations: { + 'foo:unknown': 'http://zotero.org/groups/1/items/AAAAAAAA' + } + }, true, 'response'); + + Helpers.assertStatusForObject(response, 'failed', 0, 400, "Unsupported predicate 'foo:unknown'"); + + response = await API.createItem('book', { + relations: { + 'owl:sameAs': 'Not a URI' + } + }, this, 'response'); + + Helpers.assertStatusForObject(response, 'failed', 0, 400, "'relations' values currently must be Zotero item URIs"); + + response = await API.createItem('book', { + relations: { + 'owl:sameAs': ['Not a URI'] + } + }, this, 'response'); + + Helpers.assertStatusForObject(response, 'failed', 0, 400, "'relations' values currently must be Zotero item URIs"); + }); + + + it('test_should_add_a_URL_from_a_relation_with_PATCH', async function () { + const relations = { + "dc:replaces": [ + `http://zotero.org/users/${config.userID}/items/AAAAAAAA` + ] + }; + + let itemJSON = await API.createItem("book", { + relations: relations + }, true, 'jsonData'); + + relations["dc:replaces"].push(`http://zotero.org/users/${config.userID}/items/BBBBBBBB`); + + const patchJSON = { + version: itemJSON.version, + relations: relations + }; + const response = await API.userPatch( + config.userID, + `items/${itemJSON.key}`, + JSON.stringify(patchJSON) + ); + Helpers.assert204(response); + + // Make sure the value (now a string) was updated + itemJSON = (await API.getItem(itemJSON.key, true, 'json')).data; + Helpers.assertCount(Object.keys(relations).length, itemJSON.relations); + Helpers.assertCount(Object.keys(relations['dc:replaces']).length, itemJSON.relations['dc:replaces']); + assert.include(itemJSON.relations['dc:replaces'], relations['dc:replaces'][0]); + assert.include(itemJSON.relations['dc:replaces'], relations['dc:replaces'][1]); + }); + + it('test_should_remove_a_URL_from_a_relation_with_PATCH', async function () { + const relations = { + "dc:replaces": [ + `http://zotero.org/users/${config.userID}/items/AAAAAAAA`, + `http://zotero.org/users/${config.userID}/items/BBBBBBBB` + ] + }; + + let itemJSON = await API.createItem("book", { + relations: relations + }, true, 'jsonData'); + + relations["dc:replaces"] = relations["dc:replaces"].slice(0, 1); + + const patchJSON = { + version: itemJSON.version, + relations: relations + }; + const response = await API.userPatch( + config.userID, + `items/${itemJSON.key}`, + JSON.stringify(patchJSON) + ); + Helpers.assert204(response); + + // Make sure the value (now a string) was updated + itemJSON = (await API.getItem(itemJSON.key, true, 'json')).data; + assert.equal(relations['dc:replaces'][0], itemJSON.relations['dc:replaces']); + }); + + + it('testDeleteItemRelation', async function () { + const relations = { + "owl:sameAs": [ + "http://zotero.org/groups/1/items/AAAAAAAA", + "http://zotero.org/groups/1/items/BBBBBBBB" + ], + "dc:relation": "http://zotero.org/users/" + config.userID + + "/items/AAAAAAAA" + }; + + let data = await API.createItem("book", { + relations: relations + }, true, 'jsonData'); + + let itemKey = data.key; + + // Remove a relation + data.relations['owl:sameAs'] = relations['owl:sameAs'] = relations['owl:sameAs'][0]; + const response = await API.userPut( + config.userID, + "items/" + itemKey, + JSON.stringify(data) + ); + Helpers.assertStatusCode(response, 204); + + // Make sure it's gone + data = (await API.getItem(data.key, true, 'json')).data; + + assert.equal(Object.keys(relations).length, Object.keys(data.relations).length); + for (const [predicate, object] of Object.entries(relations)) { + assert.deepEqual(object, data.relations[predicate]); + } + + // Delete all + data.relations = {}; + const deleteResponse = await API.userPut( + config.userID, + "items/" + data.key, + JSON.stringify(data) + ); + Helpers.assertStatusCode(deleteResponse, 204); + + // Make sure they're gone + data = (await API.getItem(itemKey, true, 'json')).data; + assert.lengthOf(Object.keys(data.relations), 0); + }); + + it('testCircularItemRelations', async function () { + const item1Data = await API.createItem("book", {}, true, 'jsonData'); + const item2Data = await API.createItem("book", {}, true, 'jsonData'); + + item1Data.relations = { + 'dc:relation': `http://zotero.org/users/${config.userID}/items/${item2Data.key}` + }; + item2Data.relations = { + 'dc:relation': `http://zotero.org/users/${config.userID}/items/${item1Data.key}` + }; + const response = await API.postItems([item1Data, item2Data]); + Helpers.assert200(response); + Helpers.assertUnchangedForObject(response, { index: 1 }); + }); + + + it('testNewCollectionRelations', async function () { + const relationsObj = { + "owl:sameAs": "http://zotero.org/groups/1/collections/AAAAAAAA" + }; + const data = await API.createCollection("Test", + { relations: relationsObj }, true, 'jsonData'); + assert.equal(Object.keys(relationsObj).length, Object.keys(data.relations).length); + for (const [predicate, object] of Object.entries(relationsObj)) { + assert.equal(object, data.relations[predicate]); + } + }); + + it('testInvalidCollectionRelation', async function () { + const json = { + name: "Test", + relations: { + "foo:unknown": "http://zotero.org/groups/1/collections/AAAAAAAA" + } + }; + const response = await API.userPost( + config.userID, + "collections", + JSON.stringify([json]) + ); + Helpers.assertStatusForObject(response, 'failed', 0, null, "Unsupported predicate 'foo:unknown'"); + + json.relations = { + "owl:sameAs": "Not a URI" + }; + const response2 = await API.userPost( + config.userID, + "collections", + JSON.stringify([json]) + ); + Helpers.assertStatusForObject(response2, 'failed', 0, null, "'relations' values currently must be Zotero collection URIs"); + + json.relations = ["http://zotero.org/groups/1/collections/AAAAAAAA"]; + const response3 = await API.userPost( + config.userID, + "collections", + JSON.stringify([json]) + ); + Helpers.assertStatusForObject(response3, 'failed', 0, null, "'relations' property must be an object"); + }); + + it('testDeleteCollectionRelation', async function () { + const relations = { + "owl:sameAs": "http://zotero.org/groups/1/collections/AAAAAAAA" + }; + let data = await API.createCollection("Test", { + relations: relations + }, true, 'jsonData'); + + // Remove all relations + data.relations = {}; + delete relations['owl:sameAs']; + const response = await API.userPut( + config.userID, + `collections/${data.key}`, + JSON.stringify(data) + ); + Helpers.assertStatusCode(response, 204); + + // Make sure it's gone + data = (await API.getCollection(data.key, true, 'json')).data; + assert.equal(Object.keys(data.relations).length, Object.keys(relations).length); + for (const key in relations) { + assert.equal(data.relations[key], relations[key]); + } + }); + + it('test_should_return_200_for_values_for_mendeleyDB_collection_relation', async function () { + const relations = { + "mendeleyDB:remoteFolderUUID": "b95b84b9-8b27-4a55-b5ea-5b98c1cac205" + }; + const data = await API.createCollection( + "Test", + { + relations: relations + }, + true, + 'jsonData' + ); + assert.equal(relations['mendeleyDB:remoteFolderUUID'], data.relations['mendeleyDB:remoteFolderUUID']); + }); + + + it('test_should_return_200_for_arrays_for_mendeleyDB_collection_relation', async function () { + const json = { + name: "Test", + relations: { + "mendeleyDB:remoteFolderUUID": ["b95b84b9-8b27-4a55-b5ea-5b98c1cac205"] + } + }; + const response = await API.userPost( + config.userID, + "collections", + JSON.stringify([json]) + ); + Helpers.assert200(response); + }); +}); diff --git a/tests/remote_js/test/3/searchTest.js b/tests/remote_js/test/3/searchTest.js new file mode 100644 index 00000000..b94181a6 --- /dev/null +++ b/tests/remote_js/test/3/searchTest.js @@ -0,0 +1,296 @@ +const chai = require('chai'); +const assert = chai.assert; +const config = require("../../config.js"); +const API = require('../../api3.js'); +const Helpers = require('../../helpers3.js'); +const { API3Setup, API3WrapUp } = require("../shared.js"); + +describe('SearchTests', function () { + this.timeout(config.timeout); + + before(async function () { + await API3Setup(); + }); + + after(async function () { + await API3WrapUp(); + }); + const testNewSearch = async () => { + let name = "Test Search"; + let conditions = [ + { + condition: "title", + operator: "contains", + value: "test" + }, + { + condition: "noChildren", + operator: "false", + value: "" + }, + { + condition: "fulltextContent/regexp", + operator: "contains", + value: "/test/" + } + ]; + + // DEBUG: Should fail with no version? + let response = await API.userPost( + config.userID, + "searches", + JSON.stringify([{ + name: name, + conditions: conditions + }]), + { "Content-Type": "application/json" } + ); + Helpers.assert200(response); + let libraryVersion = response.headers["last-modified-version"][0]; + let json = API.getJSONFromResponse(response); + assert.equal(Object.keys(json.successful).length, 1); + // Deprecated + assert.equal(Object.keys(json.success).length, 1); + + // Check data in write response + let data = json.successful[0].data; + assert.equal(json.successful[0].key, data.key); + assert.equal(libraryVersion, data.version); + assert.equal(libraryVersion, data.version); + assert.equal(name, data.name); + assert.isArray(data.conditions); + assert.equal(conditions.length, data.conditions.length); + for (let i = 0; i < conditions.length; i++) { + for (let key in conditions[i]) { + assert.equal(conditions[i][key], data.conditions[i][key]); + } + } + + + // Check in separate request, to be safe + let keys = Object.keys(json.successful).map(i => json.successful[i].key); + response = await API.getSearchResponse(keys); + Helpers.assertTotalResults(response, 1); + json = API.getJSONFromResponse(response); + data = json[0].data; + assert.equal(name, data.name); + assert.isArray(data.conditions); + assert.equal(conditions.length, data.conditions.length); + + for (let i = 0; i < conditions.length; i++) { + for (let key in conditions[i]) { + assert.equal(conditions[i][key], data.conditions[i][key]); + } + } + + return data; + }; + + it('testEditMultipleSearches', async function () { + const search1Name = "Test 1"; + const search1Conditions = [ + { + condition: "title", + operator: "contains", + value: "test" + } + ]; + let search1Data = await API.createSearch(search1Name, search1Conditions, this, 'jsonData'); + const search1NewName = "Test 1 Modified"; + + const search2Name = "Test 2"; + const search2Conditions = [ + { + condition: "title", + operator: "is", + value: "test2" + } + ]; + let search2Data = await API.createSearch(search2Name, search2Conditions, this, 'jsonData'); + const search2NewConditions = [ + { + condition: "title", + operator: "isNot", + value: "test1" + } + ]; + + const response = await API.userPost( + config.userID, + "searches", + JSON.stringify([ + { + key: search1Data.key, + version: search1Data.version, + name: search1NewName + }, + { + key: search2Data.key, + version: search2Data.version, + conditions: search2NewConditions + } + ]), + { + "Content-Type": "application/json" + } + ); + Helpers.assert200(response); + const libraryVersion = response.headers["last-modified-version"][0]; + const json = API.getJSONFromResponse(response); + assert.equal(Object.keys(json.successful).length, 2); + assert.equal(Object.keys(json.success).length, 2); + + // Check data in write response + assert.equal(json.successful[0].key, json.successful[0].data.key); + assert.equal(json.successful[1].key, json.successful[1].data.key); + assert.equal(libraryVersion, json.successful[0].version); + assert.equal(libraryVersion, json.successful[1].version); + assert.equal(libraryVersion, json.successful[0].data.version); + assert.equal(libraryVersion, json.successful[1].data.version); + assert.equal(search1NewName, json.successful[0].data.name); + assert.equal(search2Name, json.successful[1].data.name); + assert.deepEqual(search1Conditions, json.successful[0].data.conditions); + assert.deepEqual(search2NewConditions, json.successful[1].data.conditions); + + // Check in separate request, to be safe + const keys = Object.keys(json.successful).map(i => json.successful[i].key); + const response2 = await API.getSearchResponse(keys); + Helpers.assertTotalResults(response2, 2); + const json2 = API.getJSONFromResponse(response2); + // POST follows PATCH behavior, so unspecified values shouldn't change + assert.equal(search1NewName, json2[0].data.name); + assert.deepEqual(search1Conditions, json2[0].data.conditions); + assert.equal(search2Name, json2[1].data.name); + assert.deepEqual(search2NewConditions, json2[1].data.conditions); + }); + + + it('testModifySearch', async function () { + let searchJson = await testNewSearch(); + + // Remove one search condition + searchJson.conditions.shift(); + + const name = searchJson.name; + const conditions = searchJson.conditions; + + let response = await API.userPut( + config.userID, + `searches/${searchJson.key}`, + JSON.stringify(searchJson), + { + "Content-Type": "application/json", + "If-Unmodified-Since-Version": searchJson.version + } + ); + + Helpers.assertStatusCode(response, 204); + + searchJson = (await API.getSearch(searchJson.key, true, 'json')).data; + + assert.equal(name, searchJson.name); + assert.isArray(searchJson.conditions); + assert.equal(conditions.length, searchJson.conditions.length); + + for (let i = 0; i < conditions.length; i++) { + const condition = conditions[i]; + assert.equal(condition.field, searchJson.conditions[i].field); + assert.equal(condition.operator, searchJson.conditions[i].operator); + assert.equal(condition.value, searchJson.conditions[i].value); + } + }); + + it('testNewSearchNoName', async function () { + const conditions = [ + { + condition: 'title', + operator: 'contains', + value: 'test', + }, + ]; + const headers = { + 'Content-Type': 'application/json', + }; + const response = await API.createSearch('', conditions, headers, 'responseJSON'); + Helpers.assertStatusForObject(response, 'failed', 0, 400, 'Search name cannot be empty'); + }); + + it('testNewSearchNoConditions', async function () { + const json = await API.createSearch("Test", [], true, 'responseJSON'); + Helpers.assertStatusForObject(json, 'failed', 0, 400, "'conditions' cannot be empty"); + }); + + it('testNewSearchConditionErrors', async function () { + let json = await API.createSearch( + 'Test', + [ + { + operator: 'contains', + value: 'test' + } + ], + true, + 'responseJSON' + ); + Helpers.assertStatusForObject(json, 'failed', 0, 400, "'condition' property not provided for search condition"); + + + json = await API.createSearch( + 'Test', + [ + { + condition: '', + operator: 'contains', + value: 'test' + } + ], + true, + 'responseJSON' + ); + Helpers.assertStatusForObject(json, 'failed', 0, 400, 'Search condition cannot be empty'); + + + json = await API.createSearch( + 'Test', + [ + { + condition: 'title', + value: 'test' + } + ], + true, + 'responseJSON' + ); + Helpers.assertStatusForObject(json, 'failed', 0, 400, "'operator' property not provided for search condition"); + + + json = await API.createSearch( + 'Test', + [ + { + condition: 'title', + operator: '', + value: 'test' + } + ], + true, + 'responseJSON' + ); + Helpers.assertStatusForObject(json, 'failed', 0, 400, 'Search operator cannot be empty'); + }); + it('test_should_allow_a_search_with_emoji_values', async function () { + let response = await API.createSearch( + "🐶", // 4-byte character + [ + { + condition: "title", + operator: "contains", + value: "🐶" // 4-byte character + } + ], + true, + 'responseJSON' + ); + Helpers.assert200ForObject(response); + }); +}); diff --git a/tests/remote_js/test/3/settingsTest.js b/tests/remote_js/test/3/settingsTest.js new file mode 100644 index 00000000..547af276 --- /dev/null +++ b/tests/remote_js/test/3/settingsTest.js @@ -0,0 +1,665 @@ +const chai = require('chai'); +const assert = chai.assert; +const config = require("../../config.js"); +const API = require('../../api3.js'); +const Helpers = require('../../helpers3.js'); +const { API3Setup, API3WrapUp, resetGroups } = require("../shared.js"); + +describe('SettingsTests', function () { + this.timeout(config.timeout); + + before(async function () { + await API3Setup(); + await resetGroups(); + }); + + after(async function () { + await API3WrapUp(); + await resetGroups(); + }); + + beforeEach(async function () { + await API.userClear(config.userID); + await API.groupClear(config.ownedPrivateGroupID); + }); + + it('testAddUserSetting', async function () { + const settingKey = "tagColors"; + const value = [ + { + name: "_READ", + color: "#990000" + } + ]; + + const libraryVersion = await API.getLibraryVersion(); + + const json = { + value: value + }; + + // No version + let response = await API.userPut( + config.userID, + `settings/${settingKey}`, + JSON.stringify(json), + { "Content-Type": "application/json" } + ); + Helpers.assertStatusCode(response, 428); + + // Version must be 0 for non-existent setting + response = await API.userPut( + config.userID, + `settings/${settingKey}`, + JSON.stringify(json), + { + "Content-Type": "application/json", + "If-Unmodified-Since-Version": "1" + } + ); + Helpers.assertStatusCode(response, 412); + + // Create + response = await API.userPut( + config.userID, + `settings/${settingKey}`, + JSON.stringify(json), + { + "Content-Type": "application/json", + "If-Unmodified-Since-Version": "0" + } + ); + Helpers.assertStatusCode(response, 204); + + // Multi-object GET + response = await API.userGet( + config.userID, + `settings` + ); + Helpers.assertStatusCode(response, 200); + assert.equal(response.headers['content-type'][0], "application/json"); + let jsonResponse = JSON.parse(response.data); + + assert.property(jsonResponse, settingKey); + assert.deepEqual(value, jsonResponse[settingKey].value); + assert.equal(parseInt(libraryVersion) + 1, jsonResponse[settingKey].version); + + // Single-object GET + response = await API.userGet( + config.userID, + `settings/${settingKey}` + ); + Helpers.assertStatusCode(response, 200); + assert.equal(response.headers['content-type'][0], "application/json"); + jsonResponse = JSON.parse(response.data); + + assert.deepEqual(value, jsonResponse.value); + assert.equal(parseInt(libraryVersion) + 1, jsonResponse.version); + }); + + it('testAddUserSettingMultiple', async function () { + await API.userClear(config.userID); + let json = { + tagColors: { + value: [ + { + name: "_READ", + color: "#990000" + } + ] + }, + feeds: { + value: { + "http://www.nytimes.com/services/xml/rss/nyt/HomePage.xml": { + url: "http://www.nytimes.com/services/xml/rss/nyt/HomePage.xml", + name: "NYT > Home Page", + cleanupAfter: 2, + refreshInterval: 60 + } + } + }, + // eslint-disable-next-line camelcase + lastPageIndex_u_ABCD2345: { + value: 123 + }, + // eslint-disable-next-line camelcase + lastPageIndex_g1234567890_ABCD2345: { + value: 123 + }, + // eslint-disable-next-line camelcase + lastRead_g1234567890_ABCD2345: { + value: 1674251397 + } + }; + + let settingsKeys = Object.keys(json); + let libraryVersion = parseInt(await API.getLibraryVersion()); + + let response = await API.userPost( + config.userID, + `settings`, + JSON.stringify(json), + { 'Content-Type': 'application/json' } + ); + Helpers.assertStatusCode(response, 204); + assert.equal(++libraryVersion, response.headers['last-modified-version']); + + // Multi-object GET + const multiObjResponse = await API.userGet( + config.userID, + `settings` + ); + Helpers.assertStatusCode(multiObjResponse, 200); + + assert.equal(multiObjResponse.headers['content-type'][0], 'application/json'); + const multiObjJson = JSON.parse(multiObjResponse.data); + for (let settingsKey of settingsKeys) { + assert.property(multiObjJson, settingsKey); + assert.deepEqual(multiObjJson[settingsKey].value, json[settingsKey].value); + assert.equal(multiObjJson[settingsKey].version, parseInt(libraryVersion)); + } + + // Single-object GET + for (let settingsKey of settingsKeys) { + response = await API.userGet( + config.userID, + `settings/${settingsKey}` + ); + + Helpers.assertStatusCode(response, 200); + Helpers.assertContentType(response, 'application/json'); + const singleObjJson = JSON.parse(response.data); + assert.exists(singleObjJson); + assert.deepEqual(json[settingsKey].value, singleObjJson.value); + assert.equal(singleObjJson.version, libraryVersion); + } + }); + + it('testAddGroupSettingMultiple', async function () { + const settingKey = "tagColors"; + const value = [ + { + name: "_READ", + color: "#990000" + } + ]; + + // TODO: multiple, once more settings are supported + + const groupID = config.ownedPrivateGroupID; + const libraryVersion = await API.getGroupLibraryVersion(groupID); + + const json = { + [settingKey]: { + value: value + } + }; + + const response = await API.groupPost( + groupID, + `settings`, + JSON.stringify(json), + { "Content-Type": "application/json" } + ); + + Helpers.assertStatusCode(response, 204); + + // Multi-object GET + const response2 = await API.groupGet( + groupID, + `settings` + ); + + Helpers.assertStatusCode(response2, 200); + assert.equal(response2.headers['content-type'][0], "application/json"); + const json2 = JSON.parse(response2.data); + assert.exists(json2); + assert.property(json2, settingKey); + assert.deepEqual(value, json2[settingKey].value); + assert.equal(parseInt(libraryVersion) + 1, json2[settingKey].version); + + // Single-object GET + const response3 = await API.groupGet( + groupID, + `settings/${settingKey}` + ); + + Helpers.assertStatusCode(response3, 200); + assert.equal(response3.headers['content-type'][0], "application/json"); + const json3 = JSON.parse(response3.data); + assert.exists(json3); + assert.deepEqual(value, json3.value); + assert.equal(parseInt(libraryVersion) + 1, json3.version); + }); + + it('testDeleteNonexistentSetting', async function () { + const response = await API.userDelete(config.userID, + `settings/nonexistentSetting`, + { "If-Unmodified-Since-Version": "0" }); + Helpers.assertStatusCode(response, 404); + }); + + it('testUnsupportedSetting', async function () { + const settingKey = "unsupportedSetting"; + let value = true; + + const json = { + value: value, + version: 0 + }; + + const response = await API.userPut( + config.userID, + `settings/${settingKey}`, + JSON.stringify(json), + { "Content-Type": "application/json" } + ); + Helpers.assertStatusCode(response, 400, `Invalid setting '${settingKey}'`); + }); + + it('testUpdateUserSetting', async function () { + let settingKey = "tagColors"; + let value = [ + { + name: "_READ", + color: "#990000" + } + ]; + + let libraryVersion = await API.getLibraryVersion(); + + let json = { + value: value, + version: 0 + }; + + // Create + let response = await API.userPut( + config.userID, + `settings/${settingKey}`, + JSON.stringify(json), + { + "Content-Type": "application/json" + } + ); + Helpers.assert204(response); + + // Check + response = await API.userGet( + config.userID, + `settings/${settingKey}` + ); + Helpers.assert200(response); + Helpers.assertContentType(response, "application/json"); + json = JSON.parse(response.data); + assert.isNotNull(json); + assert.deepEqual(json.value, value); + assert.equal(parseInt(json.version), parseInt(libraryVersion) + 1); + + // Update with no change + response = await API.userPut( + config.userID, + `settings/${settingKey}`, + JSON.stringify(json), + { + "Content-Type": "application/json" + } + ); + Helpers.assert204(response); + + // Check + response = await API.userGet( + config.userID, + `settings/${settingKey}` + ); + Helpers.assert200(response); + Helpers.assertContentType(response, "application/json"); + json = JSON.parse(response.data); + assert.isNotNull(json); + assert.deepEqual(json.value, value); + assert.equal(parseInt(json.version), parseInt(libraryVersion) + 1); + + let newValue = [ + { + name: "_READ", + color: "#CC9933" + } + ]; + json.value = newValue; + + // Update, no change + response = await API.userPut( + config.userID, + `settings/${settingKey}`, + JSON.stringify(json), + { + "Content-Type": "application/json" + } + ); + Helpers.assert204(response); + + // Check + response = await API.userGet( + config.userID, + `settings/${settingKey}` + ); + Helpers.assert200(response); + Helpers.assertContentType(response, "application/json"); + json = JSON.parse(response.data); + assert.isNotNull(json); + assert.deepEqual(json.value, newValue); + assert.equal(parseInt(json.version), parseInt(libraryVersion) + 2); + }); + + it('testUpdateUserSettings', async function () { + let settingKey = "tagColors"; + let value = [ + { + name: "_READ", + color: "#990000" + } + ]; + + let libraryVersion = parseInt(await API.getLibraryVersion()); + + let json = { + value: value, + version: 0 + }; + + // Create + let response = await API.userPut( + config.userID, + `settings/${settingKey}`, + JSON.stringify(json), + { + "Content-Type": "application/json" + } + ); + Helpers.assert204(response); + assert.equal(parseInt(response.headers['last-modified-version'][0]), ++libraryVersion); + + // Check + response = await API.userGet( + config.userID, + `settings` + ); + Helpers.assert200(response); + Helpers.assertContentType(response, "application/json"); + assert.equal(parseInt(response.headers['last-modified-version'][0]), libraryVersion); + json = JSON.parse(response.data); + assert.isNotNull(json); + assert.deepEqual(json[settingKey].value, value); + assert.equal(parseInt(json[settingKey].version), libraryVersion); + + // Update with no change + response = await API.userPost( + config.userID, + `settings`, + JSON.stringify({ + [settingKey]: { + value: value + } + }), + { + "Content-Type": "application/json", + "If-Unmodified-Since-Version": libraryVersion + } + ); + + Helpers.assert204(response); + assert.equal(parseInt(response.headers['last-modified-version'][0]), libraryVersion); + // Check + response = await API.userGet( + config.userID, + `settings` + ); + Helpers.assert200(response); + Helpers.assertContentType(response, "application/json"); + assert.equal(parseInt(response.headers['last-modified-version'][0]), libraryVersion); + json = JSON.parse(response.data); + assert.isNotNull(json); + assert.deepEqual(json[settingKey].value, value); + assert.equal(parseInt(json[settingKey].version), libraryVersion); + + let newValue = [ + { + name: "_READ", + color: "#CC9933" + } + ]; + + // Update + response = await API.userPost( + config.userID, + `settings`, + JSON.stringify({ + [settingKey]: { + value: newValue + } + }), + { + "Content-Type": "application/json", + "If-Unmodified-Since-Version": libraryVersion + } + ); + Helpers.assert204(response); + assert.equal(parseInt(response.headers['last-modified-version'][0]), ++libraryVersion); + // Check + response = await API.userGet( + config.userID, + `settings/${settingKey}` + ); + Helpers.assert200(response); + Helpers.assertContentType(response, "application/json"); + assert.equal(parseInt(response.headers['last-modified-version'][0]), libraryVersion); + json = JSON.parse(response.data); + assert.isNotNull(json); + assert.deepEqual(json.value, newValue); + assert.equal(parseInt(json.version), libraryVersion); + }); + + it('testUnsupportedSettingMultiple', async function () { + const settingKey = 'unsupportedSetting'; + const json = { + tagColors: { + value: { + name: '_READ', + color: '#990000' + }, + version: 0 + }, + [settingKey]: { + value: false, + version: 0 + } + }; + + const libraryVersion = await API.getLibraryVersion(); + + let response = await API.userPut( + config.userID, + `settings/${settingKey}`, + JSON.stringify(json), + { 'Content-Type': 'application/json' } + ); + Helpers.assertStatusCode(response, 400); + + // Valid setting shouldn't exist, and library version should be unchanged + response = await API.userGet( + config.userID, + `settings/${settingKey}` + ); + Helpers.assertStatusCode(response, 404); + assert.equal(libraryVersion, await API.getLibraryVersion()); + }); + + it('testOverlongSetting', async function () { + const settingKey = "tagColors"; + const value = [ + { + name: "abcdefghij".repeat(3001), + color: "#990000" + } + ]; + + const json = { + value: value, + version: 0 + }; + + const response = await API.userPut( + config.userID, + `settings/${settingKey}`, + JSON.stringify(json), + { "Content-Type": "application/json" } + ); + Helpers.assertStatusCode(response, 400, "'value' cannot be longer than 30000 characters"); + }); + + it('testDeleteUserSetting', async function () { + let settingKey = "tagColors"; + let value = [ + { + name: "_READ", + color: "#990000" + } + ]; + + let json = { + value: value, + version: 0 + }; + + let libraryVersion = parseInt(await API.getLibraryVersion()); + + // Create + let response = await API.userPut( + config.userID, + `settings/${settingKey}`, + JSON.stringify(json), + { + "Content-Type": "application/json" + } + ); + Helpers.assert204(response); + + // Delete + response = await API.userDelete( + config.userID, + `settings/${settingKey}`, + { + "If-Unmodified-Since-Version": `${libraryVersion + 1}` + } + ); + Helpers.assert204(response); + + // Check + response = await API.userGet( + config.userID, + `settings/${settingKey}` + ); + Helpers.assert404(response); + + assert.equal(libraryVersion + 2, await API.getLibraryVersion()); + }); + + it('testSettingsSince', async function () { + let libraryVersion1 = parseInt(await API.getLibraryVersion()); + let response = await API.userPost( + config.userID, + "settings", + JSON.stringify({ + tagColors: { + value: [ + { + name: "_READ", + color: "#990000" + } + ] + } + }) + ); + Helpers.assert204(response); + let libraryVersion2 = response.headers['last-modified-version'][0]; + + response = await API.userPost( + config.userID, + "settings", + JSON.stringify({ + feeds: { + value: { + "http://www.nytimes.com/services/xml/rss/nyt/HomePage.xml": { + url: "http://www.nytimes.com/services/xml/rss/nyt/HomePage.xml", + name: "NYT > Home Page", + cleanupAfter: 2, + refreshInterval: 60 + } + } + } + }) + ); + Helpers.assert204(response); + let libraryVersion3 = response.headers['last-modified-version'][0]; + + response = await API.userGet( + config.userID, + `settings?since=${libraryVersion1}` + ); + Helpers.assertNumResults(response, 2); + + response = await API.userGet( + config.userID, + `settings?since=${libraryVersion2}` + ); + Helpers.assertNumResults(response, 1); + + response = await API.userGet( + config.userID, + `settings?since=${libraryVersion3}` + ); + Helpers.assertNumResults(response, 0); + }); + + it('test_should_reject_lastPageLabel_in_group_library', async function () { + const settingKey = `lastPageIndex_g${config.ownedPrivateGroupID}_ABCD2345`; + const value = 1234; + + const json = { + value: value, + version: 0 + }; + + + const response = await API.groupPut( + config.ownedPrivateGroupID, + `settings/${settingKey}`, + JSON.stringify(json), + { "Content-Type": "application/json" } + ); + Helpers.assert400(response, "lastPageIndex can only be set in user library"); + }); + + it('test_should_allow_emoji_character', async function () { + const settingKey = 'tagColors'; + const value = [ + { + name: "🐶", + color: "#990000" + } + ]; + + const json = { + value: value, + version: 0 + }; + + const response = await API.userPut( + config.userID, + `settings/${settingKey}`, + JSON.stringify(json), + { "Content-Type": "application/json" } + ); + Helpers.assert204(response); + }); +}); diff --git a/tests/remote_js/test/3/sortTest.js b/tests/remote_js/test/3/sortTest.js new file mode 100644 index 00000000..30c4f517 --- /dev/null +++ b/tests/remote_js/test/3/sortTest.js @@ -0,0 +1,418 @@ +const chai = require('chai'); +const assert = chai.assert; +const config = require("../../config.js"); +const API = require('../../api3.js'); +const Helpers = require('../../helpers3.js'); +const { API3Setup, API3WrapUp } = require("../shared.js"); + +describe('SortTests', function () { + this.timeout(config.timeout); + //let collectionKeys = []; + let itemKeys = []; + let childAttachmentKeys = []; + let childNoteKeys = []; + //let searchKeys = []; + + let titles = ['q', 'c', 'a', 'j', 'e', 'h', 'i']; + let names = ['m', 's', 'a', 'bb', 'ba', '', '']; + let attachmentTitles = ['v', 'x', null, 'a', null]; + let notes = [null, 'aaa', null, null, 'taf']; + + before(async function () { + await API3Setup(); + await setup(); + }); + + after(async function () { + await API3WrapUp(); + }); + + const setup = async () => { + let titleIndex = 0; + for (let i = 0; i < titles.length - 2; i++) { + const key = await API.createItem("book", { + title: titles[titleIndex], + creators: [ + { + creatorType: "author", + name: names[i] + } + ] + }, true, 'key'); + titleIndex += 1; + // Child attachments + if (attachmentTitles[i]) { + childAttachmentKeys.push(await API.createAttachmentItem( + "imported_file", { + title: attachmentTitles[i] + }, key, true, 'key')); + } + // Child notes + if (notes[i]) { + childNoteKeys.push(await API.createNoteItem(notes[i], key, true, 'key')); + } + + itemKeys.push(key); + } + // Top-level attachment + itemKeys.push(await API.createAttachmentItem("imported_file", { + title: titles[titleIndex] + }, false, null, 'key')); + titleIndex += 1; + // Top-level note + itemKeys.push(await API.createNoteItem(titles[titleIndex], false, null, 'key')); + // + // Collections + // + /*for (let i=0; i<5; i++) { + collectionKeys.push(await API.createCollection("Test", false, true, 'key')); + }*/ + + // + // Searches + // + /*for (let i=0; i<5; i++) { + searchKeys.push(await API.createSearch("Test", 'default', null, 'key')); + }*/ + }; + + it('testSortTopItemsTitle', async function () { + let response = await API.userGet( + config.userID, + "items/top?format=keys&sort=title" + ); + Helpers.assertStatusCode(response, 200); + + let keys = response.data.trim().split("\n"); + + let titlesToIndex = {}; + titles.forEach((v, i) => { + titlesToIndex[v] = i; + }); + let titlesSorted = [...titles]; + titlesSorted.sort(); + let correct = {}; + titlesSorted.forEach((title) => { + let index = titlesToIndex[title]; + correct[index] = keys[index]; + }); + correct = Object.keys(correct).map(key => correct[key]); + assert.deepEqual(correct, keys); + }); + + it('testSortTopItemsTitleOrder', async function () { + let response = await API.userGet( + config.userID, + "items/top?format=keys&order=title" + ); + Helpers.assertStatusCode(response, 200); + + let keys = response.data.trim().split("\n"); + + let titlesToIndex = {}; + titles.forEach((v, i) => { + titlesToIndex[v] = i; + }); + let titlesSorted = [...titles]; + titlesSorted.sort(); + let correct = {}; + titlesSorted.forEach((title) => { + let index = titlesToIndex[title]; + correct[index] = keys[index]; + }); + correct = Object.keys(correct).map(key => correct[key]); + assert.deepEqual(correct, keys); + }); + + it('testSortTopItemsCreator', async function () { + let response = await API.userGet( + config.userID, + "items/top?format=keys&sort=creator" + ); + Helpers.assertStatusCode(response, 200); + let keys = response.data.trim().split("\n"); + let namesCopy = { ...names }; + let sortFunction = function (a, b) { + if (a === '' && b !== '') return 1; + if (b === '' && a !== '') return -1; + if (a < b) return -1; + if (a > b) return 11; + return 0; + }; + let namesEntries = Object.entries(namesCopy); + namesEntries.sort((a, b) => sortFunction(a[1], b[1])); + assert.equal(Object.keys(namesEntries).length, keys.length); + let correct = {}; + namesEntries.forEach((entry, i) => { + correct[i] = itemKeys[parseInt(entry[0])]; + }); + correct = Object.keys(correct).map(key => correct[key]); + assert.deepEqual(correct, keys); + }); + it('testSortTopItemsCreator', async function () { + let response = await API.userGet( + config.userID, + "items/top?format=keys&order=creator" + ); + Helpers.assertStatusCode(response, 200); + let keys = response.data.trim().split("\n"); + let namesCopy = { ...names }; + let sortFunction = function (a, b) { + if (a === '' && b !== '') return 1; + if (b === '' && a !== '') return -1; + if (a < b) return -1; + if (a > b) return 11; + return 0; + }; + let namesEntries = Object.entries(namesCopy); + namesEntries.sort((a, b) => sortFunction(a[1], b[1])); + assert.equal(Object.keys(namesEntries).length, keys.length); + let correct = {}; + namesEntries.forEach((entry, i) => { + correct[i] = itemKeys[parseInt(entry[0])]; + }); + correct = Object.keys(correct).map(key => correct[key]); + assert.deepEqual(correct, keys); + }); + + + it('testSortDirection', async function () { + await API.userClear(config.userID); + let dataArray = []; + + dataArray.push(await API.createItem("book", { + title: "B", + creators: [ + { + creatorType: "author", + name: "B" + } + ], + dateAdded: '2014-02-05T00:00:00Z', + dateModified: '2014-04-05T01:00:00Z' + }, this, 'jsonData')); + + dataArray.push(await API.createItem("journalArticle", { + title: "A", + creators: [ + { + creatorType: "author", + name: "A" + } + ], + dateAdded: '2014-02-04T00:00:00Z', + dateModified: '2014-01-04T01:00:00Z' + }, this, 'jsonData')); + + dataArray.push(await API.createItem("newspaperArticle", { + title: "F", + creators: [ + { + creatorType: "author", + name: "F" + } + ], + dateAdded: '2014-02-03T00:00:00Z', + dateModified: '2014-02-03T01:00:00Z' + }, this, 'jsonData')); + + dataArray.push(await API.createItem("book", { + title: "C", + creators: [ + { + creatorType: "author", + name: "C" + } + ], + dateAdded: '2014-02-02T00:00:00Z', + dateModified: '2014-03-02T01:00:00Z' + }, this, 'jsonData')); + + dataArray.sort(function (a, b) { + return new Date(a.dateAdded) - new Date(b.dateAdded); + }); + + let keysByDateAddedAscending = dataArray.map(function (data) { + return data.key; + }); + + let keysByDateAddedDescending = [...keysByDateAddedAscending]; + keysByDateAddedDescending.reverse(); + // Ascending + let response = await API.userGet(config.userID, "items?format=keys&sort=dateAdded&direction=asc"); + Helpers.assert200(response); + assert.deepEqual(keysByDateAddedAscending, response.data.trim().split("\n")); + + response = await API.userGet(config.userID, "items?format=json&sort=dateAdded&direction=asc"); + Helpers.assert200(response); + let json = API.getJSONFromResponse(response); + let keys = json.map(val => val.key); + assert.deepEqual(keysByDateAddedAscending, keys); + + response = await API.userGet(config.userID, "items?format=atom&sort=dateAdded&direction=asc"); + Helpers.assert200(response); + let xml = API.getXMLFromResponse(response); + keys = Helpers.xpathEval(xml, '//atom:entry/zapi:key', false, true); + assert.deepEqual(keysByDateAddedAscending, keys); + + // Ascending using old 'order'/'sort' instead of 'sort'/'direction' + response = await API.userGet(config.userID, "items?format=keys&order=dateAdded&sort=asc"); + Helpers.assert200(response); + assert.deepEqual(keysByDateAddedAscending, response.data.trim().split("\n")); + + response = await API.userGet(config.userID, "items?format=json&order=dateAdded&sort=asc"); + Helpers.assert200(response); + json = API.getJSONFromResponse(response); + keys = json.map(val => val.key); + assert.deepEqual(keysByDateAddedAscending, keys); + + response = await API.userGet(config.userID, "items?format=atom&order=dateAdded&sort=asc"); + Helpers.assert200(response); + xml = API.getXMLFromResponse(response); + keys = Helpers.xpathEval(xml, '//atom:entry/zapi:key', false, true); + assert.deepEqual(keysByDateAddedAscending, keys); + + // Deprecated 'order'/'sort', but the wrong way + response = await API.userGet(config.userID, "items?format=keys&sort=dateAdded&order=asc"); + Helpers.assert200(response); + assert.deepEqual(keysByDateAddedAscending, response.data.trim().split("\n")); + + // Descending + //START + response = await API.userGet( + config.userID, + "items?format=keys&sort=dateAdded&direction=desc" + ); + Helpers.assert200(response); + assert.deepEqual(keysByDateAddedDescending, response.data.trim().split("\n")); + + response = await API.userGet( + config.userID, + "items?format=json&sort=dateAdded&direction=desc" + ); + Helpers.assert200(response); + json = API.getJSONFromResponse(response); + keys = json.map(val => val.key); + assert.deepEqual(keysByDateAddedDescending, keys); + + response = await API.userGet( + config.userID, + "items?format=atom&sort=dateAdded&direction=desc" + ); + Helpers.assert200(response); + xml = API.getXMLFromResponse(response); + keys = Helpers.xpathEval(xml, '//atom:entry/zapi:key', false, true); + assert.deepEqual(keysByDateAddedDescending, keys); + + // Descending + response = await API.userGet( + config.userID, + "items?format=keys&order=dateAdded&sort=desc" + ); + Helpers.assert200(response); + assert.deepEqual(keysByDateAddedDescending, response.data.trim().split("\n")); + + response = await API.userGet( + config.userID, + "items?format=json&order=dateAdded&sort=desc" + ); + Helpers.assert200(response); + json = API.getJSONFromResponse(response); + keys = json.map(val => val.key); + assert.deepEqual(keysByDateAddedDescending, keys); + + response = await API.userGet( + config.userID, + "items?format=atom&order=dateAdded&sort=desc" + ); + Helpers.assert200(response); + xml = API.getXMLFromResponse(response); + keys = Helpers.xpathEval(xml, '//atom:entry/zapi:key', false, true); + assert.deepEqual(keysByDateAddedDescending, keys); + }); + + + it('test_sort_top_level_items_by_item_type', async function () { + const response = await API.userGet( + config.userID, + "items/top?sort=itemType" + ); + Helpers.assert200(response); + const json = API.getJSONFromResponse(response); + const itemTypes = json.map(arr => arr.data.itemType); + const sorted = itemTypes.sort(); + assert.deepEqual(sorted, itemTypes); + }); + + it('testSortSortParamAsDirectionWithoutOrder', async function () { + const response = await API.userGet( + config.userID, + "items?format=keys&sort=asc" + ); + Helpers.assert200(response); + }); + + it('testSortDefault', async function () { + await API.userClear(config.userID); + let dataArray = []; + dataArray.push(await API.createItem("book", { + title: "B", + creators: [{ + creatorType: "author", + name: "B" + }], + dateAdded: '2014-02-05T00:00:00Z', + dateModified: '2014-04-05T01:00:00Z' + }, this, 'jsonData')); + dataArray.push(await API.createItem("journalArticle", { + title: "A", + creators: [{ + creatorType: "author", + name: "A" + }], + dateAdded: '2014-02-04T00:00:00Z', + dateModified: '2014-01-04T01:00:00Z' + }, this, 'jsonData')); + dataArray.push(await API.createItem("newspaperArticle", { + title: "F", + creators: [{ + creatorType: "author", + name: "F" + }], + dateAdded: '2014-02-03T00:00:00Z', + dateModified: '2014-02-03T01:00:00Z' + }, this, 'jsonData')); + dataArray.push(await API.createItem("book", { + title: "C", + creators: [{ + creatorType: "author", + name: "C" + }], + dateAdded: '2014-02-02T00:00:00Z', + dateModified: '2014-03-02T01:00:00Z' + }, this, 'jsonData')); + dataArray.sort((a, b) => { + return new Date(b.dateAdded) - new Date(a.dateAdded); + }); + const keysByDateAddedDescending = dataArray.map(data => data.key); + dataArray.sort((a, b) => { + return new Date(b.dateModified) - new Date(a.dateModified); + }); + const keysByDateModifiedDescending = dataArray.map(data => data.key); + let response = await API.userGet(config.userID, "items?format=keys"); + Helpers.assert200(response); + assert.deepEqual(keysByDateModifiedDescending, response.data.trim().split('\n')); + response = await API.userGet(config.userID, "items?format=json"); + Helpers.assert200(response); + const json = API.getJSONFromResponse(response); + let keys = json.map(val => val.key); + assert.deepEqual(keysByDateModifiedDescending, keys); + response = await API.userGet(config.userID, "items?format=atom"); + Helpers.assert200(response); + const xml = API.getXMLFromResponse(response); + const keysXml = Helpers.xpathEval(xml, '//atom:entry/zapi:key', false, true); + keys = keysXml.map(val => val.toString()); + assert.deepEqual(keysByDateAddedDescending, keys); + }); +}); + diff --git a/tests/remote_js/test/3/storageAdmin.js b/tests/remote_js/test/3/storageAdmin.js new file mode 100644 index 00000000..1a9b11da --- /dev/null +++ b/tests/remote_js/test/3/storageAdmin.js @@ -0,0 +1,49 @@ +const chai = require('chai'); +const assert = chai.assert; +const config = require("../../config.js"); +const API = require('../../api3.js'); +const Helpers = require('../../helpers3.js'); +const { API3Setup, API3WrapUp } = require("../shared.js"); + +describe('StorageAdminTests', function () { + this.timeout(config.timeout); + const DEFAULT_QUOTA = 300; + + before(async function () { + await API3Setup(); + await setQuota(0, 0, DEFAULT_QUOTA); + }); + + after(async function () { + await API3WrapUp(); + }); + + const setQuota = async (quota, expiration, expectedQuota) => { + let response = await API.post('users/' + config.userID + '/storageadmin', + `quota=${quota}&expiration=${expiration}`, + { "content-type": 'application/x-www-form-urlencoded' }, + { + username: config.rootUsername, + password: config.rootPassword + }); + Helpers.assertStatusCode(response, 200); + let xml = API.getXMLFromResponse(response); + let xmlQuota = xml.getElementsByTagName("quota")[0].innerHTML; + assert.equal(xmlQuota, expectedQuota); + if (expiration != 0) { + const xmlExpiration = xml.getElementsByTagName("expiration")[0].innerHTML; + assert.equal(xmlExpiration, expiration); + } + }; + it('test2GB', async function () { + const quota = 2000; + const expiration = Math.floor(Date.now() / 1000) + (86400 * 365); + await setQuota(quota, expiration, quota); + }); + + it('testUnlimited', async function () { + const quota = 'unlimited'; + const expiration = Math.floor(Date.now() / 1000) + (86400 * 365); + await setQuota(quota, expiration, quota); + }); +}); diff --git a/tests/remote_js/test/3/translationTest.js b/tests/remote_js/test/3/translationTest.js new file mode 100644 index 00000000..c163975d --- /dev/null +++ b/tests/remote_js/test/3/translationTest.js @@ -0,0 +1,168 @@ +const chai = require('chai'); +const assert = chai.assert; +const config = require("../../config.js"); +const API = require('../../api3.js'); +const Helpers = require('../../helpers3.js'); +const { API3Setup, API3WrapUp } = require("../shared.js"); + +describe('TranslationTests', function () { + this.timeout(config.timeout); + + before(async function () { + await API3Setup(); + }); + + after(async function () { + await API3WrapUp(); + }); + + it('testWebTranslationMultiple', async function () { + const url = 'https://zotero-static.s3.amazonaws.com/test-multiple.html'; + const title = 'Digital history: A guide to gathering, preserving, and presenting the past on the web'; + + let response = await API.userPost( + config.userID, + "items", + JSON.stringify({ + url: url + }), + { + "Content-Type": "application/json" + } + ); + Helpers.assert300(response); + const json = JSON.parse(response.data); + + const results = Object.assign({}, json.items); + const key = Object.keys(results)[0]; + const val = Object.values(results)[0]; + assert.equal('0', key); + assert.equal(title, val); + + const items = {}; + items[key] = val; + + // Missing token + response = await API.userPost( + config.userID, + `items?key=${config.apiKey}`, + JSON.stringify({ + url: url, + items: items + }), + { + "Content-Type": "application/json" + } + ); + Helpers.assert400(response, "Token not provided with selected items"); + + // Invalid selection + const items2 = Object.assign({}, items); + const invalidKey = "12345"; + items2[invalidKey] = items2[key]; + delete items2[key]; + response = await API.userPost( + config.userID, + `items?key=${config.apiKey}`, + JSON.stringify({ + url: url, + token: json.token, + items: items2 + }), + { + "Content-Type": "application/json" + } + ); + Helpers.assert400(response, `Index '${invalidKey}' not found for URL and token`); + + response = await API.userPost( + config.userID, + `items?key=${config.apiKey}`, + JSON.stringify({ + url: url, + token: json.token, + items: items + }), + { + "Content-Type": "application/json" + } + ); + + Helpers.assert200(response); + Helpers.assert200ForObject(response); + const itemKey = API.getJSONFromResponse(response).success[0]; + const data = (await API.getItem(itemKey, this, 'json')).data; + assert.equal(title, data.title); + }); + + //disabled + it('testWebTranslationSingleWithChildItems', async function () { + this.skip(); + let title = 'A Clustering Approach to Identify Intergenic Non-coding RNA in Mouse Macrophages'; + + let response = await API.userPost( + config.userID, + "items", + JSON.stringify({ + url: "http://www.computer.org/csdl/proceedings/bibe/2010/4083/00/4083a001-abs.html" + }), + { + "Content-Type": "application/json" + } + ); + Helpers.assert200(response); + Helpers.assert200ForObject(response, false, 0); + Helpers.assert200ForObject(response, false, 1); + let json = await API.getJSONFromResponse(response); + + // Check item + let itemKey = json.success[0]; + let data = (await API.getItem(itemKey, this, 'json')).data; + Helpers.assertEquals(title, data.title); + // NOTE: Tags currently not served via BibTeX (though available in RIS) + Helpers.assertCount(0, data.tags); + //$this->assertContains(['tag' => 'chip-seq; clustering; non-coding rna; rna polymerase; macrophage', 'type' => 1], $data['tags']); // TODO: split in translator + + // Check note + itemKey = json.success[1]; + data = (await API.getItem(itemKey, this, 'json')).data; + Helpers.assertEquals("Complete PDF document was either not available or accessible. " + + "Please make sure you're logged in to the digital library to retrieve the " + + "complete PDF document.", data.note); + }); + + it('testWebTranslationSingle', async function () { + const url = "https://forums.zotero.org"; + const title = 'Recent Discussions'; + + const response = await API.userPost( + config.userID, + "items", + JSON.stringify({ + url: url + }), + { "Content-Type": "application/json" } + ); + Helpers.assert200(response); + Helpers.assert200ForObject(response); + const json = API.getJSONFromResponse(response); + const itemKey = json.success[0]; + const data = await API.getItem(itemKey, this, 'json'); + assert.equal(title, data.data.title); + }); + + it('testWebTranslationInvalidToken', async function () { + const url = "https://zotero-static.s3.amazonaws.com/test.html"; + + const response = await API.userPost( + config.userID, + `items?key=${config.apiKey}`, + JSON.stringify({ + url: url, + token: Helpers.md5(Helpers.uniqueID()) + }), + { "Content-Type": "application/json" } + ); + Helpers.assert400(response, "'token' is valid only for item selection requests"); + }); +}); diff --git a/tests/remote_js/test/shared.js b/tests/remote_js/test/shared.js index 12e8f556..80d9d543 100644 --- a/tests/remote_js/test/shared.js +++ b/tests/remote_js/test/shared.js @@ -1,7 +1,7 @@ const config = require("../config.js"); const API = require('../api2.js'); const API3 = require('../api3.js'); - +const { resetGroups } = require("../groupsSetup.js"); // To fix socket hang up errors const retryIfNeeded = async (action) => { @@ -27,6 +27,13 @@ const retryIfNeeded = async (action) => { module.exports = { + resetGroups: async () => { + await retryIfNeeded(async () => { + await resetGroups(); + }); + }, + + API1Setup: async () => { const credentials = await API.login(); config.apiKey = credentials.user1.apiKey; From 9e746e01acb16f051fd3b00b905f745d1cbe001b Mon Sep 17 00:00:00 2001 From: Bogdan Abaev Date: Fri, 26 May 2023 17:02:23 -0400 Subject: [PATCH 12/33] removing work dir, removed unneeded data/ files --- tests/remote_js/data/sync1download.xml | 123 ---------------------- tests/remote_js/data/sync1upload.xml | 120 --------------------- tests/remote_js/test/3/publicationTest.js | 1 + 3 files changed, 1 insertion(+), 243 deletions(-) delete mode 100644 tests/remote_js/data/sync1download.xml delete mode 100644 tests/remote_js/data/sync1upload.xml diff --git a/tests/remote_js/data/sync1download.xml b/tests/remote_js/data/sync1download.xml deleted file mode 100644 index 97be9141..00000000 --- a/tests/remote_js/data/sync1download.xml +++ /dev/null @@ -1,123 +0,0 @@ - - - - - - Døn - Stîllmån - - - 汉字 - 1 - - - Test - Testerman - - - Testish McTester - 1 - - - Testy - Teststein - - - - - <p>Here's a <strong>child</strong> note.</p> - - - 3 - March 6, 2007 - My Book Section - - - DUQPU87V - - - amazon.html - AAAAAAFkAAIAAAxNYWNpbnRvc2ggSEQAAAAAAAAAAAAAAAAAAADC/J9aSCsAAAAOrpkLYW1hem9uLmh0bWwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADnT/bcS9TKEAAAAAAAAAAP////8AAAkgAAAAAAAAAAAAAAAAAAAAB0Rlc2t0b3AAABAACAAAwvzXmgAAABEACAAAxL2E4QAAAAEADAAOrpkADjPnAA4z5QACACpNYWNpbnRvc2ggSEQ6VXNlcnM6ZGFuOkRlc2t0b3A6YW1hem9uLmh0bWwADgAYAAsAYQBtAGEAegBvAG4ALgBoAHQAbQBsAA8AGgAMAE0AYQBjAGkAbgB0AG8AcwBoACAASABEABIAHVVzZXJzL2Rhbi9EZXNrdG9wL2FtYXpvbi5odG1sAAATAAEvAAAVAAIACv//AAA= - <p>Note on a top-level linked file</p> - DUQPU87V - - - http://chnm.gmu.edu/ - 2009-03-07 04:55:59 - Center for History and New Media - storage:chnm.gmu.edu.html - <p>This is a note for a snapshot.</p> - - - <p>Here's a top-level note.</p> - - - http://www.zotero.org/ - 2009-03-07 04:56:47 - Zotero: The Next-Generation Research Tool - storage:www.zotero.org.html - - - My Book - - - 6TKKAABJ 7IMJZ8V6 - - - http://chnm.gmu.edu/ - 2009-03-07 04:56:01 - Center for History and New Media - <p>This is a note for a link.</p> - - - FILE.jpg - storage:FILE.jpg - - - Trashed item - - - - - - DUQPU87V - - - HTHD884W - - - 6TKKAABJ 9P9UVFK3 - - - - - - - - - - - - - - - 6TKKAABJ - - - 6TKKAABJ - - - 6TKKAABJ DUQPU87V - - - DUQPU87V - - - HTHD884W - - - T3K4BNWP - - - - diff --git a/tests/remote_js/data/sync1upload.xml b/tests/remote_js/data/sync1upload.xml deleted file mode 100644 index ca0947eb..00000000 --- a/tests/remote_js/data/sync1upload.xml +++ /dev/null @@ -1,120 +0,0 @@ - - - - Test - Testerman - - - 汉字 - 1 - - - Testy - Teststein - - - Døn - Stîllmån - - - - - 3 - 2007-03-06 March 6, 2007 - My Book Section - - - - Testish McTester - 1 - - - - - My Book - - - 6TKKAABJ - - - <p>Here's a <strong>child</strong> note.</p> - - - http://chnm.gmu.edu/ - 2009-03-07 04:56:01 - Center for History and New Media - <p>This is a note for a link.</p> - - - http://chnm.gmu.edu/ - 2009-03-07 04:55:59 - Center for History and New Media - storage:chnm.gmu.edu.html - <p>This is a note for a snapshot.</p> - - - <p>Here's a top-level note.</p> - - - http://www.zotero.org/ - 2009-03-07 04:56:47 - Zotero: The Next-Generation Research Tool - storage:www.zotero.org.html - - - FILE.jpg - storage:FILE.jpg - - - Trashed item - - - - amazon.html - AAAAAAFkAAIAAAxNYWNpbnRvc2ggSEQAAAAAAAAAAAAAAAAAAADC/J9aSCsAAAAOrpkLYW1hem9uLmh0bWwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADnT/bcS9TKEAAAAAAAAAAP////8AAAkgAAAAAAAAAAAAAAAAAAAAB0Rlc2t0b3AAABAACAAAwvzXmgAAABEACAAAxL2E4QAAAAEADAAOrpkADjPnAA4z5QACACpNYWNpbnRvc2ggSEQ6VXNlcnM6ZGFuOkRlc2t0b3A6YW1hem9uLmh0bWwADgAYAAsAYQBtAGEAegBvAG4ALgBoAHQAbQBsAA8AGgAMAE0AYQBjAGkAbgB0AG8AcwBoACAASABEABIAHVVzZXJzL2Rhbi9EZXNrdG9wL2FtYXpvbi5odG1sAAATAAEvAAAVAAIACv//AAA= - <p>Note on a top-level linked file</p> - DUQPU87V - - - - - 6TKKAABJ 9P9UVFK3 - - - DUQPU87V - - - - HTHD884W - - - - - - - - - - - - - - DUQPU87V - - - 6TKKAABJ - - - 6TKKAABJ DUQPU87V - - - 6TKKAABJ - - - HTHD884W - - - T3K4BNWP - - - diff --git a/tests/remote_js/test/3/publicationTest.js b/tests/remote_js/test/3/publicationTest.js index 55b6f90b..80af1e0a 100644 --- a/tests/remote_js/test/3/publicationTest.js +++ b/tests/remote_js/test/3/publicationTest.js @@ -26,6 +26,7 @@ describe('PublicationTests', function () { after(async function () { await API3WrapUp(); + fs.rmdirSync("./work", { recursive: true, force: true }); if (toDelete.length > 0) { const commandInput = { Bucket: config.s3Bucket, From 339d92316f2c6b068b1a8a1ca51504543d26811f Mon Sep 17 00:00:00 2001 From: Bogdan Abaev Date: Mon, 29 May 2023 15:23:37 -0400 Subject: [PATCH 13/33] notification, tag and version tests --- tests/remote_js/api3.js | 8 +- tests/remote_js/helpers.js | 12 + tests/remote_js/helpers3.js | 6 +- tests/remote_js/test/3/notificationTest.js | 472 +++++++++ tests/remote_js/test/3/tagTest.js | 793 +++++++++++++++ tests/remote_js/test/3/versionTest.js | 1046 ++++++++++++++++++++ 6 files changed, 2331 insertions(+), 6 deletions(-) create mode 100644 tests/remote_js/test/3/notificationTest.js create mode 100644 tests/remote_js/test/3/tagTest.js create mode 100644 tests/remote_js/test/3/versionTest.js diff --git a/tests/remote_js/api3.js b/tests/remote_js/api3.js index e16c28be..42c15f2b 100644 --- a/tests/remote_js/api3.js +++ b/tests/remote_js/api3.js @@ -475,12 +475,10 @@ class API3 extends API2 { } static async superPut(url, data, headers) { - let postData = { + return this.put(url, data, headers, { username: this.config.rootUsername, password: this.config.rootPassword - }; - Object.assign(postData, data); - return this.put(url, postData, headers); + }); } static async getSearchResponse(keys, context = null, format = false, groupID = false) { @@ -784,7 +782,7 @@ class API3 extends API2 { case "item": // Convert to array - json = JSON.parse(JSON.stringify(await this.getItemTemplate("book"))); + json = await this.getItemTemplate("book"); break; case "search": diff --git a/tests/remote_js/helpers.js b/tests/remote_js/helpers.js index f5b3f4ff..82c670cb 100644 --- a/tests/remote_js/helpers.js +++ b/tests/remote_js/helpers.js @@ -189,14 +189,26 @@ class Helpers { this.assertStatusForObject(response, 'success', index, message); }; + static assert404ForObject = (response, { index = 0, message = null } = {}) => { + this.assertStatusForObject(response, 'failed', index, 404, message); + }; + static assert409ForObject = (response, { index = 0, message = null } = {}) => { this.assertStatusForObject(response, 'failed', index, 409, message); }; + static assert412ForObject = (response, { index = 0, message = null } = {}) => { + this.assertStatusForObject(response, 'failed', index, 412, message); + }; + static assert413ForObject = (response, { index = 0, message = null } = {}) => { this.assertStatusForObject(response, 'failed', index, 413, message); }; + static assert428ForObject = (response, { index = 0, message = null } = {}) => { + this.assertStatusForObject(response, 'failed', index, 428, message); + }; + static assertUnchangedForObject = (response, { index = 0, message = null } = {}) => { this.assertStatusForObject(response, 'unchanged', index, null, message); }; diff --git a/tests/remote_js/helpers3.js b/tests/remote_js/helpers3.js index 498852f4..58e750f9 100644 --- a/tests/remote_js/helpers3.js +++ b/tests/remote_js/helpers3.js @@ -18,6 +18,10 @@ class Helpers3 extends Helpers { const contentType = response.headers['content-type'][0]; if (contentType == 'application/json') { const json = JSON.parse(response.data); + if (Array.isArray(json)) { + assert.equal(json.length, expectedResults); + return; + } assert.lengthOf(Object.keys(json), expectedResults); } else if (contentType.includes('text/plain')) { @@ -96,7 +100,7 @@ class Helpers3 extends Helpers { } static assertNotificationCount(expected, response) { - let headerArr = response.headers[this.notificationHeader]; + let headerArr = response.headers[this.notificationHeader] || []; let header = headerArr.length > 0 ? headerArr[0] : ""; try { if (expected === 0) { diff --git a/tests/remote_js/test/3/notificationTest.js b/tests/remote_js/test/3/notificationTest.js new file mode 100644 index 00000000..b699c1ee --- /dev/null +++ b/tests/remote_js/test/3/notificationTest.js @@ -0,0 +1,472 @@ +const chai = require('chai'); +const assert = chai.assert; +const config = require("../../config.js"); +const API = require('../../api3.js'); +const Helpers = require('../../helpers3.js'); +const { API3Setup, API3WrapUp, resetGroups } = require("../shared.js"); + +describe('NotificationTests', function () { + this.timeout(config.timeout); + + before(async function () { + await API3Setup(); + await resetGroups(); + }); + + after(async function () { + await API3WrapUp(); + }); + beforeEach(async function () { + API.useAPIKey(config.apiKey); + }); + + it('testModifyItemNotification', async function () { + let json = await API.createItem("book", false, this, 'jsonData'); + json.title = 'test'; + let response = await API.userPut( + config.userID, + `items/${json.key}`, + JSON.stringify(json), + { "Content-Type": "application/json" } + ); + let version = parseInt(response.headers['last-modified-version'][0]); + Helpers.assertNotificationCount(1, response); + Helpers.assertHasNotification({ + event: 'topicUpdated', + topic: `/users/${config.userID}`, + version: version + }, response); + }); + + it('testKeyAddLibraryNotification', async function () { + API.useAPIKey(""); + const name = "Test " + Helpers.uniqueID(); + const json = { + name: name, + access: { + user: { + library: true + } + } + }; + + const response = await API.superPost( + 'users/' + config.userID + '/keys?showid=1', + JSON.stringify(json) + ); + + Helpers.assert201(response); + const jsonFromResponse = API.getJSONFromResponse(response); + const apiKey = jsonFromResponse.key; + const apiKeyID = jsonFromResponse.id; + + try { + json.access.groups = {}; + json.access.groups[config.ownedPrivateGroupID] = { + library: true, + write: true + }; + + const response2 = await API.superPut( + "keys/" + apiKey, + JSON.stringify(json) + ); + Helpers.assert200(response2); + + Helpers.assertNotificationCount(1, response2); + Helpers.assertHasNotification({ + event: 'topicAdded', + apiKeyID: String(apiKeyID), + topic: '/groups/' + config.ownedPrivateGroupID + }, response2); + + await API.superDelete("keys/" + apiKey); + } + // Clean up + finally { + await API.superDelete("keys/" + apiKey); + } + }); + + it('testNewItemNotification', async function () { + const response = await API.createItem("book", false, this, 'response'); + const version = API.getJSONFromResponse(response).successful[0].version; + Helpers.assertNotificationCount(1, response); + Helpers.assertHasNotification({ + event: 'topicUpdated', + topic: '/users/' + config.userID, + version: version + }, response); + }); + + + it('testKeyCreateNotification', async function () { + API.useAPIKey(""); + let name = "Test " + Helpers.uniqueID(); + let response = await API.superPost( + 'users/' + config.userID + '/keys', + JSON.stringify({ + name: name, + access: { user: { library: true } } + }) + ); + try { + Helpers.assertNotificationCount(0, response); + } + finally { + let json = API.getJSONFromResponse(response); + let key = json.key; + await API.userDelete( + config.userID, + "keys/" + key, + { "Content-Type": "application/json" } + ); + } + }); + + it('testAddDeleteOwnedGroupNotification', async function () { + API.useAPIKey(""); + const json = await createKeyWithAllGroupAccess(config.userID); + const apiKey = json.key; + + try { + const allGroupsKeys = await getKeysWithAllGroupAccess(config.userID); + + const response = await createGroup(config.userID); + const xml = API.getXMLFromResponse(response); + const groupID = parseInt(Helpers.xpathEval(xml, "/atom:entry/zapi:groupID")); + + try { + Helpers.assertNotificationCount(Object.keys(allGroupsKeys).length, response); + await Promise.all(allGroupsKeys.map(async function (key) { + const response2 = await API.superGet(`keys/${key}?showid=1`); + const json2 = await API.getJSONFromResponse(response2); + Helpers.assertHasNotification({ + event: "topicAdded", + apiKeyID: String(json2.id), + topic: `/groups/${groupID}` + }, response); + })); + } + finally { + const response = await API.superDelete(`groups/${groupID}`); + Helpers.assert204(response); + Helpers.assertNotificationCount(1, response); + Helpers.assertHasNotification({ + event: "topicDeleted", + topic: `/groups/${groupID}` + }, response); + } + } + finally { + const response = await API.superDelete(`keys/${apiKey}`); + try { + Helpers.assert204(response); + } + catch (e) { + console.log(e); + } + } + }); + + it('testDeleteItemNotification', async function () { + let json = await API.createItem("book", false, this, 'json'); + let response = await API.userDelete( + config.userID, + `items/${json.key}`, + { + "If-Unmodified-Since-Version": json.version + } + ); + let version = parseInt(response.headers['last-modified-version'][0]); + Helpers.assertNotificationCount(1, response); + Helpers.assertHasNotification({ + event: 'topicUpdated', + topic: `/users/${config.userID}`, + version: version + }, response); + }); + + it('testKeyRemoveLibraryFromAllGroupsNotification', async function () { + API.useAPIKey(""); + const removedGroup = config.ownedPrivateGroupID; + const json = await createKeyWithAllGroupAccess(config.userID); + const apiKey = json.key; + const apiKeyID = json.id; + try { + API.useAPIKey(apiKey); + const response = await API.userGet(config.userID, 'groups'); + let groupIDs = API.getJSONFromResponse(response).map(group => group.id); + groupIDs = groupIDs.filter(groupID => groupID !== removedGroup); + delete json.access.groups.all; + for (let groupID of groupIDs) { + json.access.groups[groupID] = {}; + json.access.groups[groupID].library = true; + } + API.useAPIKey(""); + const putResponse = await API.superPut(`keys/${apiKey}`, JSON.stringify(json)); + Helpers.assert200(putResponse); + Helpers.assertNotificationCount(1, putResponse); + + Helpers.assertHasNotification({ + event: "topicRemoved", + apiKeyID: String(apiKeyID), + topic: `/groups/${removedGroup}` + }, putResponse); + } + finally { + await API.superDelete(`keys/${apiKey}`); + } + }); + + it('Create and delete group owned by user', async function () { + // Dummy test function, not related to the code above. + // Just here so that the class doesn't break the syntax of the original phpunit file + // and can be tested using mocha/chai + assert(true); + }); + + async function createKey(userID, access) { + let name = "Test " + Math.random().toString(36).substring(2); + let json = { + name: name, + access: access + }; + const response = await API.superPost( + "users/" + userID + "/keys?showid=1", + JSON.stringify(json) + ); + assert.equal(response.status, 201); + json = await API.getJSONFromResponse(response); + return json; + } + + async function createKeyWithAllGroupAccess(userID) { + return createKey(userID, { + user: { + library: true + }, + groups: { + all: { + library: true + } + } + }); + } + + async function createGroup(ownerID) { + // Create new group owned by another + let xml = ''; + const response = await API.superPost( + 'groups', + xml + ); + assert.equal(response.status, 201); + return response; + } + + async function getKeysWithAllGroupAccess(userID) { + const response = await API.superGet("users/" + userID + "/keys"); + assert.equal(response.status, 200); + const json = await API.getJSONFromResponse(response); + return json.filter(keyObj => keyObj.access.groups.all.library).map(keyObj => keyObj.key); + } + + + it('testAddRemoveGroupMemberNotification', async function () { + API.useAPIKey(""); + let json = await createKeyWithAllGroupAccess(config.userID); + let apiKey = json.key; + + try { + // Get all keys with access to all groups + let allGroupsKeys = await getKeysWithAllGroupAccess(config.userID); + + // Create group owned by another user + let response = await createGroup(config.userID2); + let xml = API.getXMLFromResponse(response); + let groupID = parseInt(Helpers.xpathEval(xml, "/atom:entry/zapi:groupID")); + + try { + // Add user to group + response = await API.superPost( + "groups/" + groupID + "/users", + '', + { "Content-Type": "application/xml" } + ); + Helpers.assert200(response); + Helpers.assertNotificationCount(Object.keys(allGroupsKeys).length, response); + for (let key of allGroupsKeys) { + let response2 = await API.superGet("keys/" + key + "?showid=1"); + let json2 = API.getJSONFromResponse(response2); + Helpers.assertHasNotification({ + event: 'topicAdded', + apiKeyID: String(json2.id), + topic: '/groups/' + groupID + }, response); + } + + // Remove user from group + response = await API.superDelete("groups/" + groupID + "/users/" + config.userID); + Helpers.assert204(response); + Helpers.assertNotificationCount(Object.keys(allGroupsKeys).length, response); + for (let key of allGroupsKeys) { + let response2 = await API.superGet("keys/" + key + "?showid=1"); + let json2 = API.getJSONFromResponse(response2); + Helpers.assertHasNotification({ + event: 'topicRemoved', + apiKeyID: String(json2.id), + topic: '/groups/' + groupID + }, response); + } + } + // Delete group + finally { + response = await API.superDelete("groups/" + groupID); + Helpers.assert204(response); + Helpers.assertNotificationCount(1, response); + Helpers.assertHasNotification({ + event: 'topicDeleted', + topic: '/groups/' + groupID + }, response); + } + } + // Delete key + finally { + let response = await API.superDelete("keys/" + apiKey); + try { + Helpers.Helpers.assert204(response); + } + catch (e) { + console.log(e); + } + } + }); + + it('testKeyAddAllGroupsToNoneNotification', async function () { + API.useAPIKey(""); + const json = await createKey(config.userID, { + userId: config.userId, + body: { + user: { + library: true, + }, + }, + }); + const apiKey = json.key; + const apiKeyId = json.id; + + try { + const response = await API.superGet(`users/${config.userID}/groups`); + const groupIds = API.getJSONFromResponse(response).map(group => group.id); + json.access = {}; + json.access.groups = []; + json.access.groups[0] = { library: true }; + const putResponse = await API.superPut(`keys/${apiKey}`, JSON.stringify(json)); + Helpers.assert200(putResponse); + + Helpers.assertNotificationCount(groupIds.length, putResponse); + + for (const groupID of groupIds) { + Helpers.assertHasNotification( + { + event: "topicAdded", + apiKeyID: String(apiKeyId), + topic: `/groups/${groupID}`, + }, + putResponse + ); + } + } + finally { + await API.superDelete(`keys/${apiKey}`); + } + }); + + it('testKeyRemoveLibraryNotification', async function () { + API.useAPIKey(""); + let json = await createKey(config.userID, { + user: { + library: true + }, + groups: { + [config.ownedPrivateGroupID]: { + library: true + } + } + }); + const apiKey = json.key; + const apiKeyID = json.id; + + try { + delete json.access.groups; + const response = await API.superPut( + `keys/${apiKey}`, + JSON.stringify(json) + ); + Helpers.assert200(response); + + Helpers.assertNotificationCount(1, response); + Helpers.assertHasNotification({ + event: 'topicRemoved', + apiKeyID: String(apiKeyID), + topic: `/groups/${config.ownedPrivateGroupID}` + }, response); + } + finally { + await API.superDelete(`keys/${apiKey}`); + } + }); + + /** + * Grant access to all groups to an API key that has access to a single group + */ + + + it('testKeyAddAllGroupsToOneNotification', async function () { + API.useAPIKey(''); + + let json = await createKey(config.userID, { + user: { + library: true + }, + groups: { + [config.ownedPrivateGroupID]: { + library: true + } + } + }); + let apiKey = json.key; + let apiKeyID = json.id; + + try { + // Get list of available groups + let response = await API.superGet(`users/${config.userID}/groups`); + let groupIDs = API.getJSONFromResponse(response).map(group => group.id); + // Remove group that already had access + groupIDs = groupIDs.filter(groupID => groupID !== config.ownedPrivateGroupID); + + // Add all groups to the key, which should trigger topicAdded for each new group + // but not the group that was previously accessible + delete json.access.groups[config.ownedPrivateGroupID]; + json.access.groups.all = { + library: true + }; + response = await API.superPut(`keys/${apiKey}`, JSON.stringify(json)); + assert.equal(200, response.status); + + await Helpers.assertNotificationCount(groupIDs.length, response); + for (let groupID of groupIDs) { + Helpers.assertHasNotification({ + event: 'topicAdded', + apiKeyID: String(apiKeyID), + topic: `/groups/${groupID}` + }, response); + } + } + // Clean up + finally { + await API.superDelete(`keys/${apiKey}`); + } + }); +}); diff --git a/tests/remote_js/test/3/tagTest.js b/tests/remote_js/test/3/tagTest.js new file mode 100644 index 00000000..48755356 --- /dev/null +++ b/tests/remote_js/test/3/tagTest.js @@ -0,0 +1,793 @@ +const chai = require('chai'); +const assert = chai.assert; +const config = require("../../config.js"); +const API = require('../../api3.js'); +const Helpers = require('../../helpers3.js'); +const { API3Setup, API3WrapUp } = require("../shared.js"); + +describe('TagTests', function () { + this.timeout(0); + + before(async function () { + await API3Setup(); + }); + + after(async function () { + await API3WrapUp(); + }); + beforeEach(async function () { + await API.userClear(config.userID); + }); + + it('test_empty_tag_including_whitespace_should_be_ignored', async function () { + let json = await API.getItemTemplate("book"); + json.tags.push({ tag: "A" }); + json.tags.push({ tag: "", type: 1 }); + json.tags.push({ tag: " ", type: 1 }); + + + let response = await API.postItem(json); + Helpers.assert200ForObject(response); + json = API.getJSONFromResponse(response); + assert.deepEqual(json.successful[0].data.tags, [{ tag: 'A' }]); + }); + + it('testInvalidTagObject', async function () { + let json = await API.getItemTemplate("book"); + json.tags.push(["invalid"]); + + let headers = { "Content-Type": "application/json" }; + let response = await API.postItem(json, headers); + + Helpers.assertStatusForObject(response, 'failed', 0, 400, "Tag must be an object"); + }); + + it('testTagSearch', async function () { + const tags1 = ["a", "aa", "b"]; + const tags2 = ["b", "c", "cc"]; + + await API.createItem("book", { + tags: tags1.map((tag) => { + return { tag: tag }; + }) + }, true, 'key'); + + await API.createItem("book", { + tags: tags2.map((tag) => { + return { tag: tag }; + }) + }, true, 'key'); + + let response = await API.userGet( + config.userID, + "tags?tag=" + tags1.join("%20||%20"), + { "Content-Type": "application/json" } + ); + Helpers.assertStatusCode(response, 200); + Helpers.assertNumResults(response, tags1.length); + }); + + it('testTagNewer', async function () { + // Create items with tags + await API.createItem("book", { + tags: [ + { tag: "a" }, + { tag: "b" } + ] + }, true); + + const version = await API.getLibraryVersion(); + + // 'newer' shouldn't return any results + let response = await API.userGet( + config.userID, + `tags?newer=${version}` + ); + Helpers.assertStatusCode(response, 200); + Helpers.assertNumResults(response, 0); + + // Create another item with tags + await API.createItem("book", { + tags: [ + { tag: "a" }, + { tag: "c" } + ] + }, true); + + // 'newer' should return new tag Atom + response = await API.userGet( + config.userID, + `tags?content=json&newer=${version}` + ); + Helpers.assertStatusCode(response, 200); + Helpers.assertNumResults(response, 1); + assert.isAbove(parseInt(response.headers['last-modified-version']), parseInt(version)); + let xml = API.getXMLFromResponse(response); + let data = API.parseDataFromAtomEntry(xml); + data = JSON.parse(data.content); + assert.strictEqual(data.tag, 'c'); + assert.strictEqual(data.type, 0); + + + // 'newer' should return new tag (JSON) + response = await API.userGet( + config.userID, + `tags?newer=${version}` + ); + Helpers.assertStatusCode(response, 200); + Helpers.assertNumResults(response, 1); + assert.isAbove(parseInt(response.headers['last-modified-version']), parseInt(version)); + let json = API.getJSONFromResponse(response)[0]; + assert.strictEqual(json.tag, 'c'); + assert.strictEqual(json.meta.type, 0); + }); + + it('testMultiTagDelete', async function () { + const tags1 = ["a", "aa", "b"]; + const tags2 = ["b", "c", "cc"]; + const tags3 = ["Foo"]; + + await API.createItem("book", { + tags: tags1.map(tag => ({ tag: tag })) + }, true, 'key'); + + await API.createItem("book", { + tags: tags2.map(tag => ({ tag: tag, type: 1 })) + }, true, 'key'); + + await API.createItem("book", { + tags: tags3.map(tag => ({ tag: tag })) + }, true, 'key'); + + let libraryVersion = await API.getLibraryVersion(); + libraryVersion = parseInt(libraryVersion); + + // Missing version header + let response = await API.userDelete( + config.userID, + `tags?content=json&tag=${tags1.concat(tags2).map(tag => encodeURIComponent(tag)).join("%20||%20")}` + ); + Helpers.assertStatusCode(response, 428); + + // Outdated version header + response = await API.userDelete( + config.userID, + `tags?content=json&tag=${tags1.concat(tags2).map(tag => encodeURIComponent(tag)).join("%20||%20")}`, + { "If-Unmodified-Since-Version": `${libraryVersion - 1}` } + ); + Helpers.assertStatusCode(response, 412); + + // Delete + response = await API.userDelete( + config.userID, + `tags?content=json&tag=${tags1.concat(tags2).map(tag => encodeURIComponent(tag)).join("%20||%20")}`, + { "If-Unmodified-Since-Version": `${libraryVersion}` } + ); + Helpers.assertStatusCode(response, 204); + + // Make sure they're gone + response = await API.userGet( + config.userID, + `tags?content=json&tag=${tags1.concat(tags2, tags3).map(tag => encodeURIComponent(tag)).join("%20||%20")}` + ); + Helpers.assertStatusCode(response, 200); + Helpers.assertNumResults(response, 1); + }); + + it('testTagAddItemVersionChange', async function () { + let data1 = await API.createItem("book", { + tags: [{ + tag: "a" + }, + { + tag: "b" + }] + }, true, 'jsonData'); + + let data2 = await API.createItem("book", { + tags: [{ + tag: "a" + }, + { + tag: "c" + }] + }, true, 'jsonData'); + + let version2 = data2.version; + version2 = parseInt(version2); + + // Remove tag 'a' from item 1 + data1.tags = [{ + tag: "d" + }, + { + tag: "c" + }]; + + let response = await API.postItem(data1); + Helpers.assertStatusCode(response, 200); + + // Item 1 version should be one greater than last update + let json1 = await API.getItem(data1.key, true, 'json'); + assert.equal(parseInt(json1.version), version2 + 1); + + // Item 2 version shouldn't have changed + let json2 = await API.getItem(data2.key, true, 'json'); + assert.equal(parseInt(json2.version), version2); + }); + + it('testItemTagSearch', async function () { + // Create items with tags + let key1 = await API.createItem("book", { + tags: [ + { tag: "a" }, + { tag: "b" } + ] + }, true, 'key'); + + let key2 = await API.createItem("book", { + tags: [ + { tag: "a" }, + { tag: "c" } + ] + }, true, 'key'); + + let checkTags = async function (tagComponent, assertingKeys = []) { + let response = await API.userGet( + config.userID, + `items?format=keys&${tagComponent}` + ); + Helpers.assertStatusCode(response, 200); + if (assertingKeys.length != 0) { + let keys = response.data.trim().split("\n"); + + assert.equal(keys.length, assertingKeys.length); + for (let assertingKey of assertingKeys) { + assert.include(keys, assertingKey); + } + } + else { + assert.isEmpty(response.data.trim()); + } + return response; + }; + + // Searches + await checkTags("tag=a", [key2, key1]); + await checkTags("tag=a&tag=c", [key2]); + await checkTags("tag=b&tag=c", []); + await checkTags("tag=b%20||%20c", [key1, key2]); + await checkTags("tag=a%20||%20b%20||%20c", [key1, key2]); + await checkTags("tag=-a"); + await checkTags("tag=-b", [key2]); + await checkTags("tag=b%20||%20c&tag=a", [key1, key2]); + await checkTags("tag=-z", [key1, key2]); + await checkTags("tag=B", [key1]); + }); + + // + + + it('test_tags_within_items_within_empty_collection', async function () { + let collectionKey = await API.createCollection("Empty collection", false, this, 'key'); + await API.createItem( + "book", + { + title: "Foo", + tags: [ + { tag: "a" }, + { tag: "b" } + ] + }, + this, + 'key' + ); + + let response = await API.userGet( + config.userID, + "collections/" + collectionKey + "/items/top/tags" + ); + Helpers.assert200(response); + Helpers.assertNumResults(response, 0); + }); + + it('test_tags_within_items', async function () { + const collectionKey = await API.createCollection("Collection", false, this, 'key'); + const item1Key = await API.createItem( + "book", + { + title: "Foo", + tags: [ + { tag: "a" }, + { tag: "g" } + ] + }, + this, + 'key' + ); + // Child note + await API.createItem( + "note", + { + note: "Test Note 1", + parentItem: item1Key, + tags: [ + { tag: "a" }, + { tag: "e" } + ] + }, + this + ); + // Another item + await API.createItem( + "book", + { + title: "Bar", + tags: [ + { tag: "b" } + ] + }, + this + ); + // Item within collection + const item4Key = await API.createItem( + "book", + { + title: "Foo", + collections: [collectionKey], + tags: [ + { tag: "a" }, + { tag: "c" }, + { tag: "g" } + ] + }, + this, + 'key' + ); + // Child note within collection + await API.createItem( + "note", + { + note: "Test Note 2", + parentItem: item4Key, + tags: [ + { tag: "a" }, + { tag: "f" } + ] + }, + this + ); + // Another item within collection + await API.createItem( + "book", + { + title: "Bar", + collections: [collectionKey], + tags: [ + { tag: "d" } + ] + }, + this + ); + + // All items, equivalent to /tags + const response = await API.userGet( + config.userID, + "items/tags" + ); + Helpers.assert200(response); + Helpers.assertNumResults(response, 7); + const json = API.getJSONFromResponse(response); + assert.deepEqual( + ['a', 'b', 'c', 'd', 'e', 'f', 'g'], + json.map(tag => tag.tag).sort() + ); + + // Top-level items + const responseTop = await API.userGet( + config.userID, + "items/top/tags" + ); + Helpers.assert200(responseTop); + Helpers.assertNumResults(responseTop, 5); + const jsonTop = API.getJSONFromResponse(responseTop); + assert.deepEqual( + ['a', 'b', 'c', 'd', 'g'], + jsonTop.map(tag => tag.tag).sort() + ); + + // All items, filtered by 'tag', equivalent to /tags + const responseTag = await API.userGet( + config.userID, + "items/tags?tag=a" + ); + Helpers.assert200(responseTag); + Helpers.assertNumResults(responseTag, 1); + const jsonTag = API.getJSONFromResponse(responseTag); + assert.deepEqual( + ['a'], + jsonTag.map(tag => tag.tag).sort() + ); + + // All items, filtered by 'itemQ' + const responseItemQ1 = await API.userGet( + config.userID, + "items/tags?itemQ=foo" + ); + Helpers.assert200(responseItemQ1); + Helpers.assertNumResults(responseItemQ1, 3); + const jsonItemQ1 = API.getJSONFromResponse(responseItemQ1); + assert.deepEqual( + ['a', 'c', 'g'], + jsonItemQ1.map(tag => tag.tag).sort() + ); + const responseItemQ2 = await API.userGet( + config.userID, + "items/tags?itemQ=bar" + ); + Helpers.assert200(responseItemQ2); + Helpers.assertNumResults(responseItemQ2, 2); + const jsonItemQ2 = API.getJSONFromResponse(responseItemQ2); + assert.deepEqual( + ['b', 'd'], + jsonItemQ2.map(tag => tag.tag).sort() + ); + const responseItemQ3 = await API.userGet( + config.userID, + "items/tags?itemQ=Test%20Note" + ); + Helpers.assert200(responseItemQ3); + Helpers.assertNumResults(responseItemQ3, 3); + const jsonItemQ3 = API.getJSONFromResponse(responseItemQ3); + assert.deepEqual( + ['a', 'e', 'f'], + jsonItemQ3.map(tag => tag.tag).sort() + ); + + // All items with the given tags + const responseItemTag = await API.userGet( + config.userID, + "items/tags?itemTag=a&itemTag=g" + ); + Helpers.assert200(responseItemTag); + Helpers.assertNumResults(responseItemTag, 3); + const jsonItemTag = API.getJSONFromResponse(responseItemTag); + assert.deepEqual( + ['a', 'c', 'g'], + jsonItemTag.map(tag => tag.tag).sort() + ); + + // Disjoint tags + const responseItemTag2 = await API.userGet( + config.userID, + "items/tags?itemTag=a&itemTag=d" + ); + Helpers.assert200(responseItemTag2); + Helpers.assertNumResults(responseItemTag2, 0); + + // Items within a collection + const responseInCollection = await API.userGet( + config.userID, + `collections/${collectionKey}/items/tags` + ); + Helpers.assert200(responseInCollection); + Helpers.assertNumResults(responseInCollection, 5); + const jsonInCollection = API.getJSONFromResponse(responseInCollection); + assert.deepEqual( + ['a', 'c', 'd', 'f', 'g'], + jsonInCollection.map(tag => tag.tag).sort() + ); + + // Top-level items within a collection + const responseTopInCollection = await API.userGet( + config.userID, + `collections/${collectionKey}/items/top/tags` + ); + Helpers.assert200(responseTopInCollection); + Helpers.assertNumResults(responseTopInCollection, 4); + const jsonTopInCollection = API.getJSONFromResponse(responseTopInCollection); + assert.deepEqual( + ['a', 'c', 'd', 'g'], + jsonTopInCollection.map(tag => tag.tag).sort() + ); + + // Search within a collection + const responseSearchInCollection = await API.userGet( + config.userID, + `collections/${collectionKey}/items/tags?itemQ=Test%20Note` + ); + Helpers.assert200(responseSearchInCollection); + Helpers.assertNumResults(responseSearchInCollection, 2); + const jsonSearchInCollection = API.getJSONFromResponse(responseSearchInCollection); + assert.deepEqual( + ['a', 'f'], + jsonSearchInCollection.map(tag => tag.tag).sort() + ); + + // Items with the given tags within a collection + const responseTagInCollection = await API.userGet( + config.userID, + `collections/${collectionKey}/items/tags?itemTag=a&itemTag=g` + ); + Helpers.assert200(responseTagInCollection); + Helpers.assertNumResults(responseTagInCollection, 3); + const jsonTagInCollection = API.getJSONFromResponse(responseTagInCollection); + assert.deepEqual( + ['a', 'c', 'g'], + jsonTagInCollection.map(tag => tag.tag).sort() + ); + }); + + it('test_should_create_a_0_tag', async function () { + let data = await API.createItem("book", { + tags: [ + { tag: "0" } + ] + }, this, 'jsonData'); + + Helpers.assertCount(1, data.tags); + assert.equal("0", data.tags[0].tag); + }); + + it('test_should_handle_negation_in_top_requests', async function () { + let key1 = await API.createItem("book", { + tags: [ + { tag: "a" }, + { tag: "b" } + ] + }, this, 'key'); + let key2 = await API.createItem("book", { + tags: [ + { tag: "a" }, + { tag: "c" } + ] + }, this, 'key'); + await API.createAttachmentItem("imported_url", [], key1, this, 'jsonData'); + await API.createAttachmentItem("imported_url", [], key2, this, 'jsonData'); + let response = await API.userGet(config.userID, "items/top?format=keys&tag=-b", { + "Content-Type": "application/json" + }); + Helpers.assert200(response); + let keys = response.data.trim().split("\n"); + assert.strictEqual(keys.length, 1); + assert.include(keys, key2); + }); + + it('testTagQuery', async function () { + const tags = ["a", "abc", "bab"]; + + await API.createItem("book", { + tags: tags.map((tag) => { + return { tag }; + }) + }, this, 'key'); + + let response = await API.userGet( + config.userID, + "tags?q=ab" + ); + Helpers.assert200(response); + Helpers.assertNumResults(response, 2); + + response = await API.userGet( + config.userID, + "tags?q=ab&qmode=startswith" + ); + Helpers.assert200(response); + Helpers.assertNumResults(response, 1); + }); + + it('testTagDiacritics', async function () { + let data = await API.createItem("book", { + tags: [ + { tag: "ëtest" }, + ] + }, this, 'jsonData'); + let version = data.version; + + data.tags = [ + { tag: "ëtest" }, + { tag: "etest" }, + ]; + + let response = await API.postItem(data, { + headers: { "Content-Type": "application/json" }, + }); + Helpers.assert200(response); + Helpers.assert200ForObject(response); + + data = await API.getItem(data.key, this, 'json'); + data = data.data; + assert.equal(version + 1, data.version); + assert.equal(2, data.tags.length); + assert.deepInclude(data.tags, { tag: "ëtest" }); + assert.deepInclude(data.tags, { tag: "etest" }); + }); + + it('test_should_change_case_of_existing_tag', async function () { + let data1 = await API.createItem("book", { + tags: [ + { tag: "a" }, + ] + }, this, 'jsonData'); + + let data2 = await API.createItem("book", { + tags: [ + { tag: "a" } + ] + }, this, 'jsonData'); + + let version = data1.version; + + data1.tags = [ + { tag: "A" }, + ]; + + let response = await API.postItem(data1); + Helpers.assert200(response); + Helpers.assert200ForObject(response); + + data1 = (await API.getItem(data1.key, this, 'json')).data; + data2 = (await API.getItem(data2.key, this, 'json')).data; + assert.equal(version + 1, data2.version); + assert.equal(1, data1.tags.length); + assert.deepInclude(data1.tags, { tag: "A" }); + assert.deepInclude(data2.tags, { tag: "a" }); + }); + + it('testKeyedItemWithTags', async function () { + const itemKey = Helpers.uniqueID(); + const createItemData = { + key: itemKey, + version: 0, + tags: [ + { tag: "a" }, + { tag: "b" } + ] + }; + const json = await API.createItem('book', createItemData, this, 'responseJSON'); + + const json2 = await API.getItem(itemKey, this, 'json'); + const data = json2.data; + assert.strictEqual(data.tags.length, 2); + assert.deepStrictEqual(data.tags[0], { tag: "a" }); + assert.deepStrictEqual(data.tags[1], { tag: "b" }); + }); + + it('testTagTooLong', async function () { + let tag = Helpers.uniqueID(300); + let json = await API.getItemTemplate("book"); + json.tags.push({ + tag: tag, + type: 1 + }); + let response = await API.postItem(json); + Helpers.assert413ForObject(response); + + json = API.getJSONFromResponse(response); + assert.equal(tag, json.failed[0].data.tag); + }); + + it('should add tag to item', async function () { + let json = await API.getItemTemplate("book"); + json.tags = [{ tag: "A" }]; + let response = await API.postItem(json); + Helpers.assert200ForObject(response); + json = await API.getJSONFromResponse(response); + json = json.successful[0].data; + + json.tags.push({ tag: "C" }); + response = await API.postItem(json); + Helpers.assert200ForObject(response); + json = await API.getJSONFromResponse(response); + json = json.successful[0].data; + + json.tags.push({ tag: "B" }); + response = await API.postItem(json); + Helpers.assert200ForObject(response); + json = await API.getJSONFromResponse(response); + json = json.successful[0].data; + + json.tags.push({ tag: "D" }); + response = await API.postItem(json); + Helpers.assert200ForObject(response); + let tags = json.tags; + json = await API.getJSONFromResponse(response); + json = json.successful[0].data; + + assert.deepEqual(tags, json.tags); + }); + + it('test_utf8mb4_tag', async function () { + let json = await API.getItemTemplate('book'); + json.tags.push({ + tag: '🐻', // 4-byte character + type: 0 + }); + + let response = await API.postItem(json, { 'Content-Type': 'application/json' }); + Helpers.assert200ForObject(response); + + let newJSON = API.getJSONFromResponse(response); + newJSON = newJSON.successful[0].data; + Helpers.assertCount(1, newJSON.tags); + assert.equal(json.tags[0].tag, newJSON.tags[0].tag); + }); + + it('testOrphanedTag', async function () { + let json = await API.createItem('book', { + tags: [{ tag: "a" }] + }, this, 'jsonData'); + let libraryVersion1 = json.version; + let itemKey1 = json.key; + + json = await API.createItem('book', { + tags: [{ tag: "b" }] + }, this, 'jsonData'); + let itemKey2 = json.key; + + json = await API.createItem("book", { + tags: [{ tag: "b" }] + }, this, 'jsonData'); + let itemKey3 = json.key; + + const response = await API.userDelete( + config.userID, + `items/${itemKey1}`, + { "If-Unmodified-Since-Version": libraryVersion1 } + ); + Helpers.assert204(response); + + const response1 = await API.userGet( + config.userID, + "tags" + ); + Helpers.assert200(response1); + Helpers.assertNumResults(response1, 1); + let json1 = API.getJSONFromResponse(response1)[0]; + assert.equal("b", json1.tag); + }); + + it('test_deleting_a_tag_should_update_a_linked_item', async function () { + let tags = ["a", "aa", "b"]; + + let itemKey = await API.createItem("book", { + tags: tags.map((tag) => { + return { tag: tag }; + }) + }, this, 'key'); + + let libraryVersion = parseInt(await API.getLibraryVersion()); + + // Make sure they're on the item + let json = await API.getItem(itemKey, this, 'json'); + let tagList = json.data.tags.map((tag) => { + return tag.tag; + }); + assert.deepEqual(tagList, tags); + + // Delete + let response = await API.userDelete( + config.userID, + "tags?tag=" + tags[0], + { "If-Unmodified-Since-Version": libraryVersion } + ); + Helpers.assert204(response); + + // Make sure they're gone from the item + response = await API.userGet( + config.userID, + "items?since=" + encodeURIComponent(libraryVersion) + ); + Helpers.assert200(response); + Helpers.assertNumResults(response, 1); + json = API.getJSONFromResponse(response); + let jsonTags = json[0].data.tags.map((tag) => { + return tag.tag; + }); + assert.deepEqual( + jsonTags, + tags.slice(1) + ); + }); +}); diff --git a/tests/remote_js/test/3/versionTest.js b/tests/remote_js/test/3/versionTest.js new file mode 100644 index 00000000..fbe1abb0 --- /dev/null +++ b/tests/remote_js/test/3/versionTest.js @@ -0,0 +1,1046 @@ +const chai = require('chai'); +const assert = chai.assert; +const config = require("../../config.js"); +const API = require('../../api3.js'); +const Helpers = require('../../helpers3.js'); +const { API3Setup, API3WrapUp } = require("../shared.js"); + +describe('VersionsTests', function () { + this.timeout(config.timeout * 2); + + before(async function () { + await API3Setup(); + }); + + after(async function () { + await API3WrapUp(); + }); + + const _capitalizeFirstLetter = (string) => { + return string.charAt(0).toUpperCase() + string.slice(1); + }; + + const _modifyJSONObject = async (objectType, json) => { + switch (objectType) { + case "collection": + json.name = "New Name " + Helpers.uniqueID(); + return json; + case "item": + json.title = "New Title " + Helpers.uniqueID(); + return json; + case "search": + json.name = "New Name " + Helpers.uniqueID(); + return json; + default: + throw new Error("Unknown object type"); + } + }; + + const _testSingleObjectLastModifiedVersion = async (objectType) => { + const objectTypePlural = API.getPluralObjectType(objectType); + + let objectKey; + switch (objectType) { + case 'collection': + objectKey = await API.createCollection('Name', false, true, 'key'); + break; + case 'item': + objectKey = await API.createItem( + 'book', + { title: 'Title' }, + true, + 'key' + ); + break; + case 'search': + objectKey = await API.createSearch( + 'Name', + [ + { + condition: 'title', + operator: 'contains', + value: 'test' + } + ], + this, + 'key' + ); + break; + } + + // JSON: Make sure all three instances of the object version + // (Last-Modified-Version, 'version', and data.version) + // match the library version + let response = await API.userGet( + config.userID, + `${objectTypePlural}/${objectKey}` + ); + Helpers.assert200(response); + let objectVersion = response.headers["last-modified-version"][0]; + let json = API.getJSONFromResponse(response); + assert.equal(objectVersion, json.version); + assert.equal(objectVersion, json.data.version); + + + // Atom: Make sure all three instances of the object version + // (Last-Modified-Version, zapi:version, and the JSON + // {$objectType}Version property match the library version + response = await API.userGet( + config.userID, + `${objectTypePlural}/${objectKey}?content=json` + ); + + Helpers.assertStatusCode(response, 200); + objectVersion = parseInt(response.headers['last-modified-version'][0]); + const xml = API.getXMLFromResponse(response); + const data = API.parseDataFromAtomEntry(xml); + json = JSON.parse(data.content); + assert.equal(objectVersion, json.version); + assert.equal(objectVersion, data.version); + response = await API.userGet( + config.userID, + `${objectTypePlural}?limit=1` + ); + Helpers.assertStatusCode(response, 200); + const libraryVersion = response.headers['last-modified-version'][0]; + assert.equal(libraryVersion, objectVersion); + _modifyJSONObject(objectType, json); + + // No If-Unmodified-Since-Version or JSON version property + delete json.version; + response = await API.userPut( + config.userID, + `${objectTypePlural}/${objectKey}`, + JSON.stringify(json) + ); + Helpers.assertStatusCode(response, 428); + + // Out of date version + response = await API.userPut( + config.userID, + `${objectTypePlural}/${objectKey}`, + JSON.stringify(json), + { + 'Content-Type': 'application/json', + 'If-Unmodified-Since-Version': objectVersion - 1 + } + ); + Helpers.assertStatusCode(response, 412); + + // Update with version header + response = await API.userPut( + config.userID, + `${objectTypePlural}/${objectKey}`, + JSON.stringify(json), + { + 'Content-Type': 'application/json', + 'If-Unmodified-Since-Version': objectVersion + } + ); + Helpers.assertStatusCode(response, 204); + + // Update object with JSON version property + const newObjectVersion = parseInt(response.headers['last-modified-version'][0]); + assert.isAbove(parseInt(newObjectVersion), parseInt(objectVersion)); + _modifyJSONObject(objectType, json); + json.version = newObjectVersion; + response = await API.userPut( + config.userID, + `${objectTypePlural}/${objectKey}`, + JSON.stringify(json) + ); + Helpers.assertStatusCode(response, 204); + const newObjectVersion2 = response.headers['last-modified-version'][0]; + assert.isAbove(parseInt(newObjectVersion2), parseInt(newObjectVersion)); + response = await API.userGet( + config.userID, + `${objectTypePlural}?limit=1` + ); + Helpers.assertStatusCode(response, 200); + const newLibraryVersion = response.headers['last-modified-version'][0]; + assert.equal(parseInt(newObjectVersion2), parseInt(newLibraryVersion)); + return; + + await API.createItem('book', { title: 'Title' }, this, 'key'); + response = await API.userGet( + config.userID, + `${objectTypePlural}/${objectKey}?limit=1` + ); + Helpers.assertStatusCode(response, 200); + const newObjectVersion3 = response.headers['last-modified-version'][0]; + assert.equal(parseInt(newLibraryVersion), parseInt(newObjectVersion3)); + response = await API.userDelete( + config.userID, + `${objectTypePlural}/${objectKey}` + ); + Helpers.assertStatusCode(response, 428); + response = await API.userDelete( + config.userID, + `${objectTypePlural}/${objectKey}`, + { 'If-Unmodified-Since-Version': objectVersion } + ); + Helpers.assertStatusCode(response, 412); + response = await API.userDelete( + config.userID, + `${objectTypePlural}/${objectKey}`, + { 'If-Unmodified-Since-Version': newObjectVersion2 } + ); + Helpers.assertStatusCode(response, 204); + }; + + const _testMultiObjectLastModifiedVersion = async (objectType) => { + await API.userClear(config.userID); + const objectTypePlural = API.getPluralObjectType(objectType); + + + let response = await API.userGet( + config.userID, + `${objectTypePlural}?limit=1` + ); + + let version = parseInt(response.headers['last-modified-version'][0]); + assert.isNumber(version); + + let json; + switch (objectType) { + case 'collection': + json = {}; + json.name = "Name"; + break; + + case 'item': + json = await API.getItemTemplate("book"); + json.creators[0].firstName = "Test"; + json.creators[0].lastName = "Test"; + break; + + case 'search': + json = {}; + json.name = "Name"; + json.conditions = []; + json.conditions.push({ + condition: "title", + operator: "contains", + value: "test" + }); + break; + } + + // Outdated library version + const headers1 = { + "Content-Type": "application/json", + "If-Unmodified-Since-Version": version - 1 + }; + response = await API.userPost( + config.userID, + `${objectTypePlural}`, + JSON.stringify([json]), + headers1 + ); + + Helpers.assertStatusCode(response, 412); + + // Make sure version didn't change during failure + response = await API.userGet( + config.userID, + `${objectTypePlural}?limit=1` + ); + + assert.equal(version, parseInt(response.headers['last-modified-version'][0])); + + // Create a new object, using library timestamp + const headers2 = { + "Content-Type": "application/json", + "If-Unmodified-Since-Version": version + }; + response = await API.userPost( + config.userID, + `${objectTypePlural}`, + JSON.stringify([json]), + headers2 + ); + Helpers.assertStatusCode(response, 200); + Helpers.assertStatusForObject(response, 'success', 0); + const version2 = parseInt(response.headers['last-modified-version'][0]); + assert.isNumber(version2); + // Version should be incremented on new object + assert.isAbove(version2, version); + + const objectKey = API.getFirstSuccessKeyFromResponse(response); + + // Check single-object request + response = await API.userGet( + config.userID, + `${objectTypePlural}/${objectKey}` + ); + Helpers.assertStatusCode(response, 200); + + version = parseInt(response.headers['last-modified-version'][0]); + assert.isNumber(version); + assert.equal(version2, version); + json = API.getJSONFromResponse(response).data; + + json.key = objectKey; + // Modify object + switch (objectType) { + case 'collection': + json.name = "New Name"; + break; + + case 'item': + json.title = "New Title"; + break; + + case 'search': + json.name = "New Name"; + break; + } + + delete json.version; + + // No If-Unmodified-Since-Version or object version property + response = await API.userPost( + config.userID, + `${objectTypePlural}`, + JSON.stringify([json]), + { + "Content-Type": "application/json" + } + ); + Helpers.assertStatusForObject(response, 'failed', 0, 428); + + json.version = version - 1; + + response = await API.userPost( + config.userID, + `${objectTypePlural}`, + JSON.stringify([json]), + { + "Content-Type": "application/json", + } + ); + // Outdated object version property + const message = `${_capitalizeFirstLetter(objectType)} has been modified since specified version (expected ${json.version}, found ${version2})`; + Helpers.assertStatusForObject(response, 'failed', 0, 412, message); + // Modify object, using object version property + json.version = version; + + response = await API.userPost( + config.userID, + `${objectTypePlural}`, + JSON.stringify([json]), + { + "Content-Type": "application/json", + } + ); + Helpers.assertStatusCode(response, 200); + Helpers.assertStatusForObject(response, 'success', 0); + // Version should be incremented on modified object + const version3 = parseInt(response.headers['last-modified-version'][0]); + assert.isNumber(version3); + assert.isAbove(version3, version2); + // Check library version + response = await API.userGet( + config.userID, + `${objectTypePlural}` + ); + version = parseInt(response.headers['last-modified-version'][0]); + assert.isNumber(version); + assert.equal(version, version3); + // Check single-object request + response = await API.userGet( + config.userID, + `${objectTypePlural}/${objectKey}` + ); + version = parseInt(response.headers['last-modified-version'][0]); + assert.isNumber(version); + assert.equal(version, version3); + }; + + const _testMultiObject304NotModified = async (objectType) => { + const objectTypePlural = API.getPluralObjectType(objectType); + + let response = await API.userGet( + config.userID, + `${objectTypePlural}` + ); + + const version = parseInt(response.headers['last-modified-version'][0]); + assert.isNumber(version); + + response = await API.userGet( + config.userID, + `${objectTypePlural}`, + { 'If-Modified-Since-Version': version } + ); + Helpers.assertStatusCode(response, 304); + }; + + const _testSinceAndVersionsFormat = async (objectType, sinceParam) => { + const objectTypePlural = API.getPluralObjectType(objectType); + + const objArray = []; + + switch (objectType) { + case 'collection': + objArray.push(await API.createCollection("Name", false, true, 'jsonData')); + objArray.push(await API.createCollection("Name", false, true, 'jsonData')); + objArray.push(await API.createCollection("Name", false, true, 'jsonData')); + break; + + case 'item': + objArray.push(await API.createItem("book", { + title: "Title" + }, true, 'jsonData')); + objArray.push(await API.createNoteItem("Foo", objArray[0].key, true, 'jsonData')); + objArray.push(await API.createItem("book", { + title: "Title" + }, true, 'jsonData')); + objArray.push(await API.createItem("book", { + title: "Title" + }, true, 'jsonData')); + break; + + + case 'search': + objArray.push(await API.createSearch( + "Name", [{ + condition: "title", + operator: "contains", + value: "test" + }], + true, + 'jsonData' + )); + objArray.push(await API.createSearch( + "Name", [{ + condition: "title", + operator: "contains", + value: "test" + }], + true, + 'jsonData' + )); + objArray.push(await API.createSearch( + "Name", [{ + condition: "title", + operator: "contains", + value: "test" + }], + true, + 'jsonData' + )); + } + + let objects = [...objArray]; + + const firstVersion = objects[0].version; + + let response = await API.userGet( + config.userID, + `${objectTypePlural}?format=versions&${sinceParam}=${firstVersion}`, { + "Content-Type": "application/json" + } + ); + Helpers.assertStatusCode(response, 200); + let json = JSON.parse(response.data); + assert.ok(json); + Helpers.assertCount(Object.keys(objects).length - 1, json); + + let keys = Object.keys(json); + + let keyIndex = 0; + if (objectType == 'item') { + assert.equal(objects[3].key, keys[0]); + assert.equal(objects[3].version, json[keys[0]]); + keyIndex += 1; + } + + assert.equal(objects[2].key, keys[keyIndex]); + assert.equal(objects[2].version, json[objects[2].key]); + assert.equal(objects[1].key, keys[keyIndex + 1]); + assert.equal(objects[1].version, json[objects[1].key]); + + // Test /top for items + if (objectType == 'item') { + response = await API.userGet( + config.userID, + `items/top?format=versions&${sinceParam}=${firstVersion}` + ); + + Helpers.assert200(response); + json = JSON.parse(response.data); + assert.ok(json); + assert.equal(objects.length - 2, Object.keys(json).length);// Exclude first item and child + + keys = Object.keys(json); + + objects = [...objArray]; + + assert.equal(objects[3].key, keys[0]); + assert.equal(objects[3].version, json[keys[0]]); + assert.equal(objects[2].key, keys[1]); + assert.equal(objects[2].version, json[keys[1]]); + } + }; + + const _testUploadUnmodified = async (objectType) => { + let objectTypePlural = API.getPluralObjectType(objectType); + let data, version, response, json; + + switch (objectType) { + case "collection": + data = await API.createCollection("Name", false, true, 'jsonData'); + break; + + case "item": + data = await API.createItem("book", { title: "Title" }, true, 'jsonData'); + break; + + case "search": + data = await API.createSearch("Name", "default", true, 'jsonData'); + break; + } + + version = data.version; + assert.notEqual(0, version); + + response = await API.userPut( + config.userID, + `${objectTypePlural}/${data.key}`, + JSON.stringify(data), + { "Content-Type": "application/json" } + ); + + Helpers.assertStatusCode(response, 204); + assert.equal(version, response.headers["last-modified-version"][0]); + + switch (objectType) { + case "collection": + json = await API.getCollection(data.key, true, 'json'); + break; + + case "item": + json = await API.getItem(data.key, true, 'json'); + break; + + case "search": + json = await API.getSearch(data.key, true, 'json'); + break; + } + + assert.equal(version, json.version); + }; + + const _testTagsSince = async (param) => { + const tags1 = ["a", "aa", "b"]; + const tags2 = ["b", "c", "cc"]; + + const data1 = await API.createItem("book", { + tags: tags1.map((tag) => { + return { tag: tag }; + }) + }, true, 'jsonData'); + + await API.createItem("book", { + tags: tags2.map((tag) => { + return { tag: tag }; + }) + }, true, 'jsonData'); + + // Only newly added tags should be included in newer, + // not previously added tags or tags added to items + let response = await API.userGet( + config.userID, + `tags?${param}=${data1.version}` + ); + Helpers.assertNumResults(response, 2); + + // Deleting an item shouldn't update associated tag versions + response = await API.userDelete( + config.userID, + `items/${data1.key}`, + { + "If-Unmodified-Since-Version": data1.version + } + ); + Helpers.assertStatusCode(response, 204); + + response = await API.userGet( + config.userID, + `tags?${param}=${data1.version}` + ); + Helpers.assertNumResults(response, 2); + let libraryVersion = parseInt(response.headers["last-modified-version"][0]); + + response = await API.userGet( + config.userID, + `tags?${param}=${libraryVersion}` + ); + Helpers.assertNumResults(response, 0); + }; + + const _testPatchMissingObjectsWithVersion = async function (objectType) { + let objectTypePlural = API.getPluralObjectType(objectType); + let json = await API.createUnsavedDataObject(objectType); + json.key = 'TPMBJSWV'; + json.version = 123; + let response = await API.userPost( + config.userID, + `${objectTypePlural}`, + JSON.stringify([json]), + { "Content-Type": "application/json" } + ); + Helpers.assert404ForObject( + response, + `${objectType} doesn't exist (expected version 123; use 0 instead)` + ); + }; + + const _testPatchMissingObjectWithVersion0Header = async function (objectType) { + const objectTypePlural = API.getPluralObjectType(objectType); + const json = await API.createUnsavedDataObject(objectType); + const response = await API.userPatch( + config.userID, + `${objectTypePlural}/TPMBWVZH`, + JSON.stringify(json), + { 'Content-Type': 'application/json', 'If-Unmodified-Since-Version': '0' }, + ); + Helpers.assert204(response); + }; + + const _testPatchExistingObjectsWithOldVersionProperty = async function (objectType) { + const objectTypePlural = API.getPluralObjectType(objectType); + + const key = await API.createDataObject(objectType, null, null, 'key'); + let json = await API.createUnsavedDataObject(objectType); + json.key = key; + json.version = 1; + + const response = await API.userPost( + config.userID, + `${objectTypePlural}`, + JSON.stringify([json]), + { "Content-Type": "application/json" } + ); + Helpers.assert412ForObject(response); + }; + + const _testPatchMissingObjectWithVersionHeader = async function (objectType) { + const objectTypePlural = API.getPluralObjectType(objectType); + const json = await API.createUnsavedDataObject(objectType); + const response = await API.userPatch( + config.userID, + `${objectTypePlural}/TPMBJWVH`, + JSON.stringify(json), + { "Content-Type": "application/json", "If-Unmodified-Since-Version": "123" } + ); + Helpers.assert404(response); + }; + + const _testPatchExistingObjectWithOldVersionProperty = async function (objectType) { + const objectTypePlural = API.getPluralObjectType(objectType); + + const key = await API.createDataObject(objectType, null, null, 'key'); + let json = await API.createUnsavedDataObject(objectType); + json.version = 1; + + const response = await API.userPatch( + config.userID, + `${objectTypePlural}/${key}`, + JSON.stringify(json), + { "Content-Type": "application/json" } + ); + Helpers.assert412(response); + }; + + const _testPatchExistingObjectsWithoutVersionWithHeader = async function (objectType) { + const objectTypePlural = API.getPluralObjectType(objectType); + + const existing = await API.createDataObject(objectType, null, null, 'json'); + const key = existing.key; + const libraryVersion = existing.version; + let json = await API.createUnsavedDataObject(objectType); + json.key = key; + + const response = await API.userPost( + config.userID, + `${objectTypePlural}`, + JSON.stringify([json]), + { "Content-Type": "application/json" } + ); + Helpers.assert428ForObject(response); + }; + + const _testPatchMissingObjectsWithVersion0Property = async function (objectType) { + let objectTypePlural = API.getPluralObjectType(objectType); + let json = await API.createUnsavedDataObject(objectType); + json.key = 'TPMSWVZP'; + json.version = 0; + + let response = await API.userPost( + config.userID, + objectTypePlural, + JSON.stringify([json]), + { 'Content-Type': 'application/json' }); + Helpers.assert200ForObject(response); + + // POST with version > 0 to a missing object is a 404 for that object + }; + + const _testPatchExistingObjectWithVersion0Property = async function (objectType) { + let objectTypePlural = API.getPluralObjectType(objectType); + + let key = await API.createDataObject(objectType, null, null, 'key'); + let json = await API.createUnsavedDataObject(objectType); + json.version = 0; + + let response = await API.userPatch( + config.userID, + `${objectTypePlural}/${key}`, + JSON.stringify(json), + { + "Content-Type": "application/json" + } + ); + Helpers.assert412(response); + }; + + const _testPatchMissingObjectWithVersionProperty = async function (objectType) { + const objectTypePlural = API.getPluralObjectType(objectType); + + let json = await API.createUnsavedDataObject(objectType); + json.version = 123; + + const response = await API.userPatch( + config.userID, + `${objectTypePlural}/TPMBJWVP`, + JSON.stringify(json), + { "Content-Type": "application/json" } + ); + Helpers.assert404(response); + }; + + const _testPatchExistingObjectsWithVersion0Property = async (objectType) => { + let objectTypePlural = API.getPluralObjectType(objectType); + + let key = await API.createDataObject(objectType, null, null, 'key'); + let json = await API.createUnsavedDataObject(objectType); + json.key = key; + json.version = 0; + + let response = await API.userPost( + config.userID, + objectTypePlural, + JSON.stringify([json]), + { "Content-Type": "application/json" } + ); + Helpers.assert412ForObject(response); + }; + + const _testPostExistingLibraryWithVersion0Header = async function (objectType) { + let objectTypePlural = API.getPluralObjectType(objectType); + + let json = await API.createUnsavedDataObject(objectType); + + let response = await API.userPost( + config.userID, + objectTypePlural, + JSON.stringify([json]), + { + "Content-Type": "application/json", + "If-Unmodified-Since-Version": "0" + } + ); + Helpers.assert412(response); + }; + + const _testPatchExistingObjectWithVersion0Header = async function (objectType) { + const objectTypeName = API.getPluralObjectType(objectType); + let key = await API.createDataObject(objectType, null, null, 'key'); + const json = await API.createUnsavedDataObject(objectType); + const headers = { + "Content-Type": "application/json", + "If-Unmodified-Since-Version": "0" + }; + let response = await API.userPatch( + config.userID, + `${objectTypeName}/${key}`, + JSON.stringify(json), + headers + ); + Helpers.assert412(response); + }; + + const _testPatchExistingObjectWithoutVersion = async function (objectType) { + let objectTypePlural = API.getPluralObjectType(objectType); + let key = await API.createDataObject(objectType, null, null, 'key'); + let json = await API.createUnsavedDataObject(objectType); + let headers = { "Content-Type": "application/json" }; + + let response = await API.userPatch( + config.userID, + `${objectTypePlural}/${key}`, + JSON.stringify(json), + headers + ); + Helpers.assert428(response); + }; + + const _testPatchExistingObjectWithOldVersionHeader = async function (objectType) { + const objectTypePlural = API.getPluralObjectType(objectType); + + let key = await API.createDataObject(objectType, null, null, 'key'); + let json = await API.createUnsavedDataObject(objectType); + + let headers = { + "Content-Type": "application/json", + "If-Unmodified-Since-Version": "1" + }; + + let response = await API.userPatch( + config.userID, + `${objectTypePlural}/${key}`, + JSON.stringify(json), + headers + ); + + Helpers.assert412(response); + }; + + const _testPatchMissingObjectWithVersion0Property = async (objectType) => { + const objectTypePlural = API.getPluralObjectType(objectType); + + let json = await API.createUnsavedDataObject(objectType); + json.version = 0; + + let response = await API.userPatch( + config.userID, + `${objectTypePlural}/TPMBWVZP`, + JSON.stringify(json), + { + "Content-Type": "application/json" + } + ); + Helpers.assert204(response); + }; + + const _testPatchMissingObjectWithoutVersion = async function (objectType) { + let objectTypePlural = API.getPluralObjectType(objectType); + let json = await API.createUnsavedDataObject(objectType); + let response = await API.userPatch( + config.userID, + `${objectTypePlural}/TPMBJWNV`, + JSON.stringify(json), + { + "Content-Type": "application/json" + } + ); + Helpers.assert404(response); + }; + + const _testPatchExistingObjectsWithoutVersionWithoutHeader = async (objectType) => { + const objectTypePlural = API.getPluralObjectType(objectType); + let key = await API.createDataObject(objectType, null, null, 'key'); + let json = await API.createUnsavedDataObject(objectType); + json.key = key; + let response = await API.userPost( + config.userID, + objectTypePlural, + JSON.stringify([json]), + { "Content-Type": "application/json" } + ); + Helpers.assert428ForObject(response); + }; + + + it('testTagsSince', async function () { + await _testTagsSince('since'); + await API.userClear(config.userID); + await _testTagsSince('newer'); + }); + + it('testSingleObjectLastModifiedVersion', async function () { + await _testSingleObjectLastModifiedVersion('collection'); + await _testSingleObjectLastModifiedVersion('item'); + await _testSingleObjectLastModifiedVersion('search'); + }); + + it('testMultiObjectLastModifiedVersion', async function () { + await _testMultiObjectLastModifiedVersion('collection'); + await _testMultiObjectLastModifiedVersion('item'); + await _testMultiObjectLastModifiedVersion('search'); + }); + + it('testMultiObject304NotModified', async function () { + await _testMultiObject304NotModified('collection'); + await _testMultiObject304NotModified('item'); + await _testMultiObject304NotModified('search'); + await _testMultiObject304NotModified('setting'); + await _testMultiObject304NotModified('tag'); + }); + + it('testSinceAndVersionsFormat', async function () { + await _testSinceAndVersionsFormat('collection', 'since'); + await _testSinceAndVersionsFormat('item', 'since'); + await _testSinceAndVersionsFormat('search', 'since'); + await API.userClear(config.userID); + await _testSinceAndVersionsFormat('collection', 'newer'); + await _testSinceAndVersionsFormat('item', 'newer'); + await _testSinceAndVersionsFormat('search', 'newer'); + }); + + it('testUploadUnmodified', async function () { + await _testUploadUnmodified('collection'); + await _testUploadUnmodified('item'); + await _testUploadUnmodified('search'); + }); + + it('test_should_include_library_version_for_412', async function () { + let json = await API.createItem("book", [], this, 'json'); + let libraryVersion = json.version; + json.data.version--; + let response = await API.userPut( + config.userID, + "items/" + json.key, + JSON.stringify(json), + { + "Content-Type": "application/json", + "If-Unmodified-Since-Version": (json.version - 1) + } + ); + Helpers.assert412(response); + assert.equal(libraryVersion, response.headers['last-modified-version'][0]); + }); + + it('testPatchExistingObjectWithOldVersionHeader', async function () { + await _testPatchExistingObjectWithOldVersionHeader('collection'); + await _testPatchExistingObjectWithOldVersionHeader('item'); + await _testPatchExistingObjectWithOldVersionHeader('search'); + }); + + it('testPatchMissingObjectWithVersionHeader', async function () { + await _testPatchMissingObjectWithVersionHeader('collection'); + await _testPatchMissingObjectWithVersionHeader('item'); + await _testPatchMissingObjectWithVersionHeader('search'); + }); + + it('testPostToSettingsWithOutdatedVersionHeader', async function () { + let libraryVersion = await API.getLibraryVersion(); + // Outdated library version + let response = await API.userPost( + config.userID, + "settings", + JSON.stringify({}), + { + "Content-Type": "application/json", + "If-Unmodified-Since-Version": (libraryVersion - 1) + } + ); + Helpers.assert412(response); + }); + + it('testPatchExistingObjectsWithOldVersionProperty', async function () { + await _testPatchExistingObjectsWithOldVersionProperty('collection'); + await _testPatchExistingObjectsWithOldVersionProperty('item'); + await _testPatchExistingObjectsWithOldVersionProperty('search'); + }); + + it('testPatchExistingObjectsWithoutVersionWithoutHeader', async function () { + await _testPatchExistingObjectsWithoutVersionWithoutHeader('collection'); + await _testPatchExistingObjectsWithoutVersionWithoutHeader('item'); + await _testPatchExistingObjectsWithoutVersionWithoutHeader('search'); + }); + + it('testPatchMissingObjectWithVersion0Header', async function () { + await _testPatchMissingObjectWithVersion0Header('collection'); + await _testPatchMissingObjectWithVersion0Header('item'); + await _testPatchMissingObjectWithVersion0Header('search'); + }); + + it('testPatchExistingObjectsWithoutVersionWithHeader', async function () { + await _testPatchExistingObjectsWithoutVersionWithHeader('collection'); + await _testPatchExistingObjectsWithoutVersionWithHeader('item'); + await _testPatchExistingObjectsWithoutVersionWithHeader('search'); + }); + + it('testPatchMissingObjectWithoutVersion', async function () { + await _testPatchMissingObjectWithoutVersion('collection'); + await _testPatchMissingObjectWithoutVersion('item'); + await _testPatchMissingObjectWithoutVersion('search'); + }); + + it('test_should_not_include_library_version_for_400', async function () { + let json = await API.createItem("book", [], this, 'json'); + let libraryVersion = json.version; + let response = await API.userPut( + config.userID, + "items/" + json.key, + JSON.stringify(json), + { + "Content-Type": "application/json", + "If-Unmodified-Since-Version": (json.version - 1) + } + ); + Helpers.assert400(response); + assert.notOk(response.headers['last-modified-version']); + }); + + it('testPatchMissingObjectsWithVersion', async function () { + await _testPatchMissingObjectsWithVersion('collection'); + await _testPatchMissingObjectsWithVersion('item'); + await _testPatchMissingObjectsWithVersion('search'); + }); + + it('testPatchExistingObjectWithVersion0Property', async function () { + await _testPatchExistingObjectWithVersion0Property('collection'); + await _testPatchExistingObjectWithVersion0Property('item'); + await _testPatchExistingObjectWithVersion0Property('search'); + }); + + it('testPatchMissingObjectsWithVersion0Property', async function () { + await _testPatchMissingObjectsWithVersion0Property('collection'); + await _testPatchMissingObjectsWithVersion0Property('item'); + await _testPatchMissingObjectsWithVersion0Property('search'); + }); + + it('testPatchExistingObjectWithoutVersion', async function () { + await _testPatchExistingObjectWithoutVersion('search'); + }); + + it('testPostExistingLibraryWithVersion0Header', async function () { + await _testPostExistingLibraryWithVersion0Header('collection'); + await _testPostExistingLibraryWithVersion0Header('item'); + await _testPostExistingLibraryWithVersion0Header('search'); + }); + + it('testPatchExistingObjectWithVersion0Header', async function () { + await _testPatchExistingObjectWithVersion0Header('collection'); + await _testPatchExistingObjectWithVersion0Header('item'); + await _testPatchExistingObjectWithVersion0Header('search'); + }); + + it('testPatchMissingObjectWithVersionProperty', async function () { + await _testPatchMissingObjectWithVersionProperty('collection'); + await _testPatchMissingObjectWithVersionProperty('item'); + await _testPatchMissingObjectWithVersionProperty('search'); + }); + + it('testPatchExistingObjectWithOldVersionProperty', async function () { + await _testPatchExistingObjectWithOldVersionProperty('collection'); + await _testPatchExistingObjectWithOldVersionProperty('item'); + await _testPatchExistingObjectWithOldVersionProperty('search'); + }); + + it('testPatchExistingObjectsWithVersion0Property', async function () { + await _testPatchExistingObjectsWithVersion0Property('collection'); + await _testPatchExistingObjectsWithVersion0Property('item'); + await _testPatchExistingObjectsWithVersion0Property('search'); + }); + + it('testPatchMissingObjectWithVersion0Property', async function () { + await _testPatchMissingObjectWithVersion0Property('collection'); + await _testPatchMissingObjectWithVersion0Property('item'); + await _testPatchMissingObjectWithVersion0Property('search'); + }); +}); From 2f5ccd08a2bd1a24ec33db429428cb7ba45c9218 Mon Sep 17 00:00:00 2001 From: Bogdan Abaev Date: Tue, 30 May 2023 13:07:10 -0400 Subject: [PATCH 14/33] v3 itemTest --- tests/remote_js/api3.js | 15 +- tests/remote_js/test/2/itemsTest.js | 8 +- tests/remote_js/test/3/itemTest.js | 2819 +++++++++++++++++++++++++++ tests/remote_js/test/shared.js | 3 +- 4 files changed, 2832 insertions(+), 13 deletions(-) create mode 100644 tests/remote_js/test/3/itemTest.js diff --git a/tests/remote_js/api3.js b/tests/remote_js/api3.js index 42c15f2b..8ee98481 100644 --- a/tests/remote_js/api3.js +++ b/tests/remote_js/api3.js @@ -218,7 +218,7 @@ class API3 extends API2 { static async groupCreateItem(groupID, itemType, data = [], context = null, returnFormat = 'responseJSON') { let response = await this.get(`items/new?itemType=${itemType}`); - let json = JSON.parse(await response.data); + let json = JSON.parse(response.data); if (data) { for (let field in data) { @@ -228,7 +228,7 @@ class API3 extends API2 { response = await this.groupPost( groupID, - `items?key=${this.apiKey}`, + `items?key=${this.config.apiKey}`, JSON.stringify([json]), { "Content-Type": "application/json" } ); @@ -453,6 +453,11 @@ class API3 extends API2 { return this.delete(url, headers, auth); } + static async groupDelete(groupID, suffix, headers = {}, auth = false) { + let url = `groups/${groupID}/${suffix}`; + return this.delete(url, headers, auth); + } + static getSuccessfulKeysFromResponse(response) { let json = this.getJSONFromResponse(response); return Object.keys(json.successful).map((o) => { @@ -722,14 +727,14 @@ class API3 extends API2 { } } - static async createAnnotationItem(annotationType, data = [], parentKey, context = false, returnFormat = 'responseJSON') { + static async createAnnotationItem(annotationType, data = {}, parentKey, context = false, returnFormat = 'responseJSON') { let response = await this.get(`items/new?itemType=annotation&annotationType=${annotationType}`); - let json = await response.json(); + let json = JSON.parse(response.data); json.parentItem = parentKey; if (annotationType === 'highlight') { json.annotationText = 'This is highlighted text.'; } - if (data.annotationComment !== undefined) { + if (data.annotationComment) { json.annotationComment = data.annotationComment; } json.annotationColor = '#ff8c19'; diff --git a/tests/remote_js/test/2/itemsTest.js b/tests/remote_js/test/2/itemsTest.js index 1ba7b094..0ede51db 100644 --- a/tests/remote_js/test/2/itemsTest.js +++ b/tests/remote_js/test/2/itemsTest.js @@ -449,7 +449,7 @@ describe('ItemsTests', function () { items: [data], }), ); - Helpers.assertStatusCode(response, 200); + Helpers.assert200ForObject(response, 200); xml = await API.getItemXML(data.itemKey); data = API.parseDataFromAtomEntry(xml); const json = JSON.parse(data.content); @@ -514,12 +514,6 @@ describe('ItemsTests', function () { this.skip(); //disabled }); - it('testNewEmptyLinkAttachmentItem', async function () { - const key = await API.createItem("book", false, true, 'key'); - const xml = await API.createAttachmentItem("linked_url", [], key, true, 'atom'); - await API.parseDataFromAtomEntry(xml); - }); - it('testNewEmptyLinkAttachmentItemWithItemKey', async function () { const key = await API.createItem("book", false, true, 'key'); await API.createAttachmentItem("linked_url", [], key, true, 'atom'); diff --git a/tests/remote_js/test/3/itemTest.js b/tests/remote_js/test/3/itemTest.js new file mode 100644 index 00000000..3e474d29 --- /dev/null +++ b/tests/remote_js/test/3/itemTest.js @@ -0,0 +1,2819 @@ +const chai = require('chai'); +const assert = chai.assert; +const config = require("../../config.js"); +const API = require('../../api3.js'); +const Helpers = require('../../helpers3.js'); +const { API3Setup, API3WrapUp, resetGroups, retryIfNeeded } = require("../shared.js"); + +describe('ItemsTests', function () { + this.timeout(0); + + before(async function () { + await API3Setup(); + await resetGroups(); + }); + + after(async function () { + await API3WrapUp(); + }); + + this.beforeEach(async function () { + await retryIfNeeded(async () => { + await API.userClear(config.userID); + }); + await retryIfNeeded(async () => { + await API.groupClear(config.ownedPrivateGroupID); + }); + API.useAPIKey(config.apiKey); + }); + + const testNewEmptyBookItem = async () => { + let json = await API.createItem("book", false, true); + json = json.successful[0].data; + assert.equal(json.itemType, "book"); + assert.equal(json.title, ""); + assert.equal(json.date, ""); + assert.equal(json.place, ""); + return json; + }; + + it('testNewEmptyBookItemMultiple', async function () { + let json = await API.getItemTemplate("book"); + + const data = []; + json.title = "A"; + data.push(json); + const json2 = Object.assign({}, json); + json2.title = "B"; + data.push(json2); + const json3 = Object.assign({}, json); + json3.title = "C"; + json3.numPages = 200; + data.push(json3); + + const response = await API.postItems(data); + Helpers.assertStatusCode(response, 200); + let libraryVersion = parseInt(response.headers['last-modified-version'][0]); + json = await API.getJSONFromResponse(response); + Helpers.assertCount(3, json.successful); + Helpers.assertCount(3, json.success); + + for (let i = 0; i < 3; i++) { + assert.equal(json.successful[i].key, json.successful[i].data.key); + assert.equal(libraryVersion, json.successful[i].version); + assert.equal(libraryVersion, json.successful[i].data.version); + assert.equal(data[i].title, json.successful[i].data.title); + } + + assert.equal(data[2].numPages, json.successful[2].data.numPages); + + json = await API.getItem(Object.keys(json.success).map(k => json.success[k]), this, 'json'); + + + assert.equal(json[0].data.title, "A"); + assert.equal(json[1].data.title, "B"); + assert.equal(json[2].data.title, "C"); + }); + + it('testEditBookItem', async function () { + const newBookItem = await testNewEmptyBookItem(); + const key = newBookItem.key; + const version = newBookItem.version; + + const newTitle = 'New Title'; + const numPages = 100; + const creatorType = 'author'; + const firstName = 'Firstname'; + const lastName = 'Lastname'; + + newBookItem.title = newTitle; + newBookItem.numPages = numPages; + newBookItem.creators.push({ + creatorType: creatorType, + firstName: firstName, + lastName: lastName + }); + + const response = await API.userPut( + config.userID, + `items/${key}`, + JSON.stringify(newBookItem), + { + headers: { + 'Content-Type': 'application/json', + 'If-Unmodified-Since-Version': version + } + } + ); + Helpers.assertStatusCode(response, 204); + + let json = (await API.getItem(key, true, 'json')).data; + + assert.equal(newTitle, json.title); + assert.equal(numPages, json.numPages); + assert.equal(creatorType, json.creators[0].creatorType); + assert.equal(firstName, json.creators[0].firstName); + assert.equal(lastName, json.creators[0].lastName); + }); + + it('testDateModified', async function () { + const objectType = 'item'; + const objectTypePlural = API.getPluralObjectType(objectType); + // In case this is ever extended to other objects + let json; + let itemData; + switch (objectType) { + case 'item': + itemData = { + title: "Test" + }; + json = await API.createItem("videoRecording", itemData, this, 'jsonData'); + break; + } + + const objectKey = json.key; + const dateModified1 = json.dateModified; + + // Make sure we're in the next second + await new Promise(resolve => setTimeout(resolve, 1000)); + + // + // If no explicit dateModified, use current timestamp + // + json.title = 'Test 2'; + delete json.dateModified; + let response = await API.userPut( + config.userID, + `${objectTypePlural}/${objectKey}`, + JSON.stringify(json) + ); + Helpers.assertStatusCode(response, 204); + + switch (objectType) { + case 'item': + json = (await API.getItem(objectKey, true, 'json')).data; + break; + } + + const dateModified2 = json.dateModified; + assert.notEqual(dateModified1, dateModified2); + + // Make sure we're in the next second + await new Promise(resolve => setTimeout(resolve, 1000)); + + // + // If existing dateModified, use current timestamp + // + json.title = 'Test 3'; + json.dateModified = dateModified2; + response = await API.userPut( + config.userID, + `${objectTypePlural}/${objectKey}`, + JSON.stringify(json) + ); + Helpers.assertStatusCode(response, 204); + + switch (objectType) { + case 'item': + json = (await API.getItem(objectKey, true, 'json')).data; + break; + } + + const dateModified3 = json.dateModified; + assert.notEqual(dateModified2, dateModified3); + + // + // If explicit dateModified, use that + // + const newDateModified = "2013-03-03T21:33:53Z"; + json.title = 'Test 4'; + json.dateModified = newDateModified; + response = await API.userPut( + config.userID, + `${objectTypePlural}/${objectKey}? `, + JSON.stringify(json) + ); + Helpers.assertStatusCode(response, 204); + + switch (objectType) { + case 'item': + json = (await API.getItem(objectKey, true, 'json')).data; + break; + } + const dateModified4 = json.dateModified; + assert.equal(newDateModified, dateModified4); + }); + + it('testDateAccessedInvalid', async function () { + const date = 'February 1, 2014'; + const response = await API.createItem("book", { accessDate: date }, true, 'response'); + // Invalid dates should be ignored + Helpers.assert400ForObject(response, { message: "'accessDate' must be in ISO 8601 or UTC 'YYYY-MM-DD[ hh:mm:ss]' format or 'CURRENT_TIMESTAMP' (February 1, 2014)" }); + }); + + it('testChangeItemType', async function () { + const json = await API.getItemTemplate("book"); + json.title = "Foo"; + json.numPages = 100; + + const response = await API.userPost( + config.userID, + "items", + JSON.stringify([json]), + { "Content-Type": "application/json" } + ); + + const key = API.getFirstSuccessKeyFromResponse(response); + const json1 = (await API.getItem(key, true, 'json')).data; + const version = json1.version; + + const json2 = await API.getItemTemplate("bookSection"); + + Object.entries(json2).forEach(([field, _]) => { + if (field !== "itemType" && json1[field]) { + json2[field] = json1[field]; + } + }); + + const response2 = await API.userPut( + config.userID, + "items/" + key, + JSON.stringify(json2), + { "Content-Type": "application/json", "If-Unmodified-Since-Version": version } + ); + + Helpers.assertStatusCode(response2, 204); + + const json3 = (await API.getItem(key, true, 'json')).data; + assert.equal(json3.itemType, "bookSection"); + assert.equal(json3.title, "Foo"); + assert.notProperty(json3, "numPages"); + }); + + it('testPatchItem', async function () { + const itemData = { + title: "Test" + }; + const json = await API.createItem("book", itemData, this, 'jsonData'); + let itemKey = json.key; + let itemVersion = json.version; + + const patch = async (itemKey, itemVersion, itemData, newData) => { + for (const field in newData) { + itemData[field] = newData[field]; + } + const response = await API.userPatch( + config.userID, + "items/" + itemKey + "?key=" + config.apiKey, + JSON.stringify(newData), + { + "Content-Type": "application/json", + "If-Unmodified-Since-Version": itemVersion + } + ); + Helpers.assertStatusCode(response, 204); + const json = (await API.getItem(itemKey, true, 'json')).data; + + for (const field in itemData) { + assert.deepEqual(itemData[field], json[field]); + } + const headerVersion = parseInt(response.headers["last-modified-version"][0]); + assert.isAbove(headerVersion, itemVersion); + assert.equal(json.version, headerVersion); + + return headerVersion; + }; + + let newData = { + date: "2013" + }; + itemVersion = await patch(itemKey, itemVersion, itemData, newData); + + newData = { + title: "" + }; + itemVersion = await patch(itemKey, itemVersion, itemData, newData); + + newData = { + tags: [ + { tag: "Foo" } + ] + }; + itemVersion = await patch(itemKey, itemVersion, itemData, newData); + + newData = { + tags: [] + }; + itemVersion = await patch(itemKey, itemVersion, itemData, newData); + + const key = await API.createCollection('Test', false, this, 'key'); + newData = { + collections: [key] + }; + itemVersion = await patch(itemKey, itemVersion, itemData, newData); + + newData = { + collections: [] + }; + await patch(itemKey, itemVersion, itemData, newData); + }); + + it('testPatchItems', async function () { + const itemData = { + title: "Test" + }; + const json = await API.createItem("book", itemData, this, 'jsonData'); + let itemKey = json.key; + let itemVersion = json.version; + + const patch = async (itemKey, itemVersion, itemData, newData) => { + for (const field in newData) { + itemData[field] = newData[field]; + } + newData.key = itemKey; + newData.version = itemVersion; + + const response = await API.userPost( + config.userID, + "items", + JSON.stringify([newData]), + { + "Content-Type": "application/json" + } + ); + Helpers.assert200ForObject(response); + const json = (await API.getItem(itemKey, true, 'json')).data; + + for (const field in itemData) { + assert.deepEqual(itemData[field], json[field]); + } + const headerVersion = parseInt(response.headers["last-modified-version"][0]); + assert.isAbove(headerVersion, itemVersion); + assert.equal(json.version, headerVersion); + + return headerVersion; + }; + + let newData = { + date: "2013" + }; + itemVersion = await patch(itemKey, itemVersion, itemData, newData); + + newData = { + title: "" + }; + itemVersion = await patch(itemKey, itemVersion, itemData, newData); + + newData = { + tags: [ + { tag: "Foo" } + ] + }; + itemVersion = await patch(itemKey, itemVersion, itemData, newData); + + newData = { + tags: [] + }; + itemVersion = await patch(itemKey, itemVersion, itemData, newData); + + const key = await API.createCollection('Test', false, this, 'key'); + newData = { + collections: [key] + }; + itemVersion = await patch(itemKey, itemVersion, itemData, newData); + + newData = { + collections: [] + }; + await patch(itemKey, itemVersion, itemData, newData); + }); + + it('testNewComputerProgramItem', async function () { + const data = await API.createItem('computerProgram', false, true, 'jsonData'); + const key = data.key; + assert.equal(data.itemType, 'computerProgram'); + + const version = '1.0'; + data.versionNumber = version; + + const response = await API.userPut( + config.userID, + `items/${key}`, + JSON.stringify(data), + { "Content-Type": "application/json", "If-Unmodified-Since-Version": data.version } + ); + + Helpers.assertStatusCode(response, 204); + const json = await API.getItem(key, true, 'json'); + assert.equal(json.data.versionNumber, version); + }); + + it('testNewInvalidBookItem', async function () { + const json = await API.getItemTemplate("book"); + + // Missing item type + const json2 = { ...json }; + delete json2.itemType; + let response = await API.userPost( + config.userID, + `items`, + JSON.stringify([json2]), + { "Content-Type": "application/json" } + ); + Helpers.assertStatusForObject(response, 'failed', 0, 400, "'itemType' property not provided"); + + // contentType on non-attachment + const json3 = { ...json }; + json3.contentType = "text/html"; + response = await API.userPost( + config.userID, + `items`, + JSON.stringify([json3]), + { "Content-Type": "application/json" } + ); + Helpers.assertStatusForObject(response, 'failed', 0, 400, "'contentType' is valid only for attachment items"); + }); + + it('testEditTopLevelNote', async function () { + let noteText = "

Test

"; + let json = await API.createNoteItem(noteText, null, true, 'jsonData'); + noteText = "

Test Test

"; + json.note = noteText; + const response = await API.userPut( + config.userID, + `items/${json.key}`, + JSON.stringify(json) + ); + Helpers.assertStatusCode(response, 204); + const response2 = await API.userGet( + config.userID, + `items/${json.key}` + ); + Helpers.assertStatusCode(response2, 200); + json = API.getJSONFromResponse(response2).data; + assert.equal(json.note, noteText); + }); + + it('testEditChildNote', async function () { + let noteText = "

Test

"; + const key = await API.createItem("book", { title: "Test" }, true, 'key'); + let json = await API.createNoteItem(noteText, key, true, 'jsonData'); + + noteText = "

Test Test

"; + json.note = noteText; + const response1 = await API.userPut( + config.userID, + "items/" + json.key, + JSON.stringify(json) + ); + assert.equal(response1.status, 204); + const response2 = await API.userGet( + config.userID, + "items/" + json.key + ); + Helpers.assertStatusCode(response2, 200); + json = API.getJSONFromResponse(response2).data; + assert.equal(json.note, noteText); + }); + + it('testEditTitleWithCollectionInMultipleMode', async function () { + const collectionKey = await API.createCollection('Test', false, true, 'key'); + let json = await API.createItem('book', { + title: 'A', + collections: [ + collectionKey, + ], + }, true, 'jsonData'); + const version = json.version; + json.title = 'B'; + + const response = await API.userPost( + config.userID, + `items`, JSON.stringify([json]), + ); + Helpers.assert200ForObject(response, 200); + json = (await API.getItem(json.key, true, 'json')).data; + assert.equal(json.title, 'B'); + assert.isAbove(json.version, version); + }); + + it('testEditTitleWithTagInMultipleMode', async function () { + const tag1 = { + tag: 'foo', + type: 1, + }; + const tag2 = { + tag: 'bar', + }; + + let json = await API.createItem('book', { + title: 'A', + tags: [tag1], + }, true, 'jsonData'); + + assert.equal(json.tags.length, 1); + assert.deepEqual(json.tags[0], tag1); + + const version = json.version; + json.title = 'B'; + json.tags.push(tag2); + + const response = await API.userPost( + config.userID, + `items`, + JSON.stringify([json]), + ); + Helpers.assertStatusForObject(response, 'success', 0); + json = (await API.getItem(json.key, true, 'json')).data; + + assert.equal(json.title, 'B'); + assert.isAbove(json.version, version); + assert.equal(json.tags.length, 2); + assert.deepEqual(json.tags, [tag2, tag1]); + }); + + it('testNewTopLevelImportedFileAttachment', async function () { + const response = await API.get("items/new?itemType=attachment&linkMode=imported_file"); + const json = JSON.parse(response.data); + const userPostResponse = await API.userPost( + config.userID, + `items`, + JSON.stringify([json]), + { "Content-Type": "application/json" } + ); + Helpers.assert200(userPostResponse); + }); + + it('testNewInvalidTopLevelAttachment', async function () { + this.skip(); //disabled + }); + + it('testNewEmptyLinkAttachmentItemWithItemKey', async function () { + const key = await API.createItem("book", false, true, 'key'); + await API.createAttachmentItem("linked_url", [], key, true, 'json'); + + let response = await API.get("items/new?itemType=attachment&linkMode=linked_url"); + let json = JSON.parse(response.data); + json.parentItem = key; + + json.key = Helpers.uniqueID(); + json.version = 0; + + response = await API.userPost( + config.userID, + "items", + JSON.stringify([json]), + { "Content-Type": "application/json" } + ); + Helpers.assert200(response); + }); + + it('testEditEmptyImportedURLAttachmentItem', async function () { + let key = await API.createItem('book', false, true, 'key'); + let json = await API.createAttachmentItem("imported_url", [], key, true, 'jsonData'); + const version = json.version; + key = json.key; + + + const response = await API.userPut( + config.userID, + `items/${key}`, + JSON.stringify(json), + { + 'Content-Type': 'application/json', + 'If-Unmodified-Since-Version': version + } + ); + Helpers.assertStatusCode(response, 204); + + json = (await API.getItem(key, true, 'json')).data; + // Item Shouldn't be changed + assert.equal(version, json.version); + }); + + const testEditEmptyLinkAttachmentItem = async () => { + let key = await API.createItem('book', false, true, 'key'); + let json = await API.createAttachmentItem('linked_url', [], key, true, 'jsonData'); + + key = json.key; + const version = json.version; + + const response = await API.userPut( + config.userID, + `items/${key}`, + JSON.stringify(json), + { + "Content-Type": "application/json", + "If-Unmodified-Since-Version": version + } + ); + Helpers.assertStatusCode(response, 204); + + json = (await API.getItem(key, true, 'json')).data; + // Item shouldn't change + assert.equal(version, json.version); + return json; + }; + + it('testEditLinkAttachmentItem', async function () { + let json = await testEditEmptyLinkAttachmentItem(); + const key = json.key; + const version = json.version; + + const contentType = "text/xml"; + const charset = "utf-8"; + + json.contentType = contentType; + json.charset = charset; + + const response = await API.userPut( + config.userID, + `items/${key}`, + JSON.stringify(json), + { + "Content-Type": "application/json", + "If-Unmodified-Since-Version": version + } + ); + + Helpers.assertStatusCode(response, 204); + + json = (await API.getItem(key, true, 'json')).data; + + assert.equal(json.contentType, contentType); + assert.equal(json.charset, charset); + }); + + it('testEditAttachmentAtomUpdatedTimestamp', async function () { + const xml = await API.createAttachmentItem("linked_file", [], false, true, 'atom'); + const data = API.parseDataFromAtomEntry(xml); + const atomUpdated = Helpers.xpathEval(xml, '//atom:entry/atom:updated'); + const json = JSON.parse(data.content); + delete json.dateModified; + json.note = "Test"; + + await new Promise(resolve => setTimeout(resolve, 1000)); + + const response = await API.userPut( + config.userID, + `items/${data.key}`, + JSON.stringify(json), + { "If-Unmodified-Since-Version": data.version, + "User-Agent": "Firefox" } // TODO: Remove + ); + Helpers.assert204(response); + + const xml2 = await API.getItemXML(data.key); + const atomUpdated2 = Helpers.xpathEval(xml2, '//atom:entry/atom:updated'); + assert.notEqual(atomUpdated2, atomUpdated); + }); + + it('testEditAttachmentAtomUpdatedTimestampTmpZoteroClientHack', async function () { + const xml = await API.createAttachmentItem("linked_file", [], false, true, 'atom'); + const data = API.parseDataFromAtomEntry(xml); + const atomUpdated = Helpers.xpathEval(xml, '//atom:entry/atom:updated'); + const json = JSON.parse(data.content); + json.note = "Test"; + + await new Promise(resolve => setTimeout(resolve, 1000)); + + const response = await API.userPut( + config.userID, + `items/${data.key}`, + JSON.stringify(json), + { "If-Unmodified-Since-Version": data.version } + ); + Helpers.assert204(response); + + const xml2 = await API.getItemXML(data.key); + const atomUpdated2 = Helpers.xpathEval(xml2, '//atom:entry/atom:updated'); + assert.notEqual(atomUpdated2, atomUpdated); + }); + + it('testNewAttachmentItemInvalidLinkMode', async function () { + const response = await API.get("items/new?itemType=attachment&linkMode=linked_url"); + const json = JSON.parse(response.data); + + // Invalid linkMode + json.linkMode = "invalidName"; + const newResponse = await API.userPost( + config.userID, + "items", + JSON.stringify([json]), + { "Content-Type": "application/json" } + ); + Helpers.assertStatusForObject(newResponse, 'failed', 0, 400, "'invalidName' is not a valid linkMode"); + + // Missing linkMode + delete json.linkMode; + const missingResponse = await API.userPost( + config.userID, + "items", + JSON.stringify([json]), + { "Content-Type": "application/json" } + ); + Helpers.assertStatusForObject(missingResponse, 'failed', 0, 400, "'linkMode' property not provided"); + }); + it('testNewAttachmentItemMD5OnLinkedURL', async function () { + let json = await testNewEmptyBookItem(); + const parentKey = json.key; + + const response = await API.get("items/new?itemType=attachment&linkMode=linked_url"); + json = JSON.parse(response.data); + json.parentItem = parentKey; + + json.md5 = "c7487a750a97722ae1878ed46b215ebe"; + const postResponse = await API.userPost( + config.userID, + "items", + JSON.stringify([json]), + { "Content-Type": "application/json" } + ); + Helpers.assertStatusForObject(postResponse, 'failed', 0, 400, "'md5' is valid only for imported and embedded-image attachments"); + }); + it('testNewAttachmentItemModTimeOnLinkedURL', async function () { + let json = await testNewEmptyBookItem(); + const parentKey = json.key; + + const response = await API.get("items/new?itemType=attachment&linkMode=linked_url"); + json = JSON.parse(response.data); + json.parentItem = parentKey; + + json.mtime = "1332807793000"; + const postResponse = await API.userPost( + config.userID, + "items", + JSON.stringify([json]), + { "Content-Type": "application/json" } + ); + Helpers.assertStatusForObject(postResponse, 'failed', 0, 400, "'mtime' is valid only for imported and embedded-image attachments"); + }); + it('testMappedCreatorTypes', async function () { + const json = [ + { + itemType: 'presentation', + title: 'Test', + creators: [ + { + creatorType: "author", + name: "Foo" + } + ] + }, + { + itemType: 'presentation', + title: 'Test', + creators: [ + { + creatorType: "editor", + name: "Foo" + } + ] + } + ]; + const response = await API.userPost( + config.userID, + "items", + JSON.stringify(json) + ); + // 'author' gets mapped automatically, others dont + Helpers.assert200ForObject(response); + Helpers.assert400ForObject(response, { index: 1 }); + }); + + it('testNumChildrenJSON', async function () { + let json = await API.createItem("book", false, true, 'json'); + assert.equal(json.meta.numChildren, 0); + + const key = json.key; + + await API.createAttachmentItem("linked_url", [], key, true, 'key'); + + let response = await API.userGet( + config.userID, + `items/${key}` + ); + json = API.getJSONFromResponse(response); + assert.equal(json.meta.numChildren, 1); + + await API.createNoteItem("Test", key, true, 'key'); + + response = await API.userGet( + config.userID, + `items/${key}` + ); + json = API.getJSONFromResponse(response); + assert.equal(json.meta.numChildren, 2); + }); + + it('testNumChildrenAtom', async function () { + let xml = await API.createItem("book", false, true, 'atom'); + assert.equal(Helpers.xpathEval(xml, '//atom:entry/zapi:numChildren'), 0); + const data = API.parseDataFromAtomEntry(xml); + const key = data.key; + + await API.createAttachmentItem("linked_url", [], key, true, 'key'); + + let response = await API.userGet( + config.userID, + `items/${key}?content=json` + ); + xml = API.getXMLFromResponse(response); + assert.equal(Helpers.xpathEval(xml, '//atom:entry/zapi:numChildren'), 1); + + await API.createNoteItem("Test", key, true, 'key'); + + response = await API.userGet( + config.userID, + `items/${key}?content=json` + ); + xml = API.getXMLFromResponse(response); + assert.equal(Helpers.xpathEval(xml, '//atom:entry/zapi:numChildren'), 2); + }); + + it('testTop', async function () { + await API.userClear(config.userID); + + const collectionKey = await API.createCollection('Test', false, this, 'key'); + const emptyCollectionKey = await API.createCollection('Empty', false, this, 'key'); + + const parentTitle1 = "Parent Title"; + const childTitle1 = "This is a Test Title"; + const parentTitle2 = "Another Parent Title"; + const parentTitle3 = "Yet Another Parent Title"; + const noteText = "This is a sample note."; + const parentTitleSearch = "title"; + const childTitleSearch = "test"; + const dates = ["2013", "January 3, 2010", ""]; + const orderedDates = [dates[2], dates[1], dates[0]]; + const itemTypes = ["journalArticle", "newspaperArticle", "book"]; + + const parentKeys = []; + const childKeys = []; + + const orderedTitles = [parentTitle1, parentTitle2, parentTitle3].sort(); + const orderedDatesReverse = [...orderedDates].reverse(); + const orderedItemTypes = [...itemTypes].sort(); + const reversedItemTypes = [...orderedItemTypes].reverse(); + + parentKeys.push(await API.createItem(itemTypes[0], { + title: parentTitle1, + date: dates[0], + collections: [ + collectionKey + ] + }, this, 'key')); + + childKeys.push(await API.createAttachmentItem("linked_url", { + title: childTitle1 + }, parentKeys[0], this, 'key')); + + parentKeys.push(await API.createItem(itemTypes[1], { + title: parentTitle2, + date: dates[1] + }, this, 'key')); + + childKeys.push(await API.createNoteItem(noteText, parentKeys[1], this, 'key')); + childKeys.push(await API.createAttachmentItem( + 'embedded_image', + { contentType: "image/png" }, + childKeys[childKeys.length - 1], + this, 'key')); + + parentKeys.push(await API.createItem(itemTypes[2], { + title: parentTitle3 + }, this, 'key')); + + childKeys.push(await API.createAttachmentItem("linked_url", { + title: childTitle1, + deleted: true + }, parentKeys[parentKeys.length - 1], this, 'key')); + + const deletedKey = await API.createItem("book", { + title: "This is a deleted item", + deleted: true, + }, this, 'key'); + + await API.createNoteItem("This is a child note of a deleted item.", deletedKey, this, 'key'); + + const top = async (url, expectedResults = -1) => { + const response = await API.userGet(config.userID, url); + Helpers.assertStatusCode(response, 200); + if (expectedResults !== -1) { + Helpers.assertNumResults(response, expectedResults); + } + return response; + }; + + const checkXml = (response, expectedCount = -1, path = '//atom:entry/zapi:key') => { + const xml = API.getXMLFromResponse(response); + const xpath = Helpers.xpathEval(xml, path, false, true); + if (expectedCount !== -1) { + assert.equal(xpath.length, expectedCount); + } + return xpath; + }; + + let response, xpath, json, done; + + // /top, JSON + response = await top(`items/top`, parentKeys.length); + json = API.getJSONFromResponse(response); + done = []; + for (let item of json) { + assert.include(parentKeys, item.key); + assert.notInclude(done, item.key); + done.push(item.key); + } + + // /top, Atom + response = await top(`items/top?content=json`, parentKeys.length); + xpath = await checkXml(response, parentKeys.length); + for (let parentKey of parentKeys) { + assert.include(xpath, parentKey); + } + + // /top, JSON, in collection + response = await top(`collections/${collectionKey}/items/top`, 1); + json = API.getJSONFromResponse(response); + Helpers.assertNumResults(response, 1); + assert.equal(parentKeys[0], json[0].key); + + // /top, Atom, in collection + response = await top(`collections/${collectionKey}/items/top?content=json`, 1); + xpath = await checkXml(response, 1); + assert.include(xpath, parentKeys[0]); + + // /top, JSON, in empty collection + response = await top(`collections/${emptyCollectionKey}/items/top`, 0); + Helpers.assertTotalResults(response, 0); + + // /top, keys + response = await top(`items/top?format=keys`); + let keys = response.data.trim().split("\n"); + assert.equal(keys.length, parentKeys.length); + for (let parentKey of parentKeys) { + assert.include(keys, parentKey); + } + + // /top, keys, in collection + response = await top(`collections/${collectionKey}/items/top?format=keys`); + assert.equal(response.data.trim(), parentKeys[0]); + + // /top with itemKey for parent, JSON + response = await top(`items/top?itemKey=${parentKeys[0]}`, 1); + json = API.getJSONFromResponse(response); + assert.equal(parentKeys[0], json[0].key); + + // /top with itemKey for parent, Atom + response = await top(`items/top?content=json&itemKey=${parentKeys[0]}`, 1); + xpath = await checkXml(response); + assert.equal(parentKeys[0], xpath.shift()); + + // /top with itemKey for parent, JSON, in collection + response = await top(`collections/${collectionKey}/items/top?itemKey=${parentKeys[0]}`, 1); + json = API.getJSONFromResponse(response); + assert.equal(parentKeys[0], json[0].key); + + // /top with itemKey for parent, Atom, in collection + response = await top(`collections/${collectionKey}/items/top?content=json&itemKey=${parentKeys[0]}`, 1); + xpath = await checkXml(response); + assert.equal(parentKeys[0], xpath.shift()); + + // /top with itemKey for parent, keys + response = await top(`items/top?format=keys&itemKey=${parentKeys[0]}`); + assert.equal(parentKeys[0], response.data.trim()); + + // /top with itemKey for parent, keys, in collection + response = await top(`collections/${collectionKey}/items/top?format=keys&itemKey=${parentKeys[0]}`); + assert.equal(parentKeys[0], response.data.trim()); + + // /top with itemKey for child, JSON + response = await top(`items/top?itemKey=${childKeys[0]}`, 1); + json = API.getJSONFromResponse(response); + assert.equal(parentKeys[0], json[0].key); + + // /top with itemKey for child, Atom + response = await top(`items/top?content=json&itemKey=${childKeys[0]}`, 1); + xpath = await checkXml(response); + assert.equal(parentKeys[0], xpath.shift()); + + // /top with itemKey for child, keys + response = await top(`items/top?format=keys&itemKey=${childKeys[0]}`); + assert.equal(parentKeys[0], response.data.trim()); + + // /top, Atom, with q for all items + response = await top(`items/top?content=json&q=${parentTitleSearch}`, parentKeys.length); + xpath = await checkXml(response, parentKeys.length); + for (let parentKey of parentKeys) { + assert.include(xpath, parentKey); + } + + // /top, JSON, with q for all items + response = await top(`items/top?q=${parentTitleSearch}`, parentKeys.length); + json = API.getJSONFromResponse(response); + done = []; + for (let item of json) { + assert.include(parentKeys, item.key); + assert.notInclude(done, item.key); + done.push(item.key); + } + + // /top, JSON, in collection, with q for all items + response = await top(`collections/${collectionKey}/items/top?q=${parentTitleSearch}`, 1); + json = API.getJSONFromResponse(response); + assert.equal(parentKeys[0], json[0].key); + + // /top, Atom, in collection, with q for all items + response = await top(`collections/${collectionKey}/items/top?content=json&q=${parentTitleSearch}`, 1); + xpath = await checkXml(response, 1); + assert.include(xpath, parentKeys[0]); + + // /top, JSON, with q for child item + response = await top(`items/top?q=${childTitleSearch}`, 1); + json = API.getJSONFromResponse(response); + assert.equal(parentKeys[0], json[0].key); + + // /top, Atom, with q for child item + response = await top(`items/top?content=json&q=${childTitleSearch}`, 1); + xpath = checkXml(response, 1); + assert.include(xpath, parentKeys[0]); + + // /top, JSON, in collection, with q for child item + response = await top(`collections/${collectionKey}/items/top?q=${childTitleSearch}`, 1); + json = API.getJSONFromResponse(response); + assert.equal(parentKeys[0], json[0].key); + + // /top, Atom, in collection, with q for child item + response = await top(`collections/${collectionKey}/items/top?content=json&q=${childTitleSearch}`, 1); + xpath = checkXml(response, 1); + assert.include(xpath, parentKeys[0]); + + // /top, JSON, with q for all items, ordered by title + response = await top(`items/top?q=${parentTitleSearch}&order=title`, parentKeys.length); + json = API.getJSONFromResponse(response); + let returnedTitles = []; + for (let item of json) { + returnedTitles.push(item.data.title); + } + assert.deepEqual(orderedTitles, returnedTitles); + + // /top, Atom, with q for all items, ordered by title + response = await top(`items/top?content=json&q=${parentTitleSearch}&order=title`, parentKeys.length); + xpath = checkXml(response, parentKeys.length, '//atom:entry/atom:title'); + let orderedResults = xpath.map(val => String(val)); + assert.deepEqual(orderedTitles, orderedResults); + + // /top, Atom, with q for all items, ordered by date asc + response = await top(`items/top?content=json&q=${parentTitleSearch}&order=date&sort=asc`, parentKeys.length); + xpath = checkXml(response, parentKeys.length, '//atom:entry/atom:content'); + orderedResults = xpath.map(val => JSON.parse(val).date); + assert.deepEqual(orderedDates, orderedResults); + + // /top, JSON, with q for all items, ordered by date asc + response = await top(`items/top?q=${parentTitleSearch}&order=date&sort=asc`, parentKeys.length); + json = API.getJSONFromResponse(response); + orderedResults = Object.entries(json).map(([_, val]) => { + return val.data.date; + }); + assert.deepEqual(orderedDates, orderedResults); + + // /top, Atom, with q for all items, ordered by date desc + response = await top(`items/top?content=json&q=${parentTitleSearch}&order=date&sort=desc`, parentKeys.length); + xpath = checkXml(response, parentKeys.length, '//atom:entry/atom:content'); + orderedResults = xpath.map(val => JSON.parse(val).date); + assert.deepEqual(orderedDatesReverse, orderedResults); + + // /top, JSON, with q for all items, ordered by date desc + response = await top(`items/top?&q=${parentTitleSearch}&order=date&sort=desc`, parentKeys.length); + json = API.getJSONFromResponse(response); + orderedResults = Object.entries(json).map(([_, val]) => { + return val.data.date; + }); + assert.deepEqual(orderedDatesReverse, orderedResults); + + // /top, Atom, with q for all items, ordered by item type asc + response = await top(`items/top?content=json&q=${parentTitleSearch}&order=itemType`, parentKeys.length); + xpath = checkXml(response, parentKeys.length, '//atom:entry/zapi:itemType'); + orderedResults = xpath.map(val => String(val)); + assert.deepEqual(orderedItemTypes, orderedResults); + + // /top, JSON, with q for all items, ordered by item type asc + response = await top(`items/top?q=${parentTitleSearch}&order=itemType`, parentKeys.length); + json = API.getJSONFromResponse(response); + orderedResults = Object.entries(json).map(([_, val]) => { + return val.data.itemType; + }); + assert.deepEqual(orderedItemTypes, orderedResults); + + // /top, Atom, with q for all items, ordered by item type desc + response = await top(`items/top?content=json&q=${parentTitleSearch}&order=itemType&sort=desc`, parentKeys.length); + xpath = checkXml(response, parentKeys.length, '//atom:entry/zapi:itemType'); + orderedResults = xpath.map(val => String(val)); + assert.deepEqual(reversedItemTypes, orderedResults); + + // /top, JSON, with q for all items, ordered by item type desc + response = await top(`items/top?q=${parentTitleSearch}&order=itemType&sort=desc`, parentKeys.length); + json = API.getJSONFromResponse(response); + orderedResults = Object.entries(json).map(([_, val]) => { + return val.data.itemType; + }); + assert.deepEqual(reversedItemTypes, orderedResults); + }); + + it('testParentItem', async function () { + let json = await API.createItem("book", false, true, "jsonData"); + let parentKey = json.key; + + json = await API.createAttachmentItem("linked_file", [], parentKey, true, 'jsonData'); + let childKey = json.key; + let childVersion = json.version; + + assert.property(json, "parentItem"); + assert.equal(parentKey, json.parentItem); + + // Remove the parent, making the child a standalone attachment + delete json.parentItem; + + let response = await API.userPut( + config.userID, + `items/${childKey}`, + JSON.stringify(json), + { "If-Unmodified-Since-Version": childVersion } + ); + Helpers.assert204(response); + + json = (await API.getItem(childKey, true, 'json')).data; + assert.notProperty(json, "parentItem"); + }); + + it('testParentItemPatch', async function () { + let json = await API.createItem("book", false, true, 'jsonData'); + const parentKey = json.key; + + json = await API.createAttachmentItem("linked_file", [], parentKey, true, 'jsonData'); + const childKey = json.key; + let childVersion = json.version; + + assert.property(json, "parentItem"); + assert.equal(parentKey, json.parentItem); + + // With PATCH, parent shouldn't be removed even though unspecified + let response = await API.userPatch( + config.userID, + `items/${childKey}`, + JSON.stringify({ title: "Test" }), + { "If-Unmodified-Since-Version": childVersion }, + ); + + Helpers.assert204(response); + + json = (await API.getItem(childKey, true, "json")).data; + assert.property(json, "parentItem"); + + childVersion = json.version; + + // But it should be removed with parentItem: false + response = await API.userPatch( + config.userID, + `items/${childKey}`, + JSON.stringify({ parentItem: false }), + { "If-Unmodified-Since-Version": childVersion }, + ); + Helpers.assert204(response); + json = (await API.getItem(childKey, true, "json")).data; + assert.notProperty(json, "parentItem"); + }); + + it('testDate', async function () { + const date = "Sept 18, 2012"; + const parsedDate = '2012-09-18'; + + let json = await API.createItem("book", { date: date }, true, 'jsonData'); + const key = json.key; + + let response = await API.userGet( + config.userID, + `items/${key}` + ); + json = await API.getJSONFromResponse(response); + assert.equal(json.data.date, date); + assert.equal(json.meta.parsedDate, parsedDate); + + let xml = await API.getItem(key, true, 'atom'); + assert.equal(Helpers.xpathEval(xml, '//atom:entry/zapi:parsedDate'), parsedDate); + }); + + + it('test_patch_of_item_in_trash_without_deleted_should_not_remove_it_from_trash', async function () { + let json = await API.createItem("book", { + deleted: true + }, this, 'json'); + + let data = [ + { + key: json.key, + version: json.version, + title: 'A' + } + ]; + let response = await API.postItems(data); + let jsonResponse = await API.getJSONFromResponse(response); + + assert.property(jsonResponse.successful[0].data, 'deleted'); + Helpers.assertEquals(1, jsonResponse.successful[0].data.deleted); + }); + + it('test_deleting_parent_item_should_delete_note_and_embedded_image_attachment', async function () { + let json = await API.createItem("book", false, this, 'jsonData'); + let itemKey = json.key; + let itemVersion = json.version; + // Create embedded-image attachment + let noteKey = await API.createNoteItem( + '

Test

', itemKey, this, 'key' + ); + // Create image annotation + let attachmentKey = await API.createAttachmentItem( + 'embedded_image', { contentType: 'image/png' }, noteKey, this, 'key' + ); + // Check that all items can be found + let response = await API.userGet( + config.userID, + "items?itemKey=" + itemKey + "," + noteKey + "," + attachmentKey + ); + Helpers.assertNumResults(response, 3); + response = await API.userDelete( + config.userID, + "items/" + itemKey, + { "If-Unmodified-Since-Version": itemVersion } + ); + Helpers.assert204(response); + response = await API.userGet( + config.userID, + "items?itemKey=" + itemKey + "," + noteKey + "," + attachmentKey + ); + json = await API.getJSONFromResponse(response); + Helpers.assertNumResults(response, 0); + }); + + it('testTrash', async function () { + await API.userClear(config.userID); + + const key1 = await API.createItem("book", false, this, 'key'); + const key2 = await API.createItem("book", { + deleted: 1 + }, this, 'key'); + + // Item should show up in trash + let response = await API.userGet( + config.userID, + "items/trash" + ); + let json = await API.getJSONFromResponse(response); + Helpers.assertCount(1, json); + Helpers.assertEquals(key2, json[0].key); + + // And not show up in main items + response = await API.userGet( + config.userID, + "items" + ); + json = await API.getJSONFromResponse(response); + Helpers.assertCount(1, json); + Helpers.assertEquals(key1, json[0].key); + + // Including with ?itemKey + response = await API.userGet( + config.userID, + "items?itemKey=" + key2 + ); + json = await API.getJSONFromResponse(response); + Helpers.assertCount(0, json); + }); + + it('test_should_convert_child_note_to_top_level_and_add_to_collection_via_PUT', async function () { + let collectionKey = await API.createCollection('Test', false, this, 'key'); + let parentItemKey = await API.createItem("book", false, this, 'key'); + let noteJSON = await API.createNoteItem("", parentItemKey, this, 'jsonData'); + delete noteJSON.parentItem; + noteJSON.collections = [collectionKey]; + let response = await API.userPut( + config.userID, + `items/${noteJSON.key}`, + JSON.stringify(noteJSON), + { "Content-Type": "application/json" } + ); + Helpers.assert204(response); + let json = (await API.getItem(noteJSON.key, this, 'json')).data; + assert.notProperty(json, 'parentItem'); + Helpers.assertCount(1, json.collections); + Helpers.assertEquals(collectionKey, json.collections[0]); + }); + + it('test_should_reject_invalid_content_type_for_embedded_image_attachment', async function () { + let noteKey = await API.createNoteItem("Test", null, this, 'key'); + let response = await API.get("items/new?itemType=attachment&linkMode=embedded_image"); + let json = JSON.parse(response.data); + json.parentItem = noteKey; + json.contentType = 'application/pdf'; + response = await API.userPost( + config.userID, + "items", + JSON.stringify([json]), + { "Content-Type": "application/json" } + ); + Helpers.assert400ForObject(response, "Embedded-image attachment must have an image content type"); + }); + + it('testPatchNote', async function () { + let text = "

Test

"; + let newText = "

Test 2

"; + let json = await API.createNoteItem(text, false, this, 'jsonData'); + let itemKey = json.key; + let itemVersion = json.version; + + let response = await API.userPatch( + config.userID, + "items/" + itemKey, + JSON.stringify({ + note: newText + }), + { + "Content-Type": "application/json", + "If-Unmodified-Since-Version": itemVersion + } + ); + + Helpers.assert204(response); + json = (await API.getItem(itemKey, this, 'json')).data; + + Helpers.assertEquals(newText, json.note); + let headerVersion = parseInt(response.headers["last-modified-version"][0]); + assert.isAbove(headerVersion, itemVersion); + Helpers.assertEquals(json.version, headerVersion); + }); + + it('test_should_create_embedded_image_attachment_for_note', async function () { + let noteKey = await API.createNoteItem("Test", null, this, 'key'); + let imageKey = await API.createAttachmentItem( + 'embedded_image', { contentType: 'image/png' }, noteKey, this, 'key' + ); + assert.ok(imageKey); + }); + + it('test_should_return_409_if_a_note_references_a_note_as_a_parent_item', async function () { + let parentKey = await API.createNoteItem("

Parent

", null, this, 'key'); + let json = await API.createNoteItem("

Parent

", parentKey, this); + Helpers.assert409ForObject(json, "Parent item cannot be a note or attachment"); + Helpers.assertEquals(parentKey, json.failed[0].data.parentItem); + }); + + it('testDateModifiedTmpZoteroClientHack', async function () { + let objectType = 'item'; + let objectTypePlural = API.getPluralObjectType(objectType); + + let json = await API.createItem("videoRecording", { title: "Test" }, this, 'jsonData'); + + let objectKey = json.key; + let dateModified1 = json.dateModified; + + await new Promise(resolve => setTimeout(resolve, 1000)); + + json.title = "Test 2"; + delete json.dateModified; + let response = await API.userPut( + config.userID, + `${objectTypePlural}/${objectKey}`, + JSON.stringify(json), + { + "User-Agent": "Firefox" + } + ); + Helpers.assert204(response); + + if (objectType == 'item') { + json = (await API.getItem(objectKey, this, 'json')).data; + } + + + let dateModified2 = json.dateModified; + assert.notEqual(dateModified1, dateModified2); + + await new Promise(resolve => setTimeout(resolve, 1000)); + + json.title = "Test 3"; + json.dateModified = dateModified2.replace(/[TZ]/g, ' ').trim(); + response = await API.userPut( + config.userID, + `${objectTypePlural}/${objectKey}`, + JSON.stringify(json), + { + "User-Agent": "Firefox" + } + ); + Helpers.assert204(response); + + if (objectType == 'item') { + json = (await API.getItem(objectKey, this, 'json')).data; + } + Helpers.assertEquals(dateModified2, json.dateModified); + + let newDateModified = "2013-03-03T21:33:53Z"; + + json.title = "Test 4"; + json.dateModified = newDateModified; + response = await API.userPut( + config.userID, + `${objectTypePlural}/${objectKey}`, + JSON.stringify(json), + { + "User-Agent": "Firefox" + } + ); + Helpers.assert204(response); + + if (objectType == 'item') { + json = (await API.getItem(objectKey, this, 'json')).data; + } + Helpers.assertEquals(newDateModified, json.dateModified); + }); + + it('test_top_should_return_top_level_item_for_three_level_hierarchy', async function () { + await API.userClear(config.userID); + + let itemKey = await API.createItem("book", { title: 'aaa' }, this, 'key'); + let attachmentKey = await API.createAttachmentItem("imported_url", { + contentType: 'application/pdf', + title: 'bbb' + }, itemKey, this, 'key'); + let _ = await API.createAnnotationItem('highlight', { annotationComment: 'ccc' }, attachmentKey, this, 'key'); + + let response = await API.userGet(config.userID, "items/top?q=bbb"); + Helpers.assert200(response); + Helpers.assertNumResults(response, 1); + let json = API.getJSONFromResponse(response); + Helpers.assertEquals("aaa", json[0].data.title); + + response = await API.userGet(config.userID, "items/top?itemType=annotation"); + Helpers.assert200(response); + Helpers.assertNumResults(response, 1); + json = API.getJSONFromResponse(response); + Helpers.assertEquals("aaa", json[0].data.title); + + response = await API.userGet(config.userID, `items/top?itemKey=${attachmentKey}`); + Helpers.assert200(response); + Helpers.assertNumResults(response, 1); + json = API.getJSONFromResponse(response); + Helpers.assertEquals("aaa", json[0].data.title); + }); + + it('testDateModifiedNoChange', async function () { + let collectionKey = await API.createCollection('Test', false, this, 'key'); + + let json = await API.createItem('book', false, this, 'jsonData'); + let modified = json.dateModified; + + for (let i = 1; i <= 5; i++) { + await new Promise(resolve => setTimeout(resolve, 1000)); + + switch (i) { + case 1: + json.title = 'A'; + break; + + case 2: + // For all subsequent tests, unset field, which would normally cause it to be updated + delete json.dateModified; + + json.collections = [collectionKey]; + break; + + case 3: + json.deleted = true; + break; + + case 4: + json.deleted = false; + break; + + case 5: + json.tags = [{ + tag: 'A' + }]; + break; + } + + let response = await API.userPost( + config.userID, + "items", + JSON.stringify([json]), + { + "If-Unmodified-Since-Version": json.version, + // TODO: Remove + "User-Agent": "Firefox" + } + ); + Helpers.assert200(response); + json = API.getJSONFromResponse(response).successful[0].data; + Helpers.assertEquals(modified, json.dateModified, "Date Modified changed on loop " + i); + } + }); + + it('testDateModifiedCollectionChange', async function () { + let collectionKey = await API.createCollection('Test', false, this, 'key'); + let json = await API.createItem("book", { title: "Test" }, this, 'jsonData'); + + let objectKey = json.key; + let dateModified1 = json.dateModified; + + json.collections = [collectionKey]; + + // Make sure we're in the next second + await new Promise(resolve => setTimeout(resolve, 1000)); + + let response = await API.userPost( + config.userID, + "items", + JSON.stringify([json]), + { "Content-Type": "application/json" } + ); + Helpers.assert200ForObject(response); + + json = (await API.getItem(objectKey, this, 'json')).data; + let dateModified2 = json.dateModified; + + // Date Modified shouldn't have changed + Helpers.assertEquals(dateModified1, dateModified2); + }); + + it('test_should_return_409_if_an_attachment_references_a_note_as_a_parent_item', async function () { + let parentKey; + await API.createNoteItem('

Parent

', null, this, 'key').then((res) => { + parentKey = res; + }); + let json; + await API.createAttachmentItem('imported_file', [], parentKey, this, 'responseJSON').then((res) => { + json = res; + }); + Helpers.assert409ForObject(json, 'Parent item cannot be a note or attachment'); + Helpers.assertEquals(parentKey, json.failed[0].data.parentItem); + }); + + it('testDateAddedNewItem8601TZ', async function () { + const objectType = 'item'; + const objectTypePlural = API.getPluralObjectType(objectType); + const dateAdded = "2013-03-03T17:33:53-0400"; + const dateAddedUTC = "2013-03-03T21:33:53Z"; + let itemData = { + title: "Test", + dateAdded: dateAdded + }; + let data; + switch (objectType) { + case 'item': + data = await API.createItem("videoRecording", itemData, this, 'jsonData'); + break; + } + assert.equal(dateAddedUTC, data.dateAdded); + }); + + it('testDateAccessed8601TZ', async function () { + let date = '2014-02-01T01:23:45-0400'; + let dateUTC = '2014-02-01T05:23:45Z'; + let data = await API.createItem("book", { + accessDate: date + }, this, 'jsonData'); + Helpers.assertEquals(dateUTC, data.accessDate); + }); + + it('test_should_reject_embedded_image_attachment_without_parent', async function () { + let response = await API.get("items/new?itemType=attachment&linkMode=embedded_image"); + let json = JSON.parse(response.data); + json.parentItem = false; + json.contentType = 'image/png'; + response = await API.userPost( + config.userID, + "items", + JSON.stringify([json]), + { "Content-Type": "application/json" } + ); + Helpers.assert400ForObject(response, "Embedded-image attachment must have a parent item"); + }); + + it('testNewEmptyAttachmentFields', async function () { + let key = await API.createItem("book", false, this, 'key'); + let json = await API.createAttachmentItem("imported_url", [], key, this, 'jsonData'); + assert.notOk(json.md5); + assert.notOk(json.mtime); + }); + + it('testDateUnparseable', async function () { + let json = await API.createItem("book", { + date: 'n.d.' + }, this, 'jsonData'); + let key = json.key; + + let response = await API.userGet( + config.userID, + "items/" + key + ); + json = API.getJSONFromResponse(response); + Helpers.assertEquals('n.d.', json.data.date); + + // meta.parsedDate (JSON) + assert.notProperty(json.meta, 'parsedDate'); + + // zapi:parsedDate (Atom) + let xml = await API.getItem(key, this, 'atom'); + Helpers.assertCount(0, Helpers.xpathEval(xml, '/atom:entry/zapi:parsedDate', false, true).length); + }); + + it('test_should_ignore_null_for_existing_storage_properties', async function () { + let key = await API.createItem("book", [], this, 'key'); + let json = await API.createAttachmentItem( + "imported_url", + { + md5: Helpers.md5(Helpers.uniqueID(50)), + mtime: Date.now() + }, + key, + this, + 'jsonData' + ); + + key = json.key; + let version = json.version; + + let props = ["md5", "mtime"]; + for (let prop of props) { + let json2 = { ...json }; + json2[prop] = null; + let response = await API.userPut( + config.userID, + "items/" + key, + JSON.stringify(json2), + { + "Content-Type": "application/json", + "If-Unmodified-Since-Version": version + } + ); + Helpers.assert204(response); + } + + let json3 = await API.getItem(json.key); + Helpers.assertEquals(json.md5, json3.data.md5); + Helpers.assertEquals(json.mtime, json3.data.mtime); + }); + + it('test_should_reject_changing_parent_of_embedded_image_attachment', async function () { + let note1Key = await API.createNoteItem("Test 1", null, this, 'key'); + let note2Key = await API.createNoteItem("Test 2", null, this, 'key'); + let response = await API.get("items/new?itemType=attachment&linkMode=embedded_image"); + let json = JSON.parse(await response.data); + json.parentItem = note1Key; + json.contentType = 'image/png'; + response = await API.userPost( + config.userID, + "items", + JSON.stringify([json]), + { "Content-Type": "application/json" } + ); + Helpers.assert200ForObject(response); + json = await API.getJSONFromResponse(response); + let key = json.successful[0].key; + json = await API.getItem(key, this, 'json'); + + // Change the parent item + json = { + version: json.version, + parentItem: note2Key + }; + response = await API.userPatch( + config.userID, + `items/${key}`, + JSON.stringify(json) + ); + Helpers.assert400(response, "Cannot change parent item of embedded-image attachment"); + }); + + it('test_should_convert_child_attachment_to_top_level_and_add_to_collection_via_PATCH_without_parentItem_false', async function () { + let collectionKey = await API.createCollection('Test', false, this, 'key'); + let parentItemKey = await API.createItem("book", false, this, 'key'); + let attachmentJSON = await API.createAttachmentItem("linked_url", [], parentItemKey, this, 'jsonData'); + delete attachmentJSON.parentItem; + attachmentJSON.collections = [collectionKey]; + let response = await API.userPatch( + config.userID, + "items/" + attachmentJSON.key, + JSON.stringify(attachmentJSON) + ); + Helpers.assert204(response); + let json = (await API.getItem(attachmentJSON.key, this, 'json')).data; + assert.notProperty(json, 'parentItem'); + Helpers.assertCount(1, json.collections); + Helpers.assertEquals(collectionKey, json.collections[0]); + }); + + it('testDateModifiedChangeOnEdit', async function () { + let json = await API.createAttachmentItem("linked_file", [], false, this, 'jsonData'); + let modified = json.dateModified; + delete json.dateModified; + json.note = "Test"; + await new Promise(resolve => setTimeout(resolve, 1000)); + const headers = { "If-Unmodified-Since-Version": json.version }; + const response = await API.userPut( + config.userID, + "items/" + json.key, + JSON.stringify(json), + headers + ); + Helpers.assert204(response); + json = (await API.getItem(json.key, this, 'json')).data; + assert.notEqual(modified, json.dateModified); + }); + + it('test_patch_of_item_should_set_trash_state', async function () { + let json = await API.createItem("book", [], this, 'json'); + + let data = [ + { + key: json.key, + version: json.version, + deleted: true + } + ]; + let response = await API.postItems(data); + json = await API.getJSONFromResponse(response); + + assert.property(json.successful[0].data, 'deleted'); + Helpers.assertEquals(1, json.successful[0].data.deleted); + }); + + it('testCreateLinkedFileAttachment', async function () { + let key = await API.createItem("book", false, this, 'key'); + let path = 'attachments:tést.txt'; + let json = await API.createAttachmentItem( + "linked_file", { + path: path + }, key, this, 'jsonData' + ); + Helpers.assertEquals('linked_file', json.linkMode); + // Linked file should have path + Helpers.assertEquals(path, json.path); + // And shouldn't have other attachment properties + assert.notProperty(json, 'filename'); + assert.notProperty(json, 'md5'); + assert.notProperty(json, 'mtime'); + }); + + it('test_should_convert_child_note_to_top_level_and_add_to_collection_via_PATCH', async function () { + let collectionKey = await API.createCollection('Test', false, this, 'key'); + let parentItemKey = await API.createItem("book", false, this, 'key'); + let noteJSON = await API.createNoteItem("", parentItemKey, this, 'jsonData'); + noteJSON.parentItem = false; + noteJSON.collections = [collectionKey]; + let headers = { "Content-Type": "application/json" }; + let response = await API.userPatch( + config.userID, + `items/${noteJSON.key}`, + JSON.stringify(noteJSON), + headers + ); + Helpers.assert204(response); + let json = await API.getItem(noteJSON.key, this, 'json'); + json = json.data; + assert.notProperty(json, 'parentItem'); + Helpers.assertCount(1, json.collections); + Helpers.assertEquals(collectionKey, json.collections[0]); + }); + + it('test_createdByUser', async function () { + let json = await API.groupCreateItem( + config.ownedPrivateGroupID, + 'book', + [], + true, + 'json' + ); + Helpers.assertEquals(config.userID, json.meta.createdByUser.id); + Helpers.assertEquals(config.username, json.meta.createdByUser.username); + // TODO: Name and URI + }); + + it('testPatchNoteOnBookError', async function () { + let json = await API.createItem("book", [], this, 'jsonData'); + let itemKey = json.key; + let itemVersion = json.version; + + let response = await API.userPatch( + config.userID, + `items/${itemKey}`, + JSON.stringify({ + note: "Test" + }), + { + "Content-Type": "application/json", + "If-Unmodified-Since-Version": itemVersion + } + ); + Helpers.assert400(response, "'note' property is valid only for note and attachment items"); + }); + + it('test_deleting_parent_item_should_delete_attachment_and_annotation', async function () { + let json = await API.createItem("book", false, this, 'jsonData'); + let itemKey = json.key; + let itemVersion = json.version; + + json = await API.createAttachmentItem( + "imported_file", { contentType: 'application/pdf' }, itemKey, this, 'jsonData' + ); + let attachmentKey = json.key; + let attachmentVersion = json.version; + + let annotationKey = await API.createAnnotationItem( + 'highlight', + { annotationComment: 'ccc' }, + attachmentKey, + this, + 'key' + ); + + const response = await API.userGet( + config.userID, + `items?itemKey=${itemKey},${attachmentKey},${annotationKey}` + ); + Helpers.assertNumResults(response, 3); + + const deleteResponse = await API.userDelete( + config.userID, + `items/${itemKey}`, + { 'If-Unmodified-Since-Version': itemVersion } + ); + Helpers.assert204(deleteResponse); + + const checkResponse = await API.userGet( + config.userID, + `items?itemKey=${itemKey},${attachmentKey},${annotationKey}` + ); + json = await API.getJSONFromResponse(checkResponse); + Helpers.assertNumResults(checkResponse, 0); + }); + + it('test_deleting_group_library_attachment_should_delete_lastPageIndex_setting_for_all_users', async function () { + const json = await API.groupCreateAttachmentItem( + config.ownedPrivateGroupID, + "imported_file", + { contentType: 'application/pdf' }, + null, + this, + 'jsonData' + ); + const attachmentKey = json.key; + const attachmentVersion = json.version; + + // Add setting to both group members + // Set as user 1 + let settingKey = `lastPageIndex_g${config.ownedPrivateGroupID}_${attachmentKey}`; + let response = await API.userPut( + config.userID, + `settings/${settingKey}`, + JSON.stringify({ + value: 123, + version: 0 + }), + { "Content-Type": "application/json" } + ); + Helpers.assert204(response); + + // Set as user 2 + API.useAPIKey(config.user2APIKey); + response = await API.userPut( + config.userID2, + `settings/${settingKey}`, + JSON.stringify({ + value: 234, + version: 0 + }), + { "Content-Type": "application/json" } + ); + Helpers.assert204(response); + + API.useAPIKey(config.apiKey); + + // Delete group item + response = await API.groupDelete( + config.ownedPrivateGroupID, + `items/${attachmentKey}`, + { "If-Unmodified-Since-Version": attachmentVersion } + ); + Helpers.assert204(response); + + // Setting should be gone for both group users + response = await API.userGet( + config.userID, + `settings/${settingKey}` + ); + Helpers.assert404(response); + + response = await API.superGet( + `users/${config.userID2}/settings/${settingKey}` + ); + Helpers.assert404(response); + }); + + it('test_deleting_user_library_attachment_should_delete_lastPageIndex_setting', async function () { + let json = await API.createAttachmentItem('imported_file', { contentType: 'application/pdf' }, null, this, 'jsonData'); + let attachmentKey = json.key; + let attachmentVersion = json.version; + + let settingKey = `lastPageIndex_u_${attachmentKey}`; + let response = await API.userPut( + config.userID, + `settings/${settingKey}`, + JSON.stringify({ + value: 123, + version: 0, + }), + { 'Content-Type': 'application/json' } + ); + Helpers.assert204(response); + + response = await API.userDelete( + config.userID, + `items/${attachmentKey}`, + { 'If-Unmodified-Since-Version': attachmentVersion } + ); + Helpers.assert204(response); + + response = await API.userGet(config.userID, `settings/${settingKey}`); + Helpers.assert404(response); + + response = await API.userGet(config.userID, `deleted?since=${attachmentVersion}`); + json = API.getJSONFromResponse(response); + assert.notInclude(json.settings, settingKey); + }); + + it('test_should_reject_linked_file_attachment_in_group', async function () { + let key = await API.groupCreateItem( + config.ownedPrivateGroupID, + "book", + false, + this, + "key" + ); + const path = "attachments:tést.txt"; + let response = await API.groupCreateAttachmentItem( + config.ownedPrivateGroupID, + "linked_file", + { path: path }, + key, + this, + "response" + ); + Helpers.assert400ForObject( + response, + "Linked files can only be added to user libraries" + ); + }); + + it('test_deleting_linked_file_attachment_should_delete_child_annotation', async function () { + let json = await API.createItem("book", false, this, 'jsonData'); + let itemKey = json.key; + + let attachmentKey = await API.createAttachmentItem( + "linked_file", { contentType: "application/pdf" }, itemKey, this, 'key' + ); + json = await API.createAnnotationItem( + 'highlight', {}, attachmentKey, this, 'jsonData' + ); + let annotationKey = json.key; + let version = json.version; + + // Delete parent item + let response = await API.userDelete( + config.userID, + `items?itemKey=${attachmentKey}`, + { "If-Unmodified-Since-Version": version } + ); + Helpers.assert204(response); + + // Child items should be gone + response = await API.userGet( + config.userID, + `items?itemKey=${itemKey},${attachmentKey},${annotationKey}` + ); + Helpers.assert200(response); + Helpers.assertNumResults(response, 1); + }); + + it('test_should_move_attachment_with_annotation_under_regular_item', async function () { + let json = await API.createItem("book", false, this, 'jsonData'); + let itemKey = json.key; + + // Create standalone attachment to start + json = await API.createAttachmentItem( + "imported_file", { contentType: 'application/pdf' }, null, this, 'jsonData' + ); + let attachmentKey = json.key; + + // Create image annotation + let annotationKey = await API.createAnnotationItem('highlight', {}, attachmentKey, this, 'key'); + + // /top for the annotation key should return the attachment + let response = await API.userGet( + config.userID, + "items/top?itemKey=" + annotationKey + ); + Helpers.assertNumResults(response, 1); + json = await API.getJSONFromResponse(response); + Helpers.assertEquals(attachmentKey, json[0].key); + + // Move attachment under regular item + json[0].data.parentItem = itemKey; + response = await API.userPost( + config.userID, + "items", + JSON.stringify([json[0].data]) + ); + Helpers.assert200ForObject(response); + + // /top for the annotation key should now return the regular item + response = await API.userGet( + config.userID, + "items/top?itemKey=" + annotationKey + ); + Helpers.assertNumResults(response, 1); + json = await API.getJSONFromResponse(response); + Helpers.assertEquals(itemKey, json[0].key); + }); + + it('testDateAddedNewItem8601', async function () { + const objectType = 'item'; + const objectTypePlural = API.getPluralObjectType(objectType); + + const dateAdded = "2013-03-03T21:33:53Z"; + + let itemData = { + title: "Test", + dateAdded: dateAdded + }; + let data = await API.createItem("videoRecording", itemData, this, 'jsonData'); + + Helpers.assertEquals(dateAdded, data.dateAdded); + }); + + it('test_should_reject_embedded_note_for_embedded_image_attachment', async function () { + let noteKey = await API.createNoteItem("Test", null, this, 'key'); + let response = await API.get("items/new?itemType=attachment&linkMode=embedded_image"); + let json = JSON.parse(response.data); + json.parentItem = noteKey; + json.note = '

Foo

'; + response = await API.userPost( + config.userID, + "items", + JSON.stringify([json]), + { "Content-Type": "application/json" } + ); + Helpers.assert400ForObject(response, "'note' property is not valid for embedded images"); + }); + + it('test_deleting_parent_item_should_delete_attachment_and_child_annotation', async function () { + let json = await API.createItem("book", false, this, 'jsonData'); + let itemKey = json.key; + + let attachmentKey = await API.createAttachmentItem( + "imported_url", + { contentType: "application/pdf" }, + itemKey, + this, + 'key' + ); + json = await API.createAnnotationItem('highlight', {}, attachmentKey, this, 'jsonData'); + let annotationKey = json.key; + let version = json.version; + + // Delete parent item + let response = await API.userDelete( + config.userID, + "items?itemKey=" + itemKey, + { "If-Unmodified-Since-Version": version } + ); + Helpers.assert204(response); + + // All items should be gone + response = await API.userGet( + config.userID, + "items?itemKey=" + itemKey + "," + attachmentKey + "," + annotationKey + ); + Helpers.assert200(response); + Helpers.assertNumResults(response, 0); + }); + + it('test_should_preserve_createdByUserID_on_undelete', async function () { + const json = await API.groupCreateItem( + config.ownedPrivateGroupID, "book", false, this, 'json' + ); + const jsonData = json.data; + + assert.equal(json.meta.createdByUser.username, config.username); + + const response = await API.groupDelete( + config.ownedPrivateGroupID, + `items/${json.key}`, + { "If-Unmodified-Since-Version": json.version } + ); + Helpers.assert204(response); + + API.useAPIKey(config.user2APIKey); + jsonData.version = 0; + const postData = JSON.stringify([jsonData]); + const headers = { "Content-Type": "application/json" }; + const postResponse = await API.groupPost( + config.ownedPrivateGroupID, + "items", + postData, + headers + ); + const jsonResponse = await API.getJSONFromResponse(postResponse); + + assert.equal( + jsonResponse.successful[0].meta.createdByUser.username, + config.username + ); + }); + + it('testDateAccessedSQL', async function () { + let date = '2014-02-01 01:23:45'; + let date8601 = '2014-02-01T01:23:45Z'; + let data = await API.createItem("book", { + accessDate: date + }, this, 'jsonData'); + Helpers.assertEquals(date8601, data.accessDate); + }); + + it('testPatchAttachment', async function () { + let json = await API.createAttachmentItem("imported_file", [], false, this, 'jsonData'); + let itemKey = json.key; + let itemVersion = json.version; + + let filename = "test.pdf"; + let mtime = 1234567890000; + let md5 = "390d914fdac33e307e5b0e1f3dba9da2"; + + let response = await API.userPatch( + config.userID, + `items/${itemKey}`, + JSON.stringify({ + filename: filename, + mtime: mtime, + md5: md5, + }), + { + "Content-Type": "application/json", + "If-Unmodified-Since-Version": itemVersion + } + ); + Helpers.assert204(response); + json = (await API.getItem(itemKey, this, 'json')).data; + + Helpers.assertEquals(filename, json.filename); + Helpers.assertEquals(mtime, json.mtime); + Helpers.assertEquals(md5, json.md5); + let headerVersion = parseInt(response.headers["last-modified-version"][0]); + assert.isAbove(headerVersion, itemVersion); + Helpers.assertEquals(json.version, headerVersion); + }); + + it('test_should_move_attachment_with_annotation_out_from_under_regular_item', async function () { + let json = await API.createItem("book", false, this, 'jsonData'); + let itemKey = json.key; + + // Create standalone attachment to start + let attachmentJSON = await API.createAttachmentItem( + "imported_file", { contentType: 'application/pdf' }, itemKey, this, 'jsonData' + ); + let attachmentKey = attachmentJSON.key; + + // Create image annotation + let annotationKey = await API.createAnnotationItem('highlight', {}, attachmentKey, this, 'key'); + + // /top for the annotation key should return the item + let response = await API.userGet( + config.userID, + "items/top?itemKey=" + annotationKey + ); + Helpers.assertNumResults(response, 1); + json = API.getJSONFromResponse(response); + Helpers.assertEquals(itemKey, json[0].key); + + // Move attachment under regular item + attachmentJSON.parentItem = false; + response = await API.userPost( + config.userID, + "items", + JSON.stringify([attachmentJSON]) + ); + Helpers.assert200ForObject(response); + + // /top for the annotation key should now return the attachment item + response = await API.userGet( + config.userID, + "items/top?itemKey=" + annotationKey + ); + Helpers.assertNumResults(response, 1); + json = API.getJSONFromResponse(response); + Helpers.assertEquals(attachmentKey, json[0].key); + }); + + it('test_should_allow_emoji_in_title', async function () { + let title = "🐶"; + + let key = await API.createItem("book", { title: title }, this, 'key'); + + // Test entry (JSON) + let response = await API.userGet( + config.userID, + "items/" + key + ); + assert.include(response.data, "\"title\": \"" + title + "\""); + + // Test feed (JSON) + response = await API.userGet( + config.userID, + "items" + ); + assert.include(response.data, "\"title\": \"" + title + "\""); + + // Test entry (Atom) + response = await API.userGet( + config.userID, + "items/" + key + "?content=json" + ); + assert.include(response.data, "\"title\": \"" + title + "\""); + + // Test feed (Atom) + response = await API.userGet( + config.userID, + "items?content=json" + ); + assert.include(response.data, "\"title\": \"" + title + "\""); + }); + + it('test_should_return_409_on_missing_parent', async function () { + const missingParentKey = "BDARG2AV"; + const json = await API.createNoteItem("

test

", missingParentKey, this); + Helpers.assert409ForObject(json, "Parent item " + missingParentKey + " not found"); + Helpers.assertEquals(missingParentKey, json.failed[0].data.parentItem); + }); + + it('test_num_children_and_children_on_attachment_with_annotation', async function () { + let key = await API.createItem("book", false, this, 'key'); + let attachmentKey = await API.createAttachmentItem("imported_url", { contentType: 'application/pdf', title: 'bbb' }, key, this, 'key'); + let annotationKey = await API.createAnnotationItem("image", { annotationComment: 'ccc' }, attachmentKey, this, 'key'); + let response = await API.userGet(config.userID, `items/${attachmentKey}`); + let json = await API.getJSONFromResponse(response); + Helpers.assertEquals(1, json.meta.numChildren); + response = await API.userGet(config.userID, `items/${attachmentKey}/children`); + Helpers.assert200(response); + json = await API.getJSONFromResponse(response); + Helpers.assertCount(1, json); + Helpers.assertEquals('ccc', json[0].data.annotationComment); + }); + + it('test_should_treat_null_value_as_empty_string', async function () { + let json = { + itemType: 'book', + numPages: null + }; + let response = await API.userPost( + config.userID, + "items", + JSON.stringify([json]), + { + "Content-Type": "application/json" + } + ); + Helpers.assert200ForObject(response); + json = API.getJSONFromResponse(response); + let key = json.successful[0].key; + json = await API.getItem(key, this, 'json'); + + json = { + version: json.version, + itemType: 'journalArticle' + }; + await API.userPatch( + config.userID, + "items/" + key, + JSON.stringify(json), + { + "Content-Type": "application/json" + } + ); + + json = await API.getItem(key, this, 'json'); + assert.notProperty(json, 'numPages'); + }); + + it('testLibraryGroup', async function () { + let json = await API.groupCreateItem( + config.ownedPrivateGroupID, + 'book', + [], + this, + 'json' + ); + assert.equal('group', json.library.type); + assert.equal( + config.ownedPrivateGroupID, + json.library.id + ); + assert.equal( + config.ownedPrivateGroupName, + json.library.name + ); + Helpers.assertRegExp( + /^https?:\/\/[^/]+\/groups\/[0-9]+$/, + json.library.links.alternate.href + ); + assert.equal('text/html', json.library.links.alternate.type); + }); + + it('testPatchTopLevelAttachment', async function () { + let json = await API.createAttachmentItem("imported_url", { + title: 'A', + contentType: 'application/pdf', + filename: 'test.pdf' + }, false, this, 'jsonData'); + + // With 'attachment' and 'linkMode' + json = { + itemType: 'attachment', + linkMode: 'imported_url', + key: json.key, + version: json.version, + title: 'B' + }; + let response = await API.userPost( + config.userID, + "items", + JSON.stringify([json]), + { "Content-Type": "application/json" } + ); + Helpers.assert200ForObject(response); + json = (await API.getItem(json.key, this, 'json')).data; + Helpers.assertEquals("B", json.title); + + // Without 'linkMode' + json = { + itemType: 'attachment', + key: json.key, + version: json.version, + title: 'C' + }; + response = await API.userPost( + config.userID, + "items", + JSON.stringify([json]), + { "Content-Type": "application/json" } + ); + Helpers.assert200ForObject(response); + json = (await API.getItem(json.key, this, 'json')).data; + Helpers.assertEquals("C", json.title); + + // Without 'itemType' or 'linkMode' + json = { + key: json.key, + version: json.version, + title: 'D' + }; + response = await API.userPost( + config.userID, + "items", + JSON.stringify([json]), + { "Content-Type": "application/json" } + ); + Helpers.assert200ForObject(response); + json = (await API.getItem(json.key, this, 'json')).data; + Helpers.assertEquals("D", json.title); + }); + + it('testTopWithSince', async function () { + await API.userClear(config.userID); + + let version1 = await API.getLibraryVersion(); + let parentKeys = []; + parentKeys[0] = await API.createItem('book', [], this, 'key'); + let version2 = await API.getLibraryVersion(); + let childKeys = []; + childKeys[0] = await API.createAttachmentItem('linked_url', [], parentKeys[0], this, 'key'); + let version3 = await API.getLibraryVersion(); + parentKeys[1] = await API.createItem('journalArticle', [], this, 'key'); + let version4 = await API.getLibraryVersion(); + childKeys[1] = await API.createNoteItem('', parentKeys[1], this, 'key'); + let version5 = await API.getLibraryVersion(); + parentKeys[2] = await API.createItem('book', [], this, 'key'); + let version6 = await API.getLibraryVersion(); + + let response = await API.userGet( + config.userID, + 'items/top?since=' + version1 + ); + Helpers.assertNumResults(response, 3); + + response = await API.userGet( + config.userID, + 'items?since=' + version1 + ); + Helpers.assertNumResults(response, 5); + + response = await API.userGet( + config.userID, + 'items/top?format=versions&since=' + version4 + ); + Helpers.assertNumResults(response, 1); + let json = API.getJSONFromResponse(response); + let keys = Object.keys(json); + Helpers.assertEquals(parentKeys[2], keys[0]); + }); + + it('testDateAccessed8601', async function () { + let date = '2014-02-01T01:23:45Z'; + let data = await API.createItem("book", { + accessDate: date + }, this, 'jsonData'); + assert.equal(date, data.accessDate); + }); + + it('testLibraryUser', async function () { + let json = await API.createItem('book', false, this, 'json'); + Helpers.assertEquals('user', json.library.type); + Helpers.assertEquals(config.userID, json.library.id); + Helpers.assertEquals(config.displayName, json.library.name); + Helpers.assertRegExp('^https?://[^/]+/' + config.username, json.library.links.alternate.href); + Helpers.assertEquals('text/html', json.library.links.alternate.type); + }); + + it('test_should_return_409_on_missing_collection', async function () { + let missingCollectionKey = "BDARG2AV"; + let requestPayload = { collections: [missingCollectionKey] }; + let json = await API.createItem("book", requestPayload, this); + Helpers.assert409ForObject(json, `Collection ${missingCollectionKey} not found`); + Helpers.assertEquals(missingCollectionKey, json.failed[0].data.collection); + }); + + it('testIncludeTrashed', async function () { + await API.userClear(config.userID); + + let key1 = await API.createItem("book", false, this, 'key'); + let key2 = await API.createItem("book", { + deleted: 1 + }, this, 'key'); + let key3 = await API.createNoteItem("", key1, this, 'key'); + + // All three items should show up with includeTrashed=1 + let response = await API.userGet( + config.userID, + "items?includeTrashed=1" + ); + let json = await API.getJSONFromResponse(response); + Helpers.assertCount(3, json); + let keys = [json[0].key, json[1].key, json[2].key]; + assert.include(keys, key1); + assert.include(keys, key2); + assert.include(keys, key3); + + // ?itemKey should show the deleted item + response = await API.userGet( + config.userID, + "items?itemKey=" + key2 + "," + key3 + "&includeTrashed=1" + ); + json = await API.getJSONFromResponse(response); + Helpers.assertCount(2, json); + keys = [json[0].key, json[1].key]; + assert.include(keys, key2); + assert.include(keys, key3); + + // /top should show the deleted item + response = await API.userGet( + config.userID, + "items/top?includeTrashed=1" + ); + json = await API.getJSONFromResponse(response); + Helpers.assertCount(2, json); + keys = [json[0].key, json[1].key]; + assert.include(keys, key1); + assert.include(keys, key2); + }); + + it('test_should_return_409_on_missing_parent_if_parent_failed', async function () { + const collectionKey = await API.createCollection("A", {}, this, 'key'); + const version = await API.getLibraryVersion(); + const parentKey = "BDARG2AV"; + const tag = Helpers.uniqueID(300); + const item1JSON = await API.getItemTemplate("book"); + item1JSON.key = parentKey; + item1JSON.creators = [ + { + firstName: "A.", + lastName: "Nespola", + creatorType: "author" + } + ]; + item1JSON.tags = [ + { + tag: "A" + }, + { + tag: tag + } + ]; + item1JSON.collections = [collectionKey]; + const item2JSON = await API.getItemTemplate("note"); + item2JSON.parentItem = parentKey; + const response1 = await API.get("items/new?itemType=attachment&linkMode=linked_url"); + const item3JSON = JSON.parse(response1.data); + item3JSON.parentItem = parentKey; + item3JSON.note = "Test"; + const response2 = await API.userPost( + config.userID, + "items", + JSON.stringify([item1JSON, item2JSON, item3JSON]), + { + "Content-Type": "application/json", + "If-Unmodified-Since-Version": version + } + ); + Helpers.assert200(response2); + const json = await API.getJSONFromResponse(response2); + Helpers.assert413ForObject(json); + Helpers.assert409ForObject(json, { message: "Parent item " + parentKey + " not found", index: 1 }); + Helpers.assertEquals(parentKey, json.failed[1].data.parentItem); + Helpers.assert409ForObject(json, { message: "Parent item " + parentKey + " not found", index: 2 }); + Helpers.assertEquals(parentKey, json.failed[2].data.parentItem); + }); + + it('test_deleting_parent_item_should_delete_child_linked_file_attachment', async function () { + let json = await API.createItem('book', false, this, 'jsonData'); + let parentKey = json.key; + let parentVersion = json.version; + + json = await API.createAttachmentItem('linked_file', [], parentKey, this, 'jsonData'); + let childKey = json.key; + let childVersion = json.version; + + let response = await API.userGet(config.userID, `items?itemKey=${parentKey},${childKey}`); + Helpers.assertNumResults(response, 2); + + response = await API.userDelete( + config.userID, + `items/${parentKey}`, + { 'If-Unmodified-Since-Version': parentVersion } + ); + Helpers.assert204(response); + + response = await API.userGet(config.userID, `items?itemKey=${parentKey},${childKey}`); + json = API.getJSONFromResponse(response); + Helpers.assertNumResults(response, 0); + }); + + it('test_patch_of_item_should_clear_trash_state', async function () { + let json = await API.createItem("book", { + deleted: true + }, this, 'json'); + + let data = [ + { + key: json.key, + version: json.version, + deleted: false + } + ]; + let response = await API.postItems(data); + json = await API.getJSONFromResponse(response); + + assert.notProperty(json.successful[0].data, 'deleted'); + }); + + + /** + * Changing existing 'md5' and 'mtime' values to null was originally prevented, but some client + * versions were sending null, so now we just ignore it. + * + * At some point, we should check whether any clients are still doing this and restore the + * restriction if not. These should only be cleared on a storage purge. + */ + it('test_cannot_change_existing_storage_properties_to_null', async function () { + this.skip(); + }); + + it('testDateAddedNewItemSQL', async function () { + const objectType = 'item'; + const objectTypePlural = API.getPluralObjectType(objectType); + + const dateAdded = "2013-03-03 21:33:53"; + const dateAdded8601 = "2013-03-03T21:33:53Z"; + + let itemData = { + title: "Test", + dateAdded: dateAdded + }; + let data = await API.createItem("videoRecording", itemData, this, 'jsonData'); + + Helpers.assertEquals(dateAdded8601, data.dateAdded); + }); + + it('testDateWithoutDay', async function () { + let date = 'Sept 2012'; + let parsedDate = '2012-09'; + + let json = await API.createItem("book", { + date: date + }, this, 'jsonData'); + let key = json.key; + + let response = await API.userGet( + config.userID, + "items/" + key + ); + json = await API.getJSONFromResponse(response); + Helpers.assertEquals(date, json.data.date); + + // meta.parsedDate (JSON) + Helpers.assertEquals(parsedDate, json.meta.parsedDate); + + // zapi:parsedDate (Atom) + let xml = await API.getItem(key, this, 'atom'); + Helpers.assertEquals(parsedDate, Helpers.xpathEval(xml, '/atom:entry/zapi:parsedDate')); + }); + + it('testDateWithoutMonth', async function () { + let date = '2012'; + let parsedDate = '2012'; + + let json = await API.createItem("book", { + date: date + }, this, 'jsonData'); + let key = json.key; + + let response = await API.userGet( + config.userID, + `items/${key}` + ); + json = API.getJSONFromResponse(response); + assert.equal(date, json.data.date); + + // meta.parsedDate (JSON) + assert.equal(parsedDate, json.meta.parsedDate); + + // zapi:parsedDate (Atom) + let xml = await API.getItem(key, this, 'atom'); + assert.equal(parsedDate, Helpers.xpathEval(xml, '/atom:entry/zapi:parsedDate')); + }); + + it('test_should_allow_changing_parent_item_of_annotation_to_another_file_attachment', async function () { + let attachment1Key = await API.createAttachmentItem("imported_url", { contentType: "application/pdf" }, null, this, 'key'); + let attachment2Key = await API.createAttachmentItem("imported_url", { contentType: "application/pdf" }, null, this, 'key'); + let jsonData = await API.createAnnotationItem('highlight', {}, attachment1Key, this, 'jsonData'); + + let json = { + version: jsonData.version, + parentItem: attachment2Key + }; + let response = await API.userPatch( + config.userID, + `items/${jsonData.key}`, + JSON.stringify(json), + { "Content-Type": "application/json" } + ); + Helpers.assert204(response); + }); + + it('test_should_reject_changing_parent_item_of_annotation_to_invalid_items', async function () { + const itemKey = await API.createItem("book", false, this, 'key'); + const linkedURLAttachmentKey = await API.createAttachmentItem("linked_url", [], itemKey, this, 'key'); + + const attachmentKey = await API.createAttachmentItem( + "imported_url", + { contentType: 'application/pdf' }, + null, + this, + 'key' + ); + const jsonData = await API.createAnnotationItem('highlight', {}, attachmentKey, this, 'jsonData'); + + // No parent + let json = { + version: jsonData.version, + parentItem: false + }; + let response = await API.userPatch( + config.userID, + "items/" + jsonData.key, + JSON.stringify(json) + ); + assert.equal(response.status, 400, "Annotation must have a parent item"); + + // Regular item + json = { + version: jsonData.version, + parentItem: itemKey + }; + response = await API.userPatch( + config.userID, + "items/" + jsonData.key, + JSON.stringify(json) + ); + assert.equal(response.status, 400, "Parent item of annotation must be a PDF attachment"); + + // Linked-URL attachment + json = { + version: jsonData.version, + parentItem: linkedURLAttachmentKey + }; + response = await API.userPatch( + config.userID, + "items/" + jsonData.key, + JSON.stringify(json) + ); + assert.equal(response.status, 400, "Parent item of annotation must be a PDF attachment"); + }); + + it('testConvertChildNoteToParentViaPatch', async function () { + let key = await API.createItem("book", { title: "Test" }, this, 'key'); + let json = await API.createNoteItem("", key, this, 'jsonData'); + json.parentItem = false; + let response = await API.userPatch( + config.userID, + `items/${json.key}`, + JSON.stringify(json), + { "Content-Type": "application/json" } + ); + Helpers.assert204(response); + json = (await API.getItem(json.key, this, 'json')).data; + assert.notProperty(json, 'parentItem'); + }); + + it('test_should_reject_clearing_parent_of_embedded_image_attachment', async function () { + let noteKey = await API.createNoteItem("Test", null, this, 'key'); + let response = await API.get("items/new?itemType=attachment&linkMode=embedded_image"); + let json = JSON.parse(await response.data); + json.parentItem = noteKey; + json.contentType = 'image/png'; + response = await API.userPost( + config.userID, + "items", + JSON.stringify([json]), + { "Content-Type": "application/json" } + ); + Helpers.assert200ForObject(response); + json = API.getJSONFromResponse(response); + let key = json.successful[0].key; + json = await API.getItem(key, this, 'json'); + + // Clear the parent item + json = { + version: json.version, + parentItem: false + }; + response = await API.userPatch( + config.userID, + `items/${key}`, + JSON.stringify(json) + ); + Helpers.assert400(response, "Cannot change parent item of embedded-image attachment"); + }); + + it('test_should_reject_parentItem_that_matches_item_key', async function () { + let response = await API.get("items/new?itemType=attachment&linkMode=imported_file"); + let json = API.getJSONFromResponse(response); + json.key = Helpers.uniqueID(); + json.version = 0; + json.parentItem = json.key; + + response = await API.userPost( + config.userID, + "items", + JSON.stringify([json]), + { "Content-Type": "application/json" } + ); + let msg = "Item " + json.key + " cannot be a child of itself"; + // TEMP + msg += "\n\nCheck your database integrity from the Advanced → Files and Folders pane of the Zotero preferences."; + Helpers.assert400ForObject(response, { message: msg }); + }); + + it('test_num_children_and_children_on_note_with_embedded_image_attachment', async function () { + let noteKey = await API.createNoteItem("Test", null, this, 'key'); + let imageKey = await API.createAttachmentItem('embedded_image', { contentType: 'image/png' }, noteKey, this, 'key'); + let response = await API.userGet(config.userID, `items/${noteKey}`); + let json = await API.getJSONFromResponse(response); + Helpers.assertEquals(1, json.meta.numChildren); + + response = await API.userGet(config.userID, `items/${noteKey}/children`); + Helpers.assert200(response); + json = await API.getJSONFromResponse(response); + Helpers.assertCount(1, json); + Helpers.assertEquals(imageKey, json[0].key); + }); +}); diff --git a/tests/remote_js/test/shared.js b/tests/remote_js/test/shared.js index 80d9d543..0cd5b020 100644 --- a/tests/remote_js/test/shared.js +++ b/tests/remote_js/test/shared.js @@ -79,5 +79,6 @@ module.exports = { await API3.useAPIKey(config.apiKey); await API.userClear(config.userID); }); - } + }, + retryIfNeeded: retryIfNeeded }; From 755e133b876db536fd6d9354f351eeeeee71da0e Mon Sep 17 00:00:00 2001 From: Bogdan Abaev Date: Tue, 30 May 2023 15:00:36 -0400 Subject: [PATCH 15/33] some cleanup, proper retry logic when socket hangs --- tests/remote_js/api2.js | 1 - tests/remote_js/api3.js | 3 +- tests/remote_js/groupsSetup.js | 3 - tests/remote_js/httpHandler.js | 26 +++++++- tests/remote_js/test/2/bibTest.js | 2 +- tests/remote_js/test/2/fileTest.js | 10 +-- tests/remote_js/test/2/objectTest.js | 2 +- tests/remote_js/test/3/bibTest.js | 2 +- tests/remote_js/test/3/fileTest.js | 8 ++- tests/remote_js/test/3/itemTest.js | 34 ++++------ tests/remote_js/test/3/noteTest.js | 2 +- tests/remote_js/test/3/notificationTest.js | 2 +- tests/remote_js/test/3/objectTest.js | 2 +- tests/remote_js/test/3/paramsTest.js | 2 +- tests/remote_js/test/3/publicationTest.js | 6 +- tests/remote_js/test/3/tagTest.js | 2 +- tests/remote_js/test/shared.js | 73 ++++++---------------- 17 files changed, 80 insertions(+), 100 deletions(-) diff --git a/tests/remote_js/api2.js b/tests/remote_js/api2.js index 2253537d..d0b7f108 100644 --- a/tests/remote_js/api2.js +++ b/tests/remote_js/api2.js @@ -634,7 +634,6 @@ class API2 { let json = JSON.parse(response.data); if (responseFormat !== 'responsejson' && (!json.success || Object.keys(json.success).length !== 1)) { - console.log(json); return response; //throw new Error(`${uctype} creation failed`); } diff --git a/tests/remote_js/api3.js b/tests/remote_js/api3.js index 8ee98481..47f58ea1 100644 --- a/tests/remote_js/api3.js +++ b/tests/remote_js/api3.js @@ -3,7 +3,6 @@ const { JSDOM } = require("jsdom"); const API2 = require("./api2.js"); const Helpers = require("./helpers"); const fs = require("fs"); -const wgxpath = require('wgxpath'); class API3 extends API2 { static schemaVersion; @@ -66,7 +65,7 @@ class API3 extends API2 { static async resetSchemaVersion() { const schema = JSON.parse(fs.readFileSync("../../htdocs/zotero-schema/schema.json")); - this.schemaVersion = schema; + this.schemaVersion = schema.version; } diff --git a/tests/remote_js/groupsSetup.js b/tests/remote_js/groupsSetup.js index d7abc3e7..e8099ece 100644 --- a/tests/remote_js/groupsSetup.js +++ b/tests/remote_js/groupsSetup.js @@ -88,9 +88,6 @@ const resetGroups = async () => { await API3.deleteGroup(groupID); } - config.numOwnedGroups = 3; - config.numPublicGroups = 2; - for (let group of groups) { if (!toDelete.includes(group.id)) { await API3.groupClear(group.id); diff --git a/tests/remote_js/httpHandler.js b/tests/remote_js/httpHandler.js index d8d432dc..a184d513 100644 --- a/tests/remote_js/httpHandler.js +++ b/tests/remote_js/httpHandler.js @@ -22,11 +22,31 @@ class HTTP { } //Hardcoded for running tests against containers - if (url.includes("172.16.0.11")) { - url = url.replace('172.16.0.11', 'localhost'); + const localIPRegex = new RegExp("172.16.0.[0-9][0-9]"); + if (url.match(localIPRegex)) { + url = url.replace(localIPRegex, 'localhost'); } - let response = await fetch(url, options); + let success = false; + let attempts = 3; + let tried = 0; + let response; + while (!success && tried < attempts) { + try { + response = await fetch(url, options); + success = true; + } + catch (error) { + if (error.name === 'FetchError') { + console.log('Request aborted. Wait for 2 seconds and retry...'); + await new Promise(r => setTimeout(r, 2000)); + tried += 1; + } + } + } + if (!success) { + throw new Error(`${method} to ${url} did not succeed after ${attempts} attempts.`); + } // Fetch doesn't automatically parse the response body, so we have to do that manually let responseData = await response.text(); diff --git a/tests/remote_js/test/2/bibTest.js b/tests/remote_js/test/2/bibTest.js index 5950b2cb..c841d746 100644 --- a/tests/remote_js/test/2/bibTest.js +++ b/tests/remote_js/test/2/bibTest.js @@ -6,7 +6,7 @@ const Helpers = require('../../helpers.js'); const { API2Setup, API2WrapUp } = require("../shared.js"); describe('BibTests', function () { - this.timeout(0); + this.timeout(config.timeout); let items = {}; let styles = [ diff --git a/tests/remote_js/test/2/fileTest.js b/tests/remote_js/test/2/fileTest.js index 69d17448..2af0d2de 100644 --- a/tests/remote_js/test/2/fileTest.js +++ b/tests/remote_js/test/2/fileTest.js @@ -12,7 +12,7 @@ const util = require('util'); const exec = util.promisify(require('child_process').exec); describe('FileTestTests', function () { - this.timeout(0); + this.timeout(config.timeout); let toDelete = []; const s3Client = new S3Client({ region: "us-east-1" }); @@ -26,7 +26,9 @@ describe('FileTestTests', function () { after(async function () { await API2WrapUp(); - fs.rmdirSync("./work", { recursive: true, force: true }); + fs.rm("./work", { recursive: true, force: true }, (e) => { + if (e) console.log(e); + }); if (toDelete.length > 0) { const commandInput = { Bucket: config.s3Bucket, @@ -69,7 +71,7 @@ describe('FileTestTests', function () { assert.equal(addFileData.md5, md5(viewModeResponse.data)); const userGetDownloadModeResponse = await API.userGet(config.userID, `items/${addFileData.key}/file?key=${config.apiKey}`); Helpers.assert302(userGetDownloadModeResponse); - const downloadModeLocation = userGetDownloadModeResponse.headers.location; + const downloadModeLocation = userGetDownloadModeResponse.headers.location[0]; const s3Response = await HTTP.get(downloadModeLocation); Helpers.assert200(s3Response); assert.equal(addFileData.md5, md5(s3Response.data)); @@ -113,7 +115,7 @@ describe('FileTestTests', function () { Helpers.assert400(response); }); - // Skipped as there may or may not be an error + // Errors it('testAddFileFullParams', async function () { let xml = await API.createAttachmentItem("imported_file", [], false, this); diff --git a/tests/remote_js/test/2/objectTest.js b/tests/remote_js/test/2/objectTest.js index b8cac412..6188bdf1 100644 --- a/tests/remote_js/test/2/objectTest.js +++ b/tests/remote_js/test/2/objectTest.js @@ -6,7 +6,7 @@ const Helpers = require('../../helpers.js'); const { API2Setup, API2WrapUp } = require("../shared.js"); describe('ObjectTests', function () { - this.timeout(0); + this.timeout(config.timeout); before(async function () { await API2Setup(); diff --git a/tests/remote_js/test/3/bibTest.js b/tests/remote_js/test/3/bibTest.js index 8ebd57cd..de0819f7 100644 --- a/tests/remote_js/test/3/bibTest.js +++ b/tests/remote_js/test/3/bibTest.js @@ -6,7 +6,7 @@ const Helpers = require('../../helpers3.js'); const { API3Setup, API3WrapUp } = require("../shared.js"); describe('BibTests', function () { - this.timeout(0); + this.timeout(config.timeout); let items = {}; let multiResponses = {}; diff --git a/tests/remote_js/test/3/fileTest.js b/tests/remote_js/test/3/fileTest.js index a70daee8..5767fa87 100644 --- a/tests/remote_js/test/3/fileTest.js +++ b/tests/remote_js/test/3/fileTest.js @@ -13,7 +13,7 @@ const exec = util.promisify(require('child_process').exec); const JSZIP = require("jszip"); describe('FileTestTests', function () { - this.timeout(0); + this.timeout(config.timeout); let toDelete = []; const s3Client = new S3Client({ region: "us-east-1" }); @@ -27,7 +27,9 @@ describe('FileTestTests', function () { after(async function () { await API3WrapUp(); - fs.rmdirSync("./work", { recursive: true, force: true }); + fs.rm("./work", { recursive: true, force: true }, (e) => { + if (e) console.log(e); + }); if (toDelete.length > 0) { const commandInput = { Bucket: config.s3Bucket, @@ -70,7 +72,7 @@ describe('FileTestTests', function () { `items/${addFileData.key}/file` ); Helpers.assert302(userGetDownloadModeResponse); - const downloadModeLocation = userGetDownloadModeResponse.headers.location; + const downloadModeLocation = userGetDownloadModeResponse.headers.location[0]; const s3Response = await HTTP.get(downloadModeLocation); Helpers.assert200(s3Response); assert.equal(addFileData.md5, Helpers.md5(s3Response.data)); diff --git a/tests/remote_js/test/3/itemTest.js b/tests/remote_js/test/3/itemTest.js index 3e474d29..da065fec 100644 --- a/tests/remote_js/test/3/itemTest.js +++ b/tests/remote_js/test/3/itemTest.js @@ -3,10 +3,10 @@ const assert = chai.assert; const config = require("../../config.js"); const API = require('../../api3.js'); const Helpers = require('../../helpers3.js'); -const { API3Setup, API3WrapUp, resetGroups, retryIfNeeded } = require("../shared.js"); +const { API3Setup, API3WrapUp, resetGroups } = require("../shared.js"); describe('ItemsTests', function () { - this.timeout(0); + this.timeout(config.timeout); before(async function () { await API3Setup(); @@ -18,12 +18,8 @@ describe('ItemsTests', function () { }); this.beforeEach(async function () { - await retryIfNeeded(async () => { - await API.userClear(config.userID); - }); - await retryIfNeeded(async () => { - await API.groupClear(config.ownedPrivateGroupID); - }); + await API.userClear(config.userID); + await API.groupClear(config.ownedPrivateGroupID); API.useAPIKey(config.apiKey); }); @@ -1561,7 +1557,6 @@ describe('ItemsTests', function () { it('testDateAddedNewItem8601TZ', async function () { const objectType = 'item'; - const objectTypePlural = API.getPluralObjectType(objectType); const dateAdded = "2013-03-03T17:33:53-0400"; const dateAddedUTC = "2013-03-03T21:33:53Z"; let itemData = { @@ -1828,7 +1823,6 @@ describe('ItemsTests', function () { "imported_file", { contentType: 'application/pdf' }, itemKey, this, 'jsonData' ); let attachmentKey = json.key; - let attachmentVersion = json.version; let annotationKey = await API.createAnnotationItem( 'highlight', @@ -2049,7 +2043,6 @@ describe('ItemsTests', function () { it('testDateAddedNewItem8601', async function () { const objectType = 'item'; - const objectTypePlural = API.getPluralObjectType(objectType); const dateAdded = "2013-03-03T21:33:53Z"; @@ -2057,8 +2050,10 @@ describe('ItemsTests', function () { title: "Test", dateAdded: dateAdded }; - let data = await API.createItem("videoRecording", itemData, this, 'jsonData'); - + let data; + if (objectType == 'item') { + data = await API.createItem("videoRecording", itemData, this, 'jsonData'); + } Helpers.assertEquals(dateAdded, data.dateAdded); }); @@ -2269,7 +2264,7 @@ describe('ItemsTests', function () { it('test_num_children_and_children_on_attachment_with_annotation', async function () { let key = await API.createItem("book", false, this, 'key'); let attachmentKey = await API.createAttachmentItem("imported_url", { contentType: 'application/pdf', title: 'bbb' }, key, this, 'key'); - let annotationKey = await API.createAnnotationItem("image", { annotationComment: 'ccc' }, attachmentKey, this, 'key'); + await API.createAnnotationItem("image", { annotationComment: 'ccc' }, attachmentKey, this, 'key'); let response = await API.userGet(config.userID, `items/${attachmentKey}`); let json = await API.getJSONFromResponse(response); Helpers.assertEquals(1, json.meta.numChildren); @@ -2404,16 +2399,12 @@ describe('ItemsTests', function () { let version1 = await API.getLibraryVersion(); let parentKeys = []; parentKeys[0] = await API.createItem('book', [], this, 'key'); - let version2 = await API.getLibraryVersion(); let childKeys = []; childKeys[0] = await API.createAttachmentItem('linked_url', [], parentKeys[0], this, 'key'); - let version3 = await API.getLibraryVersion(); parentKeys[1] = await API.createItem('journalArticle', [], this, 'key'); let version4 = await API.getLibraryVersion(); childKeys[1] = await API.createNoteItem('', parentKeys[1], this, 'key'); - let version5 = await API.getLibraryVersion(); parentKeys[2] = await API.createItem('book', [], this, 'key'); - let version6 = await API.getLibraryVersion(); let response = await API.userGet( config.userID, @@ -2560,7 +2551,6 @@ describe('ItemsTests', function () { json = await API.createAttachmentItem('linked_file', [], parentKey, this, 'jsonData'); let childKey = json.key; - let childVersion = json.version; let response = await API.userGet(config.userID, `items?itemKey=${parentKey},${childKey}`); Helpers.assertNumResults(response, 2); @@ -2609,7 +2599,6 @@ describe('ItemsTests', function () { it('testDateAddedNewItemSQL', async function () { const objectType = 'item'; - const objectTypePlural = API.getPluralObjectType(objectType); const dateAdded = "2013-03-03 21:33:53"; const dateAdded8601 = "2013-03-03T21:33:53Z"; @@ -2618,7 +2607,10 @@ describe('ItemsTests', function () { title: "Test", dateAdded: dateAdded }; - let data = await API.createItem("videoRecording", itemData, this, 'jsonData'); + let data; + if (objectType == 'item') { + data = await API.createItem("videoRecording", itemData, this, 'jsonData'); + } Helpers.assertEquals(dateAdded8601, data.dateAdded); }); diff --git a/tests/remote_js/test/3/noteTest.js b/tests/remote_js/test/3/noteTest.js index 176805f4..daba41b6 100644 --- a/tests/remote_js/test/3/noteTest.js +++ b/tests/remote_js/test/3/noteTest.js @@ -7,7 +7,7 @@ const { API3Setup, API3WrapUp } = require("../shared.js"); describe('NoteTests', function () { //this.timeout(config.timeout); - this.timeout(0); + this.timeout(config.timeout); let content, json; diff --git a/tests/remote_js/test/3/notificationTest.js b/tests/remote_js/test/3/notificationTest.js index b699c1ee..692343f7 100644 --- a/tests/remote_js/test/3/notificationTest.js +++ b/tests/remote_js/test/3/notificationTest.js @@ -335,7 +335,7 @@ describe('NotificationTests', function () { finally { let response = await API.superDelete("keys/" + apiKey); try { - Helpers.Helpers.assert204(response); + Helpers.assert204(response); } catch (e) { console.log(e); diff --git a/tests/remote_js/test/3/objectTest.js b/tests/remote_js/test/3/objectTest.js index c0b742ea..e10c1a31 100644 --- a/tests/remote_js/test/3/objectTest.js +++ b/tests/remote_js/test/3/objectTest.js @@ -6,7 +6,7 @@ const Helpers = require('../../helpers3.js'); const { API3Setup, API3WrapUp } = require("../shared.js"); describe('ObjectTests', function () { - this.timeout(0); + this.timeout(config.timeout); let types = ['collection', 'search', 'item']; before(async function () { diff --git a/tests/remote_js/test/3/paramsTest.js b/tests/remote_js/test/3/paramsTest.js index 817fbd58..dc6cf197 100644 --- a/tests/remote_js/test/3/paramsTest.js +++ b/tests/remote_js/test/3/paramsTest.js @@ -6,7 +6,7 @@ const Helpers = require('../../helpers3.js'); const { API3Setup, API3WrapUp } = require("../shared.js"); describe('ParamsTests', function () { - this.timeout(0); + this.timeout(config.timeout); let collectionKeys = []; let itemKeys = []; diff --git a/tests/remote_js/test/3/publicationTest.js b/tests/remote_js/test/3/publicationTest.js index 80af1e0a..bcaa0696 100644 --- a/tests/remote_js/test/3/publicationTest.js +++ b/tests/remote_js/test/3/publicationTest.js @@ -11,7 +11,7 @@ const { JSDOM } = require("jsdom"); describe('PublicationTests', function () { - this.timeout(0); + this.timeout(config.timeout); let toDelete = []; const s3Client = new S3Client({ region: "us-east-1" }); @@ -26,7 +26,9 @@ describe('PublicationTests', function () { after(async function () { await API3WrapUp(); - fs.rmdirSync("./work", { recursive: true, force: true }); + fs.rm("./work", { recursive: true, force: true }, (e) => { + if (e) console.log(e); + }); if (toDelete.length > 0) { const commandInput = { Bucket: config.s3Bucket, diff --git a/tests/remote_js/test/3/tagTest.js b/tests/remote_js/test/3/tagTest.js index 48755356..05e29aa4 100644 --- a/tests/remote_js/test/3/tagTest.js +++ b/tests/remote_js/test/3/tagTest.js @@ -6,7 +6,7 @@ const Helpers = require('../../helpers3.js'); const { API3Setup, API3WrapUp } = require("../shared.js"); describe('TagTests', function () { - this.timeout(0); + this.timeout(config.timeout); before(async function () { await API3Setup(); diff --git a/tests/remote_js/test/shared.js b/tests/remote_js/test/shared.js index 0cd5b020..96bee79e 100644 --- a/tests/remote_js/test/shared.js +++ b/tests/remote_js/test/shared.js @@ -3,34 +3,10 @@ const API = require('../api2.js'); const API3 = require('../api3.js'); const { resetGroups } = require("../groupsSetup.js"); -// To fix socket hang up errors -const retryIfNeeded = async (action) => { - let success = false; - let attempts = 3; - let tried = 0; - while (tried < attempts && !success) { - try { - await action(); - success = true; - } - catch (e) { - console.log(e); - console.log("Waiting for 2 seconds and re-trying."); - await new Promise(r => setTimeout(r, 2000)); - tried += 1; - } - } - if (!success) { - throw new Error(`Setup action did not succeed after ${attempts} retried.`); - } -}; - module.exports = { resetGroups: async () => { - await retryIfNeeded(async () => { - await resetGroups(); - }); + await resetGroups(); }, @@ -46,39 +22,30 @@ module.exports = { }, API2Setup: async () => { - await retryIfNeeded(async () => { - const credentials = await API.login(); - config.apiKey = credentials.user1.apiKey; - config.user2APIKey = credentials.user2.apiKey; - await API.useAPIVersion(2); - await API.setKeyOption(config.userID, config.apiKey, 'libraryNotes', 1); - await API.userClear(config.userID); - }); + const credentials = await API.login(); + config.apiKey = credentials.user1.apiKey; + config.user2APIKey = credentials.user2.apiKey; + await API.useAPIVersion(2); + await API.setKeyOption(config.userID, config.apiKey, 'libraryNotes', 1); + await API.userClear(config.userID); }, API2WrapUp: async () => { - await retryIfNeeded(async () => { - await API.userClear(config.userID); - }); + await API.userClear(config.userID); }, API3Setup: async () => { - await retryIfNeeded(async () => { - const credentials = await API.login(); - config.apiKey = credentials.user1.apiKey; - config.user2APIKey = credentials.user2.apiKey; - await API3.useAPIVersion(3); - await API3.useAPIKey(config.apiKey); - await API3.resetSchemaVersion(); - await API3.setKeyUserPermission(config.apiKey, 'notes', true); - await API3.setKeyUserPermission(config.apiKey, 'write', true); - await API.userClear(config.userID); - }); + const credentials = await API.login(); + config.apiKey = credentials.user1.apiKey; + config.user2APIKey = credentials.user2.apiKey; + await API3.useAPIVersion(3); + await API3.useAPIKey(config.apiKey); + await API3.resetSchemaVersion(); + await API3.setKeyUserPermission(config.apiKey, 'notes', true); + await API3.setKeyUserPermission(config.apiKey, 'write', true); + await API.userClear(config.userID); }, API3WrapUp: async () => { - await retryIfNeeded(async () => { - await API3.useAPIKey(config.apiKey); - await API.userClear(config.userID); - }); - }, - retryIfNeeded: retryIfNeeded + await API3.useAPIKey(config.apiKey); + await API.userClear(config.userID); + } }; From 319ce22875402c541b25baaf7bb3d132aaccc128 Mon Sep 17 00:00:00 2001 From: Bogdan Abaev Date: Tue, 30 May 2023 15:39:22 -0400 Subject: [PATCH 16/33] using node-config --- tests/remote_js/api2.js | 91 +++++++++---------- tests/remote_js/api3.js | 75 +++++++-------- tests/remote_js/config.js | 10 -- .../{config.json => config/default.json5} | 23 ++--- tests/remote_js/groupsSetup.js | 2 +- tests/remote_js/httpHandler.js | 2 +- tests/remote_js/test/1/collectionTest.js | 2 +- tests/remote_js/test/1/itemsTest.js | 2 +- tests/remote_js/test/2/atomTest.js | 2 +- tests/remote_js/test/2/bibTest.js | 2 +- tests/remote_js/test/2/cacheTest.js | 2 +- tests/remote_js/test/2/collectionTest.js | 2 +- tests/remote_js/test/2/creatorTest.js | 2 +- tests/remote_js/test/2/fileTest.js | 2 +- tests/remote_js/test/2/fullText.js | 2 +- tests/remote_js/test/2/generalTest.js | 2 +- tests/remote_js/test/2/groupTest.js | 2 +- tests/remote_js/test/2/itemsTest.js | 2 +- tests/remote_js/test/2/mappingsTest.js | 2 +- tests/remote_js/test/2/noteTest.js | 2 +- tests/remote_js/test/2/objectTest.js | 2 +- tests/remote_js/test/2/paramTest.js | 2 +- tests/remote_js/test/2/permissionsTest.js | 2 +- tests/remote_js/test/2/relationsTest.js | 2 +- tests/remote_js/test/2/searchTest.js | 2 +- tests/remote_js/test/2/settingsTest.js | 2 +- tests/remote_js/test/2/sortTest.js | 2 +- tests/remote_js/test/2/storageAdmin.js | 2 +- tests/remote_js/test/2/tagTest.js | 2 +- tests/remote_js/test/2/versionTest.js | 2 +- tests/remote_js/test/3/annotationsTest.js | 2 +- tests/remote_js/test/3/atomTest.js | 2 +- tests/remote_js/test/3/bibTest.js | 2 +- tests/remote_js/test/3/cacheTest.js | 2 +- tests/remote_js/test/3/collectionTest.js | 6 +- tests/remote_js/test/3/creatorTest.js | 2 +- tests/remote_js/test/3/exportTest.js | 2 +- tests/remote_js/test/3/fileTest.js | 2 +- tests/remote_js/test/3/fullTextTest.js | 2 +- tests/remote_js/test/3/generalTest.js | 2 +- tests/remote_js/test/3/groupTest.js | 2 +- tests/remote_js/test/3/itemTest.js | 2 +- tests/remote_js/test/3/keysTest.js | 2 +- tests/remote_js/test/3/mappingsTest.js | 2 +- tests/remote_js/test/3/noteTest.js | 2 +- tests/remote_js/test/3/notificationTest.js | 2 +- tests/remote_js/test/3/objectTest.js | 2 +- tests/remote_js/test/3/paramsTest.js | 2 +- tests/remote_js/test/3/permissionTest.js | 2 +- tests/remote_js/test/3/publicationTest.js | 2 +- tests/remote_js/test/3/relationTest.js | 2 +- tests/remote_js/test/3/searchTest.js | 2 +- tests/remote_js/test/3/settingsTest.js | 2 +- tests/remote_js/test/3/sortTest.js | 2 +- tests/remote_js/test/3/storageAdmin.js | 2 +- tests/remote_js/test/3/tagTest.js | 2 +- tests/remote_js/test/3/template.js | 2 +- tests/remote_js/test/3/translationTest.js | 2 +- tests/remote_js/test/3/versionTest.js | 2 +- tests/remote_js/test/shared.js | 2 +- 60 files changed, 151 insertions(+), 164 deletions(-) delete mode 100644 tests/remote_js/config.js rename tests/remote_js/{config.json => config/default.json5} (63%) diff --git a/tests/remote_js/api2.js b/tests/remote_js/api2.js index d0b7f108..45a60056 100644 --- a/tests/remote_js/api2.js +++ b/tests/remote_js/api2.js @@ -2,23 +2,22 @@ const HTTP = require("./httpHandler"); const { JSDOM } = require("jsdom"); const wgxpath = require('wgxpath'); +var config = require('config'); class API2 { static apiVersion = null; - static config = require("./config"); - static useAPIVersion(version) { this.apiVersion = version; } static async login() { const response = await HTTP.post( - `${this.config.apiURLPrefix}test/setup?u=${this.config.userID}&u2=${this.config.userID2}`, + `${config.apiURLPrefix}test/setup?u=${config.userID}&u2=${config.userID2}`, " ", {}, { - username: this.config.rootUsername, - password: this.config.rootPassword + username: config.rootUsername, + password: config.rootPassword }); if (!response.data) { throw new Error("Could not fetch credentials!"); @@ -41,8 +40,8 @@ class API2 { } let response = await this.userPost( - this.config.userID, - `items?key=${this.config.apiKey}`, + config.userID, + `items?key=${config.apiKey}`, JSON.stringify({ items: [json] }), @@ -54,8 +53,8 @@ class API2 { static async postItems(json) { return this.userPost( - this.config.userID, - `items?key=${this.config.apiKey}`, + config.userID, + `items?key=${config.apiKey}`, JSON.stringify({ items: json }), @@ -73,7 +72,7 @@ class API2 { response = await this.groupPost( groupID, - `items?key=${this.config.apiKey}`, + `items?key=${config.apiKey}`, JSON.stringify({ items: [json] }), @@ -121,8 +120,8 @@ class API2 { } response = await this.userPost( - this.config.userID, - `items?key=${this.config.apiKey}`, + config.userID, + `items?key=${config.apiKey}`, JSON.stringify({ items: [json] }), @@ -181,7 +180,7 @@ class API2 { response = await this.groupPost( groupID, - `items?key=${this.config.apiKey}`, + `items?key=${config.apiKey}`, JSON.stringify({ items: [json] }), @@ -238,8 +237,8 @@ class API2 { } response = await this.userPost( - this.config.userID, - `items?key=${this.config.apiKey}`, + config.userID, + `items?key=${config.apiKey}`, JSON.stringify({ items: [json] }), @@ -308,8 +307,8 @@ class API2 { }; const response = await this.userPost( - this.config.userID, - `collections?key=${this.config.apiKey}`, + config.userID, + `collections?key=${config.apiKey}`, JSON.stringify(json), { "Content-Type": "application/json" } ); @@ -338,8 +337,8 @@ class API2 { }; const response = await this.userPost( - this.config.userID, - `searches?key=${this.config.apiKey}`, + config.userID, + `searches?key=${config.apiKey}`, JSON.stringify(json), { "Content-Type": "application/json" } ); @@ -349,8 +348,8 @@ class API2 { static async getLibraryVersion() { const response = await this.userGet( - this.config.userID, - `items?key=${this.config.apiKey}&format=keys&limit=1` + config.userID, + `items?key=${config.apiKey}&format=keys&limit=1` ); return response.headers["last-modified-version"][0]; } @@ -358,7 +357,7 @@ class API2 { static async getGroupLibraryVersion(groupID) { const response = await this.groupGet( groupID, - `items?key=${this.config.apiKey}&format=keys&limit=1` + `items?key=${config.apiKey}&format=keys&limit=1` ); return response.headers["last-modified-version"][0]; } @@ -374,7 +373,7 @@ class API2 { const response = await this.groupGet( groupID, - `items?key=${this.config.apiKey}&itemKey=${keys.join(',')}&order=itemKeyList&content=json` + `items?key=${config.apiKey}&itemKey=${keys.join(',')}&order=itemKeyList&content=json` ); if (context && response.status != 200) { throw new Error("Group set request failed."); @@ -397,14 +396,14 @@ class API2 { // Simple http requests with no dependencies static async get(url, headers = {}, auth = null) { - url = this.config.apiURLPrefix + url; + url = config.apiURLPrefix + url; if (this.apiVersion) { headers["Zotero-API-Version"] = this.apiVersion; } const response = await HTTP.get(url, headers, auth); - if (this.config.verbose >= 2) { + if (config.verbose >= 2) { console.log("\n\n" + response.data + "\n"); } @@ -413,8 +412,8 @@ class API2 { static async superGet(url, headers = {}) { return this.get(url, headers, { - username: this.config.username, - password: this.config.password + username: config.username, + password: config.password }); } @@ -427,7 +426,7 @@ class API2 { } static async post(url, data, headers = {}, auth = null) { - url = this.config.apiURLPrefix + url; + url = config.apiURLPrefix + url; if (this.apiVersion) { headers["Zotero-API-Version"] = this.apiVersion; } @@ -444,7 +443,7 @@ class API2 { } static async put(url, data, headers = {}, auth = null) { - url = this.config.apiURLPrefix + url; + url = config.apiURLPrefix + url; if (this.apiVersion) { headers["Zotero-API-Version"] = this.apiVersion; } @@ -461,7 +460,7 @@ class API2 { } static async patch(url, data, headers = {}, auth = null) { - url = this.config.apiURLPrefix + url; + url = config.apiURLPrefix + url; if (this.apiVersion) { headers["Zotero-API-Version"] = this.apiVersion; } @@ -474,7 +473,7 @@ class API2 { } static async delete(url, headers = {}, auth = null) { - url = this.config.apiURLPrefix + url; + url = config.apiURLPrefix + url; if (this.apiVersion) { headers["Zotero-API-Version"] = this.apiVersion; } @@ -486,7 +485,7 @@ class API2 { } static async head(url, headers = {}, auth = null) { - url = this.config.apiURLPrefix + url; + url = config.apiURLPrefix + url; if (this.apiVersion) { headers["Zotero-API-Version"] = this.apiVersion; } @@ -501,8 +500,8 @@ class API2 { "", {}, { - username: this.config.rootUsername, - password: this.config.rootPassword + username: config.rootUsername, + password: config.rootPassword } ); if (response.status !== 204) { @@ -518,8 +517,8 @@ class API2 { "", {}, { - username: this.config.rootUsername, - password: this.config.rootPassword + username: config.rootUsername, + password: config.rootPassword } ); @@ -611,8 +610,8 @@ class API2 { } let response = await this.userGet( - this.config.userID, - `${objectTypePlural}?key=${this.config.apiKey}&${objectType}Key=${keys.join(',')}&order=${objectType}KeyList&content=json` + config.userID, + `${objectTypePlural}?key=${config.apiKey}&${objectType}Key=${keys.join(',')}&order=${objectType}KeyList&content=json` ); // Checking the response status @@ -686,8 +685,8 @@ class API2 { `users/${userID}/keys/${key}`, {}, { - username: this.config.rootUsername, - password: this.config.rootPassword + username: config.rootUsername, + password: config.rootPassword } ); @@ -716,12 +715,12 @@ class API2 { if (current !== val) { access.setAttribute('notes', val); response = await this.put( - `users/${this.config.userID}/keys/${this.config.apiKey}`, + `users/${config.userID}/keys/${config.apiKey}`, xml.documentElement.outerHTML, {}, { - username: this.config.rootUsername, - password: this.config.rootPassword + username: config.rootUsername, + password: config.rootPassword } ); if (response.status !== 200) { @@ -741,12 +740,12 @@ class API2 { if (current !== val) { access.setAttribute('write', val); response = await this.put( - `users/${this.config.userID}/keys/${this.config.apiKey}`, + `users/${config.userID}/keys/${config.apiKey}`, xml.documentElement.outerHTML, {}, { - username: this.config.rootUsername, - password: this.config.rootPassword + username: config.rootUsername, + password: config.rootPassword } ); if (response.status !== 200) { diff --git a/tests/remote_js/api3.js b/tests/remote_js/api3.js index 47f58ea1..a9e8ad0f 100644 --- a/tests/remote_js/api3.js +++ b/tests/remote_js/api3.js @@ -3,20 +3,21 @@ const { JSDOM } = require("jsdom"); const API2 = require("./api2.js"); const Helpers = require("./helpers"); const fs = require("fs"); +var config = require('config'); class API3 extends API2 { static schemaVersion; static apiVersion = 3; - static apiKey = this.config.apiKey; + static apiKey = config.apiKey; static useAPIKey(key) { this.apiKey = key; } static async get(url, headers = {}, auth = false) { - url = this.config.apiURLPrefix + url; + url = config.apiURLPrefix + url; if (this.apiVersion) { headers["Zotero-API-Version"] = this.apiVersion; } @@ -27,7 +28,7 @@ class API3 extends API2 { headers.Authorization = "Bearer " + this.apiKey; } let response = await HTTP.get(url, headers, auth); - if (this.config.verbose >= 2) { + if (config.verbose >= 2) { console.log("\n\n" + response.data + "\n"); } return response; @@ -38,7 +39,7 @@ class API3 extends API2 { } static async head(url, headers = {}, auth = false) { - url = this.config.apiURLPrefix + url; + url = config.apiURLPrefix + url; if (this.apiVersion) { headers["Zotero-API-Version"] = this.apiVersion; } @@ -49,7 +50,7 @@ class API3 extends API2 { headers.Authorization = "Bearer " + this.apiKey; } let response = await HTTP.head(url, headers, auth); - if (this.config.verbose >= 2) { + if (config.verbose >= 2) { console.log("\n\n" + response.data + "\n"); } return response; @@ -70,7 +71,7 @@ class API3 extends API2 { static async delete(url, headers = {}, auth = false) { - url = this.config.apiURLPrefix + url; + url = config.apiURLPrefix + url; if (this.apiVersion) { headers["Zotero-API-Version"] = this.apiVersion; } @@ -85,7 +86,7 @@ class API3 extends API2 { } static async post(url, data, headers = {}, auth = false) { - url = this.config.apiURLPrefix + url; + url = config.apiURLPrefix + url; if (this.apiVersion) { headers["Zotero-API-Version"] = this.apiVersion; } @@ -102,23 +103,23 @@ class API3 extends API2 { static async superGet(url, headers = {}) { return this.get(url, headers, { - username: this.config.rootUsername, - password: this.config.rootPassword + username: config.rootUsername, + password: config.rootPassword }); } static async superPost(url, data, headers = {}) { return this.post(url, data, headers, { - username: this.config.rootUsername, - password: this.config.rootPassword + username: config.rootUsername, + password: config.rootPassword }); } static async superDelete(url, headers = {}) { return this.delete(url, headers, { - username: this.config.rootUsername, - password: this.config.rootPassword + username: config.rootUsername, + password: config.rootPassword }); } @@ -227,7 +228,7 @@ class API3 extends API2 { response = await this.groupPost( groupID, - `items?key=${this.config.apiKey}`, + `items?key=${config.apiKey}`, JSON.stringify([json]), { "Content-Type": "application/json" } ); @@ -239,8 +240,8 @@ class API3 extends API2 { `keys/${key}`, {}, { - username: `${this.config.rootUsername}`, - password: `${this.config.rootPassword}` + username: `${config.rootUsername}`, + password: `${config.rootPassword}` } ); if (response.status != 200) { @@ -260,12 +261,12 @@ class API3 extends API2 { } delete json.access.groups; response = await this.put( - `users/${this.config.userID}/keys/${this.config.apiKey}`, + `users/${config.userID}/keys/${config.apiKey}`, JSON.stringify(json), {}, { - username: `${this.config.rootUsername}`, - password: `${this.config.rootPassword}` + username: `${config.rootUsername}`, + password: `${config.rootPassword}` } ); if (response.status != 200) { @@ -306,7 +307,7 @@ class API3 extends API2 { } response = await this.userPost( - this.config.userID, + config.userID, `items?key=${this.apiKey}`, JSON.stringify([json]), { "Content-Type": "application/json" } @@ -323,7 +324,7 @@ class API3 extends API2 { static async postObjects(objectType, json) { let objectTypPlural = this.getPluralObjectType(objectType); return this.userPost( - this.config.userID, + config.userID, objectTypPlural, JSON.stringify(json), { "Content-Type": "application/json" } @@ -342,7 +343,7 @@ class API3 extends API2 { } static async patch(url, data, headers = {}, auth = false) { - let apiUrl = this.config.apiURLPrefix + url; + let apiUrl = config.apiURLPrefix + url; if (this.apiVersion) { headers["Zotero-API-Version"] = this.apiVersion; } @@ -380,7 +381,7 @@ class API3 extends API2 { }; static async put(url, data, headers = {}, auth = false) { - url = this.config.apiURLPrefix + url; + url = config.apiURLPrefix + url; if (this.apiVersion) { headers["Zotero-API-Version"] = this.apiVersion; } @@ -480,8 +481,8 @@ class API3 extends API2 { static async superPut(url, data, headers) { return this.put(url, data, headers, { - username: this.config.rootUsername, - password: this.config.rootPassword + username: config.rootUsername, + password: config.rootPassword }); } @@ -530,8 +531,8 @@ class API3 extends API2 { "keys/" + key, {}, { - username: this.config.rootUsername, - password: this.config.rootPassword + username: config.rootUsername, + password: config.rootPassword } ); if (response.status != 200) { @@ -553,8 +554,8 @@ class API3 extends API2 { JSON.stringify(json), {}, { - username: this.config.rootUsername, - password: this.config.rootPassword + username: config.rootUsername, + password: config.rootPassword } ); if (response.status != 200) { @@ -608,7 +609,7 @@ class API3 extends API2 { let requestBody = JSON.stringify([json]); - let response = await this.userPost(this.config.userID, "items", requestBody, headers); + let response = await this.userPost(config.userID, "items", requestBody, headers); return this.handleCreateResponse('item', response, returnFormat, context); } @@ -618,8 +619,8 @@ class API3 extends API2 { "keys/" + key, {}, { - username: this.config.rootUsername, - password: this.config.rootPassword + username: config.rootUsername, + password: config.rootPassword } ); if (response.status != 200) { @@ -664,8 +665,8 @@ class API3 extends API2 { JSON.stringify(json), {}, { - username: this.config.rootUsername, - password: this.config.rootPassword + username: config.rootUsername, + password: config.rootPassword } ); } @@ -715,8 +716,8 @@ class API3 extends API2 { xml.outterHTML, {}, { - username: this.config.rootUsername, - password: this.config.rootPassword + username: config.rootUsername, + password: config.rootPassword } ); } @@ -909,7 +910,7 @@ class API3 extends API2 { response = await this.groupGet(groupID, url); } else { - response = await this.userGet(this.config.userID, url); + response = await this.userGet(config.userID, url); } if (context) { Helpers.assert200(response); diff --git a/tests/remote_js/config.js b/tests/remote_js/config.js deleted file mode 100644 index 3b1b7e05..00000000 --- a/tests/remote_js/config.js +++ /dev/null @@ -1,10 +0,0 @@ -const fs = require('fs'); - -var config = {}; - -const data = fs.readFileSync('config.json'); -config = JSON.parse(data); -config.timeout = 60000; -config.numOwnedGroups = 3; -config.numPublicGroups = 2; -module.exports = config; diff --git a/tests/remote_js/config.json b/tests/remote_js/config/default.json5 similarity index 63% rename from tests/remote_js/config.json rename to tests/remote_js/config/default.json5 index f47a8a0d..66473a9a 100644 --- a/tests/remote_js/config.json +++ b/tests/remote_js/config/default.json5 @@ -1,17 +1,16 @@ { - "verbose": 1, - "syncURLPrefix": "http://dataserver/", + "verbose": 0, "apiURLPrefix": "http://localhost/", - "rootUsername": "YtTnrHcWUC0FqP27xuaa", - "rootPassword": "esFEIngwxnyp1kuTrUpKpH72gEftHbkiWneoeimV", - "awsRegion": "us-east-1", - "s3Bucket": "zotero", + "rootUsername": "", + "rootPassword": "", + "awsRegion": "", + "s3Bucket": "", "awsAccessKey": "", "awsSecretKey": "", - "filesystemStorage": 1, - "syncVersion": 9, - - + "timeout": 30000, + "numOwnedGroups": 3, + "numPublicGroups": 2, + "userID": 1, "libraryID": 1, "username": "testuser", @@ -23,13 +22,11 @@ "ownedPrivateGroupLibraryID": 1, "ownedPrivateGroupName": "Test Group", - "userID2": 2, "username2": "testuser2", "password2": "letmein2", "displayName2": "testuser2", "ownedPrivateGroupID2": 0, "ownedPrivateGroupLibraryID2": 0 - - + } diff --git a/tests/remote_js/groupsSetup.js b/tests/remote_js/groupsSetup.js index e8099ece..5e17d6a4 100644 --- a/tests/remote_js/groupsSetup.js +++ b/tests/remote_js/groupsSetup.js @@ -1,5 +1,5 @@ -const config = require('./config'); +var config = require('config'); const API3 = require('./api3.js'); const resetGroups = async () => { diff --git a/tests/remote_js/httpHandler.js b/tests/remote_js/httpHandler.js index a184d513..556e155e 100644 --- a/tests/remote_js/httpHandler.js +++ b/tests/remote_js/httpHandler.js @@ -1,5 +1,5 @@ const fetch = require('node-fetch'); -const config = require('./config'); +var config = require('config'); class HTTP { static verbose = config.verbose; diff --git a/tests/remote_js/test/1/collectionTest.js b/tests/remote_js/test/1/collectionTest.js index a7dd6cfe..a842a454 100644 --- a/tests/remote_js/test/1/collectionTest.js +++ b/tests/remote_js/test/1/collectionTest.js @@ -1,6 +1,6 @@ const chai = require('chai'); const assert = chai.assert; -const config = require("../../config.js"); +var config = require('config'); const API = require('../../api2.js'); const Helpers = require('../../helpers.js'); const { API1Setup, API1WrapUp } = require("../shared.js"); diff --git a/tests/remote_js/test/1/itemsTest.js b/tests/remote_js/test/1/itemsTest.js index 56dee3c1..7259d2c1 100644 --- a/tests/remote_js/test/1/itemsTest.js +++ b/tests/remote_js/test/1/itemsTest.js @@ -1,6 +1,6 @@ const { assert } = require('chai'); const API = require('../../api2.js'); -const config = require("../../config.js"); +var config = require('config'); const Helpers = require('../../helpers.js'); const { API1Setup, API1WrapUp } = require("../shared.js"); diff --git a/tests/remote_js/test/2/atomTest.js b/tests/remote_js/test/2/atomTest.js index e475baec..77bdf6d2 100644 --- a/tests/remote_js/test/2/atomTest.js +++ b/tests/remote_js/test/2/atomTest.js @@ -1,6 +1,6 @@ const chai = require('chai'); const assert = chai.assert; -const config = require("../../config.js"); +var config = require('config'); const API = require('../../api2.js'); const Helpers = require('../../helpers.js'); const { JSDOM } = require('jsdom'); diff --git a/tests/remote_js/test/2/bibTest.js b/tests/remote_js/test/2/bibTest.js index c841d746..29784553 100644 --- a/tests/remote_js/test/2/bibTest.js +++ b/tests/remote_js/test/2/bibTest.js @@ -1,6 +1,6 @@ const chai = require('chai'); const assert = chai.assert; -const config = require("../../config.js"); +var config = require('config'); const API = require('../../api2.js'); const Helpers = require('../../helpers.js'); const { API2Setup, API2WrapUp } = require("../shared.js"); diff --git a/tests/remote_js/test/2/cacheTest.js b/tests/remote_js/test/2/cacheTest.js index ecde8216..d203640a 100644 --- a/tests/remote_js/test/2/cacheTest.js +++ b/tests/remote_js/test/2/cacheTest.js @@ -1,6 +1,6 @@ const chai = require('chai'); const assert = chai.assert; -const config = require("../../config.js"); +var config = require('config'); const API = require('../../api2.js'); const { API2Setup, API2WrapUp } = require("../shared.js"); diff --git a/tests/remote_js/test/2/collectionTest.js b/tests/remote_js/test/2/collectionTest.js index b12ab278..05eb0ab7 100644 --- a/tests/remote_js/test/2/collectionTest.js +++ b/tests/remote_js/test/2/collectionTest.js @@ -1,6 +1,6 @@ const chai = require('chai'); const assert = chai.assert; -const config = require("../../config.js"); +var config = require('config'); const API = require('../../api2.js'); const Helpers = require('../../helpers.js'); const { API2Setup, API2WrapUp } = require("../shared.js"); diff --git a/tests/remote_js/test/2/creatorTest.js b/tests/remote_js/test/2/creatorTest.js index 3c57f758..e0ba78f0 100644 --- a/tests/remote_js/test/2/creatorTest.js +++ b/tests/remote_js/test/2/creatorTest.js @@ -1,6 +1,6 @@ const chai = require('chai'); const assert = chai.assert; -const config = require("../../config.js"); +var config = require('config'); const API = require('../../api2.js'); const Helpers = require('../../helpers.js'); const { API2Setup, API2WrapUp } = require("../shared.js"); diff --git a/tests/remote_js/test/2/fileTest.js b/tests/remote_js/test/2/fileTest.js index 2af0d2de..9cc55bf2 100644 --- a/tests/remote_js/test/2/fileTest.js +++ b/tests/remote_js/test/2/fileTest.js @@ -1,6 +1,6 @@ const chai = require('chai'); const assert = chai.assert; -const config = require("../../config.js"); +var config = require('config'); const API = require('../../api2.js'); const Helpers = require('../../helpers.js'); const { API2Setup, API2WrapUp } = require("../shared.js"); diff --git a/tests/remote_js/test/2/fullText.js b/tests/remote_js/test/2/fullText.js index c558e062..6409d390 100644 --- a/tests/remote_js/test/2/fullText.js +++ b/tests/remote_js/test/2/fullText.js @@ -1,6 +1,6 @@ const chai = require('chai'); const assert = chai.assert; -const config = require("../../config.js"); +var config = require('config'); const API = require('../../api2.js'); const Helpers = require('../../helpers.js'); const { API2Setup, API2WrapUp } = require("../shared.js"); diff --git a/tests/remote_js/test/2/generalTest.js b/tests/remote_js/test/2/generalTest.js index 7fbbca4b..f91f31c4 100644 --- a/tests/remote_js/test/2/generalTest.js +++ b/tests/remote_js/test/2/generalTest.js @@ -1,6 +1,6 @@ const chai = require('chai'); const assert = chai.assert; -const config = require("../../config.js"); +var config = require('config'); const API = require('../../api2.js'); const Helpers = require('../../helpers.js'); const { API2Setup, API2WrapUp } = require("../shared.js"); diff --git a/tests/remote_js/test/2/groupTest.js b/tests/remote_js/test/2/groupTest.js index 6113cabe..7af2f58c 100644 --- a/tests/remote_js/test/2/groupTest.js +++ b/tests/remote_js/test/2/groupTest.js @@ -1,6 +1,6 @@ const chai = require('chai'); const assert = chai.assert; -const config = require("../../config.js"); +var config = require('config'); const API = require('../../api2.js'); const Helpers = require('../../helpers.js'); const { API2Setup, API2WrapUp, resetGroups } = require("../shared.js"); diff --git a/tests/remote_js/test/2/itemsTest.js b/tests/remote_js/test/2/itemsTest.js index 0ede51db..012ad02a 100644 --- a/tests/remote_js/test/2/itemsTest.js +++ b/tests/remote_js/test/2/itemsTest.js @@ -1,6 +1,6 @@ const chai = require('chai'); const assert = chai.assert; -const config = require("../../config.js"); +var config = require('config'); const API = require('../../api2.js'); const Helpers = require('../../helpers.js'); const { API2Setup, API2WrapUp } = require("../shared.js"); diff --git a/tests/remote_js/test/2/mappingsTest.js b/tests/remote_js/test/2/mappingsTest.js index 471b469f..98cc0daa 100644 --- a/tests/remote_js/test/2/mappingsTest.js +++ b/tests/remote_js/test/2/mappingsTest.js @@ -1,6 +1,6 @@ const chai = require('chai'); const assert = chai.assert; -const config = require("../../config.js"); +var config = require('config'); const API = require('../../api2.js'); const Helpers = require('../../helpers.js'); const { API2Setup, API2WrapUp } = require("../shared.js"); diff --git a/tests/remote_js/test/2/noteTest.js b/tests/remote_js/test/2/noteTest.js index c802c52e..c1f62b18 100644 --- a/tests/remote_js/test/2/noteTest.js +++ b/tests/remote_js/test/2/noteTest.js @@ -1,6 +1,6 @@ const chai = require('chai'); const assert = chai.assert; -const config = require("../../config.js"); +var config = require('config'); const API = require('../../api2.js'); const Helpers = require("../../helpers.js"); const { API2Setup, API2WrapUp } = require("../shared.js"); diff --git a/tests/remote_js/test/2/objectTest.js b/tests/remote_js/test/2/objectTest.js index 6188bdf1..500d0d81 100644 --- a/tests/remote_js/test/2/objectTest.js +++ b/tests/remote_js/test/2/objectTest.js @@ -1,6 +1,6 @@ const chai = require('chai'); const assert = chai.assert; -const config = require("../../config.js"); +var config = require('config'); const API = require('../../api2.js'); const Helpers = require('../../helpers.js'); const { API2Setup, API2WrapUp } = require("../shared.js"); diff --git a/tests/remote_js/test/2/paramTest.js b/tests/remote_js/test/2/paramTest.js index f098b449..86c8893f 100644 --- a/tests/remote_js/test/2/paramTest.js +++ b/tests/remote_js/test/2/paramTest.js @@ -1,6 +1,6 @@ const chai = require('chai'); const assert = chai.assert; -const config = require("../../config.js"); +var config = require('config'); const API = require('../../api2.js'); const Helpers = require('../../helpers.js'); const { API2Setup, API2WrapUp } = require("../shared.js"); diff --git a/tests/remote_js/test/2/permissionsTest.js b/tests/remote_js/test/2/permissionsTest.js index ed4d385b..3916b101 100644 --- a/tests/remote_js/test/2/permissionsTest.js +++ b/tests/remote_js/test/2/permissionsTest.js @@ -1,6 +1,6 @@ const chai = require('chai'); const assert = chai.assert; -const config = require("../../config.js"); +var config = require('config'); const API = require('../../api2.js'); const Helpers = require('../../helpers.js'); const { API2Setup, API2WrapUp, resetGroups } = require("../shared.js"); diff --git a/tests/remote_js/test/2/relationsTest.js b/tests/remote_js/test/2/relationsTest.js index 1b8f7a41..4eca7b94 100644 --- a/tests/remote_js/test/2/relationsTest.js +++ b/tests/remote_js/test/2/relationsTest.js @@ -1,6 +1,6 @@ const chai = require('chai'); const assert = chai.assert; -const config = require("../../config.js"); +var config = require('config'); const API = require('../../api2.js'); const Helpers = require('../../helpers.js'); const { API2Setup, API2WrapUp } = require("../shared.js"); diff --git a/tests/remote_js/test/2/searchTest.js b/tests/remote_js/test/2/searchTest.js index fb64aa3c..f0594dfa 100644 --- a/tests/remote_js/test/2/searchTest.js +++ b/tests/remote_js/test/2/searchTest.js @@ -1,6 +1,6 @@ const chai = require('chai'); const assert = chai.assert; -const config = require("../../config.js"); +var config = require('config'); const API = require('../../api2.js'); const Helpers = require('../../helpers.js'); const { API2Setup, API2WrapUp } = require("../shared.js"); diff --git a/tests/remote_js/test/2/settingsTest.js b/tests/remote_js/test/2/settingsTest.js index 05ec3dc2..4af09d71 100644 --- a/tests/remote_js/test/2/settingsTest.js +++ b/tests/remote_js/test/2/settingsTest.js @@ -1,6 +1,6 @@ const chai = require('chai'); const assert = chai.assert; -const config = require("../../config.js"); +var config = require('config'); const API = require('../../api2.js'); const Helpers = require('../../helpers.js'); const { API2Setup, API2WrapUp, resetGroups } = require("../shared.js"); diff --git a/tests/remote_js/test/2/sortTest.js b/tests/remote_js/test/2/sortTest.js index 82e80cca..7a6c1438 100644 --- a/tests/remote_js/test/2/sortTest.js +++ b/tests/remote_js/test/2/sortTest.js @@ -1,6 +1,6 @@ const chai = require('chai'); const assert = chai.assert; -const config = require("../../config.js"); +var config = require('config'); const API = require('../../api2.js'); const Helpers = require('../../helpers.js'); const { API2Setup, API2WrapUp } = require("../shared.js"); diff --git a/tests/remote_js/test/2/storageAdmin.js b/tests/remote_js/test/2/storageAdmin.js index b85ecaf9..fb96cfc8 100644 --- a/tests/remote_js/test/2/storageAdmin.js +++ b/tests/remote_js/test/2/storageAdmin.js @@ -1,6 +1,6 @@ const chai = require('chai'); const assert = chai.assert; -const config = require("../../config.js"); +var config = require('config'); const API = require('../../api2.js'); const Helpers = require('../../helpers.js'); const { API2Setup, API2WrapUp } = require("../shared.js"); diff --git a/tests/remote_js/test/2/tagTest.js b/tests/remote_js/test/2/tagTest.js index d625ceff..f1435207 100644 --- a/tests/remote_js/test/2/tagTest.js +++ b/tests/remote_js/test/2/tagTest.js @@ -1,6 +1,6 @@ const chai = require('chai'); const assert = chai.assert; -const config = require("../../config.js"); +var config = require('config'); const API = require('../../api2.js'); const Helpers = require('../../helpers.js'); const { API2Setup, API2WrapUp } = require("../shared.js"); diff --git a/tests/remote_js/test/2/versionTest.js b/tests/remote_js/test/2/versionTest.js index 8f6eacae..2508a9e0 100644 --- a/tests/remote_js/test/2/versionTest.js +++ b/tests/remote_js/test/2/versionTest.js @@ -1,6 +1,6 @@ const chai = require('chai'); const assert = chai.assert; -const config = require("../../config.js"); +var config = require('config'); const API = require('../../api2.js'); const Helpers = require('../../helpers.js'); const { API2Setup, API2WrapUp } = require("../shared.js"); diff --git a/tests/remote_js/test/3/annotationsTest.js b/tests/remote_js/test/3/annotationsTest.js index c59ca3e5..77f52841 100644 --- a/tests/remote_js/test/3/annotationsTest.js +++ b/tests/remote_js/test/3/annotationsTest.js @@ -1,6 +1,6 @@ const chai = require('chai'); const assert = chai.assert; -const config = require("../../config.js"); +var config = require('config'); const API = require('../../api3.js'); const Helpers = require('../../helpers3.js'); const { resetGroups } = require('../../groupsSetup.js'); diff --git a/tests/remote_js/test/3/atomTest.js b/tests/remote_js/test/3/atomTest.js index a05c3b9f..49ce6e44 100644 --- a/tests/remote_js/test/3/atomTest.js +++ b/tests/remote_js/test/3/atomTest.js @@ -1,6 +1,6 @@ const chai = require('chai'); const assert = chai.assert; -const config = require("../../config.js"); +var config = require('config'); const API = require('../../api3.js'); const Helpers = require('../../helpers3.js'); const { JSDOM } = require('jsdom'); diff --git a/tests/remote_js/test/3/bibTest.js b/tests/remote_js/test/3/bibTest.js index de0819f7..00b81a2f 100644 --- a/tests/remote_js/test/3/bibTest.js +++ b/tests/remote_js/test/3/bibTest.js @@ -1,6 +1,6 @@ const chai = require('chai'); const assert = chai.assert; -const config = require("../../config.js"); +var config = require('config'); const API = require('../../api3.js'); const Helpers = require('../../helpers3.js'); const { API3Setup, API3WrapUp } = require("../shared.js"); diff --git a/tests/remote_js/test/3/cacheTest.js b/tests/remote_js/test/3/cacheTest.js index 4aabeb9b..d81ac0e7 100644 --- a/tests/remote_js/test/3/cacheTest.js +++ b/tests/remote_js/test/3/cacheTest.js @@ -1,6 +1,6 @@ const chai = require('chai'); const assert = chai.assert; -const config = require("../../config.js"); +var config = require('config'); const API = require('../../api3.js'); const { API3Setup, API3WrapUp } = require("../shared.js"); diff --git a/tests/remote_js/test/3/collectionTest.js b/tests/remote_js/test/3/collectionTest.js index ea7fa05f..2405806f 100644 --- a/tests/remote_js/test/3/collectionTest.js +++ b/tests/remote_js/test/3/collectionTest.js @@ -1,6 +1,6 @@ const chai = require('chai'); const assert = chai.assert; -const config = require("../../config.js"); +var config = require('config'); const API = require('../../api3.js'); const Helpers = require('../../helpers3.js'); const { API3Setup, API3WrapUp } = require("../shared.js"); @@ -120,7 +120,7 @@ describe('CollectionTests', function () { let childItemKey2 = await API.createAttachmentItem("linked_url", {}, itemKey2, this, 'key'); let response = await API.userGet( - API.config.userID, + config.userID, `collections/${collectionKey}/items?format=keys` ); Helpers.assert200(response); @@ -132,7 +132,7 @@ describe('CollectionTests', function () { assert.include(keys, childItemKey2); response = await API.userGet( - API.config.userID, + config.userID, `collections/${collectionKey}/items/top?format=keys` ); Helpers.assert200(response); diff --git a/tests/remote_js/test/3/creatorTest.js b/tests/remote_js/test/3/creatorTest.js index ccae05ac..25dbd251 100644 --- a/tests/remote_js/test/3/creatorTest.js +++ b/tests/remote_js/test/3/creatorTest.js @@ -1,6 +1,6 @@ const chai = require('chai'); const assert = chai.assert; -const config = require("../../config.js"); +var config = require('config'); const API = require('../../api3.js'); const Helpers = require('../../helpers3.js'); const { API3Setup, API3WrapUp } = require("../shared.js"); diff --git a/tests/remote_js/test/3/exportTest.js b/tests/remote_js/test/3/exportTest.js index f4963228..db9c2409 100644 --- a/tests/remote_js/test/3/exportTest.js +++ b/tests/remote_js/test/3/exportTest.js @@ -1,6 +1,6 @@ const chai = require('chai'); const assert = chai.assert; -const config = require("../../config.js"); +var config = require('config'); const API = require('../../api3.js'); const Helpers = require('../../helpers3.js'); const { API3Setup, API3WrapUp } = require("../shared.js"); diff --git a/tests/remote_js/test/3/fileTest.js b/tests/remote_js/test/3/fileTest.js index 5767fa87..ce7e8498 100644 --- a/tests/remote_js/test/3/fileTest.js +++ b/tests/remote_js/test/3/fileTest.js @@ -1,6 +1,6 @@ const chai = require('chai'); const assert = chai.assert; -const config = require("../../config.js"); +var config = require('config'); const API = require('../../api3.js'); const Helpers = require('../../helpers3.js'); const { API3Setup, API3WrapUp } = require("../shared.js"); diff --git a/tests/remote_js/test/3/fullTextTest.js b/tests/remote_js/test/3/fullTextTest.js index 7da5c75d..612f3531 100644 --- a/tests/remote_js/test/3/fullTextTest.js +++ b/tests/remote_js/test/3/fullTextTest.js @@ -1,6 +1,6 @@ const chai = require('chai'); const assert = chai.assert; -const config = require("../../config.js"); +var config = require('config'); const API = require('../../api3.js'); const Helpers = require('../../helpers3.js'); const { API3Setup, API3WrapUp } = require("../shared.js"); diff --git a/tests/remote_js/test/3/generalTest.js b/tests/remote_js/test/3/generalTest.js index 3ff19c2a..b741789f 100644 --- a/tests/remote_js/test/3/generalTest.js +++ b/tests/remote_js/test/3/generalTest.js @@ -1,6 +1,6 @@ const chai = require('chai'); const assert = chai.assert; -const config = require("../../config.js"); +var config = require('config'); const API = require('../../api3.js'); const Helpers = require('../../helpers3.js'); const { API3Setup, API3WrapUp } = require("../shared.js"); diff --git a/tests/remote_js/test/3/groupTest.js b/tests/remote_js/test/3/groupTest.js index 73e9cf72..475a9076 100644 --- a/tests/remote_js/test/3/groupTest.js +++ b/tests/remote_js/test/3/groupTest.js @@ -1,6 +1,6 @@ const chai = require('chai'); const assert = chai.assert; -const config = require("../../config.js"); +var config = require('config'); const API = require('../../api3.js'); const Helpers = require('../../helpers3.js'); const { JSDOM } = require('jsdom'); diff --git a/tests/remote_js/test/3/itemTest.js b/tests/remote_js/test/3/itemTest.js index da065fec..71ca6c89 100644 --- a/tests/remote_js/test/3/itemTest.js +++ b/tests/remote_js/test/3/itemTest.js @@ -1,6 +1,6 @@ const chai = require('chai'); const assert = chai.assert; -const config = require("../../config.js"); +var config = require('config'); const API = require('../../api3.js'); const Helpers = require('../../helpers3.js'); const { API3Setup, API3WrapUp, resetGroups } = require("../shared.js"); diff --git a/tests/remote_js/test/3/keysTest.js b/tests/remote_js/test/3/keysTest.js index 84198d89..25c319f8 100644 --- a/tests/remote_js/test/3/keysTest.js +++ b/tests/remote_js/test/3/keysTest.js @@ -1,6 +1,6 @@ const chai = require('chai'); const assert = chai.assert; -const config = require("../../config.js"); +var config = require('config'); const API = require('../../api3.js'); const Helpers = require('../../helpers3.js'); const { API3Setup, API3WrapUp } = require("../shared.js"); diff --git a/tests/remote_js/test/3/mappingsTest.js b/tests/remote_js/test/3/mappingsTest.js index 4c1199bf..4a80b576 100644 --- a/tests/remote_js/test/3/mappingsTest.js +++ b/tests/remote_js/test/3/mappingsTest.js @@ -1,6 +1,6 @@ const chai = require('chai'); const assert = chai.assert; -const config = require("../../config.js"); +var config = require('config'); const API = require('../../api3.js'); const Helpers = require('../../helpers3.js'); const { API3Setup, API3WrapUp } = require("../shared.js"); diff --git a/tests/remote_js/test/3/noteTest.js b/tests/remote_js/test/3/noteTest.js index daba41b6..e2d95c5f 100644 --- a/tests/remote_js/test/3/noteTest.js +++ b/tests/remote_js/test/3/noteTest.js @@ -1,6 +1,6 @@ const chai = require('chai'); const assert = chai.assert; -const config = require("../../config.js"); +var config = require('config'); const API = require('../../api3.js'); const Helpers = require('../../helpers3.js'); const { API3Setup, API3WrapUp } = require("../shared.js"); diff --git a/tests/remote_js/test/3/notificationTest.js b/tests/remote_js/test/3/notificationTest.js index 692343f7..d6a48172 100644 --- a/tests/remote_js/test/3/notificationTest.js +++ b/tests/remote_js/test/3/notificationTest.js @@ -1,6 +1,6 @@ const chai = require('chai'); const assert = chai.assert; -const config = require("../../config.js"); +var config = require('config'); const API = require('../../api3.js'); const Helpers = require('../../helpers3.js'); const { API3Setup, API3WrapUp, resetGroups } = require("../shared.js"); diff --git a/tests/remote_js/test/3/objectTest.js b/tests/remote_js/test/3/objectTest.js index e10c1a31..853fa76c 100644 --- a/tests/remote_js/test/3/objectTest.js +++ b/tests/remote_js/test/3/objectTest.js @@ -1,6 +1,6 @@ const chai = require('chai'); const assert = chai.assert; -const config = require("../../config.js"); +var config = require('config'); const API = require('../../api3.js'); const Helpers = require('../../helpers3.js'); const { API3Setup, API3WrapUp } = require("../shared.js"); diff --git a/tests/remote_js/test/3/paramsTest.js b/tests/remote_js/test/3/paramsTest.js index dc6cf197..cb449d64 100644 --- a/tests/remote_js/test/3/paramsTest.js +++ b/tests/remote_js/test/3/paramsTest.js @@ -1,6 +1,6 @@ const chai = require('chai'); const assert = chai.assert; -const config = require("../../config.js"); +var config = require('config'); const API = require('../../api3.js'); const Helpers = require('../../helpers3.js'); const { API3Setup, API3WrapUp } = require("../shared.js"); diff --git a/tests/remote_js/test/3/permissionTest.js b/tests/remote_js/test/3/permissionTest.js index c98b2d01..67a65ee3 100644 --- a/tests/remote_js/test/3/permissionTest.js +++ b/tests/remote_js/test/3/permissionTest.js @@ -1,6 +1,6 @@ const chai = require('chai'); const assert = chai.assert; -const config = require("../../config.js"); +var config = require('config'); const API = require('../../api3.js'); const Helpers = require('../../helpers3.js'); const { API3Setup, API3WrapUp, resetGroups } = require("../shared.js"); diff --git a/tests/remote_js/test/3/publicationTest.js b/tests/remote_js/test/3/publicationTest.js index bcaa0696..db84ef95 100644 --- a/tests/remote_js/test/3/publicationTest.js +++ b/tests/remote_js/test/3/publicationTest.js @@ -1,6 +1,6 @@ const chai = require('chai'); const assert = chai.assert; -const config = require("../../config.js"); +var config = require('config'); const API = require('../../api3.js'); const Helpers = require('../../helpers3.js'); const { API3Setup, API3WrapUp, resetGroups } = require("../shared.js"); diff --git a/tests/remote_js/test/3/relationTest.js b/tests/remote_js/test/3/relationTest.js index 4b14c91e..555b7e12 100644 --- a/tests/remote_js/test/3/relationTest.js +++ b/tests/remote_js/test/3/relationTest.js @@ -1,6 +1,6 @@ const chai = require('chai'); const assert = chai.assert; -const config = require("../../config.js"); +var config = require('config'); const API = require('../../api3.js'); const Helpers = require('../../helpers3.js'); const { API3Setup, API3WrapUp } = require("../shared.js"); diff --git a/tests/remote_js/test/3/searchTest.js b/tests/remote_js/test/3/searchTest.js index b94181a6..722669c3 100644 --- a/tests/remote_js/test/3/searchTest.js +++ b/tests/remote_js/test/3/searchTest.js @@ -1,6 +1,6 @@ const chai = require('chai'); const assert = chai.assert; -const config = require("../../config.js"); +var config = require('config'); const API = require('../../api3.js'); const Helpers = require('../../helpers3.js'); const { API3Setup, API3WrapUp } = require("../shared.js"); diff --git a/tests/remote_js/test/3/settingsTest.js b/tests/remote_js/test/3/settingsTest.js index 547af276..9aa72dcd 100644 --- a/tests/remote_js/test/3/settingsTest.js +++ b/tests/remote_js/test/3/settingsTest.js @@ -1,6 +1,6 @@ const chai = require('chai'); const assert = chai.assert; -const config = require("../../config.js"); +var config = require('config'); const API = require('../../api3.js'); const Helpers = require('../../helpers3.js'); const { API3Setup, API3WrapUp, resetGroups } = require("../shared.js"); diff --git a/tests/remote_js/test/3/sortTest.js b/tests/remote_js/test/3/sortTest.js index 30c4f517..4eac9612 100644 --- a/tests/remote_js/test/3/sortTest.js +++ b/tests/remote_js/test/3/sortTest.js @@ -1,6 +1,6 @@ const chai = require('chai'); const assert = chai.assert; -const config = require("../../config.js"); +var config = require('config'); const API = require('../../api3.js'); const Helpers = require('../../helpers3.js'); const { API3Setup, API3WrapUp } = require("../shared.js"); diff --git a/tests/remote_js/test/3/storageAdmin.js b/tests/remote_js/test/3/storageAdmin.js index 1a9b11da..4acd4f93 100644 --- a/tests/remote_js/test/3/storageAdmin.js +++ b/tests/remote_js/test/3/storageAdmin.js @@ -1,6 +1,6 @@ const chai = require('chai'); const assert = chai.assert; -const config = require("../../config.js"); +var config = require('config'); const API = require('../../api3.js'); const Helpers = require('../../helpers3.js'); const { API3Setup, API3WrapUp } = require("../shared.js"); diff --git a/tests/remote_js/test/3/tagTest.js b/tests/remote_js/test/3/tagTest.js index 05e29aa4..cd611e29 100644 --- a/tests/remote_js/test/3/tagTest.js +++ b/tests/remote_js/test/3/tagTest.js @@ -1,6 +1,6 @@ const chai = require('chai'); const assert = chai.assert; -const config = require("../../config.js"); +var config = require('config'); const API = require('../../api3.js'); const Helpers = require('../../helpers3.js'); const { API3Setup, API3WrapUp } = require("../shared.js"); diff --git a/tests/remote_js/test/3/template.js b/tests/remote_js/test/3/template.js index cca3f068..3e7f92aa 100644 --- a/tests/remote_js/test/3/template.js +++ b/tests/remote_js/test/3/template.js @@ -1,6 +1,6 @@ const chai = require('chai'); const assert = chai.assert; -const config = require("../../config.js"); +var config = require('config'); const API = require('../../api3.js'); const Helpers = require('../../helpers3.js'); const { API3Setup, API3WrapUp } = require("../shared.js"); diff --git a/tests/remote_js/test/3/translationTest.js b/tests/remote_js/test/3/translationTest.js index c163975d..3095777b 100644 --- a/tests/remote_js/test/3/translationTest.js +++ b/tests/remote_js/test/3/translationTest.js @@ -1,6 +1,6 @@ const chai = require('chai'); const assert = chai.assert; -const config = require("../../config.js"); +var config = require('config'); const API = require('../../api3.js'); const Helpers = require('../../helpers3.js'); const { API3Setup, API3WrapUp } = require("../shared.js"); diff --git a/tests/remote_js/test/3/versionTest.js b/tests/remote_js/test/3/versionTest.js index fbe1abb0..a82418a4 100644 --- a/tests/remote_js/test/3/versionTest.js +++ b/tests/remote_js/test/3/versionTest.js @@ -1,6 +1,6 @@ const chai = require('chai'); const assert = chai.assert; -const config = require("../../config.js"); +var config = require('config'); const API = require('../../api3.js'); const Helpers = require('../../helpers3.js'); const { API3Setup, API3WrapUp } = require("../shared.js"); diff --git a/tests/remote_js/test/shared.js b/tests/remote_js/test/shared.js index 96bee79e..d86b864a 100644 --- a/tests/remote_js/test/shared.js +++ b/tests/remote_js/test/shared.js @@ -1,4 +1,4 @@ -const config = require("../config.js"); +var config = require('config'); const API = require('../api2.js'); const API3 = require('../api3.js'); const { resetGroups } = require("../groupsSetup.js"); From 1ee44b1b13acf915116b416828e49c05b5a69493 Mon Sep 17 00:00:00 2001 From: Bogdan Abaev Date: Tue, 30 May 2023 16:53:21 -0400 Subject: [PATCH 17/33] removed inheritance from between api files and helpers --- tests/remote_js/api2.js | 5 - tests/remote_js/api3.js | 176 ++++++++++++++-- tests/remote_js/{helpers.js => helpers2.js} | 10 +- tests/remote_js/helpers3.js | 214 +++++++++++++++++++- tests/remote_js/test/1/collectionTest.js | 2 +- tests/remote_js/test/1/itemsTest.js | 2 +- tests/remote_js/test/2/atomTest.js | 2 +- tests/remote_js/test/2/bibTest.js | 2 +- tests/remote_js/test/2/collectionTest.js | 2 +- tests/remote_js/test/2/creatorTest.js | 2 +- tests/remote_js/test/2/fileTest.js | 2 +- tests/remote_js/test/2/fullText.js | 2 +- tests/remote_js/test/2/generalTest.js | 2 +- tests/remote_js/test/2/groupTest.js | 2 +- tests/remote_js/test/2/itemsTest.js | 2 +- tests/remote_js/test/2/mappingsTest.js | 2 +- tests/remote_js/test/2/noteTest.js | 2 +- tests/remote_js/test/2/objectTest.js | 2 +- tests/remote_js/test/2/paramTest.js | 2 +- tests/remote_js/test/2/permissionsTest.js | 2 +- tests/remote_js/test/2/relationsTest.js | 2 +- tests/remote_js/test/2/searchTest.js | 2 +- tests/remote_js/test/2/settingsTest.js | 2 +- tests/remote_js/test/2/sortTest.js | 2 +- tests/remote_js/test/2/storageAdmin.js | 2 +- tests/remote_js/test/2/tagTest.js | 2 +- tests/remote_js/test/2/versionTest.js | 2 +- tests/remote_js/test/3/atomTest.js | 3 +- tests/remote_js/test/shared.js | 6 +- 29 files changed, 395 insertions(+), 65 deletions(-) rename tests/remote_js/{helpers.js => helpers2.js} (98%) diff --git a/tests/remote_js/api2.js b/tests/remote_js/api2.js index 45a60056..92296d63 100644 --- a/tests/remote_js/api2.js +++ b/tests/remote_js/api2.js @@ -381,11 +381,6 @@ class API2 { return this.getXMLFromResponse(response); } - static async getXMLFromFirstSuccessItem(response) { - const key = this.getFirstSuccessKeyFromResponse(response); - return this.getItemXML(key); - } - static async getCollectionXML(keys, context = null) { return this.getObjectXML('collection', keys, context); } diff --git a/tests/remote_js/api3.js b/tests/remote_js/api3.js index a9e8ad0f..dd6396d1 100644 --- a/tests/remote_js/api3.js +++ b/tests/remote_js/api3.js @@ -1,11 +1,11 @@ const HTTP = require("./httpHandler"); const { JSDOM } = require("jsdom"); -const API2 = require("./api2.js"); -const Helpers = require("./helpers"); +const Helpers = require("./helpers3"); const fs = require("fs"); var config = require('config'); +const wgxpath = require('wgxpath'); -class API3 extends API2 { +class API3 { static schemaVersion; static apiVersion = 3; @@ -16,6 +16,41 @@ class API3 extends API2 { this.apiKey = key; } + static useAPIVersion(version) { + this.apiVersion = version; + } + + static async login() { + const response = await HTTP.post( + `${config.apiURLPrefix}test/setup?u=${config.userID}&u2=${config.userID2}`, + " ", + {}, { + username: config.rootUsername, + password: config.rootPassword + }); + if (!response.data) { + throw new Error("Could not fetch credentials!"); + } + return JSON.parse(response.data); + } + + static async getLibraryVersion() { + const response = await this.userGet( + config.userID, + `items?key=${config.apiKey}&format=keys&limit=1` + ); + return response.headers["last-modified-version"][0]; + } + + static async getGroupLibraryVersion(groupID) { + const response = await this.groupGet( + groupID, + `items?key=${config.apiKey}&format=keys&limit=1` + ); + return response.headers["last-modified-version"][0]; + } + + static async get(url, headers = {}, auth = false) { url = config.apiURLPrefix + url; if (this.apiVersion) { @@ -38,6 +73,11 @@ class API3 extends API2 { return this.get(`users/${userID}/${suffix}`, headers, auth); } + static async userPost(userID, suffix, data, headers = {}, auth = null) { + return this.post(`users/${userID}/${suffix}`, data, headers, auth); + } + + static async head(url, headers = {}, auth = false) { url = config.apiURLPrefix + url; if (this.apiVersion) { @@ -279,6 +319,56 @@ class API3 extends API2 { return this.getObject('item', keys, context, 'atom'); }; + static async groupGetItemXML(groupID, keys, context = null) { + if (typeof keys === 'string' || typeof keys === 'number') { + keys = [keys]; + } + + const response = await this.groupGet( + groupID, + `items?key=${config.apiKey}&itemKey=${keys.join(',')}&order=itemKeyList&content=json` + ); + if (context && response.status != 200) { + throw new Error("Group set request failed."); + } + return this.getXMLFromResponse(response); + } + + static async userClear(userID) { + const response = await this.userPost( + userID, + "clear", + "", + {}, + { + username: config.rootUsername, + password: config.rootPassword + } + ); + if (response.status !== 204) { + console.log(response.data); + throw new Error(`Error clearing user ${userID}`); + } + } + + static async groupClear(groupID) { + const response = await this.groupPost( + groupID, + "clear", + "", + {}, + { + username: config.rootUsername, + password: config.rootPassword + } + ); + + if (response.status !== 204) { + console.log(response.data); + throw new Error(`Error clearing group ${groupID}`); + } + } + static async parseLinkHeader(response) { let header = response.headers.link; let links = {}; @@ -331,11 +421,6 @@ class API3 extends API2 { ); } - static getXMLFromFirstSuccessItem = async (response) => { - let key = await this.getFirstSuccessKeyFromResponse(response); - await this.getItemXML(key); - }; - static async getCollection(keys, context = null, format = false, groupID = false) { const module = this || context; @@ -423,15 +508,6 @@ class API3 extends API2 { return this.handleCreateResponse('item', response, returnFormat, context, groupID); }; - static getFirstSuccessKeyFromResponse(response) { - let json = this.getJSONFromResponse(response); - if (!json.success) { - console.log(response.body); - throw new Error("No success keys found in response"); - } - return json.success[0]; - } - static async groupGet(groupID, suffix, headers = {}, auth = false) { return this.get(`groups/${groupID}/${suffix}`, headers, auth); } @@ -440,6 +516,10 @@ class API3 extends API2 { return this.getObject('collection', keys, context, 'atom'); } + static async postItem(json) { + return this.postItems([json]); + } + static async postItems(json) { return this.postObjects('item', json); } @@ -933,6 +1013,68 @@ class API3 extends API2 { throw new Error(`Unknown content type '${contentType}'`); } } + + //////Response parsing + static getXMLFromResponse(response) { + var result; + try { + const jsdom = new JSDOM(response.data, { contentType: "application/xml", url: "http://localhost/" }); + wgxpath.install(jsdom.window, true); + result = jsdom.window._document; + } + catch (e) { + console.log(response.data); + throw e; + } + return result; + } + + static getJSONFromResponse(response) { + const json = JSON.parse(response.data); + if (json === null) { + console.log(response.data); + throw new Error("JSON response could not be parsed"); + } + return json; + } + + static getFirstSuccessKeyFromResponse(response) { + const json = this.getJSONFromResponse(response); + if (!json.success || json.success.length === 0) { + console.log(response.data); + throw new Error("No success keys found in response"); + } + return json.success[0]; + } + + static parseDataFromAtomEntry(entryXML) { + const key = entryXML.getElementsByTagName('zapi:key')[0]; + const version = entryXML.getElementsByTagName('zapi:version')[0]; + const content = entryXML.getElementsByTagName('content')[0]; + if (content === null) { + console.log(entryXML.outerHTML); + throw new Error("Atom response does not contain "); + } + + return { + key: key ? key.textContent : null, + version: version ? version.textContent : null, + content: content ? content.textContent : null + }; + } + + static getContentFromResponse(response) { + const xml = this.getXMLFromResponse(response); + const data = this.parseDataFromAtomEntry(xml); + return data.content; + } + + static getPluralObjectType(objectType) { + if (objectType === 'search') { + return objectType + "es"; + } + return objectType + "s"; + } } module.exports = API3; diff --git a/tests/remote_js/helpers.js b/tests/remote_js/helpers2.js similarity index 98% rename from tests/remote_js/helpers.js rename to tests/remote_js/helpers2.js index 82c670cb..fc960684 100644 --- a/tests/remote_js/helpers.js +++ b/tests/remote_js/helpers2.js @@ -3,13 +3,7 @@ const chai = require('chai'); const assert = chai.assert; const crypto = require('crypto'); -class Helpers { - static isV3 = false; - - static useV3 = () => { - this.isV3 = true; - }; - +class Helpers2 { static uniqueToken = () => { const id = crypto.randomBytes(16).toString("hex"); const hash = crypto.createHash('md5').update(id).digest('hex'); @@ -223,4 +217,4 @@ class Helpers { }; } -module.exports = Helpers; +module.exports = Helpers2; diff --git a/tests/remote_js/helpers3.js b/tests/remote_js/helpers3.js index 58e750f9..44f9500f 100644 --- a/tests/remote_js/helpers3.js +++ b/tests/remote_js/helpers3.js @@ -1,19 +1,114 @@ const { JSDOM } = require("jsdom"); const chai = require('chai'); const assert = chai.assert; -const Helpers = require('./helpers'); const crypto = require('crypto'); const fs = require('fs'); -class Helpers3 extends Helpers { +class Helpers3 { static notificationHeader = 'zotero-debug-notifications'; - static assertTotalResults(response, expectedCount) { - const totalResults = parseInt(response.headers['total-results'][0]); - assert.isNumber(totalResults); - assert.equal(totalResults, expectedCount); + + static uniqueToken = () => { + const id = crypto.randomBytes(16).toString("hex"); + const hash = crypto.createHash('md5').update(id).digest('hex'); + return hash; + }; + + static uniqueID = (count = 8) => { + const chars = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Z']; + let result = ""; + for (let i = 0; i < count; i++) { + result += chars[crypto.randomInt(chars.length)]; + } + return result; + }; + + static namespaceResolver = (prefix) => { + let ns = { + atom: 'http://www.w3.org/2005/Atom', + zapi: 'http://zotero.org/ns/api', + zxfer: 'http://zotero.org/ns/transfer', + html: 'http://www.w3.org/1999/xhtml' + }; + return ns[prefix] || null; + }; + + static xpathEval = (document, xpath, returnHtml = false, multiple = false, element = null) => { + const xpathData = document.evaluate(xpath, (element || document), this.namespaceResolver, 5, null); + if (!multiple && xpathData.snapshotLength != 1) { + throw new Error("No single xpath value fetched"); + } + var node; + var result = []; + do { + node = xpathData.iterateNext(); + if (node) { + result.push(node); + } + } while (node); + + if (returnHtml) { + return multiple ? result : result[0]; + } + + return multiple ? result.map(el => el.innerHTML) : result[0].innerHTML; + }; + + static assertRegExp(exp, val) { + if (typeof exp == "string") { + exp = new RegExp(exp); + } + if (!exp.test(val)) { + throw new Error(`${val} does not match regular expression`); + } } + static assertXMLEqual = (one, two) => { + const contentDom = new JSDOM(one); + const expectedDom = new JSDOM(two); + assert.equal(contentDom.window.document.innerHTML, expectedDom.window.document.innerHTML); + }; + + static assertStatusCode = (response, expectedCode, message) => { + try { + assert.equal(response.status, expectedCode); + if (message) { + assert.equal(response.data, message); + } + } + catch (e) { + console.log(response.data); + throw e; + } + }; + + static assertStatusForObject = (response, status, recordId, httpCode, message) => { + let body = response; + if (response.data) { + body = response.data; + } + try { + body = JSON.parse(body); + } + catch (e) { } + assert.include(['unchanged', 'failed', 'success'], status); + + try { + //Make sure the recordId is in the right category - unchanged, failed, success + assert.property(body[status], recordId); + if (httpCode) { + assert.equal(body[status][recordId].code, httpCode); + } + if (message) { + assert.equal(body[status][recordId].message, message); + } + } + catch (e) { + console.log(response.data); + throw e; + } + }; + static assertNumResults = (response, expectedResults) => { const contentType = response.headers['content-type'][0]; if (contentType == 'application/json') { @@ -42,6 +137,111 @@ class Helpers3 extends Helpers { } }; + static assertTotalResults(response, expectedCount) { + const totalResults = parseInt(response.headers['total-results'][0]); + assert.isNumber(totalResults); + assert.equal(totalResults, expectedCount); + } + + static assertContentType = (response, contentType) => { + assert.include(response?.headers['content-type'], contentType.toLowerCase()); + }; + + + //Assert codes + static assert200 = (response) => { + this.assertStatusCode(response, 200); + }; + + static assert201 = (response) => { + this.assertStatusCode(response, 201); + }; + + static assert204 = (response) => { + this.assertStatusCode(response, 204); + }; + + static assert300 = (response) => { + this.assertStatusCode(response, 300); + }; + + static assert302 = (response) => { + this.assertStatusCode(response, 302); + }; + + static assert400 = (response, message) => { + this.assertStatusCode(response, 400, message); + }; + + static assert401 = (response) => { + this.assertStatusCode(response, 401); + }; + + static assert403 = (response) => { + this.assertStatusCode(response, 403); + }; + + static assert412 = (response) => { + this.assertStatusCode(response, 412); + }; + + static assert428 = (response) => { + this.assertStatusCode(response, 428); + }; + + static assert404 = (response) => { + this.assertStatusCode(response, 404); + }; + + static assert405 = (response) => { + this.assertStatusCode(response, 405); + }; + + static assert500 = (response) => { + this.assertStatusCode(response, 500); + }; + + static assert400ForObject = (response, { index = 0, message = null } = {}) => { + this.assertStatusForObject(response, 'failed', index, 400, message); + }; + + static assert200ForObject = (response, { index = 0, message = null } = {}) => { + this.assertStatusForObject(response, 'success', index, message); + }; + + static assert404ForObject = (response, { index = 0, message = null } = {}) => { + this.assertStatusForObject(response, 'failed', index, 404, message); + }; + + static assert409ForObject = (response, { index = 0, message = null } = {}) => { + this.assertStatusForObject(response, 'failed', index, 409, message); + }; + + static assert412ForObject = (response, { index = 0, message = null } = {}) => { + this.assertStatusForObject(response, 'failed', index, 412, message); + }; + + static assert413ForObject = (response, { index = 0, message = null } = {}) => { + this.assertStatusForObject(response, 'failed', index, 413, message); + }; + + static assert428ForObject = (response, { index = 0, message = null } = {}) => { + this.assertStatusForObject(response, 'failed', index, 428, message); + }; + + static assertUnchangedForObject = (response, { index = 0, message = null } = {}) => { + this.assertStatusForObject(response, 'unchanged', index, null, message); + }; + + // Methods to help during conversion + static assertEquals = (one, two) => { + assert.equal(two, one); + }; + + static assertCount = (count, object) => { + assert.lengthOf(Object.keys(object), count); + }; + static assertNoResults(response) { this.assertTotalResults(response, 0); @@ -71,7 +271,7 @@ class Helpers3 extends Helpers { static getRandomUnicodeString = function () { const rand = crypto.randomInt(10, 100); - return "Âéìøü 这是一个测试。 " + Helpers.uniqueID(rand); + return "Âéìøü 这是一个测试。 " + this.uniqueID(rand); }; static implodeParams = (params, exclude = []) => { diff --git a/tests/remote_js/test/1/collectionTest.js b/tests/remote_js/test/1/collectionTest.js index a842a454..585a5692 100644 --- a/tests/remote_js/test/1/collectionTest.js +++ b/tests/remote_js/test/1/collectionTest.js @@ -2,7 +2,7 @@ const chai = require('chai'); const assert = chai.assert; var config = require('config'); const API = require('../../api2.js'); -const Helpers = require('../../helpers.js'); +const Helpers = require('../../helpers2.js'); const { API1Setup, API1WrapUp } = require("../shared.js"); describe('CollectionTests', function () { diff --git a/tests/remote_js/test/1/itemsTest.js b/tests/remote_js/test/1/itemsTest.js index 7259d2c1..3fe9221d 100644 --- a/tests/remote_js/test/1/itemsTest.js +++ b/tests/remote_js/test/1/itemsTest.js @@ -1,7 +1,7 @@ const { assert } = require('chai'); const API = require('../../api2.js'); var config = require('config'); -const Helpers = require('../../helpers.js'); +const Helpers = require('../../helpers2.js'); const { API1Setup, API1WrapUp } = require("../shared.js"); describe('ItemTests', function () { diff --git a/tests/remote_js/test/2/atomTest.js b/tests/remote_js/test/2/atomTest.js index 77bdf6d2..461da839 100644 --- a/tests/remote_js/test/2/atomTest.js +++ b/tests/remote_js/test/2/atomTest.js @@ -2,7 +2,7 @@ const chai = require('chai'); const assert = chai.assert; var config = require('config'); const API = require('../../api2.js'); -const Helpers = require('../../helpers.js'); +const Helpers = require('../../helpers2.js'); const { JSDOM } = require('jsdom'); const { API2Setup, API2WrapUp } = require("../shared.js"); diff --git a/tests/remote_js/test/2/bibTest.js b/tests/remote_js/test/2/bibTest.js index 29784553..a8fa3be8 100644 --- a/tests/remote_js/test/2/bibTest.js +++ b/tests/remote_js/test/2/bibTest.js @@ -2,7 +2,7 @@ const chai = require('chai'); const assert = chai.assert; var config = require('config'); const API = require('../../api2.js'); -const Helpers = require('../../helpers.js'); +const Helpers = require('../../helpers2.js'); const { API2Setup, API2WrapUp } = require("../shared.js"); describe('BibTests', function () { diff --git a/tests/remote_js/test/2/collectionTest.js b/tests/remote_js/test/2/collectionTest.js index 05eb0ab7..3f225d55 100644 --- a/tests/remote_js/test/2/collectionTest.js +++ b/tests/remote_js/test/2/collectionTest.js @@ -2,7 +2,7 @@ const chai = require('chai'); const assert = chai.assert; var config = require('config'); const API = require('../../api2.js'); -const Helpers = require('../../helpers.js'); +const Helpers = require('../../helpers2.js'); const { API2Setup, API2WrapUp } = require("../shared.js"); describe('CollectionTests', function () { diff --git a/tests/remote_js/test/2/creatorTest.js b/tests/remote_js/test/2/creatorTest.js index e0ba78f0..46a176eb 100644 --- a/tests/remote_js/test/2/creatorTest.js +++ b/tests/remote_js/test/2/creatorTest.js @@ -2,7 +2,7 @@ const chai = require('chai'); const assert = chai.assert; var config = require('config'); const API = require('../../api2.js'); -const Helpers = require('../../helpers.js'); +const Helpers = require('../../helpers2.js'); const { API2Setup, API2WrapUp } = require("../shared.js"); describe('CreatorTests', function () { diff --git a/tests/remote_js/test/2/fileTest.js b/tests/remote_js/test/2/fileTest.js index 9cc55bf2..46fa9227 100644 --- a/tests/remote_js/test/2/fileTest.js +++ b/tests/remote_js/test/2/fileTest.js @@ -2,7 +2,7 @@ const chai = require('chai'); const assert = chai.assert; var config = require('config'); const API = require('../../api2.js'); -const Helpers = require('../../helpers.js'); +const Helpers = require('../../helpers2.js'); const { API2Setup, API2WrapUp } = require("../shared.js"); const { S3Client, DeleteObjectsCommand, PutObjectCommand } = require("@aws-sdk/client-s3"); const fs = require('fs'); diff --git a/tests/remote_js/test/2/fullText.js b/tests/remote_js/test/2/fullText.js index 6409d390..49cc5843 100644 --- a/tests/remote_js/test/2/fullText.js +++ b/tests/remote_js/test/2/fullText.js @@ -2,7 +2,7 @@ const chai = require('chai'); const assert = chai.assert; var config = require('config'); const API = require('../../api2.js'); -const Helpers = require('../../helpers.js'); +const Helpers = require('../../helpers2.js'); const { API2Setup, API2WrapUp } = require("../shared.js"); describe('FullTextTests', function () { diff --git a/tests/remote_js/test/2/generalTest.js b/tests/remote_js/test/2/generalTest.js index f91f31c4..0ce8b373 100644 --- a/tests/remote_js/test/2/generalTest.js +++ b/tests/remote_js/test/2/generalTest.js @@ -2,7 +2,7 @@ const chai = require('chai'); const assert = chai.assert; var config = require('config'); const API = require('../../api2.js'); -const Helpers = require('../../helpers.js'); +const Helpers = require('../../helpers2.js'); const { API2Setup, API2WrapUp } = require("../shared.js"); diff --git a/tests/remote_js/test/2/groupTest.js b/tests/remote_js/test/2/groupTest.js index 7af2f58c..766bf8e7 100644 --- a/tests/remote_js/test/2/groupTest.js +++ b/tests/remote_js/test/2/groupTest.js @@ -2,7 +2,7 @@ const chai = require('chai'); const assert = chai.assert; var config = require('config'); const API = require('../../api2.js'); -const Helpers = require('../../helpers.js'); +const Helpers = require('../../helpers2.js'); const { API2Setup, API2WrapUp, resetGroups } = require("../shared.js"); const { JSDOM } = require("jsdom"); diff --git a/tests/remote_js/test/2/itemsTest.js b/tests/remote_js/test/2/itemsTest.js index 012ad02a..ded2c119 100644 --- a/tests/remote_js/test/2/itemsTest.js +++ b/tests/remote_js/test/2/itemsTest.js @@ -2,7 +2,7 @@ const chai = require('chai'); const assert = chai.assert; var config = require('config'); const API = require('../../api2.js'); -const Helpers = require('../../helpers.js'); +const Helpers = require('../../helpers2.js'); const { API2Setup, API2WrapUp } = require("../shared.js"); describe('ItemsTests', function () { diff --git a/tests/remote_js/test/2/mappingsTest.js b/tests/remote_js/test/2/mappingsTest.js index 98cc0daa..9ed53f08 100644 --- a/tests/remote_js/test/2/mappingsTest.js +++ b/tests/remote_js/test/2/mappingsTest.js @@ -2,7 +2,7 @@ const chai = require('chai'); const assert = chai.assert; var config = require('config'); const API = require('../../api2.js'); -const Helpers = require('../../helpers.js'); +const Helpers = require('../../helpers2.js'); const { API2Setup, API2WrapUp } = require("../shared.js"); describe('MappingsTests', function () { diff --git a/tests/remote_js/test/2/noteTest.js b/tests/remote_js/test/2/noteTest.js index c1f62b18..5d6b25d3 100644 --- a/tests/remote_js/test/2/noteTest.js +++ b/tests/remote_js/test/2/noteTest.js @@ -2,7 +2,7 @@ const chai = require('chai'); const assert = chai.assert; var config = require('config'); const API = require('../../api2.js'); -const Helpers = require("../../helpers.js"); +const Helpers = require("../../helpers2.js"); const { API2Setup, API2WrapUp } = require("../shared.js"); describe('NoteTests', function () { diff --git a/tests/remote_js/test/2/objectTest.js b/tests/remote_js/test/2/objectTest.js index 500d0d81..4fcaac5b 100644 --- a/tests/remote_js/test/2/objectTest.js +++ b/tests/remote_js/test/2/objectTest.js @@ -2,7 +2,7 @@ const chai = require('chai'); const assert = chai.assert; var config = require('config'); const API = require('../../api2.js'); -const Helpers = require('../../helpers.js'); +const Helpers = require('../../helpers2.js'); const { API2Setup, API2WrapUp } = require("../shared.js"); describe('ObjectTests', function () { diff --git a/tests/remote_js/test/2/paramTest.js b/tests/remote_js/test/2/paramTest.js index 86c8893f..c49a8cf3 100644 --- a/tests/remote_js/test/2/paramTest.js +++ b/tests/remote_js/test/2/paramTest.js @@ -2,7 +2,7 @@ const chai = require('chai'); const assert = chai.assert; var config = require('config'); const API = require('../../api2.js'); -const Helpers = require('../../helpers.js'); +const Helpers = require('../../helpers2.js'); const { API2Setup, API2WrapUp } = require("../shared.js"); describe('ParametersTests', function () { diff --git a/tests/remote_js/test/2/permissionsTest.js b/tests/remote_js/test/2/permissionsTest.js index 3916b101..79ed2cbe 100644 --- a/tests/remote_js/test/2/permissionsTest.js +++ b/tests/remote_js/test/2/permissionsTest.js @@ -2,7 +2,7 @@ const chai = require('chai'); const assert = chai.assert; var config = require('config'); const API = require('../../api2.js'); -const Helpers = require('../../helpers.js'); +const Helpers = require('../../helpers2.js'); const { API2Setup, API2WrapUp, resetGroups } = require("../shared.js"); describe('PermissionsTests', function () { diff --git a/tests/remote_js/test/2/relationsTest.js b/tests/remote_js/test/2/relationsTest.js index 4eca7b94..873d5d62 100644 --- a/tests/remote_js/test/2/relationsTest.js +++ b/tests/remote_js/test/2/relationsTest.js @@ -2,7 +2,7 @@ const chai = require('chai'); const assert = chai.assert; var config = require('config'); const API = require('../../api2.js'); -const Helpers = require('../../helpers.js'); +const Helpers = require('../../helpers2.js'); const { API2Setup, API2WrapUp } = require("../shared.js"); describe('RelationsTests', function () { diff --git a/tests/remote_js/test/2/searchTest.js b/tests/remote_js/test/2/searchTest.js index f0594dfa..f60053cc 100644 --- a/tests/remote_js/test/2/searchTest.js +++ b/tests/remote_js/test/2/searchTest.js @@ -2,7 +2,7 @@ const chai = require('chai'); const assert = chai.assert; var config = require('config'); const API = require('../../api2.js'); -const Helpers = require('../../helpers.js'); +const Helpers = require('../../helpers2.js'); const { API2Setup, API2WrapUp } = require("../shared.js"); describe('SearchTests', function () { diff --git a/tests/remote_js/test/2/settingsTest.js b/tests/remote_js/test/2/settingsTest.js index 4af09d71..040277b7 100644 --- a/tests/remote_js/test/2/settingsTest.js +++ b/tests/remote_js/test/2/settingsTest.js @@ -2,7 +2,7 @@ const chai = require('chai'); const assert = chai.assert; var config = require('config'); const API = require('../../api2.js'); -const Helpers = require('../../helpers.js'); +const Helpers = require('../../helpers2.js'); const { API2Setup, API2WrapUp, resetGroups } = require("../shared.js"); describe('SettingsTests', function () { diff --git a/tests/remote_js/test/2/sortTest.js b/tests/remote_js/test/2/sortTest.js index 7a6c1438..1be5d439 100644 --- a/tests/remote_js/test/2/sortTest.js +++ b/tests/remote_js/test/2/sortTest.js @@ -2,7 +2,7 @@ const chai = require('chai'); const assert = chai.assert; var config = require('config'); const API = require('../../api2.js'); -const Helpers = require('../../helpers.js'); +const Helpers = require('../../helpers2.js'); const { API2Setup, API2WrapUp } = require("../shared.js"); describe('SortTests', function () { diff --git a/tests/remote_js/test/2/storageAdmin.js b/tests/remote_js/test/2/storageAdmin.js index fb96cfc8..1f530650 100644 --- a/tests/remote_js/test/2/storageAdmin.js +++ b/tests/remote_js/test/2/storageAdmin.js @@ -2,7 +2,7 @@ const chai = require('chai'); const assert = chai.assert; var config = require('config'); const API = require('../../api2.js'); -const Helpers = require('../../helpers.js'); +const Helpers = require('../../helpers2.js'); const { API2Setup, API2WrapUp } = require("../shared.js"); describe('StorageAdminTests', function () { diff --git a/tests/remote_js/test/2/tagTest.js b/tests/remote_js/test/2/tagTest.js index f1435207..84586911 100644 --- a/tests/remote_js/test/2/tagTest.js +++ b/tests/remote_js/test/2/tagTest.js @@ -2,7 +2,7 @@ const chai = require('chai'); const assert = chai.assert; var config = require('config'); const API = require('../../api2.js'); -const Helpers = require('../../helpers.js'); +const Helpers = require('../../helpers2.js'); const { API2Setup, API2WrapUp } = require("../shared.js"); describe('TagTests', function () { diff --git a/tests/remote_js/test/2/versionTest.js b/tests/remote_js/test/2/versionTest.js index 2508a9e0..adc733e6 100644 --- a/tests/remote_js/test/2/versionTest.js +++ b/tests/remote_js/test/2/versionTest.js @@ -2,7 +2,7 @@ const chai = require('chai'); const assert = chai.assert; var config = require('config'); const API = require('../../api2.js'); -const Helpers = require('../../helpers.js'); +const Helpers = require('../../helpers2.js'); const { API2Setup, API2WrapUp } = require("../shared.js"); describe('VersionsTests', function () { diff --git a/tests/remote_js/test/3/atomTest.js b/tests/remote_js/test/3/atomTest.js index 49ce6e44..8c241617 100644 --- a/tests/remote_js/test/3/atomTest.js +++ b/tests/remote_js/test/3/atomTest.js @@ -12,7 +12,6 @@ describe('AtomTests', function () { before(async function () { await API3Setup(); - Helpers.useV3(); let key = await API.createItem("book", { title: "Title", creators: [{ @@ -81,7 +80,7 @@ describe('AtomTests', function () { `items?itemKey=${keyStr}&content=bib,json`, ); Helpers.assertStatusCode(response, 200); - const xml = await API.getXMLFromResponse(response); + const xml = API.getXMLFromResponse(response); Helpers.assertTotalResults(response, keys.length); const entries = Helpers.xpathEval(xml, '//atom:entry', true, true); diff --git a/tests/remote_js/test/shared.js b/tests/remote_js/test/shared.js index d86b864a..22686c32 100644 --- a/tests/remote_js/test/shared.js +++ b/tests/remote_js/test/shared.js @@ -34,7 +34,7 @@ module.exports = { }, API3Setup: async () => { - const credentials = await API.login(); + const credentials = await API3.login(); config.apiKey = credentials.user1.apiKey; config.user2APIKey = credentials.user2.apiKey; await API3.useAPIVersion(3); @@ -42,10 +42,10 @@ module.exports = { await API3.resetSchemaVersion(); await API3.setKeyUserPermission(config.apiKey, 'notes', true); await API3.setKeyUserPermission(config.apiKey, 'write', true); - await API.userClear(config.userID); + await API3.userClear(config.userID); }, API3WrapUp: async () => { await API3.useAPIKey(config.apiKey); - await API.userClear(config.userID); + await API3.userClear(config.userID); } }; From 41d5981ce96318aac9b354d13add55162499a5b8 Mon Sep 17 00:00:00 2001 From: Bogdan Abaev Date: Tue, 30 May 2023 17:18:06 -0400 Subject: [PATCH 18/33] package.json --- tests/remote_js/package.json | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/remote_js/package.json b/tests/remote_js/package.json index abb9882f..561ec446 100644 --- a/tests/remote_js/package.json +++ b/tests/remote_js/package.json @@ -1,7 +1,7 @@ { "dependencies": { "@aws-sdk/client-s3": "^3.338.0", - "axios": "^1.4.0", + "config": "^3.3.9", "jsdom": "^22.0.0", "jszip": "^3.10.1", "node-fetch": "^2.6.7", @@ -13,7 +13,6 @@ "mocha": "^10.2.0" }, "scripts": { - "test": "mocha \"test/**/*.js\"", - "test_file": "mocha \"test/3/publicationTest.js\"" + "test": "mocha \"test/**/*.js\"" } } From 4d7632c8c4143fa0413d6e189b89d2567b55465b Mon Sep 17 00:00:00 2001 From: Bogdan Abaev Date: Tue, 30 May 2023 17:48:52 -0400 Subject: [PATCH 19/33] changed 'Setup' for 'Before' and 'Wrapup' for 'After' in shared.js, general test --- tests/remote_js/helpers3.js | 12 ++ tests/remote_js/test/1/collectionTest.js | 6 +- tests/remote_js/test/1/itemsTest.js | 6 +- tests/remote_js/test/2/atomTest.js | 6 +- tests/remote_js/test/2/bibTest.js | 6 +- tests/remote_js/test/2/cacheTest.js | 6 +- tests/remote_js/test/2/collectionTest.js | 6 +- tests/remote_js/test/2/creatorTest.js | 6 +- tests/remote_js/test/2/fileTest.js | 6 +- tests/remote_js/test/2/fullText.js | 6 +- tests/remote_js/test/2/generalTest.js | 6 +- tests/remote_js/test/2/groupTest.js | 6 +- tests/remote_js/test/2/itemsTest.js | 6 +- tests/remote_js/test/2/mappingsTest.js | 6 +- tests/remote_js/test/2/noteTest.js | 6 +- tests/remote_js/test/2/objectTest.js | 6 +- tests/remote_js/test/2/paramTest.js | 6 +- tests/remote_js/test/2/permissionsTest.js | 6 +- tests/remote_js/test/2/relationsTest.js | 6 +- tests/remote_js/test/2/searchTest.js | 6 +- tests/remote_js/test/2/settingsTest.js | 6 +- tests/remote_js/test/2/sortTest.js | 6 +- tests/remote_js/test/2/storageAdmin.js | 6 +- tests/remote_js/test/2/tagTest.js | 6 +- tests/remote_js/test/2/versionTest.js | 6 +- tests/remote_js/test/3/annotationsTest.js | 6 +- tests/remote_js/test/3/atomTest.js | 6 +- tests/remote_js/test/3/bibTest.js | 6 +- tests/remote_js/test/3/cacheTest.js | 6 +- tests/remote_js/test/3/collectionTest.js | 6 +- tests/remote_js/test/3/creatorTest.js | 6 +- tests/remote_js/test/3/exportTest.js | 6 +- tests/remote_js/test/3/fileTest.js | 6 +- tests/remote_js/test/3/fullTextTest.js | 6 +- tests/remote_js/test/3/generalTest.js | 6 +- tests/remote_js/test/3/groupTest.js | 6 +- tests/remote_js/test/3/itemTest.js | 6 +- tests/remote_js/test/3/keysTest.js | 6 +- tests/remote_js/test/3/mappingsTest.js | 6 +- tests/remote_js/test/3/noteTest.js | 6 +- tests/remote_js/test/3/notificationTest.js | 6 +- tests/remote_js/test/3/objectTest.js | 6 +- tests/remote_js/test/3/paramsTest.js | 6 +- tests/remote_js/test/3/permissionTest.js | 6 +- tests/remote_js/test/3/publicationTest.js | 6 +- tests/remote_js/test/3/relationTest.js | 6 +- tests/remote_js/test/3/searchTest.js | 6 +- tests/remote_js/test/3/settingsTest.js | 6 +- tests/remote_js/test/3/sortTest.js | 6 +- tests/remote_js/test/3/storageAdmin.js | 6 +- tests/remote_js/test/3/tagTest.js | 6 +- tests/remote_js/test/3/template.js | 25 --- tests/remote_js/test/3/translationTest.js | 6 +- tests/remote_js/test/3/versionTest.js | 6 +- tests/remote_js/test/general.js | 201 +++++++++++++++++++++ tests/remote_js/test/shared.js | 12 +- 56 files changed, 375 insertions(+), 187 deletions(-) delete mode 100644 tests/remote_js/test/3/template.js create mode 100644 tests/remote_js/test/general.js diff --git a/tests/remote_js/helpers3.js b/tests/remote_js/helpers3.js index 44f9500f..af411759 100644 --- a/tests/remote_js/helpers3.js +++ b/tests/remote_js/helpers3.js @@ -82,6 +82,18 @@ class Helpers3 { } }; + static assertCompression = (response) => { + assert.equal(response.headers['content-encoding'][0], 'gzip'); + }; + + static assertNoCompression = (response) => { + assert.notOk(response.headers['content-encoding']); + }; + + static assertContentLength = (response, length) => { + assert.equal(response.headers['content-length'] || 0, length); + }; + static assertStatusForObject = (response, status, recordId, httpCode, message) => { let body = response; if (response.data) { diff --git a/tests/remote_js/test/1/collectionTest.js b/tests/remote_js/test/1/collectionTest.js index 585a5692..8999cdf5 100644 --- a/tests/remote_js/test/1/collectionTest.js +++ b/tests/remote_js/test/1/collectionTest.js @@ -3,17 +3,17 @@ const assert = chai.assert; var config = require('config'); const API = require('../../api2.js'); const Helpers = require('../../helpers2.js'); -const { API1Setup, API1WrapUp } = require("../shared.js"); +const { API1Before, API1After } = require("../shared.js"); describe('CollectionTests', function () { this.timeout(config.timeout); before(async function () { - await API1Setup(); + await API1Before(); }); after(async function () { - await API1WrapUp(); + await API1After(); }); const testNewSingleCollection = async () => { diff --git a/tests/remote_js/test/1/itemsTest.js b/tests/remote_js/test/1/itemsTest.js index 3fe9221d..0c38019d 100644 --- a/tests/remote_js/test/1/itemsTest.js +++ b/tests/remote_js/test/1/itemsTest.js @@ -2,18 +2,18 @@ const { assert } = require('chai'); const API = require('../../api2.js'); var config = require('config'); const Helpers = require('../../helpers2.js'); -const { API1Setup, API1WrapUp } = require("../shared.js"); +const { API1Before, API1After } = require("../shared.js"); describe('ItemTests', function () { this.timeout(config.timeout); // setting timeout if operations are async and take some time before(async function () { - await API1Setup(); + await API1Before(); await API.setKeyOption(config.userID, config.apiKey, 'libraryNotes', 1); }); after(async function () { - await API1WrapUp(); + await API1After(); }); it('testCreateItemWithChildren', async function () { diff --git a/tests/remote_js/test/2/atomTest.js b/tests/remote_js/test/2/atomTest.js index 461da839..b8ef8e09 100644 --- a/tests/remote_js/test/2/atomTest.js +++ b/tests/remote_js/test/2/atomTest.js @@ -4,13 +4,13 @@ var config = require('config'); const API = require('../../api2.js'); const Helpers = require('../../helpers2.js'); const { JSDOM } = require('jsdom'); -const { API2Setup, API2WrapUp } = require("../shared.js"); +const { API2Before, API2After } = require("../shared.js"); describe('CollectionTests', function () { this.timeout(config.timeout); let keyObj = {}; before(async function () { - await API2Setup(); + await API2Before(); const item1 = { title: 'Title', creators: [ @@ -62,7 +62,7 @@ describe('CollectionTests', function () { }); after(async function () { - await API2WrapUp(); + await API2After(); }); it('testFeedURIs', async function () { diff --git a/tests/remote_js/test/2/bibTest.js b/tests/remote_js/test/2/bibTest.js index a8fa3be8..70dd940c 100644 --- a/tests/remote_js/test/2/bibTest.js +++ b/tests/remote_js/test/2/bibTest.js @@ -3,7 +3,7 @@ const assert = chai.assert; var config = require('config'); const API = require('../../api2.js'); const Helpers = require('../../helpers2.js'); -const { API2Setup, API2WrapUp } = require("../shared.js"); +const { API2Before, API2After } = require("../shared.js"); describe('BibTests', function () { this.timeout(config.timeout); @@ -17,7 +17,7 @@ describe('BibTests', function () { ]; before(async function () { - await API2Setup(); + await API2Before(); // Create test data let key = await API.createItem("book", { @@ -71,7 +71,7 @@ describe('BibTests', function () { }); after(async function () { - await API2WrapUp(); + await API2After(); }); it('testContentCitationMulti', async function () { diff --git a/tests/remote_js/test/2/cacheTest.js b/tests/remote_js/test/2/cacheTest.js index d203640a..81e801cd 100644 --- a/tests/remote_js/test/2/cacheTest.js +++ b/tests/remote_js/test/2/cacheTest.js @@ -2,17 +2,17 @@ const chai = require('chai'); const assert = chai.assert; var config = require('config'); const API = require('../../api2.js'); -const { API2Setup, API2WrapUp } = require("../shared.js"); +const { API2Before, API2After } = require("../shared.js"); describe('CacheTests', function () { this.timeout(config.timeout); before(async function () { - await API2Setup(); + await API2Before(); }); after(async function () { - await API2WrapUp(); + await API2After(); }); it('testCacheCreatorPrimaryData', async function () { diff --git a/tests/remote_js/test/2/collectionTest.js b/tests/remote_js/test/2/collectionTest.js index 3f225d55..bb19c965 100644 --- a/tests/remote_js/test/2/collectionTest.js +++ b/tests/remote_js/test/2/collectionTest.js @@ -3,17 +3,17 @@ const assert = chai.assert; var config = require('config'); const API = require('../../api2.js'); const Helpers = require('../../helpers2.js'); -const { API2Setup, API2WrapUp } = require("../shared.js"); +const { API2Before, API2After } = require("../shared.js"); describe('CollectionTests', function () { this.timeout(config.timeout); before(async function () { - await API2Setup(); + await API2Before(); }); after(async function () { - await API2WrapUp(); + await API2After(); }); it('testNewCollections', async function () { diff --git a/tests/remote_js/test/2/creatorTest.js b/tests/remote_js/test/2/creatorTest.js index 46a176eb..fa7a1642 100644 --- a/tests/remote_js/test/2/creatorTest.js +++ b/tests/remote_js/test/2/creatorTest.js @@ -3,17 +3,17 @@ const assert = chai.assert; var config = require('config'); const API = require('../../api2.js'); const Helpers = require('../../helpers2.js'); -const { API2Setup, API2WrapUp } = require("../shared.js"); +const { API2Before, API2After } = require("../shared.js"); describe('CreatorTests', function () { this.timeout(config.timeout); before(async function () { - await API2Setup(); + await API2Before(); }); after(async function () { - await API2WrapUp(); + await API2After(); }); it('testCreatorSummary', async function () { diff --git a/tests/remote_js/test/2/fileTest.js b/tests/remote_js/test/2/fileTest.js index 46fa9227..7fd0b6ac 100644 --- a/tests/remote_js/test/2/fileTest.js +++ b/tests/remote_js/test/2/fileTest.js @@ -3,7 +3,7 @@ const assert = chai.assert; var config = require('config'); const API = require('../../api2.js'); const Helpers = require('../../helpers2.js'); -const { API2Setup, API2WrapUp } = require("../shared.js"); +const { API2Before, API2After } = require("../shared.js"); const { S3Client, DeleteObjectsCommand, PutObjectCommand } = require("@aws-sdk/client-s3"); const fs = require('fs'); const HTTP = require('../../httpHandler.js'); @@ -17,7 +17,7 @@ describe('FileTestTests', function () { const s3Client = new S3Client({ region: "us-east-1" }); before(async function () { - await API2Setup(); + await API2Before(); try { fs.mkdirSync("./work"); } @@ -25,7 +25,7 @@ describe('FileTestTests', function () { }); after(async function () { - await API2WrapUp(); + await API2After(); fs.rm("./work", { recursive: true, force: true }, (e) => { if (e) console.log(e); }); diff --git a/tests/remote_js/test/2/fullText.js b/tests/remote_js/test/2/fullText.js index 49cc5843..1f9cb9ed 100644 --- a/tests/remote_js/test/2/fullText.js +++ b/tests/remote_js/test/2/fullText.js @@ -3,17 +3,17 @@ const assert = chai.assert; var config = require('config'); const API = require('../../api2.js'); const Helpers = require('../../helpers2.js'); -const { API2Setup, API2WrapUp } = require("../shared.js"); +const { API2Before, API2After } = require("../shared.js"); describe('FullTextTests', function () { this.timeout(config.timeout); before(async function () { - await API2Setup(); + await API2Before(); }); after(async function () { - await API2WrapUp(); + await API2After(); }); it('testSetItemContent', async function () { diff --git a/tests/remote_js/test/2/generalTest.js b/tests/remote_js/test/2/generalTest.js index 0ce8b373..71869bff 100644 --- a/tests/remote_js/test/2/generalTest.js +++ b/tests/remote_js/test/2/generalTest.js @@ -3,18 +3,18 @@ const assert = chai.assert; var config = require('config'); const API = require('../../api2.js'); const Helpers = require('../../helpers2.js'); -const { API2Setup, API2WrapUp } = require("../shared.js"); +const { API2Before, API2After } = require("../shared.js"); describe('GeneralTests', function () { this.timeout(config.timeout); before(async function () { - await API2Setup(); + await API2Before(); }); after(async function () { - await API2WrapUp(); + await API2After(); }); it('testInvalidCharacters', async function () { diff --git a/tests/remote_js/test/2/groupTest.js b/tests/remote_js/test/2/groupTest.js index 766bf8e7..f17ccf8a 100644 --- a/tests/remote_js/test/2/groupTest.js +++ b/tests/remote_js/test/2/groupTest.js @@ -3,19 +3,19 @@ const assert = chai.assert; var config = require('config'); const API = require('../../api2.js'); const Helpers = require('../../helpers2.js'); -const { API2Setup, API2WrapUp, resetGroups } = require("../shared.js"); +const { API2Before, API2After, resetGroups } = require("../shared.js"); const { JSDOM } = require("jsdom"); describe('GroupTests', function () { this.timeout(config.timeout); before(async function () { - await API2Setup(); + await API2Before(); await resetGroups(); }); after(async function () { - await API2WrapUp(); + await API2After(); }); it('testUpdateMetadata', async function () { diff --git a/tests/remote_js/test/2/itemsTest.js b/tests/remote_js/test/2/itemsTest.js index ded2c119..efe37fb8 100644 --- a/tests/remote_js/test/2/itemsTest.js +++ b/tests/remote_js/test/2/itemsTest.js @@ -3,17 +3,17 @@ const assert = chai.assert; var config = require('config'); const API = require('../../api2.js'); const Helpers = require('../../helpers2.js'); -const { API2Setup, API2WrapUp } = require("../shared.js"); +const { API2Before, API2After } = require("../shared.js"); describe('ItemsTests', function () { this.timeout(config.timeout * 2); before(async function () { - await API2Setup(); + await API2Before(); }); after(async function () { - await API2WrapUp(); + await API2After(); }); const testNewEmptyBookItem = async () => { diff --git a/tests/remote_js/test/2/mappingsTest.js b/tests/remote_js/test/2/mappingsTest.js index 9ed53f08..f5d22683 100644 --- a/tests/remote_js/test/2/mappingsTest.js +++ b/tests/remote_js/test/2/mappingsTest.js @@ -3,17 +3,17 @@ const assert = chai.assert; var config = require('config'); const API = require('../../api2.js'); const Helpers = require('../../helpers2.js'); -const { API2Setup, API2WrapUp } = require("../shared.js"); +const { API2Before, API2After } = require("../shared.js"); describe('MappingsTests', function () { this.timeout(config.timeout); before(async function () { - await API2Setup(); + await API2Before(); }); after(async function () { - await API2WrapUp(); + await API2After(); }); it('testNewItem', async function () { diff --git a/tests/remote_js/test/2/noteTest.js b/tests/remote_js/test/2/noteTest.js index 5d6b25d3..992f9114 100644 --- a/tests/remote_js/test/2/noteTest.js +++ b/tests/remote_js/test/2/noteTest.js @@ -3,7 +3,7 @@ const assert = chai.assert; var config = require('config'); const API = require('../../api2.js'); const Helpers = require("../../helpers2.js"); -const { API2Setup, API2WrapUp } = require("../shared.js"); +const { API2Before, API2After } = require("../shared.js"); describe('NoteTests', function () { this.timeout(config.timeout); @@ -11,11 +11,11 @@ describe('NoteTests', function () { let json; before(async function () { - await API2Setup(); + await API2Before(); }); after(async function () { - await API2WrapUp(); + await API2After(); }); beforeEach(async function () { diff --git a/tests/remote_js/test/2/objectTest.js b/tests/remote_js/test/2/objectTest.js index 4fcaac5b..38888cf8 100644 --- a/tests/remote_js/test/2/objectTest.js +++ b/tests/remote_js/test/2/objectTest.js @@ -3,17 +3,17 @@ const assert = chai.assert; var config = require('config'); const API = require('../../api2.js'); const Helpers = require('../../helpers2.js'); -const { API2Setup, API2WrapUp } = require("../shared.js"); +const { API2Before, API2After } = require("../shared.js"); describe('ObjectTests', function () { this.timeout(config.timeout); before(async function () { - await API2Setup(); + await API2Before(); }); after(async function () { - await API2WrapUp(); + await API2After(); }); const _testMultiObjectGet = async (objectType = 'collection') => { diff --git a/tests/remote_js/test/2/paramTest.js b/tests/remote_js/test/2/paramTest.js index c49a8cf3..af3a00eb 100644 --- a/tests/remote_js/test/2/paramTest.js +++ b/tests/remote_js/test/2/paramTest.js @@ -3,7 +3,7 @@ const assert = chai.assert; var config = require('config'); const API = require('../../api2.js'); const Helpers = require('../../helpers2.js'); -const { API2Setup, API2WrapUp } = require("../shared.js"); +const { API2Before, API2After } = require("../shared.js"); describe('ParametersTests', function () { this.timeout(config.timeout * 2); @@ -12,11 +12,11 @@ describe('ParametersTests', function () { let searchKeys = []; before(async function () { - await API2Setup(); + await API2Before(); }); after(async function () { - await API2WrapUp(); + await API2After(); }); const _testFormatKeys = async (objectType, sorted = false) => { diff --git a/tests/remote_js/test/2/permissionsTest.js b/tests/remote_js/test/2/permissionsTest.js index 79ed2cbe..27798eab 100644 --- a/tests/remote_js/test/2/permissionsTest.js +++ b/tests/remote_js/test/2/permissionsTest.js @@ -3,18 +3,18 @@ const assert = chai.assert; var config = require('config'); const API = require('../../api2.js'); const Helpers = require('../../helpers2.js'); -const { API2Setup, API2WrapUp, resetGroups } = require("../shared.js"); +const { API2Before, API2After, resetGroups } = require("../shared.js"); describe('PermissionsTests', function () { this.timeout(config.timeout); before(async function () { - await API2Setup(); + await API2Before(); await resetGroups(); }); after(async function () { - await API2WrapUp(); + await API2After(); }); it('testUserGroupsAnonymous', async function () { diff --git a/tests/remote_js/test/2/relationsTest.js b/tests/remote_js/test/2/relationsTest.js index 873d5d62..ba39f3ae 100644 --- a/tests/remote_js/test/2/relationsTest.js +++ b/tests/remote_js/test/2/relationsTest.js @@ -3,17 +3,17 @@ const assert = chai.assert; var config = require('config'); const API = require('../../api2.js'); const Helpers = require('../../helpers2.js'); -const { API2Setup, API2WrapUp } = require("../shared.js"); +const { API2Before, API2After } = require("../shared.js"); describe('RelationsTests', function () { this.timeout(config.timeout); before(async function () { - await API2Setup(); + await API2Before(); }); after(async function () { - await API2WrapUp(); + await API2After(); }); it('testNewItemRelations', async function () { diff --git a/tests/remote_js/test/2/searchTest.js b/tests/remote_js/test/2/searchTest.js index f60053cc..be63b3aa 100644 --- a/tests/remote_js/test/2/searchTest.js +++ b/tests/remote_js/test/2/searchTest.js @@ -3,17 +3,17 @@ const assert = chai.assert; var config = require('config'); const API = require('../../api2.js'); const Helpers = require('../../helpers2.js'); -const { API2Setup, API2WrapUp } = require("../shared.js"); +const { API2Before, API2After } = require("../shared.js"); describe('SearchTests', function () { this.timeout(config.timeout); before(async function () { - await API2Setup(); + await API2Before(); }); after(async function () { - await API2WrapUp(); + await API2After(); }); const testNewSearch = async () => { let name = "Test Search"; diff --git a/tests/remote_js/test/2/settingsTest.js b/tests/remote_js/test/2/settingsTest.js index 040277b7..9faa4886 100644 --- a/tests/remote_js/test/2/settingsTest.js +++ b/tests/remote_js/test/2/settingsTest.js @@ -3,18 +3,18 @@ const assert = chai.assert; var config = require('config'); const API = require('../../api2.js'); const Helpers = require('../../helpers2.js'); -const { API2Setup, API2WrapUp, resetGroups } = require("../shared.js"); +const { API2Before, API2After, resetGroups } = require("../shared.js"); describe('SettingsTests', function () { this.timeout(config.timeout); before(async function () { - await API2Setup(); + await API2Before(); await resetGroups(); }); after(async function () { - await API2WrapUp(); + await API2After(); await resetGroups(); }); diff --git a/tests/remote_js/test/2/sortTest.js b/tests/remote_js/test/2/sortTest.js index 1be5d439..8246d366 100644 --- a/tests/remote_js/test/2/sortTest.js +++ b/tests/remote_js/test/2/sortTest.js @@ -3,7 +3,7 @@ const assert = chai.assert; var config = require('config'); const API = require('../../api2.js'); const Helpers = require('../../helpers2.js'); -const { API2Setup, API2WrapUp } = require("../shared.js"); +const { API2Before, API2After } = require("../shared.js"); describe('SortTests', function () { this.timeout(config.timeout); @@ -19,12 +19,12 @@ describe('SortTests', function () { let notes = [null, 'aaa', null, null, 'taf']; before(async function () { - await API2Setup(); + await API2Before(); await setup(); }); after(async function () { - await API2WrapUp(); + await API2After(); }); const setup = async () => { diff --git a/tests/remote_js/test/2/storageAdmin.js b/tests/remote_js/test/2/storageAdmin.js index 1f530650..4e50d496 100644 --- a/tests/remote_js/test/2/storageAdmin.js +++ b/tests/remote_js/test/2/storageAdmin.js @@ -3,19 +3,19 @@ const assert = chai.assert; var config = require('config'); const API = require('../../api2.js'); const Helpers = require('../../helpers2.js'); -const { API2Setup, API2WrapUp } = require("../shared.js"); +const { API2Before, API2After } = require("../shared.js"); describe('StorageAdminTests', function () { this.timeout(config.timeout); const DEFAULT_QUOTA = 300; before(async function () { - await API2Setup(); + await API2Before(); await setQuota(0, 0, DEFAULT_QUOTA); }); after(async function () { - await API2WrapUp(); + await API2After(); }); const setQuota = async (quota, expiration, expectedQuota) => { diff --git a/tests/remote_js/test/2/tagTest.js b/tests/remote_js/test/2/tagTest.js index 84586911..1bd1674f 100644 --- a/tests/remote_js/test/2/tagTest.js +++ b/tests/remote_js/test/2/tagTest.js @@ -3,18 +3,18 @@ const assert = chai.assert; var config = require('config'); const API = require('../../api2.js'); const Helpers = require('../../helpers2.js'); -const { API2Setup, API2WrapUp } = require("../shared.js"); +const { API2Before, API2After } = require("../shared.js"); describe('TagTests', function () { this.timeout(config.timeout); before(async function () { - await API2Setup(); + await API2Before(); API.useAPIVersion(2); }); after(async function () { - await API2WrapUp(); + await API2After(); }); it('test_empty_tag_should_be_ignored', async function () { let json = await API.getItemTemplate("book"); diff --git a/tests/remote_js/test/2/versionTest.js b/tests/remote_js/test/2/versionTest.js index adc733e6..8f01db99 100644 --- a/tests/remote_js/test/2/versionTest.js +++ b/tests/remote_js/test/2/versionTest.js @@ -3,17 +3,17 @@ const assert = chai.assert; var config = require('config'); const API = require('../../api2.js'); const Helpers = require('../../helpers2.js'); -const { API2Setup, API2WrapUp } = require("../shared.js"); +const { API2Before, API2After } = require("../shared.js"); describe('VersionsTests', function () { this.timeout(config.timeout * 2); before(async function () { - await API2Setup(); + await API2Before(); }); after(async function () { - await API2WrapUp(); + await API2After(); }); const _capitalizeFirstLetter = (string) => { diff --git a/tests/remote_js/test/3/annotationsTest.js b/tests/remote_js/test/3/annotationsTest.js index 77f52841..bdc0822b 100644 --- a/tests/remote_js/test/3/annotationsTest.js +++ b/tests/remote_js/test/3/annotationsTest.js @@ -4,14 +4,14 @@ var config = require('config'); const API = require('../../api3.js'); const Helpers = require('../../helpers3.js'); const { resetGroups } = require('../../groupsSetup.js'); -const { API3Setup, API3WrapUp } = require("../shared.js"); +const { API3Before, API3After } = require("../shared.js"); describe('AnnotationsTests', function () { this.timeout(config.timeout); let attachmentKey, attachmentJSON; before(async function () { - await API3Setup(); + await API3Before(); await resetGroups(); await API.groupClear(config.ownedPrivateGroupID); @@ -28,7 +28,7 @@ describe('AnnotationsTests', function () { }); after(async function () { - await API3WrapUp(); + await API3After(); }); diff --git a/tests/remote_js/test/3/atomTest.js b/tests/remote_js/test/3/atomTest.js index 8c241617..1b117696 100644 --- a/tests/remote_js/test/3/atomTest.js +++ b/tests/remote_js/test/3/atomTest.js @@ -4,14 +4,14 @@ var config = require('config'); const API = require('../../api3.js'); const Helpers = require('../../helpers3.js'); const { JSDOM } = require('jsdom'); -const { API3Setup, API3WrapUp } = require("../shared.js"); +const { API3Before, API3After } = require("../shared.js"); describe('AtomTests', function () { this.timeout(config.timeout); let keyObj = {}; before(async function () { - await API3Setup(); + await API3Before(); let key = await API.createItem("book", { title: "Title", creators: [{ @@ -44,7 +44,7 @@ describe('AtomTests', function () { }); after(async function () { - await API3WrapUp(); + await API3After(); }); diff --git a/tests/remote_js/test/3/bibTest.js b/tests/remote_js/test/3/bibTest.js index 00b81a2f..d468c1d4 100644 --- a/tests/remote_js/test/3/bibTest.js +++ b/tests/remote_js/test/3/bibTest.js @@ -3,7 +3,7 @@ const assert = chai.assert; var config = require('config'); const API = require('../../api3.js'); const Helpers = require('../../helpers3.js'); -const { API3Setup, API3WrapUp } = require("../shared.js"); +const { API3Before, API3After } = require("../shared.js"); describe('BibTests', function () { this.timeout(config.timeout); @@ -19,7 +19,7 @@ describe('BibTests', function () { ]; before(async function () { - await API3Setup(); + await API3Before(); // Create test data let key = await API.createItem("book", { @@ -133,7 +133,7 @@ describe('BibTests', function () { }); after(async function () { - await API3WrapUp(); + await API3After(); }); it('testContentCitationMulti', async function () { diff --git a/tests/remote_js/test/3/cacheTest.js b/tests/remote_js/test/3/cacheTest.js index d81ac0e7..1ed4ff4d 100644 --- a/tests/remote_js/test/3/cacheTest.js +++ b/tests/remote_js/test/3/cacheTest.js @@ -2,17 +2,17 @@ const chai = require('chai'); const assert = chai.assert; var config = require('config'); const API = require('../../api3.js'); -const { API3Setup, API3WrapUp } = require("../shared.js"); +const { API3Before, API3After } = require("../shared.js"); describe('CacheTests', function () { this.timeout(config.timeout); before(async function () { - await API3Setup(); + await API3Before(); }); after(async function () { - await API3WrapUp(); + await API3After(); }); it('testCacheCreatorPrimaryData', async function () { diff --git a/tests/remote_js/test/3/collectionTest.js b/tests/remote_js/test/3/collectionTest.js index 2405806f..74b88d1b 100644 --- a/tests/remote_js/test/3/collectionTest.js +++ b/tests/remote_js/test/3/collectionTest.js @@ -3,17 +3,17 @@ const assert = chai.assert; var config = require('config'); const API = require('../../api3.js'); const Helpers = require('../../helpers3.js'); -const { API3Setup, API3WrapUp } = require("../shared.js"); +const { API3Before, API3After } = require("../shared.js"); describe('CollectionTests', function () { this.timeout(config.timeout); before(async function () { - await API3Setup(); + await API3Before(); }); after(async function () { - await API3WrapUp(); + await API3After(); }); this.beforeEach(async function () { diff --git a/tests/remote_js/test/3/creatorTest.js b/tests/remote_js/test/3/creatorTest.js index 25dbd251..e949dd2c 100644 --- a/tests/remote_js/test/3/creatorTest.js +++ b/tests/remote_js/test/3/creatorTest.js @@ -3,17 +3,17 @@ const assert = chai.assert; var config = require('config'); const API = require('../../api3.js'); const Helpers = require('../../helpers3.js'); -const { API3Setup, API3WrapUp } = require("../shared.js"); +const { API3Before, API3After } = require("../shared.js"); describe('CreatorTests', function () { this.timeout(config.timeout); before(async function () { - await API3Setup(); + await API3Before(); }); after(async function () { - await API3WrapUp(); + await API3After(); }); diff --git a/tests/remote_js/test/3/exportTest.js b/tests/remote_js/test/3/exportTest.js index db9c2409..b0bac6b6 100644 --- a/tests/remote_js/test/3/exportTest.js +++ b/tests/remote_js/test/3/exportTest.js @@ -3,7 +3,7 @@ const assert = chai.assert; var config = require('config'); const API = require('../../api3.js'); const Helpers = require('../../helpers3.js'); -const { API3Setup, API3WrapUp } = require("../shared.js"); +const { API3Before, API3After } = require("../shared.js"); describe('ExportTests', function () { this.timeout(config.timeout); @@ -12,7 +12,7 @@ describe('ExportTests', function () { let formats = ['bibtex', 'ris', 'csljson']; before(async function () { - await API3Setup(); + await API3Before(); await API.userClear(config.userID); // Create test data @@ -119,7 +119,7 @@ describe('ExportTests', function () { }); after(async function () { - await API3WrapUp(); + await API3After(); }); it('testExportInclude', async function () { diff --git a/tests/remote_js/test/3/fileTest.js b/tests/remote_js/test/3/fileTest.js index ce7e8498..fd1cf1c8 100644 --- a/tests/remote_js/test/3/fileTest.js +++ b/tests/remote_js/test/3/fileTest.js @@ -3,7 +3,7 @@ const assert = chai.assert; var config = require('config'); const API = require('../../api3.js'); const Helpers = require('../../helpers3.js'); -const { API3Setup, API3WrapUp } = require("../shared.js"); +const { API3Before, API3After } = require("../shared.js"); const { S3Client, DeleteObjectsCommand, PutObjectCommand } = require("@aws-sdk/client-s3"); const fs = require('fs'); const HTTP = require('../../httpHandler.js'); @@ -18,7 +18,7 @@ describe('FileTestTests', function () { const s3Client = new S3Client({ region: "us-east-1" }); before(async function () { - await API3Setup(); + await API3Before(); try { fs.mkdirSync("./work"); } @@ -26,7 +26,7 @@ describe('FileTestTests', function () { }); after(async function () { - await API3WrapUp(); + await API3After(); fs.rm("./work", { recursive: true, force: true }, (e) => { if (e) console.log(e); }); diff --git a/tests/remote_js/test/3/fullTextTest.js b/tests/remote_js/test/3/fullTextTest.js index 612f3531..cc8a9801 100644 --- a/tests/remote_js/test/3/fullTextTest.js +++ b/tests/remote_js/test/3/fullTextTest.js @@ -3,17 +3,17 @@ const assert = chai.assert; var config = require('config'); const API = require('../../api3.js'); const Helpers = require('../../helpers3.js'); -const { API3Setup, API3WrapUp } = require("../shared.js"); +const { API3Before, API3After } = require("../shared.js"); describe('FullTextTests', function () { this.timeout(config.timeout); before(async function () { - await API3Setup(); + await API3Before(); }); after(async function () { - await API3WrapUp(); + await API3After(); }); this.beforeEach(async function () { diff --git a/tests/remote_js/test/3/generalTest.js b/tests/remote_js/test/3/generalTest.js index b741789f..c666f39a 100644 --- a/tests/remote_js/test/3/generalTest.js +++ b/tests/remote_js/test/3/generalTest.js @@ -3,18 +3,18 @@ const assert = chai.assert; var config = require('config'); const API = require('../../api3.js'); const Helpers = require('../../helpers3.js'); -const { API3Setup, API3WrapUp } = require("../shared.js"); +const { API3Before, API3After } = require("../shared.js"); describe('GeneralTests', function () { this.timeout(config.timeout); before(async function () { - await API3Setup(); + await API3Before(); }); after(async function () { - await API3WrapUp(); + await API3After(); }); it('testInvalidCharacters', async function () { diff --git a/tests/remote_js/test/3/groupTest.js b/tests/remote_js/test/3/groupTest.js index 475a9076..c3668db1 100644 --- a/tests/remote_js/test/3/groupTest.js +++ b/tests/remote_js/test/3/groupTest.js @@ -4,17 +4,17 @@ var config = require('config'); const API = require('../../api3.js'); const Helpers = require('../../helpers3.js'); const { JSDOM } = require('jsdom'); -const { API3Setup, API3WrapUp } = require("../shared.js"); +const { API3Before, API3After } = require("../shared.js"); describe('Tests', function () { this.timeout(config.timeout); before(async function () { - await API3Setup(); + await API3Before(); }); after(async function () { - await API3WrapUp(); + await API3After(); }); diff --git a/tests/remote_js/test/3/itemTest.js b/tests/remote_js/test/3/itemTest.js index 71ca6c89..f7ee1355 100644 --- a/tests/remote_js/test/3/itemTest.js +++ b/tests/remote_js/test/3/itemTest.js @@ -3,18 +3,18 @@ const assert = chai.assert; var config = require('config'); const API = require('../../api3.js'); const Helpers = require('../../helpers3.js'); -const { API3Setup, API3WrapUp, resetGroups } = require("../shared.js"); +const { API3Before, API3After, resetGroups } = require("../shared.js"); describe('ItemsTests', function () { this.timeout(config.timeout); before(async function () { - await API3Setup(); + await API3Before(); await resetGroups(); }); after(async function () { - await API3WrapUp(); + await API3After(); }); this.beforeEach(async function () { diff --git a/tests/remote_js/test/3/keysTest.js b/tests/remote_js/test/3/keysTest.js index 25c319f8..854dd5bc 100644 --- a/tests/remote_js/test/3/keysTest.js +++ b/tests/remote_js/test/3/keysTest.js @@ -3,17 +3,17 @@ const assert = chai.assert; var config = require('config'); const API = require('../../api3.js'); const Helpers = require('../../helpers3.js'); -const { API3Setup, API3WrapUp } = require("../shared.js"); +const { API3Before, API3After } = require("../shared.js"); describe('KeysTests', function () { this.timeout(config.timeout); before(async function () { - await API3Setup(); + await API3Before(); }); after(async function () { - await API3WrapUp(); + await API3After(); }); // beforeEach(async function () { // await API.userClear(config.userID); diff --git a/tests/remote_js/test/3/mappingsTest.js b/tests/remote_js/test/3/mappingsTest.js index 4a80b576..5581d3a9 100644 --- a/tests/remote_js/test/3/mappingsTest.js +++ b/tests/remote_js/test/3/mappingsTest.js @@ -3,17 +3,17 @@ const assert = chai.assert; var config = require('config'); const API = require('../../api3.js'); const Helpers = require('../../helpers3.js'); -const { API3Setup, API3WrapUp } = require("../shared.js"); +const { API3Before, API3After } = require("../shared.js"); describe('MappingsTests', function () { this.timeout(config.timeout); before(async function () { - await API3Setup(); + await API3Before(); }); after(async function () { - await API3WrapUp(); + await API3After(); }); it('testLocale', async function () { diff --git a/tests/remote_js/test/3/noteTest.js b/tests/remote_js/test/3/noteTest.js index e2d95c5f..798fc03e 100644 --- a/tests/remote_js/test/3/noteTest.js +++ b/tests/remote_js/test/3/noteTest.js @@ -3,7 +3,7 @@ const assert = chai.assert; var config = require('config'); const API = require('../../api3.js'); const Helpers = require('../../helpers3.js'); -const { API3Setup, API3WrapUp } = require("../shared.js"); +const { API3Before, API3After } = require("../shared.js"); describe('NoteTests', function () { //this.timeout(config.timeout); @@ -12,11 +12,11 @@ describe('NoteTests', function () { let content, json; before(async function () { - await API3Setup(); + await API3Before(); }); after(async function () { - await API3WrapUp(); + await API3After(); }); this.beforeEach(async function() { diff --git a/tests/remote_js/test/3/notificationTest.js b/tests/remote_js/test/3/notificationTest.js index d6a48172..6a33e981 100644 --- a/tests/remote_js/test/3/notificationTest.js +++ b/tests/remote_js/test/3/notificationTest.js @@ -3,18 +3,18 @@ const assert = chai.assert; var config = require('config'); const API = require('../../api3.js'); const Helpers = require('../../helpers3.js'); -const { API3Setup, API3WrapUp, resetGroups } = require("../shared.js"); +const { API3Before, API3After, resetGroups } = require("../shared.js"); describe('NotificationTests', function () { this.timeout(config.timeout); before(async function () { - await API3Setup(); + await API3Before(); await resetGroups(); }); after(async function () { - await API3WrapUp(); + await API3After(); }); beforeEach(async function () { API.useAPIKey(config.apiKey); diff --git a/tests/remote_js/test/3/objectTest.js b/tests/remote_js/test/3/objectTest.js index 853fa76c..9e53f016 100644 --- a/tests/remote_js/test/3/objectTest.js +++ b/tests/remote_js/test/3/objectTest.js @@ -3,18 +3,18 @@ const assert = chai.assert; var config = require('config'); const API = require('../../api3.js'); const Helpers = require('../../helpers3.js'); -const { API3Setup, API3WrapUp } = require("../shared.js"); +const { API3Before, API3After } = require("../shared.js"); describe('ObjectTests', function () { this.timeout(config.timeout); let types = ['collection', 'search', 'item']; before(async function () { - await API3Setup(); + await API3Before(); }); after(async function () { - await API3WrapUp(); + await API3After(); }); beforeEach(async function () { diff --git a/tests/remote_js/test/3/paramsTest.js b/tests/remote_js/test/3/paramsTest.js index cb449d64..126788f8 100644 --- a/tests/remote_js/test/3/paramsTest.js +++ b/tests/remote_js/test/3/paramsTest.js @@ -3,7 +3,7 @@ const assert = chai.assert; var config = require('config'); const API = require('../../api3.js'); const Helpers = require('../../helpers3.js'); -const { API3Setup, API3WrapUp } = require("../shared.js"); +const { API3Before, API3After } = require("../shared.js"); describe('ParamsTests', function () { this.timeout(config.timeout); @@ -19,11 +19,11 @@ describe('ParamsTests', function () { }; before(async function () { - await API3Setup(); + await API3Before(); }); after(async function () { - await API3WrapUp(); + await API3After(); }); beforeEach(async function () { await API.userClear(config.userID); diff --git a/tests/remote_js/test/3/permissionTest.js b/tests/remote_js/test/3/permissionTest.js index 67a65ee3..c31dbbdf 100644 --- a/tests/remote_js/test/3/permissionTest.js +++ b/tests/remote_js/test/3/permissionTest.js @@ -3,17 +3,17 @@ const assert = chai.assert; var config = require('config'); const API = require('../../api3.js'); const Helpers = require('../../helpers3.js'); -const { API3Setup, API3WrapUp, resetGroups } = require("../shared.js"); +const { API3Before, API3After, resetGroups } = require("../shared.js"); describe('PermissionsTests', function () { this.timeout(config.timeout); before(async function () { - await API3Setup(); + await API3Before(); }); after(async function () { - await API3WrapUp(); + await API3After(); }); beforeEach(async function () { diff --git a/tests/remote_js/test/3/publicationTest.js b/tests/remote_js/test/3/publicationTest.js index db84ef95..a0b5101e 100644 --- a/tests/remote_js/test/3/publicationTest.js +++ b/tests/remote_js/test/3/publicationTest.js @@ -3,7 +3,7 @@ const assert = chai.assert; var config = require('config'); const API = require('../../api3.js'); const Helpers = require('../../helpers3.js'); -const { API3Setup, API3WrapUp, resetGroups } = require("../shared.js"); +const { API3Before, API3After, resetGroups } = require("../shared.js"); const { S3Client, DeleteObjectsCommand } = require("@aws-sdk/client-s3"); const HTTP = require('../../httpHandler.js'); const fs = require('fs'); @@ -16,7 +16,7 @@ describe('PublicationTests', function () { const s3Client = new S3Client({ region: "us-east-1" }); before(async function () { - await API3Setup(); + await API3Before(); await resetGroups(); try { fs.mkdirSync("./work"); @@ -25,7 +25,7 @@ describe('PublicationTests', function () { }); after(async function () { - await API3WrapUp(); + await API3After(); fs.rm("./work", { recursive: true, force: true }, (e) => { if (e) console.log(e); }); diff --git a/tests/remote_js/test/3/relationTest.js b/tests/remote_js/test/3/relationTest.js index 555b7e12..004f8be4 100644 --- a/tests/remote_js/test/3/relationTest.js +++ b/tests/remote_js/test/3/relationTest.js @@ -3,17 +3,17 @@ const assert = chai.assert; var config = require('config'); const API = require('../../api3.js'); const Helpers = require('../../helpers3.js'); -const { API3Setup, API3WrapUp } = require("../shared.js"); +const { API3Before, API3After } = require("../shared.js"); describe('RelationsTests', function () { this.timeout(config.timeout); before(async function () { - await API3Setup(); + await API3Before(); }); after(async function () { - await API3WrapUp(); + await API3After(); }); it('testNewItemRelations', async function () { diff --git a/tests/remote_js/test/3/searchTest.js b/tests/remote_js/test/3/searchTest.js index 722669c3..46d39972 100644 --- a/tests/remote_js/test/3/searchTest.js +++ b/tests/remote_js/test/3/searchTest.js @@ -3,17 +3,17 @@ const assert = chai.assert; var config = require('config'); const API = require('../../api3.js'); const Helpers = require('../../helpers3.js'); -const { API3Setup, API3WrapUp } = require("../shared.js"); +const { API3Before, API3After } = require("../shared.js"); describe('SearchTests', function () { this.timeout(config.timeout); before(async function () { - await API3Setup(); + await API3Before(); }); after(async function () { - await API3WrapUp(); + await API3After(); }); const testNewSearch = async () => { let name = "Test Search"; diff --git a/tests/remote_js/test/3/settingsTest.js b/tests/remote_js/test/3/settingsTest.js index 9aa72dcd..f10c8786 100644 --- a/tests/remote_js/test/3/settingsTest.js +++ b/tests/remote_js/test/3/settingsTest.js @@ -3,18 +3,18 @@ const assert = chai.assert; var config = require('config'); const API = require('../../api3.js'); const Helpers = require('../../helpers3.js'); -const { API3Setup, API3WrapUp, resetGroups } = require("../shared.js"); +const { API3Before, API3After, resetGroups } = require("../shared.js"); describe('SettingsTests', function () { this.timeout(config.timeout); before(async function () { - await API3Setup(); + await API3Before(); await resetGroups(); }); after(async function () { - await API3WrapUp(); + await API3After(); await resetGroups(); }); diff --git a/tests/remote_js/test/3/sortTest.js b/tests/remote_js/test/3/sortTest.js index 4eac9612..170aadd5 100644 --- a/tests/remote_js/test/3/sortTest.js +++ b/tests/remote_js/test/3/sortTest.js @@ -3,7 +3,7 @@ const assert = chai.assert; var config = require('config'); const API = require('../../api3.js'); const Helpers = require('../../helpers3.js'); -const { API3Setup, API3WrapUp } = require("../shared.js"); +const { API3Before, API3After } = require("../shared.js"); describe('SortTests', function () { this.timeout(config.timeout); @@ -19,12 +19,12 @@ describe('SortTests', function () { let notes = [null, 'aaa', null, null, 'taf']; before(async function () { - await API3Setup(); + await API3Before(); await setup(); }); after(async function () { - await API3WrapUp(); + await API3After(); }); const setup = async () => { diff --git a/tests/remote_js/test/3/storageAdmin.js b/tests/remote_js/test/3/storageAdmin.js index 4acd4f93..6de22ff7 100644 --- a/tests/remote_js/test/3/storageAdmin.js +++ b/tests/remote_js/test/3/storageAdmin.js @@ -3,19 +3,19 @@ const assert = chai.assert; var config = require('config'); const API = require('../../api3.js'); const Helpers = require('../../helpers3.js'); -const { API3Setup, API3WrapUp } = require("../shared.js"); +const { API3Before, API3After } = require("../shared.js"); describe('StorageAdminTests', function () { this.timeout(config.timeout); const DEFAULT_QUOTA = 300; before(async function () { - await API3Setup(); + await API3Before(); await setQuota(0, 0, DEFAULT_QUOTA); }); after(async function () { - await API3WrapUp(); + await API3After(); }); const setQuota = async (quota, expiration, expectedQuota) => { diff --git a/tests/remote_js/test/3/tagTest.js b/tests/remote_js/test/3/tagTest.js index cd611e29..4f8e56db 100644 --- a/tests/remote_js/test/3/tagTest.js +++ b/tests/remote_js/test/3/tagTest.js @@ -3,17 +3,17 @@ const assert = chai.assert; var config = require('config'); const API = require('../../api3.js'); const Helpers = require('../../helpers3.js'); -const { API3Setup, API3WrapUp } = require("../shared.js"); +const { API3Before, API3After } = require("../shared.js"); describe('TagTests', function () { this.timeout(config.timeout); before(async function () { - await API3Setup(); + await API3Before(); }); after(async function () { - await API3WrapUp(); + await API3After(); }); beforeEach(async function () { await API.userClear(config.userID); diff --git a/tests/remote_js/test/3/template.js b/tests/remote_js/test/3/template.js deleted file mode 100644 index 3e7f92aa..00000000 --- a/tests/remote_js/test/3/template.js +++ /dev/null @@ -1,25 +0,0 @@ -const chai = require('chai'); -const assert = chai.assert; -var config = require('config'); -const API = require('../../api3.js'); -const Helpers = require('../../helpers3.js'); -const { API3Setup, API3WrapUp } = require("../shared.js"); - -describe('Tests', function () { - this.timeout(config.timeout); - - before(async function () { - await API3Setup(); - }); - - after(async function () { - await API3WrapUp(); - }); - beforeEach(async function () { - await API.userClear(config.userID); - }); - - afterEach(async function () { - await API.userClear(config.userID); - }); -}); diff --git a/tests/remote_js/test/3/translationTest.js b/tests/remote_js/test/3/translationTest.js index 3095777b..c706de94 100644 --- a/tests/remote_js/test/3/translationTest.js +++ b/tests/remote_js/test/3/translationTest.js @@ -3,17 +3,17 @@ const assert = chai.assert; var config = require('config'); const API = require('../../api3.js'); const Helpers = require('../../helpers3.js'); -const { API3Setup, API3WrapUp } = require("../shared.js"); +const { API3Before, API3After } = require("../shared.js"); describe('TranslationTests', function () { this.timeout(config.timeout); before(async function () { - await API3Setup(); + await API3Before(); }); after(async function () { - await API3WrapUp(); + await API3After(); }); it('testWebTranslationMultiple', async function () { diff --git a/tests/remote_js/test/3/versionTest.js b/tests/remote_js/test/3/versionTest.js index a82418a4..d339e720 100644 --- a/tests/remote_js/test/3/versionTest.js +++ b/tests/remote_js/test/3/versionTest.js @@ -3,17 +3,17 @@ const assert = chai.assert; var config = require('config'); const API = require('../../api3.js'); const Helpers = require('../../helpers3.js'); -const { API3Setup, API3WrapUp } = require("../shared.js"); +const { API3Before, API3After } = require("../shared.js"); describe('VersionsTests', function () { this.timeout(config.timeout * 2); before(async function () { - await API3Setup(); + await API3Before(); }); after(async function () { - await API3WrapUp(); + await API3After(); }); const _capitalizeFirstLetter = (string) => { diff --git a/tests/remote_js/test/general.js b/tests/remote_js/test/general.js new file mode 100644 index 00000000..ca2e6f00 --- /dev/null +++ b/tests/remote_js/test/general.js @@ -0,0 +1,201 @@ +const chai = require('chai'); +const assert = chai.assert; +var config = require('config'); +const API = require('../api3.js'); +const Helpers = require('../helpers3.js'); +const { API3Before, API3After } = require("./shared.js"); +const HTTP = require("../httpHandler"); + +describe('GeneralTests', function () { + this.timeout(config.timeout); + + before(async function () { + await API3Before(); + API.useAPIVersion(false); + }); + + after(async function () { + await API3After(); + }); + + beforeEach(async function () { + API.useAPIKey(config.apiKey); + }); + + + it('test404Compression', async function () { + const response = await API.get("invalidurl"); + Helpers.assert404(response); + Helpers.assertCompression(response); + }); + + it('testAPIVersionHeader', async function () { + let minVersion = 1; + let maxVersion = 3; + let defaultVersion = 3; + let response; + + for (let i = minVersion; i <= maxVersion; i++) { + response = await API.userGet(config.userID, "items?format=keys&limit=1", + { "Zotero-API-Version": i } + ); + Helpers.assert200(response); + assert.equal(i, response.headers["zotero-api-version"][0]); + } + + // Default + response = await API.userGet(config.userID, "items?format=keys&limit=1"); + Helpers.assert200(response); + assert.equal(defaultVersion, response.headers["zotero-api-version"][0]); + }); + + it('test200Compression', async function () { + const response = await API.get('itemTypes'); + Helpers.assert200(response); + Helpers.assertCompression(response); + }); + + it('testAuthorization', async function () { + let apiKey = config.apiKey; + API.useAPIKey(false); + + // Zotero-API-Key header + let response = await API.userGet( + config.userID, + "items", + { + "Zotero-API-Key": apiKey + } + ); + Helpers.assert200(response); + + // Authorization header + response = await API.userGet( + config.userID, + "items", + { + Authorization: "Bearer " + apiKey + } + ); + Helpers.assert200(response); + + // Query parameter + response = await API.userGet( + config.userID, + "items?key=" + apiKey + ); + Helpers.assert200(response); + + // Zotero-API-Key header and query parameter + response = await API.userGet( + config.userID, + "items?key=" + apiKey, + { + "Zotero-API-Key": apiKey + } + ); + Helpers.assert200(response); + + // No key + response = await API.userGet( + config.userID, + "items" + ); + Helpers.assert403(response); + + // Zotero-API-Key header and empty key (which is still an error) + response = await API.userGet( + config.userID, + "items?key=", + { + "Zotero-API-Key": apiKey + } + ); + Helpers.assert400(response); + + // Zotero-API-Key header and incorrect Authorization key (which is ignored) + response = await API.userGet( + config.userID, + "items", + { + "Zotero-API-Key": apiKey, + Authorization: "Bearer invalidkey" + } + ); + Helpers.assert200(response); + + // Zotero-API-Key header and key mismatch + response = await API.userGet( + config.userID, + "items?key=invalidkey", + { + "Zotero-API-Key": apiKey + } + ); + Helpers.assert400(response); + + // Invalid Bearer format + response = await API.userGet( + config.userID, + "items", + { + Authorization: "Bearer key=" + apiKey + } + ); + Helpers.assert400(response); + + // Ignored OAuth 1.0 header, with key query parameter + response = await API.userGet( + config.userID, + "items?key=" + apiKey, + { + Authorization: 'OAuth oauth_consumer_key="aaaaaaaaaaaaaaaaaaaa"' + } + ); + Helpers.assert200(response); + + // Ignored OAuth 1.0 header, with no key query parameter + response = await API.userGet( + config.userID, + "items", + { + Authorization: 'OAuth oauth_consumer_key="aaaaaaaaaaaaaaaaaaaa"' + } + ); + Helpers.assert403(response); + }); + + it('testAPIVersionParameter', async function () { + let minVersion = 1; + let maxVersion = 3; + + for (let i = minVersion; i <= maxVersion; i++) { + const response = await API.userGet( + config.userID, + 'items?format=keys&limit=1&v=' + i + ); + assert.equal(i, response.headers['zotero-api-version'][0]); + } + }); + + it('testCORS', async function () { + let response = await HTTP.options(config.apiURLPrefix, { Origin: "http://example.com" }); + Helpers.assert200(response); + assert.equal('', response.data); + assert.equal('*', response.headers['access-control-allow-origin'][0]); + }); + + it('test204NoCompression', async function () { + let json = await API.createItem("book", [], null, 'jsonData'); + let response = await API.userDelete( + config.userID, + `items/${json.key}`, + { + "If-Unmodified-Since-Version": json.version + } + ); + Helpers.assert204(response); + Helpers.assertNoCompression(response); + Helpers.assertContentLength(response, 0); + }); +}); diff --git a/tests/remote_js/test/shared.js b/tests/remote_js/test/shared.js index 22686c32..28f1c061 100644 --- a/tests/remote_js/test/shared.js +++ b/tests/remote_js/test/shared.js @@ -10,18 +10,18 @@ module.exports = { }, - API1Setup: async () => { + API1Before: async () => { const credentials = await API.login(); config.apiKey = credentials.user1.apiKey; config.user2APIKey = credentials.user2.apiKey; await API.useAPIVersion(1); await API.userClear(config.userID); }, - API1WrapUp: async () => { + API1After: async () => { await API.userClear(config.userID); }, - API2Setup: async () => { + API2Before: async () => { const credentials = await API.login(); config.apiKey = credentials.user1.apiKey; config.user2APIKey = credentials.user2.apiKey; @@ -29,11 +29,11 @@ module.exports = { await API.setKeyOption(config.userID, config.apiKey, 'libraryNotes', 1); await API.userClear(config.userID); }, - API2WrapUp: async () => { + API2After: async () => { await API.userClear(config.userID); }, - API3Setup: async () => { + API3Before: async () => { const credentials = await API3.login(); config.apiKey = credentials.user1.apiKey; config.user2APIKey = credentials.user2.apiKey; @@ -44,7 +44,7 @@ module.exports = { await API3.setKeyUserPermission(config.apiKey, 'write', true); await API3.userClear(config.userID); }, - API3WrapUp: async () => { + API3After: async () => { await API3.useAPIKey(config.apiKey); await API3.userClear(config.userID); } From e150cd139490b7286692d94ba4efc44e7505e2ce Mon Sep 17 00:00:00 2001 From: Bogdan Abaev Date: Tue, 30 May 2023 23:12:11 -0400 Subject: [PATCH 20/33] removing retries, using 0-second delay instead --- tests/remote_js/httpHandler.js | 23 +++-------------------- 1 file changed, 3 insertions(+), 20 deletions(-) diff --git a/tests/remote_js/httpHandler.js b/tests/remote_js/httpHandler.js index 556e155e..b16dffa5 100644 --- a/tests/remote_js/httpHandler.js +++ b/tests/remote_js/httpHandler.js @@ -27,26 +27,9 @@ class HTTP { url = url.replace(localIPRegex, 'localhost'); } - let success = false; - let attempts = 3; - let tried = 0; - let response; - while (!success && tried < attempts) { - try { - response = await fetch(url, options); - success = true; - } - catch (error) { - if (error.name === 'FetchError') { - console.log('Request aborted. Wait for 2 seconds and retry...'); - await new Promise(r => setTimeout(r, 2000)); - tried += 1; - } - } - } - if (!success) { - throw new Error(`${method} to ${url} did not succeed after ${attempts} attempts.`); - } + await new Promise(resolve => setTimeout(resolve, 0)); + let response = await fetch(url, options); + // Fetch doesn't automatically parse the response body, so we have to do that manually let responseData = await response.text(); From ae270e5a298108100afe1511f500d050a5b2f62d Mon Sep 17 00:00:00 2001 From: Bogdan Abaev Date: Thu, 1 Jun 2023 00:11:37 -0400 Subject: [PATCH 21/33] minor cleanup, added missing comments, some tweaking with socker hanging --- tests/remote_js/api3.js | 12 +- tests/remote_js/helpers2.js | 20 +++ tests/remote_js/helpers3.js | 5 +- tests/remote_js/httpHandler.js | 21 ++- tests/remote_js/test/1/collectionTest.js | 16 +-- tests/remote_js/test/1/itemsTest.js | 2 +- tests/remote_js/test/2/atomTest.js | 12 +- tests/remote_js/test/2/bibTest.js | 6 +- tests/remote_js/test/2/cacheTest.js | 3 + tests/remote_js/test/2/collectionTest.js | 23 +-- tests/remote_js/test/2/fileTest.js | 158 ++++++++++++--------- tests/remote_js/test/2/fullText.js | 4 +- tests/remote_js/test/2/generalTest.js | 2 +- tests/remote_js/test/2/groupTest.js | 3 + tests/remote_js/test/2/itemsTest.js | 38 ++--- tests/remote_js/test/2/noteTest.js | 7 +- tests/remote_js/test/2/objectTest.js | 33 +++-- tests/remote_js/test/2/paramTest.js | 3 + tests/remote_js/test/2/permissionsTest.js | 12 +- tests/remote_js/test/2/relationsTest.js | 8 +- tests/remote_js/test/2/settingsTest.js | 2 +- tests/remote_js/test/2/sortTest.js | 3 + tests/remote_js/test/2/tagTest.js | 6 +- tests/remote_js/test/2/versionTest.js | 31 +++- tests/remote_js/test/3/annotationsTest.js | 12 +- tests/remote_js/test/3/atomTest.js | 15 +- tests/remote_js/test/3/bibTest.js | 11 +- tests/remote_js/test/3/cacheTest.js | 3 + tests/remote_js/test/3/collectionTest.js | 12 +- tests/remote_js/test/3/creatorTest.js | 2 + tests/remote_js/test/3/fileTest.js | 107 ++++++++++---- tests/remote_js/test/3/fullTextTest.js | 67 +-------- tests/remote_js/test/3/groupTest.js | 14 ++ tests/remote_js/test/3/itemTest.js | 157 +++++++++++--------- tests/remote_js/test/3/keysTest.js | 33 +++-- tests/remote_js/test/3/mappingsTest.js | 8 +- tests/remote_js/test/3/noteTest.js | 7 +- tests/remote_js/test/3/notificationTest.js | 46 +++--- tests/remote_js/test/3/objectTest.js | 37 ++--- tests/remote_js/test/3/paramsTest.js | 18 ++- tests/remote_js/test/3/permissionTest.js | 17 ++- tests/remote_js/test/3/publicationTest.js | 82 +++++++---- tests/remote_js/test/3/relationTest.js | 38 ++++- tests/remote_js/test/3/schemaTest.js | 39 +++++ tests/remote_js/test/3/settingsTest.js | 6 +- tests/remote_js/test/3/sortTest.js | 36 ++++- tests/remote_js/test/3/tagTest.js | 49 +++++-- tests/remote_js/test/3/translationTest.js | 2 +- tests/remote_js/test/3/versionTest.js | 55 ++----- 49 files changed, 807 insertions(+), 496 deletions(-) create mode 100644 tests/remote_js/test/3/schemaTest.js diff --git a/tests/remote_js/api3.js b/tests/remote_js/api3.js index dd6396d1..7ebfc60a 100644 --- a/tests/remote_js/api3.js +++ b/tests/remote_js/api3.js @@ -398,7 +398,7 @@ class API3 { response = await this.userPost( config.userID, - `items?key=${this.apiKey}`, + `items?key=${config.apiKey}`, JSON.stringify([json]), { "Content-Type": "application/json" } ); @@ -500,7 +500,7 @@ class API3 { response = await this.groupPost( groupID, - `items?key=${this.apiKey}`, + `items?key=${config.apiKey}`, JSON.stringify([json]), { "Content-Type": "application/json" } ); @@ -684,7 +684,7 @@ class API3 { let headers = { "Content-Type": "application/json", - "Zotero-API-Key": this.apiKey + "Zotero-API-Key": config.apiKey }; let requestBody = JSON.stringify([json]); @@ -741,7 +741,7 @@ class API3 { } response = await this.put( - "keys/" + this.apiKey, + "keys/" + config.apiKey, JSON.stringify(json), {}, { @@ -792,7 +792,7 @@ class API3 { } response = await this.put( - "keys/" + this.apiKey, + "keys/" + config.apiKey, xml.outterHTML, {}, { @@ -975,7 +975,7 @@ class API3 { if (single) { url += `/${keys}`; } - url += `?key=${this.apiKey}`; + url += `?key=${config.apiKey}`; if (!single) { url += `&${objectType}Key=${keys.join(',')}&order=${objectType}KeyList`; } diff --git a/tests/remote_js/helpers2.js b/tests/remote_js/helpers2.js index fc960684..c1cd3931 100644 --- a/tests/remote_js/helpers2.js +++ b/tests/remote_js/helpers2.js @@ -2,6 +2,7 @@ const { JSDOM } = require("jsdom"); const chai = require('chai'); const assert = chai.assert; const crypto = require('crypto'); +const fs = require('fs'); class Helpers2 { static uniqueToken = () => { @@ -19,6 +20,25 @@ class Helpers2 { return result; }; + static md5 = (str) => { + return crypto.createHash('md5').update(str).digest('hex'); + }; + + static md5File = (fileName) => { + const data = fs.readFileSync(fileName); + return crypto.createHash('md5').update(data).digest('hex'); + }; + + static implodeParams = (params, exclude = []) => { + let parts = []; + for (const [key, value] of Object.entries(params)) { + if (!exclude.includes(key)) { + parts.push(key + "=" + encodeURIComponent(value)); + } + } + return parts.join("&"); + }; + static namespaceResolver = (prefix) => { let ns = { atom: 'http://www.w3.org/2005/Atom', diff --git a/tests/remote_js/helpers3.js b/tests/remote_js/helpers3.js index af411759..34531964 100644 --- a/tests/remote_js/helpers3.js +++ b/tests/remote_js/helpers3.js @@ -320,11 +320,12 @@ class Helpers3 { } else { assert.ok(header); - this.assertCount(expected, JSON.parse(Buffer.from(header, 'base64'))); + const headerJSON = JSON.parse(Buffer.from(header, 'base64')); + this.assertCount(expected, headerJSON); } } catch (e) { - console.log("\nHeader: " + Buffer.from(header, 'base64') + "\n"); + console.log("\nHeader: " + JSON.parse(Buffer.from(header, 'base64')) + "\n"); throw e; } } diff --git a/tests/remote_js/httpHandler.js b/tests/remote_js/httpHandler.js index b16dffa5..651ff8d3 100644 --- a/tests/remote_js/httpHandler.js +++ b/tests/remote_js/httpHandler.js @@ -1,5 +1,21 @@ const fetch = require('node-fetch'); var config = require('config'); +const http = require("node:http"); +const https = require("node:https"); + + +// http(s) agents with keepAlive=true used to prevent socket from hanging +// due to bug in node-fetch: https://github.com/node-fetch/node-fetch/issues/1735 +const httpAgent = new http.Agent({ keepAlive: true }); +const httpsAgent = new https.Agent({ keepAlive: true }); +const agentSelector = function (_parsedURL) { + if (_parsedURL.protocol == 'http:') { + return httpAgent; + } + else { + return httpsAgent; + } +}; class HTTP { static verbose = config.verbose; @@ -27,8 +43,9 @@ class HTTP { url = url.replace(localIPRegex, 'localhost'); } - await new Promise(resolve => setTimeout(resolve, 0)); - let response = await fetch(url, options); + // workaround to prevent socket from hanging due to but in node-fetch: https://github.com/node-fetch/node-fetch/issues/1735 + await new Promise(resolve => setTimeout(resolve, 1)); + let response = await fetch(url, Object.assign(options, { agent: agentSelector })); // Fetch doesn't automatically parse the response body, so we have to do that manually diff --git a/tests/remote_js/test/1/collectionTest.js b/tests/remote_js/test/1/collectionTest.js index 8999cdf5..ead30e46 100644 --- a/tests/remote_js/test/1/collectionTest.js +++ b/tests/remote_js/test/1/collectionTest.js @@ -27,13 +27,13 @@ describe('CollectionTests', function () { { "Content-Type": "application/json" } ); - const xml = await API.getXMLFromResponse(response); + const xml = API.getXMLFromResponse(response); Helpers.assertStatusCode(response, 200); const totalResults = Helpers.xpathEval(xml, '//feed/zapi:totalResults'); const numCollections = Helpers.xpathEval(xml, '//feed//entry/zapi:numCollections'); assert.equal(parseInt(totalResults), 1); assert.equal(parseInt(numCollections), 0); - const data = await API.parseDataFromAtomEntry(xml); + const data = API.parseDataFromAtomEntry(xml); const jsonResponse = JSON.parse(data.content); assert.equal(jsonResponse.name, collectionName); return jsonResponse; @@ -65,7 +65,7 @@ describe('CollectionTests', function () { `collections/${parent}?key=${config.apiKey}` ); Helpers.assertStatusCode(response, 200); - xml = await API.getXMLFromResponse(response); + xml = API.getXMLFromResponse(response); assert.equal(parseInt(Helpers.xpathEval(xml, '/atom:entry/zapi:numCollections')), 1); }); @@ -80,9 +80,9 @@ describe('CollectionTests', function () { { "Content-Type": "application/json" } ); Helpers.assertStatusCode(response, 200); - const xml = await API.getXMLFromResponse(response); + const xml = API.getXMLFromResponse(response); assert.equal(parseInt(Helpers.xpathEval(xml, '//feed/zapi:totalResults')), 1); - const data = await API.parseDataFromAtomEntry(xml); + const data = API.parseDataFromAtomEntry(xml); const jsonResponse = JSON.parse(data.content); assert.equal(jsonResponse.name, name); }); @@ -90,7 +90,7 @@ describe('CollectionTests', function () { it('testEditSingleCollection', async function () { API.useAPIVersion(2); const xml = await API.createCollection("Test", false); - const data = await API.parseDataFromAtomEntry(xml); + const data = API.parseDataFromAtomEntry(xml); const key = data.key; API.useAPIVersion(1); @@ -111,8 +111,8 @@ describe('CollectionTests', function () { } ); Helpers.assertStatusCode(response, 200); - const xmlResponse = await API.getXMLFromResponse(response); - const dataResponse = await API.parseDataFromAtomEntry(xmlResponse); + const xmlResponse = API.getXMLFromResponse(response); + const dataResponse = API.parseDataFromAtomEntry(xmlResponse); const jsonResponse = JSON.parse(dataResponse.content); assert.equal(jsonResponse.name, newName); }); diff --git a/tests/remote_js/test/1/itemsTest.js b/tests/remote_js/test/1/itemsTest.js index 0c38019d..9a8d7b7e 100644 --- a/tests/remote_js/test/1/itemsTest.js +++ b/tests/remote_js/test/1/itemsTest.js @@ -5,7 +5,7 @@ const Helpers = require('../../helpers2.js'); const { API1Before, API1After } = require("../shared.js"); describe('ItemTests', function () { - this.timeout(config.timeout); // setting timeout if operations are async and take some time + this.timeout(config.timeout); before(async function () { await API1Before(); diff --git a/tests/remote_js/test/2/atomTest.js b/tests/remote_js/test/2/atomTest.js index b8ef8e09..ce3b3157 100644 --- a/tests/remote_js/test/2/atomTest.js +++ b/tests/remote_js/test/2/atomTest.js @@ -3,7 +3,6 @@ const assert = chai.assert; var config = require('config'); const API = require('../../api2.js'); const Helpers = require('../../helpers2.js'); -const { JSDOM } = require('jsdom'); const { API2Before, API2After } = require("../shared.js"); describe('CollectionTests', function () { @@ -73,7 +72,7 @@ describe('CollectionTests', function () { "items?key=" + config.apiKey ); Helpers.assertStatusCode(response, 200); - const xml = await API.getXMLFromResponse(response); + const xml = API.getXMLFromResponse(response); const links = Helpers.xpathEval(xml, '/atom:feed/atom:link', true, true); assert.equal(config.apiURLPrefix + "users/" + userID + "/items", links[0].getAttribute('href')); @@ -83,13 +82,12 @@ describe('CollectionTests', function () { "items?key=" + config.apiKey + "&order=dateModified&sort=asc" ); Helpers.assertStatusCode(response2, 200); - const xml2 = await API.getXMLFromResponse(response2); + const xml2 = API.getXMLFromResponse(response2); const links2 = Helpers.xpathEval(xml2, '/atom:feed/atom:link', true, true); assert.equal(config.apiURLPrefix + "users/" + userID + "/items?order=dateModified&sort=asc", links2[0].getAttribute('href')); }); - //Requires citation server to run it('testMultiContent', async function () { const keys = Object.keys(keyObj); const keyStr = keys.join(','); @@ -99,7 +97,7 @@ describe('CollectionTests', function () { `items?key=${config.apiKey}&itemKey=${keyStr}&content=bib,json`, ); Helpers.assertStatusCode(response, 200); - const xml = await API.getXMLFromResponse(response); + const xml = API.getXMLFromResponse(response); assert.equal(Helpers.xpathEval(xml, '/atom:feed/zapi:totalResults'), keys.length); const entries = Helpers.xpathEval(xml, '//atom:entry', true, true); @@ -115,9 +113,7 @@ describe('CollectionTests', function () { /"itemKey": "[A-Z0-9]{8}",(\s+)"itemVersion": [0-9]+/, '"itemKey": "",$1"itemVersion": 0', ); - const contentDom = new JSDOM(content); - const expectedDom = new JSDOM(keyObj[key]); - assert.equal(contentDom.window.document.innerHTML, expectedDom.window.document.innerHTML); + Helpers.assertXMLEqual(content, keyObj[key]); } }); }); diff --git a/tests/remote_js/test/2/bibTest.js b/tests/remote_js/test/2/bibTest.js index 70dd940c..bd604f0d 100644 --- a/tests/remote_js/test/2/bibTest.js +++ b/tests/remote_js/test/2/bibTest.js @@ -91,6 +91,7 @@ describe('BibTests', function () { for (let entry of entries) { const key = entry.getElementsByTagName("zapi:key")[0].innerHTML; let content = entry.getElementsByTagName("content")[0].outerHTML; + // Add zapi namespace content = content.replace(' { const name = "Test Collection"; const xml = await API.createCollection(name, false, true, 'atom'); @@ -26,6 +26,11 @@ describe('CollectionTests', function () { const json = JSON.parse(data.content); assert.equal(name, json.name); + return data; + }; + + it('testNewSubcollection', async function () { + const data = await testNewCollection(); const subName = "Test Subcollection"; const parent = data.key; @@ -43,13 +48,13 @@ describe('CollectionTests', function () { `collections/${parent}?key=${config.apiKey}` ); Helpers.assertStatusCode(response, 200); - const xmlRes = await API.getXMLFromResponse(response); + const xmlRes = API.getXMLFromResponse(response); assert.equal(parseInt(Helpers.xpathEval(xmlRes, '/atom:entry/zapi:numCollections')), 1); }); it('testNewMultipleCollections', async function () { const xml = await API.createCollection('Test Collection 1', false, true); - const data = await API.parseDataFromAtomEntry(xml); + const data = API.parseDataFromAtomEntry(xml); const name1 = 'Test Collection 2'; const name2 = 'Test Subcollection'; @@ -75,7 +80,7 @@ describe('CollectionTests', function () { ); Helpers.assertStatusCode(response, 200); - const jsonResponse = await API.getJSONFromResponse(response); + const jsonResponse = API.getJSONFromResponse(response); assert.lengthOf(Object.keys(jsonResponse.success), 2); const xmlResponse = await API.getCollectionXML(Object.keys(jsonResponse.success).map(key => jsonResponse.success[key])); assert.equal(parseInt(Helpers.xpathEval(xmlResponse, '/atom:feed/zapi:totalResults')), 2); @@ -91,10 +96,10 @@ describe('CollectionTests', function () { it('testEditMultipleCollections', async function () { let xml = await API.createCollection("Test 1", false, true, 'atom'); - let data = await API.parseDataFromAtomEntry(xml); + let data = API.parseDataFromAtomEntry(xml); let key1 = data.key; xml = await API.createCollection("Test 2", false, true, 'atom'); - data = await API.parseDataFromAtomEntry(xml); + data = API.parseDataFromAtomEntry(xml); let key2 = data.key; let newName1 = "Test 1 Modified"; @@ -120,7 +125,7 @@ describe('CollectionTests', function () { } ); Helpers.assertStatusCode(response, 200); - let json = await API.getJSONFromResponse(response); + let json = API.getJSONFromResponse(response); assert.lengthOf(Object.keys(json.success), 2); xml = await API.getCollectionXML(Object.keys(json.success).map(key => json.success[key])); @@ -261,13 +266,13 @@ describe('CollectionTests', function () { const collectionKey = await API.createCollection('Test', false, true, 'key'); let xml = await API.createItem("book", { collections: [collectionKey] }, this); - let data = await API.parseDataFromAtomEntry(xml); + let data = API.parseDataFromAtomEntry(xml); let itemKey1 = data.key; let json = JSON.parse(data.content); assert.deepEqual([collectionKey], json.collections); xml = await API.createItem("journalArticle", { collections: [collectionKey] }, true); - data = await API.parseDataFromAtomEntry(xml); + data = API.parseDataFromAtomEntry(xml); let itemKey2 = data.key; json = JSON.parse(data.content); assert.deepEqual([collectionKey], json.collections); diff --git a/tests/remote_js/test/2/fileTest.js b/tests/remote_js/test/2/fileTest.js index 7fd0b6ac..1d2ae771 100644 --- a/tests/remote_js/test/2/fileTest.js +++ b/tests/remote_js/test/2/fileTest.js @@ -7,7 +7,6 @@ const { API2Before, API2After } = require("../shared.js"); const { S3Client, DeleteObjectsCommand, PutObjectCommand } = require("@aws-sdk/client-s3"); const fs = require('fs'); const HTTP = require('../../httpHandler.js'); -const crypto = require('crypto'); const util = require('util'); const exec = util.promisify(require('child_process').exec); @@ -43,38 +42,38 @@ describe('FileTestTests', function () { } }); - const md5 = (str) => { - return crypto.createHash('md5').update(str).digest('hex'); - }; - - const md5File = (fileName) => { - const data = fs.readFileSync(fileName); - return crypto.createHash('md5').update(data).digest('hex'); - }; const testNewEmptyImportedFileAttachmentItem = async () => { let xml = await API.createAttachmentItem("imported_file", [], false, this); - let data = await API.parseDataFromAtomEntry(xml); + let data = API.parseDataFromAtomEntry(xml); return data; }; const testGetFile = async () => { const addFileData = await testAddFileExisting(); + + // Get in view mode const userGetViewModeResponse = await API.userGet(config.userID, `items/${addFileData.key}/file/view?key=${config.apiKey}`); Helpers.assert302(userGetViewModeResponse); const location = userGetViewModeResponse.headers.location[0]; Helpers.assertRegExp(/^https?:\/\/[^/]+\/[a-zA-Z0-9%]+\/[a-f0-9]{64}\/test_/, location); const filenameEncoded = encodeURIComponent(addFileData.filename); assert.equal(filenameEncoded, location.substring(location.length - filenameEncoded.length)); + + // Get from view mode const viewModeResponse = await HTTP.get(location); Helpers.assert200(viewModeResponse); - assert.equal(addFileData.md5, md5(viewModeResponse.data)); + assert.equal(addFileData.md5, Helpers.md5(viewModeResponse.data)); + + // Get in download mode const userGetDownloadModeResponse = await API.userGet(config.userID, `items/${addFileData.key}/file?key=${config.apiKey}`); Helpers.assert302(userGetDownloadModeResponse); const downloadModeLocation = userGetDownloadModeResponse.headers.location[0]; + + // Get from S3 const s3Response = await HTTP.get(downloadModeLocation); Helpers.assert200(s3Response); - assert.equal(addFileData.md5, md5(s3Response.data)); + assert.equal(addFileData.md5, Helpers.md5(s3Response.data)); return { key: addFileData.key, response: s3Response @@ -83,12 +82,12 @@ describe('FileTestTests', function () { it('testAddFileLinkedAttachment', async function () { let xml = await API.createAttachmentItem("linked_file", [], false, this); - let data = await API.parseDataFromAtomEntry(xml); + let data = API.parseDataFromAtomEntry(xml); let file = "./work/file"; let fileContents = getRandomUnicodeString(); fs.writeFileSync(file, fileContents); - let hash = md5File(file); + let hash = Helpers.md5File(file); let filename = "test_" + fileContents; let mtime = fs.statSync(file).mtimeMs; let size = fs.statSync(file).size; @@ -99,7 +98,7 @@ describe('FileTestTests', function () { let response = await API.userPost( config.userID, `items/${data.key}/file?key=${config.apiKey}`, - implodeParams({ + Helpers.implodeParams({ md5: hash, filename: filename, filesize: size, @@ -119,24 +118,25 @@ describe('FileTestTests', function () { it('testAddFileFullParams', async function () { let xml = await API.createAttachmentItem("imported_file", [], false, this); - let data = await API.parseDataFromAtomEntry(xml); + let data = API.parseDataFromAtomEntry(xml); let serverDateModified = Helpers.xpathEval(xml, '//atom:entry/atom:updated'); await new Promise(r => setTimeout(r, 2000)); let originalVersion = data.version; let file = "./work/file"; let fileContents = getRandomUnicodeString(); await fs.promises.writeFile(file, fileContents); - let hash = md5File(file); + let hash = Helpers.md5File(file); let filename = "test_" + fileContents; let mtime = parseInt((await fs.promises.stat(file)).mtimeMs); let size = parseInt((await fs.promises.stat(file)).size); let contentType = "text/plain"; let charset = "utf-8"; + // Get upload authorization let response = await API.userPost( config.userID, `items/${data.key}/file?key=${config.apiKey}`, - implodeParams({ + Helpers.implodeParams({ md5: hash, filename: filename, filesize: size, @@ -156,13 +156,16 @@ describe('FileTestTests', function () { assert.ok(json); toDelete.push(hash); - let boundary = "---------------------------" + md5(Helpers.uniqueID()); + // Generate form-data -- taken from S3::getUploadPostData() + let boundary = "---------------------------" + Helpers.md5(Helpers.uniqueID()); let prefix = ""; for (let key in json.params) { prefix += "--" + boundary + "\r\nContent-Disposition: form-data; name=\"" + key + "\"\r\n\r\n" + json.params[key] + "\r\n"; } prefix += "--" + boundary + "\r\nContent-Disposition: form-data; name=\"file\"\r\n\r\n"; let suffix = "\r\n--" + boundary + "--"; + + // Upload to S3 response = await HTTP.post( json.url, prefix + fileContents + suffix, @@ -172,6 +175,7 @@ describe('FileTestTests', function () { ); Helpers.assert201(response); + // Register upload response = await API.userPost( config.userID, `items/${data.key}/file?key=${config.apiKey}`, @@ -182,12 +186,14 @@ describe('FileTestTests', function () { } ); Helpers.assert204(response); + + // Verify attachment item metadata response = await API.userGet( config.userID, `items/${data.key}?key=${config.apiKey}&content=json` ); xml = API.getXMLFromResponse(response); - data = await API.parseDataFromAtomEntry(xml); + data = API.parseDataFromAtomEntry(xml); json = JSON.parse(data.content); assert.equal(hash, json.md5); assert.equal(filename, json.filename); @@ -195,7 +201,10 @@ describe('FileTestTests', function () { assert.equal(contentType, json.contentType); assert.equal(charset, json.charset); const updated = Helpers.xpathEval(xml, '/atom:entry/atom:updated'); + + // Make sure serverDateModified has changed assert.notEqual(serverDateModified, updated); + // Make sure version has changed assert.notEqual(originalVersion, data.version); }); @@ -205,22 +214,23 @@ describe('FileTestTests', function () { it('testExistingFileWithOldStyleFilename', async function () { let fileContents = getRandomUnicodeString(); - let hash = md5(fileContents); + let hash = Helpers.md5(fileContents); let filename = 'test.txt'; let size = fileContents.length; let parentKey = await API.createItem("book", false, this, 'key'); let xml = await API.createAttachmentItem("imported_file", [], parentKey, this); - let data = await API.parseDataFromAtomEntry(xml); + let data = API.parseDataFromAtomEntry(xml); let key = data.key; let mtime = Date.now(); let contentType = 'text/plain'; let charset = 'utf-8'; + // Get upload authorization let response = await API.userPost( config.userID, `items/${data.key}/file?key=${config.apiKey}`, - implodeParams({ + Helpers.implodeParams({ md5: hash, filename, filesize: size, @@ -238,6 +248,7 @@ describe('FileTestTests', function () { let json = JSON.parse(response.data); assert.isOk(json); + // Upload to old-style location toDelete.push(`${hash}/${filename}`); toDelete.push(hash); const putCommand = new PutObjectCommand({ @@ -246,6 +257,8 @@ describe('FileTestTests', function () { Body: fileContents }); await s3Client.send(putCommand); + + // Register upload response = await API.userPost( config.userID, `items/${key}/file?key=${config.apiKey}`, @@ -257,24 +270,28 @@ describe('FileTestTests', function () { ); Helpers.assert204(response); + // The file should be accessible on the item at the old-style location response = await API.userGet( config.userID, `items/${key}/file?key=${config.apiKey}` ); Helpers.assert302(response); let location = response.headers.location[0]; + // bucket.s3.amazonaws.com or s3.amazonaws.com/bucket let matches = location.match(/^https:\/\/(?:[^/]+|.+config.s3Bucket)\/([a-f0-9]{32})\/test.txt\?/); Helpers.assertEquals(2, matches.length); Helpers.assertEquals(hash, matches[1]); + // Get upload authorization for the same file and filename on another item, which should + // result in 'exists', even though we uploaded to the old-style location parentKey = await API.createItem("book", false, this, 'key'); xml = await API.createAttachmentItem("imported_file", [], parentKey, this); - data = await API.parseDataFromAtomEntry(xml); + data = API.parseDataFromAtomEntry(xml); key = data.key; response = await API.userPost( config.userID, `items/${key}/file?key=${config.apiKey}`, - implodeParams({ + Helpers.implodeParams({ md5: hash, filename, filesize: size, @@ -292,6 +309,7 @@ describe('FileTestTests', function () { assert.isOk(postJSON); Helpers.assertEquals(1, postJSON.exists); + // Get in download mode response = await API.userGet( config.userID, `items/${key}/file?key=${config.apiKey}` @@ -302,20 +320,24 @@ describe('FileTestTests', function () { Helpers.assertEquals(2, matches.length); Helpers.assertEquals(hash, matches[1]); + // Get from S3 response = await HTTP.get(location); Helpers.assert200(response); Helpers.assertEquals(fileContents, response.data); Helpers.assertEquals(`${contentType}; charset=${charset}`, response.headers['content-type'][0]); + // Get upload authorization for the same file and different filename on another item, + // which should result in 'exists' and a copy of the file to the hash-only location parentKey = await API.createItem("book", false, this, 'key'); xml = await API.createAttachmentItem("imported_file", [], parentKey, this); - data = await API.parseDataFromAtomEntry(xml); + data = API.parseDataFromAtomEntry(xml); key = data.key; + // Also use a different content type contentType = 'application/x-custom'; response = await API.userPost( config.userID, `items/${key}/file?key=${config.apiKey}`, - implodeParams({ + Helpers.implodeParams({ md5: hash, filename: "test2.txt", filesize: size, @@ -332,6 +354,7 @@ describe('FileTestTests', function () { assert.isOk(postJSON); Helpers.assertEquals(1, postJSON.exists); + // Get in download mode response = await API.userGet( config.userID, `items/${key}/file?key=${config.apiKey}` @@ -342,6 +365,7 @@ describe('FileTestTests', function () { Helpers.assertEquals(2, matches.length); Helpers.assertEquals(hash, matches[1]); + // Get from S3 response = await HTTP.get(location); Helpers.assert200(response); Helpers.assertEquals(fileContents, response.data); @@ -350,24 +374,25 @@ describe('FileTestTests', function () { const testAddFileFull = async () => { let xml = await API.createItem("book", false, this); - let data = await API.parseDataFromAtomEntry(xml); + let data = API.parseDataFromAtomEntry(xml); let parentKey = data.key; xml = await API.createAttachmentItem("imported_file", [], parentKey, this); - data = await API.parseDataFromAtomEntry(xml); + data = API.parseDataFromAtomEntry(xml); let file = "./work/file"; let fileContents = getRandomUnicodeString(); fs.writeFileSync(file, fileContents); - let hash = md5File(file); + let hash = Helpers.md5File(file); let filename = "test_" + fileContents; let mtime = fs.statSync(file).mtime * 1000; let size = fs.statSync(file).size; let contentType = "text/plain"; let charset = "utf-8"; + // Get upload authorization let response = await API.userPost( config.userID, `items/${data.key}/file?key=${config.apiKey}`, - implodeParams({ + Helpers.implodeParams({ md5: hash, filename: filename, filesize: size, @@ -384,8 +409,9 @@ describe('FileTestTests', function () { Helpers.assertContentType(response, "application/json"); let json = JSON.parse(response.data); assert.isOk(json); - toDelete.push(`${hash}`); + toDelete.push(hash); + // Upload to S3 response = await HTTP.post( json.url, json.prefix + fileContents + json.suffix, @@ -395,6 +421,9 @@ describe('FileTestTests', function () { ); Helpers.assert201(response); + // Register upload + + // No If-None-Match response = await API.userPost( config.userID, `items/${data.key}/file?key=${config.apiKey}`, @@ -405,6 +434,7 @@ describe('FileTestTests', function () { ); Helpers.assert428(response); + // Invalid upload key response = await API.userPost( config.userID, `items/${data.key}/file?key=${config.apiKey}`, @@ -427,12 +457,13 @@ describe('FileTestTests', function () { ); Helpers.assert204(response); + // Verify attachment item metadata response = await API.userGet( config.userID, `items/${data.key}?key=${config.apiKey}&content=json` ); - xml = await API.getXMLFromResponse(response); - data = await API.parseDataFromAtomEntry(xml); + xml = API.getXMLFromResponse(response); + data = API.parseDataFromAtomEntry(xml); json = JSON.parse(data.content); assert.equal(hash, json.md5); @@ -451,28 +482,27 @@ describe('FileTestTests', function () { it('testAddFileAuthorizationErrors', async function () { const data = await testNewEmptyImportedFileAttachmentItem(); const fileContents = getRandomUnicodeString(); - const hash = md5(fileContents); + const hash = Helpers.md5(fileContents); const mtime = Date.now(); const size = fileContents.length; const filename = `test_${fileContents}`; const fileParams = { md5: hash, - filename, + filename: filename, filesize: size, - mtime, + mtime: mtime, contentType: "text/plain", charset: "utf-8" }; // Check required params const requiredParams = ["md5", "filename", "filesize", "mtime"]; - for (let i = 0; i < requiredParams.length; i++) { - const exclude = requiredParams[i]; + for (let exclude of requiredParams) { const response = await API.userPost( config.userID, `items/${data.key}/file?key=${config.apiKey}`, - implodeParams(fileParams, [exclude]), + Helpers.implodeParams(fileParams, [exclude]), { "Content-Type": "application/x-www-form-urlencoded", "If-None-Match": "*" @@ -482,10 +512,10 @@ describe('FileTestTests', function () { // Seconds-based mtime const fileParams2 = { ...fileParams, mtime: Math.round(mtime / 1000) }; - const _ = await API.userPost( + await API.userPost( config.userID, `items/${data.key}/file?key=${config.apiKey}`, - implodeParams(fileParams2), + Helpers.implodeParams(fileParams2), { "Content-Type": "application/x-www-form-urlencoded", "If-None-Match": "*" @@ -498,10 +528,10 @@ describe('FileTestTests', function () { const response3 = await API.userPost( config.userID, `items/${data.key}/file?key=${config.apiKey}`, - implodeParams(fileParams), + Helpers.implodeParams(fileParams), { "Content-Type": "application/x-www-form-urlencoded", - "If-Match": md5("invalidETag") + "If-Match": Helpers.md5("invalidETag") }); Helpers.assert412(response3); @@ -509,7 +539,7 @@ describe('FileTestTests', function () { const response4 = await API.userPost( config.userID, `items/${data.key}/file?key=${config.apiKey}`, - implodeParams(fileParams), + Helpers.implodeParams(fileParams), { "Content-Type": "application/x-www-form-urlencoded" }); @@ -519,7 +549,7 @@ describe('FileTestTests', function () { const response5 = await API.userPost( config.userID, `items/${data.key}/file?key=${config.apiKey}`, - implodeParams(fileParams), + Helpers.implodeParams(fileParams), { "Content-Type": "application/x-www-form-urlencoded", "If-None-Match": "invalidETag" @@ -527,15 +557,6 @@ describe('FileTestTests', function () { Helpers.assert400(response5); }); - const implodeParams = (params, exclude = []) => { - let parts = []; - for (const [key, value] of Object.entries(params)) { - if (!exclude.includes(key)) { - parts.push(key + "=" + encodeURIComponent(value)); - } - } - return parts.join("&"); - }; it('testAddFilePartial', async function () { const getFileData = await testGetFile(); @@ -543,7 +564,7 @@ describe('FileTestTests', function () { config.userID, `items/${getFileData.key}?key=${config.apiKey}&content=json` ); - const xml = await API.getXMLFromResponse(response); + const xml = API.getXMLFromResponse(response); await new Promise(resolve => setTimeout(resolve, 1000)); @@ -564,8 +585,11 @@ describe('FileTestTests', function () { }; for (let [algo, cmd] of Object.entries(algorithms)) { + // Create random contents fs.writeFileSync(newFilename, getRandomUnicodeString() + Helpers.uniqueID()); - const newHash = md5File(newFilename); + const newHash = Helpers.md5File(newFilename); + + // Get upload authorization const fileParams = { md5: newHash, filename: `test_${fileContents}`, @@ -578,10 +602,10 @@ describe('FileTestTests', function () { const postResponse = await API.userPost( config.userID, `items/${getFileData.key}/file?key=${config.apiKey}`, - implodeParams(fileParams), + Helpers.implodeParams(fileParams), { "Content-Type": "application/x-www-form-urlencoded", - "If-Match": md5File(oldFilename), + "If-Match": Helpers.md5File(oldFilename), } ); Helpers.assert200(postResponse); @@ -600,18 +624,21 @@ describe('FileTestTests', function () { toDelete.push(newHash); + // Upload patch file let response = await API.userPatch( config.userID, `items/${getFileData.key}/file?key=${config.apiKey}&algorithm=${algo}&upload=${json.uploadKey}`, patch, { - "If-Match": md5File(oldFilename), + "If-Match": Helpers.md5File(oldFilename), } ); Helpers.assert204(response); - fs.unlinkSync(patchFilename); + fs.rm(patchFilename, (_) => {}); fs.renameSync(newFilename, oldFilename); + + // Verify attachment item metadata response = await API.userGet( config.userID, `items/${getFileData.key}?key=${config.apiKey}&content=json` @@ -623,8 +650,11 @@ describe('FileTestTests', function () { Helpers.assertEquals(fileParams.mtime, json.mtime); Helpers.assertEquals(fileParams.contentType, json.contentType); Helpers.assertEquals(fileParams.charset, json.charset); + + // Make sure version has changed assert.notEqual(originalVersion, data.version); + // Verify file in S3 const fileResponse = await API.userGet( config.userID, `items/${getFileData.key}/file?key=${config.apiKey}` @@ -634,7 +664,7 @@ describe('FileTestTests', function () { const getFileResponse = await HTTP.get(location); Helpers.assert200(getFileResponse); - Helpers.assertEquals(fileParams.md5, md5(getFileResponse.data)); + Helpers.assertEquals(fileParams.md5, Helpers.md5(getFileResponse.data)); Helpers.assertEquals( `${fileParams.contentType}${fileParams.contentType && fileParams.charset ? `; charset=${fileParams.charset}` : "" }`, @@ -654,7 +684,7 @@ describe('FileTestTests', function () { let response = await API.userPost( config.userID, `items/${key}/file?key=${config.apiKey}`, - implodeParams({ + Helpers.implodeParams({ md5: json.md5, filename: json.filename, filesize: size, @@ -676,7 +706,7 @@ describe('FileTestTests', function () { response = await API.userPost( config.userID, `items/${key}/file?key=${config.apiKey}`, - implodeParams({ + Helpers.implodeParams({ md5: json.md5, filename: json.filename + "等", // Unicode 1.1 character, to test signature generation filesize: size, diff --git a/tests/remote_js/test/2/fullText.js b/tests/remote_js/test/2/fullText.js index 1f9cb9ed..cf7852ea 100644 --- a/tests/remote_js/test/2/fullText.js +++ b/tests/remote_js/test/2/fullText.js @@ -115,7 +115,7 @@ describe('FullTextTests', function () { // Store content for one item let key = await API.createItem("book", false, true, 'key'); let xml = await API.createAttachmentItem("imported_url", [], key, true, 'atom'); - let data = await API.parseDataFromAtomEntry(xml); + let data = API.parseDataFromAtomEntry(xml); let key1 = data.key; let content = "Here is some full-text content"; @@ -135,7 +135,7 @@ describe('FullTextTests', function () { // And another key = await API.createItem("book", false, true, 'key'); xml = await API.createAttachmentItem("imported_url", [], key, true, 'atom'); - data = await API.parseDataFromAtomEntry(xml); + data = API.parseDataFromAtomEntry(xml); let key2 = data.key; response = await API.userPut( diff --git a/tests/remote_js/test/2/generalTest.js b/tests/remote_js/test/2/generalTest.js index 71869bff..bf8dca13 100644 --- a/tests/remote_js/test/2/generalTest.js +++ b/tests/remote_js/test/2/generalTest.js @@ -33,7 +33,7 @@ describe('GeneralTests', function () { ] }; const xml = await API.createItem("book", data, this, 'atom'); - const parsedData = await API.parseDataFromAtomEntry(xml); + const parsedData = API.parseDataFromAtomEntry(xml); const json = JSON.parse(parsedData.content); assert.equal("AA", json.title); assert.equal("BB", json.creators[0].name); diff --git a/tests/remote_js/test/2/groupTest.js b/tests/remote_js/test/2/groupTest.js index f17ccf8a..f6bc5775 100644 --- a/tests/remote_js/test/2/groupTest.js +++ b/tests/remote_js/test/2/groupTest.js @@ -18,6 +18,9 @@ describe('GroupTests', function () { await API2After(); }); + /** + * Changing a group's metadata should change its ETag + */ it('testUpdateMetadata', async function () { const response = await API.userGet( config.userID, diff --git a/tests/remote_js/test/2/itemsTest.js b/tests/remote_js/test/2/itemsTest.js index efe37fb8..d8f7309d 100644 --- a/tests/remote_js/test/2/itemsTest.js +++ b/tests/remote_js/test/2/itemsTest.js @@ -39,7 +39,7 @@ describe('ItemsTests', function () { const response = await API.postItems(data); Helpers.assertStatusCode(response, 200); - const jsonResponse = await API.getJSONFromResponse(response); + const jsonResponse = API.getJSONFromResponse(response); const successArray = Object.keys(jsonResponse.success).map(key => jsonResponse.success[key]); const xml = await API.getItemXML(successArray, true); const contents = Helpers.xpathEval(xml, '/atom:feed/atom:entry/atom:content', false, true); @@ -76,10 +76,8 @@ describe('ItemsTests', function () { `items/${key}?key=${config.apiKey}`, JSON.stringify(newBookItem), { - headers: { - 'Content-Type': 'application/json', - 'If-Unmodified-Since-Version': version - } + 'Content-Type': 'application/json', + 'If-Unmodified-Since-Version': version } ); Helpers.assertStatusCode(response, 204); @@ -211,7 +209,7 @@ describe('ItemsTests', function () { const key = API.getFirstSuccessKeyFromResponse(response); const xml = await API.getItemXML(key, true); - const data = await API.parseDataFromAtomEntry(xml); + const data = API.parseDataFromAtomEntry(xml); const version = data.version; const json1 = JSON.parse(data.content); @@ -235,7 +233,7 @@ describe('ItemsTests', function () { Helpers.assertStatusCode(response2, 204); const xml2 = await API.getItemXML(key); - const data2 = await API.parseDataFromAtomEntry(xml2); + const data2 = API.parseDataFromAtomEntry(xml2); const json3 = JSON.parse(data2.content); assert.equal(json3.itemType, "bookSection"); @@ -316,7 +314,7 @@ describe('ItemsTests', function () { it('testNewComputerProgramItem', async function () { const xml = await API.createItem('computerProgram', false, true); - const data = await API.parseDataFromAtomEntry(xml); + const data = API.parseDataFromAtomEntry(xml); const key = data.key; const json = JSON.parse(data.content); assert.equal(json.itemType, 'computerProgram'); @@ -333,10 +331,11 @@ describe('ItemsTests', function () { Helpers.assertStatusCode(response, 204); const xml2 = await API.getItemXML(key); - const data2 = await API.parseDataFromAtomEntry(xml2); + const data2 = API.parseDataFromAtomEntry(xml2); const json2 = JSON.parse(data2.content); assert.equal(json2.version, version); + // 'versionNumber' from v3 should work too delete json2.version; const version2 = '1.1'; json2.versionNumber = version2; @@ -349,7 +348,7 @@ describe('ItemsTests', function () { Helpers.assertStatusCode(response2, 204); const xml3 = await API.getItemXML(key); - const data3 = await API.parseDataFromAtomEntry(xml3); + const data3 = API.parseDataFromAtomEntry(xml3); const json3 = JSON.parse(data3.content); assert.equal(json3.version, version2); }); @@ -510,8 +509,9 @@ describe('ItemsTests', function () { Helpers.assertStatusCode(userPostResponse, 200); }); - it('testNewInvalidTopLevelAttachment', async function() { - this.skip(); //disabled + //Disabled -- see note at Zotero_Item::checkTopLevelAttachment() + it('testNewInvalidTopLevelAttachment', async function () { + this.skip(); }); it('testNewEmptyLinkAttachmentItemWithItemKey', async function () { @@ -560,7 +560,7 @@ describe('ItemsTests', function () { Helpers.assertStatusCode(response, 204); const xml = await API.getItemXML(key); - const data = await API.parseDataFromAtomEntry(xml); + const data = API.parseDataFromAtomEntry(xml); // Item Shouldn't be changed assert.equal(version, data.version); }); @@ -568,7 +568,7 @@ describe('ItemsTests', function () { const testEditEmptyLinkAttachmentItem = async () => { const key = await API.createItem('book', false, true, 'key'); const xml = await API.createAttachmentItem('linked_url', [], key, true, 'atom'); - const data = await API.parseDataFromAtomEntry(xml); + const data = API.parseDataFromAtomEntry(xml); const updatedKey = data.key; const version = data.version; @@ -586,7 +586,7 @@ describe('ItemsTests', function () { Helpers.assertStatusCode(response, 204); const newXml = await API.getItemXML(updatedKey); - const newData = await API.parseDataFromAtomEntry(newXml); + const newData = API.parseDataFromAtomEntry(newXml); // Item shouldn't change assert.equal(version, newData.version); return newData; @@ -1046,7 +1046,7 @@ describe('ItemsTests', function () { config.userID, `items/${key}?key=${config.apiKey}&content=json` ); - const xmlResponse = await API.getXMLFromResponse(response); + const xmlResponse = API.getXMLFromResponse(response); const dataResponse = API.parseDataFromAtomEntry(xmlResponse); const json = JSON.parse(dataResponse.content); assert.equal(date, json.date); @@ -1058,7 +1058,7 @@ describe('ItemsTests', function () { const title = "Tést"; const xml = await API.createItem("book", { title }, true); - const data = await API.parseDataFromAtomEntry(xml); + const data = API.parseDataFromAtomEntry(xml); const key = data.key; // Test entry @@ -1066,7 +1066,7 @@ describe('ItemsTests', function () { config.userID, `items/${key}?key=${config.apiKey}&content=json` ); - let xmlResponse = await API.getXMLFromResponse(response); + let xmlResponse = API.getXMLFromResponse(response); assert.equal(xmlResponse.getElementsByTagName("title")[0].innerHTML, "Tést"); // Test feed @@ -1074,7 +1074,7 @@ describe('ItemsTests', function () { config.userID, `items?key=${config.apiKey}&content=json` ); - xmlResponse = await API.getXMLFromResponse(response); + xmlResponse = API.getXMLFromResponse(response); let titleFound = false; for (var node of xmlResponse.getElementsByTagName("title")) { diff --git a/tests/remote_js/test/2/noteTest.js b/tests/remote_js/test/2/noteTest.js index 992f9114..74ce90cb 100644 --- a/tests/remote_js/test/2/noteTest.js +++ b/tests/remote_js/test/2/noteTest.js @@ -1,5 +1,3 @@ -const chai = require('chai'); -const assert = chai.assert; var config = require('config'); const API = require('../../api2.js'); const Helpers = require("../../helpers2.js"); @@ -31,9 +29,7 @@ describe('NoteTests', function () { JSON.stringify({ items: [json] }), - { - headers: { "Content-Type": "application/json" } - } + { "Content-Type": "application/json" } ); const expectedMessage = "Note '1234567890123456789012345678901234567890123456789012345678901234567890123456789...' too long"; Helpers.assertStatusForObject(response, 'failed', 0, 413, expectedMessage); @@ -86,6 +82,7 @@ describe('NoteTests', function () { Helpers.assertStatusForObject(response, 'failed', 0, 413, expectedMessage); }); + // All content within HTML tags it('testNoteTooLongWithinHTMLTags', async function () { json.note = " \n

"; diff --git a/tests/remote_js/test/2/objectTest.js b/tests/remote_js/test/2/objectTest.js index 38888cf8..99baa38e 100644 --- a/tests/remote_js/test/2/objectTest.js +++ b/tests/remote_js/test/2/objectTest.js @@ -16,6 +16,10 @@ describe('ObjectTests', function () { await API2After(); }); + beforeEach(async function() { + await API.userClear(config.userID); + }); + const _testMultiObjectGet = async (objectType = 'collection') => { const objectNamePlural = API.getPluralObjectType(objectType); const keyProp = `${objectType}Key`; @@ -92,7 +96,6 @@ describe('ObjectTests', function () { }; const _testMultiObjectDelete = async (objectType) => { - await API.userClear(config.userID); const objectTypePlural = await API.getPluralObjectType(objectType); const keyProp = `${objectType}Key`; @@ -135,6 +138,7 @@ describe('ObjectTests', function () { response = await API.userGet(config.userID, `${objectTypePlural}?key=${config.apiKey}&${keyProp}=${keepKeys.join(',')}`); Helpers.assertNumResults(response, keepKeys.length); + // Add trailing comma to itemKey param, to test key parsing response = await API.userDelete(config.userID, `${objectTypePlural}?key=${config.apiKey}&${keyProp}=${keepKeys.join(',')},`, { "If-Unmodified-Since-Version": libraryVersion }); @@ -144,11 +148,10 @@ describe('ObjectTests', function () { Helpers.assertNumResults(response, 0); }; - const _testPartialWriteFailure = async () => { + const _testPartialWriteFailure = async (objectType) => { await API.userClear(config.userID); let json; let conditions = []; - const objectType = 'collection'; let json1 = { name: "Test" }; let json2 = { name: "1234567890".repeat(6554) }; let json3 = { name: "Test" }; @@ -187,7 +190,7 @@ describe('ObjectTests', function () { { "Content-Type": "application/json" }); Helpers.assertStatusCode(response, 200); - json = await API.getJSONFromResponse(response); + json = API.getJSONFromResponse(response); Helpers.assertStatusForObject(response, 'success', 0, 200); Helpers.assertStatusForObject(response, 'success', 1, 413); @@ -211,16 +214,11 @@ describe('ObjectTests', function () { await API.userClear(config.userID); let objectTypePlural = API.getPluralObjectType(objectType); - let objectData; - let objectDataContent; - let json1; - let json2; - let json3; + let json1, json2, json3, objectData, objectDataContent; let conditions = []; switch (objectType) { case 'collection': - await new Promise(r => setTimeout(r, 1000)); objectData = await API.createCollection('Test', false, true, 'data'); objectDataContent = objectData.content; json1 = JSON.parse(objectDataContent); @@ -371,7 +369,7 @@ describe('ObjectTests', function () { let response = await API.userGet(config.userID, "items?key=" + config.apiKey + "&format=keys&limit=1"); let libraryVersion1 = response.headers["last-modified-version"][0]; - const func = async (objectType, libraryVersion, url) => { + const testDelete = async (objectType, libraryVersion, url) => { const objectTypePlural = await API.getPluralObjectType(objectType); const response = await API.userDelete(config.userID, `${objectTypePlural}?key=${config.apiKey}${url}`, @@ -380,15 +378,16 @@ describe('ObjectTests', function () { return response.headers["last-modified-version"][0]; }; - let tempLibraryVersion = await func('collection', libraryVersion1, "&collectionKey=" + objectKeys.collection[0]); - tempLibraryVersion = await func('item', tempLibraryVersion, "&itemKey=" + objectKeys.item[0]); - tempLibraryVersion = await func('search', tempLibraryVersion, "&searchKey=" + objectKeys.search[0]); + // Delete first object + let tempLibraryVersion = await testDelete('collection', libraryVersion1, "&collectionKey=" + objectKeys.collection[0]); + tempLibraryVersion = await testDelete('item', tempLibraryVersion, "&itemKey=" + objectKeys.item[0]); + tempLibraryVersion = await testDelete('search', tempLibraryVersion, "&searchKey=" + objectKeys.search[0]); let libraryVersion2 = tempLibraryVersion; // Delete second and third objects - tempLibraryVersion = await func('collection', tempLibraryVersion, "&collectionKey=" + objectKeys.collection.slice(1).join(',')); - tempLibraryVersion = await func('item', tempLibraryVersion, "&itemKey=" + objectKeys.item.slice(1).join(',')); - let libraryVersion3 = await func('search', tempLibraryVersion, "&searchKey=" + objectKeys.search.slice(1).join(',')); + tempLibraryVersion = await testDelete('collection', tempLibraryVersion, "&collectionKey=" + objectKeys.collection.slice(1).join(',')); + tempLibraryVersion = await testDelete('item', tempLibraryVersion, "&itemKey=" + objectKeys.item.slice(1).join(',')); + let libraryVersion3 = await testDelete('search', tempLibraryVersion, "&searchKey=" + objectKeys.search.slice(1).join(',')); // Request all deleted objects response = await API.userGet(config.userID, "deleted?key=" + config.apiKey + "&newer=" + libraryVersion1); diff --git a/tests/remote_js/test/2/paramTest.js b/tests/remote_js/test/2/paramTest.js index af3a00eb..062c1415 100644 --- a/tests/remote_js/test/2/paramTest.js +++ b/tests/remote_js/test/2/paramTest.js @@ -273,6 +273,7 @@ describe('ParametersTests', function () { date: 'November 25, 2012' }, true, 'key')); + // Search for one by title response = await API.userGet( config.userID, `items?key=${config.apiKey}&content=json&q=${encodeURIComponent(title1)}` @@ -284,6 +285,7 @@ describe('ParametersTests', function () { xpath = Helpers.xpathEval(xml, '//atom:entry/zapi:key', false, true); assert.equal(keys[0], xpath[0]); + // Search by both by title, date asc response = await API.userGet( config.userID, `items?key=${config.apiKey}&content=json&q=title&order=date&sort=asc` @@ -296,6 +298,7 @@ describe('ParametersTests', function () { assert.equal(keys[1], xpath[0]); assert.equal(keys[0], xpath[1]); + // Search by both by title, date desc response = await API.userGet( config.userID, `items?key=${config.apiKey}&content=json&q=title&order=date&sort=desc` diff --git a/tests/remote_js/test/2/permissionsTest.js b/tests/remote_js/test/2/permissionsTest.js index 27798eab..e87e2f2a 100644 --- a/tests/remote_js/test/2/permissionsTest.js +++ b/tests/remote_js/test/2/permissionsTest.js @@ -28,8 +28,12 @@ describe('PermissionsTests', function () { Helpers.assertTotalResults(response, config.numPublicGroups); }); + /** + * A key without note access shouldn't be able to create a note. + * Disabled + */ it('testKeyNoteAccessWriteError', async function() { - this.skip(); //disabled + this.skip(); }); it('testUserGroupsOwned', async function () { @@ -87,14 +91,14 @@ describe('PermissionsTests', function () { const makeNoteItem = async (text) => { const xml = await API.createNoteItem(text, false, true); - const data = await API.parseDataFromAtomEntry(xml); + const data = API.parseDataFromAtomEntry(xml); keys.push(data.key); topLevelKeys.push(data.key); }; const makeBookItem = async (title) => { let xml = await API.createItem('book', { title: title }, true); - let data = await API.parseDataFromAtomEntry(xml); + let data = API.parseDataFromAtomEntry(xml); keys.push(data.key); topLevelKeys.push(data.key); bookKeys.push(data.key); @@ -111,7 +115,7 @@ describe('PermissionsTests', function () { const lastKey = await makeBookItem("F"); let xml = await API.createNoteItem("

G

", lastKey, true); - let data = await API.parseDataFromAtomEntry(xml); + let data = API.parseDataFromAtomEntry(xml); keys.push(data.key); // Create collection and add items to it diff --git a/tests/remote_js/test/2/relationsTest.js b/tests/remote_js/test/2/relationsTest.js index ba39f3ae..de0b2676 100644 --- a/tests/remote_js/test/2/relationsTest.js +++ b/tests/remote_js/test/2/relationsTest.js @@ -92,6 +92,7 @@ describe('RelationsTests', function () { assert.equal(parseInt(item2JSON2.itemVersion), response2.headers["last-modified-version"][0]); }); + // Same as above, but in a single request it('testRelatedItemRelationsSingleRequest', async function () { const uriPrefix = "http://zotero.org/users/" + config.userID + "/items/"; const item1Key = Helpers.uniqueID(); @@ -179,7 +180,7 @@ describe('RelationsTests', function () { // Make sure it's gone const xml = await API.getItemXML(data.key); - const itemData = await API.parseDataFromAtomEntry(xml); + const itemData = API.parseDataFromAtomEntry(xml); json = JSON.parse(itemData.content); assert.equal(Object.keys(relations).length, Object.keys(json.relations).length); for (const [predicate, object] of Object.entries(relations)) { @@ -197,11 +198,14 @@ describe('RelationsTests', function () { // Make sure they're gone const xmlAfterDelete = await API.getItemXML(data.key); - const itemDataAfterDelete = await API.parseDataFromAtomEntry(xmlAfterDelete); + const itemDataAfterDelete = API.parseDataFromAtomEntry(xmlAfterDelete); const responseDataAfterDelete = JSON.parse(itemDataAfterDelete.content); assert.lengthOf(Object.keys(responseDataAfterDelete.relations), 0); }); + // + // Collections + // it('testNewCollectionRelations', async function () { const relationsObj = { "owl:sameAs": "http://zotero.org/groups/1/collections/AAAAAAAA" diff --git a/tests/remote_js/test/2/settingsTest.js b/tests/remote_js/test/2/settingsTest.js index 9faa4886..d7d0bd05 100644 --- a/tests/remote_js/test/2/settingsTest.js +++ b/tests/remote_js/test/2/settingsTest.js @@ -98,7 +98,6 @@ describe('SettingsTests', function () { }); it('testAddUserSettingMultiple', async function () { - await API.userClear(config.userID); const settingKey = 'tagColors'; const val = [ { @@ -107,6 +106,7 @@ describe('SettingsTests', function () { }, ]; + // TODO: multiple, once more settings are supported const libraryVersion = await API.getLibraryVersion(); const json = { diff --git a/tests/remote_js/test/2/sortTest.js b/tests/remote_js/test/2/sortTest.js index 8246d366..e52816d8 100644 --- a/tests/remote_js/test/2/sortTest.js +++ b/tests/remote_js/test/2/sortTest.js @@ -93,6 +93,7 @@ describe('SortTests', function () { titlesSorted.sort(); let correct = {}; titlesSorted.forEach((title) => { + // The key at position k in itemKeys should be at the same position in keys let index = titlesToIndex[title]; correct[index] = keys[index]; }); @@ -117,12 +118,14 @@ describe('SortTests', function () { }; let namesEntries = Object.entries(namesCopy); namesEntries.sort((a, b) => sortFunction(a[1], b[1])); + // The key at position k in itemKeys should be at the same position in keys assert.equal(Object.keys(namesEntries).length, keys.length); let correct = {}; namesEntries.forEach((entry, i) => { correct[i] = itemKeys[parseInt(entry[0])]; }); correct = Object.keys(correct).map(key => correct[key]); + // Check attachment and note, which should fall back to ordered added (itemID) assert.deepEqual(correct, keys); }); }); diff --git a/tests/remote_js/test/2/tagTest.js b/tests/remote_js/test/2/tagTest.js index 1bd1674f..d1fba2e0 100644 --- a/tests/remote_js/test/2/tagTest.js +++ b/tests/remote_js/test/2/tagTest.js @@ -157,6 +157,10 @@ describe('TagTests', function () { Helpers.assertNumResults(response, 1); }); + /** + * When modifying a tag on an item, only the item itself should have its + * version updated, not other items that had (and still have) the same tag + */ it('testTagAddItemVersionChange', async function () { let data1 = await API.createItem("book", { tags: [{ @@ -167,7 +171,6 @@ describe('TagTests', function () { }] }, true, 'data'); let json1 = JSON.parse(data1.content); - //let version1 = data1.version; let data2 = await API.createItem("book", { tags: [{ @@ -180,6 +183,7 @@ describe('TagTests', function () { let json2 = JSON.parse(data2.content); let version2 = data2.version; version2 = parseInt(version2); + // Remove tag 'a' from item 1 json1.tags = [{ tag: "d" diff --git a/tests/remote_js/test/2/versionTest.js b/tests/remote_js/test/2/versionTest.js index 8f01db99..6c9d78e3 100644 --- a/tests/remote_js/test/2/versionTest.js +++ b/tests/remote_js/test/2/versionTest.js @@ -67,6 +67,10 @@ describe('VersionsTests', function () { ); break; } + + // Make sure all three instances of the object version + // (Last-Modified-Version, zapi:version, and the JSON + // {$objectType}Version property match the library version let response = await API.userGet( config.userID, `${objectTypePlural}/${objectKey}?key=${config.apiKey}&content=json` @@ -86,7 +90,10 @@ describe('VersionsTests', function () { Helpers.assertStatusCode(response, 200); const libraryVersion = response.headers['last-modified-version'][0]; assert.equal(libraryVersion, objectVersion); + _modifyJSONObject(objectType, json); + + // No If-Unmodified-Since-Version or JSON version property delete json[versionProp]; response = await API.userPut( config.userID, @@ -95,6 +102,8 @@ describe('VersionsTests', function () { { 'Content-Type': 'application/json' } ); Helpers.assertStatusCode(response, 428); + + // Out of date version response = await API.userPut( config.userID, `${objectTypePlural}/${objectKey}?key=${config.apiKey}`, @@ -105,6 +114,8 @@ describe('VersionsTests', function () { } ); Helpers.assertStatusCode(response, 412); + + // Update with version header response = await API.userPut( config.userID, `${objectTypePlural}/${objectKey}?key=${config.apiKey}`, @@ -117,6 +128,8 @@ describe('VersionsTests', function () { Helpers.assertStatusCode(response, 204); const newObjectVersion = response.headers['last-modified-version'][0]; assert.isAbove(parseInt(newObjectVersion), parseInt(objectVersion)); + + // Update object with JSON version property _modifyJSONObject(objectType, json); json[versionProp] = newObjectVersion; response = await API.userPut( @@ -128,6 +141,8 @@ describe('VersionsTests', function () { Helpers.assertStatusCode(response, 204); const newObjectVersion2 = response.headers['last-modified-version'][0]; assert.isAbove(parseInt(newObjectVersion2), parseInt(newObjectVersion)); + + // Make sure new library version matches new object version response = await API.userGet( config.userID, `${objectTypePlural}?key=${config.apiKey}&limit=1` @@ -135,6 +150,9 @@ describe('VersionsTests', function () { Helpers.assertStatusCode(response, 200); const newLibraryVersion = response.headers['last-modified-version'][0]; assert.equal(parseInt(newObjectVersion2), parseInt(newLibraryVersion)); + + // Create an item to increase the library version, and make sure + // original object version stays the same await API.createItem('book', { title: 'Title' }, this, 'key'); response = await API.userGet( config.userID, @@ -143,17 +161,26 @@ describe('VersionsTests', function () { Helpers.assertStatusCode(response, 200); const newObjectVersion3 = response.headers['last-modified-version'][0]; assert.equal(parseInt(newLibraryVersion), parseInt(newObjectVersion3)); + + // + // Delete object + // + // No If-Unmodified-Since-Version response = await API.userDelete( config.userID, `${objectTypePlural}/${objectKey}?key=${config.apiKey}` ); Helpers.assertStatusCode(response, 428); + + // Outdated If-Unmodified-Since-Version response = await API.userDelete( config.userID, `${objectTypePlural}/${objectKey}?key=${config.apiKey}`, { 'If-Unmodified-Since-Version': objectVersion } ); Helpers.assertStatusCode(response, 412); + + // Delete object response = await API.userDelete( config.userID, `${objectTypePlural}/${objectKey}?key=${config.apiKey}`, @@ -339,6 +366,8 @@ describe('VersionsTests', function () { version = parseInt(response.headers['last-modified-version'][0]); assert.isNumber(version); assert.equal(version, version3); + + // TODO: Version should be incremented on deleted item }; const _testMultiObject304NotModified = async (objectType) => { @@ -415,7 +444,7 @@ describe('VersionsTests', function () { const objects = []; while (xmlArray.length > 0) { const xml = xmlArray.shift(); - const data = await API.parseDataFromAtomEntry(xml); + const data = API.parseDataFromAtomEntry(xml); objects.push({ key: data.key, version: data.version diff --git a/tests/remote_js/test/3/annotationsTest.js b/tests/remote_js/test/3/annotationsTest.js index bdc0822b..28d85d1f 100644 --- a/tests/remote_js/test/3/annotationsTest.js +++ b/tests/remote_js/test/3/annotationsTest.js @@ -69,6 +69,7 @@ describe('AnnotationsTests', function () { ] }) }; + // Create highlight annotation let response = await API.userPost( config.userID, "items", @@ -80,6 +81,7 @@ describe('AnnotationsTests', function () { let annotationKey = json.key; let version = json.version; + // Try to change to note annotation json = { version: version, annotationType: 'note' @@ -138,7 +140,7 @@ describe('AnnotationsTests', function () { }; let response = await API.userPost(config.userID, 'items', JSON.stringify([json]), { 'Content-Type': 'application/json' }); Helpers.assert200ForObject(response); - let jsonResponse = await API.getJSONFromResponse(response); + let jsonResponse = API.getJSONFromResponse(response); let jsonData = jsonResponse.successful[0].data; assert.notProperty(jsonData, 'annotationAuthorName'); }); @@ -187,7 +189,7 @@ describe('AnnotationsTests', function () { }; const response = await API.userPost(config.userID, 'items', JSON.stringify([json]), { "Content-Type": "application/json" }); Helpers.assert200ForObject(response); - const result = await API.getJSONFromResponse(response); + const result = API.getJSONFromResponse(response); const { key: annotationKey, version } = result.successful[0]; const patchJson = { key: annotationKey, @@ -280,7 +282,7 @@ describe('AnnotationsTests', function () { { "Content-Type": "application/json" } ); Helpers.assert200ForObject(response); - let jsonResponse = await API.getJSONFromResponse(response); + let jsonResponse = API.getJSONFromResponse(response); let jsonData = jsonResponse.successful[0].data; Helpers.assertEquals('annotation', jsonData.itemType.toString()); Helpers.assertEquals('note', jsonData.annotationType); @@ -462,7 +464,7 @@ describe('AnnotationsTests', function () { { "Content-Type": "application/json" } ); Helpers.assert200ForObject(response); - let jsonResponse = await API.getJSONFromResponse(response); + let jsonResponse = API.getJSONFromResponse(response); let jsonData = jsonResponse.successful[0].data; Helpers.assertEquals('annotation', String(jsonData.itemType)); Helpers.assertEquals('highlight', jsonData.annotationType); @@ -497,7 +499,7 @@ describe('AnnotationsTests', function () { { "Content-Type": "application/json" } ); Helpers.assert200ForObject(response); - let jsonResponse = await API.getJSONFromResponse(response); + let jsonResponse = API.getJSONFromResponse(response); jsonResponse = jsonResponse.successful[0]; let jsonData = jsonResponse.data; Helpers.assertEquals('annotation', jsonData.itemType); diff --git a/tests/remote_js/test/3/atomTest.js b/tests/remote_js/test/3/atomTest.js index 1b117696..07901bbe 100644 --- a/tests/remote_js/test/3/atomTest.js +++ b/tests/remote_js/test/3/atomTest.js @@ -3,7 +3,6 @@ const assert = chai.assert; var config = require('config'); const API = require('../../api3.js'); const Helpers = require('../../helpers3.js'); -const { JSDOM } = require('jsdom'); const { API3Before, API3After } = require("../shared.js"); describe('AtomTests', function () { @@ -53,16 +52,17 @@ describe('AtomTests', function () { let response = await API.userGet(userID, "items?format=atom"); Helpers.assert200(response); - let xml = await API.getXMLFromResponse(response); + let xml = API.getXMLFromResponse(response); let links = Helpers.xpathEval(xml, "//atom:feed/atom:link", true, true); Helpers.assertEquals( config.apiURLPrefix + "users/" + userID + "/items?format=atom", links[0].getAttribute("href") ); + // 'order'/'sort' should turn into 'sort'/'direction' response = await API.userGet(userID, "items?format=atom&order=dateModified&sort=asc"); Helpers.assert200(response); - xml = await API.getXMLFromResponse(response); + xml = API.getXMLFromResponse(response); links = Helpers.xpathEval(xml, "//atom:feed/atom:link", true, true); Helpers.assertEquals( config.apiURLPrefix + "users/" + userID + "/items?direction=asc&format=atom&sort=dateModified", @@ -88,22 +88,23 @@ describe('AtomTests', function () { const key = entry.getElementsByTagName("zapi:key")[0].innerHTML; let content = entry.getElementsByTagName("content")[0].outerHTML; + // Add namespace prefix (from ) content = content.replace( ' { - return API.createAttachmentItem("imported_file", [], false, this, 'key'); - }; - const testGetFile = async () => { const addFileData = await testAddFileExisting(); - const userGetViewModeResponse = await API.userGet( + + // Get in view mode + let response = await API.userGet( config.userID, `items/${addFileData.key}/file/view` ); - Helpers.assert302(userGetViewModeResponse); - const location = userGetViewModeResponse.headers.location[0]; + Helpers.assert302(response); + const location = response.headers.location[0]; Helpers.assertRegExp(/^https?:\/\/[^/]+\/[a-zA-Z0-9%]+\/[a-f0-9]{64}\/test_/, location); const filenameEncoded = encodeURIComponent(addFileData.filename); assert.equal(filenameEncoded, location.substring(location.length - filenameEncoded.length)); + + // Get from view mode const viewModeResponse = await HTTP.get(location); Helpers.assert200(viewModeResponse); assert.equal(addFileData.md5, Helpers.md5(viewModeResponse.data)); - const userGetDownloadModeResponse = await API.userGet( + + // Get in download mode + response = await API.userGet( config.userID, `items/${addFileData.key}/file` ); - Helpers.assert302(userGetDownloadModeResponse); - const downloadModeLocation = userGetDownloadModeResponse.headers.location[0]; + Helpers.assert302(response); + + // Get from S3 + const downloadModeLocation = response.headers.location[0]; const s3Response = await HTTP.get(downloadModeLocation); Helpers.assert200(s3Response); assert.equal(addFileData.md5, Helpers.md5(s3Response.data)); + return { key: addFileData.key, response: s3Response @@ -124,14 +128,15 @@ describe('FileTestTests', function () { let originalVersion = json.version; let file = "./work/file"; let fileContents = Helpers.getRandomUnicodeString(); - await fs.promises.writeFile(file, fileContents); + fs.writeFileSync(file, fileContents); let hash = Helpers.md5File(file); let filename = "test_" + fileContents; - let mtime = parseInt((await fs.promises.stat(file)).mtimeMs); - let size = parseInt((await fs.promises.stat(file)).size); + let size = parseInt(fs.statSync(file).size); + let mtime = parseInt(fs.statSync(file).mtimeMs); let contentType = "text/plain"; let charset = "utf-8"; + // Get upload authorization let response = await API.userPost( config.userID, `items/${attachmentKey}/file`, @@ -155,6 +160,7 @@ describe('FileTestTests', function () { assert.ok(json); toDelete.push(hash); + // Generate form-data -- taken from S3::getUploadPostData() let boundary = "---------------------------" + Helpers.md5(Helpers.uniqueID()); let prefix = ""; for (let key in json.params) { @@ -162,6 +168,7 @@ describe('FileTestTests', function () { } prefix += "--" + boundary + "\r\nContent-Disposition: form-data; name=\"file\"\r\n\r\n"; let suffix = "\r\n--" + boundary + "--"; + // Upload to S3 response = await HTTP.post( json.url, prefix + fileContents + suffix, @@ -171,6 +178,7 @@ describe('FileTestTests', function () { ); Helpers.assert201(response); + // Register upload response = await API.userPost( config.userID, `items/${attachmentKey}/file`, @@ -182,6 +190,7 @@ describe('FileTestTests', function () { ); Helpers.assert204(response); + // Verify attachment item metadata response = await API.userGet( config.userID, `items/${attachmentKey}` @@ -193,6 +202,7 @@ describe('FileTestTests', function () { assert.equal(contentType, json.contentType); assert.equal(charset, json.charset); + // Make sure version has changed assert.notEqual(originalVersion, json.version); }); @@ -232,6 +242,7 @@ describe('FileTestTests', function () { let contentType = 'text/plain'; let charset = 'utf-8'; + // Get upload authorization let response = await API.userPost( config.userID, `items/${key}/file`, @@ -253,6 +264,7 @@ describe('FileTestTests', function () { json = JSON.parse(response.data); assert.isOk(json); + // Upload to old-style location toDelete.push(`${hash}/${filename}`); toDelete.push(hash); const putCommand = new PutObjectCommand({ @@ -261,6 +273,8 @@ describe('FileTestTests', function () { Body: fileContents }); await s3Client.send(putCommand); + + // Register upload response = await API.userPost( config.userID, `items/${key}/file`, @@ -272,6 +286,7 @@ describe('FileTestTests', function () { ); Helpers.assert204(response); + // The file should be accessible on the item at the old-style location response = await API.userGet( config.userID, `items/${key}/file` @@ -309,6 +324,7 @@ describe('FileTestTests', function () { assert.isOk(postJSON); Helpers.assertEquals(1, postJSON.exists); + // Get in download mode response = await API.userGet( config.userID, `items/${key}/file` @@ -319,6 +335,7 @@ describe('FileTestTests', function () { Helpers.assertEquals(2, matches.length); Helpers.assertEquals(hash, matches[1]); + // Get from S3 response = await HTTP.get(location); Helpers.assert200(response); Helpers.assertEquals(fileContents, response.data); @@ -330,6 +347,7 @@ describe('FileTestTests', function () { json = await API.createAttachmentItem("imported_file", [], parentKey, this, 'jsonData'); key = json.key; + // Also use a different content type contentType = 'application/x-custom'; response = await API.userPost( config.userID, @@ -351,6 +369,7 @@ describe('FileTestTests', function () { assert.isOk(postJSON); Helpers.assertEquals(1, postJSON.exists); + // Get in download mode response = await API.userGet( config.userID, `items/${key}/file` @@ -361,6 +380,7 @@ describe('FileTestTests', function () { Helpers.assertEquals(2, matches.length); Helpers.assertEquals(hash, matches[1]); + // Get from S3 response = await HTTP.get(location); Helpers.assert200(response); Helpers.assertEquals(fileContents, response.data); @@ -382,6 +402,7 @@ describe('FileTestTests', function () { let contentType = "text/plain"; let charset = "utf-8"; + // Get upload authorization let response = await API.userPost( config.userID, `items/${attachmentKey}/file`, @@ -404,6 +425,7 @@ describe('FileTestTests', function () { assert.isOk(json); toDelete.push(`${hash}`); + // Upload wrong contents to S3 const wrongContent = fileContents.split('').reverse().join(""); response = await HTTP.post( json.url, @@ -415,6 +437,7 @@ describe('FileTestTests', function () { Helpers.assert400(response); assert.include(response.data, "The Content-MD5 you specified did not match what we received."); + // Upload to S3 response = await HTTP.post( json.url, json.prefix + fileContents + json.suffix, @@ -424,6 +447,9 @@ describe('FileTestTests', function () { ); Helpers.assert201(response); + // Register upload + + // No If-None-Match response = await API.userPost( config.userID, `items/${attachmentKey}/file`, @@ -434,6 +460,7 @@ describe('FileTestTests', function () { ); Helpers.assert428(response); + // Invalid upload key response = await API.userPost( config.userID, `items/${attachmentKey}/file`, @@ -456,6 +483,7 @@ describe('FileTestTests', function () { ); Helpers.assert204(response); + // Verify attachment item metadata response = await API.userGet( config.userID, `items/${attachmentKey}` @@ -475,8 +503,8 @@ describe('FileTestTests', function () { }; }; - it('testAddFileAuthorizationErrors', async function () { - const parentKey = await testNewEmptyImportedFileAttachmentItem(); + it('testAddFileFormDataAuthorizationErrors', async function () { + const parentKey = await API.createAttachmentItem("imported_file", [], false, this, 'key'); const fileContents = Helpers.getRandomUnicodeString(); const hash = Helpers.md5(fileContents); const mtime = Date.now(); @@ -561,7 +589,7 @@ describe('FileTestTests', function () { config.userID, `items/${getFileData.key}` ); - let json = await API.getJSONFromResponse(response).data; + let json = API.getJSONFromResponse(response).data; await new Promise(resolve => setTimeout(resolve, 1000)); @@ -581,8 +609,11 @@ describe('FileTestTests', function () { }; for (let [algo, cmd] of Object.entries(algorithms)) { + // Create random contents fs.writeFileSync(newFilename, Helpers.getRandomUnicodeString() + Helpers.uniqueID()); const newHash = Helpers.md5File(newFilename); + + // Get upload authorization const fileParams = { md5: newHash, filename: `test_${fileContents}`, @@ -617,6 +648,7 @@ describe('FileTestTests', function () { toDelete.push(newHash); + // Upload patch file let response = await API.userPatch( config.userID, `items/${getFileData.key}/file?algorithm=${algo}&upload=${json.uploadKey}`, @@ -627,8 +659,9 @@ describe('FileTestTests', function () { ); Helpers.assert204(response); - fs.unlinkSync(patchFilename); + fs.rmSync(patchFilename); fs.renameSync(newFilename, oldFilename); + // Verify attachment item metadata response = await API.userGet( config.userID, `items/${getFileData.key}` @@ -639,8 +672,11 @@ describe('FileTestTests', function () { Helpers.assertEquals(fileParams.mtime, json.mtime); Helpers.assertEquals(fileParams.contentType, json.contentType); Helpers.assertEquals(fileParams.charset, json.charset); + + // Make sure version has changed assert.notEqual(originalVersion, json.version); + // Verify file on S3 const fileResponse = await API.userGet( config.userID, `items/${getFileData.key}/file` @@ -719,9 +755,6 @@ describe('FileTestTests', function () { }; - //////////////// - - it('testAddFileClientV4Zip', async function () { await API.userClear(config.userID); @@ -730,6 +763,7 @@ describe('FileTestTests', function () { password: config.password, }; + // Get last storage sync const response1 = await API.userGet( config.userID, 'laststoragesync?auth=1', @@ -748,7 +782,6 @@ describe('FileTestTests', function () { const json2 = await API.createAttachmentItem('imported_url', [], key, this, 'jsonData'); key = json2.key; - //const version = json2.version; json2.contentType = fileContentType; json2.charset = fileCharset; json2.filename = fileFilename; @@ -764,6 +797,7 @@ describe('FileTestTests', function () { Helpers.assert204(response2); const originalVersion = response2.headers['last-modified-version'][0]; + // Get file info const response3 = await API.userGet( config.userID, `items/${json2.key}/file?auth=1&iskey=1&version=1&info=1`, @@ -777,6 +811,7 @@ describe('FileTestTests', function () { const filename = `${key}.zip`; + // Get upload authorization const response4 = await API.userPost( config.userID, `items/${json2.key}/file?auth=1&iskey=1&version=1`, @@ -811,11 +846,13 @@ describe('FileTestTests', function () { postData += `--${boundary}\r\nContent-Disposition: form-data; name="file"\r\n\r\n${fileContent}\r\n`; postData += `--${boundary}--`; + // Upload to S3 const response5 = await HTTP.post(`${url}`, postData, { 'Content-Type': `multipart/form-data; boundary=${boundary}`, }); Helpers.assert201(response5); + // Register upload const response6 = await API.userPost( config.userID, `items/${json2.key}/file?auth=1&iskey=1&version=1`, @@ -827,8 +864,11 @@ describe('FileTestTests', function () { ); Helpers.assert204(response6); + // Verify attachment item metadata const response7 = await API.userGet(config.userID, `items/${json2.key}`); const json3 = API.getJSONFromResponse(response7).data; + // Make sure attachment item version hasn't changed (or else the client + // will get a conflict when it tries to update the metadata) Helpers.assertEquals(originalVersion, json3.version); Helpers.assertEquals(hash, json3.md5); Helpers.assertEquals(fileFilename, json3.filename); @@ -847,6 +887,7 @@ describe('FileTestTests', function () { const mtime = response8.data; Helpers.assertRegExp(/^[0-9]{10}$/, mtime); + // File exists const response9 = await API.userPost( config.userID, `items/${json2.key}/file?auth=1&iskey=1&version=1`, @@ -866,6 +907,7 @@ describe('FileTestTests', function () { Helpers.assertContentType(response9, 'application/xml'); Helpers.assertEquals('', response9.data); + // Make sure attachment version still hasn't changed const response10 = await API.userGet(config.userID, `items/${json2.key}`); const json4 = API.getJSONFromResponse(response10).data; Helpers.assertEquals(originalVersion, json4.version); @@ -1090,7 +1132,6 @@ describe('FileTestTests', function () { charset }, false, this, 'jsonData'); const key = json.key; - //const originalVersion = json.version; // Get authorization const response = await API.userPost( @@ -1108,7 +1149,7 @@ describe('FileTestTests', function () { } ); Helpers.assert200(response); - const jsonObj = await API.getJSONFromResponse(response); + const jsonObj = API.getJSONFromResponse(response); // Try to upload to S3, which should fail const s3Response = await HTTP.post( @@ -1145,6 +1186,7 @@ describe('FileTestTests', function () { }, false, this, 'jsonData'); let itemKey = json.key; + // Get upload authorization let response = await API.userPost( config.userID, "items/" + itemKey + "/file", @@ -1165,6 +1207,7 @@ describe('FileTestTests', function () { toDelete.push(hash); + // Upload to S3 response = await HTTP.post( json.url, json.prefix + fileContents + json.suffix, @@ -1174,6 +1217,7 @@ describe('FileTestTests', function () { ); Helpers.assert201(response); + // Register upload response = await API.userPost( config.userID, "items/" + itemKey + "/file", @@ -1423,6 +1467,7 @@ describe('FileTestTests', function () { }); it('test_updating_compressed_attachment_hash_should_clear_associated_storage_file', async function () { + // Create initial file let fileContents = Helpers.getRandomUnicodeString(); let contentType = "text/html"; let charset = "utf-8"; @@ -1439,11 +1484,13 @@ describe('FileTestTests', function () { let file = "work/" + itemKey + ".zip"; let zipFilename = "work/" + itemKey + ".zip"; + // Create initial ZIP file const zipData = await generateZip(file, fileContents, zipFilename); let zipHash = zipData.hash; let zipSize = zipData.zipSize; let zipFileContents = zipData.fileContent; + // Get upload authorization let response = await API.userPost( config.userID, "items/" + itemKey + "/file", @@ -1467,6 +1514,7 @@ describe('FileTestTests', function () { toDelete.push(zipHash); + // Upload to S3 response = await HTTP.post( json.url, json.prefix + zipFileContents + json.suffix, @@ -1478,6 +1526,7 @@ describe('FileTestTests', function () { ); Helpers.assert201(response); + // Register upload response = await API.userPost( config.userID, "items/" + itemKey + "/file", @@ -1492,6 +1541,7 @@ describe('FileTestTests', function () { Helpers.assert204(response); let newVersion = response.headers['last-modified-version']; + // Set new attachment file info hash = Helpers.md5(Helpers.uniqueID()); mtime = Date.now(); zipHash = Helpers.md5(Helpers.uniqueID()); @@ -1594,7 +1644,7 @@ describe('FileTestTests', function () { Helpers.assertEquals(filename, metaDataJson.data.filename); Helpers.assertEquals(contentType, metaDataJson.data.contentType); Helpers.assertEquals(charset, metaDataJson.data.charset); - // update file + const newFileContents = Helpers.getRandomUnicodeString() + Helpers.getRandomUnicodeString(); fs.writeFileSync(file, newFileContents); @@ -1681,6 +1731,7 @@ describe('FileTestTests', function () { Helpers.assert404(response); }); + // TODO: Reject for keys not owned by user, even if public library it('testLastStorageSyncNoAuthorization', async function () { API.useAPIKey(false); let response = await API.userGet( @@ -1988,7 +2039,7 @@ describe('FileTestTests', function () { config.userID, "items/" + parentKey ); - json = await API.getJSONFromResponse(response); + json = API.getJSONFromResponse(response); assert.notProperty(json.links, 'attachment'); // Get upload authorization @@ -2007,7 +2058,7 @@ describe('FileTestTests', function () { } ); Helpers.assert200(response); - json = await API.getJSONFromResponse(response); + json = API.getJSONFromResponse(response); // If file doesn't exist on S3, upload if (!json.exists) { @@ -2039,7 +2090,7 @@ describe('FileTestTests', function () { config.userID, "items/" + parentKey ); - json = await API.getJSONFromResponse(response); + json = API.getJSONFromResponse(response); assert.property(json.links, 'attachment'); assert.property(json.links.attachment, 'href'); assert.equal('application/json', json.links.attachment.type); diff --git a/tests/remote_js/test/3/fullTextTest.js b/tests/remote_js/test/3/fullTextTest.js index cc8a9801..e518242c 100644 --- a/tests/remote_js/test/3/fullTextTest.js +++ b/tests/remote_js/test/3/fullTextTest.js @@ -17,7 +17,7 @@ describe('FullTextTests', function () { }); this.beforeEach(async function () { - await API.useAPIKey(config.apiKey); + API.useAPIKey(config.apiKey); }); it('testContentAnonymous', async function () { @@ -426,71 +426,6 @@ describe('FullTextTests', function () { assert.notProperty(json, "indexedPages"); }); - it('_testSinceContent', async function () { - await API.userClear(config.userID); - // Store content for one item - let key = await API.createItem("book", false, this, 'key'); - let json = await API.createAttachmentItem("imported_url", [], key, this, 'jsonData'); - let key1 = json.key; - - let content = "Here is some full-text content"; - - let response = await API.userPut( - config.userID, - "items/" + key1 + "/fulltext", - JSON.stringify({ - content: content - }), - { "Content-Type": "application/json" } - ); - Helpers.assert204(response); - let contentVersion1 = response.headers['last-modified-version'][0]; - assert.isAbove(parseInt(contentVersion1), 0); - - // And another - key = await API.createItem("book", false, this, 'key'); - json = await API.createAttachmentItem("imported_url", [], key, this, 'jsonData'); - let key2 = json.key; - - response = await API.userPut( - config.userID, - "items/" + key2 + "/fulltext", - JSON.stringify({ - content: content - }), - { "Content-Type": "application/json" } - ); - Helpers.assert204(response); - let contentVersion2 = response.headers['last-modified-version'][0]; - assert.isAbove(parseInt(contentVersion2), 0); - - // Get newer one - response = await API.userGet( - config.userID, - "fulltext?param=" + contentVersion1 - ); - Helpers.assert200(response); - Helpers.assertContentType(response, "application/json"); - Helpers.assertEquals(contentVersion2, response.headers['last-modified-version'][0]); - json = API.getJSONFromResponse(response); - assert.lengthOf(Object.keys(json), 2); - assert.property(json, key2); - Helpers.assertEquals(contentVersion2, json[key2]); - - // Get both with since=0 - response = await API.userGet( - config.userID, - "fulltext?param=0" - ); - Helpers.assert200(response); - Helpers.assertContentType(response, "application/json"); - json = API.getJSONFromResponse(response); - assert.lengthOf(Object.keys(json), 2); - assert.property(json, key1); - Helpers.assertEquals(contentVersion1, json[key1]); - Helpers.assertEquals(contentVersion2, json[key2]); - }); - it('testVersionsAnonymous', async function () { API.useAPIKey(false); const response = await API.userGet( diff --git a/tests/remote_js/test/3/groupTest.js b/tests/remote_js/test/3/groupTest.js index c3668db1..2aceb6c7 100644 --- a/tests/remote_js/test/3/groupTest.js +++ b/tests/remote_js/test/3/groupTest.js @@ -40,6 +40,7 @@ describe('Tests', function () { libraryReading: 'all' }); + // Get group version let response = await API.userGet(config.userID, `groups?format=versions&key=${config.apiKey}`); Helpers.assert200(response); let version = JSON.parse(response.data)[groupID]; @@ -47,12 +48,14 @@ describe('Tests', function () { response = await API.superPost(`groups/${groupID}/users`, '', { 'Content-Type': 'text/xml' }); Helpers.assert200(response); + // Group metadata version should have changed response = await API.userGet(config.userID, `groups?format=versions&key=${config.apiKey}`); Helpers.assert200(response); let json = JSON.parse(response.data); let newVersion = json[groupID]; assert.notEqual(version, newVersion); + // Check version header on individual group request response = await API.groupGet(groupID, ''); Helpers.assert200(response); Helpers.assertEquals(newVersion, response.headers['last-modified-version'][0]); @@ -60,6 +63,9 @@ describe('Tests', function () { await API.deleteGroup(groupID); }); + /** + * Changing a group's metadata should change its version + */ it('testUpdateMetadataAtom', async function () { let response = await API.userGet( config.userID, @@ -158,6 +164,9 @@ describe('Tests', function () { assert.equal(urlField, json.url); }); + /** + * Changing a group's metadata should change its version + */ it('testUpdateMetadataJSON', async function () { const response = await API.userGet( config.userID, @@ -166,12 +175,14 @@ describe('Tests', function () { Helpers.assert200(response); + // Get group API URI and version const json = API.getJSONFromResponse(response)[0]; const groupID = json.id; let url = json.links.self.href; url = url.replace(config.apiURLPrefix, ''); const version = json.version; + // Make sure format=versions returns the same version const response2 = await API.userGet( config.userID, "groups?format=versions&key=" + config.apiKey @@ -181,6 +192,7 @@ describe('Tests', function () { Helpers.assertEquals(version, JSON.parse(response2.data)[groupID]); + // Update group metadata const xmlDoc = new JSDOM(""); const groupXML = xmlDoc.window.document.getElementsByTagName("group")[0]; let name, description, urlField, newNode; @@ -244,6 +256,7 @@ describe('Tests', function () { assert.notEqual(version, newVersion); + // Check version header on individual group request const response5 = await API.groupGet( groupID, "" @@ -267,6 +280,7 @@ describe('Tests', function () { libraryReading: 'all' }); + // Group shouldn't show up if it's never had items let response = await API.superGet(`groups?q=${name}`); Helpers.assertNumResults(response, 0); diff --git a/tests/remote_js/test/3/itemTest.js b/tests/remote_js/test/3/itemTest.js index f7ee1355..d2f9e86d 100644 --- a/tests/remote_js/test/3/itemTest.js +++ b/tests/remote_js/test/3/itemTest.js @@ -50,7 +50,7 @@ describe('ItemsTests', function () { const response = await API.postItems(data); Helpers.assertStatusCode(response, 200); let libraryVersion = parseInt(response.headers['last-modified-version'][0]); - json = await API.getJSONFromResponse(response); + json = API.getJSONFromResponse(response); Helpers.assertCount(3, json.successful); Helpers.assertCount(3, json.success); @@ -64,8 +64,6 @@ describe('ItemsTests', function () { assert.equal(data[2].numPages, json.successful[2].data.numPages); json = await API.getItem(Object.keys(json.success).map(k => json.success[k]), this, 'json'); - - assert.equal(json[0].data.title, "A"); assert.equal(json[1].data.title, "B"); assert.equal(json[2].data.title, "C"); @@ -95,10 +93,8 @@ describe('ItemsTests', function () { `items/${key}`, JSON.stringify(newBookItem), { - headers: { - 'Content-Type': 'application/json', - 'If-Unmodified-Since-Version': version - } + 'Content-Type': 'application/json', + 'If-Unmodified-Since-Version': version } ); Helpers.assertStatusCode(response, 204); @@ -161,7 +157,7 @@ describe('ItemsTests', function () { // If existing dateModified, use current timestamp // json.title = 'Test 3'; - json.dateModified = dateModified2; + json.dateModified = dateModified2.replace(/T|Z/g, " ").trim(); response = await API.userPut( config.userID, `${objectTypePlural}/${objectKey}`, @@ -186,7 +182,7 @@ describe('ItemsTests', function () { json.dateModified = newDateModified; response = await API.userPut( config.userID, - `${objectTypePlural}/${objectKey}? `, + `${objectTypePlural}/${objectKey}`, JSON.stringify(json) ); Helpers.assertStatusCode(response, 204); @@ -416,7 +412,7 @@ describe('ItemsTests', function () { JSON.stringify([json2]), { "Content-Type": "application/json" } ); - Helpers.assertStatusForObject(response, 'failed', 0, 400, "'itemType' property not provided"); + Helpers.assert400ForObject(response, { message: "'itemType' property not provided" }); // contentType on non-attachment const json3 = { ...json }; @@ -427,7 +423,7 @@ describe('ItemsTests', function () { JSON.stringify([json3]), { "Content-Type": "application/json" } ); - Helpers.assertStatusForObject(response, 'failed', 0, 400, "'contentType' is valid only for attachment items"); + Helpers.assert400ForObject(response, { message: "'contentType' is valid only for attachment items" }); }); it('testEditTopLevelNote', async function () { @@ -519,7 +515,7 @@ describe('ItemsTests', function () { `items`, JSON.stringify([json]), ); - Helpers.assertStatusForObject(response, 'success', 0); + Helpers.assert200ForObject(response); json = (await API.getItem(json.key, true, 'json')).data; assert.equal(json.title, 'B'); @@ -540,8 +536,9 @@ describe('ItemsTests', function () { Helpers.assert200(userPostResponse); }); + // Disabled -- see note at Zotero_Item::checkTopLevelAttachment() it('testNewInvalidTopLevelAttachment', async function () { - this.skip(); //disabled + this.skip(); }); it('testNewEmptyLinkAttachmentItemWithItemKey', async function () { @@ -561,7 +558,7 @@ describe('ItemsTests', function () { JSON.stringify([json]), { "Content-Type": "application/json" } ); - Helpers.assert200(response); + Helpers.assert200ForObject(response); }); it('testEditEmptyImportedURLAttachmentItem', async function () { @@ -654,8 +651,7 @@ describe('ItemsTests', function () { config.userID, `items/${data.key}`, JSON.stringify(json), - { "If-Unmodified-Since-Version": data.version, - "User-Agent": "Firefox" } // TODO: Remove + { "If-Unmodified-Since-Version": data.version } ); Helpers.assert204(response); @@ -876,15 +872,17 @@ describe('ItemsTests', function () { childKeys[childKeys.length - 1], this, 'key')); + // Create item with deleted child that matches child title search parentKeys.push(await API.createItem(itemTypes[2], { title: parentTitle3 }, this, 'key')); - childKeys.push(await API.createAttachmentItem("linked_url", { + await API.createAttachmentItem("linked_url", { title: childTitle1, deleted: true - }, parentKeys[parentKeys.length - 1], this, 'key')); + }, parentKeys[parentKeys.length - 1], this, 'key'); + // Add deleted item with non-deleted child const deletedKey = await API.createItem("book", { title: "This is a deleted item", deleted: true, @@ -1192,7 +1190,7 @@ describe('ItemsTests', function () { config.userID, `items/${key}` ); - json = await API.getJSONFromResponse(response); + json = API.getJSONFromResponse(response); assert.equal(json.data.date, date); assert.equal(json.meta.parsedDate, parsedDate); @@ -1214,7 +1212,7 @@ describe('ItemsTests', function () { } ]; let response = await API.postItems(data); - let jsonResponse = await API.getJSONFromResponse(response); + let jsonResponse = API.getJSONFromResponse(response); assert.property(jsonResponse.successful[0].data, 'deleted'); Helpers.assertEquals(1, jsonResponse.successful[0].data.deleted); @@ -1248,7 +1246,7 @@ describe('ItemsTests', function () { config.userID, "items?itemKey=" + itemKey + "," + noteKey + "," + attachmentKey ); - json = await API.getJSONFromResponse(response); + json = API.getJSONFromResponse(response); Helpers.assertNumResults(response, 0); }); @@ -1265,7 +1263,7 @@ describe('ItemsTests', function () { config.userID, "items/trash" ); - let json = await API.getJSONFromResponse(response); + let json = API.getJSONFromResponse(response); Helpers.assertCount(1, json); Helpers.assertEquals(key2, json[0].key); @@ -1274,7 +1272,7 @@ describe('ItemsTests', function () { config.userID, "items" ); - json = await API.getJSONFromResponse(response); + json = API.getJSONFromResponse(response); Helpers.assertCount(1, json); Helpers.assertEquals(key1, json[0].key); @@ -1283,7 +1281,7 @@ describe('ItemsTests', function () { config.userID, "items?itemKey=" + key2 ); - json = await API.getJSONFromResponse(response); + json = API.getJSONFromResponse(response); Helpers.assertCount(0, json); }); @@ -1318,7 +1316,7 @@ describe('ItemsTests', function () { JSON.stringify([json]), { "Content-Type": "application/json" } ); - Helpers.assert400ForObject(response, "Embedded-image attachment must have an image content type"); + Helpers.assert400ForObject(response, { message: "Embedded-image attachment must have an image content type" }); }); it('testPatchNote', async function () { @@ -1375,13 +1373,14 @@ describe('ItemsTests', function () { await new Promise(resolve => setTimeout(resolve, 1000)); + // If no explicit dateModified, use current timestamp json.title = "Test 2"; delete json.dateModified; let response = await API.userPut( config.userID, `${objectTypePlural}/${objectKey}`, JSON.stringify(json), - { + { // TODO: Remove "User-Agent": "Firefox" } ); @@ -1397,8 +1396,9 @@ describe('ItemsTests', function () { await new Promise(resolve => setTimeout(resolve, 1000)); + // If dateModified provided and hasn't changed, use that json.title = "Test 3"; - json.dateModified = dateModified2.replace(/[TZ]/g, ' ').trim(); + json.dateModified = dateModified2.replace(/T|Z/g, ' ').trim(); response = await API.userPut( config.userID, `${objectTypePlural}/${objectKey}`, @@ -1415,7 +1415,7 @@ describe('ItemsTests', function () { Helpers.assertEquals(dateModified2, json.dateModified); let newDateModified = "2013-03-03T21:33:53Z"; - + // If dateModified is provided and has changed, use that json.title = "Test 4"; json.dateModified = newDateModified; response = await API.userPut( @@ -1437,6 +1437,7 @@ describe('ItemsTests', function () { it('test_top_should_return_top_level_item_for_three_level_hierarchy', async function () { await API.userClear(config.userID); + // Create parent item, PDF attachment, and annotation let itemKey = await API.createItem("book", { title: 'aaa' }, this, 'key'); let attachmentKey = await API.createAttachmentItem("imported_url", { contentType: 'application/pdf', @@ -1444,6 +1445,7 @@ describe('ItemsTests', function () { }, itemKey, this, 'key'); let _ = await API.createAnnotationItem('highlight', { annotationComment: 'ccc' }, attachmentKey, this, 'key'); + // Search for descendant items in /top mode let response = await API.userGet(config.userID, "items/top?q=bbb"); Helpers.assert200(response); Helpers.assertNumResults(response, 1); @@ -1463,6 +1465,9 @@ describe('ItemsTests', function () { Helpers.assertEquals("aaa", json[0].data.title); }); + /** + * Date Modified shouldn't be changed if 1) dateModified is provided or 2) certain fields are changed + */ it('testDateModifiedNoChange', async function () { let collectionKey = await API.createCollection('Test', false, this, 'key'); @@ -1592,7 +1597,7 @@ describe('ItemsTests', function () { JSON.stringify([json]), { "Content-Type": "application/json" } ); - Helpers.assert400ForObject(response, "Embedded-image attachment must have a parent item"); + Helpers.assert400ForObject(response, { message: "Embedded-image attachment must have a parent item" }); }); it('testNewEmptyAttachmentFields', async function () { @@ -1623,6 +1628,13 @@ describe('ItemsTests', function () { Helpers.assertCount(0, Helpers.xpathEval(xml, '/atom:entry/zapi:parsedDate', false, true).length); }); + /** + * Changing existing 'md5' and 'mtime' values to null was originally prevented, but some client + * versions were sending null, so now we just ignore it. + * + * At some point, we should check whether any clients are still doing this and restore the + * restriction if not. These should only be cleared on a storage purge. + */ it('test_should_ignore_null_for_existing_storage_properties', async function () { let key = await API.createItem("book", [], this, 'key'); let json = await API.createAttachmentItem( @@ -1664,7 +1676,7 @@ describe('ItemsTests', function () { let note1Key = await API.createNoteItem("Test 1", null, this, 'key'); let note2Key = await API.createNoteItem("Test 2", null, this, 'key'); let response = await API.get("items/new?itemType=attachment&linkMode=embedded_image"); - let json = JSON.parse(await response.data); + let json = JSON.parse(response.data); json.parentItem = note1Key; json.contentType = 'image/png'; response = await API.userPost( @@ -1674,7 +1686,7 @@ describe('ItemsTests', function () { { "Content-Type": "application/json" } ); Helpers.assert200ForObject(response); - json = await API.getJSONFromResponse(response); + json = API.getJSONFromResponse(response); let key = json.successful[0].key; json = await API.getItem(key, this, 'json'); @@ -1709,6 +1721,9 @@ describe('ItemsTests', function () { Helpers.assertEquals(collectionKey, json.collections[0]); }); + /** + * Date Modified should be updated when a field is changed if not included in upload + */ it('testDateModifiedChangeOnEdit', async function () { let json = await API.createAttachmentItem("linked_file", [], false, this, 'jsonData'); let modified = json.dateModified; @@ -1738,7 +1753,7 @@ describe('ItemsTests', function () { } ]; let response = await API.postItems(data); - json = await API.getJSONFromResponse(response); + json = API.getJSONFromResponse(response); assert.property(json.successful[0].data, 'deleted'); Helpers.assertEquals(1, json.successful[0].data.deleted); @@ -1767,12 +1782,10 @@ describe('ItemsTests', function () { let noteJSON = await API.createNoteItem("", parentItemKey, this, 'jsonData'); noteJSON.parentItem = false; noteJSON.collections = [collectionKey]; - let headers = { "Content-Type": "application/json" }; let response = await API.userPatch( config.userID, `items/${noteJSON.key}`, - JSON.stringify(noteJSON), - headers + JSON.stringify(noteJSON) ); Helpers.assert204(response); let json = await API.getItem(noteJSON.key, this, 'json'); @@ -1849,7 +1862,7 @@ describe('ItemsTests', function () { config.userID, `items?itemKey=${itemKey},${attachmentKey},${annotationKey}` ); - json = await API.getJSONFromResponse(checkResponse); + json = API.getJSONFromResponse(checkResponse); Helpers.assertNumResults(checkResponse, 0); }); @@ -1942,6 +1955,7 @@ describe('ItemsTests', function () { response = await API.userGet(config.userID, `settings/${settingKey}`); Helpers.assert404(response); + // Setting shouldn't be in delete log response = await API.userGet(config.userID, `deleted?since=${attachmentVersion}`); json = API.getJSONFromResponse(response); assert.notInclude(json.settings, settingKey); @@ -1966,7 +1980,7 @@ describe('ItemsTests', function () { ); Helpers.assert400ForObject( response, - "Linked files can only be added to user libraries" + { message: "Linked files can only be added to user libraries" } ); }); @@ -2019,7 +2033,7 @@ describe('ItemsTests', function () { "items/top?itemKey=" + annotationKey ); Helpers.assertNumResults(response, 1); - json = await API.getJSONFromResponse(response); + json = API.getJSONFromResponse(response); Helpers.assertEquals(attachmentKey, json[0].key); // Move attachment under regular item @@ -2037,7 +2051,7 @@ describe('ItemsTests', function () { "items/top?itemKey=" + annotationKey ); Helpers.assertNumResults(response, 1); - json = await API.getJSONFromResponse(response); + json = API.getJSONFromResponse(response); Helpers.assertEquals(itemKey, json[0].key); }); @@ -2069,7 +2083,7 @@ describe('ItemsTests', function () { JSON.stringify([json]), { "Content-Type": "application/json" } ); - Helpers.assert400ForObject(response, "'note' property is not valid for embedded images"); + Helpers.assert400ForObject(response, { message: "'note' property is not valid for embedded images" }); }); it('test_deleting_parent_item_should_delete_attachment_and_child_annotation', async function () { @@ -2122,15 +2136,15 @@ describe('ItemsTests', function () { API.useAPIKey(config.user2APIKey); jsonData.version = 0; const postData = JSON.stringify([jsonData]); - const headers = { "Content-Type": "application/json" }; const postResponse = await API.groupPost( config.ownedPrivateGroupID, "items", postData, - headers + { "Content-Type": "application/json" } ); - const jsonResponse = await API.getJSONFromResponse(postResponse); + const jsonResponse = API.getJSONFromResponse(postResponse); + // createdByUser shouldn't have changed assert.equal( jsonResponse.successful[0].meta.createdByUser.username, config.username @@ -2266,15 +2280,23 @@ describe('ItemsTests', function () { let attachmentKey = await API.createAttachmentItem("imported_url", { contentType: 'application/pdf', title: 'bbb' }, key, this, 'key'); await API.createAnnotationItem("image", { annotationComment: 'ccc' }, attachmentKey, this, 'key'); let response = await API.userGet(config.userID, `items/${attachmentKey}`); - let json = await API.getJSONFromResponse(response); + let json = API.getJSONFromResponse(response); Helpers.assertEquals(1, json.meta.numChildren); response = await API.userGet(config.userID, `items/${attachmentKey}/children`); Helpers.assert200(response); - json = await API.getJSONFromResponse(response); + json = API.getJSONFromResponse(response); Helpers.assertCount(1, json); Helpers.assertEquals('ccc', json[0].data.annotationComment); }); + /** + * If null is passed for a value, it should be treated the same as an empty string, not create + * a NULL in the database. + * + * TODO: Since we don't have direct access to the database, our test for this is changing the + * item type and then trying to retrieve it, which isn't ideal. Some way of checking the DB + * state would be useful. + */ it('test_should_treat_null_value_as_empty_string', async function () { let json = { itemType: 'book', @@ -2283,16 +2305,14 @@ describe('ItemsTests', function () { let response = await API.userPost( config.userID, "items", - JSON.stringify([json]), - { - "Content-Type": "application/json" - } + JSON.stringify([json]) ); Helpers.assert200ForObject(response); json = API.getJSONFromResponse(response); let key = json.successful[0].key; json = await API.getItem(key, this, 'json'); + // Change the item type to a type without the field json = { version: json.version, itemType: 'journalArticle' @@ -2300,10 +2320,7 @@ describe('ItemsTests', function () { await API.userPatch( config.userID, "items/" + key, - JSON.stringify(json), - { - "Content-Type": "application/json" - } + JSON.stringify(json) ); json = await API.getItem(key, this, 'json'); @@ -2334,7 +2351,13 @@ describe('ItemsTests', function () { assert.equal('text/html', json.library.links.alternate.type); }); + /** + * It should be possible to edit an existing PDF attachment without sending 'contentType' + * (which would cause a new attachment to be rejected) + * Disabled -- see note at Zotero_Item::checkTopLevelAttachment() + */ it('testPatchTopLevelAttachment', async function () { + this.skip(); let json = await API.createAttachmentItem("imported_url", { title: 'A', contentType: 'application/pdf', @@ -2467,7 +2490,7 @@ describe('ItemsTests', function () { config.userID, "items?includeTrashed=1" ); - let json = await API.getJSONFromResponse(response); + let json = API.getJSONFromResponse(response); Helpers.assertCount(3, json); let keys = [json[0].key, json[1].key, json[2].key]; assert.include(keys, key1); @@ -2479,7 +2502,7 @@ describe('ItemsTests', function () { config.userID, "items?itemKey=" + key2 + "," + key3 + "&includeTrashed=1" ); - json = await API.getJSONFromResponse(response); + json = API.getJSONFromResponse(response); Helpers.assertCount(2, json); keys = [json[0].key, json[1].key]; assert.include(keys, key2); @@ -2490,7 +2513,7 @@ describe('ItemsTests', function () { config.userID, "items/top?includeTrashed=1" ); - json = await API.getJSONFromResponse(response); + json = API.getJSONFromResponse(response); Helpers.assertCount(2, json); keys = [json[0].key, json[1].key]; assert.include(keys, key1); @@ -2536,7 +2559,7 @@ describe('ItemsTests', function () { } ); Helpers.assert200(response2); - const json = await API.getJSONFromResponse(response2); + const json = API.getJSONFromResponse(response2); Helpers.assert413ForObject(json); Helpers.assert409ForObject(json, { message: "Parent item " + parentKey + " not found", index: 1 }); Helpers.assertEquals(parentKey, json.failed[1].data.parentItem); @@ -2580,7 +2603,7 @@ describe('ItemsTests', function () { } ]; let response = await API.postItems(data); - json = await API.getJSONFromResponse(response); + json = API.getJSONFromResponse(response); assert.notProperty(json.successful[0].data, 'deleted'); }); @@ -2628,7 +2651,7 @@ describe('ItemsTests', function () { config.userID, "items/" + key ); - json = await API.getJSONFromResponse(response); + json = API.getJSONFromResponse(response); Helpers.assertEquals(date, json.data.date); // meta.parsedDate (JSON) @@ -2675,8 +2698,7 @@ describe('ItemsTests', function () { let response = await API.userPatch( config.userID, `items/${jsonData.key}`, - JSON.stringify(json), - { "Content-Type": "application/json" } + JSON.stringify(json) ); Helpers.assert204(response); }); @@ -2704,7 +2726,7 @@ describe('ItemsTests', function () { "items/" + jsonData.key, JSON.stringify(json) ); - assert.equal(response.status, 400, "Annotation must have a parent item"); + Helpers.assert400(response, "Annotation must have a parent item"); // Regular item json = { @@ -2716,7 +2738,7 @@ describe('ItemsTests', function () { "items/" + jsonData.key, JSON.stringify(json) ); - assert.equal(response.status, 400, "Parent item of annotation must be a PDF attachment"); + Helpers.assert400(response, "Parent item of annotation must be a PDF attachment"); // Linked-URL attachment json = { @@ -2728,7 +2750,7 @@ describe('ItemsTests', function () { "items/" + jsonData.key, JSON.stringify(json) ); - assert.equal(response.status, 400, "Parent item of annotation must be a PDF attachment"); + Helpers.assert400(response, "Parent item of annotation must be a PDF attachment"); }); it('testConvertChildNoteToParentViaPatch', async function () { @@ -2786,8 +2808,7 @@ describe('ItemsTests', function () { response = await API.userPost( config.userID, "items", - JSON.stringify([json]), - { "Content-Type": "application/json" } + JSON.stringify([json]) ); let msg = "Item " + json.key + " cannot be a child of itself"; // TEMP @@ -2799,12 +2820,12 @@ describe('ItemsTests', function () { let noteKey = await API.createNoteItem("Test", null, this, 'key'); let imageKey = await API.createAttachmentItem('embedded_image', { contentType: 'image/png' }, noteKey, this, 'key'); let response = await API.userGet(config.userID, `items/${noteKey}`); - let json = await API.getJSONFromResponse(response); + let json = API.getJSONFromResponse(response); Helpers.assertEquals(1, json.meta.numChildren); response = await API.userGet(config.userID, `items/${noteKey}/children`); Helpers.assert200(response); - json = await API.getJSONFromResponse(response); + json = API.getJSONFromResponse(response); Helpers.assertCount(1, json); Helpers.assertEquals(imageKey, json[0].key); }); diff --git a/tests/remote_js/test/3/keysTest.js b/tests/remote_js/test/3/keysTest.js index 854dd5bc..9393a24e 100644 --- a/tests/remote_js/test/3/keysTest.js +++ b/tests/remote_js/test/3/keysTest.js @@ -15,18 +15,13 @@ describe('KeysTests', function () { after(async function () { await API3After(); }); - // beforeEach(async function () { - // await API.userClear(config.userID); - // }); - - // afterEach(async function () { - // await API.userClear(config.userID); - // }); + // Private API it('testKeyCreateAndModifyWithCredentials', async function () { - await API.useAPIKey(""); + API.useAPIKey(""); let name = "Test " + Helpers.uniqueID(); + // Can't create on /users/:userID/keys with credentials let response = await API.userPost( config.userID, 'keys', @@ -43,6 +38,7 @@ describe('KeysTests', function () { ); Helpers.assert403(response); + // Create with credentials response = await API.post( 'keys', JSON.stringify({ @@ -59,7 +55,7 @@ describe('KeysTests', function () { {} ); Helpers.assert201(response); - let json = await API.getJSONFromResponse(response); + let json = API.getJSONFromResponse(response); let key = json.key; assert.equal(json.userID, config.userID); assert.equal(json.name, name); @@ -72,6 +68,7 @@ describe('KeysTests', function () { name = "Test " + Helpers.uniqueID(); + // Can't modify on /users/:userID/keys/:key with credentials response = await API.userPut( config.userID, "keys/" + key, @@ -88,6 +85,7 @@ describe('KeysTests', function () { ); Helpers.assert403(response); + // Modify with credentials response = await API.put( "keys/" + key, JSON.stringify({ @@ -102,7 +100,7 @@ describe('KeysTests', function () { }) ); Helpers.assert200(response); - json = await API.getJSONFromResponse(response); + json = API.getJSONFromResponse(response); key = json.key; assert.equal(json.name, name); @@ -114,9 +112,10 @@ describe('KeysTests', function () { }); it('testKeyCreateAndDelete', async function () { - await API.useAPIKey(''); + API.useAPIKey(''); const name = 'Test ' + Helpers.uniqueID(); + // Can't create anonymously let response = await API.userPost( config.userID, 'keys', @@ -129,6 +128,7 @@ describe('KeysTests', function () { ); Helpers.assert403(response); + // Create as root response = await API.userPost( config.userID, 'keys', @@ -145,13 +145,14 @@ describe('KeysTests', function () { } ); Helpers.assert201(response); - const json = await API.getJSONFromResponse(response); + const json = API.getJSONFromResponse(response); const key = json.key; assert.equal(config.username, json.username); assert.equal(config.displayName, json.displayName); assert.equal(name, json.name); assert.deepEqual({ user: { library: true, files: true } }, json.access); + // Delete anonymously (with embedded key) response = await API.userDelete(config.userID, 'keys/current', { 'Zotero-API-Key': key }); @@ -170,7 +171,7 @@ describe('KeysTests', function () { { "Zotero-API-Key": config.apiKey } ); Helpers.assert200(response); - const json = await API.getJSONFromResponse(response); + const json = API.getJSONFromResponse(response); assert.equal(config.apiKey, json.key); assert.equal(config.userID, json.userID); assert.equal(config.username, json.username); @@ -189,6 +190,7 @@ describe('KeysTests', function () { assert.notProperty(json, 'recentIPs'); }); + // Deprecated it('testGetKeyInfoWithUser', async function () { API.useAPIKey(""); const response = await API.userGet( @@ -209,6 +211,7 @@ describe('KeysTests', function () { assert.isOk(json.access.groups.all.write); }); + // Private API it('testKeyCreateWithEmailAddress', async function () { API.useAPIKey(""); let name = "Test " + Helpers.uniqueID(); @@ -246,7 +249,7 @@ describe('KeysTests', function () { }); it('testGetKeys', async function () { - // No anonymous access + // No anonymous access API.useAPIKey(''); let response = await API.userGet( config.userID, @@ -287,7 +290,7 @@ describe('KeysTests', function () { API.useAPIKey(""); const response = await API.get('keys/' + config.apiKey); Helpers.assert200(response); - const json = await API.getJSONFromResponse(response); + const json = API.getJSONFromResponse(response); assert.equal(config.apiKey, json.key); assert.equal(config.userID, json.userID); assert.property(json.access, 'user'); diff --git a/tests/remote_js/test/3/mappingsTest.js b/tests/remote_js/test/3/mappingsTest.js index 5581d3a9..c3341dd8 100644 --- a/tests/remote_js/test/3/mappingsTest.js +++ b/tests/remote_js/test/3/mappingsTest.js @@ -32,7 +32,7 @@ describe('MappingsTests', function () { it('test_should_return_fields_for_note_annotations', async function () { let response = await API.get("items/new?itemType=annotation&annotationType=highlight"); - let json = await API.getJSONFromResponse(response); + let json = API.getJSONFromResponse(response); assert.property(json, 'annotationText'); Helpers.assertEquals(json.annotationText, ''); }); @@ -75,7 +75,7 @@ describe('MappingsTests', function () { it('test_should_return_fields_for_highlight_annotations', async function () { const response = await API.get("items/new?itemType=annotation&annotationType=highlight"); - const json = await API.getJSONFromResponse(response); + const json = API.getJSONFromResponse(response); assert.property(json, 'annotationText'); assert.equal(json.annotationText, ''); }); @@ -83,7 +83,7 @@ describe('MappingsTests', function () { it('test_should_return_fields_for_all_annotation_types', async function () { for (let type of ['highlight', 'note', 'image']) { const response = await API.get(`items/new?itemType=annotation&annotationType=${type}`); - const json = await API.getJSONFromResponse(response); + const json = API.getJSONFromResponse(response); assert.property(json, 'annotationComment'); Helpers.assertEquals('', json.annotationComment); @@ -143,7 +143,7 @@ describe('MappingsTests', function () { it('test_should_return_fields_for_image_annotations', async function () { let response = await API.get('items/new?itemType=annotation&annotationType=image'); - let json = await API.getJSONFromResponse(response); + let json = API.getJSONFromResponse(response); Helpers.assertEquals(0, json.annotationPosition.width); Helpers.assertEquals(0, json.annotationPosition.height); }); diff --git a/tests/remote_js/test/3/noteTest.js b/tests/remote_js/test/3/noteTest.js index 798fc03e..ed8a50f8 100644 --- a/tests/remote_js/test/3/noteTest.js +++ b/tests/remote_js/test/3/noteTest.js @@ -6,7 +6,6 @@ const Helpers = require('../../helpers3.js'); const { API3Before, API3After } = require("../shared.js"); describe('NoteTests', function () { - //this.timeout(config.timeout); this.timeout(config.timeout); let content, json; @@ -63,7 +62,7 @@ describe('NoteTests', function () { it('testSaveUnchangedSanitizedNote', async function () { let json = await API.createNoteItem('Foo', false, this, 'json'); let response = await API.postItem(json.data, { "Content-Type": "application/json" }); - json = await API.getJSONFromResponse(response); + json = API.getJSONFromResponse(response); let unchanged = json.unchanged; assert.property(unchanged, 0); }); @@ -97,7 +96,7 @@ describe('NoteTests', function () { Helpers.assert200ForObject(response); - let jsonResponse = await API.getJSONFromResponse(response); + let jsonResponse = API.getJSONFromResponse(response); let data = jsonResponse.successful[0].data; assert.equal(note, data.note); }); @@ -137,7 +136,7 @@ describe('NoteTests', function () { json.data.note = val; let response = await API.postItem(json.data); - let jsonResp = await API.getJSONFromResponse(response); + let jsonResp = API.getJSONFromResponse(response); Helpers.assertEquals(val, jsonResp.successful[0].data.note); }); diff --git a/tests/remote_js/test/3/notificationTest.js b/tests/remote_js/test/3/notificationTest.js index 6a33e981..4d91c74e 100644 --- a/tests/remote_js/test/3/notificationTest.js +++ b/tests/remote_js/test/3/notificationTest.js @@ -6,7 +6,7 @@ const Helpers = require('../../helpers3.js'); const { API3Before, API3After, resetGroups } = require("../shared.js"); describe('NotificationTests', function () { - this.timeout(config.timeout); + this.timeout(0); before(async function () { await API3Before(); @@ -38,6 +38,9 @@ describe('NotificationTests', function () { }, response); }); + /** + * Grant an API key access to a group + */ it('testKeyAddLibraryNotification', async function () { API.useAPIKey(""); const name = "Test " + Helpers.uniqueID(); @@ -62,6 +65,7 @@ describe('NotificationTests', function () { try { json.access.groups = {}; + // Add a group to the key, which should trigger topicAdded json.access.groups[config.ownedPrivateGroupID] = { library: true, write: true @@ -79,8 +83,6 @@ describe('NotificationTests', function () { apiKeyID: String(apiKeyID), topic: '/groups/' + config.ownedPrivateGroupID }, response2); - - await API.superDelete("keys/" + apiKey); } // Clean up finally { @@ -111,6 +113,7 @@ describe('NotificationTests', function () { }) ); try { + // No notification when creating a new key Helpers.assertNotificationCount(0, response); } finally { @@ -124,6 +127,9 @@ describe('NotificationTests', function () { } }); + /** + * Create and delete group owned by user + */ it('testAddDeleteOwnedGroupNotification', async function () { API.useAPIKey(""); const json = await createKeyWithAllGroupAccess(config.userID); @@ -131,7 +137,7 @@ describe('NotificationTests', function () { try { const allGroupsKeys = await getKeysWithAllGroupAccess(config.userID); - + // Create new group owned by user const response = await createGroup(config.userID); const xml = API.getXMLFromResponse(response); const groupID = parseInt(Helpers.xpathEval(xml, "/atom:entry/zapi:groupID")); @@ -140,7 +146,7 @@ describe('NotificationTests', function () { Helpers.assertNotificationCount(Object.keys(allGroupsKeys).length, response); await Promise.all(allGroupsKeys.map(async function (key) { const response2 = await API.superGet(`keys/${key}?showid=1`); - const json2 = await API.getJSONFromResponse(response2); + const json2 = API.getJSONFromResponse(response2); Helpers.assertHasNotification({ event: "topicAdded", apiKeyID: String(json2.id), @@ -148,6 +154,7 @@ describe('NotificationTests', function () { }, response); })); } + // Delete group finally { const response = await API.superDelete(`groups/${groupID}`); Helpers.assert204(response); @@ -158,6 +165,7 @@ describe('NotificationTests', function () { }, response); } } + // Delete key finally { const response = await API.superDelete(`keys/${apiKey}`); try { @@ -187,6 +195,9 @@ describe('NotificationTests', function () { }, response); }); + /** + * Revoke access for a group from an API key that has access to all groups + */ it('testKeyRemoveLibraryFromAllGroupsNotification', async function () { API.useAPIKey(""); const removedGroup = config.ownedPrivateGroupID; @@ -194,15 +205,20 @@ describe('NotificationTests', function () { const apiKey = json.key; const apiKeyID = json.id; try { + // Get list of available groups API.useAPIKey(apiKey); const response = await API.userGet(config.userID, 'groups'); let groupIDs = API.getJSONFromResponse(response).map(group => group.id); + + // Remove one group, and replace access array with new set groupIDs = groupIDs.filter(groupID => groupID !== removedGroup); delete json.access.groups.all; for (let groupID of groupIDs) { json.access.groups[groupID] = {}; json.access.groups[groupID].library = true; } + + // Post new JSON, which should trigger topicRemoved for the removed group API.useAPIKey(""); const putResponse = await API.superPut(`keys/${apiKey}`, JSON.stringify(json)); Helpers.assert200(putResponse); @@ -237,7 +253,7 @@ describe('NotificationTests', function () { JSON.stringify(json) ); assert.equal(response.status, 201); - json = await API.getJSONFromResponse(response); + json = API.getJSONFromResponse(response); return json; } @@ -268,7 +284,7 @@ describe('NotificationTests', function () { async function getKeysWithAllGroupAccess(userID) { const response = await API.superGet("users/" + userID + "/keys"); assert.equal(response.status, 200); - const json = await API.getJSONFromResponse(response); + const json = API.getJSONFromResponse(response); return json.filter(keyObj => keyObj.access.groups.all.library).map(keyObj => keyObj.key); } @@ -343,24 +359,21 @@ describe('NotificationTests', function () { } }); + it('testKeyAddAllGroupsToNoneNotification', async function () { API.useAPIKey(""); - const json = await createKey(config.userID, { - userId: config.userId, - body: { - user: { - library: true, - }, - }, - }); + const json = await createKey(config.userID, + { user: { library: true } }, + ); const apiKey = json.key; const apiKeyId = json.id; try { + // Get list of available groups const response = await API.superGet(`users/${config.userID}/groups`); const groupIds = API.getJSONFromResponse(response).map(group => group.id); - json.access = {}; json.access.groups = []; + // Add all groups to the key, which should trigger topicAdded for each groups json.access.groups[0] = { library: true }; const putResponse = await API.superPut(`keys/${apiKey}`, JSON.stringify(json)); Helpers.assert200(putResponse); @@ -399,6 +412,7 @@ describe('NotificationTests', function () { const apiKeyID = json.id; try { + // Remove group from the key, which should trigger topicRemoved delete json.access.groups; const response = await API.superPut( `keys/${apiKey}`, diff --git a/tests/remote_js/test/3/objectTest.js b/tests/remote_js/test/3/objectTest.js index 9e53f016..2c367e67 100644 --- a/tests/remote_js/test/3/objectTest.js +++ b/tests/remote_js/test/3/objectTest.js @@ -50,6 +50,7 @@ describe('ObjectTests', function () { break; } + // HEAD request should include Total-Results let response = await API.userHead( config.userID, `${objectNamePlural}?key=${config.apiKey}&${keyProp}=${keys.join(',')}` @@ -150,6 +151,7 @@ describe('ObjectTests', function () { response = await API.userGet(config.userID, `${objectTypePlural}?${keyProp}=${keepKeys.join(',')}`); Helpers.assertNumResults(response, keepKeys.length); + // Add trailing comma to itemKey param, to test key parsing response = await API.userDelete(config.userID, `${objectTypePlural}?${keyProp}=${keepKeys.join(',')},`, { "If-Unmodified-Since-Version": libraryVersion }); @@ -159,9 +161,8 @@ describe('ObjectTests', function () { Helpers.assertNumResults(response, 0); }; - const _testPartialWriteFailure = async () => { + const _testPartialWriteFailure = async (objectType) => { let conditions = []; - const objectType = 'collection'; let json1 = { name: "Test" }; let json2 = { name: "1234567890".repeat(6554) }; let json3 = { name: "Test" }; @@ -219,6 +220,7 @@ describe('ObjectTests', function () { }; const _testPartialWriteFailureWithUnchanged = async (objectType) => { + await API.userClear(config.userID); let objectTypePlural = API.getPluralObjectType(objectType); let json1; @@ -365,7 +367,7 @@ describe('ObjectTests', function () { let response = await API.userGet(config.userID, "items?key=" + config.apiKey + "&format=keys&limit=1"); let libraryVersion1 = response.headers["last-modified-version"][0]; - const func = async (objectType, libraryVersion, url) => { + const testDelete = async (objectType, libraryVersion, url) => { const objectTypePlural = await API.getPluralObjectType(objectType); const response = await API.userDelete(config.userID, `${objectTypePlural}?key=${config.apiKey}${url}`, @@ -374,9 +376,9 @@ describe('ObjectTests', function () { return response.headers["last-modified-version"][0]; }; - let tempLibraryVersion = await func('collection', libraryVersion1, "&collectionKey=" + objectKeys.collection[0]); - tempLibraryVersion = await func('item', tempLibraryVersion, "&itemKey=" + objectKeys.item[0]); - tempLibraryVersion = await func('search', tempLibraryVersion, "&searchKey=" + objectKeys.search[0]); + let tempLibraryVersion = await testDelete('collection', libraryVersion1, "&collectionKey=" + objectKeys.collection[0]); + tempLibraryVersion = await testDelete('item', tempLibraryVersion, "&itemKey=" + objectKeys.item[0]); + tempLibraryVersion = await testDelete('search', tempLibraryVersion, "&searchKey=" + objectKeys.search[0]); let libraryVersion2 = tempLibraryVersion; // /deleted without 'since' should be an error @@ -387,9 +389,9 @@ describe('ObjectTests', function () { Helpers.assert400(response); // Delete second and third objects - tempLibraryVersion = await func('collection', tempLibraryVersion, "&collectionKey=" + objectKeys.collection.slice(1).join(',')); - tempLibraryVersion = await func('item', tempLibraryVersion, "&itemKey=" + objectKeys.item.slice(1).join(',')); - let libraryVersion3 = await func('search', tempLibraryVersion, "&searchKey=" + objectKeys.search.slice(1).join(',')); + tempLibraryVersion = await testDelete('collection', tempLibraryVersion, "&collectionKey=" + objectKeys.collection.slice(1).join(',')); + tempLibraryVersion = await testDelete('item', tempLibraryVersion, "&itemKey=" + objectKeys.item.slice(1).join(',')); + let libraryVersion3 = await testDelete('search', tempLibraryVersion, "&searchKey=" + objectKeys.search.slice(1).join(',')); // Request all deleted objects response = await API.userGet(config.userID, "deleted?key=" + config.apiKey + "&since=" + libraryVersion1); @@ -505,7 +507,7 @@ describe('ObjectTests', function () { } ]; const response = await API.postObjects(type, data); - const jsonResponse = await API.getJSONFromResponse(response); + const jsonResponse = API.getJSONFromResponse(response); assert.notProperty(jsonResponse.successful[0].data, 'deleted'); } }); @@ -545,7 +547,7 @@ describe('ObjectTests', function () { Helpers.assert200(response); - let json = await API.getJSONFromResponse(response); + let json = API.getJSONFromResponse(response); Helpers.assert200ForObject(response); const objectKey = json.successful[0].key; @@ -583,7 +585,7 @@ describe('ObjectTests', function () { ); Helpers.assert200(response); - json = await API.getJSONFromResponse(response); + json = API.getJSONFromResponse(response); switch (objectType) { case 'item': @@ -677,13 +679,13 @@ describe('ObjectTests', function () { { "Content-Type": "application/json" } ); Helpers.assert200(response); - let json = await API.getJSONFromResponse(response); + let json = API.getJSONFromResponse(response); Helpers.assert200ForObject(response, false, 0); Helpers.assert200ForObject(response, false, 1); response = await API.userGet(config.userID, objectTypePlural); Helpers.assert200(response); - json = await API.getJSONFromResponse(response); + json = API.getJSONFromResponse(response); switch (objectType) { case "item": json[0].data.title @@ -708,14 +710,14 @@ describe('ObjectTests', function () { { "Content-Type": "application/json" } ); Helpers.assert200(response); - json = await API.getJSONFromResponse(response); + json = API.getJSONFromResponse(response); Helpers.assert200ForObject(response, false, 0); Helpers.assert200ForObject(response, false, 1); // Check response = await API.userGet(config.userID, objectTypePlural); Helpers.assert200(response); - json = await API.getJSONFromResponse(response); + json = API.getJSONFromResponse(response); switch (objectTypePlural) { case "item": @@ -745,6 +747,7 @@ describe('ObjectTests', function () { Helpers.assert200ForObject(response); json = API.getJSONFromResponse(response); assert.property(json.successful[0].data, 'deleted'); + // TODO: Change to true in APIv4 if (type == 'item') { assert.equal(json.successful[0].data.deleted, 1); } @@ -779,7 +782,7 @@ describe('ObjectTests', function () { } ]; const response = await API.postItems(data); - const jsonResponse = await API.getJSONFromResponse(response); + const jsonResponse = API.getJSONFromResponse(response); assert.property(jsonResponse.successful[0].data, 'deleted'); assert.equal(jsonResponse.successful[0].data.deleted, 1); diff --git a/tests/remote_js/test/3/paramsTest.js b/tests/remote_js/test/3/paramsTest.js index 126788f8..e75f8c64 100644 --- a/tests/remote_js/test/3/paramsTest.js +++ b/tests/remote_js/test/3/paramsTest.js @@ -217,7 +217,7 @@ describe('ParamsTests', function () { ); Helpers.assert200(response); Helpers.assertNumResults(response, 1); - let json = await API.getJSONFromResponse(response); + let json = API.getJSONFromResponse(response); Helpers.assertEquals(keys[1], json[0].key); // Search by phrase @@ -228,7 +228,7 @@ describe('ParamsTests', function () { ); Helpers.assert200(response); Helpers.assertNumResults(response, 1); - json = await API.getJSONFromResponse(response); + json = API.getJSONFromResponse(response); Helpers.assertEquals(keys[0], json[0].key); // Search by non-matching phrase @@ -337,7 +337,7 @@ describe('ParamsTests', function () { ); Helpers.assert200(response); Helpers.assertNumResults(response, 1); - let json = await API.getJSONFromResponse(response); + let json = API.getJSONFromResponse(response); Helpers.assertEquals(keys[1], json[0].key); // No results @@ -387,6 +387,8 @@ describe('ParamsTests', function () { title: title2, date: "November 25, 2012" }, this, 'key')); + + // Search by title let response = await API.userGet( config.userID, "items?q=" + encodeURIComponent(title1) @@ -395,6 +397,8 @@ describe('ParamsTests', function () { Helpers.assertNumResults(response, 1); let json = API.getJSONFromResponse(response); Helpers.assertEquals(keys[0], json[0].key); + + // Search by both by title, date asc response = await API.userGet( config.userID, "items?q=title&sort=date&direction=asc" @@ -404,6 +408,8 @@ describe('ParamsTests', function () { json = API.getJSONFromResponse(response); Helpers.assertEquals(keys[1], json[0].key); Helpers.assertEquals(keys[0], json[1].key); + + // Search by both by title, date asc, with old-style parameters response = await API.userGet( config.userID, "items?q=title&order=date&sort=asc" @@ -413,6 +419,8 @@ describe('ParamsTests', function () { json = API.getJSONFromResponse(response); Helpers.assertEquals(keys[1], json[0].key); Helpers.assertEquals(keys[0], json[1].key); + + // Search by both by title, date desc response = await API.userGet( config.userID, "items?q=title&sort=date&direction=desc" @@ -422,6 +430,8 @@ describe('ParamsTests', function () { json = API.getJSONFromResponse(response); Helpers.assertEquals(keys[0], json[0].key); Helpers.assertEquals(keys[1], json[1].key); + + // Search by both by title, date desc, with old-style parameters response = await API.userGet( config.userID, "items?q=title&order=date&sort=desc" @@ -535,7 +545,7 @@ describe('ParamsTests', function () { Helpers.assert200(response); Helpers.assertNumResults(response, 1); Helpers.assertTotalResults(response, 1); - let json = await API.getJSONFromResponse(response); + let json = API.getJSONFromResponse(response); Helpers.assertEquals(keys[0], json[0].key); response = await API.userGet( diff --git a/tests/remote_js/test/3/permissionTest.js b/tests/remote_js/test/3/permissionTest.js index c31dbbdf..91759d1e 100644 --- a/tests/remote_js/test/3/permissionTest.js +++ b/tests/remote_js/test/3/permissionTest.js @@ -30,6 +30,7 @@ describe('PermissionsTests', function () { const response = await API.get(`users/${config.userID}/groups`); Helpers.assertStatusCode(response, 200); + // Make sure they're the right groups const json = API.getJSONFromResponse(response); const groupIDs = json.map(obj => String(obj.id)); assert.include(groupIDs, String(config.ownedPublicGroupID), `Owned public group ID ${config.ownedPublicGroupID} not found`); @@ -42,6 +43,7 @@ describe('PermissionsTests', function () { const response = await API.get(`users/${config.userID}/groups?content=json`); Helpers.assertStatusCode(response, 200); + // Make sure they're the right groups const xml = API.getXMLFromResponse(response); const groupIDs = Helpers.xpathEval(xml, '//atom:entry/zapi:groupID', false, true); assert.include(groupIDs, String(config.ownedPublicGroupID), `Owned public group ID ${config.ownedPublicGroupID} not found`); @@ -49,6 +51,9 @@ describe('PermissionsTests', function () { Helpers.assertTotalResults(response, config.numPublicGroups); }); + /** + * A key without note access shouldn't be able to create a note + */ it('testKeyNoteAccessWriteError', async function () { this.skip(); //disabled }); @@ -133,13 +138,13 @@ describe('PermissionsTests', function () { ); try { - await API.useAPIKey(config.apiKey); + API.useAPIKey(config.apiKey); let response = await API.groupGet(groupID, "items"); Helpers.assert200(response); Helpers.assertNumResults(response, 1); // An anonymous request should fail, because libraryReading is members - await API.useAPIKey(false); + API.useAPIKey(false); response = await API.groupGet(groupID, "items"); Helpers.assert403(response); } @@ -327,7 +332,7 @@ describe('PermissionsTests', function () { // totalResults with limit response = await API.userGet( config.userID, - "items?limit=1" + "items?format=atom&limit=1" ); Helpers.assertNumResults(response, 1); Helpers.assertTotalResults(response, bookKeys.length); @@ -335,7 +340,7 @@ describe('PermissionsTests', function () { // And without limit response = await API.userGet( config.userID, - "items" + "items?format=atom" ); Helpers.assertNumResults(response, bookKeys.length); Helpers.assertTotalResults(response, bookKeys.length); @@ -343,7 +348,7 @@ describe('PermissionsTests', function () { // Top response = await API.userGet( config.userID, - "items/top" + "items/top?format=atom" ); Helpers.assertNumResults(response, bookKeys.length); Helpers.assertTotalResults(response, bookKeys.length); @@ -351,7 +356,7 @@ describe('PermissionsTests', function () { // Collection response = await API.userGet( config.userID, - "collections/" + collectionKey + "/items" + "collections/" + collectionKey + "/items?format=atom" ); Helpers.assertNumResults(response, bookKeys.length); Helpers.assertTotalResults(response, bookKeys.length); diff --git a/tests/remote_js/test/3/publicationTest.js b/tests/remote_js/test/3/publicationTest.js index a0b5101e..3eb20f42 100644 --- a/tests/remote_js/test/3/publicationTest.js +++ b/tests/remote_js/test/3/publicationTest.js @@ -11,7 +11,7 @@ const { JSDOM } = require("jsdom"); describe('PublicationTests', function () { - this.timeout(config.timeout); + this.timeout(0); let toDelete = []; const s3Client = new S3Client({ region: "us-east-1" }); @@ -54,7 +54,7 @@ describe('PublicationTests', function () { }); it('test_should_show_publications_urls_in_json_response_for_multi_object_request', async function () { - await API.useAPIKey(config.apiKey); + API.useAPIKey(config.apiKey); const itemKey1 = await API.createItem("book", { inPublications: true }, this, 'key'); const itemKey2 = await API.createItem("book", { inPublications: true }, this, 'key'); @@ -63,11 +63,13 @@ describe('PublicationTests', function () { const links = await API.parseLinkHeader(response); + // Entry rel="self" Helpers.assertRegExp( `https?://[^/]+/users/${config.userID}/publications/items/(${itemKey1}|${itemKey2})`, json[0].links.self.href ); + // rel="next" Helpers.assertRegExp( `https?://[^/]+/users/${config.userID}/publications/items`, links.next @@ -78,8 +80,10 @@ describe('PublicationTests', function () { it('test_should_trigger_notification_on_publications_topic', async function () { API.useAPIKey(config.apiKey); + // Create item const response = await API.createItem('book', { inPublications: true }, this, 'response'); const version = API.getJSONFromResponse(response).successful[0].version; + // Test notification for publications topic (in addition to regular library) Helpers.assertNotificationCount(2, response); Helpers.assertHasNotification({ event: 'topicUpdated', @@ -96,7 +100,7 @@ describe('PublicationTests', function () { API.useAPIKey(config.apiKey); const itemKey = await API.createItem('book', { inPublications: true }, this, 'key'); const response = await API.get(`users/${config.userID}/publications/items/${itemKey}?format=atom`); - const xml = await API.getXMLFromResponse(response); + const xml = API.getXMLFromResponse(response); // id Helpers.assertRegExp( @@ -133,11 +137,11 @@ describe('PublicationTests', function () { }); it('test_should_show_publications_urls_in_json_response_for_single_object_request', async function () { - await API.useAPIKey(config.apiKey); + API.useAPIKey(config.apiKey); const itemKey = await API.createItem("book", { inPublications: true }, this, 'key'); const response = await API.get(`users/${config.userID}/publications/items/${itemKey}`); - const json = await API.getJSONFromResponse(response); + const json = API.getJSONFromResponse(response); // rel="self" Helpers.assertRegExp( @@ -155,28 +159,34 @@ describe('PublicationTests', function () { it('test_shouldnt_include_hidden_child_items_in_numChildren', async function () { API.useAPIKey(config.apiKey); + // Create parent item const parentItemKey = await API.createItem('book', { inPublications: true }, this, 'key'); + // Create shown child attachment const json1 = await API.getItemTemplate('attachment&linkMode=imported_file'); json1.title = 'A'; json1.parentItem = parentItemKey; json1.inPublications = true; + // Create shown child note const json2 = await API.getItemTemplate('note'); json2.note = 'B'; json2.parentItem = parentItemKey; json2.inPublications = true; + // Create hidden child attachment const json3 = await API.getItemTemplate('attachment&linkMode=imported_file'); json3.title = 'C'; json3.parentItem = parentItemKey; + // Create deleted child attachment const json4 = await API.getItemTemplate('note'); json4.note = 'D'; json4.parentItem = parentItemKey; json4.inPublications = true; json4.deleted = true; + // Create hidden deleted child attachment const json5 = await API.getItemTemplate('attachment&linkMode=imported_file'); json5.title = 'E'; json5.parentItem = parentItemKey; @@ -185,6 +195,7 @@ describe('PublicationTests', function () { let response = await API.userPost(config.userID, 'items', JSON.stringify([json1, json2, json3, json4, json5])); Helpers.assert200(response); + // Anonymous read API.useAPIKey(''); response = await API.userGet(config.userID, `publications/items/${parentItemKey}`); @@ -216,7 +227,7 @@ describe('PublicationTests', function () { json = await API.getItemTemplate("attachment&linkMode=linked_file"); json.inPublications = true; json.parentItem = itemKey; - await API.useAPIKey(config.apiKey); + API.useAPIKey(config.apiKey); response = await API.userPost( config.userID, "items", @@ -273,7 +284,7 @@ describe('PublicationTests', function () { // JSON let response = await API.userGet(config.userID, `publications/items/${itemKey}`); Helpers.assert200(response); - let json = await API.getJSONFromResponse(response); + let json = API.getJSONFromResponse(response); assert.equal(config.displayName, json.library.name); assert.equal('user', json.library.type); @@ -310,12 +321,16 @@ describe('PublicationTests', function () { it('test_should_return_404_for_anonymous_request_for_item_not_in_publications', async function () { API.useAPIKey(config.apiKey); + // Create item const key = await API.createItem("book", [], this, 'key'); - API.useAPIKey(); + + // Fetch anonymously + API.useAPIKey(''); const response = await API.get("users/" + config.userID + "/publications/items/" + key, { "Content-Type": "application/json" }); Helpers.assert404(response); }); + // Test read requests for empty publications list it('test_should_return_no_results_for_empty_publications_list_with_key', async function () { API.useAPIKey(config.apiKey); let response = await API.get(`users/${config.userID}/publications/items`); @@ -335,20 +350,20 @@ describe('PublicationTests', function () { // JSON let response = await API.userGet(config.userID, 'publications/items'); Helpers.assert200(response); - let json = await API.getJSONFromResponse(response); + let json = API.getJSONFromResponse(response); assert.include(json.map(item => item.key), itemKey); // Atom response = await API.userGet(config.userID, 'publications/items?format=atom'); Helpers.assert200(response); - let xml = await API.getXMLFromResponse(response); + let xml = API.getXMLFromResponse(response); let xpath = Helpers.xpathEval(xml, '//atom:entry/zapi:key'); assert.include(xpath, itemKey); }); it('test_should_show_publications_urls_in_atom_response_for_multi_object_request', async function () { let response = await API.get(`users/${config.userID}/publications/items?format=atom`); - let xml = await API.getXMLFromResponse(response); + let xml = API.getXMLFromResponse(response); // id let id = Helpers.xpathEval(xml, '//atom:id'); @@ -380,7 +395,10 @@ describe('PublicationTests', function () { it('test_should_return_404_for_authenticated_request_for_item_not_in_publications', async function () { API.useAPIKey(config.apiKey); + // Create item let key = await API.createItem("book", [], this, 'key'); + + // Fetch anonymously let response = await API.get("users/" + config.userID + "/publications/items/" + key, { "Content-Type": "application/json" }); Helpers.assert404(response); }); @@ -456,8 +474,8 @@ describe('PublicationTests', function () { }); it('test_shouldnt_remove_inPublications_on_POST_without_property', async function () { - await API.useAPIKey(config.apiKey); - const json = await API.getItemTemplate('book'); + API.useAPIKey(config.apiKey); + let json = await API.getItemTemplate('book'); json.inPublications = true; const response = await API.userPost(config.userID, 'items', JSON.stringify([json])); @@ -465,17 +483,16 @@ describe('PublicationTests', function () { const key = API.getJSONFromResponse(response).successful[0].key; const version = response.headers['last-modified-version'][0]; - const newJson = { + json = { key: key, version: version, - title: 'Test', - inPublications: false + title: 'Test' }; const newResponse = await API.userPost( config.userID, 'items', - JSON.stringify([newJson]), + JSON.stringify([json]), { 'Content-Type': 'application/json' } ); @@ -483,7 +500,7 @@ describe('PublicationTests', function () { const newJsonResponse = API.getJSONFromResponse(newResponse); - assert.notProperty(newJsonResponse.successful[0].data, 'inPublications'); + assert.ok(newJsonResponse.successful[0].data.inPublications); }); it('test_should_return_404_for_searches_request', async function () { @@ -494,13 +511,16 @@ describe('PublicationTests', function () { it('test_shouldnt_show_child_items_in_top_mode', async function () { API.useAPIKey(config.apiKey); + // Create parent item let parentItemKey = await API.createItem("book", { title: 'A', inPublications: true }, this, 'key'); + // Create shown child attachment let json1 = await API.getItemTemplate("attachment&linkMode=imported_file"); json1.title = 'B'; json1.parentItem = parentItemKey; json1.inPublications = true; + // Create hidden child attachment let json2 = await API.getItemTemplate("attachment&linkMode=imported_file"); json2.title = 'C'; json2.parentItem = parentItemKey; @@ -512,6 +532,7 @@ describe('PublicationTests', function () { ); Helpers.assert200(response); + // Anonymous read API.useAPIKey(""); response = await API.userGet( @@ -520,7 +541,7 @@ describe('PublicationTests', function () { ); Helpers.assert200(response); - let json = await API.getJSONFromResponse(response); + let json = API.getJSONFromResponse(response); assert.equal(json.length, 1); @@ -531,8 +552,10 @@ describe('PublicationTests', function () { it('test_shouldnt_show_child_item_not_in_publications_for_item_children_request', async function () { API.useAPIKey(config.apiKey); + // Create parent item const parentItemKey = await API.createItem("book", { title: 'A', inPublications: true }, this, 'key'); + // Create shown child attachment const json1 = await API.getItemTemplate("attachment&linkMode=imported_file"); json1.title = 'B'; json1.parentItem = parentItemKey; @@ -557,17 +580,21 @@ describe('PublicationTests', function () { it('test_shouldnt_show_child_item_not_in_publications', async function () { API.useAPIKey(config.apiKey); + // Create parent item const parentItemKey = await API.createItem('book', { title: 'A', inPublications: true }, this, 'key'); + // Create shown child attachment const json1 = await API.getItemTemplate('attachment&linkMode=imported_file'); json1.title = 'B'; json1.parentItem = parentItemKey; json1.inPublications = true; + // Create hidden child attachment const json2 = await API.getItemTemplate('attachment&linkMode=imported_file'); json2.title = 'C'; json2.parentItem = parentItemKey; const response = await API.userPost(config.userID, 'items', JSON.stringify([json1, json2])); Helpers.assert200(response); + // Anonymous read API.useAPIKey(''); const readResponse = await API.userGet(config.userID, 'publications/items'); Helpers.assert200(readResponse); @@ -592,14 +619,14 @@ describe('PublicationTests', function () { }); it('test_should_return_405_for_authenticated_write', async function () { - await API.useAPIKey(config.apiKey); + API.useAPIKey(config.apiKey); const json = await API.getItemTemplate('book'); const response = await API.userPost(config.userID, 'publications/items', JSON.stringify(json), { 'Content-Type': 'application/json' }); Helpers.assert405(response); }); it('test_shouldnt_show_trashed_item_in_versions_response', async function () { - await API.useAPIKey(config.apiKey); + API.useAPIKey(config.apiKey); let itemKey1 = await API.createItem("book", { inPublications: true }, this, 'key'); let itemKey2 = await API.createItem("book", { inPublications: true, deleted: true }, this, 'key'); @@ -608,9 +635,9 @@ describe('PublicationTests', function () { "publications/items?format=versions" ); Helpers.assert200(response); - let json = await API.getJSONFromResponse(response); - assert.equal(json.hasOwnProperty(itemKey1), true); - assert.equal(json.hasOwnProperty(itemKey2), false); + let json = API.getJSONFromResponse(response); + assert.property(json, itemKey1); + assert.notProperty(json, itemKey2); // Shouldn't show with includeTrashed=1 here response = await API.userGet( @@ -618,13 +645,12 @@ describe('PublicationTests', function () { "publications/items?format=versions&includeTrashed=1" ); Helpers.assert200(response); - json = await API.getJSONFromResponse(response); - assert.equal(json.hasOwnProperty(itemKey1), true); - assert.equal(json.hasOwnProperty(itemKey2), false); + json = API.getJSONFromResponse(response); + assert.property(json, itemKey1); + assert.notProperty(json, itemKey2); }); it('test_should_include_download_details', async function () { - API.useAPIKey(config.apiKey); const file = "work/file"; const fileContents = Helpers.getRandomUnicodeString(); const contentType = "text/html"; diff --git a/tests/remote_js/test/3/relationTest.js b/tests/remote_js/test/3/relationTest.js index 004f8be4..0d21acd8 100644 --- a/tests/remote_js/test/3/relationTest.js +++ b/tests/remote_js/test/3/relationTest.js @@ -70,7 +70,7 @@ describe('RelationsTests', function () { } // And item 2, since related items are bidirectional - const item2JSON2 =(await API.getItem(item2JSON.key, true, 'json')).data; + const item2JSON2 = (await API.getItem(item2JSON.key, true, 'json')).data; assert.equal(1, Object.keys(item2JSON2.relations).length); assert.equal(item1URI, item2JSON2.relations["dc:relation"]); @@ -265,7 +265,7 @@ describe('RelationsTests', function () { 'dc:relation': `http://zotero.org/users/${config.userID}/items/${item1Data.key}` }; const response = await API.postItems([item1Data, item2Data]); - Helpers.assert200(response); + Helpers.assert200ForObject(response, { index: 0 }); Helpers.assertUnchangedForObject(response, { index: 1 }); }); @@ -369,6 +369,38 @@ describe('RelationsTests', function () { "collections", JSON.stringify([json]) ); - Helpers.assert200(response); + Helpers.assert200ForObject(response); + }); + + it('test_should_add_a_URL_to_a_relation_with_PATCH', async function () { + const relations = { + "dc:replaces": [ + "http://zotero.org/users/" + config.userID + "/items/AAAAAAAA" + ] + }; + + const itemJSON = await API.createItem("book", { + relations: relations + }, true, 'jsonData'); + + relations["dc:replaces"].push("http://zotero.org/users/" + config.userID + "/items/BBBBBBBB"); + + const patchJSON = { + version: itemJSON.version, + relations: relations + }; + const response = await API.userPatch( + config.userID, + "items/" + itemJSON.key, + JSON.stringify(patchJSON) + ); + Helpers.assert204(response); + + // Make sure the array was updated + const json = (await API.getItem(itemJSON.key, 'json')).data; + assert.equal(Object.keys(json.relations).length, Object.keys(relations).length); + assert.equal(json.relations['dc:replaces'].length, relations['dc:replaces'].length); + assert.include(json.relations['dc:replaces'], relations['dc:replaces'][0]); + assert.include(json.relations['dc:replaces'], relations['dc:replaces'][1]); }); }); diff --git a/tests/remote_js/test/3/schemaTest.js b/tests/remote_js/test/3/schemaTest.js new file mode 100644 index 00000000..37ef2ecb --- /dev/null +++ b/tests/remote_js/test/3/schemaTest.js @@ -0,0 +1,39 @@ +var config = require('config'); +const { API3Before, API3After } = require("../shared.js"); + +describe('SchemaTests', function () { + this.timeout(config.timeout); + + before(async function () { + await API3Before(); + }); + + after(async function () { + await API3After(); + }); + + it('test_should_reject_download_from_old_client_for_item_using_newer_schema', async function () { + this.skip(); + }); + it('test_should_not_reject_download_from_old_client_for_collection_using_legacy_schema', async function () { + this.skip(); + }); + it('test_should_not_reject_download_from_old_client_for_search_using_legacy_schema', async function () { + this.skip(); + }); + it('test_should_not_reject_download_from_old_client_for_item_using_legacy_schema', async function () { + this.skip(); + }); + it('test_should_not_reject_download_from_old_client_for_attachment_using_legacy_schema', async function () { + this.skip(); + }); + it('test_should_not_reject_download_from_old_client_for_linked_file_attachment_using_legacy_schema', async function () { + this.skip(); + }); + it('test_should_not_reject_download_from_old_client_for_note_using_legacy_schema', async function () { + this.skip(); + }); + it('test_should_not_reject_download_from_old_client_for_child_note_using_legacy_schema', async function () { + this.skip(); + }); +}); diff --git a/tests/remote_js/test/3/settingsTest.js b/tests/remote_js/test/3/settingsTest.js index f10c8786..18ad320b 100644 --- a/tests/remote_js/test/3/settingsTest.js +++ b/tests/remote_js/test/3/settingsTest.js @@ -448,15 +448,15 @@ describe('SettingsTests', function () { // Check response = await API.userGet( config.userID, - `settings/${settingKey}` + `settings` ); Helpers.assert200(response); Helpers.assertContentType(response, "application/json"); assert.equal(parseInt(response.headers['last-modified-version'][0]), libraryVersion); json = JSON.parse(response.data); assert.isNotNull(json); - assert.deepEqual(json.value, newValue); - assert.equal(parseInt(json.version), libraryVersion); + assert.deepEqual(json[settingKey].value, newValue); + assert.equal(parseInt(json[settingKey].version), libraryVersion); }); it('testUnsupportedSettingMultiple', async function () { diff --git a/tests/remote_js/test/3/sortTest.js b/tests/remote_js/test/3/sortTest.js index 170aadd5..565c0b9e 100644 --- a/tests/remote_js/test/3/sortTest.js +++ b/tests/remote_js/test/3/sortTest.js @@ -94,12 +94,14 @@ describe('SortTests', function () { let correct = {}; titlesSorted.forEach((title) => { let index = titlesToIndex[title]; + // The key at position k in itemKeys should be at the same position in keys correct[index] = keys[index]; }); correct = Object.keys(correct).map(key => correct[key]); assert.deepEqual(correct, keys); }); + // Same thing, but with order parameter for backwards compatibility it('testSortTopItemsTitleOrder', async function () { let response = await API.userGet( config.userID, @@ -147,9 +149,36 @@ describe('SortTests', function () { correct[i] = itemKeys[parseInt(entry[0])]; }); correct = Object.keys(correct).map(key => correct[key]); + // Check attachment and note, which should fall back to ordered added (itemID) assert.deepEqual(correct, keys); }); it('testSortTopItemsCreator', async function () { + let response = await API.userGet( + config.userID, + "items/top?format=keys&sort=creator" + ); + Helpers.assertStatusCode(response, 200); + let keys = response.data.trim().split("\n"); + let namesCopy = { ...names }; + let sortFunction = function (a, b) { + if (a === '' && b !== '') return 1; + if (b === '' && a !== '') return -1; + if (a < b) return -1; + if (a > b) return 11; + return 0; + }; + let namesEntries = Object.entries(namesCopy); + namesEntries.sort((a, b) => sortFunction(a[1], b[1])); + assert.equal(Object.keys(namesEntries).length, keys.length); + let correct = {}; + namesEntries.forEach((entry, i) => { + correct[i] = itemKeys[parseInt(entry[0])]; + }); + correct = Object.keys(correct).map(key => correct[key]); + assert.deepEqual(correct, keys); + }); + + it('testSortTopItemsCreatorOrder', async function () { let response = await API.userGet( config.userID, "items/top?format=keys&order=creator" @@ -228,6 +257,7 @@ describe('SortTests', function () { dateModified: '2014-03-02T01:00:00Z' }, this, 'jsonData')); + // Get sorted keys dataArray.sort(function (a, b) { return new Date(a.dateAdded) - new Date(b.dateAdded); }); @@ -331,7 +361,7 @@ describe('SortTests', function () { assert.deepEqual(keysByDateAddedDescending, keys); }); - + // Sort by item type it('test_sort_top_level_items_by_item_type', async function () { const response = await API.userGet( config.userID, @@ -391,6 +421,8 @@ describe('SortTests', function () { dateAdded: '2014-02-02T00:00:00Z', dateModified: '2014-03-02T01:00:00Z' }, this, 'jsonData')); + + // Get sorted keys dataArray.sort((a, b) => { return new Date(b.dateAdded) - new Date(a.dateAdded); }); @@ -399,6 +431,8 @@ describe('SortTests', function () { return new Date(b.dateModified) - new Date(a.dateModified); }); const keysByDateModifiedDescending = dataArray.map(data => data.key); + + // Tests let response = await API.userGet(config.userID, "items?format=keys"); Helpers.assert200(response); assert.deepEqual(keysByDateModifiedDescending, response.data.trim().split('\n')); diff --git a/tests/remote_js/test/3/tagTest.js b/tests/remote_js/test/3/tagTest.js index 4f8e56db..710caa01 100644 --- a/tests/remote_js/test/3/tagTest.js +++ b/tests/remote_js/test/3/tagTest.js @@ -42,6 +42,34 @@ describe('TagTests', function () { Helpers.assertStatusForObject(response, 'failed', 0, 400, "Tag must be an object"); }); + it('test_should_add_tag_to_item', async function () { + let json = await API.getItemTemplate("book"); + json.tags.push({ tag: "A" }); + + let response = await API.postItem(json); + Helpers.assert200ForObject(response); + json = API.getJSONFromResponse(response).successful[0].data; + + json.tags.push({ tag: "C" }); + response = await API.postItem(json); + Helpers.assert200ForObject(response); + json = API.getJSONFromResponse(response).successful[0].data; + + json.tags.push({ tag: "B" }); + response = await API.postItem(json); + Helpers.assert200ForObject(response); + json = API.getJSONFromResponse(response).successful[0].data; + + json.tags.push({ tag: "D" }); + response = await API.postItem(json); + Helpers.assert200ForObject(response); + let tags = json.tags; + json = API.getJSONFromResponse(response).successful[0].data; + + assert.deepEqual(tags, json.tags); + }); + + it('testTagSearch', async function () { const tags1 = ["a", "aa", "b"]; const tags2 = ["b", "c", "cc"]; @@ -174,6 +202,10 @@ describe('TagTests', function () { Helpers.assertNumResults(response, 1); }); + /** + * When modifying a tag on an item, only the item itself should have its + * version updated, not other items that had (and still have) the same tag + */ it('testTagAddItemVersionChange', async function () { let data1 = await API.createItem("book", { tags: [{ @@ -590,9 +622,7 @@ describe('TagTests', function () { { tag: "etest" }, ]; - let response = await API.postItem(data, { - headers: { "Content-Type": "application/json" }, - }); + let response = await API.postItem(data); Helpers.assert200(response); Helpers.assert200ForObject(response); @@ -627,6 +657,7 @@ describe('TagTests', function () { Helpers.assert200(response); Helpers.assert200ForObject(response); + // Item version should be one greater than last update data1 = (await API.getItem(data1.key, this, 'json')).data; data2 = (await API.getItem(data2.key, this, 'json')).data; assert.equal(version + 1, data2.version); @@ -645,7 +676,7 @@ describe('TagTests', function () { { tag: "b" } ] }; - const json = await API.createItem('book', createItemData, this, 'responseJSON'); + await API.createItem('book', createItemData, this, 'responseJSON'); const json2 = await API.getItem(itemKey, this, 'json'); const data = json2.data; @@ -673,26 +704,26 @@ describe('TagTests', function () { json.tags = [{ tag: "A" }]; let response = await API.postItem(json); Helpers.assert200ForObject(response); - json = await API.getJSONFromResponse(response); + json = API.getJSONFromResponse(response); json = json.successful[0].data; json.tags.push({ tag: "C" }); response = await API.postItem(json); Helpers.assert200ForObject(response); - json = await API.getJSONFromResponse(response); + json = API.getJSONFromResponse(response); json = json.successful[0].data; json.tags.push({ tag: "B" }); response = await API.postItem(json); Helpers.assert200ForObject(response); - json = await API.getJSONFromResponse(response); + json = API.getJSONFromResponse(response); json = json.successful[0].data; json.tags.push({ tag: "D" }); response = await API.postItem(json); Helpers.assert200ForObject(response); let tags = json.tags; - json = await API.getJSONFromResponse(response); + json = API.getJSONFromResponse(response); json = json.successful[0].data; assert.deepEqual(tags, json.tags); @@ -724,12 +755,10 @@ describe('TagTests', function () { json = await API.createItem('book', { tags: [{ tag: "b" }] }, this, 'jsonData'); - let itemKey2 = json.key; json = await API.createItem("book", { tags: [{ tag: "b" }] }, this, 'jsonData'); - let itemKey3 = json.key; const response = await API.userDelete( config.userID, diff --git a/tests/remote_js/test/3/translationTest.js b/tests/remote_js/test/3/translationTest.js index c706de94..fc6c45c9 100644 --- a/tests/remote_js/test/3/translationTest.js +++ b/tests/remote_js/test/3/translationTest.js @@ -113,7 +113,7 @@ describe('TranslationTests', function () { Helpers.assert200(response); Helpers.assert200ForObject(response, false, 0); Helpers.assert200ForObject(response, false, 1); - let json = await API.getJSONFromResponse(response); + let json = API.getJSONFromResponse(response); // Check item let itemKey = json.success[0]; diff --git a/tests/remote_js/test/3/versionTest.js b/tests/remote_js/test/3/versionTest.js index d339e720..244b9787 100644 --- a/tests/remote_js/test/3/versionTest.js +++ b/tests/remote_js/test/3/versionTest.js @@ -20,7 +20,7 @@ describe('VersionsTests', function () { return string.charAt(0).toUpperCase() + string.slice(1); }; - const _modifyJSONObject = async (objectType, json) => { + const _modifyJSONObject = (objectType, json) => { switch (objectType) { case "collection": json.name = "New Name " + Helpers.uniqueID(); @@ -97,6 +97,7 @@ describe('VersionsTests', function () { json = JSON.parse(data.content); assert.equal(objectVersion, json.version); assert.equal(objectVersion, data.version); + response = await API.userGet( config.userID, `${objectTypePlural}?limit=1` @@ -121,7 +122,6 @@ describe('VersionsTests', function () { `${objectTypePlural}/${objectKey}`, JSON.stringify(json), { - 'Content-Type': 'application/json', 'If-Unmodified-Since-Version': objectVersion - 1 } ); @@ -133,7 +133,6 @@ describe('VersionsTests', function () { `${objectTypePlural}/${objectKey}`, JSON.stringify(json), { - 'Content-Type': 'application/json', 'If-Unmodified-Since-Version': objectVersion } ); @@ -152,6 +151,8 @@ describe('VersionsTests', function () { Helpers.assertStatusCode(response, 204); const newObjectVersion2 = response.headers['last-modified-version'][0]; assert.isAbove(parseInt(newObjectVersion2), parseInt(newObjectVersion)); + + // Make sure new library version matches new object versio response = await API.userGet( config.userID, `${objectTypePlural}?limit=1` @@ -159,37 +160,9 @@ describe('VersionsTests', function () { Helpers.assertStatusCode(response, 200); const newLibraryVersion = response.headers['last-modified-version'][0]; assert.equal(parseInt(newObjectVersion2), parseInt(newLibraryVersion)); - return; - - await API.createItem('book', { title: 'Title' }, this, 'key'); - response = await API.userGet( - config.userID, - `${objectTypePlural}/${objectKey}?limit=1` - ); - Helpers.assertStatusCode(response, 200); - const newObjectVersion3 = response.headers['last-modified-version'][0]; - assert.equal(parseInt(newLibraryVersion), parseInt(newObjectVersion3)); - response = await API.userDelete( - config.userID, - `${objectTypePlural}/${objectKey}` - ); - Helpers.assertStatusCode(response, 428); - response = await API.userDelete( - config.userID, - `${objectTypePlural}/${objectKey}`, - { 'If-Unmodified-Since-Version': objectVersion } - ); - Helpers.assertStatusCode(response, 412); - response = await API.userDelete( - config.userID, - `${objectTypePlural}/${objectKey}`, - { 'If-Unmodified-Since-Version': newObjectVersion2 } - ); - Helpers.assertStatusCode(response, 204); }; const _testMultiObjectLastModifiedVersion = async (objectType) => { - await API.userClear(config.userID); const objectTypePlural = API.getPluralObjectType(objectType); @@ -210,8 +183,6 @@ describe('VersionsTests', function () { case 'item': json = await API.getItemTemplate("book"); - json.creators[0].firstName = "Test"; - json.creators[0].lastName = "Test"; break; case 'search': @@ -296,9 +267,8 @@ describe('VersionsTests', function () { break; } - delete json.version; - // No If-Unmodified-Since-Version or object version property + delete json.version; response = await API.userPost( config.userID, `${objectTypePlural}`, @@ -309,8 +279,8 @@ describe('VersionsTests', function () { ); Helpers.assertStatusForObject(response, 'failed', 0, 428); + // Outdated object version property json.version = version - 1; - response = await API.userPost( config.userID, `${objectTypePlural}`, @@ -319,7 +289,7 @@ describe('VersionsTests', function () { "Content-Type": "application/json", } ); - // Outdated object version property + const message = `${_capitalizeFirstLetter(objectType)} has been modified since specified version (expected ${json.version}, found ${version2})`; Helpers.assertStatusForObject(response, 'failed', 0, 412, message); // Modify object, using object version property @@ -355,6 +325,8 @@ describe('VersionsTests', function () { version = parseInt(response.headers['last-modified-version'][0]); assert.isNumber(version); assert.equal(version, version3); + + // TODO: Version should be incremented on deleted item }; const _testMultiObject304NotModified = async (objectType) => { @@ -438,9 +410,7 @@ describe('VersionsTests', function () { let response = await API.userGet( config.userID, - `${objectTypePlural}?format=versions&${sinceParam}=${firstVersion}`, { - "Content-Type": "application/json" - } + `${objectTypePlural}?format=versions&${sinceParam}=${firstVersion}` ); Helpers.assertStatusCode(response, 200); let json = JSON.parse(response.data); @@ -508,8 +478,7 @@ describe('VersionsTests', function () { response = await API.userPut( config.userID, `${objectTypePlural}/${data.key}`, - JSON.stringify(data), - { "Content-Type": "application/json" } + JSON.stringify(data) ); Helpers.assertStatusCode(response, 204); @@ -659,7 +628,6 @@ describe('VersionsTests', function () { const existing = await API.createDataObject(objectType, null, null, 'json'); const key = existing.key; - const libraryVersion = existing.version; let json = await API.createUnsavedDataObject(objectType); json.key = key; @@ -972,7 +940,6 @@ describe('VersionsTests', function () { it('test_should_not_include_library_version_for_400', async function () { let json = await API.createItem("book", [], this, 'json'); - let libraryVersion = json.version; let response = await API.userPut( config.userID, "items/" + json.key, From 0d653ede4df670d7392c5099c9ca3815fee3601e Mon Sep 17 00:00:00 2001 From: Bogdan Abaev Date: Fri, 2 Jun 2023 17:20:07 -0400 Subject: [PATCH 22/33] using helper functions for statusForObject + removed unnecessary parts from httpHandler --- tests/remote_js/httpHandler.js | 20 +------------------- tests/remote_js/test/2/generalTest.js | 2 +- tests/remote_js/test/2/itemsTest.js | 19 ++++++++++--------- tests/remote_js/test/2/noteTest.js | 10 +++++----- tests/remote_js/test/2/objectTest.js | 10 +++++----- tests/remote_js/test/2/relationsTest.js | 13 +++++++------ tests/remote_js/test/2/searchTest.js | 12 ++++++------ tests/remote_js/test/2/tagTest.js | 2 +- tests/remote_js/test/2/versionTest.js | 8 ++++---- tests/remote_js/test/3/generalTest.js | 2 +- tests/remote_js/test/3/itemTest.js | 8 ++++---- tests/remote_js/test/3/objectTest.js | 10 +++++----- tests/remote_js/test/3/relationTest.js | 12 ++++++------ tests/remote_js/test/3/searchTest.js | 12 ++++++------ tests/remote_js/test/3/tagTest.js | 2 +- tests/remote_js/test/3/versionTest.js | 8 ++++---- 16 files changed, 67 insertions(+), 83 deletions(-) diff --git a/tests/remote_js/httpHandler.js b/tests/remote_js/httpHandler.js index 651ff8d3..c0894651 100644 --- a/tests/remote_js/httpHandler.js +++ b/tests/remote_js/httpHandler.js @@ -1,21 +1,5 @@ const fetch = require('node-fetch'); var config = require('config'); -const http = require("node:http"); -const https = require("node:https"); - - -// http(s) agents with keepAlive=true used to prevent socket from hanging -// due to bug in node-fetch: https://github.com/node-fetch/node-fetch/issues/1735 -const httpAgent = new http.Agent({ keepAlive: true }); -const httpsAgent = new https.Agent({ keepAlive: true }); -const agentSelector = function (_parsedURL) { - if (_parsedURL.protocol == 'http:') { - return httpAgent; - } - else { - return httpsAgent; - } -}; class HTTP { static verbose = config.verbose; @@ -43,9 +27,7 @@ class HTTP { url = url.replace(localIPRegex, 'localhost'); } - // workaround to prevent socket from hanging due to but in node-fetch: https://github.com/node-fetch/node-fetch/issues/1735 - await new Promise(resolve => setTimeout(resolve, 1)); - let response = await fetch(url, Object.assign(options, { agent: agentSelector })); + let response = await fetch(url, options); // Fetch doesn't automatically parse the response body, so we have to do that manually diff --git a/tests/remote_js/test/2/generalTest.js b/tests/remote_js/test/2/generalTest.js index bf8dca13..12efce74 100644 --- a/tests/remote_js/test/2/generalTest.js +++ b/tests/remote_js/test/2/generalTest.js @@ -52,7 +52,7 @@ describe('GeneralTests', function () { ); Helpers.assertStatusCode(response, 200); - Helpers.assertStatusForObject(response, 'success', 0); + Helpers.assert200ForObject(response); response = await API.userPost( config.userID, diff --git a/tests/remote_js/test/2/itemsTest.js b/tests/remote_js/test/2/itemsTest.js index d8f7309d..a022168f 100644 --- a/tests/remote_js/test/2/itemsTest.js +++ b/tests/remote_js/test/2/itemsTest.js @@ -4,6 +4,7 @@ var config = require('config'); const API = require('../../api2.js'); const Helpers = require('../../helpers2.js'); const { API2Before, API2After } = require("../shared.js"); +const {post}=require('../../httpHandler.js'); describe('ItemsTests', function () { this.timeout(config.timeout * 2); @@ -367,7 +368,7 @@ describe('ItemsTests', function () { }), { "Content-Type": "application/json" } ); - Helpers.assertStatusForObject(response, 'failed', 0, 400, "'itemType' property not provided"); + Helpers.assert400ForObject(response, { message: "'itemType' property not provided" }); // contentType on non-attachment const json3 = { ...json }; @@ -380,7 +381,7 @@ describe('ItemsTests', function () { }), { "Content-Type": "application/json" } ); - Helpers.assertStatusForObject(response, 'failed', 0, 400, "'contentType' is valid only for attachment items"); + Helpers.assert400ForObject(response, { message: "'contentType' is valid only for attachment items" }); }); it('testEditTopLevelNote', async function () { @@ -486,7 +487,7 @@ describe('ItemsTests', function () { items: [json], }), ); - Helpers.assertStatusForObject(response, 'success', 0); + Helpers.assert200ForObject(response); const xml2 = await API.getItemXML(json.itemKey); const data2 = API.parseDataFromAtomEntry(xml2); const json2 = JSON.parse(data2.content); @@ -660,7 +661,7 @@ describe('ItemsTests', function () { }), { "Content-Type": "application/json" } ); - Helpers.assertStatusForObject(newResponse, 'failed', 0, 400, "'invalidName' is not a valid linkMode"); + Helpers.assert400ForObject(newResponse, { message: "'invalidName' is not a valid linkMode" }); // Missing linkMode delete json.linkMode; @@ -672,7 +673,7 @@ describe('ItemsTests', function () { }), { "Content-Type": "application/json" } ); - Helpers.assertStatusForObject(missingResponse, 'failed', 0, 400, "'linkMode' property not provided"); + Helpers.assert400ForObject(missingResponse, { message: "'linkMode' property not provided" }); }); it('testNewAttachmentItemMD5OnLinkedURL', async function () { const newItemData = await testNewEmptyBookItem(); @@ -691,7 +692,7 @@ describe('ItemsTests', function () { }), { "Content-Type": "application/json" } ); - Helpers.assertStatusForObject(postResponse, 'failed', 0, 400, "'md5' is valid only for imported and embedded-image attachments"); + Helpers.assert400ForObject(postResponse, { message: "'md5' is valid only for imported and embedded-image attachments" }); }); it('testNewAttachmentItemModTimeOnLinkedURL', async function () { const newItemData = await testNewEmptyBookItem(); @@ -710,7 +711,7 @@ describe('ItemsTests', function () { }), { "Content-Type": "application/json" } ); - Helpers.assertStatusForObject(postResponse, 'failed', 0, 400, "'mtime' is valid only for imported and embedded-image attachments"); + Helpers.assert400ForObject(postResponse, { message: "'mtime' is valid only for imported and embedded-image attachments" }); }); it('testMappedCreatorTypes', async function () { const json = { @@ -743,8 +744,8 @@ describe('ItemsTests', function () { JSON.stringify(json) ); // 'author' gets mapped automatically, others dont - Helpers.assertStatusForObject(response, 'failed', 1, 400); - Helpers.assertStatusForObject(response, 'success', 0); + Helpers.assert400ForObject(response, { index: 1 }); + Helpers.assert200ForObject(response); }); it('testNumChildren', async function () { diff --git a/tests/remote_js/test/2/noteTest.js b/tests/remote_js/test/2/noteTest.js index 74ce90cb..34df9be6 100644 --- a/tests/remote_js/test/2/noteTest.js +++ b/tests/remote_js/test/2/noteTest.js @@ -32,7 +32,7 @@ describe('NoteTests', function () { { "Content-Type": "application/json" } ); const expectedMessage = "Note '1234567890123456789012345678901234567890123456789012345678901234567890123456789...' too long"; - Helpers.assertStatusForObject(response, 'failed', 0, 413, expectedMessage); + Helpers.assert413ForObject(response, { message : expectedMessage }); }); it('testNoteTooLongBlankFirstLines', async function () { @@ -47,7 +47,7 @@ describe('NoteTests', function () { { "Content-Type": "application/json" } ); const expectedMessage = "Note '1234567890123456789012345678901234567890123456789012345678901234567890123456789...' too long"; - Helpers.assertStatusForObject(response, 'failed', 0, 413, expectedMessage); + Helpers.assert413ForObject(response, { message : expectedMessage }); }); it('testNoteTooLongBlankFirstLinesHTML', async function () { @@ -63,7 +63,7 @@ describe('NoteTests', function () { ); const expectedMessage = "Note '1234567890123456789012345678901234567890123456789012345678901234567890123...' too long"; - Helpers.assertStatusForObject(response, 'failed', 0, 413, expectedMessage); + Helpers.assert413ForObject(response, { message : expectedMessage }); }); it('testNoteTooLongTitlePlusNewlines', async function () { @@ -79,7 +79,7 @@ describe('NoteTests', function () { ); const expectedMessage = "Note 'Full Text: 1234567890123456789012345678901234567890123456789012345678901234567...' too long"; - Helpers.assertStatusForObject(response, 'failed', 0, 413, expectedMessage); + Helpers.assert413ForObject(response, { message : expectedMessage }); }); // All content within HTML tags @@ -96,6 +96,6 @@ describe('NoteTests', function () { ); const expectedMessage = "Note '<p><!-- 1234567890123456789012345678901234567890123456789012345678901234...' too long"; - Helpers.assertStatusForObject(response, 'failed', 0, 413, expectedMessage); + Helpers.assert413ForObject(response, { message : expectedMessage }); }); }); diff --git a/tests/remote_js/test/2/objectTest.js b/tests/remote_js/test/2/objectTest.js index 99baa38e..81c6da9c 100644 --- a/tests/remote_js/test/2/objectTest.js +++ b/tests/remote_js/test/2/objectTest.js @@ -192,9 +192,9 @@ describe('ObjectTests', function () { Helpers.assertStatusCode(response, 200); json = API.getJSONFromResponse(response); - Helpers.assertStatusForObject(response, 'success', 0, 200); - Helpers.assertStatusForObject(response, 'success', 1, 413); - Helpers.assertStatusForObject(response, 'success', 2, 200); + Helpers.assert200ForObject(response, { index: 0 }); + Helpers.assert413ForObject(response, { index: 1 }); + Helpers.assert200ForObject(response, { index: 2 }); const responseKeys = await API.userGet( config.userID, @@ -268,8 +268,8 @@ describe('ObjectTests', function () { let json = API.getJSONFromResponse(response); Helpers.assertStatusForObject(response, 'unchanged', 0); - Helpers.assertStatusForObject(response, 'failed', 1); - Helpers.assertStatusForObject(response, 'success', 2); + Helpers.assert413ForObject(response, { index: 1 }); + Helpers.assert200ForObject(response, { index: 2 }); response = await API.userGet(config.userID, diff --git a/tests/remote_js/test/2/relationsTest.js b/tests/remote_js/test/2/relationsTest.js index de0b2676..1e1444f7 100644 --- a/tests/remote_js/test/2/relationsTest.js +++ b/tests/remote_js/test/2/relationsTest.js @@ -134,7 +134,7 @@ describe('RelationsTests', function () { } }, true, 'response'); - Helpers.assertStatusForObject(response, 'failed', 0, 400, "Unsupported predicate 'foo:unknown'"); + Helpers.assert400ForObject(response, { message: "Unsupported predicate 'foo:unknown'" }); response = await API.createItem('book', { relations: { @@ -142,7 +142,7 @@ describe('RelationsTests', function () { } }, this, 'response'); - Helpers.assertStatusForObject(response, 'failed', 0, 400, "'relations' values currently must be Zotero item URIs"); + Helpers.assert400ForObject(response, { message: "'relations' values currently must be Zotero item URIs" }); response = await API.createItem('book', { relations: { @@ -150,7 +150,7 @@ describe('RelationsTests', function () { } }, this, 'response'); - Helpers.assertStatusForObject(response, 'failed', 0, 400, "'relations' values currently must be Zotero item URIs"); + Helpers.assert400ForObject(response, { message: "'relations' values currently must be Zotero item URIs" }); }); it('testDeleteItemRelation', async function () { @@ -231,7 +231,8 @@ describe('RelationsTests', function () { "collections?key=" + config.apiKey, JSON.stringify({ collections: [json] }) ); - Helpers.assertStatusForObject(response, 'failed', 0, null, "Unsupported predicate 'foo:unknown'"); + + Helpers.assert400ForObject(response, { message: "Unsupported predicate 'foo:unknown'" }); json.relations = { "owl:sameAs": "Not a URI" @@ -241,7 +242,7 @@ describe('RelationsTests', function () { "collections?key=" + config.apiKey, JSON.stringify({ collections: [json] }) ); - Helpers.assertStatusForObject(response2, 'failed', 0, null, "'relations' values currently must be Zotero collection URIs"); + Helpers.assert400ForObject(response2, { message: "'relations' values currently must be Zotero collection URIs" }); json.relations = ["http://zotero.org/groups/1/collections/AAAAAAAA"]; const response3 = await API.userPost( @@ -249,7 +250,7 @@ describe('RelationsTests', function () { "collections?key=" + config.apiKey, JSON.stringify({ collections: [json] }) ); - Helpers.assertStatusForObject(response3, 'failed', 0, null, "'relations' property must be an object"); + Helpers.assert400ForObject(response3, { message: "'relations' property must be an object" }); }); it('testDeleteCollectionRelation', async function () { diff --git a/tests/remote_js/test/2/searchTest.js b/tests/remote_js/test/2/searchTest.js index be63b3aa..fa623489 100644 --- a/tests/remote_js/test/2/searchTest.js +++ b/tests/remote_js/test/2/searchTest.js @@ -102,12 +102,12 @@ describe('SearchTests', function () { 'Content-Type': 'application/json', }; const response = await API.createSearch('', conditions, headers, 'responsejson'); - Helpers.assertStatusForObject(response, 'failed', 0, 400, 'Search name cannot be empty'); + Helpers.assert400ForObject(response, { message: 'Search name cannot be empty' }); }); it('testNewSearchNoConditions', async function () { const json = await API.createSearch("Test", [], true, 'responsejson'); - Helpers.assertStatusForObject(json, 'failed', 0, 400, "'conditions' cannot be empty"); + Helpers.assert400ForObject(json, { message: "'conditions' cannot be empty" }); }); it('testNewSearchConditionErrors', async function () { @@ -122,7 +122,7 @@ describe('SearchTests', function () { true, 'responsejson' ); - Helpers.assertStatusForObject(json, 'failed', 0, 400, "'condition' property not provided for search condition"); + Helpers.assert400ForObject(json, { message: "'condition' property not provided for search condition" }); json = await API.createSearch( @@ -137,7 +137,7 @@ describe('SearchTests', function () { true, 'responsejson' ); - Helpers.assertStatusForObject(json, 'failed', 0, 400, 'Search condition cannot be empty'); + Helpers.assert400ForObject(json, { message: 'Search condition cannot be empty' }); json = await API.createSearch( @@ -151,7 +151,7 @@ describe('SearchTests', function () { true, 'responsejson' ); - Helpers.assertStatusForObject(json, 'failed', 0, 400, "'operator' property not provided for search condition"); + Helpers.assert400ForObject(json, { message: "'operator' property not provided for search condition" }); json = await API.createSearch( @@ -166,6 +166,6 @@ describe('SearchTests', function () { true, 'responsejson' ); - Helpers.assertStatusForObject(json, 'failed', 0, 400, 'Search operator cannot be empty'); + Helpers.assert400ForObject(json, { message: 'Search operator cannot be empty' }); }); }); diff --git a/tests/remote_js/test/2/tagTest.js b/tests/remote_js/test/2/tagTest.js index d1fba2e0..b28392ee 100644 --- a/tests/remote_js/test/2/tagTest.js +++ b/tests/remote_js/test/2/tagTest.js @@ -31,7 +31,7 @@ describe('TagTests', function () { let headers = { "Content-Type": "application/json" }; let response = await API.postItem(json, headers); - Helpers.assertStatusForObject(response, 'failed', 0, 400, "Tag must be an object"); + Helpers.assert400ForObject(response, { message: "Tag must be an object" }); }); it('testTagSearch', async function () { diff --git a/tests/remote_js/test/2/versionTest.js b/tests/remote_js/test/2/versionTest.js index 6c9d78e3..6e2f7233 100644 --- a/tests/remote_js/test/2/versionTest.js +++ b/tests/remote_js/test/2/versionTest.js @@ -266,7 +266,7 @@ describe('VersionsTests', function () { headers2 ); Helpers.assertStatusCode(response, 200); - Helpers.assertStatusForObject(response, 'success', 0); + Helpers.assert200ForObject(response); const version2 = parseInt(response.headers['last-modified-version'][0]); assert.isNumber(version2); // Version should be incremented on new object @@ -314,7 +314,7 @@ describe('VersionsTests', function () { "Content-Type": "application/json" } ); - Helpers.assertStatusForObject(response, 'failed', 0, 428); + Helpers.assert428ForObject(response); json[objectVersionProp] = version - 1; @@ -330,7 +330,7 @@ describe('VersionsTests', function () { ); // Outdated object version property const message = `${_capitalizeFirstLetter(objectType)} has been modified since specified version (expected ${json[objectVersionProp]}, found ${version2})`; - Helpers.assertStatusForObject(response, 'failed', 0, 412, message); + Helpers.assert412ForObject(response, { message: message }); // Modify object, using object version property json[objectVersionProp] = version; @@ -345,7 +345,7 @@ describe('VersionsTests', function () { } ); Helpers.assertStatusCode(response, 200); - Helpers.assertStatusForObject(response, 'success', 0); + Helpers.assert200ForObject(response); // Version should be incremented on modified object const version3 = parseInt(response.headers['last-modified-version'][0]); assert.isNumber(version3); diff --git a/tests/remote_js/test/3/generalTest.js b/tests/remote_js/test/3/generalTest.js index c666f39a..8dcaf26c 100644 --- a/tests/remote_js/test/3/generalTest.js +++ b/tests/remote_js/test/3/generalTest.js @@ -50,7 +50,7 @@ describe('GeneralTests', function () { ); Helpers.assertStatusCode(response, 200); - Helpers.assertStatusForObject(response, 'success', 0); + Helpers.assert200ForObject(response); response = await API.userPost( config.userID, diff --git a/tests/remote_js/test/3/itemTest.js b/tests/remote_js/test/3/itemTest.js index d2f9e86d..7c3b7536 100644 --- a/tests/remote_js/test/3/itemTest.js +++ b/tests/remote_js/test/3/itemTest.js @@ -694,7 +694,7 @@ describe('ItemsTests', function () { JSON.stringify([json]), { "Content-Type": "application/json" } ); - Helpers.assertStatusForObject(newResponse, 'failed', 0, 400, "'invalidName' is not a valid linkMode"); + Helpers.assert400ForObject(newResponse, { message: "'invalidName' is not a valid linkMode" }); // Missing linkMode delete json.linkMode; @@ -704,7 +704,7 @@ describe('ItemsTests', function () { JSON.stringify([json]), { "Content-Type": "application/json" } ); - Helpers.assertStatusForObject(missingResponse, 'failed', 0, 400, "'linkMode' property not provided"); + Helpers.assert400ForObject(missingResponse, { message: "'linkMode' property not provided" }); }); it('testNewAttachmentItemMD5OnLinkedURL', async function () { let json = await testNewEmptyBookItem(); @@ -721,7 +721,7 @@ describe('ItemsTests', function () { JSON.stringify([json]), { "Content-Type": "application/json" } ); - Helpers.assertStatusForObject(postResponse, 'failed', 0, 400, "'md5' is valid only for imported and embedded-image attachments"); + Helpers.assert400ForObject(postResponse, { message: "'md5' is valid only for imported and embedded-image attachments" }); }); it('testNewAttachmentItemModTimeOnLinkedURL', async function () { let json = await testNewEmptyBookItem(); @@ -738,7 +738,7 @@ describe('ItemsTests', function () { JSON.stringify([json]), { "Content-Type": "application/json" } ); - Helpers.assertStatusForObject(postResponse, 'failed', 0, 400, "'mtime' is valid only for imported and embedded-image attachments"); + Helpers.assert400ForObject(postResponse, { message: "'mtime' is valid only for imported and embedded-image attachments" }); }); it('testMappedCreatorTypes', async function () { const json = [ diff --git a/tests/remote_js/test/3/objectTest.js b/tests/remote_js/test/3/objectTest.js index 2c367e67..8092414f 100644 --- a/tests/remote_js/test/3/objectTest.js +++ b/tests/remote_js/test/3/objectTest.js @@ -201,9 +201,9 @@ describe('ObjectTests', function () { Helpers.assertStatusCode(response, 200); let successKeys = await API.getSuccessKeysFrom(response); - Helpers.assertStatusForObject(response, 'success', 0, 200); - Helpers.assertStatusForObject(response, 'success', 1, 413); - Helpers.assertStatusForObject(response, 'success', 2, 200); + Helpers.assert200ForObject(response, { index: 0 }); + Helpers.assert413ForObject(response, { index: 1 }); + Helpers.assert200ForObject(response, { index: 2 }); const responseKeys = await API.userGet( config.userID, @@ -273,8 +273,8 @@ describe('ObjectTests', function () { let successKeys = API.getSuccessfulKeysFromResponse(response); Helpers.assertStatusForObject(response, 'unchanged', 0); - Helpers.assertStatusForObject(response, 'failed', 1); - Helpers.assertStatusForObject(response, 'success', 2); + Helpers.assert413ForObject(response, { index: 1 }); + Helpers.assert200ForObject(response, { index: 2 }); response = await API.userGet(config.userID, diff --git a/tests/remote_js/test/3/relationTest.js b/tests/remote_js/test/3/relationTest.js index 0d21acd8..c8838b1c 100644 --- a/tests/remote_js/test/3/relationTest.js +++ b/tests/remote_js/test/3/relationTest.js @@ -124,7 +124,7 @@ describe('RelationsTests', function () { } }, true, 'response'); - Helpers.assertStatusForObject(response, 'failed', 0, 400, "Unsupported predicate 'foo:unknown'"); + Helpers.assert400ForObject(response, { message: "Unsupported predicate 'foo:unknown'" }); response = await API.createItem('book', { relations: { @@ -132,7 +132,7 @@ describe('RelationsTests', function () { } }, this, 'response'); - Helpers.assertStatusForObject(response, 'failed', 0, 400, "'relations' values currently must be Zotero item URIs"); + Helpers.assert400ForObject(response, { message: "'relations' values currently must be Zotero item URIs" }); response = await API.createItem('book', { relations: { @@ -140,7 +140,7 @@ describe('RelationsTests', function () { } }, this, 'response'); - Helpers.assertStatusForObject(response, 'failed', 0, 400, "'relations' values currently must be Zotero item URIs"); + Helpers.assert400ForObject(response, { message: "'relations' values currently must be Zotero item URIs" }); }); @@ -294,7 +294,7 @@ describe('RelationsTests', function () { "collections", JSON.stringify([json]) ); - Helpers.assertStatusForObject(response, 'failed', 0, null, "Unsupported predicate 'foo:unknown'"); + Helpers.assert400ForObject(response, { message: "Unsupported predicate 'foo:unknown'" }); json.relations = { "owl:sameAs": "Not a URI" @@ -304,7 +304,7 @@ describe('RelationsTests', function () { "collections", JSON.stringify([json]) ); - Helpers.assertStatusForObject(response2, 'failed', 0, null, "'relations' values currently must be Zotero collection URIs"); + Helpers.assert400ForObject(response2, { message: "'relations' values currently must be Zotero collection URIs" }); json.relations = ["http://zotero.org/groups/1/collections/AAAAAAAA"]; const response3 = await API.userPost( @@ -312,7 +312,7 @@ describe('RelationsTests', function () { "collections", JSON.stringify([json]) ); - Helpers.assertStatusForObject(response3, 'failed', 0, null, "'relations' property must be an object"); + Helpers.assert400ForObject(response3, { message: "'relations' property must be an object" }); }); it('testDeleteCollectionRelation', async function () { diff --git a/tests/remote_js/test/3/searchTest.js b/tests/remote_js/test/3/searchTest.js index 46d39972..44275924 100644 --- a/tests/remote_js/test/3/searchTest.js +++ b/tests/remote_js/test/3/searchTest.js @@ -212,12 +212,12 @@ describe('SearchTests', function () { 'Content-Type': 'application/json', }; const response = await API.createSearch('', conditions, headers, 'responseJSON'); - Helpers.assertStatusForObject(response, 'failed', 0, 400, 'Search name cannot be empty'); + Helpers.assert400ForObject(response, { message: 'Search name cannot be empty' }); }); it('testNewSearchNoConditions', async function () { const json = await API.createSearch("Test", [], true, 'responseJSON'); - Helpers.assertStatusForObject(json, 'failed', 0, 400, "'conditions' cannot be empty"); + Helpers.assert400ForObject(json, { message: "'conditions' cannot be empty" }); }); it('testNewSearchConditionErrors', async function () { @@ -232,7 +232,7 @@ describe('SearchTests', function () { true, 'responseJSON' ); - Helpers.assertStatusForObject(json, 'failed', 0, 400, "'condition' property not provided for search condition"); + Helpers.assert400ForObject(json, { message: "'condition' property not provided for search condition" }); json = await API.createSearch( @@ -247,7 +247,7 @@ describe('SearchTests', function () { true, 'responseJSON' ); - Helpers.assertStatusForObject(json, 'failed', 0, 400, 'Search condition cannot be empty'); + Helpers.assert400ForObject(json, { message: 'Search condition cannot be empty' }); json = await API.createSearch( @@ -261,7 +261,7 @@ describe('SearchTests', function () { true, 'responseJSON' ); - Helpers.assertStatusForObject(json, 'failed', 0, 400, "'operator' property not provided for search condition"); + Helpers.assert400ForObject(json, { message: "'operator' property not provided for search condition" }); json = await API.createSearch( @@ -276,7 +276,7 @@ describe('SearchTests', function () { true, 'responseJSON' ); - Helpers.assertStatusForObject(json, 'failed', 0, 400, 'Search operator cannot be empty'); + Helpers.assert400ForObject(json, { message: 'Search operator cannot be empty' }); }); it('test_should_allow_a_search_with_emoji_values', async function () { let response = await API.createSearch( diff --git a/tests/remote_js/test/3/tagTest.js b/tests/remote_js/test/3/tagTest.js index 710caa01..2fb61985 100644 --- a/tests/remote_js/test/3/tagTest.js +++ b/tests/remote_js/test/3/tagTest.js @@ -39,7 +39,7 @@ describe('TagTests', function () { let headers = { "Content-Type": "application/json" }; let response = await API.postItem(json, headers); - Helpers.assertStatusForObject(response, 'failed', 0, 400, "Tag must be an object"); + Helpers.assert400ForObject(response, { message: "Tag must be an object" }); }); it('test_should_add_tag_to_item', async function () { diff --git a/tests/remote_js/test/3/versionTest.js b/tests/remote_js/test/3/versionTest.js index 244b9787..098438f7 100644 --- a/tests/remote_js/test/3/versionTest.js +++ b/tests/remote_js/test/3/versionTest.js @@ -231,7 +231,7 @@ describe('VersionsTests', function () { headers2 ); Helpers.assertStatusCode(response, 200); - Helpers.assertStatusForObject(response, 'success', 0); + Helpers.assert200ForObject(response); const version2 = parseInt(response.headers['last-modified-version'][0]); assert.isNumber(version2); // Version should be incremented on new object @@ -277,7 +277,7 @@ describe('VersionsTests', function () { "Content-Type": "application/json" } ); - Helpers.assertStatusForObject(response, 'failed', 0, 428); + Helpers.assert428ForObject(response); // Outdated object version property json.version = version - 1; @@ -291,7 +291,7 @@ describe('VersionsTests', function () { ); const message = `${_capitalizeFirstLetter(objectType)} has been modified since specified version (expected ${json.version}, found ${version2})`; - Helpers.assertStatusForObject(response, 'failed', 0, 412, message); + Helpers.assert412ForObject(response, { message: message }); // Modify object, using object version property json.version = version; @@ -304,7 +304,7 @@ describe('VersionsTests', function () { } ); Helpers.assertStatusCode(response, 200); - Helpers.assertStatusForObject(response, 'success', 0); + Helpers.assert200ForObject(response); // Version should be incremented on modified object const version3 = parseInt(response.headers['last-modified-version'][0]); assert.isNumber(version3); From 5e213514bd43a8ca1006089b77e8ca341187d44d Mon Sep 17 00:00:00 2001 From: Bogdan Abaev Date: Thu, 8 Jun 2023 20:25:28 -0400 Subject: [PATCH 23/33] initial pdf text extraction setup --- .gitmodules | 3 + tests/remote_js/full-text-extractor | 1 + .../test/3/pdfTextExtractionTest.mjs | 176 ++++++++++++++++++ 3 files changed, 180 insertions(+) create mode 160000 tests/remote_js/full-text-extractor create mode 100644 tests/remote_js/test/3/pdfTextExtractionTest.mjs diff --git a/.gitmodules b/.gitmodules index 2bcf31ec..bb6dc6bf 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "htdocs/zotero-schema"] path = htdocs/zotero-schema url = https://github.com/zotero/zotero-schema.git +[submodule "tests/remote_js/full-text-extractor"] + path = tests/remote_js/full-text-extractor + url = https://github.com/zotero/full-text-extractor.git diff --git a/tests/remote_js/full-text-extractor b/tests/remote_js/full-text-extractor new file mode 160000 index 00000000..00932c40 --- /dev/null +++ b/tests/remote_js/full-text-extractor @@ -0,0 +1 @@ +Subproject commit 00932c40b5b39da1b12fa3b036a95edeec1ad6af diff --git a/tests/remote_js/test/3/pdfTextExtractionTest.mjs b/tests/remote_js/test/3/pdfTextExtractionTest.mjs new file mode 100644 index 00000000..4a42267c --- /dev/null +++ b/tests/remote_js/test/3/pdfTextExtractionTest.mjs @@ -0,0 +1,176 @@ +import chai from 'chai'; +const assert = chai.assert; +import config from 'config'; +import API from '../../api3.js'; +import Helpers from '../../helpers3.js'; +import shared from "../shared.js"; +import { S3Client, DeleteObjectsCommand } from "@aws-sdk/client-s3"; +import fs from 'fs'; +import HTTP from '../../httpHandler.js'; +import { localInvoke } from '../../full-text-extractor/src/local_invoke.mjs'; + + +describe('FileTestTests', function () { + this.timeout(0); + let toDelete = []; + const s3Client = new S3Client({ region: "us-east-1" }); + + before(async function () { + await shared.API3Before(); + try { + fs.mkdirSync("./work"); + } + catch {} + }); + + after(async function () { + await shared.API3After(); + fs.rm("./work", { recursive: true, force: true }, (e) => { + if (e) console.log(e); + }); + if (toDelete.length > 0) { + const commandInput = { + Bucket: config.s3Bucket, + Delete: { + Objects: toDelete.map((x) => { + return { Key: x }; + }) + } + }; + const command = new DeleteObjectsCommand(commandInput); + await s3Client.send(command); + } + }); + + beforeEach(async () => { + API.useAPIKey(config.apiKey); + }); + + it('should_extract_pdf_text', async function () { + let json = await API.createItem("book", false, this, 'json'); + assert.equal(0, json.meta.numChildren); + let parentKey = json.key; + + json = await API.createAttachmentItem("imported_file", [], parentKey, this, 'json'); + let attachmentKey = json.key; + let version = json.version; + + let filename = "dummy.pdf"; + let mtime = Date.now(); + const pdfText = makeRandomPDF(); + + let fileContents = fs.readFileSync("./work/dummy.pdf"); + let size = Buffer.from(fileContents.toString()).byteLength; + let md5 = Helpers.md5(fileContents.toString()); + + // Create attachment item + let response = await API.userPost( + config.userID, + "items", + JSON.stringify([ + { + key: attachmentKey, + contentType: "application/pdf", + } + ]), + { + "Content-Type": "application/json", + "If-Unmodified-Since-Version": version + } + ); + Helpers.assert200ForObject(response); + + // Get upload authorization + response = await API.userPost( + config.userID, + "items/" + attachmentKey + "/file", + Helpers.implodeParams({ + md5: md5, + mtime: mtime, + filename: filename, + filesize: size + }), + { + "Content-Type": "application/x-www-form-urlencoded", + "If-None-Match": "*" + } + ); + Helpers.assert200(response); + json = API.getJSONFromResponse(response); + + // Upload + response = await HTTP.post( + json.url, + json.prefix + fileContents + json.suffix, + { + "Content-Type": json.contentType + } + ); + Helpers.assert201(response); + + // Post-upload file registration + response = await API.userPost( + config.userID, + "items/" + attachmentKey + "/file", + "upload=" + json.uploadKey, + { + "Content-Type": "application/x-www-form-urlencoded", + "If-None-Match": "*" + } + ); + Helpers.assert204(response); + + toDelete.push(md5); + + // Local invoke full-text-extractor + await localInvoke(); + + // Get full text to ensure full-text-extractor worked + response = await API.userGet( + config.userID, + "items/" + attachmentKey + "/fulltext", + ); + Helpers.assert200(response); + const data = JSON.parse(response.data); + assert.property(data, 'content'); + assert.equal(data.content.trim(), pdfText); + }); + + const makeRandomPDF = () => { + const randomText = Helpers.uniqueToken(); + const pdfData = `%PDF-1.4 +1 0 obj <> +endobj +2 0 obj <> +endobj +3 0 obj<> +endobj +4 0 obj<>>> +endobj +5 0 obj<> +endobj +6 0 obj +<> +stream +BT /F1 24 Tf 175 720 Td (${randomText})Tj ET +endstream +endobj +xref +0 7 +0000000000 65535 f +0000000009 00000 n +0000000056 00000 n +0000000111 00000 n +0000000212 00000 n +0000000250 00000 n +0000000317 00000 n +trailer <> +startxref +406 +%%EOF`; + fs.writeFileSync(`./work/dummy.pdf`, pdfData); + return randomText; + }; +}); + + From 8e0936b4e821e12491d2ca2f9552c2d6ffdf29d7 Mon Sep 17 00:00:00 2001 From: Bogdan Abaev Date: Fri, 9 Jun 2023 10:57:01 -0400 Subject: [PATCH 24/33] pdf text extraction test --- tests/remote_js/config/default.json5 | 5 +- tests/remote_js/package-lock.json | 1570 ++++++++++++++++- tests/remote_js/package.json | 3 +- .../test/3/pdfTextExtractionTest.mjs | 318 +++- 4 files changed, 1855 insertions(+), 41 deletions(-) diff --git a/tests/remote_js/config/default.json5 b/tests/remote_js/config/default.json5 index 66473a9a..f5cc29ed 100644 --- a/tests/remote_js/config/default.json5 +++ b/tests/remote_js/config/default.json5 @@ -27,6 +27,9 @@ "password2": "letmein2", "displayName2": "testuser2", "ownedPrivateGroupID2": 0, - "ownedPrivateGroupLibraryID2": 0 + "ownedPrivateGroupLibraryID2": 0, + + "fullTextExtractorSQSUrl" : "", + "isLocalRun": false } diff --git a/tests/remote_js/package-lock.json b/tests/remote_js/package-lock.json index 7057205f..78f42947 100644 --- a/tests/remote_js/package-lock.json +++ b/tests/remote_js/package-lock.json @@ -5,8 +5,11 @@ "packages": { "": { "dependencies": { - "axios": "^1.4.0", + "@aws-sdk/client-s3": "^3.338.0", + "@aws-sdk/client-sqs": "^3.348.0", + "config": "^3.3.9", "jsdom": "^22.0.0", + "jszip": "^3.10.1", "node-fetch": "^2.6.7", "wgxpath": "^1.2.0" }, @@ -16,6 +19,1361 @@ "mocha": "^10.2.0" } }, + "node_modules/@aws-crypto/crc32": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-3.0.0.tgz", + "integrity": "sha512-IzSgsrxUcsrejQbPVilIKy16kAT52EwB6zSaI+M3xxIhKh5+aldEyvI+z6erM7TCLB2BJsFrtHjp6/4/sr+3dA==", + "dependencies": { + "@aws-crypto/util": "^3.0.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^1.11.1" + } + }, + "node_modules/@aws-crypto/crc32/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, + "node_modules/@aws-crypto/crc32c": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32c/-/crc32c-3.0.0.tgz", + "integrity": "sha512-ENNPPManmnVJ4BTXlOjAgD7URidbAznURqD0KvfREyc4o20DPYdEldU1f5cQ7Jbj0CJJSPaMIk/9ZshdB3210w==", + "dependencies": { + "@aws-crypto/util": "^3.0.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^1.11.1" + } + }, + "node_modules/@aws-crypto/crc32c/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, + "node_modules/@aws-crypto/ie11-detection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/ie11-detection/-/ie11-detection-3.0.0.tgz", + "integrity": "sha512-341lBBkiY1DfDNKai/wXM3aujNBkXR7tq1URPQDL9wi3AUbI80NR74uF1TXHMm7po1AcnFk8iu2S2IeU/+/A+Q==", + "dependencies": { + "tslib": "^1.11.1" + } + }, + "node_modules/@aws-crypto/ie11-detection/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, + "node_modules/@aws-crypto/sha1-browser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha1-browser/-/sha1-browser-3.0.0.tgz", + "integrity": "sha512-NJth5c997GLHs6nOYTzFKTbYdMNA6/1XlKVgnZoaZcQ7z7UJlOgj2JdbHE8tiYLS3fzXNCguct77SPGat2raSw==", + "dependencies": { + "@aws-crypto/ie11-detection": "^3.0.0", + "@aws-crypto/supports-web-crypto": "^3.0.0", + "@aws-crypto/util": "^3.0.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@aws-sdk/util-utf8-browser": "^3.0.0", + "tslib": "^1.11.1" + } + }, + "node_modules/@aws-crypto/sha1-browser/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, + "node_modules/@aws-crypto/sha256-browser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-3.0.0.tgz", + "integrity": "sha512-8VLmW2B+gjFbU5uMeqtQM6Nj0/F1bro80xQXCW6CQBWgosFWXTx77aeOF5CAIAmbOK64SdMBJdNr6J41yP5mvQ==", + "dependencies": { + "@aws-crypto/ie11-detection": "^3.0.0", + "@aws-crypto/sha256-js": "^3.0.0", + "@aws-crypto/supports-web-crypto": "^3.0.0", + "@aws-crypto/util": "^3.0.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@aws-sdk/util-utf8-browser": "^3.0.0", + "tslib": "^1.11.1" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, + "node_modules/@aws-crypto/sha256-js": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-3.0.0.tgz", + "integrity": "sha512-PnNN7os0+yd1XvXAy23CFOmTbMaDxgxXtTKHybrJ39Y8kGzBATgBFibWJKH6BhytLI/Zyszs87xCOBNyBig6vQ==", + "dependencies": { + "@aws-crypto/util": "^3.0.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^1.11.1" + } + }, + "node_modules/@aws-crypto/sha256-js/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, + "node_modules/@aws-crypto/supports-web-crypto": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-3.0.0.tgz", + "integrity": "sha512-06hBdMwUAb2WFTuGG73LSC0wfPu93xWwo5vL2et9eymgmu3Id5vFAHBbajVWiGhPO37qcsdCap/FqXvJGJWPIg==", + "dependencies": { + "tslib": "^1.11.1" + } + }, + "node_modules/@aws-crypto/supports-web-crypto/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, + "node_modules/@aws-crypto/util": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-3.0.0.tgz", + "integrity": "sha512-2OJlpeJpCR48CC8r+uKVChzs9Iungj9wkZrl8Z041DWEWvyIHILYKCPNzJghKsivj+S3mLo6BVc7mBNzdxA46w==", + "dependencies": { + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-utf8-browser": "^3.0.0", + "tslib": "^1.11.1" + } + }, + "node_modules/@aws-crypto/util/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, + "node_modules/@aws-sdk/abort-controller": { + "version": "3.347.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/abort-controller/-/abort-controller-3.347.0.tgz", + "integrity": "sha512-P/2qE6ntYEmYG4Ez535nJWZbXqgbkJx8CMz7ChEuEg3Gp3dvVYEKg+iEUEvlqQ2U5dWP5J3ehw5po9t86IsVPQ==", + "dependencies": { + "@aws-sdk/types": "3.347.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/chunked-blob-reader": { + "version": "3.310.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/chunked-blob-reader/-/chunked-blob-reader-3.310.0.tgz", + "integrity": "sha512-CrJS3exo4mWaLnWxfCH+w88Ou0IcAZSIkk4QbmxiHl/5Dq705OLoxf4385MVyExpqpeVJYOYQ2WaD8i/pQZ2fg==", + "dependencies": { + "tslib": "^2.5.0" + } + }, + "node_modules/@aws-sdk/client-s3": { + "version": "3.348.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.348.0.tgz", + "integrity": "sha512-19ShUJL/Kqol4pW2S6axD85oL2JIh91ctUgqPEuu5BzGyEgq5s+HP/DDNzcdsTKl7gfCfaIULf01yWU6RwY1EA==", + "dependencies": { + "@aws-crypto/sha1-browser": "3.0.0", + "@aws-crypto/sha256-browser": "3.0.0", + "@aws-crypto/sha256-js": "3.0.0", + "@aws-sdk/client-sts": "3.348.0", + "@aws-sdk/config-resolver": "3.347.0", + "@aws-sdk/credential-provider-node": "3.348.0", + "@aws-sdk/eventstream-serde-browser": "3.347.0", + "@aws-sdk/eventstream-serde-config-resolver": "3.347.0", + "@aws-sdk/eventstream-serde-node": "3.347.0", + "@aws-sdk/fetch-http-handler": "3.347.0", + "@aws-sdk/hash-blob-browser": "3.347.0", + "@aws-sdk/hash-node": "3.347.0", + "@aws-sdk/hash-stream-node": "3.347.0", + "@aws-sdk/invalid-dependency": "3.347.0", + "@aws-sdk/md5-js": "3.347.0", + "@aws-sdk/middleware-bucket-endpoint": "3.347.0", + "@aws-sdk/middleware-content-length": "3.347.0", + "@aws-sdk/middleware-endpoint": "3.347.0", + "@aws-sdk/middleware-expect-continue": "3.347.0", + "@aws-sdk/middleware-flexible-checksums": "3.347.0", + "@aws-sdk/middleware-host-header": "3.347.0", + "@aws-sdk/middleware-location-constraint": "3.347.0", + "@aws-sdk/middleware-logger": "3.347.0", + "@aws-sdk/middleware-recursion-detection": "3.347.0", + "@aws-sdk/middleware-retry": "3.347.0", + "@aws-sdk/middleware-sdk-s3": "3.347.0", + "@aws-sdk/middleware-serde": "3.347.0", + "@aws-sdk/middleware-signing": "3.347.0", + "@aws-sdk/middleware-ssec": "3.347.0", + "@aws-sdk/middleware-stack": "3.347.0", + "@aws-sdk/middleware-user-agent": "3.347.0", + "@aws-sdk/node-config-provider": "3.347.0", + "@aws-sdk/node-http-handler": "3.348.0", + "@aws-sdk/signature-v4-multi-region": "3.347.0", + "@aws-sdk/smithy-client": "3.347.0", + "@aws-sdk/types": "3.347.0", + "@aws-sdk/url-parser": "3.347.0", + "@aws-sdk/util-base64": "3.310.0", + "@aws-sdk/util-body-length-browser": "3.310.0", + "@aws-sdk/util-body-length-node": "3.310.0", + "@aws-sdk/util-defaults-mode-browser": "3.347.0", + "@aws-sdk/util-defaults-mode-node": "3.347.0", + "@aws-sdk/util-endpoints": "3.347.0", + "@aws-sdk/util-retry": "3.347.0", + "@aws-sdk/util-stream-browser": "3.347.0", + "@aws-sdk/util-stream-node": "3.348.0", + "@aws-sdk/util-user-agent-browser": "3.347.0", + "@aws-sdk/util-user-agent-node": "3.347.0", + "@aws-sdk/util-utf8": "3.310.0", + "@aws-sdk/util-waiter": "3.347.0", + "@aws-sdk/xml-builder": "3.310.0", + "@smithy/protocol-http": "^1.0.1", + "@smithy/types": "^1.0.0", + "fast-xml-parser": "4.2.4", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-sqs": { + "version": "3.348.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sqs/-/client-sqs-3.348.0.tgz", + "integrity": "sha512-Rglio22q7LpFGcjz3YbdOG+hNEd9Ykuw1aVHA5WQtT5BSxheYPtNv2XQunpvNrXssLZYcQWK4lab450aIfjtFg==", + "dependencies": { + "@aws-crypto/sha256-browser": "3.0.0", + "@aws-crypto/sha256-js": "3.0.0", + "@aws-sdk/client-sts": "3.348.0", + "@aws-sdk/config-resolver": "3.347.0", + "@aws-sdk/credential-provider-node": "3.348.0", + "@aws-sdk/fetch-http-handler": "3.347.0", + "@aws-sdk/hash-node": "3.347.0", + "@aws-sdk/invalid-dependency": "3.347.0", + "@aws-sdk/md5-js": "3.347.0", + "@aws-sdk/middleware-content-length": "3.347.0", + "@aws-sdk/middleware-endpoint": "3.347.0", + "@aws-sdk/middleware-host-header": "3.347.0", + "@aws-sdk/middleware-logger": "3.347.0", + "@aws-sdk/middleware-recursion-detection": "3.347.0", + "@aws-sdk/middleware-retry": "3.347.0", + "@aws-sdk/middleware-sdk-sqs": "3.347.0", + "@aws-sdk/middleware-serde": "3.347.0", + "@aws-sdk/middleware-signing": "3.347.0", + "@aws-sdk/middleware-stack": "3.347.0", + "@aws-sdk/middleware-user-agent": "3.347.0", + "@aws-sdk/node-config-provider": "3.347.0", + "@aws-sdk/node-http-handler": "3.348.0", + "@aws-sdk/smithy-client": "3.347.0", + "@aws-sdk/types": "3.347.0", + "@aws-sdk/url-parser": "3.347.0", + "@aws-sdk/util-base64": "3.310.0", + "@aws-sdk/util-body-length-browser": "3.310.0", + "@aws-sdk/util-body-length-node": "3.310.0", + "@aws-sdk/util-defaults-mode-browser": "3.347.0", + "@aws-sdk/util-defaults-mode-node": "3.347.0", + "@aws-sdk/util-endpoints": "3.347.0", + "@aws-sdk/util-retry": "3.347.0", + "@aws-sdk/util-user-agent-browser": "3.347.0", + "@aws-sdk/util-user-agent-node": "3.347.0", + "@aws-sdk/util-utf8": "3.310.0", + "@smithy/protocol-http": "^1.0.1", + "@smithy/types": "^1.0.0", + "fast-xml-parser": "4.2.4", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-sso": { + "version": "3.348.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.348.0.tgz", + "integrity": "sha512-5S23gVKBl0fhZ96RD8LdPhMKeh8E5fmebyZxMNZuWliSXz++Q9ZCrwPwQbkks3duPOTcKKobs3IoqP82HoXMvQ==", + "dependencies": { + "@aws-crypto/sha256-browser": "3.0.0", + "@aws-crypto/sha256-js": "3.0.0", + "@aws-sdk/config-resolver": "3.347.0", + "@aws-sdk/fetch-http-handler": "3.347.0", + "@aws-sdk/hash-node": "3.347.0", + "@aws-sdk/invalid-dependency": "3.347.0", + "@aws-sdk/middleware-content-length": "3.347.0", + "@aws-sdk/middleware-endpoint": "3.347.0", + "@aws-sdk/middleware-host-header": "3.347.0", + "@aws-sdk/middleware-logger": "3.347.0", + "@aws-sdk/middleware-recursion-detection": "3.347.0", + "@aws-sdk/middleware-retry": "3.347.0", + "@aws-sdk/middleware-serde": "3.347.0", + "@aws-sdk/middleware-stack": "3.347.0", + "@aws-sdk/middleware-user-agent": "3.347.0", + "@aws-sdk/node-config-provider": "3.347.0", + "@aws-sdk/node-http-handler": "3.348.0", + "@aws-sdk/smithy-client": "3.347.0", + "@aws-sdk/types": "3.347.0", + "@aws-sdk/url-parser": "3.347.0", + "@aws-sdk/util-base64": "3.310.0", + "@aws-sdk/util-body-length-browser": "3.310.0", + "@aws-sdk/util-body-length-node": "3.310.0", + "@aws-sdk/util-defaults-mode-browser": "3.347.0", + "@aws-sdk/util-defaults-mode-node": "3.347.0", + "@aws-sdk/util-endpoints": "3.347.0", + "@aws-sdk/util-retry": "3.347.0", + "@aws-sdk/util-user-agent-browser": "3.347.0", + "@aws-sdk/util-user-agent-node": "3.347.0", + "@aws-sdk/util-utf8": "3.310.0", + "@smithy/protocol-http": "^1.0.1", + "@smithy/types": "^1.0.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-sso-oidc": { + "version": "3.348.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso-oidc/-/client-sso-oidc-3.348.0.tgz", + "integrity": "sha512-tvHpcycx4EALvk38I9rAOdPeHvBDezqIB4lrE7AvnOJljlvCcdQ2gXa9GDrwrM7zuYBIZMBRE/njTMrCwoOdAA==", + "dependencies": { + "@aws-crypto/sha256-browser": "3.0.0", + "@aws-crypto/sha256-js": "3.0.0", + "@aws-sdk/config-resolver": "3.347.0", + "@aws-sdk/fetch-http-handler": "3.347.0", + "@aws-sdk/hash-node": "3.347.0", + "@aws-sdk/invalid-dependency": "3.347.0", + "@aws-sdk/middleware-content-length": "3.347.0", + "@aws-sdk/middleware-endpoint": "3.347.0", + "@aws-sdk/middleware-host-header": "3.347.0", + "@aws-sdk/middleware-logger": "3.347.0", + "@aws-sdk/middleware-recursion-detection": "3.347.0", + "@aws-sdk/middleware-retry": "3.347.0", + "@aws-sdk/middleware-serde": "3.347.0", + "@aws-sdk/middleware-stack": "3.347.0", + "@aws-sdk/middleware-user-agent": "3.347.0", + "@aws-sdk/node-config-provider": "3.347.0", + "@aws-sdk/node-http-handler": "3.348.0", + "@aws-sdk/smithy-client": "3.347.0", + "@aws-sdk/types": "3.347.0", + "@aws-sdk/url-parser": "3.347.0", + "@aws-sdk/util-base64": "3.310.0", + "@aws-sdk/util-body-length-browser": "3.310.0", + "@aws-sdk/util-body-length-node": "3.310.0", + "@aws-sdk/util-defaults-mode-browser": "3.347.0", + "@aws-sdk/util-defaults-mode-node": "3.347.0", + "@aws-sdk/util-endpoints": "3.347.0", + "@aws-sdk/util-retry": "3.347.0", + "@aws-sdk/util-user-agent-browser": "3.347.0", + "@aws-sdk/util-user-agent-node": "3.347.0", + "@aws-sdk/util-utf8": "3.310.0", + "@smithy/protocol-http": "^1.0.1", + "@smithy/types": "^1.0.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-sts": { + "version": "3.348.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.348.0.tgz", + "integrity": "sha512-4iaQlWAOHMEF4xjR/FB/ws3aUjXjJHwbsIcqbdYAxsKijDYYTZYCPc/gM0NE1yi28qlNYNhMzHipe5xTYbU2Eg==", + "dependencies": { + "@aws-crypto/sha256-browser": "3.0.0", + "@aws-crypto/sha256-js": "3.0.0", + "@aws-sdk/config-resolver": "3.347.0", + "@aws-sdk/credential-provider-node": "3.348.0", + "@aws-sdk/fetch-http-handler": "3.347.0", + "@aws-sdk/hash-node": "3.347.0", + "@aws-sdk/invalid-dependency": "3.347.0", + "@aws-sdk/middleware-content-length": "3.347.0", + "@aws-sdk/middleware-endpoint": "3.347.0", + "@aws-sdk/middleware-host-header": "3.347.0", + "@aws-sdk/middleware-logger": "3.347.0", + "@aws-sdk/middleware-recursion-detection": "3.347.0", + "@aws-sdk/middleware-retry": "3.347.0", + "@aws-sdk/middleware-sdk-sts": "3.347.0", + "@aws-sdk/middleware-serde": "3.347.0", + "@aws-sdk/middleware-signing": "3.347.0", + "@aws-sdk/middleware-stack": "3.347.0", + "@aws-sdk/middleware-user-agent": "3.347.0", + "@aws-sdk/node-config-provider": "3.347.0", + "@aws-sdk/node-http-handler": "3.348.0", + "@aws-sdk/smithy-client": "3.347.0", + "@aws-sdk/types": "3.347.0", + "@aws-sdk/url-parser": "3.347.0", + "@aws-sdk/util-base64": "3.310.0", + "@aws-sdk/util-body-length-browser": "3.310.0", + "@aws-sdk/util-body-length-node": "3.310.0", + "@aws-sdk/util-defaults-mode-browser": "3.347.0", + "@aws-sdk/util-defaults-mode-node": "3.347.0", + "@aws-sdk/util-endpoints": "3.347.0", + "@aws-sdk/util-retry": "3.347.0", + "@aws-sdk/util-user-agent-browser": "3.347.0", + "@aws-sdk/util-user-agent-node": "3.347.0", + "@aws-sdk/util-utf8": "3.310.0", + "@smithy/protocol-http": "^1.0.1", + "@smithy/types": "^1.0.0", + "fast-xml-parser": "4.2.4", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/config-resolver": { + "version": "3.347.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/config-resolver/-/config-resolver-3.347.0.tgz", + "integrity": "sha512-2ja+Sf/VnUO7IQ3nKbDQ5aumYKKJUaTm/BuVJ29wNho8wYHfuf7wHZV0pDTkB8RF5SH7IpHap7zpZAj39Iq+EA==", + "dependencies": { + "@aws-sdk/types": "3.347.0", + "@aws-sdk/util-config-provider": "3.310.0", + "@aws-sdk/util-middleware": "3.347.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-env": { + "version": "3.347.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.347.0.tgz", + "integrity": "sha512-UnEM+LKGpXKzw/1WvYEQsC6Wj9PupYZdQOE+e2Dgy2dqk/pVFy4WueRtFXYDT2B41ppv3drdXUuKZRIDVqIgNQ==", + "dependencies": { + "@aws-sdk/property-provider": "3.347.0", + "@aws-sdk/types": "3.347.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-imds": { + "version": "3.347.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-imds/-/credential-provider-imds-3.347.0.tgz", + "integrity": "sha512-7scCy/DCDRLIhlqTxff97LQWDnRwRXji3bxxMg+xWOTTaJe7PWx+etGSbBWaL42vsBHFShQjSLvJryEgoBktpw==", + "dependencies": { + "@aws-sdk/node-config-provider": "3.347.0", + "@aws-sdk/property-provider": "3.347.0", + "@aws-sdk/types": "3.347.0", + "@aws-sdk/url-parser": "3.347.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.348.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.348.0.tgz", + "integrity": "sha512-0IEH5mH/cz2iLyr/+pSa3sCsQcGADiLSEn6yivsXdfz1zDqBiv+ffDoL0+Pvnp+TKf8sA6OlX8PgoMoEBvBdKw==", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.347.0", + "@aws-sdk/credential-provider-imds": "3.347.0", + "@aws-sdk/credential-provider-process": "3.347.0", + "@aws-sdk/credential-provider-sso": "3.348.0", + "@aws-sdk/credential-provider-web-identity": "3.347.0", + "@aws-sdk/property-provider": "3.347.0", + "@aws-sdk/shared-ini-file-loader": "3.347.0", + "@aws-sdk/types": "3.347.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-node": { + "version": "3.348.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.348.0.tgz", + "integrity": "sha512-ngRWphm9e36i58KqVi7Z8WOub+k0cSl+JZaAmgfFm0+dsfBG5uheo598OeiwWV0DqlilvaQZFaMVQgG2SX/tHg==", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.347.0", + "@aws-sdk/credential-provider-imds": "3.347.0", + "@aws-sdk/credential-provider-ini": "3.348.0", + "@aws-sdk/credential-provider-process": "3.347.0", + "@aws-sdk/credential-provider-sso": "3.348.0", + "@aws-sdk/credential-provider-web-identity": "3.347.0", + "@aws-sdk/property-provider": "3.347.0", + "@aws-sdk/shared-ini-file-loader": "3.347.0", + "@aws-sdk/types": "3.347.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-process": { + "version": "3.347.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.347.0.tgz", + "integrity": "sha512-yl1z4MsaBdXd4GQ2halIvYds23S67kElyOwz7g8kaQ4kHj+UoYWxz3JVW/DGusM6XmQ9/F67utBrUVA0uhQYyw==", + "dependencies": { + "@aws-sdk/property-provider": "3.347.0", + "@aws-sdk/shared-ini-file-loader": "3.347.0", + "@aws-sdk/types": "3.347.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.348.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.348.0.tgz", + "integrity": "sha512-5cQao705376KgGkLv9xgkQ3T5H7KdNddWuyoH2wDcrHd1BA2Lnrell3Yyh7R6jQeV7uCQE/z0ugUOKhDqNKIqQ==", + "dependencies": { + "@aws-sdk/client-sso": "3.348.0", + "@aws-sdk/property-provider": "3.347.0", + "@aws-sdk/shared-ini-file-loader": "3.347.0", + "@aws-sdk/token-providers": "3.348.0", + "@aws-sdk/types": "3.347.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.347.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.347.0.tgz", + "integrity": "sha512-DxoTlVK8lXjS1zVphtz/Ab+jkN/IZor9d6pP2GjJHNoAIIzXfRwwj5C8vr4eTayx/5VJ7GRP91J8GJ2cKly8Qw==", + "dependencies": { + "@aws-sdk/property-provider": "3.347.0", + "@aws-sdk/types": "3.347.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/eventstream-codec": { + "version": "3.347.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/eventstream-codec/-/eventstream-codec-3.347.0.tgz", + "integrity": "sha512-61q+SyspjsaQ4sdgjizMyRgVph2CiW4aAtfpoH69EJFJfTxTR/OqnZ9Jx/3YiYi0ksrvDenJddYodfWWJqD8/w==", + "dependencies": { + "@aws-crypto/crc32": "3.0.0", + "@aws-sdk/types": "3.347.0", + "@aws-sdk/util-hex-encoding": "3.310.0", + "tslib": "^2.5.0" + } + }, + "node_modules/@aws-sdk/eventstream-serde-browser": { + "version": "3.347.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/eventstream-serde-browser/-/eventstream-serde-browser-3.347.0.tgz", + "integrity": "sha512-9BLVTHWgpiTo/hl+k7qt7E9iYu43zVwJN+4TEwA9ZZB3p12068t1Hay6HgCcgJC3+LWMtw/OhvypV6vQAG4UBg==", + "dependencies": { + "@aws-sdk/eventstream-serde-universal": "3.347.0", + "@aws-sdk/types": "3.347.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/eventstream-serde-config-resolver": { + "version": "3.347.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-3.347.0.tgz", + "integrity": "sha512-RcXQbNVq0PFmDqfn6+MnjCUWbbobcYVxpimaF6pMDav04o6Mcle+G2Hrefp5NlFr/lZbHW2eUKYsp1sXPaxVlQ==", + "dependencies": { + "@aws-sdk/types": "3.347.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/eventstream-serde-node": { + "version": "3.347.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/eventstream-serde-node/-/eventstream-serde-node-3.347.0.tgz", + "integrity": "sha512-pgQCWH0PkHjcHs04JE7FoGAD3Ww45ffV8Op0MSLUhg9OpGa6EDoO3EOpWi9l/TALtH4f0KRV35PVyUyHJ/wEkA==", + "dependencies": { + "@aws-sdk/eventstream-serde-universal": "3.347.0", + "@aws-sdk/types": "3.347.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/eventstream-serde-universal": { + "version": "3.347.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/eventstream-serde-universal/-/eventstream-serde-universal-3.347.0.tgz", + "integrity": "sha512-4wWj6bz6lOyDIO/dCCjwaLwRz648xzQQnf89R29sLoEqvAPP5XOB7HL+uFaQ/f5tPNh49gL6huNFSVwDm62n4Q==", + "dependencies": { + "@aws-sdk/eventstream-codec": "3.347.0", + "@aws-sdk/types": "3.347.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/fetch-http-handler": { + "version": "3.347.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/fetch-http-handler/-/fetch-http-handler-3.347.0.tgz", + "integrity": "sha512-sQ5P7ivY8//7wdxfA76LT1sF6V2Tyyz1qF6xXf9sihPN5Q1Y65c+SKpMzXyFSPqWZ82+SQQuDliYZouVyS6kQQ==", + "dependencies": { + "@aws-sdk/protocol-http": "3.347.0", + "@aws-sdk/querystring-builder": "3.347.0", + "@aws-sdk/types": "3.347.0", + "@aws-sdk/util-base64": "3.310.0", + "tslib": "^2.5.0" + } + }, + "node_modules/@aws-sdk/hash-blob-browser": { + "version": "3.347.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/hash-blob-browser/-/hash-blob-browser-3.347.0.tgz", + "integrity": "sha512-RxgstIldLsdJKN5UHUwSI9PMiatr0xKmKxS4+tnWZ1/OOg6wuWqqpDpWdNOVSJSpxpUaP6kRrvG5Yo5ZevoTXw==", + "dependencies": { + "@aws-sdk/chunked-blob-reader": "3.310.0", + "@aws-sdk/types": "3.347.0", + "tslib": "^2.5.0" + } + }, + "node_modules/@aws-sdk/hash-node": { + "version": "3.347.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/hash-node/-/hash-node-3.347.0.tgz", + "integrity": "sha512-96+ml/4EaUaVpzBdOLGOxdoXOjkPgkoJp/0i1fxOJEvl8wdAQSwc3IugVK9wZkCxy2DlENtgOe6DfIOhfffm/g==", + "dependencies": { + "@aws-sdk/types": "3.347.0", + "@aws-sdk/util-buffer-from": "3.310.0", + "@aws-sdk/util-utf8": "3.310.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/hash-stream-node": { + "version": "3.347.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/hash-stream-node/-/hash-stream-node-3.347.0.tgz", + "integrity": "sha512-tOBfcvELyt1GVuAlQ4d0mvm3QxoSSmvhH15SWIubM9RP4JWytBVzaFAn/aC02DBAWyvp0acMZ5J+47mxrWJElg==", + "dependencies": { + "@aws-sdk/types": "3.347.0", + "@aws-sdk/util-utf8": "3.310.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/invalid-dependency": { + "version": "3.347.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/invalid-dependency/-/invalid-dependency-3.347.0.tgz", + "integrity": "sha512-8imQcwLwqZ/wTJXZqzXT9pGLIksTRckhGLZaXT60tiBOPKuerTsus2L59UstLs5LP8TKaVZKFFSsjRIn9dQdmQ==", + "dependencies": { + "@aws-sdk/types": "3.347.0", + "tslib": "^2.5.0" + } + }, + "node_modules/@aws-sdk/is-array-buffer": { + "version": "3.310.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/is-array-buffer/-/is-array-buffer-3.310.0.tgz", + "integrity": "sha512-urnbcCR+h9NWUnmOtet/s4ghvzsidFmspfhYaHAmSRdy9yDjdjBJMFjjsn85A1ODUktztm+cVncXjQ38WCMjMQ==", + "dependencies": { + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/md5-js": { + "version": "3.347.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/md5-js/-/md5-js-3.347.0.tgz", + "integrity": "sha512-mChE+7DByTY9H4cQ6fnWp2x5jf8e6OZN+AdLp6WQ+W99z35zBeqBxVmgm8ziJwkMIrkSTv9j3Y7T9Ve3RIcSfg==", + "dependencies": { + "@aws-sdk/types": "3.347.0", + "@aws-sdk/util-utf8": "3.310.0", + "tslib": "^2.5.0" + } + }, + "node_modules/@aws-sdk/middleware-bucket-endpoint": { + "version": "3.347.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.347.0.tgz", + "integrity": "sha512-i9n4ylkGmGvizVcTfN4L+oN10OCL2DKvyMa4cCAVE1TJrsnaE0g7IOOyJGUS8p5KJYQrKVR7kcsa2L1S0VeEcA==", + "dependencies": { + "@aws-sdk/protocol-http": "3.347.0", + "@aws-sdk/types": "3.347.0", + "@aws-sdk/util-arn-parser": "3.310.0", + "@aws-sdk/util-config-provider": "3.310.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/middleware-content-length": { + "version": "3.347.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-content-length/-/middleware-content-length-3.347.0.tgz", + "integrity": "sha512-i4qtWTDImMaDUtwKQPbaZpXsReiwiBomM1cWymCU4bhz81HL01oIxOxOBuiM+3NlDoCSPr3KI6txZSz/8cqXCQ==", + "dependencies": { + "@aws-sdk/protocol-http": "3.347.0", + "@aws-sdk/types": "3.347.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/middleware-endpoint": { + "version": "3.347.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-endpoint/-/middleware-endpoint-3.347.0.tgz", + "integrity": "sha512-unF0c6dMaUL1ffU+37Ugty43DgMnzPWXr/Jup/8GbK5fzzWT5NQq6dj9KHPubMbWeEjQbmczvhv25JuJdK8gNQ==", + "dependencies": { + "@aws-sdk/middleware-serde": "3.347.0", + "@aws-sdk/types": "3.347.0", + "@aws-sdk/url-parser": "3.347.0", + "@aws-sdk/util-middleware": "3.347.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/middleware-expect-continue": { + "version": "3.347.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.347.0.tgz", + "integrity": "sha512-95M1unD1ENL0tx35dfyenSfx0QuXBSKtOi/qJja6LfX5771C5fm5ZTOrsrzPFJvRg/wj8pCOVWRZk+d5+jvfOQ==", + "dependencies": { + "@aws-sdk/protocol-http": "3.347.0", + "@aws-sdk/types": "3.347.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/middleware-flexible-checksums": { + "version": "3.347.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.347.0.tgz", + "integrity": "sha512-Pda7VMAIyeHw9nMp29rxdFft3EF4KP/tz/vLB6bqVoBNbLujo5rxn3SGOgStgIz7fuMLQQfoWIsmvxUm+Fp+Dw==", + "dependencies": { + "@aws-crypto/crc32": "3.0.0", + "@aws-crypto/crc32c": "3.0.0", + "@aws-sdk/is-array-buffer": "3.310.0", + "@aws-sdk/protocol-http": "3.347.0", + "@aws-sdk/types": "3.347.0", + "@aws-sdk/util-utf8": "3.310.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/middleware-host-header": { + "version": "3.347.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.347.0.tgz", + "integrity": "sha512-kpKmR9OvMlnReqp5sKcJkozbj1wmlblbVSbnQAIkzeQj2xD5dnVR3Nn2ogQKxSmU1Fv7dEroBtrruJ1o3fY38A==", + "dependencies": { + "@aws-sdk/protocol-http": "3.347.0", + "@aws-sdk/types": "3.347.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/middleware-location-constraint": { + "version": "3.347.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.347.0.tgz", + "integrity": "sha512-x5fcEV7q8fQ0OmUO+cLhN5iPqGoLWtC3+aKHIfRRb2BpOO1khyc1FKzsIAdeQz2hfktq4j+WsrmcPvFKv51pSg==", + "dependencies": { + "@aws-sdk/types": "3.347.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/middleware-logger": { + "version": "3.347.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.347.0.tgz", + "integrity": "sha512-NYC+Id5UCkVn+3P1t/YtmHt75uED06vwaKyxDy0UmB2K66PZLVtwWbLpVWrhbroaw1bvUHYcRyQ9NIfnVcXQjA==", + "dependencies": { + "@aws-sdk/types": "3.347.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.347.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.347.0.tgz", + "integrity": "sha512-qfnSvkFKCAMjMHR31NdsT0gv5Sq/ZHTUD4yQsSLpbVQ6iYAS834lrzXt41iyEHt57Y514uG7F/Xfvude3u4icQ==", + "dependencies": { + "@aws-sdk/protocol-http": "3.347.0", + "@aws-sdk/types": "3.347.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/middleware-retry": { + "version": "3.347.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-retry/-/middleware-retry-3.347.0.tgz", + "integrity": "sha512-CpdM+8dCSbX96agy4FCzOfzDmhNnGBM/pxrgIVLm5nkYTLuXp/d7ubpFEUHULr+4hCd5wakHotMt7yO29NFaVw==", + "dependencies": { + "@aws-sdk/protocol-http": "3.347.0", + "@aws-sdk/service-error-classification": "3.347.0", + "@aws-sdk/types": "3.347.0", + "@aws-sdk/util-middleware": "3.347.0", + "@aws-sdk/util-retry": "3.347.0", + "tslib": "^2.5.0", + "uuid": "^8.3.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/middleware-sdk-s3": { + "version": "3.347.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.347.0.tgz", + "integrity": "sha512-TLr92+HMvamrhJJ0VDhA/PiUh4rTNQz38B9dB9ikohTaRgm+duP+mRiIv16tNPZPGl8v82Thn7Ogk2qPByNDtg==", + "dependencies": { + "@aws-sdk/protocol-http": "3.347.0", + "@aws-sdk/types": "3.347.0", + "@aws-sdk/util-arn-parser": "3.310.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/middleware-sdk-sqs": { + "version": "3.347.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-sqs/-/middleware-sdk-sqs-3.347.0.tgz", + "integrity": "sha512-TSBTQoOVe9cDm9am4NOov1YZxbQ3LPBl7Ex0jblDFgUXqE9kNU3Kx/yc8edOLcq+5AFrgqT0NFD7pwFlQPh3KQ==", + "dependencies": { + "@aws-sdk/types": "3.347.0", + "@aws-sdk/util-hex-encoding": "3.310.0", + "@aws-sdk/util-utf8": "3.310.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/middleware-sdk-sts": { + "version": "3.347.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-sts/-/middleware-sdk-sts-3.347.0.tgz", + "integrity": "sha512-38LJ0bkIoVF3W97x6Jyyou72YV9Cfbml4OaDEdnrCOo0EssNZM5d7RhjMvQDwww7/3OBY/BzeOcZKfJlkYUXGw==", + "dependencies": { + "@aws-sdk/middleware-signing": "3.347.0", + "@aws-sdk/types": "3.347.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/middleware-serde": { + "version": "3.347.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-serde/-/middleware-serde-3.347.0.tgz", + "integrity": "sha512-x5Foi7jRbVJXDu9bHfyCbhYDH5pKK+31MmsSJ3k8rY8keXLBxm2XEEg/AIoV9/TUF9EeVvZ7F1/RmMpJnWQsEg==", + "dependencies": { + "@aws-sdk/types": "3.347.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/middleware-signing": { + "version": "3.347.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-signing/-/middleware-signing-3.347.0.tgz", + "integrity": "sha512-zVBF/4MGKnvhAE/J+oAL/VAehiyv+trs2dqSQXwHou9j8eA8Vm8HS2NdOwpkZQchIxTuwFlqSusDuPEdYFbvGw==", + "dependencies": { + "@aws-sdk/property-provider": "3.347.0", + "@aws-sdk/protocol-http": "3.347.0", + "@aws-sdk/signature-v4": "3.347.0", + "@aws-sdk/types": "3.347.0", + "@aws-sdk/util-middleware": "3.347.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/middleware-ssec": { + "version": "3.347.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-ssec/-/middleware-ssec-3.347.0.tgz", + "integrity": "sha512-467VEi2elPmUGcHAgTmzhguZ3lwTpwK+3s+pk312uZtVsS9rP1MAknYhpS3ZvssiqBUVPx8m29cLcC6Tx5nOJg==", + "dependencies": { + "@aws-sdk/types": "3.347.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/middleware-stack": { + "version": "3.347.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-stack/-/middleware-stack-3.347.0.tgz", + "integrity": "sha512-Izidg4rqtYMcKuvn2UzgEpPLSmyd8ub9+LQ2oIzG3mpIzCBITq7wp40jN1iNkMg+X6KEnX9vdMJIYZsPYMCYuQ==", + "dependencies": { + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.347.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.347.0.tgz", + "integrity": "sha512-wJbGN3OE1/daVCrwk49whhIr9E0j1N4gWwN/wi4WuyYIA+5lMUfVp0aGIOvZR+878DxuFz2hQ4XcZVT4K2WvQw==", + "dependencies": { + "@aws-sdk/protocol-http": "3.347.0", + "@aws-sdk/types": "3.347.0", + "@aws-sdk/util-endpoints": "3.347.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/node-config-provider": { + "version": "3.347.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/node-config-provider/-/node-config-provider-3.347.0.tgz", + "integrity": "sha512-faU93d3+5uTTUcotGgMXF+sJVFjrKh+ufW+CzYKT4yUHammyaIab/IbTPWy2hIolcEGtuPeVoxXw8TXbkh/tuw==", + "dependencies": { + "@aws-sdk/property-provider": "3.347.0", + "@aws-sdk/shared-ini-file-loader": "3.347.0", + "@aws-sdk/types": "3.347.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/node-http-handler": { + "version": "3.348.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/node-http-handler/-/node-http-handler-3.348.0.tgz", + "integrity": "sha512-wxdgc4tO5F6lN4wHr0CZ4TyIjDW/ORp4SJZdWYNs2L5J7+/SwqgJY2lxRlGi0i7Md+apAdE3sT3ukVQ/9pVfPg==", + "dependencies": { + "@aws-sdk/abort-controller": "3.347.0", + "@aws-sdk/protocol-http": "3.347.0", + "@aws-sdk/querystring-builder": "3.347.0", + "@aws-sdk/types": "3.347.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/property-provider": { + "version": "3.347.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/property-provider/-/property-provider-3.347.0.tgz", + "integrity": "sha512-t3nJ8CYPLKAF2v9nIHOHOlF0CviQbTvbFc2L4a+A+EVd/rM4PzL3+3n8ZJsr0h7f6uD04+b5YRFgKgnaqLXlEg==", + "dependencies": { + "@aws-sdk/types": "3.347.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/protocol-http": { + "version": "3.347.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/protocol-http/-/protocol-http-3.347.0.tgz", + "integrity": "sha512-2YdBhc02Wvy03YjhGwUxF0UQgrPWEy8Iq75pfS42N+/0B/+eWX1aQgfjFxIpLg7YSjT5eKtYOQGlYd4MFTgj9g==", + "dependencies": { + "@aws-sdk/types": "3.347.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/querystring-builder": { + "version": "3.347.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/querystring-builder/-/querystring-builder-3.347.0.tgz", + "integrity": "sha512-phtKTe6FXoV02MoPkIVV6owXI8Mwr5IBN3bPoxhcPvJG2AjEmnetSIrhb8kwc4oNhlwfZwH6Jo5ARW/VEWbZtg==", + "dependencies": { + "@aws-sdk/types": "3.347.0", + "@aws-sdk/util-uri-escape": "3.310.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/querystring-parser": { + "version": "3.347.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/querystring-parser/-/querystring-parser-3.347.0.tgz", + "integrity": "sha512-5VXOhfZz78T2W7SuXf2avfjKglx1VZgZgp9Zfhrt/Rq+MTu2D+PZc5zmJHhYigD7x83jLSLogpuInQpFMA9LgA==", + "dependencies": { + "@aws-sdk/types": "3.347.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/service-error-classification": { + "version": "3.347.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/service-error-classification/-/service-error-classification-3.347.0.tgz", + "integrity": "sha512-xZ3MqSY81Oy2gh5g0fCtooAbahqh9VhsF8vcKjVX8+XPbGC8y+kej82+MsMg4gYL8gRFB9u4hgYbNgIS6JTAvg==", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/shared-ini-file-loader": { + "version": "3.347.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/shared-ini-file-loader/-/shared-ini-file-loader-3.347.0.tgz", + "integrity": "sha512-Xw+zAZQVLb+xMNHChXQ29tzzLqm3AEHsD8JJnlkeFjeMnWQtXdUfOARl5s8NzAppcKQNlVe2gPzjaKjoy2jz1Q==", + "dependencies": { + "@aws-sdk/types": "3.347.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/signature-v4": { + "version": "3.347.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4/-/signature-v4-3.347.0.tgz", + "integrity": "sha512-58Uq1do+VsTHYkP11dTK+DF53fguoNNJL9rHRWhzP+OcYv3/mBMLoS2WPz/x9FO5mBg4ESFsug0I6mXbd36tjw==", + "dependencies": { + "@aws-sdk/eventstream-codec": "3.347.0", + "@aws-sdk/is-array-buffer": "3.310.0", + "@aws-sdk/types": "3.347.0", + "@aws-sdk/util-hex-encoding": "3.310.0", + "@aws-sdk/util-middleware": "3.347.0", + "@aws-sdk/util-uri-escape": "3.310.0", + "@aws-sdk/util-utf8": "3.310.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/signature-v4-multi-region": { + "version": "3.347.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.347.0.tgz", + "integrity": "sha512-838h7pbRCVYWlTl8W+r5+Z5ld7uoBObgAn7/RB1MQ4JjlkfLdN7emiITG6ueVL+7gWZNZc/4dXR/FJSzCgrkxQ==", + "dependencies": { + "@aws-sdk/protocol-http": "3.347.0", + "@aws-sdk/signature-v4": "3.347.0", + "@aws-sdk/types": "3.347.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "@aws-sdk/signature-v4-crt": "^3.118.0" + }, + "peerDependenciesMeta": { + "@aws-sdk/signature-v4-crt": { + "optional": true + } + } + }, + "node_modules/@aws-sdk/smithy-client": { + "version": "3.347.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/smithy-client/-/smithy-client-3.347.0.tgz", + "integrity": "sha512-PaGTDsJLGK0sTjA6YdYQzILRlPRN3uVFyqeBUkfltXssvUzkm8z2t1lz2H4VyJLAhwnG5ZuZTNEV/2mcWrU7JQ==", + "dependencies": { + "@aws-sdk/middleware-stack": "3.347.0", + "@aws-sdk/types": "3.347.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/token-providers": { + "version": "3.348.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.348.0.tgz", + "integrity": "sha512-nTjoJkUsJUrJTZuqaeMD9PW2//Rdg2HgfDjiyC4jmAXtayWYCi11mqauurMaUHJ3p5qJ8f5xzxm6vBTbrftPag==", + "dependencies": { + "@aws-sdk/client-sso-oidc": "3.348.0", + "@aws-sdk/property-provider": "3.347.0", + "@aws-sdk/shared-ini-file-loader": "3.347.0", + "@aws-sdk/types": "3.347.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/types": { + "version": "3.347.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.347.0.tgz", + "integrity": "sha512-GkCMy79mdjU9OTIe5KT58fI/6uqdf8UmMdWqVHmFJ+UpEzOci7L/uw4sOXWo7xpPzLs6cJ7s5ouGZW4GRPmHFA==", + "dependencies": { + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/url-parser": { + "version": "3.347.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/url-parser/-/url-parser-3.347.0.tgz", + "integrity": "sha512-lhrnVjxdV7hl+yCnJfDZOaVLSqKjxN20MIOiijRiqaWGLGEAiSqBreMhL89X1WKCifxAs4zZf9YB9SbdziRpAA==", + "dependencies": { + "@aws-sdk/querystring-parser": "3.347.0", + "@aws-sdk/types": "3.347.0", + "tslib": "^2.5.0" + } + }, + "node_modules/@aws-sdk/util-arn-parser": { + "version": "3.310.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.310.0.tgz", + "integrity": "sha512-jL8509owp/xB9+Or0pvn3Fe+b94qfklc2yPowZZIFAkFcCSIdkIglz18cPDWnYAcy9JGewpMS1COXKIUhZkJsA==", + "dependencies": { + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/util-base64": { + "version": "3.310.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-base64/-/util-base64-3.310.0.tgz", + "integrity": "sha512-v3+HBKQvqgdzcbL+pFswlx5HQsd9L6ZTlyPVL2LS9nNXnCcR3XgGz9jRskikRUuUvUXtkSG1J88GAOnJ/apTPg==", + "dependencies": { + "@aws-sdk/util-buffer-from": "3.310.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/util-body-length-browser": { + "version": "3.310.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-body-length-browser/-/util-body-length-browser-3.310.0.tgz", + "integrity": "sha512-sxsC3lPBGfpHtNTUoGXMQXLwjmR0zVpx0rSvzTPAuoVILVsp5AU/w5FphNPxD5OVIjNbZv9KsKTuvNTiZjDp9g==", + "dependencies": { + "tslib": "^2.5.0" + } + }, + "node_modules/@aws-sdk/util-body-length-node": { + "version": "3.310.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-body-length-node/-/util-body-length-node-3.310.0.tgz", + "integrity": "sha512-2tqGXdyKhyA6w4zz7UPoS8Ip+7sayOg9BwHNidiGm2ikbDxm1YrCfYXvCBdwaJxa4hJfRVz+aL9e+d3GqPI9pQ==", + "dependencies": { + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/util-buffer-from": { + "version": "3.310.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-buffer-from/-/util-buffer-from-3.310.0.tgz", + "integrity": "sha512-i6LVeXFtGih5Zs8enLrt+ExXY92QV25jtEnTKHsmlFqFAuL3VBeod6boeMXkN2p9lbSVVQ1sAOOYZOHYbYkntw==", + "dependencies": { + "@aws-sdk/is-array-buffer": "3.310.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/util-config-provider": { + "version": "3.310.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-config-provider/-/util-config-provider-3.310.0.tgz", + "integrity": "sha512-xIBaYo8dwiojCw8vnUcIL4Z5tyfb1v3yjqyJKJWV/dqKUFOOS0U591plmXbM+M/QkXyML3ypon1f8+BoaDExrg==", + "dependencies": { + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/util-defaults-mode-browser": { + "version": "3.347.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-defaults-mode-browser/-/util-defaults-mode-browser-3.347.0.tgz", + "integrity": "sha512-+JHFA4reWnW/nMWwrLKqL2Lm/biw/Dzi/Ix54DAkRZ08C462jMKVnUlzAI+TfxQE3YLm99EIa0G7jiEA+p81Qw==", + "dependencies": { + "@aws-sdk/property-provider": "3.347.0", + "@aws-sdk/types": "3.347.0", + "bowser": "^2.11.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@aws-sdk/util-defaults-mode-node": { + "version": "3.347.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-defaults-mode-node/-/util-defaults-mode-node-3.347.0.tgz", + "integrity": "sha512-A8BzIVhAAZE5WEukoAN2kYebzTc99ZgncbwOmgCCbvdaYlk5tzguR/s+uoT4G0JgQGol/4hAMuJEl7elNgU6RQ==", + "dependencies": { + "@aws-sdk/config-resolver": "3.347.0", + "@aws-sdk/credential-provider-imds": "3.347.0", + "@aws-sdk/node-config-provider": "3.347.0", + "@aws-sdk/property-provider": "3.347.0", + "@aws-sdk/types": "3.347.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@aws-sdk/util-endpoints": { + "version": "3.347.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.347.0.tgz", + "integrity": "sha512-/WUkirizeNAqwVj0zkcrqdQ9pUm1HY5kU+qy7xTR0OebkuJauglkmSTMD+56L1JPunWqHhlwCMVRaz5eaJdSEQ==", + "dependencies": { + "@aws-sdk/types": "3.347.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/util-hex-encoding": { + "version": "3.310.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-hex-encoding/-/util-hex-encoding-3.310.0.tgz", + "integrity": "sha512-sVN7mcCCDSJ67pI1ZMtk84SKGqyix6/0A1Ab163YKn+lFBQRMKexleZzpYzNGxYzmQS6VanP/cfU7NiLQOaSfA==", + "dependencies": { + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/util-locate-window": { + "version": "3.310.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.310.0.tgz", + "integrity": "sha512-qo2t/vBTnoXpjKxlsC2e1gBrRm80M3bId27r0BRB2VniSSe7bL1mmzM+/HFtujm0iAxtPM+aLEflLJlJeDPg0w==", + "dependencies": { + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/util-middleware": { + "version": "3.347.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-middleware/-/util-middleware-3.347.0.tgz", + "integrity": "sha512-8owqUA3ePufeYTUvlzdJ7Z0miLorTwx+rNol5lourGQZ9JXsVMo23+yGA7nOlFuXSGkoKpMOtn6S0BT2bcfeiw==", + "dependencies": { + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/util-retry": { + "version": "3.347.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-retry/-/util-retry-3.347.0.tgz", + "integrity": "sha512-NxnQA0/FHFxriQAeEgBonA43Q9/VPFQa8cfJDuT2A1YZruMasgjcltoZszi1dvoIRWSZsFTW42eY2gdOd0nffQ==", + "dependencies": { + "@aws-sdk/service-error-classification": "3.347.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@aws-sdk/util-stream-browser": { + "version": "3.347.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-stream-browser/-/util-stream-browser-3.347.0.tgz", + "integrity": "sha512-pIbmzIJfyX26qG622uIESOmJSMGuBkhmNU7I98bzhYCet5ctC0ow9L5FZw9ljOE46P/HkEcsOhh+qTHyCXlCEQ==", + "dependencies": { + "@aws-sdk/fetch-http-handler": "3.347.0", + "@aws-sdk/types": "3.347.0", + "@aws-sdk/util-base64": "3.310.0", + "@aws-sdk/util-hex-encoding": "3.310.0", + "@aws-sdk/util-utf8": "3.310.0", + "tslib": "^2.5.0" + } + }, + "node_modules/@aws-sdk/util-stream-node": { + "version": "3.348.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-stream-node/-/util-stream-node-3.348.0.tgz", + "integrity": "sha512-MFXyMUWA2oD0smBZf+sdnuyxLw8nCqyMEgYbos+6grvF1Szxn5+zbYTZrEBYiICqD1xJRLbWTzFLJU7oYm6pUg==", + "dependencies": { + "@aws-sdk/node-http-handler": "3.348.0", + "@aws-sdk/types": "3.347.0", + "@aws-sdk/util-buffer-from": "3.310.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/util-uri-escape": { + "version": "3.310.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-uri-escape/-/util-uri-escape-3.310.0.tgz", + "integrity": "sha512-drzt+aB2qo2LgtDoiy/3sVG8w63cgLkqFIa2NFlGpUgHFWTXkqtbgf4L5QdjRGKWhmZsnqkbtL7vkSWEcYDJ4Q==", + "dependencies": { + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.347.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.347.0.tgz", + "integrity": "sha512-ydxtsKVtQefgbk1Dku1q7pMkjDYThauG9/8mQkZUAVik55OUZw71Zzr3XO8J8RKvQG8lmhPXuAQ0FKAyycc0RA==", + "dependencies": { + "@aws-sdk/types": "3.347.0", + "bowser": "^2.11.0", + "tslib": "^2.5.0" + } + }, + "node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.347.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.347.0.tgz", + "integrity": "sha512-6X0b9qGsbD1s80PmbaB6v1/ZtLfSx6fjRX8caM7NN0y/ObuLoX8LhYnW6WlB2f1+xb4EjaCNgpP/zCf98MXosw==", + "dependencies": { + "@aws-sdk/node-config-provider": "3.347.0", + "@aws-sdk/types": "3.347.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } + } + }, + "node_modules/@aws-sdk/util-utf8": { + "version": "3.310.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-utf8/-/util-utf8-3.310.0.tgz", + "integrity": "sha512-DnLfFT8uCO22uOJc0pt0DsSNau1GTisngBCDw8jQuWT5CqogMJu4b/uXmwEqfj8B3GX6Xsz8zOd6JpRlPftQoA==", + "dependencies": { + "@aws-sdk/util-buffer-from": "3.310.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/util-utf8-browser": { + "version": "3.259.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-utf8-browser/-/util-utf8-browser-3.259.0.tgz", + "integrity": "sha512-UvFa/vR+e19XookZF8RzFZBrw2EUkQWxiBW0yYQAhvk3C+QVGl0H3ouca8LDBlBfQKXwmW3huo/59H8rwb1wJw==", + "dependencies": { + "tslib": "^2.3.1" + } + }, + "node_modules/@aws-sdk/util-waiter": { + "version": "3.347.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-waiter/-/util-waiter-3.347.0.tgz", + "integrity": "sha512-3ze/0PkwkzUzLncukx93tZgGL0JX9NaP8DxTi6WzflnL/TEul5Z63PCruRNK0om17iZYAWKrf8q2mFoHYb4grA==", + "dependencies": { + "@aws-sdk/abort-controller": "3.347.0", + "@aws-sdk/types": "3.347.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/xml-builder": { + "version": "3.310.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.310.0.tgz", + "integrity": "sha512-TqELu4mOuSIKQCqj63fGVs86Yh+vBx5nHRpWKNUNhB2nPTpfbziTs5c1X358be3peVWA4wPxW7Nt53KIg1tnNw==", + "dependencies": { + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -198,6 +1556,29 @@ "node": ">= 8" } }, + "node_modules/@smithy/protocol-http": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-1.0.1.tgz", + "integrity": "sha512-9OrEn0WfOVtBNYJUjUAn9AOiJ4lzERCJJ/JeZs8E6yajTGxBaFRxUnNBHiNqoDJVg076hY36UmEnPx7xXrvUSg==", + "dependencies": { + "@smithy/types": "^1.0.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/types": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-1.0.0.tgz", + "integrity": "sha512-kc1m5wPBHQCTixwuaOh9vnak/iJm21DrSf9UK6yDE5S3mQQ4u11pqAUiKWnlrZnYkeLfAI9UEHj9OaMT1v5Umg==", + "dependencies": { + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/@tootallnate/once": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", @@ -337,16 +1718,6 @@ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, - "node_modules/axios": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.4.0.tgz", - "integrity": "sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==", - "dependencies": { - "follow-redirects": "^1.15.0", - "form-data": "^4.0.0", - "proxy-from-env": "^1.1.0" - } - }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -362,6 +1733,11 @@ "node": ">=8" } }, + "node_modules/bowser": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.11.0.tgz", + "integrity": "sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA==" + }, "node_modules/brace-expansion": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", @@ -539,6 +1915,22 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true }, + "node_modules/config": { + "version": "3.3.9", + "resolved": "https://registry.npmjs.org/config/-/config-3.3.9.tgz", + "integrity": "sha512-G17nfe+cY7kR0wVpc49NCYvNtelm/pPy8czHoFkAgtV1lkmcp7DHtWCdDu+C9Z7gb2WVqa9Tm3uF9aKaPbCfhg==", + "dependencies": { + "json5": "^2.2.3" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -924,6 +2316,27 @@ "dev": true, "peer": true }, + "node_modules/fast-xml-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.2.4.tgz", + "integrity": "sha512-fbfMDvgBNIdDJLdLOwacjFAPYt67tr31H9ZhWSm45CDAxvd0I6WTlSOUo7K2P/K5sA5JgMKG64PI3DMcaFdWpQ==", + "funding": [ + { + "type": "paypal", + "url": "https://paypal.me/naturalintelligence" + }, + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "dependencies": { + "strnum": "^1.0.5" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, "node_modules/fastq": { "version": "1.15.0", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", @@ -1005,25 +2418,6 @@ "dev": true, "peer": true }, - "node_modules/follow-redirects": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", - "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, "node_modules/form-data": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", @@ -1227,6 +2621,11 @@ "node": ">= 4" } }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==" + }, "node_modules/import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -1267,8 +2666,7 @@ "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, "node_modules/is-binary-path": { "version": "2.1.0", @@ -1357,6 +2755,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -1442,6 +2845,28 @@ "dev": true, "peer": true }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -1456,6 +2881,14 @@ "node": ">= 0.8.0" } }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "dependencies": { + "immediate": "~3.0.5" + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -1708,6 +3141,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -1791,10 +3229,10 @@ "node": ">= 0.8.0" } }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" }, "node_modules/psl": { "version": "1.9.0", @@ -1844,6 +3282,25 @@ "safe-buffer": "^5.1.0" } }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -1981,6 +3438,11 @@ "randombytes": "^2.1.0" } }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -2004,6 +3466,19 @@ "node": ">=8" } }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -2042,6 +3517,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strnum": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz", + "integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==" + }, "node_modules/supports-color": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", @@ -2106,6 +3586,11 @@ "node": ">=14" } }, + "node_modules/tslib": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.3.tgz", + "integrity": "sha512-mSxlJJwl3BMEQCUNnxXBU9jP4JBktcEGhURcPR6VQVlnP0FdDEsIaz0C35dXNGLyRfrATNofF0F5p2KPxQgB+w==" + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -2168,6 +3653,19 @@ "requires-port": "^1.0.0" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/w3c-xmlserializer": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", diff --git a/tests/remote_js/package.json b/tests/remote_js/package.json index 561ec446..9522f3db 100644 --- a/tests/remote_js/package.json +++ b/tests/remote_js/package.json @@ -1,6 +1,7 @@ { "dependencies": { "@aws-sdk/client-s3": "^3.338.0", + "@aws-sdk/client-sqs": "^3.348.0", "config": "^3.3.9", "jsdom": "^22.0.0", "jszip": "^3.10.1", @@ -13,6 +14,6 @@ "mocha": "^10.2.0" }, "scripts": { - "test": "mocha \"test/**/*.js\"" + "test": "mocha \"test/**/*.*js\"" } } diff --git a/tests/remote_js/test/3/pdfTextExtractionTest.mjs b/tests/remote_js/test/3/pdfTextExtractionTest.mjs index 4a42267c..6370a591 100644 --- a/tests/remote_js/test/3/pdfTextExtractionTest.mjs +++ b/tests/remote_js/test/3/pdfTextExtractionTest.mjs @@ -5,18 +5,30 @@ import API from '../../api3.js'; import Helpers from '../../helpers3.js'; import shared from "../shared.js"; import { S3Client, DeleteObjectsCommand } from "@aws-sdk/client-s3"; +import { SQSClient, PurgeQueueCommand } from "@aws-sdk/client-sqs"; import fs from 'fs'; import HTTP from '../../httpHandler.js'; import { localInvoke } from '../../full-text-extractor/src/local_invoke.mjs'; -describe('FileTestTests', function () { +describe('PDFTextExtractionTests', function () { this.timeout(0); let toDelete = []; const s3Client = new S3Client({ region: "us-east-1" }); + const sqsClient = new SQSClient(); before(async function () { await shared.API3Before(); + // Clean up test queue. + // Calling PurgeQueue many times in a row throws an error so sometimes we have to wait. + try { + await sqsClient.send(new PurgeQueueCommand({ QueueUrl: config.fullTextExtractorSQSUrl })); + } + catch (e) { + await new Promise(r => setTimeout(r, 5000)); + await sqsClient.send(new PurgeQueueCommand({ QueueUrl: config.fullTextExtractorSQSUrl })); + } + try { fs.mkdirSync("./work"); } @@ -122,8 +134,16 @@ describe('FileTestTests', function () { toDelete.push(md5); - // Local invoke full-text-extractor - await localInvoke(); + // Local invoke full-text-extractor if it's a local test run + if (config.isLocalRun) { + await new Promise(r => setTimeout(r, 5000)); + const processedCount = await localInvoke(); + assert.equal(processedCount, 1); + } + else { + // If it's a run on AWS, just wait for lambda to finish + await new Promise(r => setTimeout(r, 10000)); + } // Get full text to ensure full-text-extractor worked response = await API.userGet( @@ -136,6 +156,298 @@ describe('FileTestTests', function () { assert.equal(data.content.trim(), pdfText); }); + it('should_not_add_non_pdf_to_queue', async function () { + let json = await API.createItem("book", false, this, 'json'); + assert.equal(0, json.meta.numChildren); + let parentKey = json.key; + + json = await API.createAttachmentItem("imported_file", [], parentKey, this, 'json'); + let attachmentKey = json.key; + let version = json.version; + + let filename = "dummy.txt"; + let mtime = Date.now(); + + let fileContents = Helpers.getRandomUnicodeString(); + let size = Buffer.from(fileContents).byteLength; + let md5 = Helpers.md5(fileContents); + + // Create attachment item + let response = await API.userPost( + config.userID, + "items", + JSON.stringify([ + { + key: attachmentKey, + contentType: "text/plain", + } + ]), + { + "Content-Type": "application/json", + "If-Unmodified-Since-Version": version + } + ); + Helpers.assert200ForObject(response); + + // Get upload authorization + response = await API.userPost( + config.userID, + "items/" + attachmentKey + "/file", + Helpers.implodeParams({ + md5: md5, + mtime: mtime, + filename: filename, + filesize: size + }), + { + "Content-Type": "application/x-www-form-urlencoded", + "If-None-Match": "*" + } + ); + Helpers.assert200(response); + json = API.getJSONFromResponse(response); + + // Upload + response = await HTTP.post( + json.url, + json.prefix + fileContents + json.suffix, + { + "Content-Type": json.contentType + } + ); + Helpers.assert201(response); + + // Post-upload file registration + response = await API.userPost( + config.userID, + "items/" + attachmentKey + "/file", + "upload=" + json.uploadKey, + { + "Content-Type": "application/x-www-form-urlencoded", + "If-None-Match": "*" + } + ); + Helpers.assert204(response); + + toDelete.push(md5); + + // Local invoke full-text-extractor if it's a local test run + if (config.isLocalRun) { + // Wait for SQS to make the message available + await new Promise(r => setTimeout(r, 5000)); + const processedCount = await localInvoke(); + assert.equal(processedCount, 0); + } + else { + // If it's a run on AWS, just wait for lambda to finish + await new Promise(r => setTimeout(r, 10000)); + } + + + // Get full text to ensure full-text-extractor was not triggered + response = await API.userGet( + config.userID, + "items/" + attachmentKey + "/fulltext", + ); + Helpers.assert404(response); + }); + + it('should_not_add_pdf_from_desktop_client_to_queue', async function () { + let json = await API.createItem("book", false, this, 'json'); + assert.equal(0, json.meta.numChildren); + let parentKey = json.key; + + json = await API.createAttachmentItem("imported_file", [], parentKey, this, 'json'); + let attachmentKey = json.key; + let version = json.version; + + let filename = "dummy.pdf"; + let mtime = Date.now(); + makeRandomPDF(); + + let fileContents = fs.readFileSync("./work/dummy.pdf"); + let size = Buffer.from(fileContents.toString()).byteLength; + let md5 = Helpers.md5(fileContents.toString()); + + // Create attachment item + let response = await API.userPost( + config.userID, + "items", + JSON.stringify([ + { + key: attachmentKey, + contentType: "application/pdf", + } + ]), + { + "Content-Type": "application/json", + "If-Unmodified-Since-Version": version + } + ); + Helpers.assert200ForObject(response); + + // Get upload authorization + response = await API.userPost( + config.userID, + "items/" + attachmentKey + "/file", + Helpers.implodeParams({ + md5: md5, + mtime: mtime, + filename: filename, + filesize: size + }), + { + "Content-Type": "application/x-www-form-urlencoded", + "If-None-Match": "*" + } + ); + Helpers.assert200(response); + json = API.getJSONFromResponse(response); + + // Upload + response = await HTTP.post( + json.url, + json.prefix + fileContents + json.suffix, + { + "Content-Type": json.contentType + } + ); + Helpers.assert201(response); + + // Post-upload file registration + response = await API.userPost( + config.userID, + "items/" + attachmentKey + "/file", + "upload=" + json.uploadKey, + { + "Content-Type": "application/x-www-form-urlencoded", + "If-None-Match": "*", + "X-Zotero-Version": "6.0.0" + } + ); + Helpers.assert204(response); + + toDelete.push(md5); + + // Local invoke full-text-extractor if it's a local test run + if (config.isLocalRun) { + await new Promise(r => setTimeout(r, 5000)); + const processedCount = await localInvoke(); + assert.equal(processedCount, 0); + } + else { + // If it's a run on AWS, just wait for lambda to finish + await new Promise(r => setTimeout(r, 10000)); + } + + // Get full text to ensure full-text-extractor was not called + response = await API.userGet( + config.userID, + "items/" + attachmentKey + "/fulltext", + ); + Helpers.assert404(response); + }); + + it('should_extract_pdf_text_group', async function () { + let filename = "dummy.pdf"; + let mtime = Date.now(); + const pdfText = makeRandomPDF(); + + let fileContents = fs.readFileSync("./work/dummy.pdf"); + let size = Buffer.from(fileContents.toString()).byteLength; + let hash = Helpers.md5(fileContents.toString()); + + let groupID = await API.createGroup({ + owner: config.userID, + type: "PublicClosed", + name: Helpers.uniqueID(14), + libraryReading: "all", + fileEditing: "members", + }); + + let parentKey = await API.groupCreateItem(groupID, "book", false, this, "key"); + let attachmentKey = await API.groupCreateAttachmentItem( + groupID, + "imported_file", + { + contentType: "text/plain", + charset: "utf-8", + }, + parentKey, + this, + "key" + ); + + // Get authorization + let response = await API.groupPost( + groupID, + `items/${attachmentKey}/file`, + Helpers.implodeParams({ + md5: hash, + mtime: mtime, + filename: filename, + filesize: size, + }), + { + "Content-Type": "application/x-www-form-urlencoded", + "If-None-Match": "*", + }, + + ); + Helpers.assert200(response); + let json = API.getJSONFromResponse(response); + + toDelete.push(hash); + + // + // Upload to S3 + // + response = await HTTP.post(json.url, `${json.prefix}${fileContents}${json.suffix}`, { + + "Content-Type": `${json.contentType}`, + }, + ); + Helpers.assert201(response); + + // Successful registration + response = await API.groupPost( + groupID, + `items/${attachmentKey}/file`, + `upload=${json.uploadKey}`, + { + + "Content-Type": "application/x-www-form-urlencoded", + "If-None-Match": "*", + }, + + ); + Helpers.assert204(response); + toDelete.push(hash); + + // Local invoke full-text-extractor if it's a local test run + if (config.isLocalRun) { + await new Promise(r => setTimeout(r, 5000)); + const processedCount = await localInvoke(); + assert.equal(processedCount, 1); + } + else { + // If it's a run on AWS, just wait for lambda to finish + await new Promise(r => setTimeout(r, 10000)); + } + + // Get full text to ensure full-text-extractor worked + response = await API.groupGet( + groupID, + "items/" + attachmentKey + "/fulltext", + ); + Helpers.assert200(response); + const data = JSON.parse(response.data); + assert.property(data, 'content'); + assert.equal(data.content.trim(), pdfText); + await API.deleteGroup(groupID); + }); + + const makeRandomPDF = () => { const randomText = Helpers.uniqueToken(); const pdfData = `%PDF-1.4 From 19764459c03b6e66078141d53265ec075655ff56 Mon Sep 17 00:00:00 2001 From: Bogdan Abaev Date: Mon, 12 Jun 2023 12:32:31 -0400 Subject: [PATCH 25/33] full-text tests with indexing function invoked locally --- .gitmodules | 3 + tests/remote_js/config/default.json5 | 7 +- tests/remote_js/full-text-indexer | 1 + .../test/2/{fullText.js => fullText.mjs} | 94 +++++++++++++++++-- .../3/{fullTextTest.js => fullTextTest.mjs} | 40 ++++++-- 5 files changed, 126 insertions(+), 19 deletions(-) create mode 160000 tests/remote_js/full-text-indexer rename tests/remote_js/test/2/{fullText.js => fullText.mjs} (74%) rename tests/remote_js/test/3/{fullTextTest.js => fullTextTest.mjs} (93%) diff --git a/.gitmodules b/.gitmodules index bb6dc6bf..1e50287d 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,6 @@ [submodule "tests/remote_js/full-text-extractor"] path = tests/remote_js/full-text-extractor url = https://github.com/zotero/full-text-extractor.git +[submodule "tests/remote_js/full-text-indexer"] + path = tests/remote_js/full-text-indexer + url = https://github.com/zotero/full-text-indexer.git diff --git a/tests/remote_js/config/default.json5 b/tests/remote_js/config/default.json5 index f5cc29ed..35a8e724 100644 --- a/tests/remote_js/config/default.json5 +++ b/tests/remote_js/config/default.json5 @@ -30,6 +30,11 @@ "ownedPrivateGroupLibraryID2": 0, "fullTextExtractorSQSUrl" : "", - "isLocalRun": false + "isLocalRun": false, + "es": { + "host": "", + "index": "item_fulltext_index", + "type": "item_fulltext" + } } diff --git a/tests/remote_js/full-text-indexer b/tests/remote_js/full-text-indexer new file mode 160000 index 00000000..d9624695 --- /dev/null +++ b/tests/remote_js/full-text-indexer @@ -0,0 +1 @@ +Subproject commit d962469501641c196c9a356ef8e02fe6a5aad772 diff --git a/tests/remote_js/test/2/fullText.js b/tests/remote_js/test/2/fullText.mjs similarity index 74% rename from tests/remote_js/test/2/fullText.js rename to tests/remote_js/test/2/fullText.mjs index cf7852ea..87a8fd03 100644 --- a/tests/remote_js/test/2/fullText.js +++ b/tests/remote_js/test/2/fullText.mjs @@ -1,19 +1,22 @@ -const chai = require('chai'); +import chai from 'chai'; const assert = chai.assert; -var config = require('config'); -const API = require('../../api2.js'); -const Helpers = require('../../helpers2.js'); -const { API2Before, API2After } = require("../shared.js"); +import config from 'config'; +import API from '../../api2.js'; +import Helpers from '../../helpers2.js'; +import shared from "../shared.js"; +import { s3 } from "../../full-text-indexer/index.mjs"; +import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3"; describe('FullTextTests', function () { this.timeout(config.timeout); + const s3Client = new S3Client({ region: "us-east-1" }); before(async function () { - await API2Before(); + await shared.API2Before(); }); after(async function () { - await API2After(); + await shared.API2After(); }); it('testSetItemContent', async function () { @@ -179,8 +182,81 @@ describe('FullTextTests', function () { }); //Requires ES - it('testSearchItemContent', async function() { - this.skip(); + it('testSearchItemContent', async function () { + let key = await API.createItem("book", [], this, 'key'); + let xml = await API.createAttachmentItem("imported_url", [], key, this, 'atom'); + let data = API.parseDataFromAtomEntry(xml); + + let response = await API.userGet( + config.userID, + "items/" + data.key + "/fulltext?key=" + config.apiKey + ); + Helpers.assert404(response); + + let content = "Here is some unique full-text content"; + let pages = 50; + + // Store content + response = await API.userPut( + config.userID, + "items/" + data.key + "/fulltext?key=" + config.apiKey, + JSON.stringify({ + content: content, + indexedPages: pages, + totalPages: pages + }), + { "Content-Type": "application/json" } + ); + + Helpers.assert204(response); + + // Local fake-invoke of lambda function that indexes pdf + if (config.isLocalRun) { + const s3Result = await s3Client.send(new GetObjectCommand({ Bucket: config.s3Bucket, Key: `${config.userID}/${data.key}` })); + + const event = { + eventName: "ObjectCreated", + s3: { + bucket: { + name: config.s3Bucket + }, + object: { + key: `${config.userID}/${data.key}`, + eTag: s3Result.ETag.slice(1, -1) + } + }, + + }; + await s3({ Records: [event] }); + } + + // Wait for indexing via Lambda + await new Promise(resolve => setTimeout(resolve, 6000)); + + // Search for a word + response = await API.userGet( + config.userID, + "items?q=unique&qmode=everything&format=keys&key=" + config.apiKey + ); + Helpers.assert200(response); + Helpers.assertEquals(data.key, response.data.trim()); + + // Search for a phrase + response = await API.userGet( + config.userID, + "items?q=unique%20full-text&qmode=everything&format=keys&key=" + config.apiKey + ); + Helpers.assert200(response); + Helpers.assertEquals(data.key, response.data.trim()); + + + // Search for nonexistent word + response = await API.userGet( + config.userID, + "items?q=nothing&qmode=everything&format=keys&key=" + config.apiKey + ); + Helpers.assert200(response); + Helpers.assertEquals("", response.data.trim()); }); it('testDeleteItemContent', async function () { diff --git a/tests/remote_js/test/3/fullTextTest.js b/tests/remote_js/test/3/fullTextTest.mjs similarity index 93% rename from tests/remote_js/test/3/fullTextTest.js rename to tests/remote_js/test/3/fullTextTest.mjs index e518242c..83213216 100644 --- a/tests/remote_js/test/3/fullTextTest.js +++ b/tests/remote_js/test/3/fullTextTest.mjs @@ -1,19 +1,22 @@ -const chai = require('chai'); +import chai from 'chai'; const assert = chai.assert; -var config = require('config'); -const API = require('../../api3.js'); -const Helpers = require('../../helpers3.js'); -const { API3Before, API3After } = require("../shared.js"); +import config from 'config'; +import API from '../../api3.js'; +import Helpers from '../../helpers3.js'; +import shared from "../shared.js"; +import { s3 } from "../../full-text-indexer/index.mjs"; +import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3"; describe('FullTextTests', function () { - this.timeout(config.timeout); + this.timeout(0); + const s3Client = new S3Client({ region: "us-east-1" }); before(async function () { - await API3Before(); + await shared.API3Before(); }); after(async function () { - await API3After(); + await shared.API3After(); }); this.beforeEach(async function () { @@ -225,7 +228,6 @@ describe('FullTextTests', function () { // Requires ES it('testSearchItemContent', async function () { - this.skip(); let collectionKey = await API.createCollection('Test', false, this, 'key'); let parentKey = await API.createItem("book", { collections: [collectionKey] }, this, 'key'); let json = await API.createAttachmentItem("imported_url", [], parentKey, this, 'jsonData'); @@ -254,6 +256,26 @@ describe('FullTextTests', function () { Helpers.assert204(response); + // Local fake-invoke of lambda function that indexes pdf + if (config.isLocalRun) { + const s3Result = await s3Client.send(new GetObjectCommand({ Bucket: config.s3Bucket, Key: `${config.userID}/${attachmentKey}` })); + + const event = { + eventName: "ObjectCreated", + s3: { + bucket: { + name: config.s3Bucket + }, + object: { + key: `${config.userID}/${attachmentKey}`, + eTag: s3Result.ETag.slice(1, -1) + } + }, + + }; + await s3({ Records: [event] }); + } + // Wait for indexing via Lambda await new Promise(resolve => setTimeout(resolve, 6000)); From d8f34271f9af4cf373ebf0b4bda085a227e5fb94 Mon Sep 17 00:00:00 2001 From: Bogdan Abaev Date: Fri, 23 Jun 2023 14:06:44 -0400 Subject: [PATCH 26/33] items/unfiled and items/unfiled/tags tests For #152 --- tests/remote_js/test/3/itemTest.js | 19 +++++++++++++++++++ tests/remote_js/test/3/tagTest.js | 22 ++++++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/tests/remote_js/test/3/itemTest.js b/tests/remote_js/test/3/itemTest.js index 7c3b7536..38cc4490 100644 --- a/tests/remote_js/test/3/itemTest.js +++ b/tests/remote_js/test/3/itemTest.js @@ -1465,6 +1465,25 @@ describe('ItemsTests', function () { Helpers.assertEquals("aaa", json[0].data.title); }); + it('test_unfiled', async function () { + await API.userClear(config.userID); + + let collectionKey = await API.createCollection('Test', false, this, 'key'); + await API.createItem("book", { title: 'aaa' }, this, 'key'); + await API.createItem("book", { title: 'bbb' }, this, 'key'); + + await API.createItem("book", { title: 'ccc', collections: [collectionKey] }, this, 'key'); + let parentBookInCollection = await API.createItem("book", { title: 'ddd', collections: [collectionKey] }, this, 'key'); + await API.createNoteItem("some note", parentBookInCollection, this, 'key'); + + let response = await API.userGet(config.userID, `items/unfiled?sort=title`); + Helpers.assert200(response); + Helpers.assertNumResults(response, 2); + let json = API.getJSONFromResponse(response); + Helpers.assertEquals("aaa", json[0].data.title); + Helpers.assertEquals("bbb", json[1].data.title); + }); + /** * Date Modified shouldn't be changed if 1) dateModified is provided or 2) certain fields are changed */ diff --git a/tests/remote_js/test/3/tagTest.js b/tests/remote_js/test/3/tagTest.js index 2fb61985..a2b16776 100644 --- a/tests/remote_js/test/3/tagTest.js +++ b/tests/remote_js/test/3/tagTest.js @@ -819,4 +819,26 @@ describe('TagTests', function () { tags.slice(1) ); }); + + it('tests_unfiled_tags', async function () { + await API.userClear(config.userID); + + let collectionKey = await API.createCollection('Test', false, this, 'key'); + await API.createItem("book", { title: 'aaa', tags: [{ tag: "unfiled" }] }, this, 'key'); + await API.createItem("book", { title: 'bbb', tags: [{ tag: "unfiled" }] }, this, 'key'); + + await API.createItem("book", { title: 'ccc', collections: [collectionKey], tags: [{ tag: "filed" }] }, this, 'key'); + let parentBookInCollection = await API.createItem("book", + { title: 'ddd', + collections: [collectionKey], + tags: [{ tag: "also_filed" }] }, + this, 'key'); + await API.createNoteItem("some note", parentBookInCollection, this, 'key'); + + let response = await API.userGet(config.userID, `items/unfiled/tags`); + Helpers.assert200(response); + Helpers.assertNumResults(response, 1); + let json = API.getJSONFromResponse(response); + Helpers.assertEquals("unfiled", json[0].tag); + }); }); From 2e7a8531cade28c27363944f2e7c4d4171876f0e Mon Sep 17 00:00:00 2001 From: Bogdan Abaev Date: Fri, 23 Jun 2023 14:59:37 -0400 Subject: [PATCH 27/33] ?sort=editBy for group libraries test for #154 --- tests/remote_js/test/3/sortTest.js | 51 +++++++++++++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/tests/remote_js/test/3/sortTest.js b/tests/remote_js/test/3/sortTest.js index 565c0b9e..26f8e05f 100644 --- a/tests/remote_js/test/3/sortTest.js +++ b/tests/remote_js/test/3/sortTest.js @@ -3,7 +3,7 @@ const assert = chai.assert; var config = require('config'); const API = require('../../api3.js'); const Helpers = require('../../helpers3.js'); -const { API3Before, API3After } = require("../shared.js"); +const { API3Before, API3After, resetGroups } = require("../shared.js"); describe('SortTests', function () { this.timeout(config.timeout); @@ -21,6 +21,7 @@ describe('SortTests', function () { before(async function () { await API3Before(); await setup(); + await resetGroups(); }); after(async function () { @@ -76,6 +77,54 @@ describe('SortTests', function () { }*/ }; + it('test_sort_group_by_editedBy', async function () { + // user 1 makes item + let jsonOne = await API.groupCreateItem( + config.ownedPrivateGroupID, + 'book', + { title: `title_one` }, + true, + 'jsonData' + ); + + API.useAPIKey(config.user2APIKey); + + // user 2 makes item + let jsonTwo = await API.groupCreateItem( + config.ownedPrivateGroupID, + 'book', + { title: `title_two` }, + true, + 'jsonData' + ); + + // make sure, user's one item goes first + let response = await API.get(`groups/${config.ownedPrivateGroupID}/items?sort=editedBy&format=keys`); + let sortedKeys = response.data.split('\n'); + assert.equal(sortedKeys[0], jsonOne.key); + assert.equal(sortedKeys[1], jsonTwo.key); + + // user 2 updates user1's item, and the other way around + response = await API.patch(`groups/${config.ownedPrivateGroupID}/items/${jsonOne.key}`, + JSON.stringify({ title: 'updated_by_user 2' }), + { 'If-Unmodified-Since-Version': jsonOne.version }); + + Helpers.assert204(response); + + API.useAPIKey(config.apiKey); + + response = await API.patch(`groups/${config.ownedPrivateGroupID}/items/${jsonTwo.key}`, + JSON.stringify({ title: 'updated_by_user 2' }), + { 'If-Unmodified-Since-Version': jsonTwo.version }); + Helpers.assert204(response); + + // now order should be switched + response = await API.get(`groups/${config.ownedPrivateGroupID}/items?sort=editedBy&format=keys`); + sortedKeys = response.data.split('\n'); + assert.equal(sortedKeys[0], jsonTwo.key); + assert.equal(sortedKeys[1], jsonOne.key); + }); + it('testSortTopItemsTitle', async function () { let response = await API.userGet( config.userID, From d4465f2675ae5241a181b13ceb5b470da8fe6621 Mon Sep 17 00:00:00 2001 From: Bogdan Abaev Date: Fri, 23 Jun 2023 15:57:24 -0400 Subject: [PATCH 28/33] test to rename tag for PR #150 --- tests/remote_js/test/3/tagTest.js | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/remote_js/test/3/tagTest.js b/tests/remote_js/test/3/tagTest.js index a2b16776..c12605cd 100644 --- a/tests/remote_js/test/3/tagTest.js +++ b/tests/remote_js/test/3/tagTest.js @@ -32,6 +32,26 @@ describe('TagTests', function () { assert.deepEqual(json.successful[0].data.tags, [{ tag: 'A' }]); }); + it('test_rename_tag', async function () { + let json = await API.getItemTemplate("book"); + json.tags.push({ tag: "A" }); + + let response = await API.postItem(json); + Helpers.assert200ForObject(response); + json = API.getJSONFromResponse(response); + assert.deepEqual(json.successful[0].data.tags, [{ tag: 'A' }]); + let libraryVersion = await API.getLibraryVersion(); + response = await API.userPost(config.userID, `tags?tag=A&tagName=B`, '{}', { 'If-Unmodified-Since-Version': libraryVersion }); + Helpers.assert204(response); + + response = await API.userGet(config.userID, `/items/${json.successful[0].key}`); + const data = JSON.parse(response.data).data; + assert.equal(data.tags[0].tag, "B"); + + response = await API.userGet(`/tags/A`); + Helpers.assert404(response); + }); + it('testInvalidTagObject', async function () { let json = await API.getItemTemplate("book"); json.tags.push(["invalid"]); From e7518c2435c9a1a61ecf7d50f007aa4304f88302 Mon Sep 17 00:00:00 2001 From: Bogdan Abaev Date: Fri, 23 Jun 2023 16:42:50 -0400 Subject: [PATCH 29/33] test for literal || escaping for tags PR #143 --- tests/remote_js/test/3/tagTest.js | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/remote_js/test/3/tagTest.js b/tests/remote_js/test/3/tagTest.js index c12605cd..72775961 100644 --- a/tests/remote_js/test/3/tagTest.js +++ b/tests/remote_js/test/3/tagTest.js @@ -52,6 +52,30 @@ describe('TagTests', function () { Helpers.assert404(response); }); + it('test_||_escaping', async function () { + let json = await API.getItemTemplate("book"); + json.tags.push({ tag: "This || That" }); + + let response = await API.postItem(json); + Helpers.assert200ForObject(response); + json = API.getJSONFromResponse(response); + assert.deepEqual(json.successful[0].data.tags, [{ tag: 'This || That' }]); + + response = await API.userGet(config.userID, `/items/?tag=This&format=keys`); + assert.equal(response.data, "\n"); + response = await API.userGet(config.userID, `/items/?tag=That&format=keys`); + assert.equal(response.data, "\n"); + response = await API.userGet(config.userID, '/items/?tag=This%20\\||%20That&format=keys'); + assert.equal(response.data, `${json.successful[0].key}\n`); + + let libraryVersion = await API.getLibraryVersion(); + response = await API.userDelete(config.userID, `tags?tag=This%20\\||%20That`, { "If-Unmodified-Since-Version": libraryVersion }); + Helpers.assert204(response); + + response = await API.userGet(config.userID, `/items/?tag=This%20\\||%20That&format=keys`); + assert.equal(response.data, `\n`); + }); + it('testInvalidTagObject', async function () { let json = await API.getItemTemplate("book"); json.tags.push(["invalid"]); From c5b1a93e0ac3e7e6016051a6b58c2f1e6e2e1904 Mon Sep 17 00:00:00 2001 From: Bogdan Abaev Date: Tue, 27 Jun 2023 23:41:28 -0400 Subject: [PATCH 30/33] new annotation types test --- tests/remote_js/test/3/annotationsTest.js | 67 ++++++++++++++++++++++- 1 file changed, 64 insertions(+), 3 deletions(-) diff --git a/tests/remote_js/test/3/annotationsTest.js b/tests/remote_js/test/3/annotationsTest.js index 28d85d1f..3380e2a9 100644 --- a/tests/remote_js/test/3/annotationsTest.js +++ b/tests/remote_js/test/3/annotationsTest.js @@ -8,18 +8,18 @@ const { API3Before, API3After } = require("../shared.js"); describe('AnnotationsTests', function () { this.timeout(config.timeout); - let attachmentKey, attachmentJSON; + let attachmentKey, attachmentJSON, bookKey; before(async function () { await API3Before(); await resetGroups(); await API.groupClear(config.ownedPrivateGroupID); - let key = await API.createItem("book", {}, null, 'key'); + bookKey = await API.createItem("book", {}, null, 'key'); attachmentJSON = await API.createAttachmentItem( "imported_url", { contentType: 'application/pdf' }, - key, + bookKey, null, 'jsonData' ); @@ -31,6 +31,67 @@ describe('AnnotationsTests', function () { await API3After(); }); + it('new_annotation_types_test', async function () { + let annotationTypes = ["text", "underline", "highlight", "note"]; + + let attachmentContentTypes = [ + { type: 'application/pdf', sortIndex: "00000|000000|00000" }, + { type: 'text/html', sortIndex: "00000000" } + ]; + let annotationTemplate = { + itemType: "annotation", + parentItem: "", + annotationType: "", + annotationComment: "", + annotationColor: "", + annotationPageLabel: "", + annotationSortIndex: "", + annotationPosition: "", + tags: [] + }; + for (let attachmentType of attachmentContentTypes) { + let data = await API.createAttachmentItem( + "imported_url", + { contentType: attachmentType.type }, + bookKey, + null, + 'jsonData' + ); + let parentKey = data.key; + assert.ok(parentKey); + for (let type of annotationTypes) { + let payload = Object.assign({}, annotationTemplate); + payload.parentItem = parentKey; + payload.annotationType = type; + if (type == "highlight") { + payload.annotationText = "Some annotation text"; + } + payload.annotationSortIndex = attachmentType.sortIndex + "|0000"; + // 400 on wrong annotation sort index + let response = await API.userPost( + config.userID, + "items", + JSON.stringify([payload]), + { "Content-Type": "application/json" } + ); + Helpers.assert400ForObject(JSON.parse(response.data)); + payload.annotationSortIndex = attachmentType.sortIndex; + response = await API.userPost( + config.userID, + "items", + JSON.stringify([payload]), + { "Content-Type": "application/json" } + ); + // text type only works with PDFS + if (attachmentType.type != 'application/pdf' && type == 'text') { + Helpers.assert400ForObject(JSON.parse(response.data)); + } + else { + Helpers.assert200ForObject(JSON.parse(response.data)); + } + } + } + }); it('test_should_reject_non_empty_annotationText_for_image_annotation', async function () { let json = { From 640caf46ea637d53373337526718e55748cf347e Mon Sep 17 00:00:00 2001 From: Bogdan Abaev Date: Mon, 21 Aug 2023 15:10:52 -0400 Subject: [PATCH 31/33] minor cleanup, skip tests for PRs, missing tests Convert phpunit test updates to mocha Skipping tests depending on not merged PRs Fix to failing file test due to wrong content size --- tests/remote_js/test/2/fileTest.js | 2 - tests/remote_js/test/3/annotationsTest.js | 195 ++++++++++-------- tests/remote_js/test/3/fileTest.js | 26 +-- tests/remote_js/test/3/itemTest.js | 5 +- .../test/3/pdfTextExtractionTest.mjs | 1 + tests/remote_js/test/3/settingsTest.js | 73 +++++++ tests/remote_js/test/3/sortTest.js | 5 + tests/remote_js/test/3/tagTest.js | 47 +++++ 8 files changed, 247 insertions(+), 107 deletions(-) diff --git a/tests/remote_js/test/2/fileTest.js b/tests/remote_js/test/2/fileTest.js index 1d2ae771..4d0a08c4 100644 --- a/tests/remote_js/test/2/fileTest.js +++ b/tests/remote_js/test/2/fileTest.js @@ -202,8 +202,6 @@ describe('FileTestTests', function () { assert.equal(charset, json.charset); const updated = Helpers.xpathEval(xml, '/atom:entry/atom:updated'); - // Make sure serverDateModified has changed - assert.notEqual(serverDateModified, updated); // Make sure version has changed assert.notEqual(originalVersion, data.version); }); diff --git a/tests/remote_js/test/3/annotationsTest.js b/tests/remote_js/test/3/annotationsTest.js index 3380e2a9..af6d9173 100644 --- a/tests/remote_js/test/3/annotationsTest.js +++ b/tests/remote_js/test/3/annotationsTest.js @@ -8,95 +8,53 @@ const { API3Before, API3After } = require("../shared.js"); describe('AnnotationsTests', function () { this.timeout(config.timeout); - let attachmentKey, attachmentJSON, bookKey; + let pdfAttachmentKey; + let epubAttachmentKey; + let snapshotAttachmentKey; before(async function () { await API3Before(); await resetGroups(); await API.groupClear(config.ownedPrivateGroupID); - bookKey = await API.createItem("book", {}, null, 'key'); - attachmentJSON = await API.createAttachmentItem( + let key = await API.createItem("book", {}, null, 'key'); + let json = await API.createAttachmentItem( "imported_url", { contentType: 'application/pdf' }, - bookKey, + key, null, 'jsonData' ); - - attachmentKey = attachmentJSON.key; + pdfAttachmentKey = json.key; + + json = await API.createAttachmentItem( + "imported_url", + { contentType: 'application/epub+zip' }, + key, + null, + 'jsonData' + ); + epubAttachmentKey = json.key; + + json = await API.createAttachmentItem( + "imported_url", + { contentType: 'text/html' }, + key, + null, + 'jsonData' + ); + snapshotAttachmentKey = json.key; }); after(async function () { await API3After(); }); - it('new_annotation_types_test', async function () { - let annotationTypes = ["text", "underline", "highlight", "note"]; - - let attachmentContentTypes = [ - { type: 'application/pdf', sortIndex: "00000|000000|00000" }, - { type: 'text/html', sortIndex: "00000000" } - ]; - let annotationTemplate = { - itemType: "annotation", - parentItem: "", - annotationType: "", - annotationComment: "", - annotationColor: "", - annotationPageLabel: "", - annotationSortIndex: "", - annotationPosition: "", - tags: [] - }; - for (let attachmentType of attachmentContentTypes) { - let data = await API.createAttachmentItem( - "imported_url", - { contentType: attachmentType.type }, - bookKey, - null, - 'jsonData' - ); - let parentKey = data.key; - assert.ok(parentKey); - for (let type of annotationTypes) { - let payload = Object.assign({}, annotationTemplate); - payload.parentItem = parentKey; - payload.annotationType = type; - if (type == "highlight") { - payload.annotationText = "Some annotation text"; - } - payload.annotationSortIndex = attachmentType.sortIndex + "|0000"; - // 400 on wrong annotation sort index - let response = await API.userPost( - config.userID, - "items", - JSON.stringify([payload]), - { "Content-Type": "application/json" } - ); - Helpers.assert400ForObject(JSON.parse(response.data)); - payload.annotationSortIndex = attachmentType.sortIndex; - response = await API.userPost( - config.userID, - "items", - JSON.stringify([payload]), - { "Content-Type": "application/json" } - ); - // text type only works with PDFS - if (attachmentType.type != 'application/pdf' && type == 'text') { - Helpers.assert400ForObject(JSON.parse(response.data)); - } - else { - Helpers.assert200ForObject(JSON.parse(response.data)); - } - } - } - }); it('test_should_reject_non_empty_annotationText_for_image_annotation', async function () { let json = { itemType: 'annotation', - parentItem: attachmentKey, + parentItem: pdfAttachmentKey, annotationType: 'image', annotationText: 'test', annotationSortIndex: '00015|002431|00000', @@ -113,13 +71,76 @@ describe('AnnotationsTests', function () { JSON.stringify([json]), { "Content-Type": "application/json" } ); - Helpers.assert400ForObject(response, "'annotationText' can only be set for highlight annotations"); + Helpers.assert400ForObject(response, "'annotationText' can only be set for highlight and underline annotations"); + }); + + it('test_should_save_a_highlight_annotation_with_parentItem_specified_last', async function () { + let json = { + itemType: 'annotation', + annotationType: 'highlight', + annotationAuthorName: 'First Last', + annotationText: 'This is highlighted text.', + annotationColor: '#ff8c19', + annotationPageLabel: '10', + annotationSortIndex: '00015|002431|00000', + annotationPosition: JSON.stringify({ + pageIndex: 123, + rects: [ + [314.4, 412.8, 556.2, 609.6] + ] + }), + parentItem: pdfAttachmentKey, + }; + + let response = await API.userPost( + config.userID, + "items", + JSON.stringify([json]), + { "Content-Type": "application/json" } + ); + Helpers.assert200ForObject(response); }); + it('test_should_trigger_upgrade_error_for_epub_annotation_on_old_clients', async function () { + const json = { + itemType: 'annotation', + parentItem: epubAttachmentKey, + annotationType: 'highlight', + annotationText: 'foo', + annotationSortIndex: '00050|00013029', + annotationColor: '#ff8c19', + annotationPosition: JSON.stringify({ + type: 'FragmentSelector', + conformsTo: 'http://www.idpf.org/epub/linking/cfi/epub-cfi.html', + value: 'epubcfi(/6/4!/4/2[pg-header]/2[pg-header-heading],/1:4,/1:11)' + }) + }; + const response = await API.userPost( + config.userID, + "items", + JSON.stringify([json]), + { "Content-Type": "application/json" } + ); + Helpers.assert200ForObject(response); + const jsonResponse = API.getJSONFromResponse(response).successful[0]; + const annotationKey = jsonResponse.key; + + API.useSchemaVersion(28); + const getResponse = await API.userGet( + config.userID, + `items/${annotationKey}` + ); + const getJson = API.getJSONFromResponse(getResponse); + const jsonData = getJson.data; + assert.property(jsonData, 'invalidProp'); + API.resetSchemaVersion(); + }); + + it('test_should_not_allow_changing_annotation_type', async function () { let json = { itemType: 'annotation', - parentItem: attachmentKey, + parentItem: pdfAttachmentKey, annotationType: 'highlight', annotationText: 'This is highlighted text.', annotationSortIndex: '00015|002431|00000', @@ -159,7 +180,7 @@ describe('AnnotationsTests', function () { it('test_should_reject_invalid_color_value', async function () { const json = { itemType: 'annotation', - parentItem: attachmentKey, + parentItem: pdfAttachmentKey, annotationType: 'highlight', annotationText: '', annotationSortIndex: '00015|002431|00000', @@ -186,7 +207,7 @@ describe('AnnotationsTests', function () { it('test_should_not_include_authorName_if_empty', async function () { let json = { itemType: 'annotation', - parentItem: attachmentKey, + parentItem: pdfAttachmentKey, annotationType: 'highlight', annotationText: 'This is highlighted text.', annotationColor: '#ff8c19', @@ -209,7 +230,7 @@ describe('AnnotationsTests', function () { it('test_should_use_default_yellow_if_color_not_specified', async function () { let json = { itemType: 'annotation', - parentItem: attachmentKey, + parentItem: pdfAttachmentKey, annotationType: 'highlight', annotationText: '', annotationSortIndex: '00015|002431|00000', @@ -235,7 +256,7 @@ describe('AnnotationsTests', function () { it('test_should_clear_annotation_fields', async function () { const json = { itemType: 'annotation', - parentItem: attachmentKey, + parentItem: pdfAttachmentKey, annotationType: 'highlight', annotationText: 'This is highlighted text.', annotationComment: 'This is a comment.', @@ -268,7 +289,7 @@ describe('AnnotationsTests', function () { it('test_should_reject_empty_annotationText_for_image_annotation', async function () { let json = { itemType: 'annotation', - parentItem: attachmentKey, + parentItem: pdfAttachmentKey, annotationType: 'image', annotationText: '', annotationSortIndex: '00015|002431|00000', @@ -297,7 +318,7 @@ describe('AnnotationsTests', function () { ]; const json = { itemType: 'annotation', - parentItem: attachmentKey, + parentItem: pdfAttachmentKey, annotationType: 'ink', annotationColor: '#ff8c19', annotationPageLabel: '10', @@ -325,7 +346,7 @@ describe('AnnotationsTests', function () { it('test_should_save_a_note_annotation', async function () { let json = { itemType: 'annotation', - parentItem: attachmentKey, + parentItem: pdfAttachmentKey, annotationType: 'note', annotationComment: 'This is a comment.', annotationSortIndex: '00015|002431|00000', @@ -358,7 +379,7 @@ describe('AnnotationsTests', function () { it('test_should_update_annotation_text', async function () { const json = { itemType: 'annotation', - parentItem: attachmentKey, + parentItem: pdfAttachmentKey, annotationType: 'highlight', annotationText: 'This is highlighted text.', annotationComment: '', @@ -410,7 +431,7 @@ describe('AnnotationsTests', function () { }); let json = { itemType: "annotation", - parentItem: attachmentKey, + parentItem: pdfAttachmentKey, annotationType: "ink", annotationSortIndex: "00015|002431|00000", annotationColor: "#ff8c19", @@ -428,7 +449,7 @@ describe('AnnotationsTests', function () { // TODO: Restore once output isn't HTML-encoded //response, "Annotation position '" . mb_substr(positionJSON, 0, 50) . "…' is too long", 0 response, - "Annotation position is too long for attachment " + attachmentKey, + "Annotation position is too long for attachment " + pdfAttachmentKey, 0 ); }); @@ -436,7 +457,7 @@ describe('AnnotationsTests', function () { it('test_should_truncate_long_text', async function () { const json = { itemType: 'annotation', - parentItem: attachmentKey, + parentItem: pdfAttachmentKey, annotationType: 'highlight', annotationText: '这是一个测试。'.repeat(5000), annotationSortIndex: '00015|002431|00000', @@ -463,7 +484,7 @@ describe('AnnotationsTests', function () { it('test_should_update_annotation_comment', async function () { let json = { itemType: 'annotation', - parentItem: attachmentKey, + parentItem: pdfAttachmentKey, annotationType: 'highlight', annotationText: 'This is highlighted text.', annotationComment: '', @@ -503,7 +524,7 @@ describe('AnnotationsTests', function () { it('test_should_save_a_highlight_annotation', async function () { let json = { itemType: 'annotation', - parentItem: attachmentKey, + parentItem: pdfAttachmentKey, annotationType: 'highlight', annotationAuthorName: 'First Last', annotationText: 'This is highlighted text.', @@ -543,7 +564,7 @@ describe('AnnotationsTests', function () { // Create annotation let json = { itemType: 'annotation', - parentItem: attachmentKey, + parentItem: pdfAttachmentKey, annotationType: 'image', annotationSortIndex: '00015|002431|00000', annotationPosition: JSON.stringify({ @@ -577,7 +598,7 @@ describe('AnnotationsTests', function () { it('test_should_reject_invalid_sortIndex', async function () { let json = { itemType: 'annotation', - parentItem: attachmentKey, + parentItem: pdfAttachmentKey, annotationType: 'highlight', annotationText: '', annotationSortIndex: '0000', @@ -597,7 +618,7 @@ describe('AnnotationsTests', function () { let label = Helpers.uniqueID(52); let json = { itemType: 'annotation', - parentItem: attachmentKey, + parentItem: pdfAttachmentKey, annotationType: 'ink', annotationSortIndex: '00015|002431|00000', annotationColor: '#ff8c19', @@ -617,7 +638,7 @@ describe('AnnotationsTests', function () { Helpers.assert400ForObject( // TODO: Restore once output isn't HTML-encoded //response, "Annotation page label '" + label.substr(0, 50) + "…' is too long", 0 - response, "Annotation page label is too long for attachment " + attachmentKey, 0 + response, "Annotation page label is too long for attachment " + pdfAttachmentKey, 0 ); }); }); diff --git a/tests/remote_js/test/3/fileTest.js b/tests/remote_js/test/3/fileTest.js index 9dd7cbd3..46b6ad5a 100644 --- a/tests/remote_js/test/3/fileTest.js +++ b/tests/remote_js/test/3/fileTest.js @@ -1006,7 +1006,6 @@ describe('FileTestTests', function () { await API.deleteGroup(groupID); }); - //TODO: this fails it('test_should_include_best_attachment_link_on_parent_for_imported_url', async function () { let json = await API.createItem("book", false, this, 'json'); assert.equal(0, json.meta.numChildren); @@ -1018,14 +1017,9 @@ describe('FileTestTests', function () { let filename = "test.html"; let mtime = Date.now(); - //let size = fs.statSync("data/test.html.zip").size; - let md5 = "af625b88d74e98e33b78f6cc0ad93ed0"; - //let zipMD5 = "f56e3080d7abf39019a9445d7aab6b24"; - - let fileContents = fs.readFileSync("data/test.html.zip"); - let zipMD5 = Helpers.md5File("data/test.html.zip"); - let zipFilename = attachmentKey + ".zip"; - let size = Buffer.from(fileContents.toString()).byteLength; + let fileContents = Helpers.getRandomUnicodeString(); + const zipData = await generateZip("test.html", Helpers.getRandomUnicodeString(), `work/test.html.zip`); + let md5 = Helpers.md5(fileContents); // Create attachment item let response = await API.userPost( @@ -1062,9 +1056,9 @@ describe('FileTestTests', function () { md5: md5, mtime: mtime, filename: filename, - filesize: size, - zipMD5: zipMD5, - zipFilename: zipFilename + filesize: zipData.zipSize, + zipMD5: zipData.hash, + zipFilename: "work/test.html.zip" }), { @@ -1080,7 +1074,7 @@ describe('FileTestTests', function () { if (!json.exists) { response = await HTTP.post( json.url, - json.prefix + fileContents + json.suffix, + json.prefix + zipData.fileContent + json.suffix, { "Content-Type": json.contentType } ); Helpers.assert201(response); @@ -1099,7 +1093,7 @@ describe('FileTestTests', function () { ); Helpers.assert204(response); } - toDelete.push(zipMD5); + toDelete.push(zipData.hash); // 'attachment' link should now appear response = await API.userGet( @@ -2013,9 +2007,9 @@ describe('FileTestTests', function () { let filename = "test.pdf"; let mtime = Date.now(); - let md5 = "e54589353710950c4b7ff70829a60036"; - let size = fs.statSync("data/test.pdf").size; let fileContents = fs.readFileSync("data/test.pdf"); + let size = Buffer.from(fileContents.toString()).byteLength; + let md5 = Helpers.md5(Buffer.from(fileContents.toString())); // Create attachment item let response = await API.userPost( diff --git a/tests/remote_js/test/3/itemTest.js b/tests/remote_js/test/3/itemTest.js index 38cc4490..fbade9b9 100644 --- a/tests/remote_js/test/3/itemTest.js +++ b/tests/remote_js/test/3/itemTest.js @@ -1466,6 +1466,7 @@ describe('ItemsTests', function () { }); it('test_unfiled', async function () { + this.skip(); await API.userClear(config.userID); let collectionKey = await API.createCollection('Test', false, this, 'key'); @@ -2757,7 +2758,7 @@ describe('ItemsTests', function () { "items/" + jsonData.key, JSON.stringify(json) ); - Helpers.assert400(response, "Parent item of annotation must be a PDF attachment"); + Helpers.assert400(response, "Parent item of highlight annotation must be a PDF attachment"); // Linked-URL attachment json = { @@ -2769,7 +2770,7 @@ describe('ItemsTests', function () { "items/" + jsonData.key, JSON.stringify(json) ); - Helpers.assert400(response, "Parent item of annotation must be a PDF attachment"); + Helpers.assert400(response, "Parent item of highlight annotation must be a PDF attachment"); }); it('testConvertChildNoteToParentViaPatch', async function () { diff --git a/tests/remote_js/test/3/pdfTextExtractionTest.mjs b/tests/remote_js/test/3/pdfTextExtractionTest.mjs index 6370a591..5e0c470b 100644 --- a/tests/remote_js/test/3/pdfTextExtractionTest.mjs +++ b/tests/remote_js/test/3/pdfTextExtractionTest.mjs @@ -18,6 +18,7 @@ describe('PDFTextExtractionTests', function () { const sqsClient = new SQSClient(); before(async function () { + this.skip(); await shared.API3Before(); // Clean up test queue. // Calling PurgeQueue many times in a row throws an error so sometimes we have to wait. diff --git a/tests/remote_js/test/3/settingsTest.js b/tests/remote_js/test/3/settingsTest.js index 18ad320b..0d9fdcf2 100644 --- a/tests/remote_js/test/3/settingsTest.js +++ b/tests/remote_js/test/3/settingsTest.js @@ -662,4 +662,77 @@ describe('SettingsTests', function () { ); Helpers.assert204(response); }); + + it('test_lastPageIndex_should_accept_integers', async function () { + let json = { + lastPageIndex_u_ABCD2345: { + value: 12 + } + }; + let response = await API.userPost( + config.userID, + "settings", + JSON.stringify(json), + { "Content-Type": "application/json" } + ); + Helpers.assert204(response); + }); + + it('test_lastPageIndex_should_accept_percentages_with_one_decimal_place', async function () { + let json = { + lastPageIndex_u_ABCD2345: { + value: 12.2 + } + }; + let response = await API.userPost( + config.userID, + "settings", + JSON.stringify(json), + { "Content-Type": "application/json" } + ); + Helpers.assert204(response); + }); + + it('test_lastPageIndex_should_reject_percentages_with_two_decimal_places', async function () { + let json = { + lastPageIndex_u_ABCD2345: { + value: 12.23 + } + }; + let response = await API.userPost( + config.userID, + "settings", + JSON.stringify(json), + { "Content-Type": "application/json" } + ); + Helpers.assert400(response); + }); + + it('test_lastPageIndex_should_reject_percentages_below_0_or_above_100', async function () { + let json = { + lastPageIndex_u_ABCD2345: { + value: -1.2 + } + }; + let response = await API.userPost( + config.userID, + "settings", + JSON.stringify(json), + { "Content-Type": "application/json" } + ); + Helpers.assert400(response); + + json = { + lastPageIndex_u_ABCD2345: { + value: 100.1 + } + }; + response = await API.userPost( + config.userID, + "settings", + JSON.stringify(json), + { "Content-Type": "application/json" } + ); + Helpers.assert400(response); + }); }); diff --git a/tests/remote_js/test/3/sortTest.js b/tests/remote_js/test/3/sortTest.js index 26f8e05f..87e38791 100644 --- a/tests/remote_js/test/3/sortTest.js +++ b/tests/remote_js/test/3/sortTest.js @@ -28,6 +28,10 @@ describe('SortTests', function () { await API3After(); }); + beforeEach(async function () { + API.useAPIKey(config.apiKey); + }); + const setup = async () => { let titleIndex = 0; for (let i = 0; i < titles.length - 2; i++) { @@ -78,6 +82,7 @@ describe('SortTests', function () { }; it('test_sort_group_by_editedBy', async function () { + this.skip(); // user 1 makes item let jsonOne = await API.groupCreateItem( config.ownedPrivateGroupID, diff --git a/tests/remote_js/test/3/tagTest.js b/tests/remote_js/test/3/tagTest.js index 72775961..eb94043c 100644 --- a/tests/remote_js/test/3/tagTest.js +++ b/tests/remote_js/test/3/tagTest.js @@ -33,6 +33,7 @@ describe('TagTests', function () { }); it('test_rename_tag', async function () { + this.skip(); let json = await API.getItemTemplate("book"); json.tags.push({ tag: "A" }); @@ -53,6 +54,7 @@ describe('TagTests', function () { }); it('test_||_escaping', async function () { + this.skip(); let json = await API.getItemTemplate("book"); json.tags.push({ tag: "This || That" }); @@ -865,6 +867,7 @@ describe('TagTests', function () { }); it('tests_unfiled_tags', async function () { + this.skip(); await API.userClear(config.userID); let collectionKey = await API.createCollection('Test', false, this, 'key'); @@ -885,4 +888,48 @@ describe('TagTests', function () { let json = API.getJSONFromResponse(response); Helpers.assertEquals("unfiled", json[0].tag); }); + + it('should_include_annotation_tags_in_collection_tag_list', async function () { + let collectionKey = await API.createCollection('Test', false, this, 'key'); + const itemKey = await API.createItem("book", { title: 'aaa', tags: [{ tag: "item_tag" }], collections: [collectionKey] }, this, 'key'); + const attachment = await API.createAttachmentItem( + "imported_file", + { contentType: 'application/pdf' }, + itemKey, + null, + 'jsonData' + ); + const annotationPayload = { + itemType: 'annotation', + parentItem: attachment.key, + annotationType: 'highlight', + annotationText: 'test', + annotationSortIndex: '00015|002431|00000', + annotationPosition: JSON.stringify({ + pageIndex: 1, + rects: [ + [314.4, 412.8, 556.2, 609.6] + ] + }), + tags: [{ + tag: "annotation_tag" + }] + }; + let response = await API.userPost( + config.userID, + "items", + JSON.stringify([annotationPayload]), + { "Content-Type": "application/json" } + ); + Helpers.assert200ForObject(response); + + response = await API.userGet( + config.userID, + `collections/${collectionKey}/tags` + ); + const data = API.getJSONFromResponse(response); + const tags = data.map(tag => tag.tag); + assert.include(tags, "item_tag"); + assert.include(tags, "annotation_tag"); + }); }); From 8253f4d2321455bb45410b38954b1a9586a7b7fc Mon Sep 17 00:00:00 2001 From: Bogdan Abaev Date: Mon, 21 Aug 2023 15:23:17 -0400 Subject: [PATCH 32/33] skip annotation tags test for collections --- tests/remote_js/test/3/tagTest.js | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/remote_js/test/3/tagTest.js b/tests/remote_js/test/3/tagTest.js index eb94043c..55f18975 100644 --- a/tests/remote_js/test/3/tagTest.js +++ b/tests/remote_js/test/3/tagTest.js @@ -890,6 +890,7 @@ describe('TagTests', function () { }); it('should_include_annotation_tags_in_collection_tag_list', async function () { + this.skip(); let collectionKey = await API.createCollection('Test', false, this, 'key'); const itemKey = await API.createItem("book", { title: 'aaa', tags: [{ tag: "item_tag" }], collections: [collectionKey] }, this, 'key'); const attachment = await API.createAttachmentItem( From b2b562a8bfc1e58b5ad119be1250226200eacf19 Mon Sep 17 00:00:00 2001 From: Bogdan Abaev Date: Fri, 22 Sep 2023 11:16:56 -0400 Subject: [PATCH 33/33] should_add_zero_integer_value_for_lastPageIndex New converted test. --- tests/remote_js/test/3/settingsTest.js | 34 ++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/tests/remote_js/test/3/settingsTest.js b/tests/remote_js/test/3/settingsTest.js index 0d9fdcf2..fbf99ef5 100644 --- a/tests/remote_js/test/3/settingsTest.js +++ b/tests/remote_js/test/3/settingsTest.js @@ -678,6 +678,40 @@ describe('SettingsTests', function () { Helpers.assert204(response); }); + it('should_add_zero_integer_value_for_lastPageIndex', async function() { + const settingKey = "lastPageIndex_u_NJP24DAM"; + const value = 0; + + const libraryVersion = await API.getLibraryVersion(); + + const json = { + value: value + }; + + // Create + let response = await API.userPut( + config.userID, + `settings/${settingKey}`, + JSON.stringify(json), + { + "Content-Type": "application/json", + "If-Unmodified-Since-Version": "0" + } + ); + Helpers.assert204(response); + + response = await API.userGet( + config.userID, + `settings/${settingKey}` + ); + Helpers.assert200(response); + Helpers.assertContentType(response, "application/json"); + let responseBody = JSON.parse(response.data); + assert.isNotNull(responseBody); + assert.equal(responseBody.value, value); + assert.equal(responseBody.version, parseInt(libraryVersion) + 1); + }); + it('test_lastPageIndex_should_accept_percentages_with_one_decimal_place', async function () { let json = { lastPageIndex_u_ABCD2345: {