| // Licensed under the Apache License, Version 2.0 (the "License"); you may not |
| // use this file except in compliance with the License. You may obtain a copy of |
| // the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
| // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| // License for the specific language governing permissions and limitations under |
| // the License. |
| |
| couchTests.attachments= function(debug) { |
| var db_name = get_random_db_name(); |
| var db = new CouchDB(db_name, {"X-Couch-Full-Commit":"false"}); |
| db.createDb(); |
| if (debug) debugger; |
| |
| |
| // MD5 Digests of compressible attachments and therefore Etags |
| // will vary depending on platform gzip implementation. |
| // These MIME types are defined in [attachments] compressible_types |
| var binAttDoc = { |
| _id: "bin_doc", |
| _attachments:{ |
| "foo.txt": { |
| content_type:"application/octet-stream", |
| data: "VGhpcyBpcyBhIGJhc2U2NCBlbmNvZGVkIHRleHQ=" |
| } |
| } |
| }; |
| |
| var save_response = db.save(binAttDoc); |
| T(save_response.ok); |
| |
| var badAttDoc = { |
| _id: "bad_doc", |
| _attachments: { |
| "foo.txt": { |
| content_type: "text/plain", |
| data: "notBase64Encoded=" |
| } |
| } |
| }; |
| |
| try { |
| db.save(badAttDoc); |
| T(false && "Shouldn't get here!"); |
| } catch (e) { |
| TEquals("bad_request", e.error); |
| TEquals("Invalid attachment data for foo.txt", e.message); |
| } |
| |
| var xhr = CouchDB.request("GET", "/" + db_name + "/bin_doc/foo.txt"); |
| T(xhr.responseText == "This is a base64 encoded text"); |
| T(xhr.getResponseHeader("Content-Type") == "application/octet-stream"); |
| TEquals("\"aEI7pOYCRBLTRQvvqYrrJQ==\"", xhr.getResponseHeader("Etag")); |
| |
| // empty attachment |
| var binAttDoc2 = { |
| _id: "bin_doc2", |
| _attachments:{ |
| "foo.txt": { |
| content_type:"text/plain", |
| data: "" |
| } |
| } |
| } |
| |
| T(db.save(binAttDoc2).ok); |
| |
| var xhr = CouchDB.request("GET", "/" + db_name + "/bin_doc2/foo.txt"); |
| T(xhr.responseText.length == 0); |
| T(xhr.getResponseHeader("Content-Type") == "text/plain"); |
| |
| // test RESTful doc API |
| |
| var xhr = CouchDB.request("PUT", "/" + db_name + "/bin_doc2/foo2.txt?rev=" + binAttDoc2._rev, { |
| body:"This is no base64 encoded text", |
| headers:{"Content-Type": "text/plain;charset=utf-8"} |
| }); |
| T(xhr.status == 201); |
| TEquals("/bin_doc2/foo2.txt", |
| xhr.getResponseHeader("Location").substr(-18), |
| "should return Location header to newly created or updated attachment"); |
| |
| var rev = JSON.parse(xhr.responseText).rev; |
| |
| binAttDoc2 = db.open("bin_doc2"); |
| |
| T(binAttDoc2._attachments["foo.txt"] !== undefined); |
| T(binAttDoc2._attachments["foo2.txt"] !== undefined); |
| TEqualsIgnoreCase("text/plain;charset=utf-8", binAttDoc2._attachments["foo2.txt"].content_type); |
| T(binAttDoc2._attachments["foo2.txt"].length == 30); |
| |
| var xhr = CouchDB.request("GET", "/" + db_name + "/bin_doc2/foo2.txt"); |
| T(xhr.responseText == "This is no base64 encoded text"); |
| TEqualsIgnoreCase("text/plain;charset=utf-8", xhr.getResponseHeader("Content-Type")); |
| |
| // test without rev, should fail |
| var xhr = CouchDB.request("DELETE", "/" + db_name + "/bin_doc2/foo2.txt"); |
| T(xhr.status == 409); |
| |
| // test with rev, should not fail |
| var xhr = CouchDB.request("DELETE", "/" + db_name + "/bin_doc2/foo2.txt?rev=" + rev); |
| T(xhr.status == 200); |
| TEquals(null, xhr.getResponseHeader("Location"), |
| "should not return Location header on DELETE request"); |
| |
| // test binary data |
| var bin_data = "JHAPDO*AU£PN ){(3u[d 93DQ9¡€])} ææøo'∂ƒæ≤çæππ•¥∫¶®#†π¶®¥π€ª®˙π8np"; |
| var xhr = CouchDB.request("PUT", "/" + db_name + "/bin_doc3/attachment.txt", { |
| headers:{"Content-Type":"text/plain;charset=utf-8"}, |
| body:bin_data |
| }); |
| T(xhr.status == 201); |
| var rev = JSON.parse(xhr.responseText).rev; |
| // TODO: revisit Etags (missing on doc write) |
| // TEquals('"' + rev + '"', xhr.getResponseHeader("Etag")); |
| |
| var xhr = CouchDB.request("GET", "/" + db_name + "/bin_doc3/attachment.txt"); |
| T(xhr.responseText == bin_data); |
| TEqualsIgnoreCase("text/plain;charset=utf-8", xhr.getResponseHeader("Content-Type")); |
| |
| // without rev |
| var xhr = CouchDB.request("PUT", "/" + db_name + "/bin_doc3/attachment.txt", { |
| headers:{"Content-Type":"text/plain;charset=utf-8"}, |
| body:bin_data |
| }); |
| T(xhr.status == 409); |
| |
| // with nonexistent rev |
| var xhr = CouchDB.request("PUT", "/" + db_name + "/bin_doc3/attachment.txt" + "?rev=1-adae8575ecea588919bd08eb020c708e", { |
| headers:{"Content-Type":"text/plain;charset=utf-8"}, |
| body:bin_data |
| }); |
| T(xhr.status == 409); |
| |
| // with current rev |
| var xhr = CouchDB.request("PUT", "/" + db_name + "/bin_doc3/attachment.txt?rev=" + rev, { |
| headers:{"Content-Type":"text/plain;charset=utf-8"}, |
| body:bin_data |
| }); |
| T(xhr.status == 201); |
| var rev = JSON.parse(xhr.responseText).rev; |
| // TODO: revisit Etags (missing on doc write) |
| // TEquals('"' + rev + '"', xhr.getResponseHeader("Etag")); |
| |
| var xhr = CouchDB.request("GET", "/" + db_name + "/bin_doc3/attachment.txt"); |
| T(xhr.responseText == bin_data); |
| TEqualsIgnoreCase("text/plain;charset=utf-8", xhr.getResponseHeader("Content-Type")); |
| |
| var xhr = CouchDB.request("GET", "/" + db_name + "/bin_doc3/attachment.txt?rev=" + rev); |
| T(xhr.responseText == bin_data); |
| TEqualsIgnoreCase("text/plain;charset=utf-8", xhr.getResponseHeader("Content-Type")); |
| |
| var xhr = CouchDB.request("DELETE", "/" + db_name + "/bin_doc3/attachment.txt?rev=" + rev); |
| T(xhr.status == 200); |
| |
| var xhr = CouchDB.request("GET", "/" + db_name + "/bin_doc3/attachment.txt"); |
| T(xhr.status == 404); |
| |
| // deleted attachment is still accessible with revision |
| var xhr = CouchDB.request("GET", "/" + db_name + "/bin_doc3/attachment.txt?rev=" + rev); |
| T(xhr.status == 200); |
| T(xhr.responseText == bin_data); |
| TEqualsIgnoreCase("text/plain;charset=utf-8", xhr.getResponseHeader("Content-Type")); |
| |
| // empty attachments |
| var xhr = CouchDB.request("PUT", "/" + db_name + "/bin_doc4/attachment.txt", { |
| headers:{"Content-Type":"text/plain;charset=utf-8"}, |
| body:"" |
| }); |
| T(xhr.status == 201); |
| var rev = JSON.parse(xhr.responseText).rev; |
| |
| var xhr = CouchDB.request("GET", "/" + db_name + "/bin_doc4/attachment.txt"); |
| T(xhr.status == 200); |
| T(xhr.responseText.length == 0); |
| |
| // overwrite previsously empty attachment |
| var xhr = CouchDB.request("PUT", "/" + db_name + "/bin_doc4/attachment.txt?rev=" + rev, { |
| headers:{"Content-Type":"text/plain;charset=utf-8"}, |
| body:"This is a string" |
| }); |
| T(xhr.status == 201); |
| |
| var xhr = CouchDB.request("GET", "/" + db_name + "/bin_doc4/attachment.txt"); |
| T(xhr.status == 200); |
| T(xhr.responseText == "This is a string"); |
| |
| // Attachment sparseness COUCHDB-220 |
| |
| var docs = []; |
| for (var i = 0; i < 5; i++) { |
| var doc = { |
| _id: (i).toString(), |
| _attachments:{ |
| "foo.txt": { |
| content_type:"text/plain", |
| data: "VGhpcyBpcyBhIGJhc2U2NCBlbmNvZGVkIHRleHQ=" |
| } |
| } |
| }; |
| docs.push(doc); |
| } |
| |
| var saved = db.bulkSave(docs); |
| // now delete the docs, and while we are looping over them, remove the |
| // '_rev' field so we can re-create after deletion. |
| var to_up = []; |
| for (i=0;i<saved.length;i++) { |
| to_up.push({'_id': saved[i]['id'], '_rev': saved[i]['rev'], '_deleted': true}); |
| delete docs[i]._rev; |
| } |
| // delete them. |
| var saved2 = db.bulkSave(to_up); |
| // re-create them |
| var saved3 = db.bulkSave(docs); |
| |
| var before = db.info().sizes.file; |
| |
| // Compact it. |
| /*T(db.compact().ok); |
| T(db.last_req.status == 202); |
| // compaction isn't instantaneous, loop until done |
| while (db.info().compact_running) {}; |
| |
| var after = db.info().sizes.file; |
| |
| // Compaction should reduce the database slightly, but not |
| // orders of magnitude (unless attachments introduce sparseness) |
| T(after > before * 0.1, "before: " + before + " after: " + after); |
| */ |
| |
| // test large attachments - COUCHDB-366 |
| var lorem = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. ' |
| for (var i=0; i<10; i++) { |
| lorem = lorem + lorem; |
| } |
| var xhr = CouchDB.request("PUT", "/" + db_name + "/bin_doc5/lorem.txt", { |
| headers:{"Content-Type":"text/plain;charset=utf-8"}, |
| body:lorem |
| }); |
| T(xhr.status == 201); |
| var rev = JSON.parse(xhr.responseText).rev; |
| |
| var xhr = CouchDB.request("GET", "/" + db_name + "/bin_doc5/lorem.txt"); |
| T(xhr.responseText == lorem); |
| TEqualsIgnoreCase("text/plain;charset=utf-8", xhr.getResponseHeader("Content-Type")); |
| |
| // test large inline attachment too |
| var lorem_b64 = 'TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQsIGNvbnNlY3RldHVyIGFkaXBpc2NpbmcgZWxpdC4g' |
| for (var i=0; i<10; i++) { |
| lorem_b64 = lorem_b64 + lorem_b64; |
| } |
| var doc = db.open("bin_doc5", {attachments:true}); |
| TEquals(lorem_b64, doc._attachments["lorem.txt"].data, 'binary attachment data should match'); |
| |
| // test etags for attachments. |
| var xhr = CouchDB.request("GET", "/" + db_name + "/bin_doc5/lorem.txt"); |
| T(xhr.status == 200); |
| var etag = xhr.getResponseHeader("etag"); |
| xhr = CouchDB.request("GET", "/" + db_name + "/bin_doc5/lorem.txt", { |
| headers: {"if-none-match": etag} |
| }); |
| T(xhr.status == 304); |
| |
| // test COUCHDB-497 - empty attachments |
| var xhr = CouchDB.request("PUT", "/" + db_name + "/bin_doc5/empty.txt?rev="+rev, { |
| headers:{"Content-Type":"text/plain;charset=utf-8", "Content-Length": "0"}, |
| body:"" |
| }); |
| TEquals(201, xhr.status, "should send 201 Accepted"); |
| var rev = JSON.parse(xhr.responseText).rev; |
| var xhr = CouchDB.request("PUT", "/" + db_name + "/bin_doc5/empty.txt?rev="+rev, { |
| headers:{"Content-Type":"text/plain;charset=utf-8"} |
| }); |
| TEquals(201, xhr.status, "should send 201 Accepted"); |
| |
| // implicit doc creation allows creating docs with a reserved id. COUCHDB-565 |
| var xhr = CouchDB.request("PUT", "/" + db_name + "/_nonexistant/attachment.txt", { |
| headers: {"Content-Type":"text/plain;charset=utf-8"}, |
| body: "THIS IS AN ATTACHMENT. BOOYA!" |
| }); |
| TEquals(400, xhr.status, "should return error code 400 Bad Request"); |
| |
| // test COUCHDB-809 - stubs should only require the 'stub' field |
| var bin_doc6 = { |
| _id: "bin_doc6", |
| _attachments:{ |
| "foo.txt": { |
| content_type:"text/plain", |
| data: "VGhpcyBpcyBhIGJhc2U2NCBlbmNvZGVkIHRleHQ=" |
| } |
| } |
| }; |
| T(db.save(bin_doc6).ok); |
| // stub out the attachment |
| bin_doc6._attachments["foo.txt"] = { stub: true }; |
| T(db.save(bin_doc6).ok == true); |
| |
| // wrong rev pos specified |
| |
| // stub out the attachment with the wrong revpos |
| bin_doc6._attachments["foo.txt"] = { stub: true, revpos: 10}; |
| try { |
| T(db.save(bin_doc6).ok == true); |
| T(false && "Shouldn't get here!"); |
| } catch (e) { |
| T(e.error == "missing_stub"); |
| } |
| |
| // test MD5 header |
| var bin_data = "foo bar" |
| var xhr = CouchDB.request("PUT", "/" + db_name + "/bin_doc7/attachment.txt", { |
| headers:{"Content-Type":"application/octet-stream", |
| "Content-MD5":"MntvB0NYESObxH4VRDUycw=="}, |
| body:bin_data |
| }); |
| TEquals(201, xhr.status); |
| |
| var xhr = CouchDB.request("GET", "/" + db_name + "/bin_doc7/attachment.txt"); |
| TEquals('MntvB0NYESObxH4VRDUycw==', xhr.getResponseHeader("Content-MD5")); |
| |
| // test attachment via multipart/form-data |
| var bin_doc8 = { |
| _id: "bin_doc8" |
| }; |
| T(db.save(bin_doc8).ok); |
| var doc = db.open("bin_doc8"); |
| var body = "------TF\r\n" + |
| "Content-Disposition: form-data; name=\"_rev\"\r\n\r\n" + |
| doc._rev + "\r\n" + |
| "------TF\r\n" + |
| "Content-Disposition: form-data; name=\"_attachments\"; filename=\"file.txt\"\r\n" + |
| "Content-Type: text/plain\r\n\r\n" + |
| "contents of file.txt\r\n\r\n" + |
| "------TF--" |
| xhr = CouchDB.request("POST", "/" + db_name + "/bin_doc8", { |
| headers: { |
| "Content-Type": "multipart/form-data; boundary=----TF", |
| "Content-Length": body.length |
| }, |
| body: body |
| }); |
| TEquals(201, xhr.status); |
| TEquals(true, JSON.parse(xhr.responseText).ok); |
| var doc = db.open("bin_doc8"); |
| T(doc._attachments); |
| T(doc._attachments['file.txt']); |
| |
| // cleanup |
| db.deleteDb(); |
| |
| }; |