www

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | Submodules | README | LICENSE

server_connectorTest.js (21215B)


      1 "use strict";
      2 
      3 describe("Connector Server", function () {
      4 	Components.utils.import("resource://zotero-unit/httpd.js");
      5 	var win, connectorServerPath, testServerPath, httpd;
      6 	var testServerPort = 16213;
      7 	
      8 	before(function* () {
      9 		this.timeout(20000);
     10 		Zotero.Prefs.set("httpServer.enabled", true);
     11 		yield resetDB({
     12 			thisArg: this,
     13 			skipBundledFiles: true
     14 		});
     15 		yield Zotero.Translators.init();
     16 		
     17 		win = yield loadZoteroPane();
     18 		connectorServerPath = 'http://127.0.0.1:' + Zotero.Prefs.get('httpServer.port');
     19 	});
     20 	
     21 	beforeEach(function () {
     22 		// Alternate ports to prevent exceptions not catchable in JS
     23 		testServerPort += (testServerPort & 1) ? 1 : -1;
     24 		testServerPath = 'http://127.0.0.1:' + testServerPort;
     25 		httpd = new HttpServer();
     26 		httpd.start(testServerPort);
     27 	});
     28 	
     29 	afterEach(function* () {
     30 		var defer = new Zotero.Promise.defer();
     31 		httpd.stop(() => defer.resolve());
     32 		yield defer.promise;
     33 	});
     34 	
     35 	after(function () {
     36 		win.close();
     37 	});
     38 
     39 
     40 	describe('/connector/getTranslatorCode', function() {
     41 		it('should respond with translator code', function* () {
     42 			var code = 'function detectWeb() {}\nfunction doImport() {}';
     43 			var translator = buildDummyTranslator(4, code);
     44 			sinon.stub(Zotero.Translators, 'get').returns(translator);
     45 
     46 			var response = yield Zotero.HTTP.request(
     47 				'POST',
     48 				connectorServerPath + "/connector/getTranslatorCode",
     49 				{
     50 					headers: {
     51 						"Content-Type": "application/json"
     52 					},
     53 					body: JSON.stringify({
     54 						translatorID: "dummy-translator",
     55 					})
     56 				}
     57 			);
     58 
     59 			assert.isTrue(Zotero.Translators.get.calledWith('dummy-translator'));
     60 			let translatorCode = yield translator.getCode();
     61 			assert.equal(response.response, translatorCode);
     62 
     63 			Zotero.Translators.get.restore();
     64 		})
     65 	});
     66 	
     67 	
     68 	describe("/connector/detect", function() {
     69 		it("should return relevant translators with proxies", function* () {
     70 			var code = 'function detectWeb() {return "newspaperArticle";}\nfunction doWeb() {}';
     71 			var translator = buildDummyTranslator("web", code, {target: "https://www.example.com/.*"});
     72 			sinon.stub(Zotero.Translators, 'getAllForType').resolves([translator]);
     73 			
     74 			var response = yield Zotero.HTTP.request(
     75 				'POST',
     76 				connectorServerPath + "/connector/detect",
     77 				{
     78 					headers: {
     79 						"Content-Type": "application/json"
     80 					},
     81 					body: JSON.stringify({
     82 						uri: "https://www-example-com.proxy.example.com/article",
     83 						html: "<head><title>Owl</title></head><body><p>🦉</p></body>"
     84 					})
     85 				}
     86 			);
     87 			
     88 			assert.equal(JSON.parse(response.response)[0].proxy.scheme, 'https://%h.proxy.example.com/%p');
     89 
     90 			Zotero.Translators.getAllForType.restore();
     91 		});
     92 	});
     93 	
     94 	
     95 	describe("/connector/saveItems", function () {
     96 		// TODO: Test cookies
     97 		it("should save a translated item to the current selected collection", function* () {
     98 			var collection = yield createDataObject('collection');
     99 			yield waitForItemsLoad(win);
    100 			
    101 			var body = {
    102 				items: [
    103 					{
    104 						itemType: "newspaperArticle",
    105 						title: "Title",
    106 						creators: [
    107 							{
    108 								firstName: "First",
    109 								lastName: "Last",
    110 								creatorType: "author"
    111 							}
    112 						],
    113 						attachments: [
    114 							{
    115 								title: "Attachment",
    116 								url: `${testServerPath}/attachment`,
    117 								mimeType: "text/html"
    118 							}
    119 						]
    120 					}
    121 				],
    122 				uri: "http://example.com"
    123 			};
    124 			
    125 			httpd.registerPathHandler(
    126 				"/attachment",
    127 				{
    128 					handle: function (request, response) {
    129 						response.setStatusLine(null, 200, "OK");
    130 						response.write("<html><head><title>Title</title><body>Body</body></html>");
    131 					}
    132 				}
    133 			);
    134 			
    135 			var promise = waitForItemEvent('add');
    136 			var reqPromise = Zotero.HTTP.request(
    137 				'POST',
    138 				connectorServerPath + "/connector/saveItems",
    139 				{
    140 					headers: {
    141 						"Content-Type": "application/json"
    142 					},
    143 					body: JSON.stringify(body)
    144 				}
    145 			);
    146 			
    147 			// Check parent item
    148 			var ids = yield promise;
    149 			assert.lengthOf(ids, 1);
    150 			var item = Zotero.Items.get(ids[0]);
    151 			assert.equal(Zotero.ItemTypes.getName(item.itemTypeID), 'newspaperArticle');
    152 			assert.isTrue(collection.hasItem(item.id));
    153 			
    154 			// Check attachment
    155 			promise = waitForItemEvent('add');
    156 			ids = yield promise;
    157 			assert.lengthOf(ids, 1);
    158 			item = Zotero.Items.get(ids[0]);
    159 			assert.isTrue(item.isImportedAttachment());
    160 			
    161 			var req = yield reqPromise;
    162 			assert.equal(req.status, 201);
    163 		});
    164 		
    165 		
    166 		it("should switch to My Library if read-only library is selected", function* () {
    167 			var group = yield createGroup({
    168 				editable: false
    169 			});
    170 			yield selectLibrary(win, group.libraryID);
    171 			yield waitForItemsLoad(win);
    172 			
    173 			var body = {
    174 				items: [
    175 					{
    176 						itemType: "newspaperArticle",
    177 						title: "Title",
    178 						creators: [
    179 							{
    180 								firstName: "First",
    181 								lastName: "Last",
    182 								creatorType: "author"
    183 							}
    184 						],
    185 						attachments: []
    186 					}
    187 				],
    188 				uri: "http://example.com"
    189 			};
    190 			
    191 			var promise = waitForItemEvent('add');
    192 			var reqPromise = Zotero.HTTP.request(
    193 				'POST',
    194 				connectorServerPath + "/connector/saveItems",
    195 				{
    196 					headers: {
    197 						"Content-Type": "application/json"
    198 					},
    199 					body: JSON.stringify(body),
    200 					successCodes: false
    201 				}
    202 			);
    203 			
    204 			// My Library be selected, and the item should be in it
    205 			var ids = yield promise;
    206 			assert.equal(
    207 				win.ZoteroPane.collectionsView.getSelectedLibraryID(),
    208 				Zotero.Libraries.userLibraryID
    209 			);
    210 			assert.lengthOf(ids, 1);
    211 			var item = Zotero.Items.get(ids[0]);
    212 			assert.equal(item.libraryID, Zotero.Libraries.userLibraryID);
    213 			assert.equal(Zotero.ItemTypes.getName(item.itemTypeID), 'newspaperArticle');
    214 			
    215 			var req = yield reqPromise;
    216 			assert.equal(req.status, 201);
    217 		});
    218 		
    219 		it("should use the provided proxy to deproxify item url", function* () {
    220 			yield selectLibrary(win, Zotero.Libraries.userLibraryID);
    221 			yield waitForItemsLoad(win);
    222 			
    223 			var body = {
    224 				items: [
    225 					{
    226 						itemType: "newspaperArticle",
    227 						title: "Title",
    228 						creators: [
    229 							{
    230 								firstName: "First",
    231 								lastName: "Last",
    232 								creatorType: "author"
    233 							}
    234 						],
    235 						attachments: [],
    236 						url: "https://www-example-com.proxy.example.com/path"
    237 					}
    238 				],
    239 				uri: "https://www-example-com.proxy.example.com/path",
    240 				proxy: {scheme: 'https://%h.proxy.example.com/%p', dotsToHyphens: true}
    241 			};
    242 			
    243 			var promise = waitForItemEvent('add');
    244 			var req = yield Zotero.HTTP.request(
    245 				'POST',
    246 				connectorServerPath + "/connector/saveItems",
    247 				{
    248 					headers: {
    249 						"Content-Type": "application/json"
    250 					},
    251 					body: JSON.stringify(body)
    252 				}
    253 			);
    254 			
    255 			// Check item
    256 			var ids = yield promise;
    257 			assert.lengthOf(ids, 1);
    258 			var item = Zotero.Items.get(ids[0]);
    259 			assert.equal(item.getField('url'), 'https://www.example.com/path');
    260 		});
    261 	});
    262 	
    263 	describe("/connector/saveSnapshot", function () {
    264 		it("should save a webpage item and snapshot to the current selected collection", function* () {
    265 			var collection = yield createDataObject('collection');
    266 			yield waitForItemsLoad(win);
    267 			
    268 			// saveSnapshot saves parent and child before returning
    269 			var ids1, ids2;
    270 			var promise = waitForItemEvent('add').then(function (ids) {
    271 				ids1 = ids;
    272 				return waitForItemEvent('add').then(function (ids) {
    273 					ids2 = ids;
    274 				});
    275 			});
    276 			yield Zotero.HTTP.request(
    277 				'POST',
    278 				connectorServerPath + "/connector/saveSnapshot",
    279 				{
    280 					headers: {
    281 						"Content-Type": "application/json"
    282 					},
    283 					body: JSON.stringify({
    284 						url: "http://example.com",
    285 						html: "<html><head><title>Title</title><body>Body</body></html>"
    286 					})
    287 				}
    288 			);
    289 			
    290 			assert.isTrue(promise.isFulfilled());
    291 			
    292 			// Check parent item
    293 			assert.lengthOf(ids1, 1);
    294 			var item = Zotero.Items.get(ids1[0]);
    295 			assert.equal(Zotero.ItemTypes.getName(item.itemTypeID), 'webpage');
    296 			assert.isTrue(collection.hasItem(item.id));
    297 			assert.equal(item.getField('title'), 'Title');
    298 			
    299 			// Check attachment
    300 			assert.lengthOf(ids2, 1);
    301 			item = Zotero.Items.get(ids2[0]);
    302 			assert.isTrue(item.isImportedAttachment());
    303 			assert.equal(item.getField('title'), 'Title');
    304 		});
    305 		
    306 		it("should save a PDF to the current selected collection and retrieve metadata", async function () {
    307 			var collection = await createDataObject('collection');
    308 			await waitForItemsLoad(win);
    309 			
    310 			var file = getTestDataDirectory();
    311 			file.append('test.pdf');
    312 			httpd.registerFile("/test.pdf", file);
    313 			
    314 			var promise = waitForItemEvent('add');
    315 			var recognizerPromise = waitForRecognizer();
    316 			
    317 			var origRequest = Zotero.HTTP.request.bind(Zotero.HTTP);
    318 			var called = 0;
    319 			var stub = sinon.stub(Zotero.HTTP, 'request').callsFake(function (method, url, options) {
    320 				// Forward saveSnapshot request
    321 				if (url.endsWith('saveSnapshot')) {
    322 					return origRequest(...arguments);
    323 				}
    324 				
    325 				// Fake recognizer response
    326 				return Zotero.Promise.resolve({
    327 					getResponseHeader: () => {},
    328 					responseText: JSON.stringify({
    329 						title: 'Test',
    330 						authors: []
    331 					})
    332 				});
    333 			});
    334 			
    335 			await Zotero.HTTP.request(
    336 				'POST',
    337 				connectorServerPath + "/connector/saveSnapshot",
    338 				{
    339 					headers: {
    340 						"Content-Type": "application/json"
    341 					},
    342 					body: JSON.stringify({
    343 						url: testServerPath + "/test.pdf",
    344 						pdf: true
    345 					})
    346 				}
    347 			);
    348 			
    349 			var ids = await promise;
    350 			
    351 			assert.lengthOf(ids, 1);
    352 			var item = Zotero.Items.get(ids[0]);
    353 			assert.isTrue(item.isImportedAttachment());
    354 			assert.equal(item.attachmentContentType, 'application/pdf');
    355 			assert.isTrue(collection.hasItem(item.id));
    356 			
    357 			var progressWindow = await recognizerPromise;
    358 			progressWindow.close();
    359 			Zotero.RecognizePDF.cancel();
    360 			assert.isFalse(item.isTopLevelItem());
    361 			
    362 			stub.restore();
    363 		});
    364 		
    365 		it("should switch to My Library if a read-only library is selected", function* () {
    366 			var group = yield createGroup({
    367 				editable: false
    368 			});
    369 			yield selectLibrary(win, group.libraryID);
    370 			yield waitForItemsLoad(win);
    371 			
    372 			var promise = waitForItemEvent('add');
    373 			var reqPromise = Zotero.HTTP.request(
    374 				'POST',
    375 				connectorServerPath + "/connector/saveSnapshot",
    376 				{
    377 					headers: {
    378 						"Content-Type": "application/json"
    379 					},
    380 					body: JSON.stringify({
    381 						url: "http://example.com",
    382 						html: "<html><head><title>Title</title><body>Body</body></html>"
    383 					}),
    384 					successCodes: false
    385 				}
    386 			);
    387 			
    388 			// My Library be selected, and the item should be in it
    389 			var ids = yield promise;
    390 			assert.equal(
    391 				win.ZoteroPane.collectionsView.getSelectedLibraryID(),
    392 				Zotero.Libraries.userLibraryID
    393 			);
    394 			assert.lengthOf(ids, 1);
    395 			var item = Zotero.Items.get(ids[0]);
    396 			assert.equal(item.libraryID, Zotero.Libraries.userLibraryID);
    397 			
    398 			var req = yield reqPromise;
    399 			assert.equal(req.status, 201);
    400 		});
    401 	});
    402 	
    403 	describe("/connector/savePage", function() {
    404 		before(async function () {
    405 			await selectLibrary(win);
    406 			await waitForItemsLoad(win);
    407 		});
    408 		
    409 		it("should return 500 if no translator available for page", function* () {
    410 			var xmlhttp = yield Zotero.HTTP.request(
    411 				'POST',
    412 				connectorServerPath + "/connector/savePage",
    413 				{
    414 					headers: {
    415 						"Content-Type": "application/json"
    416 					},
    417 					body: JSON.stringify({
    418 						uri: "http://example.com",
    419 						html: "<html><head><title>Title</title><body>Body</body></html>"
    420 					}),
    421 					successCodes: false
    422 				}
    423 			);
    424 			assert.equal(xmlhttp.status, 500);
    425 		});
    426 		
    427 		it("should translate a page if translators are available", function* () {
    428 			var html = Zotero.File.getContentsFromURL(getTestDataUrl('coins.html'));
    429 			var promise = waitForItemEvent('add');
    430 			var xmlhttp = yield Zotero.HTTP.request(
    431 				'POST',
    432 				connectorServerPath + "/connector/savePage",
    433 				{
    434 					headers: {
    435 						"Content-Type": "application/json"
    436 					},
    437 					body: JSON.stringify({
    438 						uri: "https://example.com/test",
    439 						html
    440 					}),
    441 					successCodes: false
    442 				}
    443 			);
    444 
    445 			let ids = yield promise;
    446 			var item = Zotero.Items.get(ids[0]);
    447 			var title = "Test Page";
    448 			assert.equal(JSON.parse(xmlhttp.responseText).items[0].title, title);
    449 			assert.equal(item.getField('title'), title);
    450 			assert.equal(xmlhttp.status, 201);
    451 		});
    452 	});
    453 	
    454 	describe("/connector/updateSession", function () {
    455 		it("should update collections and tags of item saved via /saveItems", async function () {
    456 			var collection1 = await createDataObject('collection');
    457 			var collection2 = await createDataObject('collection');
    458 			await waitForItemsLoad(win);
    459 			
    460 			var sessionID = Zotero.Utilities.randomString();
    461 			var body = {
    462 				sessionID,
    463 				items: [
    464 					{
    465 						itemType: "newspaperArticle",
    466 						title: "Title",
    467 						creators: [
    468 							{
    469 								firstName: "First",
    470 								lastName: "Last",
    471 								creatorType: "author"
    472 							}
    473 						],
    474 						attachments: [
    475 							{
    476 								title: "Attachment",
    477 								url: `${testServerPath}/attachment`,
    478 								mimeType: "text/html"
    479 							}
    480 						]
    481 					}
    482 				],
    483 				uri: "http://example.com"
    484 			};
    485 			
    486 			httpd.registerPathHandler(
    487 				"/attachment",
    488 				{
    489 					handle: function (request, response) {
    490 						response.setStatusLine(null, 200, "OK");
    491 						response.write("<html><head><title>Title</title><body>Body</body></html>");
    492 					}
    493 				}
    494 			);
    495 			
    496 			var reqPromise = Zotero.HTTP.request(
    497 				'POST',
    498 				connectorServerPath + "/connector/saveItems",
    499 				{
    500 					headers: {
    501 						"Content-Type": "application/json"
    502 					},
    503 					body: JSON.stringify(body)
    504 				}
    505 			);
    506 			
    507 			var ids = await waitForItemEvent('add');
    508 			var item = Zotero.Items.get(ids[0]);
    509 			assert.isTrue(collection2.hasItem(item.id));
    510 			await waitForItemEvent('add');
    511 			
    512 			var req = await reqPromise;
    513 			assert.equal(req.status, 201);
    514 			
    515 			// Update saved item
    516 			var req = await Zotero.HTTP.request(
    517 				'POST',
    518 				connectorServerPath + "/connector/updateSession",
    519 				{
    520 					headers: {
    521 						"Content-Type": "application/json"
    522 					},
    523 					body: JSON.stringify({
    524 						sessionID,
    525 						target: collection1.treeViewID,
    526 						tags: "A, B"
    527 					})
    528 				}
    529 			);
    530 			
    531 			assert.equal(req.status, 200);
    532 			assert.isTrue(collection1.hasItem(item.id));
    533 			assert.isTrue(item.hasTag("A"));
    534 			assert.isTrue(item.hasTag("B"));
    535 		});
    536 		
    537 		it("should update collections and tags of PDF saved via /saveSnapshot", async function () {
    538 			var sessionID = Zotero.Utilities.randomString();
    539 			
    540 			var collection1 = await createDataObject('collection');
    541 			var collection2 = await createDataObject('collection');
    542 			await waitForItemsLoad(win);
    543 			
    544 			var file = getTestDataDirectory();
    545 			file.append('test.pdf');
    546 			httpd.registerFile("/test.pdf", file);
    547 			
    548 			var ids;
    549 			var promise = waitForItemEvent('add');
    550 			var reqPromise = Zotero.HTTP.request(
    551 				'POST',
    552 				connectorServerPath + "/connector/saveSnapshot",
    553 				{
    554 					headers: {
    555 						"Content-Type": "application/json"
    556 					},
    557 					body: JSON.stringify({
    558 						sessionID,
    559 						url: testServerPath + "/test.pdf",
    560 						pdf: true
    561 					})
    562 				}
    563 			);
    564 			
    565 			var ids = await promise;
    566 			var item = Zotero.Items.get(ids[0]);
    567 			assert.isTrue(collection2.hasItem(item.id));
    568 			var req = await reqPromise;
    569 			assert.equal(req.status, 201);
    570 			
    571 			// Update saved item
    572 			var req = await Zotero.HTTP.request(
    573 				'POST',
    574 				connectorServerPath + "/connector/updateSession",
    575 				{
    576 					headers: {
    577 						"Content-Type": "application/json"
    578 					},
    579 					body: JSON.stringify({
    580 						sessionID,
    581 						target: collection1.treeViewID,
    582 						tags: "A, B"
    583 					})
    584 				}
    585 			);
    586 			
    587 			assert.equal(req.status, 200);
    588 			assert.isTrue(collection1.hasItem(item.id));
    589 			assert.isTrue(item.hasTag("A"));
    590 			assert.isTrue(item.hasTag("B"));
    591 		});
    592 		
    593 		it("should update collections and tags of webpage saved via /saveSnapshot", async function () {
    594 			var sessionID = Zotero.Utilities.randomString();
    595 			
    596 			var collection1 = await createDataObject('collection');
    597 			var collection2 = await createDataObject('collection');
    598 			await waitForItemsLoad(win);
    599 			
    600 			// saveSnapshot saves parent and child before returning
    601 			var ids1, ids2;
    602 			var promise = waitForItemEvent('add').then(function (ids) {
    603 				ids1 = ids;
    604 				return waitForItemEvent('add').then(function (ids) {
    605 					ids2 = ids;
    606 				});
    607 			});
    608 			await Zotero.HTTP.request(
    609 				'POST',
    610 				connectorServerPath + "/connector/saveSnapshot",
    611 				{
    612 					headers: {
    613 						"Content-Type": "application/json"
    614 					},
    615 					body: JSON.stringify({
    616 						sessionID,
    617 						url: "http://example.com",
    618 						html: "<html><head><title>Title</title><body>Body</body></html>"
    619 					})
    620 				}
    621 			);
    622 			
    623 			assert.isTrue(promise.isFulfilled());
    624 			
    625 			var item = Zotero.Items.get(ids1[0]);
    626 			
    627 			// Update saved item
    628 			var req = await Zotero.HTTP.request(
    629 				'POST',
    630 				connectorServerPath + "/connector/updateSession",
    631 				{
    632 					headers: {
    633 						"Content-Type": "application/json"
    634 					},
    635 					body: JSON.stringify({
    636 						sessionID,
    637 						target: collection1.treeViewID,
    638 						tags: "A, B"
    639 					})
    640 				}
    641 			);
    642 			
    643 			assert.equal(req.status, 200);
    644 			assert.isTrue(collection1.hasItem(item.id));
    645 			assert.isTrue(item.hasTag("A"));
    646 			assert.isTrue(item.hasTag("B"));
    647 		});
    648 	});
    649 	
    650 	describe('/connector/installStyle', function() {
    651 		var endpoint;
    652 		
    653 		before(function() {
    654 			endpoint = connectorServerPath + "/connector/installStyle";
    655 		});
    656 		
    657 		it('should reject styles with invalid text', function* () {
    658 			var error = yield getPromiseError(Zotero.HTTP.request(
    659 				'POST',
    660 				endpoint,
    661 				{
    662 					headers: { "Content-Type": "application/json" },
    663 					body: '{}'
    664 				}
    665 			));	
    666 			assert.instanceOf(error, Zotero.HTTP.UnexpectedStatusException);
    667 			assert.equal(error.xmlhttp.status, 400);
    668 			assert.equal(error.xmlhttp.responseText, Zotero.getString("styles.installError", "(null)"));
    669 		});
    670 		
    671 		it('should import a style with application/vnd.citationstyles.style+xml content-type', function* () {
    672 			sinon.stub(Zotero.Styles, 'install').callsFake(function(style) {
    673 				var parser = Components.classes["@mozilla.org/xmlextras/domparser;1"]
    674 					.createInstance(Components.interfaces.nsIDOMParser),
    675 				doc = parser.parseFromString(style, "application/xml");
    676 				
    677 				return Zotero.Promise.resolve(
    678 					Zotero.Utilities.xpathText(doc, '/csl:style/csl:info[1]/csl:title[1]',
    679 						Zotero.Styles.ns)
    680 				);
    681 			});
    682 			
    683 			var style = `<?xml version="1.0" encoding="utf-8"?>
    684 <style xmlns="http://purl.org/net/xbiblio/csl" version="1.0" default-locale="de-DE">
    685   <info>
    686     <title>Test1</title>
    687     <id>http://www.example.com/test2</id>
    688     <link href="http://www.zotero.org/styles/cell" rel="independent-parent"/>
    689   </info>
    690 </style>
    691 `;
    692 			var response = yield Zotero.HTTP.request(
    693 				'POST',
    694 				endpoint,
    695 				{
    696 					headers: { "Content-Type": "application/vnd.citationstyles.style+xml" },
    697 					body: style
    698 				}
    699 			);	
    700 			assert.equal(response.status, 201);
    701 			assert.equal(response.response, JSON.stringify({name: 'Test1'}));
    702 			Zotero.Styles.install.restore();
    703 		});
    704 	});
    705 	
    706 	describe('/connector/import', function() {
    707 		var endpoint;
    708 		
    709 		before(function() {
    710 			endpoint = connectorServerPath + "/connector/import";
    711 		});
    712 		
    713 		it('should reject resources that do not contain import data', function* () {
    714 			var error = yield getPromiseError(Zotero.HTTP.request(
    715 				'POST',
    716 				endpoint,
    717 				{
    718 					headers: { "Content-Type": "text/plain" },
    719 					body: 'Owl'
    720 				}
    721 			));
    722 			assert.instanceOf(error, Zotero.HTTP.UnexpectedStatusException);
    723 			assert.equal(error.xmlhttp.status, 400);
    724 		});
    725 		
    726 		it('should import resources (BibTeX) into selected collection', function* () {
    727 			var collection = yield createDataObject('collection');
    728 			yield waitForItemsLoad(win);
    729 			
    730 			var resource = `@book{test1,
    731   title={Test1},
    732   author={Owl},
    733   year={1000},
    734   publisher={Curly Braces Publishing},
    735   keywords={A, B}
    736 }`;
    737 			
    738 			var addedItemIDsPromise = waitForItemEvent('add');
    739 			var req = yield Zotero.HTTP.request(
    740 				'POST',
    741 				endpoint,
    742 				{
    743 					headers: { "Content-Type": "application/x-bibtex" },
    744 					body: resource
    745 				}
    746 			);	
    747 			assert.equal(req.status, 201);
    748 			assert.equal(JSON.parse(req.responseText)[0].title, 'Test1');
    749 			
    750 			let itemIDs = yield addedItemIDsPromise;
    751 			assert.isTrue(collection.hasItem(itemIDs[0]));
    752 			var item = Zotero.Items.get(itemIDs[0]);
    753 			assert.sameDeepMembers(item.getTags(), [{ tag: 'A', type: 1 }, { tag: 'B', type: 1 }]);
    754 		});
    755 		
    756 		
    757 		it('should switch to My Library if read-only library is selected', function* () {
    758 			var group = yield createGroup({
    759 				editable: false
    760 			});
    761 			yield selectLibrary(win, group.libraryID);
    762 			yield waitForItemsLoad(win);
    763 			
    764 			var resource = `@book{test1,
    765   title={Test1},
    766   author={Owl},
    767   year={1000},
    768   publisher={Curly Braces Publishing}
    769 }`;
    770 			
    771 			var addedItemIDsPromise = waitForItemEvent('add');
    772 			var req = yield Zotero.HTTP.request(
    773 				'POST',
    774 				endpoint,
    775 				{
    776 					headers: { "Content-Type": "application/x-bibtex" },
    777 					body: resource,
    778 					successCodes: false
    779 				}
    780 			);
    781 			
    782 			assert.equal(req.status, 201);
    783 			assert.equal(
    784 				win.ZoteroPane.collectionsView.getSelectedLibraryID(),
    785 				Zotero.Libraries.userLibraryID
    786 			);
    787 			
    788 			let itemIDs = yield addedItemIDsPromise;
    789 			var item = Zotero.Items.get(itemIDs[0]);
    790 			assert.equal(item.libraryID, Zotero.Libraries.userLibraryID);
    791 		});
    792 	});
    793 });