You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@couchdb.apache.org by ja...@apache.org on 2014/10/11 15:38:54 UTC

[02/38] Move JS tests to test/javascript/tests

http://git-wip-us.apache.org/repos/asf/couchdb/blob/013ee66e/test/javascript/tests/rev_stemming.js
----------------------------------------------------------------------
diff --git a/test/javascript/tests/rev_stemming.js b/test/javascript/tests/rev_stemming.js
new file mode 100644
index 0000000..954da79
--- /dev/null
+++ b/test/javascript/tests/rev_stemming.js
@@ -0,0 +1,110 @@
+// 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.rev_stemming = function(debug) {
+  var db = new CouchDB("test_suite_db", {"X-Couch-Full-Commit":"false"});
+  db.deleteDb();
+  db.createDb();
+  var db = new CouchDB("test_suite_db_a", {"X-Couch-Full-Commit":"false"});
+  db.deleteDb();
+  db.createDb();
+  var dbB = new CouchDB("test_suite_db_b", {"X-Couch-Full-Commit":"false"});
+  dbB.deleteDb();
+  dbB.createDb();
+  if (debug) debugger;
+
+  var newLimit = 5;
+
+  T(db.getDbProperty("_revs_limit") == 1000);
+
+  // Make an invalid request to _revs_limit
+  // Should return 400
+  var xhr = CouchDB.request("PUT", "/test_suite_db/_revs_limit", {body:"\"foo\""});
+  T(xhr.status == 400);
+  var result = JSON.parse(xhr.responseText);
+  T(result.error == "bad_request");
+  T(result.reason == "Rev limit has to be an integer");
+
+  var doc = {_id:"foo",foo:0}
+  for( var i=0; i < newLimit + 1; i++) {
+    doc.foo++;
+    T(db.save(doc).ok);
+  }
+  var doc0 = db.open("foo", {revs:true});
+  T(doc0._revisions.ids.length == newLimit + 1);
+
+  var docBar = {_id:"bar",foo:0}
+  for( var i=0; i < newLimit + 1; i++) {
+    docBar.foo++;
+    T(db.save(docBar).ok);
+  }
+  T(db.open("bar", {revs:true})._revisions.ids.length == newLimit + 1);
+
+  T(db.setDbProperty("_revs_limit", newLimit).ok);
+
+  for( var i=0; i < newLimit + 1; i++) {
+    doc.foo++;
+    T(db.save(doc).ok);
+  }
+  doc0 = db.open("foo", {revs:true});
+  T(doc0._revisions.ids.length == newLimit);
+
+
+  // If you replicate after you make more edits than the limit, you'll
+  // cause a spurious edit conflict.
+  CouchDB.replicate("test_suite_db_a", "test_suite_db_b");
+  var docB1 = dbB.open("foo",{conflicts:true})
+  T(docB1._conflicts == null);
+
+  for( var i=0; i < newLimit - 1; i++) {
+    doc.foo++;
+    T(db.save(doc).ok);
+  }
+
+  // one less edit than limit, no conflict
+  CouchDB.replicate("test_suite_db_a", "test_suite_db_b");
+  var docB1 = dbB.open("foo",{conflicts:true})
+  T(docB1._conflicts == null);
+
+  //now we hit the limit
+  for( var i=0; i < newLimit; i++) {
+    doc.foo++;
+    T(db.save(doc).ok);
+  }
+
+  CouchDB.replicate("test_suite_db_a", "test_suite_db_b");
+
+  var docB2 = dbB.open("foo",{conflicts:true});
+
+  // we have a conflict, but the previous replicated rev is always the losing
+  // conflict
+  T(docB2._conflicts[0] == docB1._rev)
+
+  // We having already updated bar before setting the limit, so it's still got
+  // a long rev history. compact to stem the revs.
+
+  T(db.open("bar", {revs:true})._revisions.ids.length == newLimit + 1);
+
+  T(db.compact().ok);
+
+  // compaction isn't instantaneous, loop until done
+  while (db.info().compact_running) {};
+
+  // force reload because ETags don't honour compaction
+  var req = db.request("GET", "/test_suite_db_a/bar?revs=true", {
+    headers:{"if-none-match":"pommes"}
+  });
+
+  var finalDoc = JSON.parse(req.responseText);
+  TEquals(newLimit, finalDoc._revisions.ids.length,
+    "should return a truncated revision list");
+};

