mirror of
https://github.com/zotero/zotero.git
synced 2026-08-28 05:25:31 +00:00
290 lines
9.3 KiB
JavaScript
290 lines
9.3 KiB
JavaScript
/*
|
|
***** BEGIN LICENSE BLOCK *****
|
|
|
|
Copyright © 2026 Corporation for Digital Scholarship
|
|
Vienna, Virginia, USA
|
|
http://zotero.org
|
|
|
|
This file is part of Zotero.
|
|
|
|
Zotero is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU Affero General Public License as published by
|
|
the Free Software Foundation, either version 3 of the License, or
|
|
(at your option) any later version.
|
|
|
|
Zotero is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU Affero General Public License for more details.
|
|
|
|
You should have received a copy of the GNU Affero General Public License
|
|
along with Zotero. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
***** END LICENSE BLOCK *****
|
|
*/
|
|
|
|
describe("Zotero.BrowserRequest", function () {
|
|
describe("#downloadPDF()", function () {
|
|
var httpd, port, baseURL;
|
|
var tmpFile = Zotero.getTempDirectory();
|
|
tmpFile.append('browserRequestTest.pdf');
|
|
|
|
before(async function () {
|
|
({ httpd, port } = await startHTTPServer());
|
|
baseURL = `http://localhost:${port}/`;
|
|
});
|
|
|
|
after(async function () {
|
|
await new Promise(resolve => httpd.stop(resolve));
|
|
});
|
|
|
|
it("should download a PDF from a JS redirect page", async function () {
|
|
var dir = getTestDataDirectory().path;
|
|
httpd.registerFile(
|
|
'/test-pdf-redirect.html',
|
|
Zotero.File.pathToFile(PathUtils.join(dir, 'test-pdf-redirect.html'))
|
|
);
|
|
httpd.registerFile(
|
|
'/test.pdf',
|
|
Zotero.File.pathToFile(PathUtils.join(dir, 'test.pdf'))
|
|
);
|
|
|
|
await Zotero.BrowserRequest.downloadPDF(`${baseURL}test-pdf-redirect.html`, tmpFile.path);
|
|
|
|
var sample = await Zotero.File.getContentsAsync(tmpFile, null, 1000);
|
|
assert.equal(Zotero.MIME.sniffForMIMEType(sample), 'application/pdf');
|
|
});
|
|
|
|
// Needs a js-redirect delay in test-pdf-redirect.html
|
|
it.skip("should display a viewer to clear a captcha if detected", async function () {
|
|
// Force downloadPDF() to time out with a hidden browser, which simulates running into a captcha
|
|
Zotero.Prefs.set('browserRequest.timeout', 10);
|
|
let downloadPDFStub = sinon.stub(Zotero.BrowserRequest, "downloadPDFViaViewer");
|
|
|
|
let promise = Zotero.BrowserRequest.downloadPDF('https://zotero-static.s3.amazonaws.com/test-pdf-redirect.html', tmpFile.path,
|
|
{ shouldDisplayCaptcha: true });
|
|
await new Promise(resolve => downloadPDFStub.callsFake((...args) => {
|
|
resolve();
|
|
Zotero.Prefs.set('browserRequest.timeout', 60e3);
|
|
return downloadPDFStub.wrappedMethod.call(Zotero.BrowserRequest, ...args);
|
|
}));
|
|
|
|
await promise;
|
|
|
|
assert.isTrue(downloadPDFStub.calledOnce);
|
|
downloadPDFStub.restore();
|
|
});
|
|
});
|
|
|
|
describe("#getEntryForURL()", function () {
|
|
it("returns null for unrecognized URLs", function () {
|
|
assert.isNull(Zotero.BrowserRequest.getEntryForURL('https://example.com/foo'));
|
|
});
|
|
|
|
it("matches registered substrings", function () {
|
|
let entry = Zotero.BrowserRequest.getEntryForURL(
|
|
'https://search.worldcat.org/api/search?q=bn%3A123'
|
|
);
|
|
assert.isNotNull(entry);
|
|
assert.equal(entry.match, '://search.worldcat.org');
|
|
});
|
|
|
|
it("sniffs response bodies via detectBlock", function () {
|
|
let entry = Zotero.BrowserRequest.getEntryForURL(
|
|
'https://search.worldcat.org/api/search?q=bn%3A123'
|
|
);
|
|
assert.isFunction(entry.detectBlock);
|
|
assert.isTrue(entry.detectBlock(403, '{"turnstile_required":true}'));
|
|
assert.isFalse(entry.detectBlock(403, 'some other 403 body'));
|
|
assert.isFalse(entry.detectBlock(200, '{"turnstile_required":true}'));
|
|
});
|
|
});
|
|
|
|
describe("#_pollForCookie()", function () {
|
|
it("ignores a stale cookie present at poll start", async function () {
|
|
// Simulate a leftover cookie from a previous session whose HMAC
|
|
// is no longer valid: _pollForCookie must wait for the value to
|
|
// change, not declare success on first read.
|
|
let readStub = sinon.stub(Zotero.BrowserRequest, "_readCookieValue");
|
|
readStub.returns("stale-value");
|
|
|
|
let onFound = sinon.spy();
|
|
let handle = Zotero.BrowserRequest._pollForCookie({
|
|
successCookie: { host: 'example.com', name: 'c' },
|
|
onFound,
|
|
intervalMs: 10
|
|
});
|
|
|
|
try {
|
|
await Zotero.Promise.delay(40);
|
|
assert.isFalse(onFound.called, "should not fire while value is unchanged");
|
|
|
|
readStub.returns("fresh-value");
|
|
await Zotero.Promise.delay(40);
|
|
assert.isTrue(onFound.called, "should fire once a new value appears");
|
|
}
|
|
finally {
|
|
clearInterval(handle);
|
|
readStub.restore();
|
|
}
|
|
});
|
|
|
|
it("fires when the cookie first appears from nothing", async function () {
|
|
let readStub = sinon.stub(Zotero.BrowserRequest, "_readCookieValue");
|
|
readStub.returns(null);
|
|
|
|
let onFound = sinon.spy();
|
|
let handle = Zotero.BrowserRequest._pollForCookie({
|
|
successCookie: { host: 'example.com', name: 'c' },
|
|
onFound,
|
|
intervalMs: 10
|
|
});
|
|
|
|
try {
|
|
await Zotero.Promise.delay(40);
|
|
assert.isFalse(onFound.called);
|
|
|
|
readStub.returns("new-value");
|
|
await Zotero.Promise.delay(40);
|
|
assert.isTrue(onFound.called);
|
|
}
|
|
finally {
|
|
clearInterval(handle);
|
|
readStub.restore();
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("translator request retry", function () {
|
|
function makeUtils() {
|
|
let fakeTranslate = {
|
|
resolveURL: url => url,
|
|
requestHeaders: {},
|
|
cookieSandbox: undefined
|
|
};
|
|
return new Zotero.Utilities.Translate(fakeTranslate);
|
|
}
|
|
|
|
it("retries via clearChallenge when the initial request throws a registered 403", async function () {
|
|
let url = 'https://search.worldcat.org/api/search?q=bn%3A978-0-585-03015-9';
|
|
let clearStub = sinon.stub(Zotero.BrowserRequest, "clearChallenge").resolves();
|
|
let call = 0;
|
|
let requestStub = sinon.stub(Zotero.HTTP, "request").callsFake(() => {
|
|
call++;
|
|
if (call === 1) {
|
|
let xhr = {
|
|
status: 403,
|
|
response: '{"turnstile_required":true}',
|
|
responseText: '{"turnstile_required":true}',
|
|
channel: null
|
|
};
|
|
throw new Zotero.HTTP.UnexpectedStatusException(xhr, url, "Forbidden");
|
|
}
|
|
return {
|
|
status: 200,
|
|
response: { ok: true },
|
|
responseText: '{"ok":true}',
|
|
getAllResponseHeaders: () => ''
|
|
};
|
|
});
|
|
|
|
try {
|
|
let utils = makeUtils();
|
|
let result = await utils.request(url);
|
|
assert.equal(result.status, 200);
|
|
assert.deepEqual(result.body, { ok: true });
|
|
assert.equal(requestStub.callCount, 2);
|
|
assert.isTrue(clearStub.calledOnce);
|
|
assert.equal(clearStub.firstCall.args[0], 'https://search.worldcat.org/search?q=bn%3A978-0-585-03015-9');
|
|
}
|
|
finally {
|
|
requestStub.restore();
|
|
clearStub.restore();
|
|
}
|
|
});
|
|
|
|
it("retries when the 403's XHR is in responseType=json mode (responseText throws)", async function () {
|
|
let url = 'https://search.worldcat.org/api/search?q=bn%3A978-0-585-03015-9';
|
|
let clearStub = sinon.stub(Zotero.BrowserRequest, "clearChallenge").resolves();
|
|
let call = 0;
|
|
let requestStub = sinon.stub(Zotero.HTTP, "request").callsFake(() => {
|
|
call++;
|
|
if (call === 1) {
|
|
let xhr = {
|
|
status: 403,
|
|
response: { turnstile_required: true },
|
|
get responseText() { throw new Error('responseText unavailable'); },
|
|
channel: null
|
|
};
|
|
throw new Zotero.HTTP.UnexpectedStatusException(xhr, url, "Forbidden");
|
|
}
|
|
return {
|
|
status: 200,
|
|
response: { ok: true },
|
|
responseText: '{"ok":true}',
|
|
getAllResponseHeaders: () => ''
|
|
};
|
|
});
|
|
|
|
try {
|
|
let utils = makeUtils();
|
|
let result = await utils.request(url, { responseType: 'json' });
|
|
assert.equal(result.status, 200);
|
|
assert.deepEqual(result.body, { ok: true });
|
|
assert.equal(requestStub.callCount, 2);
|
|
assert.isTrue(clearStub.calledOnce);
|
|
}
|
|
finally {
|
|
requestStub.restore();
|
|
clearStub.restore();
|
|
}
|
|
});
|
|
|
|
it("does not retry when the URL is not registered", async function () {
|
|
let url = 'https://example.com/api/search';
|
|
let clearStub = sinon.stub(Zotero.BrowserRequest, "clearChallenge").resolves();
|
|
let requestStub = sinon.stub(Zotero.HTTP, "request").callsFake(() => {
|
|
let xhr = { status: 403, response: '', responseText: '', channel: null };
|
|
throw new Zotero.HTTP.UnexpectedStatusException(xhr, url, "Forbidden");
|
|
});
|
|
|
|
try {
|
|
let utils = makeUtils();
|
|
let err;
|
|
try { await utils.request(url); }
|
|
catch (e) { err = e; }
|
|
assert.instanceOf(err, Zotero.HTTP.UnexpectedStatusException);
|
|
assert.equal(requestStub.callCount, 1);
|
|
assert.isTrue(clearStub.notCalled);
|
|
}
|
|
finally {
|
|
requestStub.restore();
|
|
clearStub.restore();
|
|
}
|
|
});
|
|
|
|
it("does not retry in browser on a 200 body that matches detectBlock (status must also match)", async function () {
|
|
let url = 'https://search.worldcat.org/api/search?q=bn%3A1';
|
|
let clearStub = sinon.stub(Zotero.BrowserRequest, "clearChallenge").resolves();
|
|
let requestStub = sinon.stub(Zotero.HTTP, "request").returns({
|
|
status: 200,
|
|
response: '{"turnstile_required":true}',
|
|
responseText: '{"turnstile_required":true}',
|
|
getAllResponseHeaders: () => ''
|
|
});
|
|
|
|
try {
|
|
let utils = makeUtils();
|
|
// WorldCat's detectBlock requires status === 403; a 200 with the same
|
|
// body should not trigger a retry.
|
|
await utils.request(url, { responseType: 'text' });
|
|
assert.equal(requestStub.callCount, 1);
|
|
assert.isTrue(clearStub.notCalled);
|
|
}
|
|
finally {
|
|
requestStub.restore();
|
|
clearStub.restore();
|
|
}
|
|
});
|
|
});
|
|
});
|