// 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; if (debug) debugger; // 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; } testChanges("live"); testChanges("continuous"); function testChanges(feed) { var db_name = get_random_db_name(); // (write-quorums help keep a consistent feed) db = new CouchDB(db_name, {"X-Couch-Full-Commit":"true"}, {"w": 3}); db.createDb(); var req = CouchDB.request("GET", "/" + db_name + "/_changes"); var resp = JSON.parse(req.responseText); TEquals(0, resp.results.length, "db must be empty") TEquals("0", resp.last_seq.substr(0, 1), "seq must start with 0") var docFoo = {_id:"foo", bar:1}; T(db.save(docFoo).ok); T(db.ensureFullCommit().ok); T(db.open(docFoo._id)._id == docFoo._id); retry_part(function(){ // avoid Heisenbugs req = CouchDB.request("GET", "/" + db_name + "/_changes"); var resp = JSON.parse(req.responseText); TEquals("1", resp.last_seq.substr(0, 1), "seq must start with 1"); T(resp.results.length == 1, "one doc db"); T(resp.results[0].changes[0].rev == docFoo._rev); }); // test with callback // TODO: either allow jsonp in the default global config or implement a config chg mechanism analogouts 2 sebastianrothbucher:clustertest - or leave out // run_on_modified_server( // [{section: "httpd", // key: "allow_jsonp", // value: "true"}], // function() { // var xhr = CouchDB.request("GET", "/" + db_name + "/_changes?callback=jsonp"); // T(xhr.status == 200); // jsonp_flag = 0; // eval(xhr.responseText); // T(jsonp_flag == 1); // }); // increase timeout to 100 to have enough time 2 assemble (seems like too little timeouts kill req = CouchDB.request("GET", "/" + db_name + "/_changes?feed=" + feed + "&timeout=100"); var lines = req.responseText.split("\n"); T(JSON.parse(lines[0]).changes[0].rev == docFoo._rev); // the sequence is not fully ordered and a complex structure now T(JSON.parse(lines[1]).last_seq[0] == 1); var xhr; try { xhr = CouchDB.newXhr(); } catch (err) { } // these will NEVER run as we're always in navigator == undefined 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("/" + db_name + "/_changes?feed=" + feed + "&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 heartbeat newlines are sent xhr.open("GET", CouchDB.proxyUrl("/" + db_name + "/_changes?feed=" + feed + "&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(); } db.deleteDb(); } // these will NEVER run as we're always in navigator == undefined if (!is_safari && xhr) { // test Server Sent Event (eventsource) if (!!window.EventSource) { var source = new EventSource( "/" + db_name + "/_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 that we receive EventSource heartbeat events if (!!window.EventSource) { var source = new EventSource( "/" + db_name + "/_changes?feed=eventsource&heartbeat=10"); var count_heartbeats = 0; source.addEventListener('heartbeat', function () { count_heartbeats = count_heartbeats + 1; } , false); waitForSuccess(function() { if (count_heartbeats < 3) { throw "keep waiting"; } return true; }, "eventsource-heartbeat"); T(count_heartbeats >= 3); source.close(); } // test longpolling xhr = CouchDB.newXhr(); xhr.open("GET", CouchDB.proxyUrl("/" + db_name + "/_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("/" + db_name + "/_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", "/" + db_name + "/_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}'); } db.deleteDb(); // test on a new DB var db_name = get_random_db_name(); db = new CouchDB(db_name, {"X-Couch-Full-Commit":"true"}, {"w": 3}); db.createDb(); // 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", "/" + db_name + "/_changes?filter=changes_filter/bop"); var resp = JSON.parse(req.responseText); T(resp.results.length == 0); var docres1 = db.save({"bop" : "foom"}); T(docres1.ok); var docres2 = db.save({"bop" : false}); T(docres2.ok); var req = CouchDB.request("GET", "/" + db_name + "/_changes?filter=changes_filter/bop"); var resp = JSON.parse(req.responseText); var seqold = resp.results[0].seq; T(resp.results.length == 1, "filtered/bop"); T(resp.results[0].changes[0].rev == docres1.rev, "filtered/bop rev"); // save and reload (substitute for all those parts that never run) var chgdoc1 = db.open(docres1.id); chgdoc1.newattr = "s/th new"; docres1 = db.save(chgdoc1); T(docres1.ok); req = CouchDB.request("GET", "/" + db_name + "/_changes?filter=changes_filter/bop"); resp = JSON.parse(req.responseText); var seqchg = resp.results[0].seq; T(resp.results.length == 1, "filtered/bop new"); T(resp.results[0].changes[0].rev == docres1.rev, "filtered/bop rev new"); T(seqold != seqchg, "filtered/bop new seq number"); req = CouchDB.request("GET", "/" + db_name + "/_changes?filter=changes_filter/dynamic&field=woox"); resp = JSON.parse(req.responseText); T(resp.results.length == 0); req = CouchDB.request("GET", "/" + db_name + "/_changes?filter=changes_filter/dynamic&field=bop"); resp = JSON.parse(req.responseText); T(resp.results.length == 1, "changes_filter/dynamic&field=bop"); T(resp.results[0].changes[0].rev == docres1.rev, "filtered/dynamic&field=bop rev"); // these will NEVER run as we're always in navigator == undefined 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("/" + db_name + "/_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("/" + db_name + "/_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("/" + db_name + "/_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", "/" + db_name + "/_changes?filter=nothingtosee/bop"); TEquals(404, req.status, "should return 404 for non existant design doc"); // non-existing filter var req = CouchDB.request("GET", "/" + db_name + "/_changes?filter=changes_filter/movealong"); TEquals(404, req.status, "should return 404 for non existant filter fun"); // both var req = CouchDB.request("GET", "/" + db_name + "/_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", "/" + db_name + "/_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", "/" + db_name + "/_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 // TODO: either make part of global config, or allow 4 config changes - or leave out /* 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", "/" + db_name + "/_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", "/" + db_name + "/_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", "/" + db_name + "/_changes?limit=1"); resp = JSON.parse(req.responseText); TEquals(1, resp.results.length); //filter includes _conflicts // TODO: all_or_nothing not yet in place // var id = db.save({'food' : 'pizza'}).id; // db.bulkSave([{_id: id, 'food' : 'pasta'}], {all_or_nothing:true}); // // req = CouchDB.request("GET", "/" + db_name + "/_changes?filter=changes_filter/conflicted"); // resp = JSON.parse(req.responseText); // T(resp.results.length == 1, "filter=changes_filter/conflicted"); // test with erlang filter function // TODO: either make part of global config, or allow 4 config changes - or leave out /* 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", "/" + db_name + "/_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", "/" + db_name + "/_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", "/" + db_name + "/_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", "/" + db_name + "/_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", "/" + db_name + "/_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", "/" + db_name + "/_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", "/" + db_name + "/_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("/" + db_name + "/_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"); } }); */ db.deleteDb(); // COUCHDB-1037 - empty result for ?limit=1&filter=foo/bar in some cases // test w/ new temp DB db_name = get_random_db_name(); db = new CouchDB(db_name, {"X-Couch-Full-Commit":"true"}, {"w": 3}); 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)); // for n>1 you can't be sure all docs are there immediately - so either stick w/ -n 1 or implement check-wait-check or use the quorum (for now, the latter seems 2 suffice) req = CouchDB.request("GET", "/" + db.name + "/_changes"); resp = JSON.parse(req.responseText); // you can't know wether 7 is the last seq as you don't know how many collapse into one number //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); // (seq as before) //TEquals(3, resp.last_seq); TEquals(1, resp.results.length); // also, we can't guarantee ordering T(resp.results[0].id.match("[0-5]")); req = CouchDB.request( "GET", "/" + db.name + "/_changes?limit=2&filter=testdocs/testdocsonly"); resp = JSON.parse(req.responseText); // (seq as before) //TEquals(4, resp.last_seq); TEquals(2, resp.results.length); // also, we can't guarantee ordering T(resp.results[0].id.match("[0-5]")); T(resp.results[1].id.match("[0-5]")); // TODO: either use local port for stats (and aggregate when n>1) or leave out // TEquals(0, CouchDB.requestStats(['couchdb', 'httpd', 'clients_requesting_changes'], true).value); // CouchDB.request("GET", "/" + db.name + "/_changes"); // TEquals(0, CouchDB.requestStats(['couchdb', 'httpd', 'clients_requesting_changes'], true).value); db.deleteDb(); // COUCHDB-1256 // test w/ new temp DB db_name = get_random_db_name(); db = new CouchDB(db_name, {"X-Couch-Full-Commit":"true"}, {"w": 3}); 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); // (seq as before) //TEquals(3, resp.last_seq); TEquals(2, resp.results.length); // we can no longer pass a number into 'since' - but we have the 2nd last above - so we can use it (puh!) req = CouchDB.request("GET", "/" + db.name + "/_changes?style=all_docs&since=" + encodeURIComponent(resp.results[0].seq)); resp = JSON.parse(req.responseText); // (seq as before) //TEquals(3, resp.last_seq); TEquals(1, resp.results.length); // TEquals(2, resp.results[0].changes.length); db.deleteDb(); // COUCHDB-1852 // test w/ new temp DB db_name = get_random_db_name(); db = new CouchDB(db_name, {"X-Couch-Full-Commit":"true"}, {"w": 3}); T(db.createDb()); // create 4 documents... this assumes the update sequnce will start from 0 and then do sth in the cluster db.save({"bop" : "foom"}); db.save({"bop" : "foom"}); db.save({"bop" : "foom"}); db.save({"bop" : "foom"}); // because of clustering, we need the 2nd entry as since value req = CouchDB.request("GET", "/" + db_name + "/_changes"); // simulate an EventSource request with a Last-Event-ID header // increase timeout to 100 to have enough time 2 assemble (seems like too little timeouts kill req = CouchDB.request("GET", "/" + db_name + "/_changes?feed=eventsource&timeout=100&since=0", {"headers": {"Accept": "text/event-stream", "Last-Event-ID": JSON.parse(req.responseText).results[1].seq}}); // "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); // seq is different now //T(changes[0][1] === "3"); //T(changes[1][1] === "4"); db.deleteDb(); // COUCHDB-1923 // test w/ new temp DB db_name = get_random_db_name(); db = new CouchDB(db_name, {"X-Couch-Full-Commit":"true"}, {"w": 3}); T(db.createDb()); var attachmentData = "VGhpcyBpcyBhIGJhc2U2NCBlbmNvZGVkIHRleHQ="; db.bulkSave(makeDocs(20, 30, { _attachments:{ "foo.txt": { content_type:"text/plain", data: attachmentData }, "bar.txt": { content_type:"text/plain", data: attachmentData } } })); var mapFunction = function(doc) { var count = 0; for(var idx in doc._attachments) { count = count + 1; } emit(parseInt(doc._id), count); }; var req = CouchDB.request("GET", "/" + db_name + "/_changes?include_docs=true"); var resp = JSON.parse(req.responseText); T(resp.results.length == 10); T(resp.results[0].doc._attachments['foo.txt'].stub === true); T(resp.results[0].doc._attachments['foo.txt'].data === undefined); T(resp.results[0].doc._attachments['foo.txt'].encoding === undefined); T(resp.results[0].doc._attachments['foo.txt'].encoded_length === undefined); T(resp.results[0].doc._attachments['bar.txt'].stub === true); T(resp.results[0].doc._attachments['bar.txt'].data === undefined); T(resp.results[0].doc._attachments['bar.txt'].encoding === undefined); T(resp.results[0].doc._attachments['bar.txt'].encoded_length === undefined); var req = CouchDB.request("GET", "/" + db_name + "/_changes?include_docs=true&attachments=true"); var resp = JSON.parse(req.responseText); T(resp.results.length == 10); T(resp.results[0].doc._attachments['foo.txt'].stub === undefined); T(resp.results[0].doc._attachments['foo.txt'].data === attachmentData); T(resp.results[0].doc._attachments['foo.txt'].encoding === undefined); T(resp.results[0].doc._attachments['foo.txt'].encoded_length === undefined); T(resp.results[0].doc._attachments['bar.txt'].stub === undefined); T(resp.results[0].doc._attachments['bar.txt'].data == attachmentData); T(resp.results[0].doc._attachments['bar.txt'].encoding === undefined); T(resp.results[0].doc._attachments['bar.txt'].encoded_length === undefined); var req = CouchDB.request("GET", "/" + db_name + "/_changes?include_docs=true&att_encoding_info=true"); var resp = JSON.parse(req.responseText); T(resp.results.length == 10); T(resp.results[0].doc._attachments['foo.txt'].stub === true); T(resp.results[0].doc._attachments['foo.txt'].data === undefined); T(resp.results[0].doc._attachments['foo.txt'].encoding === "gzip"); T(resp.results[0].doc._attachments['foo.txt'].encoded_length === 47); T(resp.results[0].doc._attachments['bar.txt'].stub === true); T(resp.results[0].doc._attachments['bar.txt'].data === undefined); T(resp.results[0].doc._attachments['bar.txt'].encoding === "gzip"); T(resp.results[0].doc._attachments['bar.txt'].encoded_length === 47); db.deleteDb(); };