http://git-wip-us.apache.org/repos/asf/couchdb/blob/013ee66e/test/javascript/tests/rewrite.js
----------------------------------------------------------------------
diff --git a/test/javascript/tests/rewrite.js b/test/javascript/tests/rewrite.js
new file mode 100644
index 0000000..5c56fa5
--- /dev/null
+++ b/test/javascript/tests/rewrite.js
@@ -0,0 +1,505 @@
+// 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) {
+  if (debug) debugger;
+  var dbNames = ["test_suite_db", "test_suite_db/with_slashes"];
+  for (var i=0; i < dbNames.length; i++) {
+    var db = new CouchDB(dbNames[i]);
+    var dbName = encodeURIComponent(dbNames[i]);
+    db.deleteDb();
+    db.createDb();
+  
+    
+    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": "/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"
+              },
+              "formats": {
+                "start": "int",
+                "end": "int"
+              }
+            },
+            {
+              "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": "simpleForm/complexView7/:a/:b",
+              "to": "_view/complexView3",
+              "query": {
+                "key": [":a", ":b"],
+                "include_docs": ":doc"
+              },
+              "format": {
+                "doc": "bool"
+              }
+
+            },
+            {
+              "from": "/",
+              "to": "_view/basicView",
+            },
+            {
+              "from": "/db/*",
+              "to": "../../*"
+            }
+          ],
+          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"];
+            })
+          },
+          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", "/"+dbName+"/_design/test/_rewrite/foo");
+        T(req.responseText == "This is a base64 encoded text");
+        T(req.getResponseHeader("Content-Type") == "text/plain");
+        
+        req = CouchDB.request("GET", "/"+dbName+"/_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", "/"+dbName+"/_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", "/"+dbName+"/_design/test/_rewrite/welcome?name=user");
+        T(req.responseText == "Welcome user");
+        
+        req = CouchDB.request("GET", "/"+dbName+"/_design/test/_rewrite/welcome/user");
+        T(req.responseText == "Welcome user");
+        
+        req = CouchDB.request("GET", "/"+dbName+"/_design/test/_rewrite/welcome2");
+        T(req.responseText == "Welcome user");
+        
+        xhr = CouchDB.request("PUT", "/"+dbName+"/_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", "/"+dbName+"/_design/test/_rewrite/welcome3/test");
+        T(xhr.responseText == "Welcome test");
+
+        req = CouchDB.request("GET", "/"+dbName+"/_design/test/_rewrite/welcome4/user");
+        T(req.responseText == "Welcome user");
+
+        req = CouchDB.request("GET", "/"+dbName+"/_design/test/_rewrite/welcome5/welcome3");
+        T(req.responseText == "Welcome welcome3");
+        
+        xhr = CouchDB.request("GET", "/"+dbName+"/_design/test/_rewrite/basicView");
+        T(xhr.status == 200, "view call");
+        T(/{"total_rows":9/.test(xhr.responseText)); 
+
+        xhr = CouchDB.request("GET", "/"+dbName+"/_design/test/_rewrite/");
+        T(xhr.status == 200, "view call");
+        T(/{"total_rows":9/.test(xhr.responseText)); 
+
+        
+        // get with query params
+        xhr = CouchDB.request("GET", "/"+dbName+"/_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", "/"+dbName+"/_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", "/"+dbName+"/_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", "/"+dbName+"/_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", "/"+dbName+"/_design/test/_rewrite/simpleForm/complexView");
+        T(xhr.status == 200, "with query params");
+        T(/FirstKey: [1, 2]/.test(xhr.responseText));
+        
+        xhr = CouchDB.request("GET", "/"+dbName+"/_design/test/_rewrite/simpleForm/complexView2");
+        T(xhr.status == 200, "with query params");
+        T(/Value: doc 3/.test(xhr.responseText));
+        
+        xhr = CouchDB.request("GET", "/"+dbName+"/_design/test/_rewrite/simpleForm/complexView3");
+        T(xhr.status == 200, "with query params");
+        T(/Value: doc 4/.test(xhr.responseText));
+        
+        xhr = CouchDB.request("GET", "/"+dbName+"/_design/test/_rewrite/simpleForm/complexView4");
+        T(xhr.status == 200, "with query params");
+        T(/Value: doc 5/.test(xhr.responseText));
+        
+        xhr = CouchDB.request("GET", "/"+dbName+"/_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", "/"+dbName+"/_design/test/_rewrite/simpleForm/complexView6?a=test&b=essai");
+        T(xhr.status == 200, "with query params");
+        T(/Value: doc 4/.test(xhr.responseText));
+
+        xhr = CouchDB.request("GET", "/"+dbName+"/_design/test/_rewrite/simpleForm/complexView7/test/essai?doc=true");
+        T(xhr.status == 200, "with query params");
+        var result = JSON.parse(xhr.responseText);
+        T(typeof(result.rows[0].doc) === "object");
+        
+        // COUCHDB-2031 - path normalization versus qs params
+        xhr = CouchDB.request("GET", "/"+dbName+"/_design/test/_rewrite/db/_design/test?meta=true");
+        T(xhr.status == 200, "path normalization works with qs params");
+        var result = JSON.parse(xhr.responseText);
+        T(result['_id'] == "_design/test");
+        T(typeof(result['_revs_info']) === "object");
+
+        // test path relative to server
+        designDoc.rewrites.push({
+          "from": "uuids",
+          "to": "../../../_uuids"
+        });
+        T(db.save(designDoc).ok);
+        
+        var xhr = CouchDB.request("GET", "/"+dbName+"/_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", "/"+dbName+"/_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];
+          });
+      });
+
+    // test invalid rewrites
+    // string
+    var ddoc = {
+      _id: "_design/invalid",
+      rewrites: "[{\"from\":\"foo\",\"to\":\"bar\"}]"
+    }
+    db.save(ddoc);
+    var res = CouchDB.request("GET", "/"+dbName+"/_design/invalid/_rewrite/foo");
+    TEquals(400, res.status, "should return 400");
+
+    var ddoc_requested_path = {
+      _id: "_design/requested_path",
+      rewrites:[
+        {"from": "show", "to": "_show/origin/0"},
+        {"from": "show_rewritten", "to": "_rewrite/show"}
+      ],
+      shows: {
+        origin: stringFun(function(doc, req) {
+          return req.headers["x-couchdb-requested-path"];
+        })}
+    };
+
+    db.save(ddoc_requested_path);
+    var url = "/"+dbName+"/_design/requested_path/_rewrite/show";
+    var res = CouchDB.request("GET", url);
+    TEquals(url, res.responseText, "should return the original url");
+
+    var url = "/"+dbName+"/_design/requested_path/_rewrite/show_rewritten";
+    var res = CouchDB.request("GET", url);
+    TEquals(url, res.responseText, "returned the original url");
+
+    var ddoc_loop = {
+      _id: "_design/loop",
+      rewrites: [{ "from": "loop",  "to": "_rewrite/loop"}]
+    };
+    db.save(ddoc_loop);
+
+    // Assert loop detection
+    run_on_modified_server(
+      [{section: "httpd",
+        key: "rewrite_limit",
+        value: "2"}],
+      function(){
+        var url = "/"+dbName+"/_design/loop/_rewrite/loop";
+        var xhr = CouchDB.request("GET", url);
+        TEquals(400, xhr.status);
+      });
+
+    // Assert serial execution is not spuriously counted as loop
+    run_on_modified_server(
+      [{section: "httpd",
+        key: "rewrite_limit",
+        value: "2"},
+       {section: "httpd",
+        key: "secure_rewrites",
+        value: "false"}],
+      function(){
+        var url = "/"+dbName+"/_design/test/_rewrite/foo";
+        for (var i=0; i < 5; i++) {
+            var xhr = CouchDB.request("GET", url);
+            TEquals(200, xhr.status);
+        }
+      });
+  }
+}

