blob: fcd8306195a6a73c61889d4b12f805f4d11e9dd7 [file] [log] [blame]
// 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.
function jsonp(obj) {
T(jsonp_flag == 0);
T(obj.results.length == 1 && obj.last_seq == 1, "jsonp");
jsonp_flag = 1;
}
couchTests.changes = function(debug) {
var db = new CouchDB("test_suite_db", {"X-Couch-Full-Commit":"true"});
db.deleteDb();
db.createDb();
if (debug) debugger;
var req = CouchDB.request("GET", "/test_suite_db/_changes");
var resp = JSON.parse(req.responseText);
T(resp.results.length == 0 && resp.last_seq == 0, "empty db");
var docFoo = {_id:"foo", bar:1};
T(db.save(docFoo).ok);
T(db.ensureFullCommit().ok);
T(db.open(docFoo._id)._id == docFoo._id);
req = CouchDB.request("GET", "/test_suite_db/_changes");
var resp = JSON.parse(req.responseText);
T(resp.last_seq == 1);
T(resp.results.length == 1, "one doc db");
T(resp.results[0].changes[0].rev == docFoo._rev);
// test with callback
run_on_modified_server(
[{section: "httpd",
key: "allow_jsonp",
value: "true"}],
function() {
var xhr = CouchDB.request("GET", "/test_suite_db/_changes?callback=jsonp");
T(xhr.status == 200);
jsonp_flag = 0;
eval(xhr.responseText);
T(jsonp_flag == 1);
});
req = CouchDB.request("GET", "/test_suite_db/_changes?feed=continuous&timeout=10");
var lines = req.responseText.split("\n");
T(JSON.parse(lines[0]).changes[0].rev == docFoo._rev);
T(JSON.parse(lines[1]).last_seq == 1);
var xhr;
try {
xhr = CouchDB.newXhr();
} catch (err) {
}
// poor man's browser detection
var is_safari = false;
if(typeof(navigator) == "undefined") {
is_safari = true; // For CouchHTTP based runners
} else if(navigator.userAgent.match(/AppleWebKit/)) {
is_safari = true;
};
if (!is_safari && xhr) {
// Only test the continuous stuff if we have a real XHR object
// with real async support.
// WebKit (last checked on nightly #47686) does fail on processing
// the async-request properly while javascript is executed.
xhr.open("GET", CouchDB.proxyUrl("/test_suite_db/_changes?feed=continuous&timeout=500"), true);
xhr.send("");
var docBar = {_id:"bar", bar:1};
db.save(docBar);
var lines, change1, change2;
waitForSuccess(function() {
lines = xhr.responseText.split("\n");
change1 = JSON.parse(lines[0]);
change2 = JSON.parse(lines[1]);
if (change2.seq != 2) {
throw "bad seq, try again";
}
return true;
}, "bar-only");
T(change1.seq == 1);
T(change1.id == "foo");
T(change2.seq == 2);
T(change2.id == "bar");
T(change2.changes[0].rev == docBar._rev);
var docBaz = {_id:"baz", baz:1};
db.save(docBaz);
var change3;
waitForSuccess(function() {
lines = xhr.responseText.split("\n");
change3 = JSON.parse(lines[2]);
if (change3.seq != 3) {
throw "bad seq, try again";
}
return true;
});
T(change3.seq == 3);
T(change3.id == "baz");
T(change3.changes[0].rev == docBaz._rev);
xhr = CouchDB.newXhr();
//verify the hearbeat newlines are sent
xhr.open("GET", CouchDB.proxyUrl("/test_suite_db/_changes?feed=continuous&heartbeat=10&timeout=500"), true);
xhr.send("");
var str;
waitForSuccess(function() {
str = xhr.responseText;
if (str.charAt(str.length - 1) != "\n" || str.charAt(str.length - 2) != "\n") {
throw("keep waiting");
}
return true;
}, "heartbeat");
T(str.charAt(str.length - 1) == "\n");
T(str.charAt(str.length - 2) == "\n");
// otherwise we'll continue to receive heartbeats forever
xhr.abort();
// test Server Sent Event (eventsource)
if (!!window.EventSource) {
var source = new EventSource(
"/test_suite_db/_changes?feed=eventsource");
var results = [];
var sourceListener = function(e) {
var data = JSON.parse(e.data);
results.push(data);
};
source.addEventListener('message', sourceListener , false);
waitForSuccess(function() {
if (results.length != 3) {
throw "bad seq, try again";
}
return true;
});
source.removeEventListener('message', sourceListener, false);
T(results[0].seq == 1);
T(results[0].id == "foo");
T(results[1].seq == 2);
T(results[1].id == "bar");
T(results[1].changes[0].rev == docBar._rev);
}
// test longpolling
xhr = CouchDB.newXhr();
xhr.open("GET", CouchDB.proxyUrl("/test_suite_db/_changes?feed=longpoll"), true);
xhr.send("");
waitForSuccess(function() {
lines = xhr.responseText.split("\n");
if (lines[5] != '"last_seq":3}') {
throw("still waiting");
}
return true;
}, "last_seq");
xhr = CouchDB.newXhr();
xhr.open("GET", CouchDB.proxyUrl("/test_suite_db/_changes?feed=longpoll&since=3"), true);
xhr.send("");
var docBarz = {_id:"barz", bar:1};
db.save(docBarz);
var parse_changes_line = function(line) {
if (line.charAt(line.length-1) == ",") {
var linetrimmed = line.substring(0, line.length-1);
} else {
var linetrimmed = line;
}
return JSON.parse(linetrimmed);
};
waitForSuccess(function() {
lines = xhr.responseText.split("\n");
if (lines[3] != '"last_seq":4}') {
throw("still waiting");
}
return true;
}, "change_lines");
var change = parse_changes_line(lines[1]);
T(change.seq == 4);
T(change.id == "barz");
T(change.changes[0].rev == docBarz._rev);
T(lines[3]=='"last_seq":4}');
// test since=now
xhr = CouchDB.newXhr();
xhr.open("GET", "/test_suite_db/_changes?feed=longpoll&since=now", true);
xhr.send("");
var docBarz = {_id:"barzzzz", bar:1};
db.save(docBarz);
var parse_changes_line = function(line) {
if (line.charAt(line.length-1) == ",") {
var linetrimmed = line.substring(0, line.length-1);
} else {
var linetrimmed = line;
}
return JSON.parse(linetrimmed);
};
waitForSuccess(function() {
lines = xhr.responseText.split("\n");
if (lines[3] != '"last_seq":5}') {
throw("still waiting");
}
return true;
}, "change_lines");
var change = parse_changes_line(lines[1]);
T(change.seq == 5);
T(change.id == "barzzzz");
T(change.changes[0].rev == docBarz._rev);
T(lines[3]=='"last_seq":5}');
}
// test the filtered changes
var ddoc = {
_id : "_design/changes_filter",
"filters" : {
"bop" : "function(doc, req) { return (doc.bop);}",
"dynamic" : stringFun(function(doc, req) {
var field = req.query.field;
return doc[field];
}),
"userCtx" : stringFun(function(doc, req) {
return doc.user && (doc.user == req.userCtx.name);
}),
"conflicted" : "function(doc, req) { return (doc._conflicts);}"
},
options : {
local_seq : true
},
views : {
local_seq : {
map : "function(doc) {emit(doc._local_seq, null)}"
},
blah: {
map : 'function(doc) {' +
' if (doc._id == "blah") {' +
' emit(null, null);' +
' }' +
'}'
}
}
};
db.save(ddoc);
var req = CouchDB.request("GET", "/test_suite_db/_changes?filter=changes_filter/bop");
var resp = JSON.parse(req.responseText);
T(resp.results.length == 0);
db.save({"bop" : "foom"});
db.save({"bop" : false});
var req = CouchDB.request("GET", "/test_suite_db/_changes?filter=changes_filter/bop");
var resp = JSON.parse(req.responseText);
T(resp.results.length == 1, "filtered/bop");
req = CouchDB.request("GET", "/test_suite_db/_changes?filter=changes_filter/dynamic&field=woox");
resp = JSON.parse(req.responseText);
T(resp.results.length == 0);
req = CouchDB.request("GET", "/test_suite_db/_changes?filter=changes_filter/dynamic&field=bop");
resp = JSON.parse(req.responseText);
T(resp.results.length == 1, "changes_filter/dynamic&field=bop");
if (!is_safari && xhr) { // full test requires parallel connections
// filter with longpoll
// longpoll filters full history when run without a since seq
xhr = CouchDB.newXhr();
xhr.open("GET", CouchDB.proxyUrl("/test_suite_db/_changes?feed=longpoll&filter=changes_filter/bop"), false);
xhr.send("");
var resp = JSON.parse(xhr.responseText);
T(resp.last_seq == 8);
// longpoll waits until a matching change before returning
xhr = CouchDB.newXhr();
xhr.open("GET", CouchDB.proxyUrl("/test_suite_db/_changes?feed=longpoll&since=7&filter=changes_filter/bop"), true);
xhr.send("");
db.save({"_id":"falsy", "bop" : ""}); // empty string is falsy
db.save({"_id":"bingo","bop" : "bingo"});
waitForSuccess(function() {
resp = JSON.parse(xhr.responseText);
return true;
}, "longpoll-since");
T(resp.last_seq == 10);
T(resp.results && resp.results.length > 0 && resp.results[0]["id"] == "bingo", "filter the correct update");
xhr.abort();
var timeout = 500;
var last_seq = 11;
while (true) {
// filter with continuous
xhr = CouchDB.newXhr();
xhr.open("GET", CouchDB.proxyUrl("/test_suite_db/_changes?feed=continuous&filter=changes_filter/bop&timeout="+timeout), true);
xhr.send("");
db.save({"_id":"rusty", "bop" : "plankton"});
T(xhr.readyState != 4, "test client too slow");
var rusty = db.open("rusty", {cache_bust : new Date()});
T(rusty._id == "rusty");
waitForSuccess(function() { // throws an error after 5 seconds
if (xhr.readyState != 4) {
throw("still waiting");
}
return true;
}, "continuous-rusty");
lines = xhr.responseText.split("\n");
var good = false;
try {
JSON.parse(lines[3]);
good = true;
} catch(e) {
}
if (good) {
T(JSON.parse(lines[1]).id == "bingo", lines[1]);
T(JSON.parse(lines[2]).id == "rusty", lines[2]);
T(JSON.parse(lines[3]).last_seq == last_seq, lines[3]);
break;
} else {
xhr.abort();
db.deleteDoc(rusty);
timeout = timeout * 2;
last_seq = last_seq + 2;
}
}
}
// error conditions
// non-existing design doc
var req = CouchDB.request("GET",
"/test_suite_db/_changes?filter=nothingtosee/bop");
TEquals(404, req.status, "should return 404 for non existant design doc");
// non-existing filter
var req = CouchDB.request("GET",
"/test_suite_db/_changes?filter=changes_filter/movealong");
TEquals(404, req.status, "should return 404 for non existant filter fun");
// both
var req = CouchDB.request("GET",
"/test_suite_db/_changes?filter=nothingtosee/movealong");
TEquals(404, req.status,
"should return 404 for non existant design doc and filter fun");
// changes get all_docs style with deleted docs
var doc = {a:1};
db.save(doc);
db.deleteDoc(doc);
var req = CouchDB.request("GET",
"/test_suite_db/_changes?filter=changes_filter/bop&style=all_docs");
var resp = JSON.parse(req.responseText);
var expect = (!is_safari && xhr) ? 3: 1;
TEquals(expect, resp.results.length, "should return matching rows");
// test filter on view function (map)
//
T(db.save({"_id":"blah", "bop" : "plankton"}).ok);
var req = CouchDB.request("GET", "/test_suite_db/_changes?filter=_view&view=changes_filter/blah");
var resp = JSON.parse(req.responseText);
T(resp.results.length === 1);
T(resp.results[0].id === "blah");
// test for userCtx
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 authOpts = {"headers":{"WWW-Authenticate": "X-Couch-Test-Auth Chris Anderson:mp3"}};
var req = CouchDB.request("GET", "/_session", authOpts);
var resp = JSON.parse(req.responseText);
T(db.save({"user" : "Noah Slater"}).ok);
var req = CouchDB.request("GET", "/test_suite_db/_changes?filter=changes_filter/userCtx", authOpts);
var resp = JSON.parse(req.responseText);
T(resp.results.length == 0);
var docResp = db.save({"user" : "Chris Anderson"});
T(docResp.ok);
T(db.ensureFullCommit().ok);
req = CouchDB.request("GET", "/test_suite_db/_changes?filter=changes_filter/userCtx", authOpts);
resp = JSON.parse(req.responseText);
T(resp.results.length == 1, "userCtx");
T(resp.results[0].id == docResp.id);
}
);
req = CouchDB.request("GET", "/test_suite_db/_changes?limit=1");
resp = JSON.parse(req.responseText);
TEquals(1, resp.results.length);
//filter includes _conflicts
var id = db.save({'food' : 'pizza'}).id;
db.bulkSave([{_id: id, 'food' : 'pasta'}], {all_or_nothing:true});
req = CouchDB.request("GET", "/test_suite_db/_changes?filter=changes_filter/conflicted");
resp = JSON.parse(req.responseText);
T(resp.results.length == 1, "filter=changes_filter/conflicted");
// test with erlang filter function
run_on_modified_server([{
section: "native_query_servers",
key: "erlang",
value: "{couch_native_process, start_link, []}"
}], function() {
var erl_ddoc = {
_id: "_design/erlang",
language: "erlang",
filters: {
foo:
'fun({Doc}, Req) -> ' +
' case couch_util:get_value(<<"value">>, Doc) of' +
' undefined -> false;' +
' Value -> (Value rem 2) =:= 0;' +
' _ -> false' +
' end ' +
'end.'
}
};
db.deleteDb();
db.createDb();
T(db.save(erl_ddoc).ok);
var req = CouchDB.request("GET", "/test_suite_db/_changes?filter=erlang/foo");
var resp = JSON.parse(req.responseText);
T(resp.results.length === 0);
T(db.save({_id: "doc1", value : 1}).ok);
T(db.save({_id: "doc2", value : 2}).ok);
T(db.save({_id: "doc3", value : 3}).ok);
T(db.save({_id: "doc4", value : 4}).ok);
var req = CouchDB.request("GET", "/test_suite_db/_changes?filter=erlang/foo");
var resp = JSON.parse(req.responseText);
T(resp.results.length === 2);
T(resp.results[0].id === "doc2");
T(resp.results[1].id === "doc4");
// test filtering on docids
//
var options = {
headers: {"Content-Type": "application/json"},
body: JSON.stringify({"doc_ids": ["something", "anotherthing", "andmore"]})
};
var req = CouchDB.request("POST", "/test_suite_db/_changes?filter=_doc_ids", options);
var resp = JSON.parse(req.responseText);
T(resp.results.length === 0);
T(db.save({"_id":"something", "bop" : "plankton"}).ok);
var req = CouchDB.request("POST", "/test_suite_db/_changes?filter=_doc_ids", options);
var resp = JSON.parse(req.responseText);
T(resp.results.length === 1);
T(resp.results[0].id === "something");
T(db.save({"_id":"anotherthing", "bop" : "plankton"}).ok);
var req = CouchDB.request("POST", "/test_suite_db/_changes?filter=_doc_ids", options);
var resp = JSON.parse(req.responseText);
T(resp.results.length === 2);
T(resp.results[0].id === "something");
T(resp.results[1].id === "anotherthing");
var docids = JSON.stringify(["something", "anotherthing", "andmore"]),
req = CouchDB.request("GET", "/test_suite_db/_changes?filter=_doc_ids&doc_ids="+docids, options);
var resp = JSON.parse(req.responseText);
T(resp.results.length === 2);
T(resp.results[0].id === "something");
T(resp.results[1].id === "anotherthing");
var req = CouchDB.request("GET", "/test_suite_db/_changes?filter=_design");
var resp = JSON.parse(req.responseText);
T(resp.results.length === 1);
T(resp.results[0].id === "_design/erlang");
if (!is_safari && xhr) {
// filter docids with continuous
xhr = CouchDB.newXhr();
xhr.open("POST", CouchDB.proxyUrl("/test_suite_db/_changes?feed=continuous&timeout=500&since=7&filter=_doc_ids"), true);
xhr.setRequestHeader("Content-Type", "application/json");
xhr.send(options.body);
T(db.save({"_id":"andmore", "bop" : "plankton"}).ok);
waitForSuccess(function() {
if (xhr.readyState != 4) {
throw("still waiting");
}
return true;
}, "andmore-only");
var line = JSON.parse(xhr.responseText.split("\n")[0]);
T(line.seq == 8);
T(line.id == "andmore");
}
});
// COUCHDB-1037 - empty result for ?limit=1&filter=foo/bar in some cases
T(db.deleteDb());
T(db.createDb());
ddoc = {
_id: "_design/testdocs",
filters: {
testdocsonly: (function(doc, req) {
return (typeof doc.integer === "number");
}).toString()
}
};
T(db.save(ddoc));
ddoc = {
_id: "_design/foobar",
foo: "bar"
};
T(db.save(ddoc));
db.bulkSave(makeDocs(0, 5));
req = CouchDB.request("GET", "/" + db.name + "/_changes");
resp = JSON.parse(req.responseText);
TEquals(7, resp.last_seq);
TEquals(7, resp.results.length);
req = CouchDB.request(
"GET", "/"+ db.name + "/_changes?limit=1&filter=testdocs/testdocsonly");
resp = JSON.parse(req.responseText);
TEquals(3, resp.last_seq);
TEquals(1, resp.results.length);
TEquals("0", resp.results[0].id);
req = CouchDB.request(
"GET", "/" + db.name + "/_changes?limit=2&filter=testdocs/testdocsonly");
resp = JSON.parse(req.responseText);
TEquals(4, resp.last_seq);
TEquals(2, resp.results.length);
TEquals("0", resp.results[0].id);
TEquals("1", resp.results[1].id);
TEquals(0, CouchDB.requestStats('httpd', 'clients_requesting_changes').current);
CouchDB.request("GET", "/" + db.name + "/_changes");
TEquals(0, CouchDB.requestStats('httpd', 'clients_requesting_changes').current);
// COUCHDB-1256
T(db.deleteDb());
T(db.createDb());
T(db.save({"_id":"foo", "a" : 123}).ok);
T(db.save({"_id":"bar", "a" : 456}).ok);
options = {
headers: {"Content-Type": "application/json"},
body: JSON.stringify({"_rev":"1-cc609831f0ca66e8cd3d4c1e0d98108a", "a":456})
};
req = CouchDB.request("PUT", "/" + db.name + "/foo?new_edits=false", options);
req = CouchDB.request("GET", "/" + db.name + "/_changes?style=all_docs");
resp = JSON.parse(req.responseText);
TEquals(3, resp.last_seq);
TEquals(2, resp.results.length);
req = CouchDB.request("GET", "/" + db.name + "/_changes?style=all_docs&since=2");
resp = JSON.parse(req.responseText);
TEquals(3, resp.last_seq);
TEquals(1, resp.results.length);
TEquals(2, resp.results[0].changes.length);
// COUCHDB-1852
T(db.deleteDb());
T(db.createDb());
// create 4 documents... this assumes the update sequnce will start from 0 and get to 4
db.save({"bop" : "foom"});
db.save({"bop" : "foom"});
db.save({"bop" : "foom"});
db.save({"bop" : "foom"});
// simulate an EventSource request with a Last-Event-ID header
req = CouchDB.request("GET", "/test_suite_db/_changes?feed=eventsource&timeout=0&since=0",
{"headers": {"Accept": "text/event-stream", "Last-Event-ID": "2"}});
// "parse" the eventsource response and collect only the "id: ..." lines
var changes = req.responseText.split('\n')
.map(function (el) {
return el.split(":").map(function (el) { return el.trim()});
})
.filter(function (el) { return (el[0] === "id"); })
// make sure we only got 2 changes, and they are update_seq=3 and update_seq=4
T(changes.length === 2);
T(changes[0][1] === "3");
T(changes[1][1] === "4");
// cleanup
db.deleteDb();
};