summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJohn Christopher Anderson <jchris@apache.org>2008-12-16 20:48:00 +0000
committerJohn Christopher Anderson <jchris@apache.org>2008-12-16 20:48:00 +0000
commit287dce73d2ededa4abb5f8c80599032dace46268 (patch)
tree1d588be96c0d3adfe3e074c62e30efae53c30224
parente64377c89cb535d4d649f637d3b434356ff984b5 (diff)
action.js and tests
git-svn-id: https://svn.apache.org/repos/asf/couchdb/trunk@727141 13f79535-47bb-0310-9956-ffa450edef68
-rw-r--r--share/server/action.js587
-rw-r--r--share/www/script/couch_tests.js194
2 files changed, 780 insertions, 1 deletions
diff --git a/share/server/action.js b/share/server/action.js
new file mode 100644
index 00000000..0c1bddcd
--- /dev/null
+++ b/share/server/action.js
@@ -0,0 +1,587 @@
+// 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.
+
+// JSON from json2.js
+/*
+ json2.js
+ 2008-03-14
+
+ Public Domain
+
+ No warranty expressed or implied. Use at your own risk.
+
+ See http://www.JSON.org/js.html
+
+ This is a reference implementation. You are free to copy, modify, or
+ redistribute.
+
+ Use your own copy. It is extremely unwise to load third party
+ code into your pages.
+*/
+
+var JSON = (function () {
+
+ function f(n) { // Format integers to have at least two digits.
+ return n < 10 ? '0' + n : n;
+ }
+
+ Date.prototype.toJSON = function () {
+ return this.getUTCFullYear() + '-' +
+ f(this.getUTCMonth() + 1) + '-' +
+ f(this.getUTCDate()) + 'T' +
+ f(this.getUTCHours()) + ':' +
+ f(this.getUTCMinutes()) + ':' +
+ f(this.getUTCSeconds()) + 'Z';
+ };
+
+
+ var m = { // table of character substitutions
+ '\b': '\\b',
+ '\t': '\\t',
+ '\n': '\\n',
+ '\f': '\\f',
+ '\r': '\\r',
+ '"' : '\\"',
+ '\\': '\\\\'
+ };
+
+ function stringify(value, whitelist) {
+ var a, // The array holding the partial texts.
+ i, // The loop counter.
+ k, // The member key.
+ l, // Length.
+ r = /["\\\x00-\x1f\x7f-\x9f]/g,
+ v; // The member value.
+
+ switch (typeof value) {
+ case 'string':
+
+ return r.test(value) ?
+ '"' + value.replace(r, function (a) {
+ var c = m[a];
+ if (c) {
+ return c;
+ }
+ c = a.charCodeAt();
+ return '\\u00' + Math.floor(c / 16).toString(16) +
+ (c % 16).toString(16);
+ }) + '"' :
+ '"' + value + '"';
+
+ case 'number':
+
+ return isFinite(value) ? String(value) : 'null';
+
+ case 'boolean':
+ case 'null':
+ return String(value);
+
+ case 'object':
+
+ if (!value) {
+ return 'null';
+ }
+
+ if (typeof value.toJSON === 'function') {
+ return stringify(value.toJSON());
+ }
+ a = [];
+ if (typeof value.length === 'number' &&
+ !(value.propertyIsEnumerable('length'))) {
+
+ l = value.length;
+ for (i = 0; i < l; i += 1) {
+ a.push(stringify(value[i], whitelist) || 'null');
+ }
+
+ return '[' + a.join(',') + ']';
+ }
+ if (whitelist) {
+ l = whitelist.length;
+ for (i = 0; i < l; i += 1) {
+ k = whitelist[i];
+ if (typeof k === 'string') {
+ v = stringify(value[k], whitelist);
+ if (v) {
+ a.push(stringify(k) + ':' + v);
+ }
+ }
+ }
+ } else {
+
+ for (k in value) {
+ if (typeof k === 'string') {
+ v = stringify(value[k], whitelist);
+ if (v) {
+ a.push(stringify(k) + ':' + v);
+ }
+ }
+ }
+ }
+
+ return '{' + a.join(',') + '}';
+ }
+ }
+
+ return {
+ stringify: stringify,
+ parse: function (text, filter) {
+ var j;
+
+ function walk(k, v) {
+ var i, n;
+ if (v && typeof v === 'object') {
+ for (i in v) {
+ if (Object.prototype.hasOwnProperty.apply(v, [i])) {
+ n = walk(i, v[i]);
+ if (n !== undefined) {
+ v[i] = n;
+ } else {
+ delete v[i];
+ }
+ }
+ }
+ }
+ return filter(k, v);
+ }
+
+ if (/^[\],:{}\s]*$/.test(text.replace(/\\["\\\/bfnrtu]/g, '@').
+replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']').
+replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) {
+
+ j = eval('(' + text + ')');
+
+ return typeof filter === 'function' ? walk('', j) : j;
+ }
+
+ throw new SyntaxError('parseJSON');
+ }
+ };
+})();
+
+// HTTP object provides access to curl via couchjs.
+
+var HTTP = (function() {
+ function parseCurl(string) {
+ var parts = string.split(/\r\n\r\n/);
+ var body = parts.pop();
+ var header = parts.pop();
+ var headers = header.split(/\n/);
+
+ var status = /HTTP\/1.\d (\d*)/.exec(header)[1];
+ return {
+ responseText: body,
+ status: parseInt(status),
+ getResponseHeader: function(key) {
+ var keymatcher = new RegExp(RegExp.escape(key), "i");
+ for (var i in headers) {
+ var h = headers[i];
+ if (keymatcher.test(h)) {
+ var value = h.substr(key.length+2);
+ value = value.slice(0, value.length-1);
+ return value;
+ }
+ }
+ return "";
+ }
+ }
+ };
+ return {
+ GET : function(url, body, headers) {
+ var st, urx = url, hx = (headers || null);
+ st = gethttp(urx, hx);
+ return parseCurl(st);
+ },
+ HEAD : function(url, body, headers) {
+ var st, urx = url, hx = (headers || null);
+ st = headhttp(urx, hx);
+ return parseCurl(st);
+ },
+ DELETE : function(url, body, headers) {
+ var st, urx = url, hx = (headers || null);
+ st = delhttp(urx, hx);
+ return parseCurl(st);
+ },
+ MOVE : function(url, body, headers) {
+ var st, urx = url, hx = (headers || null);
+ st = movehttp(urx, hx);
+ return parseCurl(st);
+ },
+ COPY : function(url, body, headers) {
+ var st, urx = url, hx = (headers || null);
+ st = copyhttp(urx, hx);
+ return parseCurl(st);
+ },
+ POST : function(url, body, headers) {
+ var st, urx = url, bx = (body || ""), hx = (headers || {});
+ hx['Content-Type'] = hx['Content-Type'] || "application/json";
+ st = posthttp(urx, bx, hx);
+ return parseCurl(st);
+ },
+ PUT : function(url, body, headers) {
+ var st, urx = url, bx = (body || ""), hx = (headers || {});
+ hx['Content-Type'] = hx['Content-Type'] || "application/json";
+ st = puthttp(urx, bx, hx);
+ return parseCurl(st);
+ }
+ };
+})();
+
+
+// couch.js
+
+// A simple class to represent a database. Uses XMLHttpRequest to interface with
+// the CouchDB server.
+
+function CouchDB(name) {
+ this.name = name
+ this.uri = "/" + encodeURIComponent(name) + "/";
+ request = CouchDB.request;
+
+ // Creates the database on the server
+ this.createDb = function() {
+ var req = request("PUT", this.uri);
+ var result = JSON.parse(req.responseText);
+ if (req.status != 201)
+ throw result;
+ return result;
+ }
+
+ // Deletes the database on the server
+ this.deleteDb = function() {
+ var req = request("DELETE", this.uri);
+ if (req.status == 404)
+ return false;
+ var result = JSON.parse(req.responseText);
+ if (req.status != 200)
+ throw result;
+ return result;
+ }
+
+ // Save a document to the database
+ this.save = function(doc, options) {
+ var req;
+ if (doc._id == undefined)
+ doc._id = CouchDB.newUuids(1)[0];
+
+ req = request("PUT", this.uri + encodeURIComponent(doc._id) + encodeOptions(options), {
+ body: JSON.stringify(doc)
+ });
+ var result = JSON.parse(req.responseText);
+ if (req.status != 201)
+ throw result;
+ doc._rev = result.rev;
+ return result;
+ }
+
+ // Open a document from the database
+ this.open = function(docId, options) {
+ var req = request("GET", this.uri + encodeURIComponent(docId) + encodeOptions(options));
+ if (req.status == 404)
+ return null;
+ var result = JSON.parse(req.responseText);
+ if (req.status != 200)
+ throw result;
+ return result;
+ }
+
+ // Deletes a document from the database
+ this.deleteDoc = function(doc) {
+ var req = request("DELETE", this.uri + encodeURIComponent(doc._id) + "?rev=" + doc._rev);
+ var result = JSON.parse(req.responseText);
+ if (req.status != 200)
+ throw result;
+ doc._rev = result.rev; //record rev in input document
+ doc._deleted = true;
+ return result;
+ }
+
+ // Deletes an attachment from a document
+ this.deleteDocAttachment = function(doc, attachment_name) {
+ var req = request("DELETE", this.uri + encodeURIComponent(doc._id) + "/" + attachment_name + "?rev=" + doc._rev);
+ var result = JSON.parse(req.responseText);
+ if (req.status != 200)
+ throw result;
+ doc._rev = result.rev; //record rev in input document
+ return result;
+ }
+
+ this.bulkSave = function(docs, options) {
+ // first prepoulate the UUIDs for new documents
+ var newCount = 0
+ for (var i=0; i<docs.length; i++) {
+ if (docs[i]._id == undefined)
+ newCount++;
+ }
+ var newUuids = CouchDB.newUuids(docs.length);
+ var newCount = 0
+ for (var i=0; i<docs.length; i++) {
+ if (docs[i]._id == undefined)
+ docs[i]._id = newUuids.pop();
+ }
+ var req = request("POST", this.uri + "_bulk_docs" + encodeOptions(options), {
+ body: JSON.stringify({"docs": docs})
+ });
+ var result = JSON.parse(req.responseText);
+ if (req.status != 201)
+ throw result;
+ for (var i = 0; i < docs.length; i++) {
+ docs[i]._rev = result.new_revs[i].rev;
+ }
+ return result;
+ }
+
+ // Applies the map function to the contents of database and returns the results.
+ this.query = function(mapFun, reduceFun, options, keys) {
+ var body = {language: "javascript"};
+ if(keys) {
+ body.keys = keys ;
+ }
+ if (typeof(mapFun) != "string")
+ mapFun = mapFun.toSource ? mapFun.toSource() : "(" + mapFun.toString() + ")";
+ body.map = mapFun;
+ if (reduceFun != null) {
+ if (typeof(reduceFun) != "string")
+ reduceFun = reduceFun.toSource ? reduceFun.toSource() : "(" + reduceFun.toString() + ")";
+ body.reduce = reduceFun;
+ }
+ var req = request("POST", this.uri + "_temp_view" + encodeOptions(options), {
+ headers: {"Content-Type": "application/json"},
+ body: JSON.stringify(body)
+ });
+ var result = JSON.parse(req.responseText);
+ if (req.status != 200)
+ throw result;
+ return result;
+ }
+
+ this.view = function(viewname, options, keys) {
+ var req = null ;
+ if(!keys) {
+ req = request("GET", this.uri + "_view/" + viewname + encodeOptions(options));
+ } else {
+ req = request("POST", this.uri + "_view/" + viewname + encodeOptions(options), {
+ headers: {"Content-Type": "application/json"},
+ body: JSON.stringify({keys:keys})
+ });
+ }
+ if (req.status == 404)
+ return null;
+ var result = JSON.parse(req.responseText);
+ if (req.status != 200)
+ throw result;
+ return result;
+ }
+
+ // gets information about the database
+ this.info = function() {
+ var req = request("GET", this.uri);
+ var result = JSON.parse(req.responseText);
+ if (req.status != 200)
+ throw result;
+ return result;
+ }
+
+ this.allDocs = function(options,keys) {
+ var req = null;
+ if(!keys) {
+ req = request("GET", this.uri + "_all_docs" + encodeOptions(options));
+ } else {
+ req = request("POST", this.uri + "_all_docs" + encodeOptions(options), {
+ headers: {"Content-Type": "application/json"},
+ body: JSON.stringify({keys:keys})
+ });
+ }
+ var result = JSON.parse(req.responseText);
+ if (req.status != 200)
+ throw result;
+ return result;
+ }
+
+ this.compact = function() {
+ var req = request("POST", this.uri + "_compact");
+ var result = JSON.parse(req.responseText);
+ if (req.status != 202)
+ throw result;
+ return result;
+ }
+
+ // Convert a options object to an url query string.
+ // ex: {key:'value',key2:'value2'} becomes '?key="value"&key2="value2"'
+ function encodeOptions(options) {
+ var buf = []
+ if (typeof(options) == "object" && options !== null) {
+ for (var name in options) {
+ if (!options.hasOwnProperty(name)) continue;
+ var value = options[name];
+ if (name == "key" || name == "startkey" || name == "endkey") {
+ value = toJSON(value);
+ }
+ buf.push(encodeURIComponent(name) + "=" + encodeURIComponent(value));
+ }
+ }
+ if (!buf.length) {
+ return "";
+ }
+ return "?" + buf.join("&");
+ }
+
+ function toJSON(obj) {
+ return obj !== null ? JSON.stringify(obj) : null;
+ }
+}
+
+CouchDB.allDbs = function() {
+ var req = CouchDB.request("GET", "/_all_dbs");
+ var result = JSON.parse(req.responseText);
+ if (req.status != 200)
+ throw result;
+ return result;
+}
+
+CouchDB.getVersion = function() {
+ var req = CouchDB.request("GET", "/");
+ var result = JSON.parse(req.responseText);
+ if (req.status != 200)
+ throw result;
+ return result.version;
+}
+
+CouchDB.replicate = function(source, target) {
+ var req = CouchDB.request("POST", "/_replicate", {
+ body: JSON.stringify({source: source, target: target})
+ });
+ var result = JSON.parse(req.responseText);
+ if (req.status != 200)
+ throw result;
+ return result;
+}
+
+CouchDB.uuids_cache = [];
+
+CouchDB.newUuids = function(n) {
+ if (CouchDB.uuids_cache.length >= n) {
+ var uuids = CouchDB.uuids_cache.slice(CouchDB.uuids_cache.length - n);
+ if(CouchDB.uuids_cache.length - n == 0) {
+ CouchDB.uuids_cache = [];
+ } else {
+ CouchDB.uuids_cache =
+ CouchDB.uuids_cache.slice(0, CouchDB.uuids_cache.length - n);
+ }
+ return uuids;
+ } else {
+ var req = CouchDB.request("POST", "/_uuids?count=" + (100 + n));
+ var result = JSON.parse(req.responseText);
+ if (req.status != 200)
+ throw result;
+ CouchDB.uuids_cache =
+ CouchDB.uuids_cache.concat(result.uuids.slice(0, 100));
+ return result.uuids.slice(100);
+ }
+}
+
+CouchDB.host = (typeof window == 'undefined' || !window) ? "127.0.0.1:5984" : window;
+
+CouchDB.request = function(method, uri, options) {
+ var full_uri = "http://" + CouchDB.host + uri;
+ options = options || {};
+ var response = HTTP[method](full_uri, options.body, options.headers);
+ return response;
+}
+
+log = function(message) {
+ print(toJSON({log: toJSON(message)}));
+}
+
+var sandbox = null;
+
+try {
+ // if possible, use evalcx (not always available)
+ sandbox = evalcx('');
+ sandbox.JSON = JSON;
+ sandbox.HTTP = HTTP;
+ sandbox.CouchDB = CouchDB;
+ sandbox.log = log;
+} catch (e) {}
+
+function error(httpcode, type, msg) {
+ return {code: httpcode , json:{error: type, reason: msg}};
+}
+
+function process_request(req) {
+ // Req is an object that contains information about the HTTP
+ // request made to the action server.
+ // Something like: {"verb": "GET", "path": ["foo", "bar"], "query": ..., "post":...}
+ // Here we use "path" to fetch the action, compile it, and run it.
+ if (req.path.length != 2) {
+ return error(404, "action_error", "Invalid path: \"" + req.path.join("/") + "\".");
+ }
+
+ var desname = "_design%2F" + req.path[0];
+
+ // todo: make this work with other ports and addresses...
+ // maybe couchdb can send config information over to the _external server on boot
+
+ var desdoc = HTTP.GET("http://127.0.0.1:5984/" + req.info.db_name + "/" + desname);
+
+ if (desdoc.status != 200) {
+ return error(404, "action_error", "Design document '" + desname + "' not found.");
+ }
+
+ var desjson = JSON.parse(desdoc.responseText);
+
+ if (!desjson.actions) {
+ return error(500, "action_error", "No actions found in design doc '" + desname + "'.");
+ }
+
+ if (!desjson.actions[req.path[1]]) {
+ return error(500, "action_error", "No action '" + req.path[1] + "' in design doc '" + desname + "'");
+ }
+
+ var func = null;
+ try {
+ // todo - use compileFunction from main.js?
+ func = eval(desjson.actions[req.path[1]]);
+ } catch(exception) {
+ return error(500, "action_error", "Failed to compile action: " + JSON.stringify(exception));
+ }
+
+ var ret = null;
+ var db = new CouchDB(req.info.db_name);
+ return func(req, db);
+}
+
+function respond(obj) {
+ print(JSON.stringify(obj));
+};
+
+var req;
+var resp;
+while (req = JSON.parse(readline())) {
+ try {
+ resp = process_request(req);
+ } catch(exception) {
+ if (exception.code) {
+ resp = exception;
+ } else if (exception.error && exception.reason) {
+ resp = error(500, exception.error, exception.reason);
+ } else {
+ resp = error(500, "action_error", JSON.stringify(exception));
+ }
+ }
+
+ try {
+ respond(resp);
+ } catch(e) {
+ respond(error(500, "json_error", "Error encoding JSON response. Exception: "+e.toString()));
+ }
+} \ No newline at end of file
diff --git a/share/www/script/couch_tests.js b/share/www/script/couch_tests.js
index ba88281f..78b0c302 100644
--- a/share/www/script/couch_tests.js
+++ b/share/www/script/couch_tests.js
@@ -1991,7 +1991,199 @@ var tests = {
xhr = CouchDB.request("GET", "/_config/test/foo");
T(xhr.responseText == '"bar"');
},
-
+
+ actions: function(debug) {
+ var db = new CouchDB("test_suite_db");
+ db.deleteDb();
+ db.createDb();
+ if (debug) debugger;
+
+ var xhr;
+ var numDocs = 10;
+
+ T(db.bulkSave(makeDocs(1, numDocs + 1)).ok);
+
+ var designDoc = {
+ _id:"_design/no_actions",
+ language: "javascript",
+ views: {
+ single_doc: {map: "function(doc) { if (doc._id == \"1\") { emit(1, null) }}"}
+ }
+ }
+ T(db.save(designDoc).ok);
+
+ designDoc = {
+ _id:"_design/errors",
+ language: "javascript",
+ actions: {
+ syntax: "function( bad syntax",
+ except: "function(req) { throw \"Failed to execute\" ; }"
+ }
+ }
+ T(db.save(designDoc).ok);
+
+ // var testControllerDoc = {
+ // _id: "_design/test",
+ // language: "javascript",
+ // actions: {
+ // "execution": "function(req) { return {json : req.query.userdata}}",
+ // }
+ // }
+
+ designDoc = {
+ _id:"_design/an_action",
+ actions: {
+ "times_two": "function(req) {return {code:200, json: {val: req.query.q * 2}};}",
+ "html" : "function() {return {body:'<p>Lorem ipsum...</p>'}}",
+ "request_object": "function(req) {return {json:req} }",
+ "bad_return": "function(req) {return {foo:\"bar\"} }",
+ "requires_put": 'function(req) {if (req.verb != "PUT") { throw {code:405, body:"Method Not Allowed, Punk!"}} else { return {body:"thanks for the PUT"}}};'
+ }
+ }
+ T(db.save(designDoc).ok);
+
+ // Make sure we don't succeed on something that shouldn't
+ xhr = CouchDB.request("GET", "/test_suite_db/_external");
+ T(xhr.status == 404);
+ T(JSON.parse(xhr.responseText).reason == "No server name specified.");
+ xhr = CouchDB.request("GET", "/test_suite_db/_external/bannana");
+ T(xhr.status == 404);
+ T(JSON.parse(xhr.responseText).reason == "No server configured for \"bannana\".");
+ xhr = CouchDB.request("GET", "/test_suite_db/_external/action");
+ T(xhr.status == 404);
+ xhr = CouchDB.request("GET", "/test_suite_db/_external/action/no_actions");
+ T(xhr.status == 404)
+ T(JSON.parse(xhr.responseText).reason == "Invalid path: \"no_actions\".");;
+ xhr = CouchDB.request("GET", "/test_suite_db/_external/action/no_actions/foo");
+ T(xhr.status == 500);
+ T(/^No actions found/.test(JSON.parse(xhr.responseText).reason));
+ xhr = CouchDB.request("GET", "/test_suite_db/_external/action/an_action/invalid");
+ T(xhr.status == 500);
+ T(/^No action \'invalid\'/.test(JSON.parse(xhr.responseText).reason));
+ xhr = CouchDB.request("GET", "/test_suite_db/_external/action/errors/syntax");
+ T(xhr.status == 500);
+ T(/^Failed to compile/.test(JSON.parse(xhr.responseText).reason));
+ xhr = CouchDB.request("GET", "/test_suite_db/_external/action/errors/except");
+ T(/Failed to execute/.test(JSON.parse(xhr.responseText).reason));
+
+ xhr = CouchDB.request("GET", "/test_suite_db/_external/action/an_action/times_two");
+ T(xhr.status == 200);
+ T(JSON.parse(xhr.responseText).val == null);
+
+ xhr = CouchDB.request("GET", "/test_suite_db/_external/action/an_action/bad_return");
+ T(xhr.status == 500);
+ T(/^Invalid data from external server/.test(JSON.parse(xhr.responseText).reason));
+
+
+ // test that we invoke the action server
+ xhr = CouchDB.request("GET", "/test_suite_db/_external/action/an_action/times_two?q=3");
+ T(xhr.status == 200);
+ T(JSON.parse(xhr.responseText).val == 6);
+
+ // Test that we can return raw text
+ xhr = CouchDB.request("GET", "/test_suite_db/_external/action/an_action/html");
+ T(xhr.status == 200);
+ T(xhr.responseText == '<p>Lorem ipsum...</p>');
+
+ // Test environment
+ xhr = CouchDB.request("GET", "/test_suite_db/_external/action/an_action/request_object?couchdb=relax");
+ T(xhr.status == 200);
+ var req = JSON.parse(xhr.responseText);
+
+ // Test that actions know their verbs
+ T(req.verb == "GET");
+ // Test that actions know their req
+ T(req.info.db_name == "test_suite_db");
+ T(req.path[0] == "an_action");
+ T(req.path[1] == "request_object");
+ T(req.query.couchdb == "relax");
+
+ xhr = CouchDB.request("POST", "/test_suite_db/_external/action/an_action/request_object?couchdb=relax",{
+ "body" : "some=text",
+ "headers" : {
+ "Content-Type" : "application/x-www-form-urlencoded"
+ }
+ });
+ T(xhr.status == 200);
+ var req = JSON.parse(xhr.responseText);
+ // Test that actions know their verbs
+ T(req.verb == "POST");
+ // Test that actions know their db
+ T(req.info.db_name == "test_suite_db");
+ T(req.path[0] == "an_action");
+ T(req.path[1] == "request_object");
+ // T(req.path[2] == "foo"); // this would be fun
+ T(req.query.couchdb == "relax");
+
+ // we get raw access to the post body as well as access to the mochiweb-parsed form
+ T(req.body == "some=text");
+ T(req.form.some == "text");
+
+ // we can send error codes back
+ xhr = CouchDB.request("GET", "/test_suite_db/_external/action/an_action/requires_put");
+ T(xhr.status == 405);
+ T(xhr.responseText == "Method Not Allowed, Punk!");
+
+ xhr = CouchDB.request("PUT", "/test_suite_db/_external/action/an_action/requires_put");
+ T(xhr.status == 200);
+ T(xhr.responseText == "thanks for the PUT");
+ },
+
+ action_server_requests : function(debug) {
+ var db = new CouchDB("test_suite_db");
+ db.deleteDb();
+ db.createDb();
+ if (debug) debugger;
+
+ // create views for get, post, put and delete
+ var verbControllerDoc = {
+ _id: "_design/verbs",
+ language: "javascript",
+ actions: {
+ "get" : "function(req, db) { var doc = db.open(req.query.docid); return {json:doc} }",
+ "post" : "function(req, db) { var rs = db.save({'req' : req }); return {json:rs} }",
+ "put" : "function(req, db) { var rs = db.save({ '_id' : req.query.setid, 'req' : req.query }); return {json:rs} }",
+ "delete": "function(req, db) { var id = req.query.delid; var doc = db.open(id); var rd = db.deleteDoc(doc); return {json:rd} }"
+ }
+ }
+ T(db.save(verbControllerDoc).ok);
+
+ // test firing verbs
+
+ // test GET
+ var doc = {foo:"bar"};
+ var result = db.save(doc);
+ var xhr = CouchDB.request("GET", "/test_suite_db/_external/action/verbs/get?docid="+result.id);
+ var resp = JSON.parse(xhr.responseText);
+ T(resp.foo == "bar");
+
+ // test POST
+ xhr = CouchDB.request("GET", "/test_suite_db/_external/action/verbs/post?baz=boom");
+ resp = JSON.parse(xhr.responseText);
+ doc = db.open(resp.id);
+ T(doc.req.query.baz == "boom");
+
+ // test PUT
+ xhr = CouchDB.request("GET", "/test_suite_db/_external/action/verbs/put?setid=mynewdocid&flim=flam");
+ doc = db.open("mynewdocid");
+ T(doc.req.flim == "flam");
+
+ // test DELETE
+ xhr = CouchDB.request("GET", "/test_suite_db/_external/action/verbs/delete?delid=mynewdocid");
+ T(db.open("mynewdocid") == null);
+
+ // PUT through on top of an existing id and see the error at the client
+ var created = db.save({
+ _id : "takethisid",
+ key : "value"
+ });
+ T(created.ok);
+ xhr = CouchDB.request("GET", "/test_suite_db/_external/action/verbs/put?setid=takethisid&flim=flam");
+ resp = JSON.parse(xhr.responseText);
+ T(resp.error == "conflict");
+ T(resp.reason == "Document update conflict.");
+ },
+
security_validation : function(debug) {
// This tests couchdb's security and validation features. This does
// not test authentication, except to use test authentication code made