http://git-wip-us.apache.org/repos/asf/couchdb/blob/013ee66e/test/javascript/tests/security_validation.js
----------------------------------------------------------------------
diff --git a/test/javascript/tests/security_validation.js b/test/javascript/tests/security_validation.js
new file mode 100644
index 0000000..14e5d04
--- /dev/null
+++ b/test/javascript/tests/security_validation.js
@@ -0,0 +1,338 @@
+// 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.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 WWW-Authenticate scheme named
+  // X-Couch-Test-Auth, and the user names and passwords are hard coded
+  // on the server-side.
+  //
+  // We could have used Basic authentication, however the XMLHttpRequest
+  // 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
+  // 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.
+  //
+  // So for testing purposes we are using this custom X-Couch-Test-Auth.
+  // It's identical to Basic auth, except it doesn't even base64 encode
+  // the "username:password" string, it's sent completely plain text.
+  // Firefox and Safari both deal with this correctly (which is to say
+  // they correctly do nothing special).
+
+  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 () {
+      // try saving document using the wrong credentials
+      var wrongPasswordDb = new CouchDB("test_suite_db",
+        {"WWW-Authenticate": "X-Couch-Test-Auth Damien Katz:foo"}
+      );
+
+      try {
+        wrongPasswordDb.save({foo:1,author:"Damien Katz"});
+        T(false && "Can't get here. Should have thrown an error 1");
+      } catch (e) {
+        T(e.error == "unauthorized");
+        T(wrongPasswordDb.last_req.status == 401);
+      }
+
+      // test force basic login
+      var resp = wrongPasswordDb.request("GET", "/_session?basic=true");
+      var err = JSON.parse(resp.responseText);
+      T(err.error == "unauthorized");
+      T(resp.status == 401);
+
+      // Create the design doc that will run custom validation code
+      var designDoc = {
+        _id:"_design/test",
+        language: "javascript",
+        validate_doc_update: stringFun(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:
+                "Documents must have an author field"};
+          }
+          if (oldDoc && oldDoc.author != userCtx.name) {
+              throw {unauthorized:
+                  "You are not the author of this document. You jerk."};
+          }
+        })
+      }
+
+      // Save a document normally
+      var userDb = new CouchDB("test_suite_db",
+        {"WWW-Authenticate": "X-Couch-Test-Auth Damien Katz:pecan pie"}
+      );
+
+      T(userDb.save({_id:"testdoc", foo:1, author:"Damien Katz"}).ok);
+
+      // Attempt to save the design as a non-admin
+      try {
+        userDb.save(designDoc);
+        T(false && "Can't get here. Should have thrown an error on design doc");
+      } catch (e) {
+        T(e.error == "unauthorized");
+        T(userDb.last_req.status == 401);
+      }
+
+      // set user as the admin
+      T(db.setSecObj({
+        admins : {names : ["Damien Katz"]}
+      }).ok);
+
+      T(userDb.save(designDoc).ok);
+
+      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)
+      designDoc.foo = "bar";
+      designDoc._rev = "2-642e20f96624a0aae6025b4dba0c6fb2";
+      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).userCtx;
+      T(user.name == "Damien Katz");
+      // test that the roles are listed properly
+      TEquals(user.roles, []);
+
+
+      // update the document
+      var doc = userDb.open("testdoc");
+      doc.foo=2;
+      T(userDb.save(doc).ok);
+
+      // 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 doc = user2Db.open("testdoc");
+      doc.foo=3;
+      try {
+        user2Db.save(doc);
+        T(false && "Can't get here. Should have thrown an error 3");
+      } catch (e) {
+        T(e.error == "unauthorized");
+        T(user2Db.last_req.status == 401);
+      }
+
+      // Now have Damien change the author to Jan
+      doc = userDb.open("testdoc");
+      doc.author="Jan Lehnardt";
+      T(userDb.save(doc).ok);
+
+      // Now update the document as Jan
+      doc = user2Db.open("testdoc");
+      doc.foo = 3;
+      T(user2Db.save(doc).ok);
+
+      // Damien can't delete it
+      try {
+        userDb.deleteDoc(doc);
+        T(false && "Can't get here. Should have thrown an error 4");
+      } catch (e) {
+        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);
+
+      // now test bulk docs
+      var docs = [{_id:"bahbah",author:"Damien Katz",foo:"bar"},{_id:"fahfah",foo:"baz"}];
+
+      // Create the docs
+      var results = db.bulkSave(docs);
+
+      T(results[0].rev)
+      T(results[0].error == undefined)
+      T(results[1].rev === undefined)
+      T(results[1].error == "forbidden")
+
+      T(db.open("bahbah"));
+      T(db.open("fahfah") == null);
+
+
+      // now all or nothing with a failure
+      var docs = [{_id:"booboo",author:"Damien Katz",foo:"bar"},{_id:"foofoo",foo:"baz"}];
+
+      // Create the docs
+      var results = db.bulkSave(docs, {all_or_nothing:true});
+
+      T(results.errors.length == 1);
+      T(results.errors[0].error == "forbidden");
+      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;
+      var dbPairs = [
+        {source:"test_suite_db_a",
+          target:"test_suite_db_b"},
+
+        {source:"test_suite_db_a",
+          target:{url: CouchDB.protocol + host + "/test_suite_db_b",
+                  headers: AuthHeaders}},
+
+        {source:{url:CouchDB.protocol + host + "/test_suite_db_a",
+                 headers: AuthHeaders},
+          target:"test_suite_db_b"},
+
+        {source:{url:CouchDB.protocol + host + "/test_suite_db_a",
+                 headers: AuthHeaders},
+         target:{url:CouchDB.protocol + host + "/test_suite_db_b",
+                 headers: AuthHeaders}},
+      ]
+      var adminDbA = new CouchDB("test_suite_db_a", {"X-Couch-Full-Commit":"false"});
+      var adminDbB = new CouchDB("test_suite_db_b", {"X-Couch-Full-Commit":"false"});
+      var dbA = new CouchDB("test_suite_db_a",
+          {"WWW-Authenticate": "X-Couch-Test-Auth Christopher Lenz:dog food"});
+      var dbB = new CouchDB("test_suite_db_b",
+          {"WWW-Authenticate": "X-Couch-Test-Auth Christopher Lenz:dog food"});
+      var xhr;
+      for (var testPair = 0; testPair < dbPairs.length; testPair++) {
+        var A = dbPairs[testPair].source
+        var B = dbPairs[testPair].target
+
+        adminDbA.deleteDb();
+        adminDbA.createDb();
+        adminDbB.deleteDb();
+        adminDbB.createDb();
+
+        // save and replicate a documents that will and will not pass our design
+        // doc validation function.
+        dbA.save({_id:"foo1",value:"a",author:"Noah Slater"});
+        dbA.save({_id:"foo2",value:"a",author:"Christopher Lenz"});
+        dbA.save({_id:"bad1",value:"a"});
+
+        T(CouchDB.replicate(A, B, {headers:AuthHeaders}).ok);
+        T(CouchDB.replicate(B, A, {headers:AuthHeaders}).ok);
+
+        T(dbA.open("foo1"));
+        T(dbB.open("foo1"));
+        T(dbA.open("foo2"));
+        T(dbB.open("foo2"));
+
+        // save the design doc to dbA
+        delete designDoc._rev; // clear rev from previous saves
+        adminDbA.save(designDoc);
+
+        // no affect on already saved docs
+        T(dbA.open("bad1"));
+
+        // Update some docs on dbB. Since the design hasn't replicated, anything
+        // is allowed.
+
+        // this edit will fail validation on replication to dbA (no author)
+        T(dbB.save({_id:"bad2",value:"a"}).ok);
+
+        // this edit will fail security on replication to dbA (wrong author
+        //  replicating the change)
+        var foo1 = dbB.open("foo1");
+        foo1.value = "b";
+        dbB.save(foo1);
+
+        // this is a legal edit
+        var foo2 = dbB.open("foo2");
+        foo2.value = "b";
+        dbB.save(foo2);
+
+        var results = CouchDB.replicate(B, A, {headers:AuthHeaders});
+
+        T(results.ok);
+
+        T(results.history[0].docs_written == 1);
+        T(results.history[0].doc_write_failures == 2);
+
+        // bad2 should not be on dbA
+        T(dbA.open("bad2") == null);
+
+        // The edit to foo1 should not have replicated.
+        T(dbA.open("foo1").value == "a");
+
+        // The edit to foo2 should have replicated.
+        T(dbA.open("foo2").value == "b");
+      }
+    });
+};

