diff options
Diffstat (limited to 'share/www/script/test')
51 files changed, 4492 insertions, 562 deletions
diff --git a/share/www/script/test/attachment_names.js b/share/www/script/test/attachment_names.js index d90c24c4..988dd2d2 100644 --- a/share/www/script/test/attachment_names.js +++ b/share/www/script/test/attachment_names.js @@ -24,7 +24,7 @@ couchTests.attachment_names = function(debug) { data: "VGhpcyBpcyBhIGJhc2U2NCBlbmNvZGVkIHRleHQ=" } } - } + }; // inline attachments try { @@ -72,7 +72,7 @@ couchTests.attachment_names = function(debug) { data: "VGhpcyBpcyBhIGJhc2U2NCBlbmNvZGVkIHRleHQ=" } } - } + }; try { db.save(binAttDoc); diff --git a/share/www/script/test/attachment_paths.js b/share/www/script/test/attachment_paths.js index a2a0f69c..3f6ffb7c 100644 --- a/share/www/script/test/attachment_paths.js +++ b/share/www/script/test/attachment_paths.js @@ -33,7 +33,7 @@ couchTests.attachment_paths = function(debug) { data: "V2UgbGlrZSBwZXJjZW50IHR3byBGLg==" } } - } + }; T(db.save(binAttDoc).ok); @@ -73,7 +73,10 @@ couchTests.attachment_paths = function(debug) { T(binAttDoc._attachments["foo/bar.txt"] !== undefined); T(binAttDoc._attachments["foo%2Fbaz.txt"] !== undefined); T(binAttDoc._attachments["foo/bar2.txt"] !== undefined); - T(binAttDoc._attachments["foo/bar2.txt"].content_type == "text/plain;charset=utf-8"); + TEquals("text/plain;charset=utf-8", // thank you Safari + binAttDoc._attachments["foo/bar2.txt"].content_type.toLowerCase(), + "correct content-type" + ); T(binAttDoc._attachments["foo/bar2.txt"].length == 30); //// now repeat the while thing with a design doc @@ -92,7 +95,7 @@ couchTests.attachment_paths = function(debug) { data: "V2UgbGlrZSBwZXJjZW50IHR3byBGLg==" } } - } + }; T(db.save(binAttDoc).ok); @@ -141,7 +144,10 @@ couchTests.attachment_paths = function(debug) { T(binAttDoc._attachments["foo/bar.txt"] !== undefined); T(binAttDoc._attachments["foo/bar2.txt"] !== undefined); - T(binAttDoc._attachments["foo/bar2.txt"].content_type == "text/plain;charset=utf-8"); + TEquals("text/plain;charset=utf-8", // thank you Safari + binAttDoc._attachments["foo/bar2.txt"].content_type.toLowerCase(), + "correct content-type" + ); T(binAttDoc._attachments["foo/bar2.txt"].length == 30); } }; diff --git a/share/www/script/test/attachment_ranges.js b/share/www/script/test/attachment_ranges.js new file mode 100644 index 00000000..e1d40eae --- /dev/null +++ b/share/www/script/test/attachment_ranges.js @@ -0,0 +1,134 @@ +// 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.attachment_ranges = function(debug) { + var db = new CouchDB("test_suite_db", { + "X-Couch-Full-Commit": "false" + }); + db.deleteDb(); + db.createDb(); + + if (debug) debugger; + + 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); + + // Fetching the whole entity is a 206. + var xhr = CouchDB.request("GET", "/test_suite_db/bin_doc/foo.txt", { + headers: { + "Range": "bytes=0-28" + } + }); + TEquals(206, xhr.status, "fetch 0-28"); + TEquals("This is a base64 encoded text", xhr.responseText); + TEquals("bytes 0-28/29", xhr.getResponseHeader("Content-Range")); + TEquals("29", xhr.getResponseHeader("Content-Length")); + + // Fetch the whole entity without an end offset is a 206. + var xhr = CouchDB.request("GET", "/test_suite_db/bin_doc/foo.txt", { + headers: { + "Range": "bytes=0-" + } + }); + TEquals(206, xhr.status, "fetch 0-"); + TEquals("This is a base64 encoded text", xhr.responseText); + TEquals("bytes 0-28/29", xhr.getResponseHeader("Content-Range")); + TEquals("29", xhr.getResponseHeader("Content-Length")); + + // Badly formed range header is a 200. + var xhr = CouchDB.request("GET", "/test_suite_db/bin_doc/foo.txt", { + headers: { + "Range": "bytes:0-" + } + }); + TEquals(200, xhr.status, "fetch with bad range header"); + + // Fetch the end of an entity without an end offset is a 206. + var xhr = CouchDB.request("GET", "/test_suite_db/bin_doc/foo.txt", { + headers: { + "Range": "bytes=2-" + } + }); + TEquals(206, xhr.status, "fetch 2-"); + TEquals("is is a base64 encoded text", xhr.responseText); + TEquals("bytes 2-28/29", xhr.getResponseHeader("Content-Range")); + TEquals("27", xhr.getResponseHeader("Content-Length")); + + // Fetch past the end of the entity is a 206 + var xhr = CouchDB.request("GET", "/test_suite_db/bin_doc/foo.txt", { + headers: { + "Range": "bytes=0-29" + } + }); + TEquals(206, xhr.status, "fetch 0-29"); + TEquals("bytes 0-28/29", xhr.getResponseHeader("Content-Range")); + TEquals("29", xhr.getResponseHeader("Content-Length")); + + // Fetch first part of entity is a 206 + var xhr = CouchDB.request("GET", "/test_suite_db/bin_doc/foo.txt", { + headers: { + "Range": "bytes=0-3" + } + }); + TEquals(206, xhr.status, "fetch 0-3"); + TEquals("This", xhr.responseText); + TEquals("4", xhr.getResponseHeader("Content-Length")); + TEquals("bytes 0-3/29", xhr.getResponseHeader("Content-Range")); + + // Fetch middle of entity is also a 206 + var xhr = CouchDB.request("GET", "/test_suite_db/bin_doc/foo.txt", { + headers: { + "Range": "bytes=10-15" + } + }); + TEquals(206, xhr.status, "fetch 10-15"); + TEquals("base64", xhr.responseText); + TEquals("6", xhr.getResponseHeader("Content-Length")); + TEquals("bytes 10-15/29", xhr.getResponseHeader("Content-Range")); + + // Fetch end of entity is also a 206 + var xhr = CouchDB.request("GET", "/test_suite_db/bin_doc/foo.txt", { + headers: { + "Range": "bytes=-3" + } + }); + TEquals(206, xhr.status, "fetch -3"); + TEquals("ext", xhr.responseText); + TEquals("3", xhr.getResponseHeader("Content-Length")); + TEquals("bytes 26-28/29", xhr.getResponseHeader("Content-Range")); + + // backward range is 416 + var xhr = CouchDB.request("GET", "/test_suite_db/bin_doc/foo.txt", { + headers: { + "Range": "bytes=5-3" + } + }); + TEquals(416, xhr.status, "fetch 5-3"); + + // range completely outside of entity is 416 + var xhr = CouchDB.request("GET", "/test_suite_db/bin_doc/foo.txt", { + headers: { + "Range": "bytes=300-310" + } + }); + TEquals(416, xhr.status, "fetch 300-310"); + +}; diff --git a/share/www/script/test/attachment_views.js b/share/www/script/test/attachment_views.js index fd30dcfc..a92a8ad0 100644 --- a/share/www/script/test/attachment_views.js +++ b/share/www/script/test/attachment_views.js @@ -68,11 +68,11 @@ couchTests.attachment_views= function(debug) { } emit(parseInt(doc._id), count); - } + }; var reduceFunction = function(key, values) { return sum(values); - } + }; var result = db.query(mapFunction, reduceFunction); diff --git a/share/www/script/test/attachments.js b/share/www/script/test/attachments.js index 36f5a5ad..b0cfd2c5 100644 --- a/share/www/script/test/attachments.js +++ b/share/www/script/test/attachments.js @@ -24,7 +24,7 @@ couchTests.attachments= function(debug) { data: "VGhpcyBpcyBhIGJhc2U2NCBlbmNvZGVkIHRleHQ=" } } - } + }; var save_response = db.save(binAttDoc); T(save_response.ok); @@ -68,12 +68,12 @@ couchTests.attachments= function(debug) { T(binAttDoc2._attachments["foo.txt"] !== undefined); T(binAttDoc2._attachments["foo2.txt"] !== undefined); - T(binAttDoc2._attachments["foo2.txt"].content_type == "text/plain;charset=utf-8"); + TEqualsIgnoreCase("text/plain;charset=utf-8", binAttDoc2._attachments["foo2.txt"].content_type); T(binAttDoc2._attachments["foo2.txt"].length == 30); var xhr = CouchDB.request("GET", "/test_suite_db/bin_doc2/foo2.txt"); T(xhr.responseText == "This is no base64 encoded text"); - T(xhr.getResponseHeader("Content-Type") == "text/plain;charset=utf-8"); + TEqualsIgnoreCase("text/plain;charset=utf-8", xhr.getResponseHeader("Content-Type")); // test without rev, should fail var xhr = CouchDB.request("DELETE", "/test_suite_db/bin_doc2/foo2.txt"); @@ -93,10 +93,11 @@ couchTests.attachments= function(debug) { }); T(xhr.status == 201); var rev = JSON.parse(xhr.responseText).rev; + TEquals('"' + rev + '"', xhr.getResponseHeader("Etag")); var xhr = CouchDB.request("GET", "/test_suite_db/bin_doc3/attachment.txt"); T(xhr.responseText == bin_data); - T(xhr.getResponseHeader("Content-Type") == "text/plain;charset=utf-8"); + TEqualsIgnoreCase("text/plain;charset=utf-8", xhr.getResponseHeader("Content-Type")); var xhr = CouchDB.request("PUT", "/test_suite_db/bin_doc3/attachment.txt", { headers:{"Content-Type":"text/plain;charset=utf-8"}, @@ -110,14 +111,15 @@ couchTests.attachments= function(debug) { }); T(xhr.status == 201); var rev = JSON.parse(xhr.responseText).rev; + TEquals('"' + rev + '"', xhr.getResponseHeader("Etag")); var xhr = CouchDB.request("GET", "/test_suite_db/bin_doc3/attachment.txt"); T(xhr.responseText == bin_data); - T(xhr.getResponseHeader("Content-Type") == "text/plain;charset=utf-8"); + TEqualsIgnoreCase("text/plain;charset=utf-8", xhr.getResponseHeader("Content-Type")); var xhr = CouchDB.request("GET", "/test_suite_db/bin_doc3/attachment.txt?rev=" + rev); T(xhr.responseText == bin_data); - T(xhr.getResponseHeader("Content-Type") == "text/plain;charset=utf-8"); + TEqualsIgnoreCase("text/plain;charset=utf-8", xhr.getResponseHeader("Content-Type")); var xhr = CouchDB.request("DELETE", "/test_suite_db/bin_doc3/attachment.txt?rev=" + rev); T(xhr.status == 200); @@ -129,7 +131,7 @@ couchTests.attachments= function(debug) { var xhr = CouchDB.request("GET", "/test_suite_db/bin_doc3/attachment.txt?rev=" + rev); T(xhr.status == 200); T(xhr.responseText == bin_data); - T(xhr.getResponseHeader("Content-Type") == "text/plain;charset=utf-8"); + TEqualsIgnoreCase("text/plain;charset=utf-8", xhr.getResponseHeader("Content-Type")); // empty attachments var xhr = CouchDB.request("PUT", "/test_suite_db/bin_doc4/attachment.txt", { @@ -156,7 +158,7 @@ couchTests.attachments= function(debug) { // Attachment sparseness COUCHDB-220 - var docs = [] + var docs = []; for (var i = 0; i < 5; i++) { var doc = { _id: (i).toString(), @@ -166,8 +168,8 @@ couchTests.attachments= function(debug) { data: "VGhpcyBpcyBhIGJhc2U2NCBlbmNvZGVkIHRleHQ=" } } - } - docs.push(doc) + }; + docs.push(doc); } var saved = db.bulkSave(docs); @@ -210,7 +212,7 @@ couchTests.attachments= function(debug) { var xhr = CouchDB.request("GET", "/test_suite_db/bin_doc5/lorem.txt"); T(xhr.responseText == lorem); - T(xhr.getResponseHeader("Content-Type") == "text/plain;charset=utf-8"); + TEqualsIgnoreCase("text/plain;charset=utf-8", xhr.getResponseHeader("Content-Type")); // test large inline attachment too var lorem_b64 = CouchDB.request("GET", "/_utils/script/test/lorem_b64.txt").responseText; @@ -244,4 +246,30 @@ couchTests.attachments= function(debug) { 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"); + } }; diff --git a/share/www/script/test/attachments_multipart.js b/share/www/script/test/attachments_multipart.js index 2b79e559..96fe344f 100644 --- a/share/www/script/test/attachments_multipart.js +++ b/share/www/script/test/attachments_multipart.js @@ -58,7 +58,7 @@ couchTests.attachments_multipart= function(debug) { var result = JSON.parse(xhr.responseText); - T(result.ok) + T(result.ok); @@ -115,8 +115,11 @@ couchTests.attachments_multipart= function(debug) { // now test receiving multipart docs function getBoundary(xhr) { - var ctype = xhr.getResponseHeader("Content-Type"); - + if (xhr instanceof XMLHttpRequest) { + var ctype = xhr.getResponseHeader("Content-Type"); + } else { + var ctype = xhr.headers['Content-Type']; + } var ctypeArgs = ctype.split("; ").slice(1); var boundary = null; for(var i=0; i<ctypeArgs.length; i++) { @@ -134,7 +137,11 @@ couchTests.attachments_multipart= function(debug) { function parseMultipart(xhr) { var boundary = getBoundary(xhr); - var mimetext = xhr.responseText; + if (xhr instanceof XMLHttpRequest) { + var mimetext = xhr.responseText; + } else { + var mimetext = xhr.body; + } // strip off leading boundary var leading = "--" + boundary + "\r\n"; var last = "\r\n--" + boundary + "--"; @@ -193,7 +200,7 @@ couchTests.attachments_multipart= function(debug) { // a certain rev). xhr = CouchDB.request("GET", "/test_suite_db/multipart?atts_since=[\"" + firstrev + "\"]", - {headers:{"accept": "multipart/related,*/*;"}}); + {headers:{"accept": "multipart/related, */*"}}); T(xhr.status == 200); @@ -207,7 +214,34 @@ couchTests.attachments_multipart= function(debug) { T(doc._attachments['bar.txt'].follows == true); T(sections[1].body == "this is 18 chars l"); - + + // try the atts_since parameter together with the open_revs parameter + xhr = CouchDB.request( + "GET", + '/test_suite_db/multipart?open_revs=["' + + doc._rev + '"]&atts_since=["' + firstrev + '"]', + {headers: {"accept": "multipart/mixed"}} + ); + + T(xhr.status === 200); + + sections = parseMultipart(xhr); + // 1 section, with a multipart/related Content-Type + T(sections.length === 1); + T(sections[0].headers['Content-Type'].indexOf('multipart/related;') === 0); + + var innerSections = parseMultipart(sections[0]); + // 2 inner sections: a document body section plus an attachment data section + T(innerSections.length === 2); + T(innerSections[0].headers['content-type'] === 'application/json'); + + doc = JSON.parse(innerSections[0].body); + + T(doc._attachments['foo.txt'].stub === true); + T(doc._attachments['bar.txt'].follows === true); + + T(innerSections[1].body === "this is 18 chars l"); + // try it with a rev that doesn't exist (should get all attachments) xhr = CouchDB.request("GET", "/test_suite_db/multipart?atts_since=[\"1-2897589\"]", diff --git a/share/www/script/test/auth_cache.js b/share/www/script/test/auth_cache.js new file mode 100644 index 00000000..e48f7370 --- /dev/null +++ b/share/www/script/test/auth_cache.js @@ -0,0 +1,280 @@ +// 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.auth_cache = function(debug) { + + if (debug) debugger; + + // Simple secret key generator + function generateSecret(length) { + var tab = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" + + "0123456789+/"; + var secret = ''; + for (var i = 0; i < length; i++) { + secret += tab.charAt(Math.floor(Math.random() * 64)); + } + return secret; + } + + var authDb = new CouchDB("test_suite_users", {"X-Couch-Full-Commit":"false"}); + var server_config = [ + { + section: "couch_httpd_auth", + key: "authentication_db", + value: authDb.name + }, + { + section: "couch_httpd_auth", + key: "auth_cache_size", + value: "3" + }, + { + section: "httpd", + key: "authentication_handlers", + value: "{couch_httpd_auth, default_authentication_handler}" + }, + { + section: "couch_httpd_auth", + key: "secret", + value: generateSecret(64) + } + ]; + + + function hits() { + var hits = CouchDB.requestStats("couchdb", "auth_cache_hits", true); + return hits.current || 0; + } + + + function misses() { + var misses = CouchDB.requestStats("couchdb", "auth_cache_misses", true); + return misses.current || 0; + } + + + function testFun() { + var hits_before, + misses_before, + hits_after, + misses_after; + + var fdmanana = CouchDB.prepareUserDoc({ + name: "fdmanana", + roles: ["dev"] + }, "qwerty"); + + T(authDb.save(fdmanana).ok); + + var chris = CouchDB.prepareUserDoc({ + name: "chris", + roles: ["dev", "mafia", "white_costume"] + }, "the_god_father"); + + T(authDb.save(chris).ok); + + var joe = CouchDB.prepareUserDoc({ + name: "joe", + roles: ["erlnager"] + }, "functional"); + + T(authDb.save(joe).ok); + + var johndoe = CouchDB.prepareUserDoc({ + name: "johndoe", + roles: ["user"] + }, "123456"); + + T(authDb.save(johndoe).ok); + + hits_before = hits(); + misses_before = misses(); + + T(CouchDB.login("fdmanana", "qwerty").ok); + + hits_after = hits(); + misses_after = misses(); + + T(misses_after === (misses_before + 1)); + T(hits_after === hits_before); + + hits_before = hits_after; + misses_before = misses_after; + + T(CouchDB.logout().ok); + T(CouchDB.login("fdmanana", "qwerty").ok); + + hits_after = hits(); + misses_after = misses(); + + T(misses_after === misses_before); + T(hits_after === (hits_before + 1)); + + hits_before = hits_after; + misses_before = misses_after; + + T(CouchDB.logout().ok); + T(CouchDB.login("chris", "the_god_father").ok); + + hits_after = hits(); + misses_after = misses(); + + T(misses_after === (misses_before + 1)); + T(hits_after === hits_before); + + hits_before = hits_after; + misses_before = misses_after; + + T(CouchDB.logout().ok); + T(CouchDB.login("joe", "functional").ok); + + hits_after = hits(); + misses_after = misses(); + + T(misses_after === (misses_before + 1)); + T(hits_after === hits_before); + + hits_before = hits_after; + misses_before = misses_after; + + T(CouchDB.logout().ok); + T(CouchDB.login("johndoe", "123456").ok); + + hits_after = hits(); + misses_after = misses(); + + T(misses_after === (misses_before + 1)); + T(hits_after === hits_before); + + hits_before = hits_after; + misses_before = misses_after; + + T(CouchDB.logout().ok); + T(CouchDB.login("joe", "functional").ok); + + hits_after = hits(); + misses_after = misses(); + + // it's an MRU cache, joe was removed from cache to add johndoe + T(misses_after === (misses_before + 1)); + T(hits_after === hits_before); + + hits_before = hits_after; + misses_before = misses_after; + + T(CouchDB.logout().ok); + T(CouchDB.login("fdmanana", "qwerty").ok); + + hits_after = hits(); + misses_after = misses(); + + T(misses_after === misses_before); + T(hits_after === (hits_before + 1)); + + hits_before = hits_after; + misses_before = misses_after; + + var new_salt = CouchDB.newUuids(1)[0]; + var new_passwd = hex_sha1("foobar" + new_salt); + fdmanana.salt = new_salt; + fdmanana.password_sha = new_passwd; + + T(authDb.save(fdmanana).ok); + T(CouchDB.logout().ok); + + // cache was refreshed + T(CouchDB.login("fdmanana", "qwerty").error === "unauthorized"); + T(CouchDB.login("fdmanana", "foobar").ok); + + hits_after = hits(); + misses_after = misses(); + + T(misses_after === misses_before); + T(hits_after === (hits_before + 2)); + + T(CouchDB.logout().ok); + + hits_before = hits_after; + misses_before = misses_after; + + // and yet another update + new_salt = CouchDB.newUuids(1)[0]; + new_passwd = hex_sha1("javascript" + new_salt); + fdmanana.salt = new_salt; + fdmanana.password_sha = new_passwd; + + T(authDb.save(fdmanana).ok); + T(CouchDB.logout().ok); + + // cache was refreshed + T(CouchDB.login("fdmanana", "foobar").error === "unauthorized"); + T(CouchDB.login("fdmanana", "javascript").ok); + + hits_after = hits(); + misses_after = misses(); + + T(misses_after === misses_before); + T(hits_after === (hits_before + 2)); + + T(authDb.deleteDoc(fdmanana).ok); + T(CouchDB.logout().ok); + + hits_before = hits_after; + misses_before = misses_after; + + T(CouchDB.login("fdmanana", "javascript").error === "unauthorized"); + + hits_after = hits(); + misses_after = misses(); + + T(misses_after === misses_before); + T(hits_after === (hits_before + 1)); + + // login, compact authentication DB, login again and verify that + // there was a cache hit + hits_before = hits_after; + misses_before = misses_after; + + T(CouchDB.login("johndoe", "123456").ok); + + hits_after = hits(); + misses_after = misses(); + + T(misses_after === (misses_before + 1)); + T(hits_after === hits_before); + + T(CouchDB.logout().ok); + T(authDb.compact().ok); + + while (authDb.info().compact_running); + + hits_before = hits_after; + misses_before = misses_after; + + T(CouchDB.login("johndoe", "123456").ok); + + hits_after = hits(); + misses_after = misses(); + + T(misses_after === misses_before); + T(hits_after === (hits_before + 1)); + + T(CouchDB.logout().ok); + } + + + authDb.deleteDb(); + run_on_modified_server(server_config, testFun); + + // cleanup + authDb.deleteDb(); +}
\ No newline at end of file diff --git a/share/www/script/test/basics.js b/share/www/script/test/basics.js index 0f9ac44f..30c27c11 100644 --- a/share/www/script/test/basics.js +++ b/share/www/script/test/basics.js @@ -37,15 +37,14 @@ couchTests.basics = function(debug) { TEquals(dbname, xhr.getResponseHeader("Location").substr(-dbname.length), "should return Location header to newly created document"); - - TEquals("http://", - xhr.getResponseHeader("Location").substr(0, 7), + TEquals(CouchDB.protocol, + xhr.getResponseHeader("Location").substr(0, CouchDB.protocol.length), "should return absolute Location header to newly created document"); }); // Get the database info, check the db_name T(db.info().db_name == "test_suite_db"); - T(CouchDB.allDbs().indexOf("test_suite_db") != -1) + T(CouchDB.allDbs().indexOf("test_suite_db") != -1); // Get the database info, check the doc_count T(db.info().doc_count == 0); @@ -91,13 +90,13 @@ couchTests.basics = function(debug) { emit(null, doc.b); }; - results = db.query(mapFunction); + var results = db.query(mapFunction); // verify only one document found and the result value (doc.b). T(results.total_rows == 1 && results.rows[0].value == 16); // reopen document we saved earlier - existingDoc = db.open(id); + var existingDoc = db.open(id); T(existingDoc.a==1); @@ -152,18 +151,20 @@ couchTests.basics = function(debug) { // test that the POST response has a Location header var xhr = CouchDB.request("POST", "/test_suite_db", { - body: JSON.stringify({"foo":"bar"}) + body: JSON.stringify({"foo":"bar"}), + headers: {"Content-Type": "application/json"} }); var resp = JSON.parse(xhr.responseText); T(resp.ok); var loc = xhr.getResponseHeader("Location"); T(loc, "should have a Location header"); var locs = loc.split('/'); - T(locs[4] == resp.id); - T(locs[3] == "test_suite_db"); + T(locs[locs.length-1] == resp.id); + T(locs[locs.length-2] == "test_suite_db"); // test that that POST's with an _id aren't overriden with a UUID. var xhr = CouchDB.request("POST", "/test_suite_db", { + headers: {"Content-Type": "application/json"}, body: JSON.stringify({"_id": "oppossum", "yar": "matey"}) }); var resp = JSON.parse(xhr.responseText); @@ -179,9 +180,8 @@ couchTests.basics = function(debug) { TEquals("/test_suite_db/newdoc", xhr.getResponseHeader("Location").substr(-21), "should return Location header to newly created document"); - - TEquals("http://", - xhr.getResponseHeader("Location").substr(0, 7), + TEquals(CouchDB.protocol, + xhr.getResponseHeader("Location").substr(0, CouchDB.protocol.length), "should return absolute Location header to newly created document"); // deleting a non-existent doc should be 404 @@ -189,12 +189,12 @@ couchTests.basics = function(debug) { T(xhr.status == 404); // Check for invalid document members - bad_docs = [ + var bad_docs = [ ["goldfish", {"_zing": 4}], ["zebrafish", {"_zoom": "hello"}], ["mudfish", {"zane": "goldfish", "_fan": "something smells delicious"}], ["tastyfish", {"_bing": {"wha?": "soda can"}}] - ] + ]; var test_doc = function(info) { var data = JSON.stringify(info[1]); xhr = CouchDB.request("PUT", "/test_suite_db/" + info[0], {body: data}); @@ -202,7 +202,10 @@ couchTests.basics = function(debug) { result = JSON.parse(xhr.responseText); T(result.error == "doc_validation"); - xhr = CouchDB.request("POST", "/test_suite_db/", {body: data}); + xhr = CouchDB.request("POST", "/test_suite_db/", { + headers: {"Content-Type": "application/json"}, + body: data + }); T(xhr.status == 500); result = JSON.parse(xhr.responseText); T(result.error == "doc_validation"); diff --git a/share/www/script/test/batch_save.js b/share/www/script/test/batch_save.js index 1c8a2be9..a1b00192 100644 --- a/share/www/script/test/batch_save.js +++ b/share/www/script/test/batch_save.js @@ -36,7 +36,10 @@ couchTests.batch_save = function(debug) { // repeat the tests for POST for(i=0; i < 100; i++) { - var resp = db.request("POST", db.uri + "?batch=ok", {body: JSON.stringify({a:1})}); + var resp = db.request("POST", db.uri + "?batch=ok", { + headers: {"Content-Type": "application/json"}, + body: JSON.stringify({a:1}) + }); T(JSON.parse(resp.responseText).ok); } diff --git a/share/www/script/test/bulk_docs.js b/share/www/script/test/bulk_docs.js index 346aea83..9095e6b3 100644 --- a/share/www/script/test/bulk_docs.js +++ b/share/www/script/test/bulk_docs.js @@ -51,12 +51,12 @@ couchTests.bulk_docs = function(debug) { T(results.length == 5); T(results[0].id == "0"); T(results[0].error == "conflict"); - T(results[0].rev === undefined); // no rev member when a conflict + T(typeof results[0].rev === "undefined"); // no rev member when a conflict // but the rest are not for (i = 1; i < 5; i++) { T(results[i].id == i.toString()); - T(results[i].rev) + T(results[i].rev); T(db.open(docs[i]._id) == null); } @@ -64,7 +64,7 @@ couchTests.bulk_docs = function(debug) { // save doc 0, this will cause a conflict when we save docs[0] var doc = db.open("0"); - docs[0] = db.open("0") + docs[0] = db.open("0"); db.save(doc); docs[0].shooby = "dooby"; @@ -93,8 +93,8 @@ couchTests.bulk_docs = function(debug) { // Regression test for failure on update/delete var newdoc = {"_id": "foobar", "body": "baz"}; T(db.save(newdoc).ok); - update = {"_id": newdoc._id, "_rev": newdoc._rev, "body": "blam"}; - torem = {"_id": newdoc._id, "_rev": newdoc._rev, "_deleted": true}; + var update = {"_id": newdoc._id, "_rev": newdoc._rev, "body": "blam"}; + var torem = {"_id": newdoc._id, "_rev": newdoc._rev, "_deleted": true}; results = db.bulkSave([update, torem]); T(results[0].error == "conflict" || results[1].error == "conflict"); }; diff --git a/share/www/script/test/changes.js b/share/www/script/test/changes.js index 0cbf3bd6..7ce3baaa 100644 --- a/share/www/script/test/changes.js +++ b/share/www/script/test/changes.js @@ -12,12 +12,12 @@ function jsonp(obj) { T(jsonp_flag == 0); - T(obj.results.length == 1 && obj.last_seq==1, "jsonp") + T(obj.results.length == 1 && obj.last_seq == 1, "jsonp"); jsonp_flag = 1; } couchTests.changes = function(debug) { - var db = new CouchDB("test_suite_db", {"X-Couch-Full-Commit":"false"}); + var db = new CouchDB("test_suite_db", {"X-Couch-Full-Commit":"true"}); db.deleteDb(); db.createDb(); if (debug) debugger; @@ -25,24 +25,32 @@ couchTests.changes = function(debug) { var req = CouchDB.request("GET", "/test_suite_db/_changes"); var resp = JSON.parse(req.responseText); - T(resp.results.length == 0 && resp.last_seq==0, "empty db") + T(resp.results.length == 0 && resp.last_seq == 0, "empty db"); var docFoo = {_id:"foo", bar:1}; T(db.save(docFoo).ok); T(db.ensureFullCommit().ok); + T(db.open(docFoo._id)._id == docFoo._id); req = CouchDB.request("GET", "/test_suite_db/_changes"); var resp = JSON.parse(req.responseText); - T(resp.results.length == 1 && resp.last_seq==1, "one doc db") - T(resp.results[0].changes[0].rev == docFoo._rev) + T(resp.last_seq == 1); + T(resp.results.length == 1, "one doc db"); + T(resp.results[0].changes[0].rev == docFoo._rev); // test with callback - var xhr = CouchDB.request("GET", "/test_suite_db/_changes?callback=jsonp"); - T(xhr.status == 200); - jsonp_flag = 0; - eval(xhr.responseText); - T(jsonp_flag == 1); - + + run_on_modified_server( + [{section: "httpd", + key: "allow_jsonp", + value: "true"}], + function() { + var xhr = CouchDB.request("GET", "/test_suite_db/_changes?callback=jsonp"); + T(xhr.status == 200); + jsonp_flag = 0; + eval(xhr.responseText); + T(jsonp_flag == 1); + }); req = CouchDB.request("GET", "/test_suite_db/_changes?feed=continuous&timeout=10"); var lines = req.responseText.split("\n"); @@ -70,98 +78,111 @@ couchTests.changes = function(debug) { // WebKit (last checked on nightly #47686) does fail on processing // the async-request properly while javascript is executed. - var sleep = function(msecs) { - // by making a slow sync request, we allow the waiting XHR request data - // to be received. - var req = CouchDB.request("GET", "/_sleep?time=" + msecs); - T(JSON.parse(req.responseText).ok == true); - } - - xhr.open("GET", "/test_suite_db/_changes?feed=continuous", true); + xhr.open("GET", "/test_suite_db/_changes?feed=continuous&timeout=500", true); xhr.send(""); var docBar = {_id:"bar", bar:1}; db.save(docBar); + + var lines, change1, change2; + waitForSuccess(function() { + lines = xhr.responseText.split("\n"); + change1 = JSON.parse(lines[0]); + change2 = JSON.parse(lines[1]); + if (change2.seq != 2) { + throw "bad seq, try again"; + } + }, "bar-only"); - sleep(100); - var lines = xhr.responseText.split("\n"); - - var change = JSON.parse(lines[0]); - - T(change.seq == 1) - T(change.id == "foo") - - change = JSON.parse(lines[1]); - - T(change.seq == 2) - T(change.id == "bar") - T(change.changes[0].rev == docBar._rev) - + T(change1.seq == 1); + T(change1.id == "foo"); + + T(change2.seq == 2); + T(change2.id == "bar"); + T(change2.changes[0].rev == docBar._rev); + + var docBaz = {_id:"baz", baz:1}; db.save(docBaz); - sleep(100); - var lines = xhr.responseText.split("\n"); - - change = JSON.parse(lines[2]); - - T(change.seq == 3); - T(change.id == "baz"); - T(change.changes[0].rev == docBaz._rev); + var change3; + waitForSuccess(function() { + lines = xhr.responseText.split("\n"); + change3 = JSON.parse(lines[2]); + if (change3.seq != 3) { + throw "bad seq, try again"; + } + }); + + T(change3.seq == 3); + T(change3.id == "baz"); + T(change3.changes[0].rev == docBaz._rev); xhr = CouchDB.newXhr(); //verify the hearbeat newlines are sent - xhr.open("GET", "/test_suite_db/_changes?feed=continuous&heartbeat=10", true); + xhr.open("GET", "/test_suite_db/_changes?feed=continuous&heartbeat=10&timeout=500", true); xhr.send(""); + + var str; + waitForSuccess(function() { + str = xhr.responseText; + if (str.charAt(str.length - 1) != "\n" || str.charAt(str.length - 2) != "\n") { + throw("keep waiting"); + } + }, "heartbeat"); - sleep(100); - - var str = xhr.responseText; - - T(str.charAt(str.length - 1) == "\n") - T(str.charAt(str.length - 2) == "\n") + T(str.charAt(str.length - 1) == "\n"); + T(str.charAt(str.length - 2) == "\n"); + // otherwise we'll continue to receive heartbeats forever + xhr.abort(); // test longpolling xhr = CouchDB.newXhr(); xhr.open("GET", "/test_suite_db/_changes?feed=longpoll", true); xhr.send(""); - - sleep(100); - var lines = xhr.responseText.split("\n"); - T(lines[5]=='"last_seq":3}'); - + + waitForSuccess(function() { + lines = xhr.responseText.split("\n"); + if (lines[5] != '"last_seq":3}') { + throw("still waiting"); + } + }, "last_seq"); + xhr = CouchDB.newXhr(); xhr.open("GET", "/test_suite_db/_changes?feed=longpoll&since=3", true); xhr.send(""); - sleep(100); - var docBarz = {_id:"barz", bar:1}; db.save(docBarz); - - sleep(100); - - var lines = xhr.responseText.split("\n"); - + var parse_changes_line = function(line) { if (line.charAt(line.length-1) == ",") { - line = line.substring(0, line.length-1); + var linetrimmed = line.substring(0, line.length-1); + } else { + var linetrimmed = line; } - return JSON.parse(line); - } - - change = parse_changes_line(lines[1]); - + return JSON.parse(linetrimmed); + }; + + waitForSuccess(function() { + lines = xhr.responseText.split("\n"); + if (lines[3] != '"last_seq":4}') { + throw("still waiting"); + } + }, "change_lines"); + + var change = parse_changes_line(lines[1]); T(change.seq == 4); T(change.id == "barz"); T(change.changes[0].rev == docBarz._rev); T(lines[3]=='"last_seq":4}'); - + + } // test the filtered changes @@ -175,9 +196,18 @@ couchTests.changes = function(debug) { }), "userCtx" : stringFun(function(doc, req) { return doc.user && (doc.user == req.userCtx.name); - }) + }), + "conflicted" : "function(doc, req) { return (doc._conflicts);}" + }, + options : { + local_seq : true + }, + views : { + local_seq : { + map : "function(doc) {emit(doc._local_seq, null)}" + } } - } + }; db.save(ddoc); @@ -190,7 +220,7 @@ couchTests.changes = function(debug) { var req = CouchDB.request("GET", "/test_suite_db/_changes?filter=changes_filter/bop"); var resp = JSON.parse(req.responseText); - T(resp.results.length == 1); + T(resp.results.length == 1, "filtered/bop"); req = CouchDB.request("GET", "/test_suite_db/_changes?filter=changes_filter/dynamic&field=woox"); resp = JSON.parse(req.responseText); @@ -198,15 +228,14 @@ couchTests.changes = function(debug) { req = CouchDB.request("GET", "/test_suite_db/_changes?filter=changes_filter/dynamic&field=bop"); resp = JSON.parse(req.responseText); - T(resp.results.length == 1); + T(resp.results.length == 1, "changes_filter/dynamic&field=bop"); if (!is_safari && xhr) { // full test requires parallel connections // filter with longpoll // longpoll filters full history when run without a since seq xhr = CouchDB.newXhr(); - xhr.open("GET", "/test_suite_db/_changes?feed=longpoll&filter=changes_filter/bop", true); + xhr.open("GET", "/test_suite_db/_changes?feed=longpoll&filter=changes_filter/bop", false); xhr.send(""); - sleep(100); var resp = JSON.parse(xhr.responseText); T(resp.last_seq == 7); // longpoll waits until a matching change before returning @@ -215,22 +244,53 @@ couchTests.changes = function(debug) { xhr.send(""); db.save({"_id":"falsy", "bop" : ""}); // empty string is falsy db.save({"_id":"bingo","bop" : "bingo"}); - sleep(100); - var resp = JSON.parse(xhr.responseText); + + waitForSuccess(function() { + resp = JSON.parse(xhr.responseText); + }, "longpoll-since"); + T(resp.last_seq == 9); T(resp.results && resp.results.length > 0 && resp.results[0]["id"] == "bingo", "filter the correct update"); - - // filter with continuous - xhr = CouchDB.newXhr(); - xhr.open("GET", "/test_suite_db/_changes?feed=continuous&filter=changes_filter/bop&timeout=200", true); - xhr.send(""); - db.save({"_id":"rusty", "bop" : "plankton"}); - T(db.ensureFullCommit().ok); - sleep(300); - var lines = xhr.responseText.split("\n"); - T(JSON.parse(lines[1]).id == "bingo", lines[1]); - T(JSON.parse(lines[2]).id == "rusty", lines[2]); - T(JSON.parse(lines[3]).last_seq == 10, lines[3]); + xhr.abort(); + + var timeout = 500; + var last_seq = 10; + while (true) { + + // filter with continuous + xhr = CouchDB.newXhr(); + xhr.open("GET", "/test_suite_db/_changes?feed=continuous&filter=changes_filter/bop&timeout="+timeout, true); + xhr.send(""); + + db.save({"_id":"rusty", "bop" : "plankton"}); + T(xhr.readyState != 4, "test client too slow"); + var rusty = db.open("rusty", {cache_bust : new Date()}); + T(rusty._id == "rusty"); + + waitForSuccess(function() { // throws an error after 5 seconds + if (xhr.readyState != 4) { + throw("still waiting"); + } + }, "continuous-rusty"); + lines = xhr.responseText.split("\n"); + var good = false; + try { + JSON.parse(lines[3]); + good = true; + } catch(e) { + } + if (good) { + T(JSON.parse(lines[1]).id == "bingo", lines[1]); + T(JSON.parse(lines[2]).id == "rusty", lines[2]); + T(JSON.parse(lines[3]).last_seq == last_seq, lines[3]); + break; + } else { + xhr.abort(); + db.deleteDoc(rusty); + timeout = timeout * 2; + last_seq = last_seq + 2; + } + } } // error conditions @@ -257,7 +317,8 @@ couchTests.changes = function(debug) { var req = CouchDB.request("GET", "/test_suite_db/_changes?filter=changes_filter/bop&style=all_docs"); var resp = JSON.parse(req.responseText); - TEquals(3, resp.results.length, "should return matching rows"); + var expect = (!is_safari && xhr) ? 3: 1; + TEquals(expect, resp.results.length, "should return matching rows"); // test for userCtx run_on_modified_server( @@ -286,9 +347,119 @@ couchTests.changes = function(debug) { resp = JSON.parse(req.responseText); T(resp.results.length == 1, "userCtx"); T(resp.results[0].id == docResp.id); - }); - - req = CouchDB.request("GET", "/test_suite_db/_changes?limit=1"); - resp = JSON.parse(req.responseText); - TEquals(1, resp.results.length) + } + ); + + req = CouchDB.request("GET", "/test_suite_db/_changes?limit=1"); + resp = JSON.parse(req.responseText); + TEquals(1, resp.results.length); + + //filter includes _conflicts + var id = db.save({'food' : 'pizza'}).id; + db.bulkSave([{_id: id, 'food' : 'pasta'}], {all_or_nothing:true}); + + req = CouchDB.request("GET", "/test_suite_db/_changes?filter=changes_filter/conflicted"); + resp = JSON.parse(req.responseText); + T(resp.results.length == 1, "filter=changes_filter/conflicted"); + + // test with erlang filter function + run_on_modified_server([{ + section: "native_query_servers", + key: "erlang", + value: "{couch_native_process, start_link, []}" + }], function() { + var erl_ddoc = { + _id: "_design/erlang", + language: "erlang", + filters: { + foo: + 'fun({Doc}, Req) -> ' + + ' Value = couch_util:get_value(<<"value">>, Doc),' + + ' (Value rem 2) =:= 0' + + 'end.' + } + }; + + db.deleteDb(); + db.createDb(); + T(db.save(erl_ddoc).ok); + + var req = CouchDB.request("GET", "/test_suite_db/_changes?filter=erlang/foo"); + var resp = JSON.parse(req.responseText); + T(resp.results.length === 0); + + T(db.save({_id: "doc1", value : 1}).ok); + T(db.save({_id: "doc2", value : 2}).ok); + T(db.save({_id: "doc3", value : 3}).ok); + T(db.save({_id: "doc4", value : 4}).ok); + + var req = CouchDB.request("GET", "/test_suite_db/_changes?filter=erlang/foo"); + var resp = JSON.parse(req.responseText); + T(resp.results.length === 2); + T(resp.results[0].id === "doc2"); + T(resp.results[1].id === "doc4"); + + // test filtering on docids + // + + var options = { + headers: {"Content-Type": "application/json"}, + body: JSON.stringify({"doc_ids": ["something", "anotherthing", "andmore"]}) + }; + + var req = CouchDB.request("POST", "/test_suite_db/_changes?filter=_doc_ids", options); + var resp = JSON.parse(req.responseText); + T(resp.results.length === 0); + + T(db.save({"_id":"something", "bop" : "plankton"}).ok); + var req = CouchDB.request("POST", "/test_suite_db/_changes?filter=_doc_ids", options); + var resp = JSON.parse(req.responseText); + T(resp.results.length === 1); + T(resp.results[0].id === "something"); + + T(db.save({"_id":"anotherthing", "bop" : "plankton"}).ok); + var req = CouchDB.request("POST", "/test_suite_db/_changes?filter=_doc_ids", options); + var resp = JSON.parse(req.responseText); + T(resp.results.length === 2); + T(resp.results[0].id === "something"); + T(resp.results[1].id === "anotherthing"); + + var docids = JSON.stringify(["something", "anotherthing", "andmore"]), + req = CouchDB.request("GET", "/test_suite_db/_changes?filter=_doc_ids&doc_ids="+docids, options); + var resp = JSON.parse(req.responseText); + T(resp.results.length === 2); + T(resp.results[0].id === "something"); + T(resp.results[1].id === "anotherthing"); + + var req = CouchDB.request("GET", "/test_suite_db/_changes?filter=_design"); + var resp = JSON.parse(req.responseText); + T(resp.results.length === 1); + T(resp.results[0].id === "_design/erlang"); + + + if (!is_safari && xhr) { + // filter docids with continuous + xhr = CouchDB.newXhr(); + xhr.open("POST", "/test_suite_db/_changes?feed=continuous&timeout=500&since=7&filter=_doc_ids", true); + xhr.setRequestHeader("Content-Type", "application/json"); + + xhr.send(options.body); + + T(db.save({"_id":"andmore", "bop" : "plankton"}).ok); + + + waitForSuccess(function() { + if (xhr.readyState != 4) { + throw("still waiting"); + } + }, "andmore-only"); + + var line = JSON.parse(xhr.responseText.split("\n")[0]); + T(line.seq == 8); + T(line.id == "andmore"); + } + + }); + }; + diff --git a/share/www/script/test/compact.js b/share/www/script/test/compact.js index f63bfc57..805a3b08 100644 --- a/share/www/script/test/compact.js +++ b/share/www/script/test/compact.js @@ -26,11 +26,12 @@ couchTests.compact = function(debug) { data: "VGhpcyBpcyBhIGJhc2U2NCBlbmNvZGVkIHRleHQ=" } } - } + }; T(db.save(binAttDoc).ok); var originalsize = db.info().disk_size; + var start_time = db.info().instance_start_time; for(var i in docs) { db.deleteDoc(docs[i]); @@ -38,17 +39,20 @@ couchTests.compact = function(debug) { T(db.ensureFullCommit().ok); var deletesize = db.info().disk_size; T(deletesize > originalsize); + T(db.setDbProperty("_revs_limit", 666).ok); T(db.compact().ok); T(db.last_req.status == 202); // compaction isn't instantaneous, loop until done while (db.info().compact_running) {}; + T(db.info().instance_start_time == start_time); + T(db.getDbProperty("_revs_limit") === 666); T(db.ensureFullCommit().ok); restartServer(); var xhr = CouchDB.request("GET", "/test_suite_db/bin_doc/foo.txt"); - T(xhr.responseText == "This is a base64 encoded text") - T(xhr.getResponseHeader("Content-Type") == "text/plain") + T(xhr.responseText == "This is a base64 encoded text"); + T(xhr.getResponseHeader("Content-Type") == "text/plain"); T(db.info().doc_count == 1); T(db.info().disk_size < deletesize); diff --git a/share/www/script/test/config.js b/share/www/script/test/config.js index ef74934b..e83ecfd9 100644 --- a/share/www/script/test/config.js +++ b/share/www/script/test/config.js @@ -29,19 +29,25 @@ couchTests.config = function(debug) { */ var server_port = CouchDB.host.split(':'); if(server_port.length == 1 && CouchDB.inBrowser) { - var proto = window.location.protocol; - if(proto == "http:") { + if(CouchDB.protocol == "http://") { port = 80; } - if(proto == "https:") { + if(CouchDB.protocol == "https://") { port = 443; } } else { port = server_port.pop(); } + if(CouchDB.protocol == "http://") { + config_port = config.httpd.port; + } + if(CouchDB.protocol == "https://") { + config_port = config.ssl.port; + } + if(port) { - T(config.httpd.port == port); + TEquals(config_port, port, "ports should match"); } T(config.couchdb.database_dir); @@ -50,7 +56,8 @@ couchTests.config = function(debug) { T(config.log.level); T(config.query_servers.javascript); - // test that settings can be altered + // test that settings can be altered, and that an undefined whitelist allows any change + TEquals(undefined, config.httpd.config_whitelist, "Default whitelist is empty"); xhr = CouchDB.request("PUT", "/_config/test/foo",{ body : JSON.stringify("bar"), headers: {"X-Couch-Persist": "false"} @@ -64,4 +71,93 @@ couchTests.config = function(debug) { xhr = CouchDB.request("GET", "/_config/test/foo"); config = JSON.parse(xhr.responseText); T(config == "bar"); + + // Non-term whitelist values allow further modification of the whitelist. + xhr = CouchDB.request("PUT", "/_config/httpd/config_whitelist",{ + body : JSON.stringify("!This is an invalid Erlang term!"), + headers: {"X-Couch-Persist": "false"} + }); + TEquals(200, xhr.status, "Set config whitelist to an invalid Erlang term"); + xhr = CouchDB.request("DELETE", "/_config/httpd/config_whitelist",{ + headers: {"X-Couch-Persist": "false"} + }); + TEquals(200, xhr.status, "Modify whitelist despite it being invalid syntax"); + + // Non-list whitelist values allow further modification of the whitelist. + xhr = CouchDB.request("PUT", "/_config/httpd/config_whitelist",{ + body : JSON.stringify("{[yes, a_valid_erlang_term, but_unfortunately, not_a_list]}"), + headers: {"X-Couch-Persist": "false"} + }); + TEquals(200, xhr.status, "Set config whitelist to an non-list term"); + xhr = CouchDB.request("DELETE", "/_config/httpd/config_whitelist",{ + headers: {"X-Couch-Persist": "false"} + }); + TEquals(200, xhr.status, "Modify whitelist despite it not being a list"); + + // Keys not in the whitelist may not be modified. + xhr = CouchDB.request("PUT", "/_config/httpd/config_whitelist",{ + body : JSON.stringify("[{httpd,config_whitelist}, {test,foo}]"), + headers: {"X-Couch-Persist": "false"} + }); + TEquals(200, xhr.status, "Set config whitelist to something valid"); + + ["PUT", "DELETE"].forEach(function(method) { + ["test/not_foo", "not_test/foo", "neither_test/nor_foo"].forEach(function(pair) { + var path = "/_config/" + pair; + var test_name = method + " to " + path + " disallowed: not whitelisted"; + + xhr = CouchDB.request(method, path, { + body : JSON.stringify("Bummer! " + test_name), + headers: {"X-Couch-Persist": "false"} + }); + TEquals(400, xhr.status, test_name); + }); + }); + + // Keys in the whitelist may be modified. + ["PUT", "DELETE"].forEach(function(method) { + xhr = CouchDB.request(method, "/_config/test/foo",{ + body : JSON.stringify(method + " to whitelisted config variable"), + headers: {"X-Couch-Persist": "false"} + }); + TEquals(200, xhr.status, "Keys in the whitelist may be modified"); + }); + + // Non-2-tuples in the whitelist are ignored + xhr = CouchDB.request("PUT", "/_config/httpd/config_whitelist",{ + body : JSON.stringify("[{httpd,config_whitelist}, these, {are}, {nOt, 2, tuples}," + + " [so], [they, will], [all, become, noops], {test,foo}]"), + headers: {"X-Couch-Persist": "false"} + }); + TEquals(200, xhr.status, "Set config whitelist with some inert values"); + ["PUT", "DELETE"].forEach(function(method) { + xhr = CouchDB.request(method, "/_config/test/foo",{ + body : JSON.stringify(method + " to whitelisted config variable"), + headers: {"X-Couch-Persist": "false"} + }); + TEquals(200, xhr.status, "Update whitelisted variable despite invalid entries"); + }); + + // Atoms, binaries, and strings suffice as whitelist sections and keys. + ["{test,foo}", '{"test","foo"}', '{<<"test">>,<<"foo">>}'].forEach(function(pair) { + xhr = CouchDB.request("PUT", "/_config/httpd/config_whitelist",{ + body : JSON.stringify("[{httpd,config_whitelist}, " + pair + "]"), + headers: {"X-Couch-Persist": "false"} + }); + TEquals(200, xhr.status, "Set config whitelist to include " + pair); + + var pair_format = {"t":"tuple", '"':"string", "<":"binary"}[pair[1]]; + ["PUT", "DELETE"].forEach(function(method) { + xhr = CouchDB.request(method, "/_config/test/foo",{ + body : JSON.stringify(method + " with " + pair_format), + headers: {"X-Couch-Persist": "false"} + }); + TEquals(200, xhr.status, "Whitelist works with " + pair_format); + }); + }); + + xhr = CouchDB.request("DELETE", "/_config/httpd/config_whitelist",{ + headers: {"X-Couch-Persist": "false"} + }); + TEquals(200, xhr.status, "Reset config whitelist to undefined"); }; diff --git a/share/www/script/test/conflicts.js b/share/www/script/test/conflicts.js index b8b93946..7258bc31 100644 --- a/share/www/script/test/conflicts.js +++ b/share/www/script/test/conflicts.js @@ -44,7 +44,7 @@ couchTests.conflicts = function(debug) { var changes = db.changes(); - T( changes.results.length == 1) + T(changes.results.length == 1); // Now clear out the _rev member and save. This indicates this document is // new, not based on an existing revision. diff --git a/share/www/script/test/content_negotiation.js b/share/www/script/test/content_negotiation.js index 171dbb3d..c79df948 100644 --- a/share/www/script/test/content_negotiation.js +++ b/share/www/script/test/content_negotiation.js @@ -17,11 +17,14 @@ couchTests.content_negotiation = function(debug) { if (debug) debugger; var xhr; - xhr = CouchDB.request("GET", "/test_suite_db/"); - TEquals("text/plain;charset=utf-8", xhr.getResponseHeader("Content-Type")); + // with no accept header + var req = CouchDB.newXhr(); + req.open("GET", "/test_suite_db/", false); + req.send(""); + TEquals("text/plain;charset=utf-8", req.getResponseHeader("Content-Type")); // make sure JSON responses end in a newline - var text = xhr.responseText; + var text = req.responseText; TEquals("\n", text[text.length-1]); xhr = CouchDB.request("GET", "/test_suite_db/", { diff --git a/share/www/script/test/cookie_auth.js b/share/www/script/test/cookie_auth.js index 9eadfee0..8ad993cc 100644 --- a/share/www/script/test/cookie_auth.js +++ b/share/www/script/test/cookie_auth.js @@ -46,39 +46,39 @@ couchTests.cookie_auth = function(debug) { // Create a user var jasonUserDoc = CouchDB.prepareUserDoc({ - username: "Jason Davies", + name: "Jason Davies", roles: ["dev"] }, password); T(usersDb.save(jasonUserDoc).ok); var checkDoc = usersDb.open(jasonUserDoc._id); - T(checkDoc.username == "Jason Davies"); + T(checkDoc.name == "Jason Davies"); var jchrisUserDoc = CouchDB.prepareUserDoc({ - username: "jchris@apache.org" + name: "jchris@apache.org" }, "funnybone"); T(usersDb.save(jchrisUserDoc).ok); // make sure we cant create duplicate users var duplicateJchrisDoc = CouchDB.prepareUserDoc({ - username: "jchris@apache.org" + name: "jchris@apache.org" }, "eh, Boo-Boo?"); try { - usersDb.save(duplicateJchrisDoc) + usersDb.save(duplicateJchrisDoc); T(false && "Can't create duplicate user names. Should have thrown an error."); } catch (e) { T(e.error == "conflict"); T(usersDb.last_req.status == 409); } - // we can't create _usernames + // we can't create _names var underscoreUserDoc = CouchDB.prepareUserDoc({ - username: "_why" + name: "_why" }, "copperfield"); try { - usersDb.save(underscoreUserDoc) + usersDb.save(underscoreUserDoc); T(false && "Can't create underscore user names. Should have thrown an error."); } catch (e) { T(e.error == "forbidden"); @@ -87,46 +87,59 @@ couchTests.cookie_auth = function(debug) { // we can't create docs with malformed ids var badIdDoc = CouchDB.prepareUserDoc({ - username: "foo" + name: "foo" }, "bar"); badIdDoc._id = "org.apache.couchdb:w00x"; try { - usersDb.save(badIdDoc) + usersDb.save(badIdDoc); T(false && "Can't create malformed docids. Should have thrown an error."); } catch (e) { T(e.error == "forbidden"); T(usersDb.last_req.status == 403); } - - try { - usersDb.save(underscoreUserDoc) - T(false && "Can't create underscore user names. Should have thrown an error."); - } catch (e) { - T(e.error == "forbidden"); - T(usersDb.last_req.status == 403); - } // login works T(CouchDB.login('Jason Davies', password).ok); - T(CouchDB.session().name == 'Jason Davies'); + T(CouchDB.session().userCtx.name == 'Jason Davies'); + // JSON login works + var xhr = CouchDB.request("POST", "/_session", { + headers: {"Content-Type": "application/json"}, + body: JSON.stringify({ + name: 'Jason Davies', + password: password + }) + }); + + T(JSON.parse(xhr.responseText).ok); + T(CouchDB.session().userCtx.name == 'Jason Davies'); + // update one's own credentials document jasonUserDoc.foo=2; T(usersDb.save(jasonUserDoc).ok); + T(CouchDB.session().userCtx.roles.indexOf("_admin") == -1); + // can't delete another users doc unless you are admin + try { + usersDb.deleteDoc(jchrisUserDoc); + T(false && "Can't delete other users docs. Should have thrown an error."); + } catch (e) { + T(e.error == "forbidden"); + T(usersDb.last_req.status == 403); + } // TODO should login() throw an exception here? T(!CouchDB.login('Jason Davies', "2.71828").ok); T(!CouchDB.login('Robert Allen Zimmerman', 'd00d').ok); // a failed login attempt should log you out - T(CouchDB.session().name != 'Jason Davies'); + T(CouchDB.session().userCtx.name != 'Jason Davies'); // test redirect xhr = CouchDB.request("POST", "/_session?next=/", { headers: {"Content-Type": "application/x-www-form-urlencoded"}, - body: "username=Jason%20Davies&password="+encodeURIComponent(password) + body: "name=Jason%20Davies&password="+encodeURIComponent(password) }); // should this be a redirect code instead of 200? // The cURL adapter is returning the expected 302 here. @@ -134,23 +147,23 @@ couchTests.cookie_auth = function(debug) { // to follow the redirect, ie, the browser follows and does a // GET on the returned Location if (xhr.status == 200) { - T(/Welcome/.test(xhr.responseText)) + T(/Welcome/.test(xhr.responseText)); } else { - T(xhr.status == 302) - T(xhr.getResponseHeader("Location")) + T(xhr.status == 302); + T(xhr.getResponseHeader("Location")); } // test users db validations // // test that you can't update docs unless you are logged in as the user (or are admin) T(CouchDB.login("jchris@apache.org", "funnybone").ok); - T(CouchDB.session().name == "jchris@apache.org"); - T(CouchDB.session().roles.length == 0); + T(CouchDB.session().userCtx.name == "jchris@apache.org"); + T(CouchDB.session().userCtx.roles.length == 0); jasonUserDoc.foo=3; try { - usersDb.save(jasonUserDoc) + usersDb.save(jasonUserDoc); T(false && "Can't update someone else's user doc. Should have thrown an error."); } catch (e) { T(e.error == "forbidden"); @@ -161,7 +174,7 @@ couchTests.cookie_auth = function(debug) { jchrisUserDoc.roles = ["foo"]; try { - usersDb.save(jchrisUserDoc) + usersDb.save(jchrisUserDoc); T(false && "Can't set roles unless you are admin. Should have thrown an error."); } catch (e) { T(e.error == "forbidden"); @@ -169,7 +182,7 @@ couchTests.cookie_auth = function(debug) { } T(CouchDB.logout().ok); - T(CouchDB.session().roles[0] == "_admin"); + T(CouchDB.session().userCtx.roles[0] == "_admin"); jchrisUserDoc.foo = ["foo"]; T(usersDb.save(jchrisUserDoc).ok); @@ -178,7 +191,7 @@ couchTests.cookie_auth = function(debug) { jchrisUserDoc.roles = ["_bar"]; try { - usersDb.save(jchrisUserDoc) + usersDb.save(jchrisUserDoc); T(false && "Can't add system roles to user's db. Should have thrown an error."); } catch (e) { T(e.error == "forbidden"); @@ -187,24 +200,24 @@ couchTests.cookie_auth = function(debug) { // make sure the foo role has been applied T(CouchDB.login("jchris@apache.org", "funnybone").ok); - T(CouchDB.session().name == "jchris@apache.org"); - T(CouchDB.session().roles.indexOf("_admin") == -1); - T(CouchDB.session().roles.indexOf("foo") != -1); + T(CouchDB.session().userCtx.name == "jchris@apache.org"); + T(CouchDB.session().userCtx.roles.indexOf("_admin") == -1); + T(CouchDB.session().userCtx.roles.indexOf("foo") != -1); // now let's make jchris a server admin T(CouchDB.logout().ok); - T(CouchDB.session().roles[0] == "_admin"); - T(CouchDB.session().name == null); + T(CouchDB.session().userCtx.roles[0] == "_admin"); + T(CouchDB.session().userCtx.name == null); // set the -hashed- password so the salt matches // todo ask on the ML about this run_on_modified_server([{section: "admins", key: "jchris@apache.org", value: "funnybone"}], function() { T(CouchDB.login("jchris@apache.org", "funnybone").ok); - T(CouchDB.session().name == "jchris@apache.org"); - T(CouchDB.session().roles.indexOf("_admin") != -1); + T(CouchDB.session().userCtx.name == "jchris@apache.org"); + T(CouchDB.session().userCtx.roles.indexOf("_admin") != -1); // test that jchris still has the foo role - T(CouchDB.session().roles.indexOf("foo") != -1); + T(CouchDB.session().userCtx.roles.indexOf("foo") != -1); // should work even when user doc has no password jchrisUserDoc = usersDb.open(jchrisUserDoc._id); @@ -214,13 +227,13 @@ couchTests.cookie_auth = function(debug) { T(CouchDB.logout().ok); T(CouchDB.login("jchris@apache.org", "funnybone").ok); var s = CouchDB.session(); - T(s.name == "jchris@apache.org"); - T(s.roles.indexOf("_admin") != -1); + T(s.userCtx.name == "jchris@apache.org"); + T(s.userCtx.roles.indexOf("_admin") != -1); // test session info - T(s.info.authenticated == "{couch_httpd_auth, cookie_authentication_handler}"); - T(s.info.user_db == "test_suite_users"); + T(s.info.authenticated == "cookie"); + T(s.info.authentication_db == "test_suite_users"); // test that jchris still has the foo role - T(CouchDB.session().roles.indexOf("foo") != -1); + T(CouchDB.session().userCtx.roles.indexOf("foo") != -1); }); } finally { diff --git a/share/www/script/test/copy_doc.js b/share/www/script/test/copy_doc.js index a6de1892..99e3c7fe 100644 --- a/share/www/script/test/copy_doc.js +++ b/share/www/script/test/copy_doc.js @@ -36,6 +36,9 @@ couchTests.copy_doc = function(debug) { }); T(xhr.status == 409); // conflict + var xhr = CouchDB.request("COPY", "/test_suite_db/doc_to_be_copied2"); + T(xhr.status == 400); // bad request (no Destination header) + var rev = db.open("doc_to_be_overwritten")._rev; var xhr = CouchDB.request("COPY", "/test_suite_db/doc_to_be_copied2", { headers: {"Destination":"doc_to_be_overwritten?rev=" + rev} diff --git a/share/www/script/test/delayed_commits.js b/share/www/script/test/delayed_commits.js index d0c87182..dbb072fb 100644 --- a/share/www/script/test/delayed_commits.js +++ b/share/www/script/test/delayed_commits.js @@ -122,4 +122,33 @@ couchTests.delayed_commits = function(debug) { } }); + + // Test that a conflict can't cause delayed commits to fail + run_on_modified_server( + [{section: "couchdb", + key: "delayed_commits", + value: "true"}], + + function() { + //First save a document and commit it + T(db.save({_id:"6",a:2,b:4}).ok); + T(db.ensureFullCommit().ok); + //Generate a conflict + try { + db.save({_id:"6",a:2,b:4}); + } catch( e) { + T(e.error == "conflict"); + } + //Wait for the delayed commit interval to pass + var time = new Date(); + while(new Date() - time < 2000); + //Save a new doc + T(db.save({_id:"7",a:2,b:4}).ok); + //Wait for the delayed commit interval to pass + var time = new Date(); + while(new Date() - time < 2000); + //Crash the server and make sure the last doc was written + restartServer(); + T(db.open("7") != null); + }); }; diff --git a/share/www/script/test/design_docs.js b/share/www/script/test/design_docs.js index 9318d2bc..f2f63841 100644 --- a/share/www/script/test/design_docs.js +++ b/share/www/script/test/design_docs.js @@ -13,139 +13,309 @@ couchTests.design_docs = function(debug) { var db = new CouchDB("test_suite_db", {"X-Couch-Full-Commit":"false"}); var db2 = new CouchDB("test_suite_db_a", {"X-Couch-Full-Commit":"false"}); + + if (debug) debugger; + db.deleteDb(); db.createDb(); db2.deleteDb(); db2.createDb(); - if (debug) debugger; - run_on_modified_server( - [{section: "query_server_config", + var server_config = [ + { + section: "query_server_config", key: "reduce_limit", - value: "false"}], -function() { + value: "false" + } + ]; - var numDocs = 500; + var testFun = function() { + var numDocs = 500; - function makebigstring(power) { - var str = "a"; - while(power-- > 0) { - str = str + str; + function makebigstring(power) { + var str = "a"; + while(power-- > 0) { + str = str + str; + } + return str; } - return str; - } - - var designDoc = { - _id:"_design/test", // turn off couch.js id escaping? - language: "javascript", - views: { - all_docs_twice: {map: "function(doc) { emit(doc.integer, null); emit(doc.integer, null) }"}, - no_docs: {map: "function(doc) {}"}, - single_doc: {map: "function(doc) { if (doc._id == \"1\") { emit(1, null) }}"}, - summate: {map:"function (doc) {emit(doc.integer, doc.integer)};", - reduce:"function (keys, values) { return sum(values); };"}, - summate2: {map:"function (doc) {emit(doc.integer, doc.integer)};", - reduce:"function (keys, values) { return sum(values); };"}, - huge_src_and_results: {map: "function(doc) { if (doc._id == \"1\") { emit(\"" + makebigstring(16) + "\", null) }}", - reduce:"function (keys, values) { return \"" + makebigstring(16) + "\"; };"} - }, - shows: { - simple: "function() {return 'ok'};" - } - } - var xhr = CouchDB.request("PUT", "/test_suite_db_a/_design/test", {body: JSON.stringify(designDoc)}); - var resp = JSON.parse(xhr.responseText); - - TEquals(resp.rev, db.save(designDoc).rev); - - // test that editing a show fun on the ddoc results in a change in output - var xhr = CouchDB.request("GET", "/test_suite_db/_design/test/_show/simple"); - T(xhr.status == 200); - TEquals(xhr.responseText, "ok"); - - designDoc.shows.simple = "function() {return 'ko'};" - T(db.save(designDoc).ok); - - var xhr = CouchDB.request("GET", "/test_suite_db/_design/test/_show/simple"); - T(xhr.status == 200); - TEquals(xhr.responseText, "ko"); - - var xhr = CouchDB.request("GET", "/test_suite_db_a/_design/test/_show/simple?cache=buster"); - T(xhr.status == 200); - TEquals("ok", xhr.responseText, 'query server used wrong ddoc'); - - // test that we get design doc info back - var dinfo = db.designInfo("_design/test"); - TEquals("test", dinfo.name); - var vinfo = dinfo.view_index; - TEquals(51, vinfo.disk_size); - TEquals(false, vinfo.compact_running); - TEquals("3f88e53b303e2342e49a66c538c30679", vinfo.signature); - - db.bulkSave(makeDocs(1, numDocs + 1)); - - // test that the _all_docs view returns correctly with keys - var results = db.allDocs({startkey:"_design", endkey:"_design0"}); - T(results.rows.length == 1); - - for (var loop = 0; loop < 2; loop++) { - var rows = db.view("test/all_docs_twice").rows; - for (var i = 0; i < numDocs; i++) { - T(rows[2*i].key == i+1); - T(rows[(2*i)+1].key == i+1); - } - T(db.view("test/no_docs").total_rows == 0) - T(db.view("test/single_doc").total_rows == 1) + + var designDoc = { + _id: "_design/test", + language: "javascript", + whatever : { + stringzone : "exports.string = 'plankton';", + commonjs : { + whynot : "exports.test = require('../stringzone'); " + + "exports.foo = require('whatever/stringzone');", + upper : "exports.testing = require('./whynot').test.string.toUpperCase()+" + + "module.id+require('./whynot').foo.string" + } + }, + views: { + all_docs_twice: { + map: + (function(doc) { + emit(doc.integer, null); + emit(doc.integer, null); + }).toString() + }, + no_docs: { + map: + (function(doc) { + }).toString() + }, + single_doc: { + map: + (function(doc) { + if (doc._id === "1") { + emit(1, null); + } + }).toString() + }, + summate: { + map: + (function(doc) { + emit(doc.integer, doc.integer); + }).toString(), + reduce: + (function(keys, values) { + return sum(values); + }).toString() + }, + summate2: { + map: + (function(doc) { + emit(doc.integer, doc.integer); + }).toString(), + reduce: + (function(keys, values) { + return sum(values); + }).toString() + }, + huge_src_and_results: { + map: + (function(doc) { + if (doc._id === "1") { + emit(makebigstring(16), null); + } + }).toString(), + reduce: + (function(keys, values) { + return makebigstring(16); + }).toString() + }, + lib : { + baz : "exports.baz = 'bam';", + foo : { + foo : "exports.foo = 'bar';", + boom : "exports.boom = 'ok';", + zoom : "exports.zoom = 'yeah';" + } + }, + commonjs : { + map : + (function(doc) { + emit(null, require('views/lib/foo/boom').boom); + }).toString() + } + }, + shows: { + simple: + (function() { + return 'ok'; + }).toString(), + requirey: + (function() { + var lib = require('whatever/commonjs/upper'); + return lib.testing; + }).toString(), + circular: + (function() { + var lib = require('whatever/commonjs/upper'); + return JSON.stringify(this); + }).toString() + } + }; // designDoc + + var xhr = CouchDB.request( + "PUT", "/test_suite_db_a/_design/test", {body: JSON.stringify(designDoc)} + ); + var resp = JSON.parse(xhr.responseText); + + TEquals(resp.rev, db.save(designDoc).rev); + + // test that editing a show fun on the ddoc results in a change in output + xhr = CouchDB.request("GET", "/test_suite_db/_design/test/_show/simple"); + T(xhr.status == 200); + TEquals(xhr.responseText, "ok"); + + designDoc.shows.simple = (function() { + return 'ko'; + }).toString(); + T(db.save(designDoc).ok); + + xhr = CouchDB.request("GET", "/test_suite_db/_design/test/_show/simple"); + T(xhr.status == 200); + TEquals(xhr.responseText, "ko"); + + xhr = CouchDB.request( + "GET", "/test_suite_db_a/_design/test/_show/simple?cache=buster" + ); + T(xhr.status == 200); + TEquals("ok", xhr.responseText, 'query server used wrong ddoc'); + + // test commonjs require + xhr = CouchDB.request("GET", "/test_suite_db/_design/test/_show/requirey"); + T(xhr.status == 200); + TEquals("PLANKTONwhatever/commonjs/upperplankton", xhr.responseText); + + xhr = CouchDB.request("GET", "/test_suite_db/_design/test/_show/circular"); + T(xhr.status == 200); + TEquals("javascript", JSON.parse(xhr.responseText).language); + + var prev_view_sig = db.designInfo("_design/test").view_index.signature; + var prev_view_size = db.designInfo("_design/test").view_index.disk_size; + + db.bulkSave(makeDocs(1, numDocs + 1)); T(db.ensureFullCommit().ok); - restartServer(); - }; - - // test when language not specified, Javascript is implied - var designDoc2 = { - _id:"_design/test2", - // language: "javascript", - views: { - single_doc: {map: "function(doc) { if (doc._id == \"1\") { emit(1, null) }}"} - } - }; - T(db.save(designDoc2).ok); - T(db.view("test2/single_doc").total_rows == 1); + // test that we get correct design doc info back, + // and also that GET /db/_design/test/_info + // hasn't triggered an update of the views + db.view("test/summate", {stale: "ok"}); // make sure view group's open + for (var i = 0; i < 2; i++) { + var dinfo = db.designInfo("_design/test"); + TEquals("test", dinfo.name); + var vinfo = dinfo.view_index; + TEquals(prev_view_size, vinfo.disk_size, "view group disk size didn't change"); + TEquals(false, vinfo.compact_running); + TEquals(prev_view_sig, vinfo.signature, 'ddoc sig'); + // wait some time (there were issues where an update + // of the views had been triggered in the background) + var start = new Date().getTime(); + while (new Date().getTime() < start + 2000); + TEquals(0, db.view("test/all_docs_twice", {stale: "ok"}).total_rows, 'view info'); + TEquals(0, db.view("test/single_doc", {stale: "ok"}).total_rows, 'view info'); + TEquals(0, db.view("test/summate", {stale: "ok"}).rows.length, 'view info'); + T(db.ensureFullCommit().ok); + restartServer(); + }; + + db.bulkSave(makeDocs(numDocs + 1, numDocs * 2 + 1)); + T(db.ensureFullCommit().ok); + + // open view group + db.view("test/summate", {stale: "ok"}); + // wait so the views can get initialized + var start = new Date().getTime(); + while (new Date().getTime() < start + 2000); + + // test that POST /db/_view_cleanup + // doesn't trigger an update of the views + var len1 = db.view("test/all_docs_twice", {stale: "ok"}).total_rows; + var len2 = db.view("test/single_doc", {stale: "ok"}).total_rows; + var len3 = db.view("test/summate", {stale: "ok"}).rows.length; + for (i = 0; i < 2; i++) { + T(db.viewCleanup().ok); + // wait some time (there were issues where an update + // of the views had been triggered in the background) + start = new Date().getTime(); + while (new Date().getTime() < start + 2000); + TEquals(len1, db.view("test/all_docs_twice", {stale: "ok"}).total_rows, 'view cleanup'); + TEquals(len2, db.view("test/single_doc", {stale: "ok"}).total_rows, 'view cleanup'); + TEquals(len3, db.view("test/summate", {stale: "ok"}).rows.length, 'view cleanup'); + T(db.ensureFullCommit().ok); + restartServer(); + // we'll test whether the view group stays closed + // and the views stay uninitialized (they should!) + len1 = len2 = len3 = 0; + }; + + // test commonjs in map functions + resp = db.view("test/commonjs", {limit:1}); + T(resp.rows[0].value == 'ok'); + + // test that the _all_docs view returns correctly with keys + var results = db.allDocs({startkey:"_design", endkey:"_design0"}); + T(results.rows.length == 1); + + for (i = 0; i < 2; i++) { + var rows = db.view("test/all_docs_twice").rows; + for (var j = 0; j < numDocs; j++) { + T(rows[2 * j].key == (j + 1)); + T(rows[(2 * j) + 1].key == (j + 1)); + }; + T(db.view("test/no_docs").total_rows == 0); + T(db.view("test/single_doc").total_rows == 1); + T(db.ensureFullCommit().ok); + restartServer(); + }; - var summate = function(N) {return (N+1)*N/2;}; - var result = db.view("test/summate"); - T(result.rows[0].value == summate(numDocs)); + // test when language not specified, Javascript is implied + var designDoc2 = { + _id: "_design/test2", + // language: "javascript", + views: { + single_doc: { + map: + (function(doc) { + if (doc._id === "1") { + emit(1, null); + } + }).toString() + } + } + }; - result = db.view("test/summate", {startkey:4,endkey:4}); - T(result.rows[0].value == 4); + T(db.save(designDoc2).ok); + T(db.view("test2/single_doc").total_rows == 1); - result = db.view("test/summate", {startkey:4,endkey:5}); - T(result.rows[0].value == 9); + var summate = function(N) { + return (N + 1) * (N / 2); + }; + var result = db.view("test/summate"); + T(result.rows[0].value == summate(numDocs * 2)); - result = db.view("test/summate", {startkey:4,endkey:6}); - T(result.rows[0].value == 15); + result = db.view("test/summate", {startkey: 4, endkey: 4}); + T(result.rows[0].value == 4); - // Verify that a shared index (view def is an exact copy of "summate") - // does not confuse the reduce stage - result = db.view("test/summate2", {startkey:4,endkey:6}); - T(result.rows[0].value == 15); + result = db.view("test/summate", {startkey: 4, endkey: 5}); + T(result.rows[0].value == 9); - for(var i=1; i<numDocs/2; i+=30) { - result = db.view("test/summate", {startkey:i,endkey:numDocs-i}); - T(result.rows[0].value == summate(numDocs-i) - summate(i-1)); - } + result = db.view("test/summate", {startkey: 4, endkey: 6}); + T(result.rows[0].value == 15); - T(db.deleteDoc(designDoc).ok); - T(db.open(designDoc._id) == null); - T(db.view("test/no_docs") == null); + // test start_key and end_key aliases + result = db.view("test/summate", {start_key: 4, end_key: 6}); + T(result.rows[0].value == 15); - T(db.ensureFullCommit().ok); - restartServer(); - T(db.open(designDoc._id) == null); - T(db.view("test/no_docs") == null); + // Verify that a shared index (view def is an exact copy of "summate") + // does not confuse the reduce stage + result = db.view("test/summate2", {startkey: 4, endkey: 6}); + T(result.rows[0].value == 15); - // trigger ddoc cleanup - T(db.viewCleanup().ok); + for(i = 1; i < (numDocs / 2); i += 30) { + result = db.view("test/summate", {startkey: i, endkey: (numDocs - i)}); + T(result.rows[0].value == summate(numDocs - i) - summate(i - 1)); + } + + T(db.deleteDoc(designDoc).ok); + T(db.open(designDoc._id) == null); + T(db.view("test/no_docs") == null); + + T(db.ensureFullCommit().ok); + restartServer(); + T(db.open(designDoc._id) == null); + T(db.view("test/no_docs") == null); -}); + // trigger ddoc cleanup + T(db.viewCleanup().ok); + }; // enf of testFun + + run_on_modified_server(server_config, testFun); + + // cleanup + db.deleteDb(); + db2.deleteDb(); }; diff --git a/share/www/script/test/erlang_views.js b/share/www/script/test/erlang_views.js index e9e0363f..7eddab40 100644 --- a/share/www/script/test/erlang_views.js +++ b/share/www/script/test/erlang_views.js @@ -29,8 +29,8 @@ couchTests.erlang_views = function(debug) { T(db.save(doc).ok); var mfun = 'fun({Doc}) -> ' + - ' K = proplists:get_value(<<"integer">>, Doc, null), ' + - ' V = proplists:get_value(<<"string">>, Doc, null), ' + + ' K = couch_util:get_value(<<"integer">>, Doc, null), ' + + ' V = couch_util:get_value(<<"string">>, Doc, null), ' + ' Emit(K, V) ' + 'end.'; @@ -44,7 +44,7 @@ couchTests.erlang_views = function(debug) { // check simple reduction - another doc with same key. var doc = {_id: "2", integer: 1, string: "str2"}; T(db.save(doc).ok); - rfun = "fun(Keys, Values, ReReduce) -> length(Values) end." + rfun = "fun(Keys, Values, ReReduce) -> length(Values) end."; results = db.query(mfun, rfun, null, null, "erlang"); T(results.rows[0].value == 2); @@ -55,9 +55,9 @@ couchTests.erlang_views = function(debug) { shows: { simple: 'fun(Doc, {Req}) -> ' + - ' {Info} = proplists:get_value(<<"info">>, Req, {[]}), ' + - ' Purged = proplists:get_value(<<"purge_seq">>, Info, -1), ' + - ' Verb = proplists:get_value(<<"method">>, Req, <<"not_get">>), ' + + ' {Info} = couch_util:get_value(<<"info">>, Req, {[]}), ' + + ' Purged = couch_util:get_value(<<"purge_seq">>, Info, -1), ' + + ' Verb = couch_util:get_value(<<"method">>, Req, <<"not_get">>), ' + ' R = list_to_binary(io_lib:format("~b - ~s", [Purged, Verb])), ' + ' {[{<<"code">>, 200}, {<<"headers">>, {[]}}, {<<"body">>, R}]} ' + 'end.' @@ -67,7 +67,7 @@ couchTests.erlang_views = function(debug) { 'fun(Head, {Req}) -> ' + ' Send(<<"head">>), ' + ' Fun = fun({Row}, _) -> ' + - ' Val = proplists:get_value(<<"value">>, Row, -1), ' + + ' Val = couch_util:get_value(<<"value">>, Row, -1), ' + ' Send(list_to_binary(integer_to_list(Val))), ' + ' {ok, nil} ' + ' end, ' + @@ -117,10 +117,10 @@ couchTests.erlang_views = function(debug) { T(db.bulkSave(docs).length, 250, "Saved big doc set."); var mfun = 'fun({Doc}) -> ' + - 'Words = proplists:get_value(<<"words">>, Doc), ' + + 'Words = couch_util:get_value(<<"words">>, Doc), ' + 'lists:foreach(fun({Word}) -> ' + - 'WordString = proplists:get_value(<<"word">>, Word), ' + - 'Count = proplists:get_value(<<"count">>, Word), ' + + 'WordString = couch_util:get_value(<<"word">>, Word), ' + + 'Count = couch_util:get_value(<<"count">>, Word), ' + 'Emit(WordString , Count) ' + 'end, Words) ' + 'end.'; diff --git a/share/www/script/test/etags_views.js b/share/www/script/test/etags_views.js index a12734f8..f556d6ac 100644 --- a/share/www/script/test/etags_views.js +++ b/share/www/script/test/etags_views.js @@ -11,23 +11,34 @@ // the License. couchTests.etags_views = function(debug) { - var db = new CouchDB("test_suite_db", {"X-Couch-Full-Commit":"false"}); + var db = new CouchDB("test_suite_db", {"X-Couch-Full-Commit":"true"}); db.deleteDb(); db.createDb(); if (debug) debugger; var designDoc = { - _id:"_design/etags", + _id: "_design/etags", language: "javascript", views : { + fooView: { + map: stringFun(function(doc) { + if (doc.foo) { + emit("bar", 1); + } + }), + }, basicView : { map : stringFun(function(doc) { - emit(doc.integer, doc.string); + if(doc.integer && doc.string) { + emit(doc.integer, doc.string); + } }) }, withReduce : { map : stringFun(function(doc) { - emit(doc.integer, doc.string); + if(doc.integer && doc.string) { + emit(doc.integer, doc.string); + } }), reduce : stringFun(function(keys, values, rereduce) { if (rereduce) { @@ -38,11 +49,11 @@ couchTests.etags_views = function(debug) { }) } } - } + }; T(db.save(designDoc).ok); + db.bulkSave(makeDocs(0, 10)); + var xhr; - var docs = makeDocs(0, 10); - db.bulkSave(docs); // verify get w/Etag on map view xhr = CouchDB.request("GET", "/test_suite_db/_design/etags/_view/basicView"); @@ -52,17 +63,92 @@ couchTests.etags_views = function(debug) { headers: {"if-none-match": etag} }); T(xhr.status == 304); - // TODO GET with keys (when that is available) + + // verify ETag doesn't change when an update + // doesn't change the view group's index + T(db.save({"_id":"doc1", "foo":"bar"}).ok); + xhr = CouchDB.request("GET", "/test_suite_db/_design/etags/_view/basicView"); + var etag1 = xhr.getResponseHeader("etag"); + T(etag1 == etag); + + // Verify that purges affect etags + xhr = CouchDB.request("GET", "/test_suite_db/_design/etags/_view/fooView"); + var foo_etag = xhr.getResponseHeader("etag"); + var doc1 = db.open("doc1"); + xhr = CouchDB.request("POST", "/test_suite_db/_purge", { + body: JSON.stringify({"doc1":[doc1._rev]}) + }); + xhr = CouchDB.request("GET", "/test_suite_db/_design/etags/_view/fooView"); + var etag1 = xhr.getResponseHeader("etag"); + T(etag1 != foo_etag); + + // Test that _purge didn't affect the other view etags. + xhr = CouchDB.request("GET", "/test_suite_db/_design/etags/_view/basicView"); + var etag1 = xhr.getResponseHeader("etag"); + T(etag1 == etag); + + // verify different views in the same view group may have different ETags + xhr = CouchDB.request("GET", "/test_suite_db/_design/etags/_view/fooView"); + var etag1 = xhr.getResponseHeader("etag"); + xhr = CouchDB.request("GET", "/test_suite_db/_design/etags/_view/basicView"); + var etag2 = xhr.getResponseHeader("etag"); + T(etag1 != etag2); + + // verify ETag changes when an update changes the view group's index. + db.bulkSave(makeDocs(10, 20)); + xhr = CouchDB.request("GET", "/test_suite_db/_design/etags/_view/basicView"); + var etag1 = xhr.getResponseHeader("etag"); + T(etag1 != etag); + + // verify ETag is the same after a restart + restartServer(); + xhr = CouchDB.request("GET", "/test_suite_db/_design/etags/_view/basicView"); + var etag2 = xhr.getResponseHeader("etag"); + T(etag1 == etag2); // reduce view xhr = CouchDB.request("GET", "/test_suite_db/_design/etags/_view/withReduce"); T(xhr.status == 200); var etag = xhr.getResponseHeader("etag"); - xhr = CouchDB.request("GET", "/test_suite_db/_design/etags/_view/withReduce", { + xhr = CouchDB.request("GET", "/test_suite_db/_design/etags/_view/withReduce",{ headers: {"if-none-match": etag} }); T(xhr.status == 304); + // verify ETag doesn't change when an update + // doesn't change the view group's index + T(db.save({"_id":"doc3", "foo":"bar"}).ok); + xhr = CouchDB.request("GET", "/test_suite_db/_design/etags/_view/withReduce"); + var etag1 = xhr.getResponseHeader("etag"); + T(etag1 == etag); + // purge + var doc3 = db.open("doc3"); + xhr = CouchDB.request("POST", "/test_suite_db/_purge", { + body: JSON.stringify({"doc3":[doc3._rev]}) + }); + xhr = CouchDB.request("GET", "/test_suite_db/_design/etags/_view/withReduce"); + var etag1 = xhr.getResponseHeader("etag"); + T(etag1 == etag); + + // verify different views in the same view group may have different ETags + xhr = CouchDB.request("GET", "/test_suite_db/_design/etags/_view/fooView"); + var etag1 = xhr.getResponseHeader("etag"); + xhr = CouchDB.request("GET", "/test_suite_db/_design/etags/_view/withReduce"); + var etag2 = xhr.getResponseHeader("etag"); + T(etag1 != etag2); + + // verify ETag changes when an update changes the view group's index + db.bulkSave(makeDocs(20, 30)); + xhr = CouchDB.request("GET", "/test_suite_db/_design/etags/_view/withReduce"); + var etag1 = xhr.getResponseHeader("etag"); + T(etag1 != etag); + + // verify ETag is the same after a restart + restartServer(); + xhr = CouchDB.request("GET", "/test_suite_db/_design/etags/_view/withReduce"); + var etag2 = xhr.getResponseHeader("etag"); + T(etag1 == etag2); + // confirm ETag changes with different POST bodies xhr = CouchDB.request("POST", "/test_suite_db/_design/etags/_view/basicView", {body: JSON.stringify({keys:[1]})} diff --git a/share/www/script/test/http.js b/share/www/script/test/http.js index 8a2e09b8..5f46af52 100644 --- a/share/www/script/test/http.js +++ b/share/www/script/test/http.js @@ -25,7 +25,7 @@ couchTests.http = function(debug) { var xhr = CouchDB.request("PUT", "/test_suite_db/test", {body: "{}"}); var host = CouchDB.host; - TEquals("http://" + host + "/test_suite_db/test", + TEquals(CouchDB.protocol + host + "/test_suite_db/test", xhr.getResponseHeader("Location"), "should include ip address"); @@ -34,7 +34,7 @@ couchTests.http = function(debug) { headers: {"X-Forwarded-Host": "mysite.com"} }); - TEquals("http://mysite.com/test_suite_db/test2", + TEquals(CouchDB.protocol + "mysite.com/test_suite_db/test2", xhr.getResponseHeader("Location"), "should include X-Forwarded-Host"); @@ -47,7 +47,7 @@ couchTests.http = function(debug) { body: "{}", headers: {"X-Host": "mysite2.com"} }); - TEquals("http://mysite2.com/test_suite_db/test3", + TEquals(CouchDB.protocol + "mysite2.com/test_suite_db/test3", xhr.getResponseHeader("Location"), "should include X-Host"); }); diff --git a/share/www/script/test/jsonp.js b/share/www/script/test/jsonp.js index dfd6a0df..9aba7189 100644 --- a/share/www/script/test/jsonp.js +++ b/share/www/script/test/jsonp.js @@ -32,38 +32,51 @@ couchTests.jsonp = function(debug) { db.deleteDb(); db.createDb(); if (debug) debugger; - + var doc = {_id:"0",a:0,b:0}; T(db.save(doc).ok); + + // callback param is ignored unless jsonp is configured + var xhr = CouchDB.request("GET", "/test_suite_db/0?callback=jsonp_not_configured"); + JSON.parse(xhr.responseText); - // Test unchunked callbacks. - var xhr = CouchDB.request("GET", "/test_suite_db/0?callback=jsonp_no_chunk"); - T(xhr.status == 200); - jsonp_flag = 0; - eval(xhr.responseText); - T(jsonp_flag == 1); - xhr = CouchDB.request("GET", "/test_suite_db/0?callback=foo\""); - T(xhr.status == 400); + run_on_modified_server( + [{section: "httpd", + key: "allow_jsonp", + value: "true"}], + function() { - // Test chunked responses - var doc = {_id:"1",a:1,b:1}; - T(db.save(doc).ok); + // Test unchunked callbacks. + var xhr = CouchDB.request("GET", "/test_suite_db/0?callback=jsonp_no_chunk"); + T(xhr.status == 200); + jsonp_flag = 0; + eval(xhr.responseText); + T(jsonp_flag == 1); + xhr = CouchDB.request("GET", "/test_suite_db/0?callback=foo\""); + T(xhr.status == 400); + + // Test chunked responses + var doc = {_id:"1",a:1,b:1}; + T(db.save(doc).ok); + + var designDoc = { + _id:"_design/test", + language: "javascript", + views: { + all_docs: {map: "function(doc) {if(doc.a) emit(null, doc.a);}"} + } + }; + T(db.save(designDoc).ok); + + var url = "/test_suite_db/_design/test/_view/all_docs?callback=jsonp_chunk"; + xhr = CouchDB.request("GET", url); + T(xhr.status == 200); + jsonp_flag = 0; + eval(xhr.responseText); + T(jsonp_flag == 1); + xhr = CouchDB.request("GET", url + "\'"); + T(xhr.status == 400); + }); - var designDoc = { - _id:"_design/test", - language: "javascript", - views: { - all_docs: {map: "function(doc) {if(doc.a) emit(null, doc.a);}"} - } - } - T(db.save(designDoc).ok); - var url = "/test_suite_db/_design/test/_view/all_docs?callback=jsonp_chunk"; - xhr = CouchDB.request("GET", url); - T(xhr.status == 200); - jsonp_flag = 0; - eval(xhr.responseText); - T(jsonp_flag == 1); - xhr = CouchDB.request("GET", url + "\'"); - T(xhr.status == 400); }; diff --git a/share/www/script/test/list_views.js b/share/www/script/test/list_views.js index 54214c2c..e98a212e 100644 --- a/share/www/script/test/list_views.js +++ b/share/www/script/test/list_views.js @@ -156,6 +156,9 @@ couchTests.list_views = function(debug) { var row = getRow(); send(row.doc.integer); return "tail"; + }), + secObj: stringFun(function(head, req) { + return toJSON(req.secObj); }) } }; @@ -178,7 +181,7 @@ couchTests.list_views = function(debug) { 'fun(Head, {Req}) -> ' + ' Send(<<"[">>), ' + ' Fun = fun({Row}, Sep) -> ' + - ' Val = proplists:get_value(<<"key">>, Row, 23), ' + + ' Val = couch_util:get_value(<<"key">>, Row, 23), ' + ' Send(list_to_binary(Sep ++ integer_to_list(Val))), ' + ' {ok, ","} ' + ' end, ' + @@ -201,6 +204,7 @@ couchTests.list_views = function(debug) { T(xhr.status == 200, "standard get should be 200"); T(/head0123456789tail/.test(xhr.responseText)); + // test that etags are available var etag = xhr.getResponseHeader("etag"); xhr = CouchDB.request("GET", "/test_suite_db/_design/lists/_list/basicBasic/basicView", { @@ -220,10 +224,13 @@ couchTests.list_views = function(debug) { T(etag1 != etag2, "POST to map _list generates key-depdendent ETags"); // test the richness of the arguments - xhr = CouchDB.request("GET", "/test_suite_db/_design/lists/_list/basicJSON/basicView"); + xhr = CouchDB.request("GET", "/test_suite_db/_design/lists/_list/basicJSON/basicView?update_seq=true"); T(xhr.status == 200, "standard get should be 200"); var resp = JSON.parse(xhr.responseText); - TEquals(resp.head, {total_rows:10, offset:0}); + TEquals(10, resp.head.total_rows); + TEquals(0, resp.head.offset); + TEquals(11, resp.head.update_seq); + T(resp.rows.length == 10); TEquals(resp.rows[0], {"id": "0","key": 0,"value": "0"}); @@ -320,6 +327,16 @@ couchTests.list_views = function(debug) { T(/FirstKey: 2/.test(xhr.responseText)); T(/LastKey: 7/.test(xhr.responseText)); + // multi-key fetch with GET + var xhr = CouchDB.request("GET", "/test_suite_db/_design/lists/_list/simpleForm/basicView" + + "?keys=[2,4,5,7]"); + + T(xhr.status == 200, "multi key"); + T(!(/Key: 1 /.test(xhr.responseText))); + T(/Key: 2/.test(xhr.responseText)); + T(/FirstKey: 2/.test(xhr.responseText)); + T(/LastKey: 7/.test(xhr.responseText)); + // no multi-key fetch allowed when group=false xhr = CouchDB.request("POST", "/test_suite_db/_design/lists/_list/simpleForm/withReduce?group=false", { body: '{"keys":[2,4,5,7]}' @@ -343,13 +360,21 @@ couchTests.list_views = function(debug) { // T(xhr.getResponseHeader("Content-Type") == "text/plain"); T(xhr.responseText.match(/^head 0 1 2 tail$/) && "basic stop"); - xhr = CouchDB.request("GET", "/test_suite_db/_design/lists/_list/stopIter2/basicView"); + xhr = CouchDB.request("GET", "/test_suite_db/_design/lists/_list/stopIter2/basicView", { + headers : { + "Accept" : "text/html" + } + }); T(xhr.responseText.match(/^head 0 1 2 tail$/) && "stop 2"); // aborting iteration with reduce var xhr = CouchDB.request("GET", "/test_suite_db/_design/lists/_list/stopIter/withReduce?group=true"); T(xhr.responseText.match(/^head 0 1 2 tail$/) && "reduce stop"); - xhr = CouchDB.request("GET", "/test_suite_db/_design/lists/_list/stopIter2/withReduce?group=true"); + xhr = CouchDB.request("GET", "/test_suite_db/_design/lists/_list/stopIter2/withReduce?group=true", { + headers : { + "Accept" : "text/html" + } + }); T(xhr.responseText.match(/^head 0 1 2 tail$/) && "reduce stop 2"); // with accept headers for HTML @@ -383,7 +408,7 @@ couchTests.list_views = function(debug) { T(/LastKey: 0/.test(xhr.responseText)); // Test we do multi-key requests on lists and views in separate docs. - var url = "/test_suite_db/_design/lists/_list/simpleForm/views/basicView" + var url = "/test_suite_db/_design/lists/_list/simpleForm/views/basicView"; xhr = CouchDB.request("POST", url, { body: '{"keys":[-2,-4,-5,-7]}' }); @@ -394,6 +419,12 @@ couchTests.list_views = function(debug) { T(/FirstKey: -2/.test(xhr.responseText)); T(/LastKey: -7/.test(xhr.responseText)); + // Test if secObj is available + var xhr = CouchDB.request("GET", "/test_suite_db/_design/lists/_list/secObj/basicView"); + T(xhr.status == 200, "standard get should be 200"); + var resp = JSON.parse(xhr.responseText); + T(typeof(resp) == "object"); + var erlViewTest = function() { T(db.save(erlListDoc).ok); var url = "/test_suite_db/_design/erlang/_list/simple/views/basicView" + @@ -408,6 +439,8 @@ couchTests.list_views = function(debug) { } }; + + run_on_modified_server([{ section: "native_query_servers", key: "erlang", diff --git a/share/www/script/test/method_override.js b/share/www/script/test/method_override.js new file mode 100644 index 00000000..0bb4c61f --- /dev/null +++ b/share/www/script/test/method_override.js @@ -0,0 +1,40 @@ +// 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. + +// Allow broken HTTP clients to fake a full method vocabulary with an X-HTTP-METHOD-OVERRIDE header +couchTests.method_override = function(debug) { + var result = JSON.parse(CouchDB.request("GET", "/").responseText); + T(result.couchdb == "Welcome"); + + var db = new CouchDB("test_suite_db", {"X-Couch-Full-Commit":"false"}); + db.deleteDb(); + + db.createDb(); + + var doc = {bob : "connie"}; + xhr = CouchDB.request("POST", "/test_suite_db/fnord", {body: JSON.stringify(doc), headers:{"X-HTTP-Method-Override" : "PUT"}}); + T(xhr.status == 201); + + doc = db.open("fnord"); + T(doc.bob == "connie"); + + xhr = CouchDB.request("POST", "/test_suite_db/fnord?rev=" + doc._rev, {headers:{"X-HTTP-Method-Override" : "DELETE"}}); + T(xhr.status == 200); + + xhr = CouchDB.request("GET", "/test_suite_db/fnord2", {body: JSON.stringify(doc), headers:{"X-HTTP-Method-Override" : "PUT"}}); + // Method Override is ignored when original Method isn't POST + T(xhr.status == 404); + + doc = db.open("fnord"); + T(doc == null); + +}; diff --git a/share/www/script/test/oauth.js b/share/www/script/test/oauth.js index d55d13e8..82ebe8a4 100644 --- a/share/www/script/test/oauth.js +++ b/share/www/script/test/oauth.js @@ -71,7 +71,7 @@ couchTests.oauth = function(debug) { var host = CouchDB.host; var dbPair = { source: { - url: "http://" + host + "/test_suite_db_a", + url: CouchDB.protocol + host + "/test_suite_db_a", auth: { oauth: { consumer_key: "key", @@ -82,7 +82,7 @@ couchTests.oauth = function(debug) { } }, target: { - url: "http://" + host + "/test_suite_db_b", + url: CouchDB.protocol + host + "/test_suite_db_b", headers: {"Authorization": adminBasicAuthHeaderValue()} } }; @@ -90,16 +90,26 @@ couchTests.oauth = function(debug) { // this function will be called on the modified server var testFun = function () { try { - CouchDB.request("PUT", "http://" + host + "/_config/admins/testadmin", { + CouchDB.request("PUT", CouchDB.protocol + host + "/_config/admins/testadmin", { headers: {"X-Couch-Persist": "false"}, body: JSON.stringify(testadminPassword) }); - - CouchDB.request("GET", "/_sleep?time=50"); + var i = 0; + waitForSuccess(function() { + //loop until the couch server has processed the password + i += 1; + var xhr = CouchDB.request("GET", CouchDB.protocol + host + "/_config/admins/testadmin?foo="+i,{ + headers: { + "Authorization": adminBasicAuthHeaderValue() + }}); + if (xhr.responseText.indexOf("\"-hashed-") != 0) { + throw("still waiting"); + } + }, "wait-for-admin"); CouchDB.newUuids(2); // so we have one to make the salt - CouchDB.request("PUT", "http://" + host + "/_config/couch_httpd_auth/require_valid_user", { + CouchDB.request("PUT", CouchDB.protocol + host + "/_config/couch_httpd_auth/require_valid_user", { headers: { "X-Couch-Persist": "false", "Authorization": adminBasicAuthHeaderValue() @@ -116,7 +126,7 @@ couchTests.oauth = function(debug) { // Create a user var jasonUserDoc = CouchDB.prepareUserDoc({ - username: "jason", + name: "jason", roles: ["test"] }, "testpassword"); T(usersDb.save(jasonUserDoc).ok); @@ -147,11 +157,11 @@ couchTests.oauth = function(debug) { }; // Get request token via Authorization header - xhr = oauthRequest("GET", "http://" + host + "/_oauth/request_token", message, accessor); + xhr = oauthRequest("GET", CouchDB.protocol + host + "/_oauth/request_token", message, accessor); T(xhr.status == expectedCode); // GET request token via query parameters - xhr = oauthRequest("GET", "http://" + host + "/_oauth/request_token", message, accessor); + xhr = oauthRequest("GET", CouchDB.protocol + host + "/_oauth/request_token", message, accessor); T(xhr.status == expectedCode); responseMessage = OAuth.decodeForm(xhr.responseText); @@ -161,7 +171,7 @@ couchTests.oauth = function(debug) { //xhr = CouchDB.request("GET", authorization_url + '?oauth_token=' + responseMessage.oauth_token); //T(xhr.status == expectedCode); - xhr = oauthRequest("GET", "http://" + host + "/_session", message, accessor); + xhr = oauthRequest("GET", CouchDB.protocol + host + "/_session", message, accessor); T(xhr.status == expectedCode); if (xhr.status == expectedCode == 200) { data = JSON.parse(xhr.responseText); @@ -169,11 +179,11 @@ couchTests.oauth = function(debug) { T(data.roles[0] == "test"); } - xhr = oauthRequest("GET", "http://" + host + "/_session?foo=bar", message, accessor); + xhr = oauthRequest("GET", CouchDB.protocol + host + "/_session?foo=bar", message, accessor); T(xhr.status == expectedCode); // Test HEAD method - xhr = oauthRequest("HEAD", "http://" + host + "/_session?foo=bar", message, accessor); + xhr = oauthRequest("HEAD", CouchDB.protocol + host + "/_session?foo=bar", message, accessor); T(xhr.status == expectedCode); // Replication @@ -197,7 +207,7 @@ couchTests.oauth = function(debug) { oauth_version: "1.0" } }; - xhr = oauthRequest("GET", "http://" + host + "/_session?foo=bar", message, adminAccessor); + xhr = oauthRequest("GET", CouchDB.protocol + host + "/_session?foo=bar", message, adminAccessor); if (xhr.status == expectedCode == 200) { data = JSON.parse(xhr.responseText); T(data.name == "testadmin"); @@ -206,13 +216,13 @@ couchTests.oauth = function(debug) { // Test when the user's token doesn't exist. message.parameters.oauth_token = "not a token!"; - xhr = oauthRequest("GET", "http://" + host + "/_session?foo=bar", + xhr = oauthRequest("GET", CouchDB.protocol + host + "/_session?foo=bar", message, adminAccessor); T(xhr.status == 400, "Request should be invalid."); } } } finally { - var xhr = CouchDB.request("PUT", "http://" + host + "/_config/couch_httpd_auth/require_valid_user", { + var xhr = CouchDB.request("PUT", CouchDB.protocol + host + "/_config/couch_httpd_auth/require_valid_user", { headers: { "Authorization": adminBasicAuthHeaderValue(), "X-Couch-Persist": "false" @@ -221,7 +231,7 @@ couchTests.oauth = function(debug) { }); T(xhr.status == 200); - var xhr = CouchDB.request("DELETE", "http://" + host + "/_config/admins/testadmin", { + var xhr = CouchDB.request("DELETE", CouchDB.protocol + host + "/_config/admins/testadmin", { headers: { "Authorization": adminBasicAuthHeaderValue(), "X-Couch-Persist": "false" diff --git a/share/www/script/test/proxyauth.js b/share/www/script/test/proxyauth.js new file mode 100644 index 00000000..40af0089 --- /dev/null +++ b/share/www/script/test/proxyauth.js @@ -0,0 +1,130 @@ +// 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.proxyauth = function(debug) { + // this test proxy authentification handler + + var usersDb = new CouchDB("test_suite_users", {"X-Couch-Full-Commit":"false"}); + var db = new CouchDB("test_suite_db", {"X-Couch-Full-Commit":"false"}); + + if (debug) debugger; + + // Simple secret key generator + function generateSecret(length) { + var tab = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + var secret = ''; + for (var i=0; i<length; i++) { + secret += tab.charAt(Math.floor(Math.random() * 64)); + } + return secret; + } + + var secret = generateSecret(64); + + function TestFun() { + usersDb.deleteDb(); + usersDb.createDb(); + db.deleteDb(); + db.createDb(); + + var benoitcUserDoc = CouchDB.prepareUserDoc({ + name: "benoitc@apache.org" + }, "test"); + T(usersDb.save(benoitcUserDoc).ok); + + T(CouchDB.session().userCtx.name == null); + + // test that you can use basic auth aginst the users db + var s = CouchDB.session({ + headers : { + "Authorization" : "Basic YmVub2l0Y0BhcGFjaGUub3JnOnRlc3Q=" + } + }); + T(s.userCtx.name == "benoitc@apache.org"); + T(s.info.authenticated == "default"); + + CouchDB.logout(); + + var headers = { + "X-Auth-CouchDB-UserName": "benoitc@apache.org", + "X-Auth-CouchDB-Roles": "test", + "X-Auth-CouchDB-Token": hex_hmac_sha1(secret, "benoitc@apache.org") + }; + + var designDoc = { + _id:"_design/test", + language: "javascript", + + shows: { + "welcome": stringFun(function(doc,req) { + return "Welcome " + req.userCtx["name"]; + }), + "role": stringFun(function(doc, req) { + return req.userCtx['roles'][0]; + }) + } + }; + + db.save(designDoc); + + var req = CouchDB.request("GET", "/test_suite_db/_design/test/_show/welcome", + {headers: headers}); + T(req.responseText == "Welcome benoitc@apache.org"); + + req = CouchDB.request("GET", "/test_suite_db/_design/test/_show/role", + {headers: headers}); + T(req.responseText == "test"); + + var xhr = CouchDB.request("PUT", "/_config/couch_httpd_auth/proxy_use_secret",{ + body : JSON.stringify("true"), + headers: {"X-Couch-Persist": "false"} + }); + T(xhr.status == 200); + + req = CouchDB.request("GET", "/test_suite_db/_design/test/_show/welcome", + {headers: headers}); + T(req.responseText == "Welcome benoitc@apache.org"); + + req = CouchDB.request("GET", "/test_suite_db/_design/test/_show/role", + {headers: headers}); + T(req.responseText == "test"); + + } + + run_on_modified_server( + [{section: "httpd", + key: "authentication_handlers", + value:"{couch_httpd_auth, proxy_authentification_handler}, {couch_httpd_auth, default_authentication_handler}"}, + {section: "couch_httpd_auth", + key: "authentication_db", + value: "test_suite_users"}, + {section: "couch_httpd_auth", + key: "secret", + value: secret}, + {section: "couch_httpd_auth", + key: "x_auth_username", + value: "X-Auth-CouchDB-UserName"}, + {section: "couch_httpd_auth", + key: "x_auth_roles", + value: "X-Auth-CouchDB-Roles"}, + {section: "couch_httpd_auth", + key: "x_auth_token", + value: "X-Auth-CouchDB-Token"}, + {section: "couch_httpd_auth", + key: "proxy_use_secret", + value: "false"}], + TestFun + ); + +};
\ No newline at end of file diff --git a/share/www/script/test/purge.js b/share/www/script/test/purge.js index ca68b363..af72ea4f 100644 --- a/share/www/script/test/purge.js +++ b/share/www/script/test/purge.js @@ -30,7 +30,7 @@ couchTests.purge = function(debug) { all_docs_twice: {map: "function(doc) { emit(doc.integer, null); emit(doc.integer, null) }"}, single_doc: {map: "function(doc) { if (doc._id == \"1\") { emit(1, null) }}"} } - } + }; T(db.save(designDoc).ok); @@ -50,17 +50,19 @@ couchTests.purge = function(debug) { // purge the documents var xhr = CouchDB.request("POST", "/test_suite_db/_purge", { - body: JSON.stringify({"1":[doc1._rev], "2":[doc2._rev]}), + body: JSON.stringify({"1":[doc1._rev], "2":[doc2._rev]}) }); T(xhr.status == 200); + var result = JSON.parse(xhr.responseText); var newInfo = db.info(); + // purging increments the update sequence T(info.update_seq+1 == newInfo.update_seq); // and it increments the purge_seq T(info.purge_seq+1 == newInfo.purge_seq); + T(result.purge_seq == newInfo.purge_seq); - var result = JSON.parse(xhr.responseText); T(result.purged["1"][0] == doc1._rev); T(result.purged["2"][0] == doc2._rev); @@ -81,16 +83,18 @@ couchTests.purge = function(debug) { var doc4 = db.open("4"); xhr = CouchDB.request("POST", "/test_suite_db/_purge", { - body: JSON.stringify({"3":[doc3._rev]}), + body: JSON.stringify({"3":[doc3._rev]}) }); T(xhr.status == 200); xhr = CouchDB.request("POST", "/test_suite_db/_purge", { - body: JSON.stringify({"4":[doc4._rev]}), + body: JSON.stringify({"4":[doc4._rev]}) }); T(xhr.status == 200); + result = JSON.parse(xhr.responseText); + T(result.purge_seq == db.info().purge_seq); var rows = db.view("test/all_docs_twice").rows; for (var i = 4; i < numDocs; i++) { diff --git a/share/www/script/test/reader_acl.js b/share/www/script/test/reader_acl.js new file mode 100644 index 00000000..cc249ea4 --- /dev/null +++ b/share/www/script/test/reader_acl.js @@ -0,0 +1,198 @@ +// 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.reader_acl = function(debug) { + // this tests read access control + + var usersDb = new CouchDB("test_suite_users", {"X-Couch-Full-Commit":"false"}); + var secretDb = new CouchDB("test_suite_db", {"X-Couch-Full-Commit":"false"}); + function testFun() { + try { + usersDb.deleteDb(); + usersDb.createDb(); + secretDb.deleteDb(); + secretDb.createDb(); + + // create a user with top-secret-clearance + var jchrisUserDoc = CouchDB.prepareUserDoc({ + name: "jchris@apache.org", + roles : ["top-secret"] + }, "funnybone"); + T(usersDb.save(jchrisUserDoc).ok); + usersDb.ensureFullCommit(); + + T(CouchDB.session().userCtx.name == null); + + // set secret db to be read controlled + T(secretDb.save({_id:"baz",foo:"bar"}).ok); + T(secretDb.open("baz").foo == "bar"); + + T(secretDb.setSecObj({ + "readers" : { + roles : ["super-secret-club"], + names : ["joe","barb"] + } + }).ok); + } finally { + CouchDB.logout(); + } + } + + // split into 2 funs so we can test restart behavior + function testFun2() { + try { + // can't read it as jchris b/c he's missing the needed role + T(CouchDB.login("jchris@apache.org", "funnybone").ok); + T(CouchDB.session().userCtx.name == "jchris@apache.org"); + + try { + secretDb.open("baz"); + T(false && "can't open a doc from a secret db") ; + } catch(e) { + T(true) + } + + CouchDB.logout(); + + // make anyone with the top-secret role an admin + // db admins are automatically readers + T(secretDb.setSecObj({ + "admins" : { + roles : ["top-secret"], + names : [] + }, + "readers" : { + roles : ["super-secret-club"], + names : ["joe","barb"] + } + }).ok); + + + T(CouchDB.login("jchris@apache.org", "funnybone").ok); + + // db admin can read + T(secretDb.open("baz").foo == "bar"); + + // and run temp views + TEquals(secretDb.query(function(doc) { + emit(null, null) + }).total_rows, 1); + + CouchDB.logout(); + T(CouchDB.session().userCtx.roles.indexOf("_admin") != -1); + + // admin now adds the top-secret role to the db's readers + // and removes db-admins + T(secretDb.setSecObj({ + "admins" : { + roles : [], + names : [] + }, + "readers" : { + roles : ["super-secret-club", "top-secret"], + names : ["joe","barb"] + } + }).ok); + + // server _admin can always read + T(secretDb.open("baz").foo == "bar"); + + // and run temp views + TEquals(secretDb.query(function(doc) { + emit(null, null) + }).total_rows, 1); + + T(secretDb.save({ + "_id" : "_design/foo", + views : { + bar : { + map : "function(doc){emit(null, null)}" + } + } + }).ok) + + // now top-secret users can read too + T(CouchDB.login("jchris@apache.org", "funnybone").ok); + T(CouchDB.session().userCtx.roles.indexOf("_admin") == -1); + T(secretDb.open("baz").foo == "bar"); + // readers can query stored views + T(secretDb.view("foo/bar").total_rows == 1); + + // readers can't do temp views + try { + var results = secretDb.query(function(doc) { + emit(null, null); + }); + T(false && "temp view should be admin only"); + } catch (e) { + T(true && "temp view is admin only"); + } + + + CouchDB.logout(); + + // can't set non string reader names or roles + try { + secretDb.setSecObj({ + "readers" : { + roles : ["super-secret-club", {"top-secret":"awesome"}], + names : ["joe","barb"] + } + }) + T(false && "only string roles"); + } catch (e) {} + + try { + secretDb.setSecObj({ + "readers" : { + roles : ["super-secret-club", {"top-secret":"awesome"}], + names : ["joe",22] + } + }); + T(false && "only string names"); + } catch (e) {} + + try { + secretDb.setSecObj({ + "readers" : { + roles : ["super-secret-club", {"top-secret":"awesome"}], + names : "joe" + } + }); + T(false && "only lists of names"); + } catch (e) {} + } finally { + CouchDB.logout(); + } + }; + + run_on_modified_server( + [{section: "httpd", + key: "authentication_handlers", + value: "{couch_httpd_auth, cookie_authentication_handler}, {couch_httpd_auth, default_authentication_handler}"}, + {section: "couch_httpd_auth", + key: "authentication_db", value: "test_suite_users"}], + testFun + ); + + // security changes will always commit synchronously + restartServer(); + + run_on_modified_server( + [{section: "httpd", + key: "authentication_handlers", + value: "{couch_httpd_auth, cookie_authentication_handler}, {couch_httpd_auth, default_authentication_handler}"}, + {section: "couch_httpd_auth", + key: "authentication_db", value: "test_suite_users"}], + testFun2 + ); +} diff --git a/share/www/script/test/recreate_doc.js b/share/www/script/test/recreate_doc.js index a6a64ac0..05843558 100644 --- a/share/www/script/test/recreate_doc.js +++ b/share/www/script/test/recreate_doc.js @@ -51,7 +51,7 @@ couchTests.recreate_doc = function(debug) { data: "VGhpcyBpcyBhIGJhc2U2NCBlbmNvZGVkIHRleHQ=" } } - } + }; try { // same as before, but with binary db.save(binAttDoc); diff --git a/share/www/script/test/reduce.js b/share/www/script/test/reduce.js index 9c80fa7f..979a0292 100644 --- a/share/www/script/test/reduce.js +++ b/share/www/script/test/reduce.js @@ -15,14 +15,15 @@ couchTests.reduce = function(debug) { db.deleteDb(); db.createDb(); if (debug) debugger; - var numDocs = 500 + var numDocs = 500; var docs = makeDocs(1,numDocs + 1); db.bulkSave(docs); var summate = function(N) {return (N+1)*N/2;}; var map = function (doc) { emit(doc.integer, doc.integer); - emit(doc.integer, doc.integer)}; + emit(doc.integer, doc.integer); + }; var reduce = function (keys, values) { return sum(values); }; var result = db.query(map, reduce); T(result.rows[0].value == 2*summate(numDocs)); @@ -69,7 +70,7 @@ couchTests.reduce = function(debug) { T(db.info().doc_count == ((i - 1) * 10 * 11) + ((j + 1) * 11)); } - map = function (doc) {emit(doc.keys, 1)}; + map = function (doc) { emit(doc.keys, 1); }; reduce = function (keys, values) { return sum(values); }; var results = db.query(map, reduce, {group:true}); @@ -107,7 +108,7 @@ couchTests.reduce = function(debug) { db.createDb(); - var map = function (doc) {emit(doc.val, doc.val)}; + var map = function (doc) { emit(doc.val, doc.val); }; var reduceCombine = function (keys, values, rereduce) { // This computes the standard deviation of the mapped results var stdDeviation=0.0; diff --git a/share/www/script/test/reduce_builtin.js b/share/www/script/test/reduce_builtin.js index 7938a0cf..b3cc3cc7 100644 --- a/share/www/script/test/reduce_builtin.js +++ b/share/www/script/test/reduce_builtin.js @@ -16,22 +16,37 @@ couchTests.reduce_builtin = function(debug) { db.createDb(); if (debug) debugger; - var numDocs = 500 + var numDocs = 500; var docs = makeDocs(1,numDocs + 1); db.bulkSave(docs); var summate = function(N) {return (N+1)*N/2;}; + var sumsqr = function(N) { + var acc = 0; + for (var i=1; i<=N; ++i) { + acc += i*i; + } + return acc; + }; + // this is the same test as the reduce.js test // only we'll let CouchDB run reduce in Erlang var map = function (doc) { emit(doc.integer, doc.integer); - emit(doc.integer, doc.integer)}; + emit(doc.integer, doc.integer); + }; var result = db.query(map, "_sum"); T(result.rows[0].value == 2*summate(numDocs)); result = db.query(map, "_count"); T(result.rows[0].value == 1000); + result = db.query(map, "_stats"); + T(result.rows[0].value.sum == 2*summate(numDocs)); + T(result.rows[0].value.count == 1000); + T(result.rows[0].value.min == 1); + T(result.rows[0].value.max == 500); + T(result.rows[0].value.sumsqr == 2*sumsqr(numDocs)); result = db.query(map, "_sum", {startkey: 4, endkey: 4}); T(result.rows[0].value == 8); @@ -58,6 +73,26 @@ couchTests.reduce_builtin = function(debug) { T(result.rows[0].value == 2*(summate(numDocs-i) - summate(i-1))); } + // test for trailing characters after builtin functions, desired behaviour + // is to disregard any trailing characters + // I think the behavior should be a prefix test, so that even "_statsorama" + // or "_stats\nare\awesome" should work just as "_stats" does. - JChris + + var trailing = ["\u000a", "orama", "\nare\nawesome", " ", " \n "]; + + for(var i=0; i < trailing.length; i++) { + result = db.query(map, "_sum" + trailing[i]); + T(result.rows[0].value == 2*summate(numDocs)); + result = db.query(map, "_count" + trailing[i]); + T(result.rows[0].value == 1000); + result = db.query(map, "_stats" + trailing[i]); + T(result.rows[0].value.sum == 2*summate(numDocs)); + T(result.rows[0].value.count == 1000); + T(result.rows[0].value.min == 1); + T(result.rows[0].value.max == 500); + T(result.rows[0].value.sumsqr == 2*sumsqr(numDocs)); + } + db.deleteDb(); db.createDb(); @@ -81,7 +116,7 @@ couchTests.reduce_builtin = function(debug) { T(db.info().doc_count == ((i - 1) * 10 * 11) + ((j + 1) * 11)); } - map = function (doc) {emit(doc.keys, 1)}; + map = function (doc) { emit(doc.keys, 1); }; // with emitted values being 1, count should be the same as sum var builtins = ["_sum", "_count"]; @@ -115,5 +150,30 @@ couchTests.reduce_builtin = function(debug) { T(equals(results.rows[5], {key:["d","b"],value:10*i})); T(equals(results.rows[6], {key:["d","c"],value:10*i})); }; + + map = function (doc) { emit(doc.keys, [1, 1]); }; + + var results = db.query(map, "_sum", {group:true}); + T(equals(results.rows[0], {key:["a"],value:[20*i,20*i]})); + T(equals(results.rows[1], {key:["a","b"],value:[20*i,20*i]})); + T(equals(results.rows[2], {key:["a", "b", "c"],value:[10*i,10*i]})); + T(equals(results.rows[3], {key:["a", "b", "d"],value:[10*i,10*i]})); + + var results = db.query(map, "_sum", {group: true, limit: 2}); + T(equals(results.rows[0], {key: ["a"], value: [20*i,20*i]})); + T(equals(results.rows.length, 2)); + + var results = db.query(map, "_sum", {group_level:1}); + T(equals(results.rows[0], {key:["a"],value:[70*i,70*i]})); + T(equals(results.rows[1], {key:["d"],value:[40*i,40*i]})); + + var results = db.query(map, "_sum", {group_level:2}); + T(equals(results.rows[0], {key:["a"],value:[20*i,20*i]})); + T(equals(results.rows[1], {key:["a","b"],value:[40*i,40*i]})); + T(equals(results.rows[2], {key:["a","c"],value:[10*i,10*i]})); + T(equals(results.rows[3], {key:["d"],value:[10*i,10*i]})); + T(equals(results.rows[4], {key:["d","a"],value:[10*i,10*i]})); + T(equals(results.rows[5], {key:["d","b"],value:[10*i,10*i]})); + T(equals(results.rows[6], {key:["d","c"],value:[10*i,10*i]})); } } diff --git a/share/www/script/test/replication.js b/share/www/script/test/replication.js index 78678937..5acff4ab 100644 --- a/share/www/script/test/replication.js +++ b/share/www/script/test/replication.js @@ -17,19 +17,19 @@ couchTests.replication = function(debug) { {source:"test_suite_db_a", target:"test_suite_db_b"}, {source:"test_suite_db_a", - target:"http://" + host + "/test_suite_db_b"}, - {source:"http://" + host + "/test_suite_db_a", + target:CouchDB.protocol + host + "/test_suite_db_b"}, + {source:CouchDB.protocol + host + "/test_suite_db_a", target:"test_suite_db_b"}, - {source:"http://" + host + "/test_suite_db_a", - target:"http://" + host + "/test_suite_db_b"} - ] + {source:CouchDB.protocol + host + "/test_suite_db_a", + target:CouchDB.protocol + host + "/test_suite_db_b"} + ]; var dbA = new CouchDB("test_suite_db_a", {"X-Couch-Full-Commit":"false"}); var dbB = new CouchDB("test_suite_db_b", {"X-Couch-Full-Commit":"false"}); var numDocs = 10; var xhr; for (var testPair = 0; testPair < dbPairs.length; testPair++) { - var A = dbPairs[testPair].source - var B = dbPairs[testPair].target + var A = dbPairs[testPair].source; + var B = dbPairs[testPair].target; dbA.deleteDb(); dbA.createDb(); @@ -41,7 +41,7 @@ couchTests.replication = function(debug) { test_template: new function () { this.init = function(dbA, dbB) { // before anything has happened - } + }; this.afterAB1 = function(dbA, dbB) { // called after replicating src=A tgt=B first time. }; @@ -165,20 +165,20 @@ couchTests.replication = function(debug) { this.afterAB1 = function(dbA, dbB) { var xhr = CouchDB.request("GET", "/test_suite_db_a/bin_doc/foo%2Bbar.txt"); - T(xhr.responseText == "This is a base64 encoded text") + T(xhr.responseText == "This is a base64 encoded text"); xhr = CouchDB.request("GET", "/test_suite_db_b/bin_doc/foo%2Bbar.txt"); - T(xhr.responseText == "This is a base64 encoded text") + T(xhr.responseText == "This is a base64 encoded text"); // and the design-doc xhr = CouchDB.request("GET", "/test_suite_db_a/_design/with_bin/foo%2Bbar.txt"); - T(xhr.responseText == "This is a base64 encoded text") + T(xhr.responseText == "This is a base64 encoded text"); xhr = CouchDB.request("GET", "/test_suite_db_b/_design/with_bin/foo%2Bbar.txt"); - T(xhr.responseText == "This is a base64 encoded text") + T(xhr.responseText == "This is a base64 encoded text"); }; }, @@ -209,8 +209,8 @@ couchTests.replication = function(debug) { var docB = dbB.open("foo", {conflicts: true, deleted_conflicts: true}); // We should have no conflicts this time - T(docA._conflicts === undefined) - T(docB._conflicts === undefined); + T(typeof docA._conflicts === "undefined"); + T(typeof docB._conflicts === "undefined"); // They show up as deleted conflicts instead T(docA._deleted_conflicts[0] == docB._deleted_conflicts[0]); @@ -229,7 +229,7 @@ couchTests.replication = function(debug) { var seqA = result.source_last_seq; T(0 == result.history[0].start_last_seq); - T(result.history[1] === undefined) + T(typeof result.history[1] === "undefined"); for(test in repTests) { if(repTests[test].afterAB1) repTests[test].afterAB1(dbA, dbB); @@ -239,7 +239,7 @@ couchTests.replication = function(debug) { var seqB = result.source_last_seq; T(0 == result.history[0].start_last_seq); - T(result.history[1] === undefined) + T(typeof result.history[1] === "undefined"); for(test in repTests) { if(repTests[test].afterBA1) repTests[test].afterBA1(dbA, dbB); @@ -252,7 +252,7 @@ couchTests.replication = function(debug) { T(seqA < result2.source_last_seq); T(seqA == result2.history[0].start_last_seq); - T(result2.history[1].end_last_seq == seqA) + T(result2.history[1].end_last_seq == seqA); seqA = result2.source_last_seq; @@ -260,11 +260,11 @@ couchTests.replication = function(debug) { if(repTests[test].afterAB2) repTests[test].afterAB2(dbA, dbB); } - result = CouchDB.replicate(B, A) + result = CouchDB.replicate(B, A); T(seqB < result.source_last_seq); T(seqB == result.history[0].start_last_seq); - T(result.history[1].end_last_seq == seqB) + T(result.history[1].end_last_seq == seqB); seqB = result.source_last_seq; @@ -296,9 +296,436 @@ couchTests.replication = function(debug) { // remote dbB.deleteDb(); - CouchDB.replicate(dbA.name, "http://" + CouchDB.host + "/test_suite_db_b", { + CouchDB.replicate(dbA.name, CouchDB.protocol + CouchDB.host + "/test_suite_db_b", { body: {"create_target": true} }); TEquals("test_suite_db_b", dbB.info().db_name, "Target database should exist"); + + // continuous + var continuousResult = CouchDB.replicate(dbA.name, "test_suite_db_b", { + body: {"continuous": true} + }); + T(continuousResult.ok); + T(continuousResult._local_id); + + var cancelResult = CouchDB.replicate(dbA.name, "test_suite_db_b", { + body: {"cancel": true} + }); + T(cancelResult.ok); + T(continuousResult._local_id == cancelResult._local_id); + + try { + var cancelResult2 = CouchDB.replicate(dbA.name, "test_suite_db_b", { + body: {"cancel": true} + }); + } catch (e) { + T(e.error == "not_found"); + } + // test replication object option doc_ids + + var dbA = new CouchDB("test_suite_rep_docs_db_a", {"X-Couch-Full-Commit":"false"}); + var dbB = new CouchDB("test_suite_rep_docs_db_b", {"X-Couch-Full-Commit":"false"}); + + dbA.deleteDb(); + dbA.createDb(); + + var all_docs = [ + { + _id: "foo1", + value: "a" + }, + { + _id: "foo2", + value: "b" + }, + { + _id: "foo3", + value: "c" + }, + { + _id: "slashed/foo", + value: "s" + }, + { + _id: "_design/foobar", + language: "javascript", + value: "I am a design doc", + filters: { + idfilter: (function(doc, req) { + return doc.value == Number(req.filter_value); + }).toString() + }, + views: { + countview: (function(doc) { + emit(doc.value, 1); + }).toString() + } + } + ]; + + for (var i = 0; i < all_docs.length; i++) { + T(dbA.save(all_docs[i]).ok); + } + + var dbPairs = [ + {source:"test_suite_rep_docs_db_a", + target:"test_suite_rep_docs_db_b"}, + {source:"test_suite_rep_docs_db_a", + target:CouchDB.protocol + host + "/test_suite_rep_docs_db_b"}, + {source:CouchDB.protocol + host + "/test_suite_rep_docs_db_a", + target:"test_suite_rep_docs_db_b"}, + {source:CouchDB.protocol + host + "/test_suite_rep_docs_db_a", + target:CouchDB.protocol + host + "/test_suite_rep_docs_db_b"} + ]; + + var target_doc_ids = [ + ["foo1", "foo3", "foo666"], + ["foo1", "foo666"], + ["foo666", "foo2"], + ["foo2", "foo9999", "foo1"], + ["foo3", "slashed/foo"], + ["foo3", "slashed%2Ffoo"], + ["foo1", "_design/foobar"], + ["foo1", "foo1001", "_design%2Ffoobar"] + ]; + + for (var i = 0; i < dbPairs.length; i++) { + var src_db = dbPairs[i].source; + var tgt_db = dbPairs[i].target; + + for (var j = 0; j < target_doc_ids.length; j++) { + var doc_ids = target_doc_ids[j]; + var valid_doc_ids = []; + var invalid_doc_ids = []; + + for (var p = 0; p < doc_ids.length; p++) { + var id = doc_ids[p]; + var found = false; + + for (var k = 0; k < all_docs.length; k++) { + var doc = all_docs[k]; + + if (id === doc._id) { + found = true; + break; + } + } + + if (found) { + valid_doc_ids.push(id); + } else { + invalid_doc_ids.push(id); + } + }; + + dbB.deleteDb(); + dbB.createDb(); + + var repResult = CouchDB.replicate(src_db, tgt_db, { + body: {"doc_ids": doc_ids} + }); + + T(repResult.ok); + T(repResult.docs_written === valid_doc_ids.length); + T(repResult.docs_read === valid_doc_ids.length); + T(repResult.doc_write_failures === 0); + + for (var k = 0; k < all_docs.length; k++) { + var doc = all_docs[k]; + var tgt_doc = dbB.open(doc._id); + + if (doc_ids.indexOf(doc._id) >= 0) { + T(tgt_doc !== null); + T(tgt_doc.value === doc.value); + } else { + T(tgt_doc === null); + } + } + + for (var k = 0; k < invalid_doc_ids.length; k++) { + var tgt_doc = dbB.open(invalid_doc_ids[k]); + + T(tgt_doc === null); + } + } + } + + // test filtered replication + var filterFun1 = (function(doc, req) { + if (doc.value < Number(req.query.maxvalue)) { + return true; + } else { + return false; + } + }).toString(); + + var filterFun2 = (function(doc, req) { + return true; + }).toString(); + + var dbPairs = [ + {source:"test_suite_filtered_rep_db_a", + target:"test_suite_filtered_rep_db_b"}, + {source:"test_suite_filtered_rep_db_a", + target:CouchDB.protocol + host + "/test_suite_filtered_rep_db_b"}, + {source:CouchDB.protocol + host + "/test_suite_filtered_rep_db_a", + target:"test_suite_filtered_rep_db_b"}, + {source:CouchDB.protocol + host + "/test_suite_filtered_rep_db_a", + target:CouchDB.protocol + host + "/test_suite_filtered_rep_db_b"} + ]; + var sourceDb = new CouchDB("test_suite_filtered_rep_db_a"); + var targetDb = new CouchDB("test_suite_filtered_rep_db_b"); + + for (var i = 0; i < dbPairs.length; i++) { + sourceDb.deleteDb(); + sourceDb.createDb(); + + T(sourceDb.save({_id: "foo1", value: 1}).ok); + T(sourceDb.save({_id: "foo2", value: 2}).ok); + T(sourceDb.save({_id: "foo3", value: 3}).ok); + T(sourceDb.save({_id: "foo4", value: 4}).ok); + + var ddoc = { + "_id": "_design/mydesign", + "language": "javascript", + "filters": { + "myfilter": filterFun1 + } + }; + + T(sourceDb.save(ddoc).ok); + + targetDb.deleteDb(); + targetDb.createDb(); + + var dbA = dbPairs[i].source; + var dbB = dbPairs[i].target; + + var repResult = CouchDB.replicate(dbA, dbB, { + body: { + "filter" : "mydesign/myfilter", + "query_params" : { + "maxvalue": "3" + } + } + }); + + T(repResult.ok); + T(repResult.history instanceof Array); + T(repResult.history.length === 1); + T(repResult.history[0].docs_written === 2); + T(repResult.history[0].docs_read === 2); + T(repResult.history[0].doc_write_failures === 0); + + var docFoo1 = targetDb.open("foo1"); + T(docFoo1 !== null); + T(docFoo1.value === 1); + + var docFoo2 = targetDb.open("foo2"); + T(docFoo2 !== null); + T(docFoo2.value === 2); + + var docFoo3 = targetDb.open("foo3"); + T(docFoo3 === null); + + var docFoo4 = targetDb.open("foo4"); + T(docFoo4 === null); + + // replication should start from scratch after the filter's code changed + + ddoc.filters.myfilter = filterFun2; + T(sourceDb.save(ddoc).ok); + + repResult = CouchDB.replicate(dbA, dbB, { + body: { + "filter" : "mydesign/myfilter", + "query_params" : { + "maxvalue": "3" + } + } + }); + + T(repResult.ok); + T(repResult.history instanceof Array); + T(repResult.history.length === 1); + T(repResult.history[0].docs_written === 3); + T(repResult.history[0].docs_read === 3); + T(repResult.history[0].doc_write_failures === 0); + + docFoo1 = targetDb.open("foo1"); + T(docFoo1 !== null); + T(docFoo1.value === 1); + + docFoo2 = targetDb.open("foo2"); + T(docFoo2 !== null); + T(docFoo2.value === 2); + + docFoo3 = targetDb.open("foo3"); + T(docFoo3 !== null); + T(docFoo3.value === 3); + + docFoo4 = targetDb.open("foo4"); + T(docFoo4 !== null); + T(docFoo4.value === 4); + + T(targetDb.open("_design/mydesign") !== null); + } + + // test for COUCHDB-868 - design docs' attachments not getting replicated + // when doing a pull replication with HTTP basic auth + dbA = new CouchDB("test_suite_db_a"); + dbB = new CouchDB("test_suite_db_b"); + var usersDb = new CouchDB("test_suite_auth"); + var lorem = CouchDB.request( + "GET", "/_utils/script/test/lorem.txt").responseText; + var lorem_b64 = CouchDB.request( + "GET", "/_utils/script/test/lorem_b64.txt").responseText; + + usersDb.deleteDb(); + usersDb.createDb(); + dbA.deleteDb(); + dbA.createDb(); + dbB.deleteDb(); + dbB.createDb(); + + var atts_ddoc = { + _id: "_design/i_have_atts", + language: "javascript" + }; + T(dbA.save(atts_ddoc).ok); + + var rev = atts_ddoc._rev; + var att_1_name = "lorem.txt"; + var att_2_name = "lorem.dat"; + var xhr = CouchDB.request( + "PUT", "/" + dbA.name + "/" + atts_ddoc._id + "/" + att_1_name + "?rev=" + rev, { + headers: {"Content-Type": "text/plain;charset=utf-8"}, + body: lorem + }); + rev = JSON.parse(xhr.responseText).rev; + T(xhr.status === 201); + xhr = CouchDB.request( + "PUT", "/" + dbA.name + "/" + atts_ddoc._id + "/" + att_2_name + "?rev=" + rev, { + headers: {"Content-Type": "application/data"}, + body: lorem_b64 + }); + T(xhr.status === 201); + + var fdmananaUserDoc = CouchDB.prepareUserDoc({ + name: "fdmanana", + roles: ["reader"] + }, "qwerty"); + T(usersDb.save(fdmananaUserDoc).ok); + + T(dbA.setSecObj({ + admins: { + names: [], + roles: ["admin"] + }, + readers: { + names: [], + roles: ["reader"] + } + }).ok); + T(dbB.setSecObj({ + admins: { + names: ["fdmanana"], + roles: [] + } + }).ok); + + var server_config = [ + { + section: "couch_httpd_auth", + key: "authentication_db", + value: usersDb.name + }, + // to prevent admin party mode + { + section: "admins", + key: "joe", + value: "erlang" + } + ]; + + var test_fun = function() { + T(CouchDB.login("fdmanana", "qwerty").ok); + T(CouchDB.session().userCtx.name === "fdmanana"); + T(CouchDB.session().userCtx.roles.indexOf("_admin") === -1); + + var repResult = CouchDB.replicate( + CouchDB.protocol + "fdmanana:qwerty@" + host + "/" + dbA.name, + dbB.name + ); + T(repResult.ok === true); + T(repResult.history instanceof Array); + T(repResult.history.length === 1); + T(repResult.history[0].docs_written === 1); + T(repResult.history[0].docs_read === 1); + T(repResult.history[0].doc_write_failures === 0); + + var atts_ddoc_copy = dbB.open(atts_ddoc._id); + T(atts_ddoc_copy !== null); + T(typeof atts_ddoc_copy._attachments === "object"); + T(atts_ddoc_copy._attachments !== null); + T(att_1_name in atts_ddoc_copy._attachments); + T(att_2_name in atts_ddoc_copy._attachments); + + var xhr = CouchDB.request("GET", "/" + dbB.name + "/" + atts_ddoc._id + "/" + att_1_name); + T(xhr.status === 200); + T(xhr.responseText === lorem); + + xhr = CouchDB.request("GET", "/" + dbB.name + "/" + atts_ddoc._id + "/" + att_2_name); + T(xhr.status === 200); + T(xhr.responseText === lorem_b64); + + CouchDB.logout(); + T(CouchDB.login("joe", "erlang").ok); + T(dbA.setSecObj({ + admins: { + names: [], + roles: ["bar"] + }, + readers: { + names: [], + roles: ["foo"] + } + }).ok); + T(dbB.deleteDb().ok === true); + T(dbB.createDb().ok === true); + T(dbB.setSecObj({ + admins: { + names: ["fdmanana"], + roles: [] + } + }).ok); + CouchDB.logout(); + + T(CouchDB.login("fdmanana", "qwerty").ok); + T(CouchDB.session().userCtx.name === "fdmanana"); + T(CouchDB.session().userCtx.roles.indexOf("_admin") === -1); + try { + repResult = CouchDB.replicate( + CouchDB.protocol + "fdmanana:qwerty@" + host + "/" + dbA.name, + dbB.name + ); + T(false, "replication should have failed"); + } catch(x) { + T(x.error === "unauthorized"); + } + + atts_ddoc_copy = dbB.open(atts_ddoc._id); + T(atts_ddoc_copy === null); + + CouchDB.logout(); + T(CouchDB.login("joe", "erlang").ok); + }; + + run_on_modified_server(server_config, test_fun); + + // cleanup + dbA.deleteDb(); + dbB.deleteDb(); + usersDb.deleteDb(); }; diff --git a/share/www/script/test/replicator_db.js b/share/www/script/test/replicator_db.js new file mode 100644 index 00000000..3c6a5d8e --- /dev/null +++ b/share/www/script/test/replicator_db.js @@ -0,0 +1,818 @@ +// 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.replicator_db = function(debug) { + + if (debug) debugger; + + var wait_rep_doc = 500; // number of millisecs to wait after saving a Rep Doc + var host = CouchDB.host; + var dbA = new CouchDB("test_suite_rep_db_a", {"X-Couch-Full-Commit":"false"}); + var dbB = new CouchDB("test_suite_rep_db_b", {"X-Couch-Full-Commit":"false"}); + var repDb = new CouchDB("test_suite_rep_db", {"X-Couch-Full-Commit":"false"}); + var usersDb = new CouchDB("test_suite_auth", {"X-Couch-Full-Commit":"false"}); + + var docs1 = [ + { + _id: "foo1", + value: 11 + }, + { + _id: "foo2", + value: 22 + }, + { + _id: "foo3", + value: 33 + } + ]; + + function waitForRep(repDb, repDoc, state) { + var newRep, + t0 = new Date(), + t1, + ms = 1000; + + do { + newRep = repDb.open(repDoc._id); + t1 = new Date(); + } while (((t1 - t0) <= ms) && newRep._replication_state !== state); + } + + function waitForSeq(sourceDb, targetDb) { + var targetSeq, + sourceSeq = sourceDb.info().update_seq, + t0 = new Date(), + t1, + ms = 1000; + + do { + targetSeq = targetDb.info().update_seq; + t1 = new Date(); + } while (((t1 - t0) <= ms) && targetSeq < sourceSeq); + } + + function wait(ms) { + var t0 = new Date(), t1; + do { + CouchDB.request("GET", "/"); + t1 = new Date(); + } while ((t1 - t0) <= ms); + } + + + function populate_db(db, docs) { + db.deleteDb(); + db.createDb(); + for (var i = 0; i < docs.length; i++) { + var d = docs[i]; + delete d._rev; + T(db.save(d).ok); + } + } + + function simple_replication() { + populate_db(dbA, docs1); + populate_db(dbB, []); + + var repDoc = { + _id: "foo_simple_rep", + source: dbA.name, + target: dbB.name + }; + T(repDb.save(repDoc).ok); + + waitForRep(repDb, repDoc, "completed"); + for (var i = 0; i < docs1.length; i++) { + var doc = docs1[i]; + var copy = dbB.open(doc._id); + T(copy !== null); + T(copy.value === doc.value); + } + + var repDoc1 = repDb.open(repDoc._id); + T(repDoc1 !== null); + T(repDoc1.source === repDoc.source); + T(repDoc1.target === repDoc.target); + T(repDoc1._replication_state === "completed", "simple"); + T(typeof repDoc1._replication_state_time === "number"); + T(typeof repDoc1._replication_id === "string"); + } + + + function filtered_replication() { + var docs2 = docs1.concat([ + { + _id: "_design/mydesign", + language : "javascript", + filters : { + myfilter : (function(doc, req) { + return (doc.value % 2) !== Number(req.query.myparam); + }).toString() + } + } + ]); + + populate_db(dbA, docs2); + populate_db(dbB, []); + + var repDoc = { + _id: "foo_filt_rep_doc", + source: "http://" + host + "/" + dbA.name, + target: dbB.name, + filter: "mydesign/myfilter", + query_params: { + myparam: 1 + } + }; + T(repDb.save(repDoc).ok); + + waitForRep(repDb, repDoc, "completed"); + for (var i = 0; i < docs2.length; i++) { + var doc = docs2[i]; + var copy = dbB.open(doc._id); + + if (typeof doc.value === "number") { + if ((doc.value % 2) !== 1) { + T(copy !== null); + T(copy.value === doc.value); + } else { + T(copy === null); + } + } + } + + var repDoc1 = repDb.open(repDoc._id); + T(repDoc1 !== null); + T(repDoc1.source === repDoc.source); + T(repDoc1.target === repDoc.target); + T(repDoc1._replication_state === "completed", "filtered"); + T(typeof repDoc1._replication_state_time === "number"); + T(typeof repDoc1._replication_id === "string"); + } + + + function continuous_replication() { + populate_db(dbA, docs1); + populate_db(dbB, []); + + var repDoc = { + _id: "foo_cont_rep_doc", + source: "http://" + host + "/" + dbA.name, + target: dbB.name, + continuous: true + }; + + T(repDb.save(repDoc).ok); + + waitForSeq(dbA, dbB); + for (var i = 0; i < docs1.length; i++) { + var doc = docs1[i]; + var copy = dbB.open(doc._id); + T(copy !== null); + T(copy.value === doc.value); + } + + // add another doc to source, it will be replicated to target + var docX = { + _id: "foo1000", + value: 1001 + }; + + T(dbA.save(docX).ok); + + waitForSeq(dbA, dbB); + var copy = dbB.open("foo1000"); + T(copy !== null); + T(copy.value === 1001); + + var repDoc1 = repDb.open(repDoc._id); + T(repDoc1 !== null); + T(repDoc1.source === repDoc.source); + T(repDoc1.target === repDoc.target); + T(repDoc1._replication_state === "triggered"); + T(typeof repDoc1._replication_state_time === "number"); + T(typeof repDoc1._replication_id === "string"); + + // add a design doc to source, it will be replicated to target + // when the "user_ctx" property is not defined in the replication doc, + // the replication will be done under an _admin context, therefore + // design docs will be replicated + var ddoc = { + _id: "_design/foobar", + language: "javascript" + }; + + T(dbA.save(ddoc).ok); + + waitForSeq(dbA, dbB); + var ddoc_copy = dbB.open("_design/foobar"); + T(ddoc_copy !== null); + T(ddoc.language === "javascript"); + + // update the design doc on source, test that the new revision is replicated + ddoc.language = "erlang"; + T(dbA.save(ddoc).ok); + T(ddoc._rev.indexOf("2-") === 0); + + waitForSeq(dbA, dbB); + ddoc_copy = dbB.open("_design/foobar"); + T(ddoc_copy !== null); + T(ddoc_copy._rev === ddoc._rev); + T(ddoc.language === "erlang"); + + // stop replication by deleting the replication document + T(repDb.deleteDoc(repDoc1).ok); + + // add another doc to source, it will NOT be replicated to target + var docY = { + _id: "foo666", + value: 999 + }; + + T(dbA.save(docY).ok); + + wait(200); // is there a way to avoid wait here? + var copy = dbB.open("foo666"); + T(copy === null); + } + + + function by_doc_ids_replication() { + // to test that we can replicate docs with slashes in their IDs + var docs2 = docs1.concat([ + { + _id: "_design/mydesign", + language : "javascript" + } + ]); + + populate_db(dbA, docs2); + populate_db(dbB, []); + + var repDoc = { + _id: "foo_cont_rep_doc", + source: "http://" + host + "/" + dbA.name, + target: dbB.name, + doc_ids: ["foo666", "foo3", "_design/mydesign", "foo999", "foo1"] + }; + T(repDb.save(repDoc).ok); + + waitForRep(repDb, repDoc, "completed"); + var copy = dbB.open("foo1"); + T(copy !== null); + T(copy.value === 11); + + copy = dbB.open("foo2"); + T(copy === null); + + copy = dbB.open("foo3"); + T(copy !== null); + T(copy.value === 33); + + copy = dbB.open("foo666"); + T(copy === null); + + copy = dbB.open("foo999"); + T(copy === null); + + copy = dbB.open("_design/mydesign"); + T(copy !== null); + T(copy.language === "javascript"); + } + + + function successive_identical_replications() { + populate_db(dbA, docs1); + populate_db(dbB, []); + + var repDoc1 = { + _id: "foo_ident_rep_1", + source: dbA.name, + target: dbB.name + }; + T(repDb.save(repDoc1).ok); + + waitForRep(repDb, repDoc1, "completed"); + for (var i = 0; i < docs1.length; i++) { + var doc = docs1[i]; + var copy = dbB.open(doc._id); + T(copy !== null); + T(copy.value === doc.value); + } + + var repDoc1_copy = repDb.open(repDoc1._id); + T(repDoc1_copy !== null); + T(repDoc1_copy.source === repDoc1.source); + T(repDoc1_copy.target === repDoc1.target); + T(repDoc1_copy._replication_state === "completed"); + T(typeof repDoc1_copy._replication_state_time === "number"); + T(typeof repDoc1_copy._replication_id === "string"); + + var newDoc = { + _id: "doc666", + value: 666 + }; + T(dbA.save(newDoc).ok); + + wait(200); + var newDoc_copy = dbB.open(newDoc._id); + // not replicated because first replication is complete (not continuous) + T(newDoc_copy === null); + + var repDoc2 = { + _id: "foo_ident_rep_2", + source: dbA.name, + target: dbB.name + }; + T(repDb.save(repDoc2).ok); + + waitForRep(repDb, repDoc2, "completed"); + var newDoc_copy = dbB.open(newDoc._id); + T(newDoc_copy !== null); + T(newDoc_copy.value === newDoc.value); + + var repDoc2_copy = repDb.open(repDoc2._id); + T(repDoc2_copy !== null); + T(repDoc2_copy.source === repDoc1.source); + T(repDoc2_copy.target === repDoc1.target); + T(repDoc2_copy._replication_state === "completed"); + T(typeof repDoc2_copy._replication_state_time === "number"); + T(typeof repDoc2_copy._replication_id === "string"); + T(repDoc2_copy._replication_id === repDoc1_copy._replication_id); + } + + + // test the case where multiple replication docs (different IDs) + // describe in fact the same replication (source, target, etc) + function identical_rep_docs() { + populate_db(dbA, docs1); + populate_db(dbB, []); + + var repDoc1 = { + _id: "foo_dup_rep_doc_1", + source: "http://" + host + "/" + dbA.name, + target: dbB.name + }; + var repDoc2 = { + _id: "foo_dup_rep_doc_2", + source: "http://" + host + "/" + dbA.name, + target: dbB.name + }; + + T(repDb.save(repDoc1).ok); + T(repDb.save(repDoc2).ok); + + waitForRep(repDb, repDoc1, "completed"); + for (var i = 0; i < docs1.length; i++) { + var doc = docs1[i]; + var copy = dbB.open(doc._id); + T(copy !== null); + T(copy.value === doc.value); + } + + repDoc1 = repDb.open("foo_dup_rep_doc_1"); + T(repDoc1 !== null); + T(repDoc1._replication_state === "completed", "identical"); + T(typeof repDoc1._replication_state_time === "number"); + T(typeof repDoc1._replication_id === "string"); + + repDoc2 = repDb.open("foo_dup_rep_doc_2"); + T(repDoc2 !== null); + T(typeof repDoc2._replication_state === "undefined"); + T(typeof repDoc2._replication_state_time === "undefined"); + T(repDoc2._replication_id === repDoc1._replication_id); + } + + + // test the case where multiple replication docs (different IDs) + // describe in fact the same continuous replication (source, target, etc) + function identical_continuous_rep_docs() { + populate_db(dbA, docs1); + populate_db(dbB, []); + + var repDoc1 = { + _id: "foo_dup_cont_rep_doc_1", + source: "http://" + host + "/" + dbA.name, + target: dbB.name, + continuous: true + }; + var repDoc2 = { + _id: "foo_dup_cont_rep_doc_2", + source: "http://" + host + "/" + dbA.name, + target: dbB.name, + continuous: true + }; + + T(repDb.save(repDoc1).ok); + T(repDb.save(repDoc2).ok); + + waitForSeq(dbA, dbB); + for (var i = 0; i < docs1.length; i++) { + var doc = docs1[i]; + var copy = dbB.open(doc._id); + T(copy !== null); + T(copy.value === doc.value); + } + + repDoc1 = repDb.open("foo_dup_cont_rep_doc_1"); + T(repDoc1 !== null); + T(repDoc1._replication_state === "triggered"); + T(typeof repDoc1._replication_state_time === "number"); + T(typeof repDoc1._replication_id === "string"); + + repDoc2 = repDb.open("foo_dup_cont_rep_doc_2"); + T(repDoc2 !== null); + T(typeof repDoc2._replication_state === "undefined"); + T(typeof repDoc2._replication_state_time === "undefined"); + T(repDoc2._replication_id === repDoc1._replication_id); + + var newDoc = { + _id: "foo666", + value: 999 + }; + T(dbA.save(newDoc).ok); + + waitForSeq(dbA, dbB); + var copy = dbB.open("foo666"); + T(copy !== null); + T(copy.value === 999); + + // deleting second replication doc, doesn't affect the 1st one and + // neither it stops the replication + T(repDb.deleteDoc(repDoc2).ok); + repDoc1 = repDb.open("foo_dup_cont_rep_doc_1"); + T(repDoc1 !== null); + T(repDoc1._replication_state === "triggered"); + T(typeof repDoc1._replication_state_time === "number"); + + var newDoc2 = { + _id: "foo5000", + value: 5000 + }; + T(dbA.save(newDoc2).ok); + + waitForSeq(dbA, dbB); + var copy = dbB.open("foo5000"); + T(copy !== null); + T(copy.value === 5000); + + // deleting the 1st replication document stops the replication + T(repDb.deleteDoc(repDoc1).ok); + var newDoc3 = { + _id: "foo1983", + value: 1983 + }; + T(dbA.save(newDoc3).ok); + + wait(wait_rep_doc); //how to remove wait? + var copy = dbB.open("foo1983"); + T(copy === null); + } + + + function test_replication_credentials_delegation() { + populate_db(usersDb, []); + + var joeUserDoc = CouchDB.prepareUserDoc({ + name: "joe", + roles: ["god", "erlanger"] + }, "erly"); + T(usersDb.save(joeUserDoc).ok); + + var ddoc = { + _id: "_design/beer", + language: "javascript" + }; + populate_db(dbA, docs1.concat([ddoc])); + populate_db(dbB, []); + + T(dbB.setSecObj({ + admins: { + names: [], + roles: ["god"] + } + }).ok); + + var server_admins_config = [ + { + section: "admins", + key: "fdmanana", + value: "qwerty" + } + ]; + + run_on_modified_server(server_admins_config, function() { + + T(CouchDB.login("fdmanana", "qwerty").ok); + T(CouchDB.session().userCtx.name === "fdmanana"); + T(CouchDB.session().userCtx.roles.indexOf("_admin") !== -1); + + var repDoc = { + _id: "foo_rep_del_doc_1", + source: dbA.name, + target: dbB.name, + user_ctx: { + name: "joe", + roles: ["erlanger"] + } + }; + + T(repDb.save(repDoc).ok); + + waitForRep(repDb, repDoc, "completed"); + for (var i = 0; i < docs1.length; i++) { + var doc = docs1[i]; + var copy = dbB.open(doc._id); + T(copy !== null); + T(copy.value === doc.value); + } + + // design doc was not replicated, because joe is not an admin of db B + var doc = dbB.open(ddoc._id); + T(doc === null); + + // now test the same replication but putting the role "god" in the + // delegation user context property + var repDoc2 = { + _id: "foo_rep_del_doc_2", + source: dbA.name, + target: dbB.name, + user_ctx: { + name: "joe", + roles: ["erlanger", "god"] + } + }; + T(repDb.save(repDoc2).ok); + + waitForRep(repDb, repDoc2, "completed"); + for (var i = 0; i < docs1.length; i++) { + var doc = docs1[i]; + var copy = dbB.open(doc._id); + T(copy !== null); + T(copy.value === doc.value); + } + + // because anyone with a 'god' role is an admin of db B, a replication + // that is delegated to a 'god' role can write design docs to db B + doc = dbB.open(ddoc._id); + T(doc !== null); + T(doc.language === ddoc.language); + }); + } + + + function continuous_replication_survives_restart() { + var origRepDbName = CouchDB.request( + "GET", "/_config/replicator/db").responseText; + + repDb.deleteDb(); + + var xhr = CouchDB.request("PUT", "/_config/replicator/db", { + body : JSON.stringify(repDb.name), + headers: {"X-Couch-Persist": "false"} + }); + T(xhr.status === 200); + + populate_db(dbA, docs1); + populate_db(dbB, []); + + var repDoc = { + _id: "foo_cont_rep_survives_doc", + source: "http://" + host + "/" + dbA.name, + target: dbB.name, + continuous: true + }; + + T(repDb.save(repDoc).ok); + + waitForSeq(dbA, dbB); + for (var i = 0; i < docs1.length; i++) { + var doc = docs1[i]; + var copy = dbB.open(doc._id); + T(copy !== null); + T(copy.value === doc.value); + } + + repDb.ensureFullCommit(); + dbA.ensureFullCommit(); + + restartServer(); + + xhr = CouchDB.request("PUT", "/_config/replicator/db", { + body : JSON.stringify(repDb.name), + headers: {"X-Couch-Persist": "false"} + }); + + T(xhr.status === 200); + + // add another doc to source, it will be replicated to target + var docX = { + _id: "foo1000", + value: 1001 + }; + + T(dbA.save(docX).ok); + + waitForSeq(dbA, dbB); + var copy = dbB.open("foo1000"); + T(copy !== null); + T(copy.value === 1001); + + repDoc = repDb.open("foo_cont_rep_survives_doc"); + T(repDoc !== null); + T(repDoc.continuous === true); + + // stop replication + T(repDb.deleteDoc(repDoc).ok); + + xhr = CouchDB.request("PUT", "/_config/replicator/db", { + body : origRepDbName, + headers: {"X-Couch-Persist": "false"} + }); + T(xhr.status === 200); + } + + + function rep_db_write_authorization() { + populate_db(dbA, docs1); + populate_db(dbB, []); + + var server_admins_config = [ + { + section: "admins", + key: "fdmanana", + value: "qwerty" + } + ]; + + run_on_modified_server(server_admins_config, function() { + var repDoc = { + _id: "foo_rep_doc", + source: dbA.name, + target: dbB.name + }; + + T(CouchDB.login("fdmanana", "qwerty").ok); + T(CouchDB.session().userCtx.name === "fdmanana"); + T(CouchDB.session().userCtx.roles.indexOf("_admin") !== -1); + + T(repDb.save(repDoc).ok); + + waitForRep(repDb, repDoc, "completed"); + + for (var i = 0; i < docs1.length; i++) { + var doc = docs1[i]; + var copy = dbB.open(doc._id); + + T(copy !== null); + T(copy.value === doc.value); + } + + repDoc = repDb.open("foo_rep_doc"); + T(repDoc !== null); + repDoc.target = "test_suite_foo_db"; + repDoc.create_target = true; + + // Only the replicator can update replication documents. + // Admins can only add and delete replication documents. + try { + repDb.save(repDoc); + T(false && "Should have thrown an exception"); + } catch (x) { + T(x["error"] === "forbidden"); + } + }); + } + + + function rep_doc_with_bad_rep_id() { + populate_db(dbA, docs1); + populate_db(dbB, []); + + var repDoc = { + _id: "foo_rep", + source: dbA.name, + target: dbB.name, + replication_id: "1234abc" + }; + T(repDb.save(repDoc).ok); + + waitForRep(repDb, repDoc, "completed"); + for (var i = 0; i < docs1.length; i++) { + var doc = docs1[i]; + var copy = dbB.open(doc._id); + T(copy !== null); + T(copy.value === doc.value); + } + + var repDoc1 = repDb.open(repDoc._id); + T(repDoc1 !== null); + T(repDoc1.source === repDoc.source); + T(repDoc1.target === repDoc.target); + T(repDoc1._replication_state === "completed", + "replication document with bad replication id failed"); + T(typeof repDoc1._replication_state_time === "number"); + T(typeof repDoc1._replication_id === "string"); + T(repDoc1._replication_id !== "1234abc"); + } + + + function error_state_replication() { + populate_db(dbA, docs1); + + var repDoc = { + _id: "foo_error_rep", + source: dbA.name, + target: "nonexistent_test_db" + }; + T(repDb.save(repDoc).ok); + + waitForRep(repDb, repDoc, "error"); + var repDoc1 = repDb.open(repDoc._id); + T(repDoc1 !== null); + T(repDoc1._replication_state === "error"); + T(typeof repDoc1._replication_state_time === "number"); + T(typeof repDoc1._replication_id === "string"); + } + + + // run all the tests + var server_config = [ + { + section: "replicator", + key: "db", + value: repDb.name + } + ]; + + repDb.deleteDb(); + run_on_modified_server(server_config, simple_replication); + + repDb.deleteDb(); + restartServer(); + run_on_modified_server(server_config, filtered_replication); + + repDb.deleteDb(); + restartServer(); + run_on_modified_server(server_config, continuous_replication); + + repDb.deleteDb(); + restartServer(); + run_on_modified_server(server_config, by_doc_ids_replication); + + repDb.deleteDb(); + restartServer(); + run_on_modified_server(server_config, successive_identical_replications); + + repDb.deleteDb(); + restartServer(); + run_on_modified_server(server_config, identical_rep_docs); + + repDb.deleteDb(); + restartServer(); + run_on_modified_server(server_config, identical_continuous_rep_docs); + + repDb.deleteDb(); + restartServer(); + run_on_modified_server(server_config, rep_db_write_authorization); + + repDb.deleteDb(); + restartServer(); + run_on_modified_server(server_config, rep_doc_with_bad_rep_id); + + var server_config_2 = server_config.concat([ + { + section: "couch_httpd_auth", + key: "authentication_db", + value: usersDb.name + } + ]); + repDb.deleteDb(); + restartServer(); + run_on_modified_server(server_config_2, test_replication_credentials_delegation); + + repDb.deleteDb(); + restartServer(); + continuous_replication_survives_restart(); + + repDb.deleteDb(); + restartServer(); + run_on_modified_server(server_config, error_state_replication); + + + // cleanup + repDb.deleteDb(); + usersDb.deleteDb(); + dbA.deleteDb(); + dbB.deleteDb(); +}; diff --git a/share/www/script/test/rewrite.js b/share/www/script/test/rewrite.js new file mode 100644 index 00000000..474abb5e --- /dev/null +++ b/share/www/script/test/rewrite.js @@ -0,0 +1,443 @@ +// 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.rewrite = function(debug) { + // this test _rewrite handler + + + var db = new CouchDB("test_suite_db", {"X-Couch-Full-Commit":"false"}); + db.deleteDb(); + db.createDb(); + + + if (debug) debugger; + run_on_modified_server( + [{section: "httpd", + key: "authentication_handlers", + value: "{couch_httpd_auth, special_test_authentication_handler}"}, + {section:"httpd", + key: "WWW-Authenticate", + value: "X-Couch-Test-Auth"}], + + function(){ + var designDoc = { + _id:"_design/test", + language: "javascript", + _attachments:{ + "foo.txt": { + content_type:"text/plain", + data: "VGhpcyBpcyBhIGJhc2U2NCBlbmNvZGVkIHRleHQ=" + } + }, + rewrites: [ + { + "from": "foo", + "to": "foo.txt" + }, + { + "from": "foo2", + "to": "foo.txt", + "method": "GET" + }, + { + "from": "hello/:id", + "to": "_update/hello/:id", + "method": "PUT" + }, + { + "from": "/welcome", + "to": "_show/welcome" + }, + { + "from": "/welcome/:name", + "to": "_show/welcome", + "query": { + "name": ":name" + } + }, + { + "from": "/welcome2", + "to": "_show/welcome", + "query": { + "name": "user" + } + }, + { + "from": "/welcome3/:name", + "to": "_update/welcome2/:name", + "method": "PUT" + }, + { + "from": "/welcome3/:name", + "to": "_show/welcome2/:name", + "method": "GET" + }, + { + "from": "/welcome4/*", + "to" : "_show/welcome3", + "query": { + "name": "*" + } + }, + { + "from": "/type/<type>.json", + "to": "_show/type/:type", + "query": { + "format": "json" + } + }, + { + "from": "/type/<type>.xml", + "to": "_show/type/:type", + "query": { + "format": "xml" + } + }, + { + "from": "/type/<type>", + "to": "_show/type/:type", + "query": { + "format": "html" + } + }, + { + "from": "/welcome5/*", + "to" : "_show/*", + "query": { + "name": "*" + } + }, + { + "from": "basicView", + "to": "_view/basicView", + }, + { + "from": "simpleForm/basicView", + "to": "_list/simpleForm/basicView", + }, + { + "from": "simpleForm/basicViewFixed", + "to": "_list/simpleForm/basicView", + "query": { + "startkey": 3, + "endkey": 8 + } + }, + { + "from": "simpleForm/basicViewPath/:start/:end", + "to": "_list/simpleForm/basicView", + "query": { + "startkey": ":start", + "endkey": ":end" + } + }, + { + "from": "simpleForm/complexView", + "to": "_list/simpleForm/complexView", + "query": { + "key": [1, 2] + } + }, + { + "from": "simpleForm/complexView2", + "to": "_list/simpleForm/complexView", + "query": { + "key": ["test", {}] + } + }, + { + "from": "simpleForm/complexView3", + "to": "_list/simpleForm/complexView", + "query": { + "key": ["test", ["test", "essai"]] + } + }, + { + "from": "simpleForm/complexView4", + "to": "_list/simpleForm/complexView2", + "query": { + "key": {"c": 1} + } + }, + { + "from": "simpleForm/complexView5/:a/:b", + "to": "_list/simpleForm/complexView3", + "query": { + "key": [":a", ":b"] + } + }, + { + "from": "simpleForm/complexView6", + "to": "_list/simpleForm/complexView3", + "query": { + "key": [":a", ":b"] + } + }, + { + "from": "/", + "to": "_view/basicView", + } + ], + lists: { + simpleForm: stringFun(function(head, req) { + log("simpleForm"); + send('<ul>'); + var row, row_number = 0, prevKey, firstKey = null; + while (row = getRow()) { + row_number += 1; + if (!firstKey) firstKey = row.key; + prevKey = row.key; + send('\n<li>Key: '+row.key + +' Value: '+row.value + +' LineNo: '+row_number+'</li>'); + } + return '</ul><p>FirstKey: '+ firstKey + ' LastKey: '+ prevKey+'</p>'; + }), + }, + shows: { + "welcome": stringFun(function(doc,req) { + return "Welcome " + req.query["name"]; + }), + "welcome2": stringFun(function(doc, req) { + return "Welcome " + doc.name; + }), + "welcome3": stringFun(function(doc,req) { + return "Welcome " + req.query["name"]; + }), + "type": stringFun(function(doc, req) { + return req.id + " as " + req.query.format; + }) + }, + updates: { + "hello" : stringFun(function(doc, req) { + if (!doc) { + if (req.id) { + return [{ + _id : req.id + }, "New World"] + } + return [null, "Empty World"]; + } + doc.world = "hello"; + doc.edited_by = req.userCtx; + return [doc, "hello doc"]; + }), + "welcome2": stringFun(function(doc, req) { + if (!doc) { + if (req.id) { + return [{ + _id: req.id, + name: req.id + }, "New World"] + } + return [null, "Empty World"]; + } + return [doc, "hello doc"]; + }) + }, + views : { + basicView : { + map : stringFun(function(doc) { + if (doc.integer) { + emit(doc.integer, doc.string); + } + + }) + }, + complexView: { + map: stringFun(function(doc) { + if (doc.type == "complex") { + emit([doc.a, doc.b], doc.string); + } + }) + }, + complexView2: { + map: stringFun(function(doc) { + if (doc.type == "complex") { + emit(doc.a, doc.string); + } + }) + }, + complexView3: { + map: stringFun(function(doc) { + if (doc.type == "complex") { + emit(doc.b, doc.string); + } + }) + } + } + } + + db.save(designDoc); + + var docs = makeDocs(0, 10); + db.bulkSave(docs); + + var docs2 = [ + {"a": 1, "b": 1, "string": "doc 1", "type": "complex"}, + {"a": 1, "b": 2, "string": "doc 2", "type": "complex"}, + {"a": "test", "b": {}, "string": "doc 3", "type": "complex"}, + {"a": "test", "b": ["test", "essai"], "string": "doc 4", "type": "complex"}, + {"a": {"c": 1}, "b": "", "string": "doc 5", "type": "complex"} + ]; + + db.bulkSave(docs2); + + // test simple rewriting + + req = CouchDB.request("GET", "/test_suite_db/_design/test/_rewrite/foo"); + T(req.responseText == "This is a base64 encoded text"); + T(req.getResponseHeader("Content-Type") == "text/plain"); + + req = CouchDB.request("GET", "/test_suite_db/_design/test/_rewrite/foo2"); + T(req.responseText == "This is a base64 encoded text"); + T(req.getResponseHeader("Content-Type") == "text/plain"); + + + // test POST + // hello update world + + var doc = {"word":"plankton", "name":"Rusty"} + var resp = db.save(doc); + T(resp.ok); + var docid = resp.id; + + xhr = CouchDB.request("PUT", "/test_suite_db/_design/test/_rewrite/hello/"+docid); + T(xhr.status == 201); + T(xhr.responseText == "hello doc"); + T(/charset=utf-8/.test(xhr.getResponseHeader("Content-Type"))) + + doc = db.open(docid); + T(doc.world == "hello"); + + req = CouchDB.request("GET", "/test_suite_db/_design/test/_rewrite/welcome?name=user"); + T(req.responseText == "Welcome user"); + + req = CouchDB.request("GET", "/test_suite_db/_design/test/_rewrite/welcome/user"); + T(req.responseText == "Welcome user"); + + req = CouchDB.request("GET", "/test_suite_db/_design/test/_rewrite/welcome2"); + T(req.responseText == "Welcome user"); + + xhr = CouchDB.request("PUT", "/test_suite_db/_design/test/_rewrite/welcome3/test"); + T(xhr.status == 201); + T(xhr.responseText == "New World"); + T(/charset=utf-8/.test(xhr.getResponseHeader("Content-Type"))); + + xhr = CouchDB.request("GET", "/test_suite_db/_design/test/_rewrite/welcome3/test"); + T(xhr.responseText == "Welcome test"); + + req = CouchDB.request("GET", "/test_suite_db/_design/test/_rewrite/welcome4/user"); + T(req.responseText == "Welcome user"); + + req = CouchDB.request("GET", "/test_suite_db/_design/test/_rewrite/welcome5/welcome3"); + T(req.responseText == "Welcome welcome3"); + + xhr = CouchDB.request("GET", "/test_suite_db/_design/test/_rewrite/basicView"); + T(xhr.status == 200, "view call"); + T(/{"total_rows":9/.test(xhr.responseText)); + + xhr = CouchDB.request("GET", "/test_suite_db/_design/test/_rewrite/"); + T(xhr.status == 200, "view call"); + T(/{"total_rows":9/.test(xhr.responseText)); + + + // get with query params + xhr = CouchDB.request("GET", "/test_suite_db/_design/test/_rewrite/simpleForm/basicView?startkey=3&endkey=8"); + T(xhr.status == 200, "with query params"); + T(!(/Key: 1/.test(xhr.responseText))); + T(/FirstKey: 3/.test(xhr.responseText)); + T(/LastKey: 8/.test(xhr.responseText)); + + xhr = CouchDB.request("GET", "/test_suite_db/_design/test/_rewrite/simpleForm/basicViewFixed"); + T(xhr.status == 200, "with query params"); + T(!(/Key: 1/.test(xhr.responseText))); + T(/FirstKey: 3/.test(xhr.responseText)); + T(/LastKey: 8/.test(xhr.responseText)); + + // get with query params + xhr = CouchDB.request("GET", "/test_suite_db/_design/test/_rewrite/simpleForm/basicViewFixed?startkey=4"); + T(xhr.status == 200, "with query params"); + T(!(/Key: 1/.test(xhr.responseText))); + T(/FirstKey: 3/.test(xhr.responseText)); + T(/LastKey: 8/.test(xhr.responseText)); + + // get with query params + xhr = CouchDB.request("GET", "/test_suite_db/_design/test/_rewrite/simpleForm/basicViewPath/3/8"); + T(xhr.status == 200, "with query params"); + T(!(/Key: 1/.test(xhr.responseText))); + T(/FirstKey: 3/.test(xhr.responseText)); + T(/LastKey: 8/.test(xhr.responseText)); + + // get with query params + xhr = CouchDB.request("GET", "/test_suite_db/_design/test/_rewrite/simpleForm/complexView"); + T(xhr.status == 200, "with query params"); + T(/FirstKey: [1, 2]/.test(xhr.responseText)); + + xhr = CouchDB.request("GET", "/test_suite_db/_design/test/_rewrite/simpleForm/complexView2"); + T(xhr.status == 200, "with query params"); + T(/Value: doc 3/.test(xhr.responseText)); + + xhr = CouchDB.request("GET", "/test_suite_db/_design/test/_rewrite/simpleForm/complexView3"); + T(xhr.status == 200, "with query params"); + T(/Value: doc 4/.test(xhr.responseText)); + + xhr = CouchDB.request("GET", "/test_suite_db/_design/test/_rewrite/simpleForm/complexView4"); + T(xhr.status == 200, "with query params"); + T(/Value: doc 5/.test(xhr.responseText)); + + xhr = CouchDB.request("GET", "/test_suite_db/_design/test/_rewrite/simpleForm/complexView5/test/essai"); + T(xhr.status == 200, "with query params"); + T(/Value: doc 4/.test(xhr.responseText)); + + xhr = CouchDB.request("GET", "/test_suite_db/_design/test/_rewrite/simpleForm/complexView6?a=test&b=essai"); + T(xhr.status == 200, "with query params"); + T(/Value: doc 4/.test(xhr.responseText)); + + req = CouchDB.request("GET", "/test_suite_db/_design/test/_rewrite/type/test.json"); + T(req.responseText == "test as json"); + + req = CouchDB.request("GET", "/test_suite_db/_design/test/_rewrite/type/test.xml"); + T(req.responseText == "test as xml"); + + req = CouchDB.request("GET", "/test_suite_db/_design/test/_rewrite/type/test"); + T(req.responseText == "test as html"); + + // test path relative to server + designDoc.rewrites.push({ + "from": "uuids", + "to": "../../../_uuids" + }); + T(db.save(designDoc).ok); + + var xhr = CouchDB.request("GET", "/test_suite_db/_design/test/_rewrite/uuids"); + T(xhr.status == 500); + var result = JSON.parse(xhr.responseText); + T(result.error == "insecure_rewrite_rule"); + + run_on_modified_server( + [{section: "httpd", + key: "secure_rewrites", + value: "false"}], + function() { + var xhr = CouchDB.request("GET", "/test_suite_db/_design/test/_rewrite/uuids?cache=bust"); + T(xhr.status == 200); + var result = JSON.parse(xhr.responseText); + T(result.uuids.length == 1); + var first = result.uuids[0]; + }); + + }); + +} diff --git a/share/www/script/test/security_validation.js b/share/www/script/test/security_validation.js index d07195e1..42aa11c9 100644 --- a/share/www/script/test/security_validation.js +++ b/share/www/script/test/security_validation.js @@ -13,7 +13,7 @@ couchTests.security_validation = function(debug) { // This tests couchdb's security and validation features. This does // not test authentication, except to use test authentication code made - // specifically for this testing. It is a WWWW-Authenticate scheme named + // specifically for this testing. It is a WWW-Authenticate scheme named // X-Couch-Test-Auth, and the user names and passwords are hard coded // on the server-side. // @@ -21,7 +21,7 @@ couchTests.security_validation = function(debug) { // implementation for Firefox and Safari, and probably other browsers are // broken (Firefox always prompts the user on 401 failures, Safari gives // odd security errors when using different name/passwords, perhaps due - // to cross site scripting prevention). These problems essentially make Basic + // to cross site scripting prevention). These problems essentially make Basic // authentication testing in the browser impossible. But while hard to // test automated in the browser, Basic auth may still useful for real // world use where these bugs/behaviors don't matter. @@ -69,7 +69,13 @@ couchTests.security_validation = function(debug) { var designDoc = { _id:"_design/test", language: "javascript", - validate_doc_update: "(" + (function (newDoc, oldDoc, userCtx) { + validate_doc_update: "(" + (function (newDoc, oldDoc, userCtx, secObj) { + if (secObj.admin_override) { + if (userCtx.roles.indexOf('_admin') != -1) { + // user is admin, they can do anything + return true; + } + } // docs should have an author field. if (!newDoc._deleted && !newDoc.author) { throw {forbidden: @@ -99,13 +105,27 @@ couchTests.security_validation = function(debug) { } // set user as the admin - T(db.setDbProperty("_admins", ["Damien Katz"]).ok); + T(db.setSecObj({ + admins : {names : ["Damien Katz"]} + }).ok); T(userDb.save(designDoc).ok); - // test the _whoami endpoint + var user2Db = new CouchDB("test_suite_db", + {"WWW-Authenticate": "X-Couch-Test-Auth Jan Lehnardt:apple"} + ); + // Attempt to save the design as a non-admin (in replication scenario) + try { + user2Db.save(designDoc, {new_edits : false}); + T(false && "Can't get here. Should have thrown an error on design doc"); + } catch (e) { + T(e.error == "unauthorized"); + T(user2Db.last_req.status == 401); + } + + // test the _session API var resp = userDb.request("GET", "/_session"); - var user = JSON.parse(resp.responseText) + var user = JSON.parse(resp.responseText).userCtx; T(user.name == "Damien Katz"); // test that the roles are listed properly TEquals(user.roles, []); @@ -116,20 +136,23 @@ couchTests.security_validation = function(debug) { doc.foo=2; T(userDb.save(doc).ok); - // Save a document that's missing an author field. - try { - userDb.save({foo:1}); - T(false && "Can't get here. Should have thrown an error 2"); - } catch (e) { - T(e.error == "forbidden"); - T(userDb.last_req.status == 403); + // Save a document that's missing an author field (before and after compaction) + for (var i=0; i<2; i++) { + try { + userDb.save({foo:1}); + T(false && "Can't get here. Should have thrown an error 2"); + } catch (e) { + T(e.error == "forbidden"); + T(userDb.last_req.status == 403); + } + // compact. + T(db.compact().ok); + T(db.last_req.status == 202); + // compaction isn't instantaneous, loop until done + while (db.info().compact_running) {}; } // Now attempt to update the document as a different user, Jan - var user2Db = new CouchDB("test_suite_db", - {"WWW-Authenticate": "X-Couch-Test-Auth Jan Lehnardt:apple"} - ); - var doc = user2Db.open("testdoc"); doc.foo=3; try { @@ -158,6 +181,37 @@ couchTests.security_validation = function(debug) { T(e.error == "unauthorized"); T(userDb.last_req.status == 401); } + + // admin must save with author field unless admin override + var resp = db.request("GET", "/_session"); + var user = JSON.parse(resp.responseText).userCtx; + T(user.name == null); + // test that we are admin + TEquals(user.roles, ["_admin"]); + + // can't save the doc even though we are admin + var doc = db.open("testdoc"); + doc.foo=3; + try { + db.save(doc); + T(false && "Can't get here. Should have thrown an error 3"); + } catch (e) { + T(e.error == "unauthorized"); + T(db.last_req.status == 401); + } + + // now turn on admin override + T(db.setDbProperty("_security", {admin_override : true}).ok); + T(db.save(doc).ok); + + // try to do something lame + try { + db.setDbProperty("_security", ["foo"]); + T(false && "can't do this"); + } catch(e) {} + + // go back to normal + T(db.setDbProperty("_security", {admin_override : false}).ok); // Now delete document T(user2Db.deleteDoc(doc).ok); @@ -188,7 +242,6 @@ couchTests.security_validation = function(debug) { T(db.open("booboo") == null); T(db.open("foofoo") == null); - // Now test replication var AuthHeaders = {"WWW-Authenticate": "X-Couch-Test-Auth Christopher Lenz:dog food"}; var host = CouchDB.host; @@ -197,16 +250,16 @@ couchTests.security_validation = function(debug) { target:"test_suite_db_b"}, {source:"test_suite_db_a", - target:{url: "http://" + host + "/test_suite_db_b", + target:{url: CouchDB.protocol + host + "/test_suite_db_b", headers: AuthHeaders}}, - {source:{url:"http://" + host + "/test_suite_db_a", + {source:{url:CouchDB.protocol + host + "/test_suite_db_a", headers: AuthHeaders}, target:"test_suite_db_b"}, - {source:{url:"http://" + host + "/test_suite_db_a", + {source:{url:CouchDB.protocol + host + "/test_suite_db_a", headers: AuthHeaders}, - target:{url:"http://" + host + "/test_suite_db_b", + target:{url:CouchDB.protocol + host + "/test_suite_db_b", headers: AuthHeaders}}, ] var adminDbA = new CouchDB("test_suite_db_a", {"X-Couch-Full-Commit":"false"}); diff --git a/share/www/script/test/show_documents.js b/share/www/script/test/show_documents.js index f484e029..55ed9698 100644 --- a/share/www/script/test/show_documents.js +++ b/share/www/script/test/show_documents.js @@ -25,7 +25,11 @@ couchTests.show_documents = function(debug) { if (doc) { return "Hello World"; } else { - return "Empty World"; + if(req.id) { + return "New World"; + } else { + return "Empty World"; + } } }), "just-name" : stringFun(function(doc, req) { @@ -50,18 +54,28 @@ couchTests.show_documents = function(debug) { json : req } }), + "show-deleted" : stringFun(function(doc, req) { + if(doc) { + return doc._id; + } else { + return "No doc " + req.id; + } + }), "render-error" : stringFun(function(doc, req) { return noSuchVariable; }), "empty" : stringFun(function(doc, req) { return ""; }), + "fail" : stringFun(function(doc, req) { + return doc._id; + }), "xml-type" : stringFun(function(doc, req) { return { "headers" : { "Content-Type" : "application/xml" }, - "body" : new XML('<xml><node foo="bar"/></xml>') + "body" : new XML('<xml><node foo="bar"/></xml>').toXMLString() } }), "no-set-etag" : stringFun(function(doc, req) { @@ -72,6 +86,25 @@ couchTests.show_documents = function(debug) { "body" : "something" } }), + "list-api" : stringFun(function(doc, req) { + start({"X-Couch-Test-Header": "Yeah"}); + send("Hey"); + }), + "list-api-mix" : stringFun(function(doc, req) { + start({"X-Couch-Test-Header": "Yeah"}); + send("Hey "); + return "Dude"; + }), + "list-api-mix-with-header" : stringFun(function(doc, req) { + start({"X-Couch-Test-Header": "Yeah"}); + send("Hey "); + return { + headers: { + "X-Couch-Test-Header-Awesome": "Oh Yeah!" + }, + body: "Dude" + }; + }), "accept-switch" : stringFun(function(doc, req) { if (req.headers["Accept"].match(/image/)) { return { @@ -114,12 +147,19 @@ couchTests.show_documents = function(debug) { // E4X outside of a string. Outside of tests you // can just use E4X literals. eval('xml.node.@foo = doc.word'); - return xml; + log('xml: '+xml.toSource()); + return xml.toXMLString(); }); provides("foo", function() { return "foofoo"; }); + }), + "withSlash": stringFun(function(doc, req) { + return { json: doc } + }), + "secObj": stringFun(function(doc, req) { + return { json: req.secObj }; }) } }; @@ -157,7 +197,7 @@ couchTests.show_documents = function(debug) { T(xhr.responseText == ""); // // hello template world (non-existing docid) - xhr = CouchDB.request("GET", "/test_suite_db/_design/template/_show/hello/nonExistingDoc"); + xhr = CouchDB.request("GET", "/test_suite_db/_design/template/_show/fail/nonExistingDoc"); T(xhr.status == 404); var resp = JSON.parse(xhr.responseText); T(resp.error == "not_found"); @@ -169,8 +209,7 @@ couchTests.show_documents = function(debug) { // show with missing doc xhr = CouchDB.request("GET", "/test_suite_db/_design/template/_show/just-name/missingdoc"); T(xhr.status == 404); - var resp = JSON.parse(xhr.responseText); - T(resp.error == "not_found"); + TEquals("No such doc", xhr.responseText); // show with missing func xhr = CouchDB.request("GET", "/test_suite_db/_design/template/_show/missing/"+docid); @@ -342,4 +381,56 @@ couchTests.show_documents = function(debug) { xhr = CouchDB.request("GET", "/test_suite_db/_design/template/_show/json/foo"); TEquals(1, JSON.parse(xhr.responseText)._conflicts.length); + var doc3 = {_id:"a/b/c", a:1}; + db.save(doc3); + xhr = CouchDB.request("GET", "/test_suite_db/_design/template/_show/withSlash/a/b/c"); + T(xhr.status == 200); + + // hello template world (non-existing docid) + xhr = CouchDB.request("GET", "/test_suite_db/_design/template/_show/hello/nonExistingDoc"); + T(xhr.responseText == "New World"); + + // test list() compatible API + xhr = CouchDB.request("GET", "/test_suite_db/_design/template/_show/list-api/foo"); + T(xhr.responseText == "Hey"); + TEquals("Yeah", xhr.getResponseHeader("X-Couch-Test-Header"), "header should be cool"); + + xhr = CouchDB.request("GET", "/test_suite_db/_design/template/_show/list-api-mix/foo"); + T(xhr.responseText == "Hey Dude"); + TEquals("Yeah", xhr.getResponseHeader("X-Couch-Test-Header"), "header should be cool"); + + xhr = CouchDB.request("GET", "/test_suite_db/_design/template/_show/list-api-mix-with-header/foo"); + T(xhr.responseText == "Hey Dude"); + TEquals("Yeah", xhr.getResponseHeader("X-Couch-Test-Header"), "header should be cool"); + TEquals("Oh Yeah!", xhr.getResponseHeader("X-Couch-Test-Header-Awesome"), "header should be cool"); + + // test deleted docs + var doc = {_id:"testdoc",foo:1}; + db.save(doc); + var xhr = CouchDB.request("GET", "/test_suite_db/_design/template/_show/show-deleted/testdoc"); + TEquals("testdoc", xhr.responseText, "should return 'testdoc'"); + + db.deleteDoc(doc); + var xhr = CouchDB.request("GET", "/test_suite_db/_design/template/_show/show-deleted/testdoc"); + TEquals("No doc testdoc", xhr.responseText, "should return 'no doc testdoc'"); + + + run_on_modified_server( + [{section: "httpd", + key: "authentication_handlers", + value: "{couch_httpd_auth, special_test_authentication_handler}"}, + {section:"httpd", + key: "WWW-Authenticate", + value: "X-Couch-Test-Auth"}], + + function() { + T(db.setDbProperty("_security", {foo: true}).ok); + T(db.save(doc).ok); + + xhr = CouchDB.request("GET", "/test_suite_db/_design/template/_show/secObj"); + var resp = JSON.parse(xhr.responseText); + T(resp.foo == true); + } + ); + }; diff --git a/share/www/script/test/stats.js b/share/www/script/test/stats.js index 793b390d..6fb0fbba 100644 --- a/share/www/script/test/stats.js +++ b/share/www/script/test/stats.js @@ -30,7 +30,7 @@ couchTests.stats = function(debug) { _id:"_design/test", // turn off couch.js id escaping? language: "javascript", views: { - all_docs: {map: "function(doc) {emit(doc.integer, null);}"}, + all_docs: {map: "function(doc) {emit(doc.integer, null);}"} } }; db.save(designDoc); @@ -81,19 +81,17 @@ couchTests.stats = function(debug) { var pre_dbs = getStat("couchdb", "open_databases").current || 0; var pre_files = getStat("couchdb", "open_os_files").current || 0; - // We have to make sure that as we open the max'th database - // that we've waited for more than 1 second since opening - // the first database so that any delayed commits will be - // flushed. var triggered = false; var db = null; for(var i = 0; i < max*2; i++) { - try { - db = newDb("test_suite_db_" + i, true); - } catch(e) { - triggered = true; - CouchDB.request("GET", "/_sleep?time=1500"); - db = newDb("test_suite_db_" + i, true); + while (true) { + try { + db = newDb("test_suite_db_" + i, true); + break; + } catch(e) { + // all_dbs_active error! + triggered = true; + } } // Trigger a delayed commit @@ -162,12 +160,15 @@ couchTests.stats = function(debug) { runTest("couchdb", "database_writes", { run: function(db) { - CouchDB.request("POST", "/test_suite_db", {body: '{"a": "1"}'}) + CouchDB.request("POST", "/test_suite_db", { + headers: {"Content-Type": "application/json"}, + body: '{"a": "1"}' + }); }, test: function(before, after) { TEquals(before+1, after, "POST'ing new docs increments doc writes."); } - }) + }); runTest("couchdb", "database_writes", { setup: function(db) {db.save({"_id": "test"});}, @@ -246,7 +247,7 @@ couchTests.stats = function(debug) { }); runTest("httpd", "temporary_view_reads", { - run: function(db) {db.query(function(doc) {emit(doc._id)})}, + run: function(db) { db.query(function(doc) { emit(doc._id); }); }, test: function(before, after) { TEquals(before+1, after, "Temporary views have their own counter."); } @@ -260,7 +261,7 @@ couchTests.stats = function(debug) { }); runTest("httpd", "view_reads", { - run: function(db) {db.query(function(doc) {emit(doc._id)});}, + run: function(db) { db.query(function(doc) { emit(doc._id); }); }, test: function(before, after) { TEquals(before, after, "Temporary views don't affect permanent views."); } diff --git a/share/www/script/test/update_documents.js b/share/www/script/test/update_documents.js index 87fc7352..49d3b68a 100644 --- a/share/www/script/test/update_documents.js +++ b/share/www/script/test/update_documents.js @@ -68,10 +68,13 @@ couchTests.update_documents = function(debug) { "headers" : { "Content-Type" : "application/xml" }, - "body" : xml + "body" : xml.toXMLString() }; return [doc, resp]; + }), + "get-uuid" : stringFun(function(doc, req) { + return [null, req.uuid]; }) } }; @@ -110,9 +113,13 @@ couchTests.update_documents = function(debug) { T(JSON.parse(xhr.responseText).error == "method_not_allowed"); // // hello update world (non-existing docid) + xhr = CouchDB.request("GET", "/test_suite_db/nonExistingDoc"); + T(xhr.status == 404); xhr = CouchDB.request("PUT", "/test_suite_db/_design/update/_update/hello/nonExistingDoc"); T(xhr.status == 201); T(xhr.responseText == "<p>New World</p>"); + xhr = CouchDB.request("GET", "/test_suite_db/nonExistingDoc"); + T(xhr.status == 200); // in place update xhr = CouchDB.request("PUT", "/test_suite_db/_design/update/_update/in-place/"+docid+'?field=title&value=test'); @@ -123,7 +130,7 @@ couchTests.update_documents = function(debug) { // bump counter xhr = CouchDB.request("PUT", "/test_suite_db/_design/update/_update/bump-counter/"+docid, { - headers : {"X-Couch-Full-Commit":"false"} + headers : {"X-Couch-Full-Commit":"true"} }); T(xhr.status == 201); T(xhr.responseText == "<h1>bumped it!</h1>"); @@ -135,12 +142,16 @@ couchTests.update_documents = function(debug) { headers : {"X-Couch-Full-Commit":"true"} }); + var NewRev = xhr.getResponseHeader("X-Couch-Update-NewRev"); doc = db.open(docid); + T(doc['_rev'] == NewRev); + + T(doc.counter == 2); // parse xml xhr = CouchDB.request("PUT", "/test_suite_db/_design/update/_update/xml/"+docid, { - headers : {"X-Couch-Full-Commit":"false"}, + headers : {"X-Couch-Full-Commit":"true"}, "body" : '<xml><foo>bar</foo></xml>' }); T(xhr.status == 201); @@ -148,5 +159,10 @@ couchTests.update_documents = function(debug) { doc = db.open(docid); T(doc.via_xml == "bar"); + + // Server provides UUID when POSTing without an ID in the URL + xhr = CouchDB.request("POST", "/test_suite_db/_design/update/_update/get-uuid/"); + T(xhr.status == 200); + T(xhr.responseText.length == 32); }; diff --git a/share/www/script/test/users_db.js b/share/www/script/test/users_db.js index 2cf63fcf..1e13e5d7 100644 --- a/share/www/script/test/users_db.js +++ b/share/www/script/test/users_db.js @@ -24,44 +24,101 @@ couchTests.users_db = function(debug) { // to determine the actual users db name. function testFun() { - usersDb.deleteDb(); - // test that the validation function is installed var ddoc = usersDb.open("_design/_auth"); T(ddoc.validate_doc_update); // test that you can login as a user using basic auth var jchrisUserDoc = CouchDB.prepareUserDoc({ - username: "jchris@apache.org" + name: "jchris@apache.org" }, "funnybone"); T(usersDb.save(jchrisUserDoc).ok); - T(CouchDB.session().name == null); + T(CouchDB.session().userCtx.name == null); + + // test that you can use basic auth aginst the users db var s = CouchDB.session({ headers : { + // base64_encode("jchris@apache.org:funnybone") "Authorization" : "Basic amNocmlzQGFwYWNoZS5vcmc6ZnVubnlib25l" } }); - T(s.name == "jchris@apache.org"); - T(s.user_doc._id == "org.couchdb.user:jchris@apache.org") - T(s.info.authenticated == "{couch_httpd_auth, default_authentication_handler}"); - T(s.info.user_db == "test_suite_users"); - TEquals(["{couch_httpd_oauth, oauth_authentication_handler}", - "{couch_httpd_auth, cookie_authentication_handler}", - "{couch_httpd_auth, default_authentication_handler}"], s.info.handlers); + T(s.userCtx.name == "jchris@apache.org"); + T(s.info.authenticated == "default"); + T(s.info.authentication_db == "test_suite_users"); + TEquals(["oauth", "cookie", "default"], s.info.authentication_handlers); var s = CouchDB.session({ headers : { - "Authorization" : "Basic Xzpf" // username and pass of _:_ + "Authorization" : "Basic Xzpf" // name and pass of _:_ } }); T(s.name == null); - T(s.info.authenticated == "{couch_httpd_auth, default_authentication_handler}"); + T(s.info.authenticated == "default"); + + + // ok, now create a conflicting edit on the jchris doc, and make sure there's no login. + var jchrisUser2 = JSON.parse(JSON.stringify(jchrisUserDoc)); + jchrisUser2.foo = "bar"; + T(usersDb.save(jchrisUser2).ok); + try { + usersDb.save(jchrisUserDoc); + T(false && "should be an update conflict") + } catch(e) { + T(true); + } + // save as bulk with new_edits=false to force conflict save + var resp = usersDb.bulkSave([jchrisUserDoc],{all_or_nothing : true}); + + var jchrisWithConflict = usersDb.open(jchrisUserDoc._id, {conflicts : true}); + T(jchrisWithConflict._conflicts.length == 1) + + // no login with conflicted user doc + try { + var s = CouchDB.session({ + headers : { + "Authorization" : "Basic amNocmlzQGFwYWNoZS5vcmc6ZnVubnlib25l" + } + }); + T(false && "this will throw") + } catch(e) { + T(e.error == "unauthorized") + T(/conflict/.test(e.reason)) + } + + // you can delete a user doc + s = CouchDB.session().userCtx; + T(s.name == null); + T(s.roles.indexOf("_admin") !== -1); + T(usersDb.deleteDoc(jchrisWithConflict).ok); + + // you can't change doc from type "user" + jchrisUserDoc = usersDb.open(jchrisUserDoc._id); + jchrisUserDoc.type = "not user"; + try { + usersDb.save(jchrisUserDoc); + T(false && "should only allow us to save doc when type == 'user'"); + } catch(e) { + T(e.reason == "doc.type must be user"); + } + jchrisUserDoc.type = "user"; + + // "roles" must be an array + jchrisUserDoc.roles = "not an array"; + try { + usersDb.save(jchrisUserDoc); + T(false && "should only allow us to save doc when roles is an array"); + } catch(e) { + T(e.reason == "doc.roles must be an array"); + } + jchrisUserDoc.roles = []; }; - + + usersDb.deleteDb(); run_on_modified_server( [{section: "couch_httpd_auth", - key: "authentication_db", value: "test_suite_users"}], + key: "authentication_db", value: usersDb.name}], testFun ); + usersDb.deleteDb(); // cleanup -}
\ No newline at end of file +} diff --git a/share/www/script/test/uuids.js b/share/www/script/test/uuids.js index 4de7ce90..fc33a105 100644 --- a/share/www/script/test/uuids.js +++ b/share/www/script/test/uuids.js @@ -93,12 +93,20 @@ couchTests.uuids = function(debug) { xhr = CouchDB.request("GET", "/_uuids?count=1000"); T(xhr.status == 200); result = JSON.parse(xhr.responseText); - for(var i = 1; i < result.uuids.length; i++) { - T(result.uuids[i].length == 32); - var u1 = result.uuids[i-1].substr(0, 13); - var u2 = result.uuids[i].substr(0, 13); - T(u1 < u2, "UTC uuids are roughly ordered."); + T(result.uuids[1].length == 32); + + // no collisions + var seen = {}; + for(var i in result.uuids) { + var id = result.uuids[i]; + T(seen[id] === undefined); + seen[id] = 1; } + + // roughly ordered + var u1 = result.uuids[1].substr(0, 13); + var u2 = result.uuids[result.uuids.length-1].substr(0, 13); + T(u1 < u2, "UTC uuids are only roughly ordered, so this assertion may fail occasionally. Don't sweat it."); }; run_on_modified_server([{ diff --git a/share/www/script/test/view_collation.js b/share/www/script/test/view_collation.js index f4ae4a15..b01a5c50 100644 --- a/share/www/script/test/view_collation.js +++ b/share/www/script/test/view_collation.js @@ -90,27 +90,27 @@ couchTests.view_collation = function(debug) { // the inclusive_end=true functionality is limited to endkey currently // if you need inclusive_start=false for startkey, please do implement. ;) var rows = db.query(queryFun, null, {endkey : "b", inclusive_end:true}).rows; - T(rows[rows.length-1].key == "b") + T(rows[rows.length-1].key == "b"); // descending=true var rows = db.query(queryFun, null, {endkey : "b", descending:true, inclusive_end:true}).rows; - T(rows[rows.length-1].key == "b") + T(rows[rows.length-1].key == "b"); // test inclusive_end=false var rows = db.query(queryFun, null, {endkey : "b", inclusive_end:false}).rows; - T(rows[rows.length-1].key == "aa") + T(rows[rows.length-1].key == "aa"); // descending=true var rows = db.query(queryFun, null, {endkey : "b", descending:true, inclusive_end:false}).rows; - T(rows[rows.length-1].key == "B") + T(rows[rows.length-1].key == "B"); var rows = db.query(queryFun, null, { endkey : "b", endkey_docid: "10", inclusive_end:false}).rows; - T(rows[rows.length-1].key == "aa") + T(rows[rows.length-1].key == "aa"); var rows = db.query(queryFun, null, { endkey : "b", endkey_docid: "11", inclusive_end:false}).rows; - T(rows[rows.length-1].key == "b") + T(rows[rows.length-1].key == "b"); }; diff --git a/share/www/script/test/view_collation_raw.js b/share/www/script/test/view_collation_raw.js index 08f37fae..31624cdb 100644 --- a/share/www/script/test/view_collation_raw.js +++ b/share/www/script/test/view_collation_raw.js @@ -97,27 +97,27 @@ couchTests.view_collation_raw = function(debug) { // the inclusive_end=true functionality is limited to endkey currently // if you need inclusive_start=false for startkey, please do implement. ;) var rows = db.view("test/test", {endkey : "b", inclusive_end:true}).rows; - T(rows[rows.length-1].key == "b") + T(rows[rows.length-1].key == "b"); // descending=true var rows = db.view("test/test", {endkey : "b", descending:true, inclusive_end:true}).rows; - T(rows[rows.length-1].key == "b") + T(rows[rows.length-1].key == "b"); // test inclusive_end=false var rows = db.view("test/test", {endkey : "b", inclusive_end:false}).rows; - T(rows[rows.length-1].key == "aa") + T(rows[rows.length-1].key == "aa"); // descending=true var rows = db.view("test/test", {endkey : "b", descending:true, inclusive_end:false}).rows; - T(rows[rows.length-1].key == "ba") + T(rows[rows.length-1].key == "ba"); var rows = db.view("test/test", { endkey : "b", endkey_docid: "10", inclusive_end:false}).rows; - T(rows[rows.length-1].key == "aa") + T(rows[rows.length-1].key == "aa"); var rows = db.view("test/test", { endkey : "b", endkey_docid: "11", inclusive_end:false}).rows; - T(rows[rows.length-1].key == "aa") + T(rows[rows.length-1].key == "aa"); }; diff --git a/share/www/script/test/view_compaction.js b/share/www/script/test/view_compaction.js new file mode 100644 index 00000000..a11fb7bd --- /dev/null +++ b/share/www/script/test/view_compaction.js @@ -0,0 +1,104 @@ +// 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.view_compaction = function(debug) { + + if (debug) debugger; + + var db = new CouchDB("test_suite_db", {"X-Couch-Full-Commit": "true"}); + + db.deleteDb(); + db.createDb(); + + var ddoc = { + _id: "_design/foo", + language: "javascript", + views: { + view1: { + map: "function(doc) { emit(doc._id, doc.value) }" + }, + view2: { + map: "function(doc) { emit(doc._id, doc.value); }", + reduce: "function(keys, values, rereduce) { return sum(values); }" + } + } + }; + T(db.save(ddoc).ok); + + var docs = makeDocs(0, 1000); + db.bulkSave(docs); + + var resp = db.view('foo/view1', {}); + T(resp.rows.length === 1000); + + resp = db.view('foo/view2', {}); + T(resp.rows.length === 1); + + resp = db.designInfo("_design/foo"); + T(resp.view_index.update_seq === 1001); + + + // update docs + for (var i = 0; i < docs.length; i++) { + docs[i].integer = docs[i].integer + 1; + } + db.bulkSave(docs); + + + resp = db.view('foo/view1', {}); + T(resp.rows.length === 1000); + + resp = db.view('foo/view2', {}); + T(resp.rows.length === 1); + + resp = db.designInfo("_design/foo"); + T(resp.view_index.update_seq === 2001); + + + // update docs again... + for (var i = 0; i < docs.length; i++) { + docs[i].integer = docs[i].integer + 2; + } + db.bulkSave(docs); + + + resp = db.view('foo/view1', {}); + T(resp.rows.length === 1000); + + resp = db.view('foo/view2', {}); + T(resp.rows.length === 1); + + resp = db.designInfo("_design/foo"); + T(resp.view_index.update_seq === 3001); + + var disk_size_before_compact = resp.view_index.disk_size; + + // compact view group + var xhr = CouchDB.request("POST", "/" + db.name + "/_compact" + "/foo"); + T(JSON.parse(xhr.responseText).ok === true); + + resp = db.designInfo("_design/foo"); + while (resp.view_index.compact_running === true) { + resp = db.designInfo("_design/foo"); + } + + + resp = db.view('foo/view1', {}); + T(resp.rows.length === 1000); + + resp = db.view('foo/view2', {}); + T(resp.rows.length === 1); + + resp = db.designInfo("_design/foo"); + T(resp.view_index.update_seq === 3001); + T(resp.view_index.disk_size < disk_size_before_compact); +};
\ No newline at end of file diff --git a/share/www/script/test/view_errors.js b/share/www/script/test/view_errors.js index 0f90c46f..e8bd08e4 100644 --- a/share/www/script/test/view_errors.js +++ b/share/www/script/test/view_errors.js @@ -16,8 +16,6 @@ couchTests.view_errors = function(debug) { db.createDb(); if (debug) debugger; - - run_on_modified_server( [{section: "couchdb", key: "os_process_timeout", @@ -26,12 +24,13 @@ couchTests.view_errors = function(debug) { var doc = {integer: 1, string: "1", array: [1, 2, 3]}; T(db.save(doc).ok); - // emitting a key value that is undefined should result in that row not - // being included in the view results + // emitting a key value that is undefined should result in that row + // being included in the view results as null var results = db.query(function(doc) { emit(doc.undef, null); }); - T(results.total_rows == 0); + T(results.total_rows == 1); + T(results.rows[0].key == null); // if a view function throws an exception, its results are not included in // the view index, but the view does not itself raise an error @@ -41,13 +40,13 @@ couchTests.view_errors = function(debug) { T(results.total_rows == 0); // if a view function includes an undefined value in the emitted key or - // value, an error is logged and the result is not included in the view - // index, and the view itself does not raise an error + // value, it is treated as null var results = db.query(function(doc) { emit([doc._id, doc.undef], null); }); - T(results.total_rows == 0); - + T(results.total_rows == 1); + T(results.rows[0].key[1] == null); + // querying a view with invalid params should give a resonable error message var xhr = CouchDB.request("POST", "/test_suite_db/_temp_view?startkey=foo", { headers: {"Content-Type": "application/json"}, @@ -57,14 +56,14 @@ couchTests.view_errors = function(debug) { }); T(JSON.parse(xhr.responseText).error == "bad_request"); - // views should ignore Content-Type, like the rest of CouchDB + // content type must be json var xhr = CouchDB.request("POST", "/test_suite_db/_temp_view", { headers: {"Content-Type": "application/x-www-form-urlencoded"}, body: JSON.stringify({language: "javascript", map : "function(doc){}" }) }); - T(xhr.status == 200); + T(xhr.status == 415); var map = function (doc) {emit(doc.integer, doc.integer);}; @@ -75,9 +74,6 @@ couchTests.view_errors = function(debug) { T(e.error == "query_parse_error"); } - // reduce=false on map views doesn't work, so group=true will - // never throw for temp reduce views. - var designDoc = { _id:"_design/test", language: "javascript", @@ -105,6 +101,15 @@ couchTests.view_errors = function(debug) { db.view("test/no_reduce", {group: true}); T(0 == 1); } catch(e) { + T(db.last_req.status == 400); + T(e.error == "query_parse_error"); + } + + try { + db.view("test/no_reduce", {group_level: 1}); + T(0 == 1); + } catch(e) { + T(db.last_req.status == 400); T(e.error == "query_parse_error"); } @@ -116,10 +121,23 @@ couchTests.view_errors = function(debug) { T(e.error == "query_parse_error"); } + db.view("test/no_reduce", {reduce: false}); + TEquals(200, db.last_req.status, "reduce=false for map views (without" + + " group or group_level) is allowed"); + try { db.view("test/with_reduce", {group: true, reduce: false}); T(0 == 1); } catch(e) { + T(db.last_req.status == 400); + T(e.error == "query_parse_error"); + } + + try { + db.view("test/with_reduce", {group_level: 1, reduce: false}); + T(0 == 1); + } catch(e) { + T(db.last_req.status == 400); T(e.error == "query_parse_error"); } @@ -159,5 +177,13 @@ couchTests.view_errors = function(debug) { T(xhr.status == 500); result = JSON.parse(xhr.responseText); T(result.error == "reduce_overflow_error"); + + try { + db.query(function() {emit(null, null)}, null, {startkey: 2, endkey:1}); + T(0 == 1); + } catch(e) { + T(e.error == "query_parse_error"); + T(e.reason.match(/no rows can match/i)); + } }); }; diff --git a/share/www/script/test/view_multi_key_all_docs.js b/share/www/script/test/view_multi_key_all_docs.js index 62e49665..1113be4d 100644 --- a/share/www/script/test/view_multi_key_all_docs.js +++ b/share/www/script/test/view_multi_key_all_docs.js @@ -25,24 +25,52 @@ couchTests.view_multi_key_all_docs = function(debug) { for(var i=0; i<rows.length; i++) T(rows[i].id == keys[i]); + // keys in GET parameters + rows = db.allDocs({keys:keys}, null).rows; + T(rows.length == keys.length); + for(var i=0; i<rows.length; i++) + T(rows[i].id == keys[i]); + rows = db.allDocs({limit: 1}, keys).rows; T(rows.length == 1); T(rows[0].id == keys[0]); + // keys in GET parameters + rows = db.allDocs({limit: 1, keys: keys}, null).rows; + T(rows.length == 1); + T(rows[0].id == keys[0]); + rows = db.allDocs({skip: 2}, keys).rows; T(rows.length == 3); for(var i=0; i<rows.length; i++) T(rows[i].id == keys[i+2]); + // keys in GET parameters + rows = db.allDocs({skip: 2, keys: keys}, null).rows; + T(rows.length == 3); + for(var i=0; i<rows.length; i++) + T(rows[i].id == keys[i+2]); + rows = db.allDocs({descending: "true"}, keys).rows; T(rows.length == keys.length); for(var i=0; i<rows.length; i++) T(rows[i].id == keys[keys.length-i-1]); + // keys in GET parameters + rows = db.allDocs({descending: "true", keys: keys}, null).rows; + T(rows.length == keys.length); + for(var i=0; i<rows.length; i++) + T(rows[i].id == keys[keys.length-i-1]); + rows = db.allDocs({descending: "true", skip: 3, limit:1}, keys).rows; T(rows.length == 1); T(rows[0].id == keys[1]); + // keys in GET parameters + rows = db.allDocs({descending: "true", skip: 3, limit:1, keys: keys}, null).rows; + T(rows.length == 1); + T(rows[0].id == keys[1]); + // Check we get invalid rows when the key doesn't exist rows = db.allDocs({}, [1, "i_dont_exist", "0"]).rows; T(rows.length == 3); @@ -51,4 +79,13 @@ couchTests.view_multi_key_all_docs = function(debug) { T(rows[1].error == "not_found"); T(!rows[1].id); T(rows[2].id == rows[2].key && rows[2].key == "0"); + + // keys in GET parameters + rows = db.allDocs({keys: [1, "i_dont_exist", "0"]}, null).rows; + T(rows.length == 3); + T(rows[0].error == "not_found"); + T(!rows[0].id); + T(rows[1].error == "not_found"); + T(!rows[1].id); + T(rows[2].id == rows[2].key && rows[2].key == "0"); }; diff --git a/share/www/script/test/view_multi_key_design.js b/share/www/script/test/view_multi_key_design.js index 5a2f645d..38396955 100644 --- a/share/www/script/test/view_multi_key_design.js +++ b/share/www/script/test/view_multi_key_design.js @@ -34,11 +34,11 @@ couchTests.view_multi_key_design = function(debug) { reduce:"function (keys, values) { return sum(values); };" } } - } + }; T(db.save(designDoc).ok); // Test that missing keys work too - var keys = [101,30,15,37,50] + var keys = [101,30,15,37,50]; var reduce = db.view("test/summate",{group:true},keys).rows; T(reduce.length == keys.length-1); // 101 is missing for(var i=0; i<reduce.length; i++) { @@ -54,6 +54,13 @@ couchTests.view_multi_key_design = function(debug) { T(rows[i].key == rows[i].value); } + // with GET keys + rows = db.view("test/all_docs",{keys:keys},null).rows; + for(var i=0;i<rows.length; i++) { + T(keys.indexOf(rows[i].key) != -1); + T(rows[i].key == rows[i].value); + } + var reduce = db.view("test/summate",{group:true},keys).rows; T(reduce.length == keys.length); for(var i=0; i<reduce.length; i++) { @@ -61,8 +68,18 @@ couchTests.view_multi_key_design = function(debug) { T(reduce[i].key == reduce[i].value); } + // with GET keys + reduce = db.view("test/summate",{group:true,keys:keys},null).rows; + T(reduce.length == keys.length); + for(var i=0; i<reduce.length; i++) { + T(keys.indexOf(reduce[i].key) != -1); + T(reduce[i].key == reduce[i].value); + } + // Test that invalid parameter combinations get rejected var badargs = [{startkey:0}, {endkey:0}, {key: 0}, {group_level: 2}]; + var getbadargs = [{startkey:0, keys:keys}, {endkey:0, keys:keys}, + {key:0, keys:keys}, {group_level: 2, keys:keys}]; for(var i in badargs) { try { @@ -71,6 +88,13 @@ couchTests.view_multi_key_design = function(debug) { } catch (e) { T(e.error == "query_parse_error"); } + + try { + db.view("test/all_docs",getbadargs[i],null); + T(0==1); + } catch (e) { + T(e.error = "query_parse_error"); + } } try { @@ -80,8 +104,18 @@ couchTests.view_multi_key_design = function(debug) { T(e.error == "query_parse_error"); } + try { + db.view("test/summate",{keys:keys},null); + T(0==1); + } catch (e) { + T(e.error == "query_parse_error"); + } + // Test that a map & reduce containing func support keys when reduce=false - resp = db.view("test/summate", {reduce: false}, keys); + var resp = db.view("test/summate", {reduce: false}, keys); + T(resp.rows.length == 5); + + resp = db.view("test/summate", {reduce: false, keys: keys}, null); T(resp.rows.length == 5); // Check that limiting by startkey_docid and endkey_docid get applied @@ -96,34 +130,66 @@ couchTests.view_multi_key_design = function(debug) { T(curr[i].value == exp_val[i]); } + curr = db.view("test/multi_emit", {startkey_docid: 21, endkey_docid: 23, keys: [0, 2]}, null).rows; + T(curr.length == 6); + for( var i = 0 ; i < 6 ; i++) + { + T(curr[i].key == exp_key[i]); + T(curr[i].value == exp_val[i]); + } + // Check limit works curr = db.view("test/all_docs", {limit: 1}, keys).rows; T(curr.length == 1); T(curr[0].key == 10); + curr = db.view("test/all_docs", {limit: 1, keys: keys}, null).rows; + T(curr.length == 1); + T(curr[0].key == 10); + // Check offset works curr = db.view("test/multi_emit", {skip: 1}, [0]).rows; T(curr.length == 99); T(curr[0].value == 1); + curr = db.view("test/multi_emit", {skip: 1, keys: [0]}, null).rows; + T(curr.length == 99); + T(curr[0].value == 1); + // Check that dir works curr = db.view("test/multi_emit", {descending: "true"}, [1]).rows; T(curr.length == 100); T(curr[0].value == 99); T(curr[99].value == 0); + curr = db.view("test/multi_emit", {descending: "true", keys: [1]}, null).rows; + T(curr.length == 100); + T(curr[0].value == 99); + T(curr[99].value == 0); + // Check a couple combinations curr = db.view("test/multi_emit", {descending: "true", skip: 3, limit: 2}, [2]).rows; T(curr.length, 2); T(curr[0].value == 96); T(curr[1].value == 95); + curr = db.view("test/multi_emit", {descending: "true", skip: 3, limit: 2, keys: [2]}, null).rows; + T(curr.length, 2); + T(curr[0].value == 96); + T(curr[1].value == 95); + curr = db.view("test/multi_emit", {skip: 2, limit: 3, startkey_docid: "13"}, [0]).rows; T(curr.length == 3); T(curr[0].value == 15); T(curr[1].value == 16); T(curr[2].value == 17); + curr = db.view("test/multi_emit", {skip: 2, limit: 3, startkey_docid: "13", keys: [0]}, null).rows; + T(curr.length == 3); + T(curr[0].value == 15); + T(curr[1].value == 16); + T(curr[2].value == 17); + curr = db.view("test/multi_emit", {skip: 1, limit: 5, startkey_docid: "25", endkey_docid: "27"}, [1]).rows; T(curr.length == 2); @@ -131,8 +197,20 @@ couchTests.view_multi_key_design = function(debug) { T(curr[1].value == 27); curr = db.view("test/multi_emit", + {skip: 1, limit: 5, startkey_docid: "25", endkey_docid: "27", keys: [1]}, null).rows; + T(curr.length == 2); + T(curr[0].value == 26); + T(curr[1].value == 27); + + curr = db.view("test/multi_emit", {skip: 1, limit: 5, startkey_docid: "28", endkey_docid: "26", descending: "true"}, [1]).rows; T(curr.length == 2); T(curr[0].value == 27); T(curr[1].value == 26); + + curr = db.view("test/multi_emit", + {skip: 1, limit: 5, startkey_docid: "28", endkey_docid: "26", descending: "true", keys: [1]}, null).rows; + T(curr.length == 2); + T(curr[0].value == 27); + T(curr[1].value == 26); }; diff --git a/share/www/script/test/view_pagination.js b/share/www/script/test/view_pagination.js index 1af2df35..ed3a7ee1 100644 --- a/share/www/script/test/view_pagination.js +++ b/share/www/script/test/view_pagination.js @@ -19,7 +19,7 @@ couchTests.view_pagination = function(debug) { var docs = makeDocs(0, 100); db.bulkSave(docs); - var queryFun = function(doc) { emit(doc.integer, null) }; + var queryFun = function(doc) { emit(doc.integer, null); }; var i; // page through the view ascending @@ -29,13 +29,26 @@ couchTests.view_pagination = function(debug) { startkey_docid: i, limit: 10 }); - T(queryResults.rows.length == 10) - T(queryResults.total_rows == docs.length) - T(queryResults.offset == i) + T(queryResults.rows.length == 10); + T(queryResults.total_rows == docs.length); + T(queryResults.offset == i); var j; for (j = 0; j < 10;j++) { T(queryResults.rows[j].key == i + j); } + + // test aliases start_key and start_key_doc_id + queryResults = db.query(queryFun, null, { + start_key: i, + start_key_doc_id: i, + limit: 10 + }); + T(queryResults.rows.length == 10); + T(queryResults.total_rows == docs.length); + T(queryResults.offset == i); + for (j = 0; j < 10;j++) { + T(queryResults.rows[j].key == i + j); + } } // page through the view descending @@ -46,9 +59,9 @@ couchTests.view_pagination = function(debug) { descending: true, limit: 10 }); - T(queryResults.rows.length == 10) - T(queryResults.total_rows == docs.length) - T(queryResults.offset == docs.length - i - 1) + T(queryResults.rows.length == 10); + T(queryResults.total_rows == docs.length); + T(queryResults.offset == docs.length - i - 1); var j; for (j = 0; j < 10; j++) { T(queryResults.rows[j].key == i - j); @@ -63,60 +76,72 @@ couchTests.view_pagination = function(debug) { descending: false, limit: 10 }); - T(queryResults.rows.length == 10) - T(queryResults.total_rows == docs.length) - T(queryResults.offset == i) + T(queryResults.rows.length == 10); + T(queryResults.total_rows == docs.length); + T(queryResults.offset == i); var j; for (j = 0; j < 10;j++) { T(queryResults.rows[j].key == i + j); } } + function testEndkeyDocId(queryResults) { + T(queryResults.rows.length == 35); + T(queryResults.total_rows == docs.length); + T(queryResults.offset == 1); + T(queryResults.rows[0].id == "1"); + T(queryResults.rows[1].id == "10"); + T(queryResults.rows[2].id == "11"); + T(queryResults.rows[3].id == "12"); + T(queryResults.rows[4].id == "13"); + T(queryResults.rows[5].id == "14"); + T(queryResults.rows[6].id == "15"); + T(queryResults.rows[7].id == "16"); + T(queryResults.rows[8].id == "17"); + T(queryResults.rows[9].id == "18"); + T(queryResults.rows[10].id == "19"); + T(queryResults.rows[11].id == "2"); + T(queryResults.rows[12].id == "20"); + T(queryResults.rows[13].id == "21"); + T(queryResults.rows[14].id == "22"); + T(queryResults.rows[15].id == "23"); + T(queryResults.rows[16].id == "24"); + T(queryResults.rows[17].id == "25"); + T(queryResults.rows[18].id == "26"); + T(queryResults.rows[19].id == "27"); + T(queryResults.rows[20].id == "28"); + T(queryResults.rows[21].id == "29"); + T(queryResults.rows[22].id == "3"); + T(queryResults.rows[23].id == "30"); + T(queryResults.rows[24].id == "31"); + T(queryResults.rows[25].id == "32"); + T(queryResults.rows[26].id == "33"); + T(queryResults.rows[27].id == "34"); + T(queryResults.rows[28].id == "35"); + T(queryResults.rows[29].id == "36"); + T(queryResults.rows[30].id == "37"); + T(queryResults.rows[31].id == "38"); + T(queryResults.rows[32].id == "39"); + T(queryResults.rows[33].id == "4"); + T(queryResults.rows[34].id == "40"); + } + // test endkey_docid - var queryResults = db.query(function(doc) { emit(null, null);}, null, { + var queryResults = db.query(function(doc) { emit(null, null); }, null, { startkey: null, startkey_docid: 1, endkey: null, endkey_docid: 40 }); + testEndkeyDocId(queryResults); - T(queryResults.rows.length == 35) - T(queryResults.total_rows == docs.length) - T(queryResults.offset == 1) - T(queryResults.rows[0].id == "1"); - T(queryResults.rows[1].id == "10"); - T(queryResults.rows[2].id == "11"); - T(queryResults.rows[3].id == "12"); - T(queryResults.rows[4].id == "13"); - T(queryResults.rows[5].id == "14"); - T(queryResults.rows[6].id == "15"); - T(queryResults.rows[7].id == "16"); - T(queryResults.rows[8].id == "17"); - T(queryResults.rows[9].id == "18"); - T(queryResults.rows[10].id == "19"); - T(queryResults.rows[11].id == "2"); - T(queryResults.rows[12].id == "20"); - T(queryResults.rows[13].id == "21"); - T(queryResults.rows[14].id == "22"); - T(queryResults.rows[15].id == "23"); - T(queryResults.rows[16].id == "24"); - T(queryResults.rows[17].id == "25"); - T(queryResults.rows[18].id == "26"); - T(queryResults.rows[19].id == "27"); - T(queryResults.rows[20].id == "28"); - T(queryResults.rows[21].id == "29"); - T(queryResults.rows[22].id == "3"); - T(queryResults.rows[23].id == "30"); - T(queryResults.rows[24].id == "31"); - T(queryResults.rows[25].id == "32"); - T(queryResults.rows[26].id == "33"); - T(queryResults.rows[27].id == "34"); - T(queryResults.rows[28].id == "35"); - T(queryResults.rows[29].id == "36"); - T(queryResults.rows[30].id == "37"); - T(queryResults.rows[31].id == "38"); - T(queryResults.rows[32].id == "39"); - T(queryResults.rows[33].id == "4"); - T(queryResults.rows[34].id == "40"); + // test aliases end_key_doc_id and end_key + queryResults = db.query(function(doc) { emit(null, null); }, null, { + start_key: null, + start_key_doc_id: 1, + end_key: null, + end_key_doc_id: 40 + }); + testEndkeyDocId(queryResults); }; diff --git a/share/www/script/test/view_sandboxing.js b/share/www/script/test/view_sandboxing.js index 9f893b28..02951d9f 100644 --- a/share/www/script/test/view_sandboxing.js +++ b/share/www/script/test/view_sandboxing.js @@ -42,11 +42,99 @@ couchTests.view_sandboxing = function(debug) { // make sure that a view cannot access the map_funs array defined used by // the view server - var results = db.query(function(doc) { map_funs.push(1); emit(null, doc) }); + var results = db.query(function(doc) { map_funs.push(1); emit(null, doc); }); T(results.total_rows == 0); // make sure that a view cannot access the map_results array defined used by // the view server - var results = db.query(function(doc) { map_results.push(1); emit(null, doc) }); + var results = db.query(function(doc) { map_results.push(1); emit(null, doc); }); T(results.total_rows == 0); + + // test for COUCHDB-925 + // altering 'doc' variable in map function affects other map functions + var ddoc = { + _id: "_design/foobar", + language: "javascript", + views: { + view1: { + map: + (function(doc) { + if (doc.values) { + doc.values = [666]; + } + if (doc.tags) { + doc.tags.push("qwerty"); + } + if (doc.tokens) { + doc.tokens["c"] = 3; + } + }).toString() + }, + view2: { + map: + (function(doc) { + if (doc.values) { + emit(doc._id, doc.values); + } + if (doc.tags) { + emit(doc._id, doc.tags); + } + if (doc.tokens) { + emit(doc._id, doc.tokens); + } + }).toString() + } + } + }; + var doc1 = { + _id: "doc1", + values: [1, 2, 3] + }; + var doc2 = { + _id: "doc2", + tags: ["foo", "bar"], + tokens: {a: 1, b: 2} + }; + + db.deleteDb(); + db.createDb(); + T(db.save(ddoc).ok); + T(db.save(doc1).ok); + T(db.save(doc2).ok); + + var view1Results = db.view( + "foobar/view1", {bypass_cache: Math.round(Math.random() * 1000)}); + var view2Results = db.view( + "foobar/view2", {bypass_cache: Math.round(Math.random() * 1000)}); + + TEquals(0, view1Results.rows.length, "view1 has 0 rows"); + TEquals(3, view2Results.rows.length, "view2 has 3 rows"); + + TEquals(doc1._id, view2Results.rows[0].key); + TEquals(doc2._id, view2Results.rows[1].key); + TEquals(doc2._id, view2Results.rows[2].key); + + // https://bugzilla.mozilla.org/show_bug.cgi?id=449657 + TEquals(3, view2Results.rows[0].value.length, + "Warning: installed SpiderMonkey version doesn't allow sealing of arrays"); + if (view2Results.rows[0].value.length === 3) { + TEquals(1, view2Results.rows[0].value[0]); + TEquals(2, view2Results.rows[0].value[1]); + TEquals(3, view2Results.rows[0].value[2]); + } + + TEquals(1, view2Results.rows[1].value["a"]); + TEquals(2, view2Results.rows[1].value["b"]); + TEquals('undefined', typeof view2Results.rows[1].value["c"], + "doc2.tokens object was not sealed"); + + TEquals(2, view2Results.rows[2].value.length, + "Warning: installed SpiderMonkey version doesn't allow sealing of arrays"); + if (view2Results.rows[2].value.length === 2) { + TEquals("foo", view2Results.rows[2].value[0]); + TEquals("bar", view2Results.rows[2].value[1]); + } + + // cleanup + db.deleteDb(); }; diff --git a/share/www/script/test/view_update_seq.js b/share/www/script/test/view_update_seq.js new file mode 100644 index 00000000..69b8c42d --- /dev/null +++ b/share/www/script/test/view_update_seq.js @@ -0,0 +1,106 @@ +// 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.view_update_seq = function(debug) { + var db = new CouchDB("test_suite_db", {"X-Couch-Full-Commit":"true"}); + db.deleteDb(); + db.createDb(); + if (debug) debugger; + + T(db.info().update_seq == 0); + + var resp = db.allDocs({update_seq:true}); + + T(resp.rows.length == 0); + T(resp.update_seq == 0); + + var designDoc = { + _id:"_design/test", + language: "javascript", + views: { + all_docs: { + map: "function(doc) { emit(doc.integer, doc.string) }" + }, + summate: { + map:"function (doc) {emit(doc.integer, doc.integer)};", + reduce:"function (keys, values) { return sum(values); };" + } + } + }; + T(db.save(designDoc).ok); + + T(db.info().update_seq == 1); + + resp = db.allDocs({update_seq:true}); + + T(resp.rows.length == 1); + T(resp.update_seq == 1); + + var docs = makeDocs(0, 100); + db.bulkSave(docs); + + resp = db.allDocs({limit: 1}); + T(resp.rows.length == 1); + T(!resp.update_seq, "all docs"); + + resp = db.allDocs({limit: 1, update_seq:true}); + T(resp.rows.length == 1); + T(resp.update_seq == 101); + + resp = db.view('test/all_docs', {limit: 1, update_seq:true}); + T(resp.rows.length == 1); + T(resp.update_seq == 101); + + resp = db.view('test/all_docs', {limit: 1, update_seq:false}); + T(resp.rows.length == 1); + T(!resp.update_seq, "view"); + + resp = db.view('test/summate', {update_seq:true}); + T(resp.rows.length == 1); + T(resp.update_seq == 101); + + db.save({"id":"0"}); + resp = db.view('test/all_docs', {limit: 1,stale: "ok", update_seq:true}); + T(resp.rows.length == 1); + T(resp.update_seq == 101); + + db.save({"id":"00"}); + resp = db.view('test/all_docs', + {limit: 1, stale: "update_after", update_seq: true}); + T(resp.rows.length == 1); + T(resp.update_seq == 101); + + // wait 5 seconds for the next assertions to pass in very slow machines + var t0 = new Date(), t1; + do { + CouchDB.request("GET", "/"); + t1 = new Date(); + } while ((t1 - t0) < 5000); + + resp = db.view('test/all_docs', {limit: 1, stale: "ok", update_seq: true}); + T(resp.rows.length == 1); + T(resp.update_seq == 103); + + resp = db.view('test/all_docs', {limit: 1, update_seq:true}); + T(resp.rows.length == 1); + T(resp.update_seq == 103); + + resp = db.view('test/all_docs',{update_seq:true},["0","1"]); + T(resp.update_seq == 103); + + resp = db.view('test/all_docs',{update_seq:true},["0","1"]); + T(resp.update_seq == 103); + + resp = db.view('test/summate',{group:true, update_seq:true},["0","1"]); + T(resp.update_seq == 103); + +}; diff --git a/share/www/script/test/view_xml.js b/share/www/script/test/view_xml.js index 451fb6a8..3403b47c 100644 --- a/share/www/script/test/view_xml.js +++ b/share/www/script/test/view_xml.js @@ -22,7 +22,7 @@ couchTests.view_xml = function(debug) { var results = db.query( "function(doc) {\n" + " var xml = new XML(doc.content);\n" + - " emit(xml.title.text(), null);\n" + + " emit(xml.title.text().toXMLString(), null);\n" + "}"); T(results.total_rows == 2); T(results.rows[0].key == "Testing E4X"); @@ -31,7 +31,7 @@ couchTests.view_xml = function(debug) { var results = db.query( "function(doc) {\n" + " var xml = new XML(doc.content);\n" + - " emit(xml.title.@id, null);\n" + + " emit(xml.title.@id.toXMLString(), null);\n" + "}"); T(results.total_rows == 2); T(results.rows[0].key == "e4x"); |