This commit is contained in:
Zhang Hongyu 2026-08-27 10:38:57 +08:00 committed by GitHub
commit ddcc5e075f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 852 additions and 0 deletions

View file

@ -289,6 +289,276 @@ Zotero.Server.Connector.Detect.prototype = {
},
}
Zotero.Server.Connector.FindExistingItems = function () {};
Zotero.Server.Endpoints["/connector/findExistingItems"] = Zotero.Server.Connector.FindExistingItems;
Zotero.Server.Connector.FindExistingItems.prototype = {
supportedMethods: ["POST"],
supportedDataTypes: ["application/json"],
permitBookmarklet: true,
init: async function (requestData) {
let data = requestData.data || {};
let identifiers = Zotero.Server.Connector._getItemIdentifiers(data);
if (!identifiers.doi.length && !identifiers.url.length) {
return [400, "application/json", JSON.stringify({ error: "IDENTIFIERS_NOT_PROVIDED" })];
}
let target;
if (data.target) {
try {
target = Zotero.Server.Connector.resolveTarget(data.target);
}
catch (e) {
Zotero.debug(`Could not resolve duplicate lookup target '${data.target}': ${e.message}`);
}
}
let { library } = target && target.library
? target
: Zotero.Server.Connector.getSaveTarget();
let matches = await Zotero.Server.Connector.findExistingItemsByIdentifiers(
identifiers,
library.libraryID
);
return [200, "application/json", JSON.stringify({ matches })];
}
};
Zotero.Server.Connector._getItemIdentifiers = function (data) {
let identifiers = {
doi: new Set(),
url: new Set()
};
let itemIdentifiers = [];
let proxy = data.proxy && new Zotero.Proxy(data.proxy);
let addDOI = (doi) => {
doi = doi && Zotero.Utilities.cleanDOI(doi);
if (doi) {
doi = doi.toLowerCase();
identifiers.doi.add(doi);
return doi;
}
};
let addURL = (url) => {
if (proxy && url) {
try {
// Only deproxify when the proxy regexp actually matches.
// toProper() normalizes non-matching URLs through new URL().href,
// which can break exact-match lookups against values stored from
// the translator (e.g. https://example.com vs https://example.com/).
let normalized = new URL(url).href;
if (proxy.regexp.test(normalized)) {
url = proxy.toProper(normalized);
}
}
catch (e) {
Zotero.debug(`Could not deproxify item URL for duplicate lookup: ${e.message}`);
return;
}
}
if (url && typeof url == 'string') {
identifiers.url.add(url);
return url;
}
};
if (data.identifiers) {
for (let doi of data.identifiers.doi || []) {
addDOI(doi);
}
for (let url of data.identifiers.url || []) {
addURL(url);
}
}
for (let item of data.items || []) {
let itemIdentifier = {
doi: new Set(),
url: new Set()
};
let addItemDOI = (doi) => {
doi = addDOI(doi);
if (doi) {
itemIdentifier.doi.add(doi);
}
};
let addItemURL = (url) => {
url = addURL(url);
if (url) {
itemIdentifier.url.add(url);
}
};
addItemDOI(item.DOI);
if (item.extra) {
let { fields } = Zotero.Utilities.Internal.extractExtraFields(item.extra);
addItemDOI(fields.get('DOI'));
}
addItemURL(item.url);
itemIdentifiers.push({
doi: Array.from(itemIdentifier.doi),
url: Array.from(itemIdentifier.url)
});
}
return {
doi: Array.from(identifiers.doi),
url: Array.from(identifiers.url),
itemIdentifiers
};
};
Zotero.Server.Connector.findExistingItemsByIdentifiers = async function (identifiers, libraryID) {
let itemIDs = new Set();
let doiSet = new Set(identifiers.doi);
let urlSet = new Set(identifiers.url);
let requestItemIdentifiers = identifiers.itemIdentifiers || [];
let matches = [];
let getMatchedItemIndexes = function (matchedDOIs, matchedURL) {
if (!requestItemIdentifiers.length) {
return [];
}
let indexes = [];
for (let i = 0; i < requestItemIdentifiers.length; i++) {
let itemIdentifiers = requestItemIdentifiers[i];
if (matchedDOIs.some(doi => itemIdentifiers.doi.includes(doi))) {
indexes.push(i);
continue;
}
if (matchedURL && itemIdentifiers.url.includes(matchedURL)) {
indexes.push(i);
}
}
return indexes;
};
if (doiSet.size) {
let doiCandidateChunkSize = Math.min(100, Zotero.DB.MAX_BOUND_PARAMETERS - 2);
let getDOICandidateRows = async function (fieldID) {
let allRows = [];
await Zotero.Utilities.Internal.forEachChunkAsync(
identifiers.doi,
doiCandidateChunkSize,
async function (chunk) {
let rows = await Zotero.DB.queryAsync(
"SELECT itemID, value FROM items JOIN itemData USING (itemID) "
+ "JOIN itemDataValues USING (valueID) "
+ "WHERE libraryID=? AND fieldID=? "
+ "AND (" + chunk.map(() => "LOWER(value) LIKE ?").join(" OR ") + ") "
+ "AND itemID NOT IN (SELECT itemID FROM deletedItems)",
[
libraryID,
fieldID,
...chunk.map(doi => `%${doi}%`)
]
);
allRows.push(...rows);
}
);
return allRows;
};
let doiRows = await getDOICandidateRows(Zotero.ItemFields.getID('DOI'));
for (let row of doiRows) {
let doi = Zotero.Utilities.cleanDOI(row.value);
if (doi && doiSet.has(doi.toLowerCase())) {
itemIDs.add(row.itemID);
}
}
let extraRows = await getDOICandidateRows(Zotero.ItemFields.getID('extra'));
for (let row of extraRows) {
let { fields } = Zotero.Utilities.Internal.extractExtraFields(row.value);
let extraDOI = fields.get('DOI');
let doi = extraDOI && Zotero.Utilities.cleanDOI(extraDOI);
if (doi && doiSet.has(doi.toLowerCase())) {
itemIDs.add(row.itemID);
}
}
}
if (urlSet.size) {
await Zotero.Utilities.Internal.forEachChunkAsync(
identifiers.url,
Zotero.DB.MAX_BOUND_PARAMETERS - 2,
async function (chunk) {
let rows = await Zotero.DB.queryAsync(
"SELECT itemID, value FROM items JOIN itemData USING (itemID) "
+ "JOIN itemDataValues USING (valueID) "
+ "WHERE libraryID=? AND fieldID=? "
+ "AND value IN (" + chunk.map(() => '?').join(',') + ") "
+ "AND itemID NOT IN (SELECT itemID FROM deletedItems)",
[
libraryID,
Zotero.ItemFields.getID('url'),
...chunk
]
);
for (let row of rows) {
if (urlSet.has(row.value)) {
itemIDs.add(row.itemID);
}
}
}
);
}
for (let itemID of itemIDs) {
let item = await Zotero.Items.getAsync(itemID);
if (!item || !item.isTopLevelItem()) {
continue;
}
await item.loadDataType('itemData');
let matchedFields = [];
let matchedIdentifiers = {};
let itemDOIs = [
item.getField('DOI'),
item.getExtraField('DOI')
]
.map(doi => Zotero.Utilities.cleanDOI(doi))
.filter(doi => doi && doiSet.has(doi.toLowerCase()));
let matchedDOIs = Array.from(new Set(itemDOIs.map(doi => doi.toLowerCase())));
if (itemDOIs.length) {
matchedFields.push('DOI');
matchedIdentifiers.doi = itemDOIs[0];
}
let itemURL = item.getField('url');
if (itemURL && urlSet.has(itemURL)) {
matchedFields.push('url');
matchedIdentifiers.url = itemURL;
}
if (!matchedFields.length) {
continue;
}
let match = {
id: item.id,
key: item.key,
libraryID: item.libraryID,
title: item.getDisplayTitle(),
matchedFields,
matchedIdentifiers
};
let matchedItemIndexes = getMatchedItemIndexes(matchedDOIs, matchedIdentifiers.url);
if (matchedItemIndexes.length) {
// Keep the singular field aligned with the identifiers older
// Connector versions receive.
let compatibilityDOIs = matchedIdentifiers.doi
? [matchedIdentifiers.doi.toLowerCase()]
: [];
let matchedItemIndex = getMatchedItemIndexes(
compatibilityDOIs,
matchedIdentifiers.url
)[0];
if (matchedItemIndex !== undefined) {
match.matchedItemIndex = matchedItemIndex;
}
match.matchedItemIndexes = matchedItemIndexes;
}
matches.push(match);
}
return matches;
};
/**
* Saves items to DB
*

View file

@ -320,6 +320,588 @@ describe("Connector Server", function () {
});
});
describe("/connector/findExistingItems", function () {
it("should return matching top-level items for DOI and URL in the selected target library", async function () {
await selectLibrary(win, Zotero.Libraries.userLibraryID);
await waitForItemsLoad(win);
let item = new Zotero.Item("journalArticle");
item.setField("title", "Existing Article");
item.setField("DOI", "10.1234/Example.1");
item.setField("url", "https://example.com/article");
await item.saveTx();
Zotero.Items.unload(item.id);
let response = await httpRequest(
"POST",
connectorServerPath + "/connector/findExistingItems",
{
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
identifiers: {
doi: ["10.1234/example.1"],
url: ["https://example.com/article"]
}
})
}
);
let data = JSON.parse(response.response);
assert.lengthOf(data.matches, 1);
assert.equal(data.matches[0].id, item.id);
assert.equal(data.matches[0].libraryID, Zotero.Libraries.userLibraryID);
assert.sameMembers(data.matches[0].matchedFields, ["DOI", "url"]);
});
it("should use the requested target library for duplicate lookup", async function () {
await selectLibrary(win, Zotero.Libraries.userLibraryID);
await waitForItemsLoad(win);
let userItem = new Zotero.Item("journalArticle");
userItem.setField("title", "Existing User Article");
userItem.setField("DOI", "10.1234/targeted");
await userItem.saveTx();
let group = await createGroup({ editable: true });
let groupItem = new Zotero.Item("journalArticle");
groupItem.libraryID = group.libraryID;
groupItem.setField("title", "Existing Group Article");
groupItem.setField("DOI", "10.1234/targeted");
await groupItem.saveTx();
Zotero.Items.unload([userItem.id, groupItem.id]);
let response = await httpRequest(
"POST",
connectorServerPath + "/connector/findExistingItems",
{
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
target: group.treeViewID,
identifiers: {
doi: ["10.1234/targeted"]
}
})
}
);
let data = JSON.parse(response.response);
assert.lengthOf(data.matches, 1);
assert.equal(data.matches[0].id, groupItem.id);
assert.equal(data.matches[0].libraryID, group.libraryID);
});
it("should deproxify item URLs before duplicate lookup", async function () {
await selectLibrary(win, Zotero.Libraries.userLibraryID);
await waitForItemsLoad(win);
let item = new Zotero.Item("webpage");
item.setField("title", "Existing Proxied Page");
item.setField("url", "https://duplicate.example.com/path");
await item.saveTx();
Zotero.Items.unload(item.id);
let response = await httpRequest(
"POST",
connectorServerPath + "/connector/findExistingItems",
{
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
proxy: {
scheme: "https://%h.proxy.example.com/%p"
},
items: [{
itemType: "webpage",
title: "New Proxied Page",
url: "https://duplicate-example-com.proxy.example.com/path"
}]
})
}
);
let data = JSON.parse(response.response);
assert.lengthOf(data.matches, 1);
assert.equal(data.matches[0].id, item.id);
assert.equal(data.matches[0].matchedIdentifiers.url, "https://duplicate.example.com/path");
});
it("should chunk large URL duplicate lookups", async function () {
await selectLibrary(win, Zotero.Libraries.userLibraryID);
await waitForItemsLoad(win);
let item = new Zotero.Item("webpage");
item.setField("title", "Existing Large Lookup Page");
item.setField("url", "https://example.com/target-large-lookup");
await item.saveTx();
Zotero.Items.unload(item.id);
let urls = [];
for (let i = 0; i < Zotero.DB.MAX_BOUND_PARAMETERS + 5; i++) {
urls.push(`https://example.com/not-${i}`);
}
urls.push("https://example.com/target-large-lookup");
let response = await httpRequest(
"POST",
connectorServerPath + "/connector/findExistingItems",
{
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
identifiers: { url: urls }
})
}
);
let data = JSON.parse(response.response);
assert.lengthOf(data.matches, 1);
assert.equal(data.matches[0].id, item.id);
});
it("should chunk large DOI duplicate lookups", async function () {
await selectLibrary(win, Zotero.Libraries.userLibraryID);
await waitForItemsLoad(win);
let item = new Zotero.Item("journalArticle");
item.setField("title", "Existing Large DOI Lookup Article");
item.setField("DOI", "10.1234/target-large-doi-lookup");
await item.saveTx();
Zotero.Items.unload(item.id);
let dois = [];
for (let i = 0; i < Zotero.DB.MAX_BOUND_PARAMETERS + 5; i++) {
dois.push(`10.1234/not-large-doi-${i}`);
}
dois.push("10.1234/target-large-doi-lookup");
let response = await httpRequest(
"POST",
connectorServerPath + "/connector/findExistingItems",
{
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
identifiers: { doi: dois }
})
}
);
let data = JSON.parse(response.response);
assert.lengthOf(data.matches, 1);
assert.equal(data.matches[0].id, item.id);
});
it("should extract DOI from translated item Extra before duplicate lookup", async function () {
await selectLibrary(win, Zotero.Libraries.userLibraryID);
await waitForItemsLoad(win);
let item = new Zotero.Item("journalArticle");
item.setField("title", "Existing Extra DOI Article");
item.setField("DOI", "10.1234/extra-doi");
await item.saveTx();
Zotero.Items.unload(item.id);
let response = await httpRequest(
"POST",
connectorServerPath + "/connector/findExistingItems",
{
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
items: [{
itemType: "journalArticle",
title: "New Extra DOI Article",
extra: "DOI: 10.1234/extra-doi"
}]
})
}
);
let data = JSON.parse(response.response);
assert.lengthOf(data.matches, 1);
assert.equal(data.matches[0].id, item.id);
assert.equal(data.matches[0].matchedItemIndex, 0);
assert.equal(data.matches[0].matchedIdentifiers.doi, "10.1234/extra-doi");
});
it("should return every translated item index sharing an Extra DOI match", async function () {
await selectLibrary(win, Zotero.Libraries.userLibraryID);
await waitForItemsLoad(win);
let item = new Zotero.Item("journalArticle");
item.setField("title", "Existing Shared Extra DOI Article");
item.setField("DOI", "10.1234/shared-extra-doi");
await item.saveTx();
Zotero.Items.unload(item.id);
let response = await httpRequest(
"POST",
connectorServerPath + "/connector/findExistingItems",
{
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
items: [
{
itemType: "webpage",
title: "First Shared Extra DOI Page",
extra: "DOI: 10.1234/shared-extra-doi"
},
{
itemType: "webpage",
title: "Second Shared Extra DOI Page",
extra: "DOI: 10.1234/shared-extra-doi"
}
]
})
}
);
let data = JSON.parse(response.response);
assert.lengthOf(data.matches, 1);
assert.equal(data.matches[0].matchedItemIndex, 0);
assert.deepEqual(data.matches[0].matchedItemIndexes, [0, 1]);
});
it("should return item indexes for every matching DOI stored on one item", async function () {
await selectLibrary(win, Zotero.Libraries.userLibraryID);
await waitForItemsLoad(win);
let item = new Zotero.Item("journalArticle");
item.setField("title", "Existing Article with Two DOI Values");
item.setField("DOI", "10.1234/primary-doi");
item.setField("extra", "DOI: 10.1234/extra-doi");
await item.saveTx();
Zotero.Items.unload(item.id);
let response = await httpRequest(
"POST",
connectorServerPath + "/connector/findExistingItems",
{
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
items: [
{
itemType: "webpage",
title: "Translated Extra DOI Page",
extra: "DOI: 10.1234/extra-doi"
},
{
itemType: "journalArticle",
title: "Translated Primary DOI Article",
DOI: "10.1234/primary-doi"
}
]
})
}
);
let data = JSON.parse(response.response);
assert.lengthOf(data.matches, 1);
assert.equal(data.matches[0].id, item.id);
assert.equal(data.matches[0].matchedIdentifiers.doi, "10.1234/primary-doi");
assert.equal(data.matches[0].matchedItemIndex, 1);
assert.deepEqual(data.matches[0].matchedItemIndexes, [0, 1]);
});
it("should match saved DOI values with resolver prefixes", async function () {
await selectLibrary(win, Zotero.Libraries.userLibraryID);
await waitForItemsLoad(win);
let item = new Zotero.Item("journalArticle");
item.setField("title", "Existing Prefixed DOI Article");
item.setField("DOI", "https://doi.org/10.1234/prefixed-doi");
await item.saveTx();
Zotero.Items.unload(item.id);
let response = await httpRequest(
"POST",
connectorServerPath + "/connector/findExistingItems",
{
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
identifiers: {
doi: ["10.1234/prefixed-doi"]
}
})
}
);
let data = JSON.parse(response.response);
assert.lengthOf(data.matches, 1);
assert.equal(data.matches[0].id, item.id);
assert.equal(data.matches[0].matchedIdentifiers.doi, "10.1234/prefixed-doi");
});
it("should match existing item DOI values stored in Extra", async function () {
await selectLibrary(win, Zotero.Libraries.userLibraryID);
await waitForItemsLoad(win);
let item = new Zotero.Item("webpage");
item.setField("title", "Existing Extra DOI Page");
item.setField("extra", "DOI: 10.1234/existing-extra-doi");
await item.saveTx();
Zotero.Items.unload(item.id);
let response = await httpRequest(
"POST",
connectorServerPath + "/connector/findExistingItems",
{
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
identifiers: {
doi: ["10.1234/existing-extra-doi"]
}
})
}
);
let data = JSON.parse(response.response);
assert.lengthOf(data.matches, 1);
assert.equal(data.matches[0].id, item.id);
assert.equal(data.matches[0].matchedIdentifiers.doi, "10.1234/existing-extra-doi");
});
it("should filter DOI lookup candidates before parsing values", async function () {
await selectLibrary(win, Zotero.Libraries.userLibraryID);
await waitForItemsLoad(win);
let unrelatedDOIItem = new Zotero.Item("journalArticle");
unrelatedDOIItem.setField("title", "Unrelated DOI Article");
unrelatedDOIItem.setField("DOI", "10.9999/unrelated-doi-candidate");
await unrelatedDOIItem.saveTx();
let unrelatedExtraItem = new Zotero.Item("webpage");
unrelatedExtraItem.setField("title", "Unrelated Extra Page");
unrelatedExtraItem.setField("extra", "UNRELATED_EXTRA_SHOULD_NOT_PARSE");
await unrelatedExtraItem.saveTx();
let doiItem = new Zotero.Item("journalArticle");
doiItem.setField("title", "Existing Filtered DOI Article");
doiItem.setField("DOI", "https://doi.org/10.1234/filtered-doi-candidate");
await doiItem.saveTx();
let extraItem = new Zotero.Item("webpage");
extraItem.setField("title", "Existing Filtered Extra Page");
extraItem.setField("extra", "DOI: 10.1234/filtered-extra-candidate");
await extraItem.saveTx();
Zotero.Items.unload([unrelatedDOIItem.id, unrelatedExtraItem.id, doiItem.id, extraItem.id]);
let originalCleanDOI = Zotero.Utilities.cleanDOI;
let originalExtractExtraFields = Zotero.Utilities.Internal.extractExtraFields;
let cleanedUnrelatedDOI = false;
let parsedUnrelatedExtra = false;
sinon.stub(Zotero.Utilities, "cleanDOI").callsFake(function (doi) {
if (doi && doi.includes("unrelated-doi-candidate")) {
cleanedUnrelatedDOI = true;
}
return originalCleanDOI.apply(this, arguments);
});
sinon.stub(Zotero.Utilities.Internal, "extractExtraFields").callsFake(function (extra) {
if (extra && extra.includes("UNRELATED_EXTRA_SHOULD_NOT_PARSE")) {
parsedUnrelatedExtra = true;
}
return originalExtractExtraFields.apply(this, arguments);
});
try {
let response = await httpRequest(
"POST",
connectorServerPath + "/connector/findExistingItems",
{
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
identifiers: {
doi: [
"10.1234/filtered-doi-candidate",
"10.1234/filtered-extra-candidate"
]
}
})
}
);
let data = JSON.parse(response.response);
assert.sameMembers(data.matches.map(match => match.id), [doiItem.id, extraItem.id]);
assert.isFalse(cleanedUnrelatedDOI);
assert.isFalse(parsedUnrelatedExtra);
}
finally {
Zotero.Utilities.cleanDOI.restore();
Zotero.Utilities.Internal.extractExtraFields.restore();
}
});
it("should ignore free-text Extra DOI candidates without failing the lookup", async function () {
await selectLibrary(win, Zotero.Libraries.userLibraryID);
await waitForItemsLoad(win);
let freeTextExtraItem = new Zotero.Item("webpage");
freeTextExtraItem.setField("title", "Free Text Extra Page");
freeTextExtraItem.setField("extra", "See https://doi.org/10.1234/free-text-extra for related work");
await freeTextExtraItem.saveTx();
let doiItem = new Zotero.Item("journalArticle");
doiItem.setField("title", "Existing DOI Article");
doiItem.setField("DOI", "10.1234/free-text-extra");
await doiItem.saveTx();
Zotero.Items.unload([freeTextExtraItem.id, doiItem.id]);
let response = await httpRequest(
"POST",
connectorServerPath + "/connector/findExistingItems",
{
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
identifiers: {
doi: ["10.1234/free-text-extra"]
}
})
}
);
let data = JSON.parse(response.response);
assert.sameMembers(data.matches.map(match => match.id), [doiItem.id]);
});
it("should keep DOI lookup working when proxied item URL normalization fails", async function () {
await selectLibrary(win, Zotero.Libraries.userLibraryID);
await waitForItemsLoad(win);
let item = new Zotero.Item("journalArticle");
item.setField("title", "Existing DOI With Bad URL Payload");
item.setField("DOI", "10.1234/bad-proxy-url");
await item.saveTx();
Zotero.Items.unload(item.id);
let response = await httpRequest(
"POST",
connectorServerPath + "/connector/findExistingItems",
{
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
proxy: {
scheme: "https://%h.proxy.example.com/%p"
},
items: [{
itemType: "journalArticle",
title: "New DOI With Bad URL Payload",
DOI: "10.1234/bad-proxy-url",
url: "/relative/path"
}]
})
}
);
let data = JSON.parse(response.response);
assert.lengthOf(data.matches, 1);
assert.equal(data.matches[0].id, item.id);
});
it("should fall back to the current save target when requested collection no longer exists", async function () {
await selectLibrary(win, Zotero.Libraries.userLibraryID);
await waitForItemsLoad(win);
let collection = await createDataObject('collection');
let targetID = collection.treeViewID;
await collection.eraseTx();
let item = new Zotero.Item("journalArticle");
item.setField("title", "Existing User Library Article");
item.setField("DOI", "10.1234/deleted-target");
await item.saveTx();
Zotero.Items.unload(item.id);
let response = await httpRequest(
"POST",
connectorServerPath + "/connector/findExistingItems",
{
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
target: targetID,
identifiers: {
doi: ["10.1234/deleted-target"]
}
})
}
);
let data = JSON.parse(response.response);
assert.lengthOf(data.matches, 1);
assert.equal(data.matches[0].id, item.id);
assert.equal(data.matches[0].libraryID, Zotero.Libraries.userLibraryID);
});
it("should return display titles for base-mapped title fields", async function () {
await selectLibrary(win, Zotero.Libraries.userLibraryID);
await waitForItemsLoad(win);
let item = new Zotero.Item("case");
item.setField("caseName", "Existing Case Name");
item.setField("url", "https://example.com/case");
await item.saveTx();
Zotero.Items.unload(item.id);
let response = await httpRequest(
"POST",
connectorServerPath + "/connector/findExistingItems",
{
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
identifiers: {
url: ["https://example.com/case"]
}
})
}
);
let data = JSON.parse(response.response);
assert.lengthOf(data.matches, 1);
assert.equal(data.matches[0].id, item.id);
assert.equal(data.matches[0].title, "Existing Case Name");
});
it("should reject requests without identifiers", async function () {
let error = await getPromiseError(httpRequest(
"POST",
connectorServerPath + "/connector/findExistingItems",
{
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({})
}
));
assert.instanceOf(error, Zotero.HTTP.UnexpectedStatusException);
assert.include(error.message, "400");
});
});
describe("/connector/saveSingleFile", function () {
it("should save a webpage item with /saveSnapshot", async function () {
var collection = await createDataObject('collection');