http://git-wip-us.apache.org/repos/asf/couchdb/blob/013ee66e/test/javascript/tests/show_documents.js
----------------------------------------------------------------------
diff --git a/test/javascript/tests/show_documents.js b/test/javascript/tests/show_documents.js
new file mode 100644
index 0000000..618925f
--- /dev/null
+++ b/test/javascript/tests/show_documents.js
@@ -0,0 +1,420 @@
+// 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.show_documents = function(debug) {
+  var db = new CouchDB("test_suite_db", {"X-Couch-Full-Commit":"false"});
+  db.deleteDb();
+  db.createDb();
+  if (debug) debugger;
+
+  var designDoc = {
+    _id:"_design/template",
+    language: "javascript",
+    shows: {
+      "hello" : stringFun(function(doc, req) {
+        log("hello fun");
+        if (doc) {
+          return "Hello World";
+        } else {
+          if(req.id) {
+            return "New World";
+          } else {
+            return "Empty World";
+          }
+        }
+      }),
+      "just-name" : stringFun(function(doc, req) {
+        if (doc) {
+          return {
+            body : "Just " + doc.name
+          };
+        } else {
+          return {
+            body : "No such doc",
+            code : 404
+          };
+        }
+      }),
+      "json" : stringFun(function(doc, req) {
+        return {
+          json : doc
+        }
+      }),
+      "req-info" : stringFun(function(doc, req) {
+        return {
+          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;
+      }),
+      "no-set-etag" : stringFun(function(doc, req) {
+        return {
+          headers : {
+            "Etag" : "skipped"
+          },
+          "body" : "something"
+        }
+      }),
+      "list-api" : stringFun(function(doc, req) {
+        start({"X-Couch-Test-Header": "Yeah"});
+        send("Hey");
+      }),
+      "list-api-provides" : stringFun(function(doc, req) {
+        provides("text", function(){
+            send("foo, ");
+            send("bar, ");
+            send("baz!");
+        })
+      }),
+      "list-api-provides-and-return" : stringFun(function(doc, req) {
+        provides("text", function(){
+            send("4, ");
+            send("5, ");
+            send("6, ");
+            return "7!";
+        })
+        send("1, ");
+        send("2, ");
+        return "3, ";
+      }),
+      "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 {
+            // a 16x16 px version of the CouchDB logo
+            "base64" :
+["iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAsV",
+"BMVEUAAAD////////////////////////5ur3rEBn////////////////wDBL/",
+"AADuBAe9EB3IEBz/7+//X1/qBQn2AgP/f3/ilpzsDxfpChDtDhXeCA76AQH/v7",
+"/84eLyWV/uc3bJPEf/Dw/uw8bRWmP1h4zxSlD6YGHuQ0f6g4XyQkXvCA36MDH6",
+"wMH/z8/yAwX64ODeh47BHiv/Ly/20dLQLTj98PDXWmP/Pz//39/wGyJ7Iy9JAA",
+"AADHRSTlMAbw8vf08/bz+Pv19jK/W3AAAAg0lEQVR4Xp3LRQ4DQRBD0QqTm4Y5",
+"zMxw/4OleiJlHeUtv2X6RbNO1Uqj9g0RMCuQO0vBIg4vMFeOpCWIWmDOw82fZx",
+"vaND1c8OG4vrdOqD8YwgpDYDxRgkSm5rwu0nQVBJuMg++pLXZyr5jnc1BaH4GT",
+"LvEliY253nA3pVhQqdPt0f/erJkMGMB8xucAAAAASUVORK5CYII="].join(''),
+            headers : {
+              "Content-Type" : "image/png",
+              "Vary" : "Accept" // we set this for proxy caches
+            }
+          };
+        } else {
+          return {
+            "body" : "accepting text requests",
+            headers : {
+              "Content-Type" : "text/html",
+              "Vary" : "Accept"
+            }
+          };
+        }
+      }),
+      "provides" : stringFun(function(doc, req) {
+        registerType("foo", "application/foo","application/x-foo");
+
+        provides("html", function() {
+          return "Ha ha, you said \"" + doc.word + "\".";
+        });
+
+        provides("foo", function() {
+          return "foofoo";
+        });
+      }),
+      "withSlash": stringFun(function(doc, req) {
+        return { json: doc }
+      }),
+      "secObj": stringFun(function(doc, req) {
+        return { json: req.secObj };
+      })
+    }
+  };
+  T(db.save(designDoc).ok);
+
+  var doc = {"word":"plankton", "name":"Rusty"}
+  var resp = db.save(doc);
+  T(resp.ok);
+  var docid = resp.id;
+
+  // show error
+  var xhr = CouchDB.request("GET", "/test_suite_db/_design/template/_show/");
+  T(xhr.status == 404, 'Should be missing');
+  T(JSON.parse(xhr.responseText).reason == "Invalid path.");
+
+  // hello template world
+  xhr = CouchDB.request("GET", "/test_suite_db/_design/template/_show/hello/"+docid);
+  T(xhr.responseText == "Hello World", "hello");
+  T(/charset=utf-8/.test(xhr.getResponseHeader("Content-Type")));
+
+
+  // Fix for COUCHDB-379
+  T(equals(xhr.getResponseHeader("Server").substr(0,7), "CouchDB"));
+
+  // // error stacktraces
+  // xhr = CouchDB.request("GET", "/test_suite_db/_design/template/_show/render-error/"+docid);
+  // T(JSON.parse(xhr.responseText).error == "render_error");
+
+  // hello template world (no docid)
+  xhr = CouchDB.request("GET", "/test_suite_db/_design/template/_show/hello");
+  T(xhr.responseText == "Empty World");
+
+  // hello template world (no docid)
+  xhr = CouchDB.request("GET", "/test_suite_db/_design/template/_show/empty");
+  T(xhr.responseText == "");
+
+  // // hello template world (non-existing docid)
+  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");
+  
+  // show with doc
+  xhr = CouchDB.request("GET", "/test_suite_db/_design/template/_show/just-name/"+docid);
+  T(xhr.responseText == "Just Rusty");
+
+  // show with missing doc
+  xhr = CouchDB.request("GET", "/test_suite_db/_design/template/_show/just-name/missingdoc");
+  T(xhr.status == 404);
+  TEquals("No such doc", xhr.responseText);
+
+  // show with missing func
+  xhr = CouchDB.request("GET", "/test_suite_db/_design/template/_show/missing/"+docid);
+  T(xhr.status == 404, "function is missing");
+
+  // missing design doc
+  xhr = CouchDB.request("GET", "/test_suite_db/_design/missingddoc/_show/just-name/"+docid);
+  T(xhr.status == 404);
+  var resp = JSON.parse(xhr.responseText);
+  T(resp.error == "not_found");
+
+  // query parameters
+  xhr = CouchDB.request("GET", "/test_suite_db/_design/template/_show/req-info/"+docid+"?foo=bar", {
+    headers: {
+      "Accept": "text/html;text/plain;*/*",
+      "X-Foo" : "bar"
+    }
+  });
+  var resp = JSON.parse(xhr.responseText);
+  T(equals(resp.headers["X-Foo"], "bar"));
+  T(equals(resp.query, {foo:"bar"}));
+  T(equals(resp.method, "GET"));
+  T(equals(resp.path[5], docid));
+  T(equals(resp.info.db_name, "test_suite_db"));
+
+  // accept header switching
+  // different mime has different etag
+  xhr = CouchDB.request("GET", "/test_suite_db/_design/template/_show/accept-switch/"+docid, {
+    headers: {"Accept": "text/html;text/plain;*/*"}
+  });
+  var ct = xhr.getResponseHeader("Content-Type");
+  T(/text\/html/.test(ct))
+  T("Accept" == xhr.getResponseHeader("Vary"));
+  var etag = xhr.getResponseHeader("etag");
+
+  xhr = CouchDB.request("GET", "/test_suite_db/_design/template/_show/accept-switch/"+docid, {
+    headers: {"Accept": "image/png;*/*"}
+  });
+  T(xhr.responseText.match(/PNG/))
+  T("image/png" == xhr.getResponseHeader("Content-Type"));
+  var etag2 = xhr.getResponseHeader("etag");
+  T(etag2 != etag);
+
+  // proper etags
+  // show with doc
+  xhr = CouchDB.request("GET", "/test_suite_db/_design/template/_show/just-name/"+docid);
+  // extract the ETag header values
+  etag = xhr.getResponseHeader("etag");
+  // get again with etag in request
+  xhr = CouchDB.request("GET", "/test_suite_db/_design/template/_show/just-name/"+docid, {
+    headers: {"if-none-match": etag}
+  });
+  // should be 304
+  T(xhr.status == 304);
+
+  // update the doc
+  doc.name = "Crusty";
+  resp = db.save(doc);
+  T(resp.ok);
+  // req with same etag
+  xhr = CouchDB.request("GET", "/test_suite_db/_design/template/_show/just-name/"+docid, {
+    headers: {"if-none-match": etag}
+  });
+  // status is 200
+  T(xhr.status == 200);
+
+  // get new etag and request again
+  etag = xhr.getResponseHeader("etag");
+  xhr = CouchDB.request("GET", "/test_suite_db/_design/template/_show/just-name/"+docid, {
+    headers: {"if-none-match": etag}
+  });
+  // should be 304
+  T(xhr.status == 304);
+
+  // update design doc (but not function)
+  designDoc.isChanged = true;
+  T(db.save(designDoc).ok);
+
+  xhr = CouchDB.request("GET", "/test_suite_db/_design/template/_show/just-name/"+docid, {
+    headers: {"if-none-match": etag}
+  });
+  // should not be 304 if we change the doc
+  T(xhr.status != 304, "changed ddoc");
+
+  // update design doc function
+  designDoc.shows["just-name"] = stringFun(function(doc, req) {
+   return {
+     body : "Just old " + doc.name
+   };
+  });
+  T(db.save(designDoc).ok);
+
+  xhr = CouchDB.request("GET", "/test_suite_db/_design/template/_show/just-name/"+docid, {
+    headers: {"if-none-match": etag}
+  });
+  // status is 200
+  T(xhr.status == 200);
+
+
+  // JS can't set etag
+  xhr = CouchDB.request("GET", "/test_suite_db/_design/template/_show/no-set-etag/"+docid);
+  // extract the ETag header values
+  etag = xhr.getResponseHeader("etag");
+  T(etag != "skipped")
+
+  // test the provides mime matcher
+  xhr = CouchDB.request("GET", "/test_suite_db/_design/template/_show/provides/"+docid, {
+    headers: {
+      "Accept": 'text/html,application/atom+xml; q=0.9'
+    }
+  });
+  var ct = xhr.getResponseHeader("Content-Type");
+  T(/charset=utf-8/.test(ct))
+  T(/text\/html/.test(ct))
+  T(xhr.responseText == "Ha ha, you said \"plankton\".");
+
+  // registering types works
+  xhr = CouchDB.request("GET", "/test_suite_db/_design/template/_show/provides/"+docid, {
+    headers: {
+      "Accept": "application/x-foo"
+    }
+  });
+  T(xhr.getResponseHeader("Content-Type") == "application/x-foo");
+  T(xhr.responseText.match(/foofoo/));
+
+  // test the provides mime matcher without a match
+  xhr = CouchDB.request("GET", "/test_suite_db/_design/template/_show/provides/"+docid, {
+   headers: {
+     "Accept": 'text/monkeys'
+   }
+  });
+  var rs = JSON.parse(xhr.responseText);
+  T(rs.error == "not_acceptable")
+
+
+  // test inclusion of conflict state
+  var doc1 = {_id:"foo", a:1};
+  var doc2 = {_id:"foo", a:2};
+  db.save(doc1);
+
+  // create the conflict with an all_or_nothing bulk docs request
+  var docs = [doc2];
+  db.bulkSave(docs, {all_or_nothing:true});
+
+  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");
+
+  // test list() compatible API with provides function
+  xhr = CouchDB.request("GET", "/test_suite_db/_design/template/_show/list-api-provides/foo?format=text");
+  TEquals(xhr.responseText, "foo, bar, baz!", "should join chunks to response body");
+
+  // should keep next result order: chunks + return value + provided chunks + provided return value
+  xhr = CouchDB.request("GET", "/test_suite_db/_design/template/_show/list-api-provides-and-return/foo?format=text");
+  TEquals(xhr.responseText, "1, 2, 3, 4, 5, 6, 7!", "should not break 1..7 range");
+
+  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({_id:"testdoc",foo:1}).ok);
+
+        xhr = CouchDB.request("GET", "/test_suite_db/_design/template/_show/secObj");
+        var resp = JSON.parse(xhr.responseText);
+        T(resp.foo == true);
+      }
+  );
+  
+};

http://git-wip-us.apache.org/repos/asf/couchdb/blob/013ee66e/test/javascript/tests/stats.js
----------------------------------------------------------------------
diff --git a/test/javascript/tests/stats.js b/test/javascript/tests/stats.js
new file mode 100644
index 0000000..87440b3
--- /dev/null
+++ b/test/javascript/tests/stats.js
@@ -0,0 +1,348 @@
+// 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.stats = function(debug) {
+
+  function newDb(name, doSetup) {
+    var db = new CouchDB(name, {"X-Couch-Full-Commit": "false"});
+    if(doSetup) {
+      db.deleteDb();
+      db.createDb();
+    }
+    return db;
+  };
+
+  function getStat(path) {
+    var stat = CouchDB.requestStats(path, true);
+    return stat ? stat.value : null;
+  };
+
+  function doView(db) {
+    var designDoc = {
+      _id:"_design/test", // turn off couch.js id escaping?
+      language: "javascript",
+      views: {
+        all_docs: {map: "function(doc) {emit(doc.integer, null);}"}
+      }
+    };
+    db.save(designDoc);
+    db.view("test/all_docs");
+  };
+
+  function runTest(path, funcs) {
+    var db = newDb("test_suite_db", true);
+    if(funcs.setup) funcs.setup(db);
+    var before = getStat(path);
+    if(funcs.run) funcs.run(db);
+    var after = getStat(path);
+    if(funcs.test) funcs.test(before, after);
+  }
+
+  if (debug) debugger;
+
+  (function() {
+    var db = newDb("test_suite_db");
+    db.deleteDb();
+  
+    var before = getStat(["couchdb", "open_databases"]);
+    db.createDb();
+    var after = getStat(["couchdb", "open_databases"]);
+    TEquals(before+1, after, "Creating a db increments open db count.");
+  })();
+  
+  runTest(["couchdb", "open_databases"], {
+    setup: function() {restartServer();},
+    run: function(db) {db.open("123");},
+    test: function(before, after) {
+      TEquals(before+1, after, "Opening a db increments open db count.");
+    }
+  });
+  
+  runTest(["couchdb", "open_databases"], {
+    run: function(db) {db.deleteDb();},
+    test: function(before, after) {
+      TEquals(before-1, after, "Deleting a db decrements open db count.");
+    }
+  });
+  
+  (function() {
+    restartServer();
+    var max = 5;
+    
+    var testFun = function() {
+      var pre_dbs = getStat(["couchdb", "open_databases"]) || 0;
+      var pre_files = getStat(["couchdb", "open_os_files"]) || 0;
+     
+      var triggered = false;
+      var db = null;
+      for(var i = 0; i < max*2; i++) {
+        while (true) {
+            try {
+              db = newDb("test_suite_db_" + i, true);
+              break;
+            } catch(e) {
+                // all_dbs_active error!
+              triggered = true;
+            }
+        }
+
+        // Trigger a delayed commit
+        db.save({_id: "" + i, "lang": "Awesome!"});
+      }
+      T(triggered, "We managed to force a all_dbs_active error.");
+      
+      var open_dbs = getStat(["couchdb", "open_databases"]);
+      TEquals(open_dbs > 0, true, "We actually opened some dbs.");
+      TEquals(max, open_dbs, "We only have max db's open.");
+      
+      for(var i = 0; i < max * 2; i++) {
+        newDb("test_suite_db_" + i).deleteDb();
+      }
+      
+      var post_dbs = getStat(["couchdb", "open_databases"]);
+      var post_files = getStat(["couchdb", "open_os_files"]);
+      TEquals(pre_dbs, post_dbs, "We have the same number of open dbs.");
+      TEquals(pre_files, post_files, "We have the same number of open files.");
+    };
+    
+    run_on_modified_server(
+      [{section: "couchdb", key: "max_dbs_open", value: "5"}],
+      testFun
+    );
+  })();
+  
+  // Just fetching the before value is the extra +1 in test
+  runTest(["couchdb", "httpd", "requests"], {
+    run: function() {CouchDB.request("GET", "/");},
+    test: function(before, after) {
+      TEquals(before+2, after, "Request counts are incremented properly.");
+    }
+  });
+  
+  runTest(["couchdb", "database_reads"], {
+    setup: function(db) {db.save({"_id": "test"});},
+    run: function(db) {db.open("test");},
+    test: function(before, after) {
+      TEquals(before+1, after, "Reading a doc increments docs reads.");
+    }
+  });
+  
+  runTest(["couchdb", "database_reads"], {
+    setup: function(db) {db.save({"_id": "test"});},
+    run: function(db) {db.request("GET", "/");},
+    test: function(before, after) {
+      TEquals(before, after, "Only doc reads increment doc reads.");
+    }
+  });
+  
+  runTest(["couchdb", "database_reads"], {
+    setup: function(db) {db.save({"_id": "test"});},
+    run: function(db) {db.open("test", {"open_revs": "all"});},
+    test: function(before, after) {
+      TEquals(before+1, after, "Reading doc revs increments docs reads.");
+    }
+  });
+  
+  runTest(["couchdb", "database_writes"], {
+    run: function(db) {db.save({"a": "1"});},
+    test: function(before, after) {
+      TEquals(before+1, after, "Saving docs incrememnts doc writes.");
+    }
+  });
+  
+  runTest(["couchdb", "database_writes"], {
+    run: function(db) {
+      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"});},
+    run: function(db) {var doc = db.open("test"); db.save(doc);},
+    test: function(before, after) {
+      TEquals(before+1, after, "Updating docs incrememnts doc writes.");
+    }
+  });
+  
+  runTest(["couchdb", "database_writes"], {
+    setup: function(db) {db.save({"_id": "test"});},
+    run: function(db) {var doc = db.open("test"); db.deleteDoc(doc);},
+    test: function(before, after) {
+      TEquals(before+1, after, "Deleting docs increments doc writes.");
+    }
+  });
+  
+  runTest(["couchdb", "database_writes"], {
+    setup: function(db) {db.save({"_id": "test"});},
+    run: function(db) {
+      CouchDB.request("COPY", "/test_suite_db/test", {
+        headers: {"Destination": "copy_of_test"}
+      });
+    },
+    test: function(before, after) {
+      TEquals(before+1, after, "Copying docs increments doc writes.");
+    }
+  });
+  
+  runTest(["couchdb", "database_writes"], {
+    run: function() {
+      CouchDB.request("PUT", "/test_suite_db/bin_doc2/foo2.txt", {
+        body: "This is no base64 encoded test",
+        headers: {"Content-Type": "text/plain;charset=utf-8"}
+      });
+    },
+    test: function(before, after) {
+      TEquals(before+1, after, "Create with attachment increments doc writes.");
+    }
+  });
+  
+  runTest(["couchdb", "database_writes"], {
+    setup: function(db) {db.save({"_id": "test"});},
+    run: function(db) {
+      var doc = db.open("test");
+      CouchDB.request("PUT", "/test_suite_db/test/foo2.txt?rev=" + doc._rev, {
+        body: "This is no base64 encoded text",
+        headers: {"Content-Type": "text/plainn;charset=utf-8"}
+      });
+    },
+    test: function(before, after) {
+      TEquals(before+1, after, "Adding attachment increments doc writes.");
+    }
+  });
+  
+  runTest(["couchdb", "httpd", "bulk_requests"], {
+    run: function(db) {db.bulkSave(makeDocs(5));},
+    test: function(before, after) {
+      TEquals(before+1, after, "The bulk_requests counter is incremented.");
+    }
+  });
+  
+  runTest(["couchdb", "httpd", "view_reads"], {
+    run: function(db) {doView(db);},
+    test: function(before, after) {
+      TEquals(before+1, after, "Reading a view increments view reads.");
+    }
+  });
+  
+  runTest(["couchdb", "httpd", "view_reads"], {
+    setup: function(db) {db.save({"_id": "test"});},
+    run: function(db) {db.open("test");},
+    test: function(before, after) {
+      TEquals(before, after, "Reading a doc doesn't increment view reads.");
+    }
+  });
+  
+  runTest(["couchdb", "httpd", "temporary_view_reads"], {
+    run: function(db) { db.query(function(doc) { emit(doc._id); }); },
+    test: function(before, after) {
+      TEquals(before+1, after, "Temporary views have their own counter.");
+    }
+  });
+  
+  runTest(["couchdb", "httpd", "temporary_view_reads"], {
+    run: function(db) {doView(db);},
+    test: function(before, after) {
+      TEquals(before, after, "Permanent views don't affect temporary views.");
+    }
+  });
+  
+  runTest(["couchdb", "httpd", "view_reads"], {
+    run: function(db) { db.query(function(doc) { emit(doc._id); }); },
+    test: function(before, after) {
+      TEquals(before, after, "Temporary views don't affect permanent views.");
+    }
+  });
+  
+  // Relies on getting the stats values being GET requests.
+  runTest(["couchdb", "httpd_request_methods", "GET"], {
+    test: function(before, after) {
+      TEquals(before+1, after, "Get requests are incremented properly.");
+    }
+  });
+  
+  runTest(["couchdb", "httpd_request_methods", "GET"], {
+    run: function() {CouchDB.request("POST", "/");},
+    test: function(before, after) {
+      TEquals(before+1, after, "POST requests don't affect GET counter.");
+    }
+  });
+  
+  runTest(["couchdb", "httpd_request_methods", "POST"], {
+    run: function() {CouchDB.request("POST", "/");},
+    test: function(before, after) {
+      TEquals(before+1, after, "POST requests are incremented properly.");
+    }
+  });
+  
+  runTest(["couchdb", "httpd_status_codes", "404"], {
+    run: function() {CouchDB.request("GET", "/nonexistant_db");},
+    test: function(before, after) {
+      TEquals(before+1, after, "Increments 404 counter on db not found.");
+    }
+  });
+  
+  runTest(["couchdb", "httpd_status_codes", "404"], {
+    run: function() {CouchDB.request("GET", "/");},
+    test: function(before, after) {
+      TEquals(before, after, "Getting DB info doesn't increment 404's");
+    }
+  });
+
+  var test_metric = function(metric, expected_fields) {
+    for (var k in metric) {
+      T(expected_fields.indexOf(k) >= 0, "Unknown property name: " + k);
+    }
+    for (var k in expected_fields) {
+      T(metric[expected_fields[k]] !== undefined, "Missing required property: " + k);
+    }
+  };
+
+  var test_histogram = function(histo) {
+    test_metric(histo, ["value", "type", "desc"]);
+    test_metric(histo.value, ["min", "max", "arithmetic_mean",
+      "geometric_mean", "harmonic_mean", "median", "variance",
+       "standard_deviation", "skewness", "kurtosis", "percentile",
+       "histogram", "n"]);
+  };
+
+  var test_counter = function(counter) {
+    test_metric(counter, ["value", "desc", "type"]);
+  };
+
+  var test_metrics = function(metrics) {
+    if (metrics.type === "counter") {
+      test_counter(metrics);
+    } else if (metrics.type === "gauge") {
+      test_counter(metrics);
+    } else if (metrics.type === "histogram") {
+      test_histogram(metrics);
+    } else if (metrics.type === undefined) {
+      for (var k in metrics) {
+        test_metrics(metrics[k]);
+      }
+    }
+  };
+
+  (function() {
+    var summary = JSON.parse(CouchDB.request("GET", "/_stats", {
+      headers: {"Accept": "application/json"}
+    }).responseText);
+    T(typeof(summary) === 'object');
+    test_metrics(summary);
+  })();
+};

http://git-wip-us.apache.org/repos/asf/couchdb/blob/013ee66e/test/javascript/tests/update_documents.js
----------------------------------------------------------------------
diff --git a/test/javascript/tests/update_documents.js b/test/javascript/tests/update_documents.js
new file mode 100644
index 0000000..bdb7a99
--- /dev/null
+++ b/test/javascript/tests/update_documents.js
@@ -0,0 +1,235 @@
+// 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.update_documents = function(debug) {
+  var db = new CouchDB("test_suite_db", {"X-Couch-Full-Commit":"false"});
+  db.deleteDb();
+  db.createDb();
+  if (debug) debugger;
+      
+  var designDoc = {
+    _id:"_design/update",
+    language: "javascript",
+    updates: {
+      "hello" : stringFun(function(doc, req) {
+        log(doc);
+        log(req);
+        if (!doc) {
+          if (req.id) {
+            return [
+            // Creates a new document with the PUT docid,
+            { _id : req.id,
+              reqs : [req] },
+            // and returns an HTML response to the client.
+            "<p>New World</p>"];
+          };
+          //
+          return [null, "<p>Empty World</p>"];
+        };
+        // we can update the document inline
+        doc.world = "hello";
+        // we can record aspects of the request or use them in application logic.
+        doc.reqs && doc.reqs.push(req);
+        doc.edited_by = req.userCtx;
+        return [doc, "<p>hello doc</p>"];
+      }),
+      "in-place" : stringFun(function(doc, req) {
+        var field = req.query.field;
+        var value = req.query.value;
+        var message = "set "+field+" to "+value;
+        doc[field] = value;
+        return [doc, message];
+      }),
+      "form-update" : stringFun(function(doc, req) {
+        for (var field in req.form) {
+          doc[field] = req.form[field];
+        }
+        var message = "updated doc from form";
+        return [doc, message];
+      }),
+      "bump-counter" : stringFun(function(doc, req) {
+        if (!doc.counter) doc.counter = 0;
+        doc.counter += 1;
+        var message = "<h1>bumped it!</h1>";
+        return [doc, message];
+      }),
+      "error" : stringFun(function(doc, req) {
+        superFail.badCrash;
+      }),
+       "get-uuid" : stringFun(function(doc, req) {
+         return [null, req.uuid];
+       }),
+       "code-n-bump" : stringFun(function(doc,req) {
+         if (!doc.counter) doc.counter = 0;
+         doc.counter += 1;
+         var message = "<h1>bumped it!</h1>";
+         resp = {"code": 302, "body": message}
+         return [doc, resp];
+       }),
+       "resp-code" : stringFun(function(doc,req) {
+         resp = {"code": 302}
+         return [null, resp];
+       }),
+       "resp-code-and-json" : stringFun(function(doc,req) {
+         resp = {"code": 302, "json": {"ok": true}}
+         return [{"_id": req["uuid"]}, resp];
+       }),
+       "binary" : stringFun(function(doc, req) {
+         var resp = {
+           "headers" : {
+             "Content-Type" : "application/octet-stream"
+           },
+           "base64" : "aGVsbG8gd29ybGQh" // "hello world!" encoded
+         };
+         return [doc, resp];
+       }),
+      "empty" : stringFun(function(doc, req) {
+        return [{}, 'oops'];
+      })
+    }
+  };
+  T(db.save(designDoc).ok);
+  
+  var doc = {"word":"plankton", "name":"Rusty"}
+  var resp = db.save(doc);
+  T(resp.ok);
+  var docid = resp.id;
+
+  // update error
+  var xhr = CouchDB.request("POST", "/test_suite_db/_design/update/_update/");
+  T(xhr.status == 404, 'Should be missing');
+  T(JSON.parse(xhr.responseText).reason == "Invalid path.");
+  
+  // hello update world
+  xhr = CouchDB.request("PUT", "/test_suite_db/_design/update/_update/hello/"+docid);
+  T(xhr.status == 201);
+  T(xhr.responseText == "<p>hello doc</p>");
+  T(/charset=utf-8/.test(xhr.getResponseHeader("Content-Type")));
+  T(equals(docid, xhr.getResponseHeader("X-Couch-Id")));
+
+  doc = db.open(docid);
+  T(doc.world == "hello");
+
+  // Fix for COUCHDB-379
+  T(equals(xhr.getResponseHeader("Server").substr(0,7), "CouchDB"));
+
+  // hello update world (no docid)
+  xhr = CouchDB.request("POST", "/test_suite_db/_design/update/_update/hello");
+  T(xhr.status == 200);
+  T(xhr.responseText == "<p>Empty World</p>");
+
+  // no GET allowed
+  xhr = CouchDB.request("GET", "/test_suite_db/_design/update/_update/hello");
+  // T(xhr.status == 405); // TODO allow qs to throw error code as well as error message
+  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');
+  T(xhr.status == 201);
+  T(xhr.responseText == "set title to test");
+  doc = db.open(docid);
+  T(doc.title == "test");
+  
+  // form update via application/x-www-form-urlencoded
+  xhr = CouchDB.request("PUT", "/test_suite_db/_design/update/_update/form-update/"+docid, {
+    headers : {"Content-Type":"application/x-www-form-urlencoded"},
+    body    : "formfoo=bar&formbar=foo"
+  });
+  TEquals(201, xhr.status);
+  TEquals("updated doc from form", xhr.responseText);
+  doc = db.open(docid);
+  TEquals("bar", doc.formfoo);
+  TEquals("foo", doc.formbar);
+  
+  // bump counter
+  xhr = CouchDB.request("PUT", "/test_suite_db/_design/update/_update/bump-counter/"+docid, {
+    headers : {"X-Couch-Full-Commit":"true"}
+  });
+  T(xhr.status == 201);
+  T(xhr.responseText == "<h1>bumped it!</h1>");
+  doc = db.open(docid);
+  T(doc.counter == 1);
+  
+  // _update honors full commit if you need it to
+  xhr = CouchDB.request("PUT", "/test_suite_db/_design/update/_update/bump-counter/"+docid, {
+    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);
+
+  // 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);
+
+  // COUCHDB-1229 - allow slashes in doc ids for update handlers
+  // /db/_design/doc/_update/handler/doc/id
+
+  var doc = {
+      _id:"with/slash",
+      counter:1
+  };
+  db.save(doc);
+  xhr = CouchDB.request("PUT", "/test_suite_db/_design/update/_update/bump-counter/with/slash");
+  TEquals(201, xhr.status, "should return a 200 status");
+  TEquals("<h1>bumped it!</h1>", xhr.responseText, "should report bumping");
+
+  var doc = db.open("with/slash");
+  TEquals(2, doc.counter, "counter should be 2");
+
+  // COUCHDB-648 - the code in the JSON response should be honored
+
+  xhr = CouchDB.request("PUT", "/test_suite_db/_design/update/_update/code-n-bump/"+docid, {
+    headers : {"X-Couch-Full-Commit":"true"}
+  });
+  T(xhr.status == 302);
+  T(xhr.responseText == "<h1>bumped it!</h1>");
+  doc = db.open(docid);
+  T(doc.counter == 3);
+
+  xhr = CouchDB.request("POST", "/test_suite_db/_design/update/_update/resp-code/");
+  T(xhr.status == 302);
+
+  xhr = CouchDB.request("POST", "/test_suite_db/_design/update/_update/resp-code-and-json/");
+  TEquals(302, xhr.status);
+  T(JSON.parse(xhr.responseText).ok);
+
+  // base64 response
+  xhr = CouchDB.request("PUT", "/test_suite_db/_design/update/_update/binary/"+docid, {
+    headers : {"X-Couch-Full-Commit":"false"},
+    body    : 'rubbish'
+  });
+  T(xhr.status == 201);
+  T(xhr.responseText == "hello world!");
+  T(/application\/octet-stream/.test(xhr.getResponseHeader("Content-Type")));
+
+  // Insert doc with empty id
+  xhr = CouchDB.request("PUT", "/test_suite_db/_design/update/_update/empty/foo");
+  TEquals(400, xhr.status);
+  TEquals("Document id must not be empty", JSON.parse(xhr.responseText).reason);
+
+};

http://git-wip-us.apache.org/repos/asf/couchdb/blob/013ee66e/test/javascript/tests/users_db.js
----------------------------------------------------------------------
diff --git a/test/javascript/tests/users_db.js b/test/javascript/tests/users_db.js
new file mode 100644
index 0000000..56dae6b
--- /dev/null
+++ b/test/javascript/tests/users_db.js
@@ -0,0 +1,173 @@
+// 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.users_db = function(debug) {
+  // This tests the users db, especially validations
+  // this should also test that you can log into the couch
+  
+  var usersDb = new CouchDB("test_suite_users", {"X-Couch-Full-Commit":"false"});
+
+  // test that you can treat "_user" as a db-name
+  // this can complicate people who try to secure the users db with 
+  // an http proxy and fail to get both the actual db and the _user path
+  // maybe it's not the right approach...
+  // hard to know what else to do, as we don't let non-admins inspect the config
+  // to determine the actual users db name.
+
+  function testFun() {
+    // 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({
+      name: "jchris@apache.org"
+    }, "funnybone");
+    T(usersDb.save(jchrisUserDoc).ok);
+    
+    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.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" // name and pass of _:_
+      }
+    });
+    T(s.name == null);
+    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 = [];
+
+    // "roles" must be an array of strings
+    jchrisUserDoc.roles = [12];
+    try {
+      usersDb.save(jchrisUserDoc);
+      T(false && "should only allow us to save doc when roles is an array of strings");
+    } catch(e) {
+      TEquals(e.reason, "doc.roles can only contain strings");
+    }
+    jchrisUserDoc.roles = [];
+
+    // "roles" must exist
+    delete jchrisUserDoc.roles;
+    try {
+      usersDb.save(jchrisUserDoc);
+      T(false && "should only allow us to save doc when roles exists");
+    } catch(e) {
+      T(e.reason == "doc.roles must exist");
+    }
+    jchrisUserDoc.roles = [];
+
+    // character : is not allowed in usernames
+    var joeUserDoc = CouchDB.prepareUserDoc({
+      name: "joe:erlang"
+    }, "qwerty");
+    try {
+      usersDb.save(joeUserDoc);
+      T(false, "shouldn't allow : in usernames");
+    } catch(e) {
+      TEquals("Character `:` is not allowed in usernames.", e.reason);
+    }
+
+    // test that you can login as a user with a password starting with :
+    var doc = CouchDB.prepareUserDoc({
+      name: "foo@example.org"
+    }, ":bar");
+    T(usersDb.save(doc).ok);
+
+    T(CouchDB.session().userCtx.name == null);
+
+    // test that you can use basic auth aginst the users db
+    var s = CouchDB.session({
+      headers : {
+        //                 base64_encode("foo@example.org::bar")
+        "Authorization" : "Basic Zm9vQGV4YW1wbGUub3JnOjpiYXI="
+      }
+    });
+    T(s.userCtx.name == "foo@example.org");
+
+  };
+
+  usersDb.deleteDb();
+  run_on_modified_server(
+    [{section: "couch_httpd_auth",
+      key: "authentication_db", value: usersDb.name}],
+    testFun
+  );
+  usersDb.deleteDb(); // cleanup
+  
+}