http://git-wip-us.apache.org/repos/asf/couchdb/blob/013ee66e/test/javascript/json2.js ---------------------------------------------------------------------- diff --git a/test/javascript/json2.js b/test/javascript/json2.js new file mode 100644 index 0000000..a1a3b17 --- /dev/null +++ b/test/javascript/json2.js @@ -0,0 +1,482 @@ +/* + http://www.JSON.org/json2.js + 2010-03-20 + + Public Domain. + + NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK. + + See http://www.JSON.org/js.html + + + This code should be minified before deployment. + See http://javascript.crockford.com/jsmin.html + + USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO + NOT CONTROL. + + + This file creates a global JSON object containing two methods: stringify + and parse. + + JSON.stringify(value, replacer, space) + value any JavaScript value, usually an object or array. + + replacer an optional parameter that determines how object + values are stringified for objects. It can be a + function or an array of strings. + + space an optional parameter that specifies the indentation + of nested structures. If it is omitted, the text will + be packed without extra whitespace. If it is a number, + it will specify the number of spaces to indent at each + level. If it is a string (such as '\t' or ' '), + it contains the characters used to indent at each level. + + This method produces a JSON text from a JavaScript value. + + When an object value is found, if the object contains a toJSON + method, its toJSON method will be called and the result will be + stringified. A toJSON method does not serialize: it returns the + value represented by the name/value pair that should be serialized, + or undefined if nothing should be serialized. The toJSON method + will be passed the key associated with the value, and this will be + bound to the value + + For example, this would serialize Dates as ISO strings. + + Date.prototype.toJSON = function (key) { + function f(n) { + // Format integers to have at least two digits. + return n < 10 ? '0' + n : n; + } + + return this.getUTCFullYear() + '-' + + f(this.getUTCMonth() + 1) + '-' + + f(this.getUTCDate()) + 'T' + + f(this.getUTCHours()) + ':' + + f(this.getUTCMinutes()) + ':' + + f(this.getUTCSeconds()) + 'Z'; + }; + + You can provide an optional replacer method. It will be passed the + key and value of each member, with this bound to the containing + object. The value that is returned from your method will be + serialized. If your method returns undefined, then the member will + be excluded from the serialization. + + If the replacer parameter is an array of strings, then it will be + used to select the members to be serialized. It filters the results + such that only members with keys listed in the replacer array are + stringified. + + Values that do not have JSON representations, such as undefined or + functions, will not be serialized. Such values in objects will be + dropped; in arrays they will be replaced with null. You can use + a replacer function to replace those with JSON values. + JSON.stringify(undefined) returns undefined. + + The optional space parameter produces a stringification of the + value that is filled with line breaks and indentation to make it + easier to read. + + If the space parameter is a non-empty string, then that string will + be used for indentation. If the space parameter is a number, then + the indentation will be that many spaces. + + Example: + + text = JSON.stringify(['e', {pluribus: 'unum'}]); + // text is '["e",{"pluribus":"unum"}]' + + + text = JSON.stringify(['e', {pluribus: 'unum'}], null, '\t'); + // text is '[\n\t"e",\n\t{\n\t\t"pluribus": "unum"\n\t}\n]' + + text = JSON.stringify([new Date()], function (key, value) { + return this[key] instanceof Date ? + 'Date(' + this[key] + ')' : value; + }); + // text is '["Date(---current time---)"]' + + + JSON.parse(text, reviver) + This method parses a JSON text to produce an object or array. + It can throw a SyntaxError exception. + + The optional reviver parameter is a function that can filter and + transform the results. It receives each of the keys and values, + and its return value is used instead of the original value. + If it returns what it received, then the structure is not modified. + If it returns undefined then the member is deleted. + + Example: + + // Parse the text. Values that look like ISO date strings will + // be converted to Date objects. + + myData = JSON.parse(text, function (key, value) { + var a; + if (typeof value === 'string') { + a = +/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value); + if (a) { + return new Date(Date.UTC(+a[1], +a[2] - 1, +a[3], +a[4], + +a[5], +a[6])); + } + } + return value; + }); + + myData = JSON.parse('["Date(09/09/2001)"]', function (key, value) { + var d; + if (typeof value === 'string' && + value.slice(0, 5) === 'Date(' && + value.slice(-1) === ')') { + d = new Date(value.slice(5, -1)); + if (d) { + return d; + } + } + return value; + }); + + + This is a reference implementation. You are free to copy, modify, or + redistribute. +*/ + +/*jslint evil: true, strict: false */ + +/*members "", "\b", "\t", "\n", "\f", "\r", "\"", JSON, "\\", apply, + call, charCodeAt, getUTCDate, getUTCFullYear, getUTCHours, + getUTCMinutes, getUTCMonth, getUTCSeconds, hasOwnProperty, join, + lastIndex, length, parse, prototype, push, replace, slice, stringify, + test, toJSON, toString, valueOf +*/ + + +// Create a JSON object only if one does not already exist. We create the +// methods in a closure to avoid creating global variables. + +if (!this.JSON) { + this.JSON = {}; +} + +(function () { + + function f(n) { + // Format integers to have at least two digits. + return n < 10 ? '0' + n : n; + } + + if (typeof Date.prototype.toJSON !== 'function') { + + Date.prototype.toJSON = function (key) { + + return isFinite(this.valueOf()) ? + this.getUTCFullYear() + '-' + + f(this.getUTCMonth() + 1) + '-' + + f(this.getUTCDate()) + 'T' + + f(this.getUTCHours()) + ':' + + f(this.getUTCMinutes()) + ':' + + f(this.getUTCSeconds()) + 'Z' : null; + }; + + String.prototype.toJSON = + Number.prototype.toJSON = + Boolean.prototype.toJSON = function (key) { + return this.valueOf(); + }; + } + + var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, + escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, + gap, + indent, + meta = { // table of character substitutions + '\b': '\\b', + '\t': '\\t', + '\n': '\\n', + '\f': '\\f', + '\r': '\\r', + '"' : '\\"', + '\\': '\\\\' + }, + rep; + + + function quote(string) { + +// If the string contains no control characters, no quote characters, and no +// backslash characters, then we can safely slap some quotes around it. +// Otherwise we must also replace the offending characters with safe escape +// sequences. + + escapable.lastIndex = 0; + return escapable.test(string) ? + '"' + string.replace(escapable, function (a) { + var c = meta[a]; + return typeof c === 'string' ? c : + '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); + }) + '"' : + '"' + string + '"'; + } + + + function str(key, holder) { + +// Produce a string from holder[key]. + + var i, // The loop counter. + k, // The member key. + v, // The member value. + length, + mind = gap, + partial, + value = holder[key]; + +// If the value has a toJSON method, call it to obtain a replacement value. + + if (value && typeof value === 'object' && + typeof value.toJSON === 'function') { + value = value.toJSON(key); + } + +// If we were called with a replacer function, then call the replacer to +// obtain a replacement value. + + if (typeof rep === 'function') { + value = rep.call(holder, key, value); + } + +// What happens next depends on the value's type. + + switch (typeof value) { + case 'string': + return quote(value); + + case 'number': + +// JSON numbers must be finite. Encode non-finite numbers as null. + + return isFinite(value) ? String(value) : 'null'; + + case 'boolean': + case 'null': + +// If the value is a boolean or null, convert it to a string. Note: +// typeof null does not produce 'null'. The case is included here in +// the remote chance that this gets fixed someday. + + return String(value); + +// If the type is 'object', we might be dealing with an object or an array or +// null. + + case 'object': + +// Due to a specification blunder in ECMAScript, typeof null is 'object', +// so watch out for that case. + + if (!value) { + return 'null'; + } + +// Make an array to hold the partial results of stringifying this object value. + + gap += indent; + partial = []; + +// Is the value an array? + + if (Object.prototype.toString.apply(value) === '[object Array]') { + +// The value is an array. Stringify every element. Use null as a placeholder +// for non-JSON values. + + length = value.length; + for (i = 0; i < length; i += 1) { + partial[i] = str(i, value) || 'null'; + } + +// Join all of the elements together, separated with commas, and wrap them in +// brackets. + + v = partial.length === 0 ? '[]' : + gap ? '[\n' + gap + + partial.join(',\n' + gap) + '\n' + + mind + ']' : + '[' + partial.join(',') + ']'; + gap = mind; + return v; + } + +// If the replacer is an array, use it to select the members to be stringified. + + if (rep && typeof rep === 'object') { + length = rep.length; + for (i = 0; i < length; i += 1) { + k = rep[i]; + if (typeof k === 'string') { + v = str(k, value); + if (v) { + partial.push(quote(k) + (gap ? ': ' : ':') + v); + } + } + } + } else { + +// Otherwise, iterate through all of the keys in the object. + + for (k in value) { + if (Object.hasOwnProperty.call(value, k)) { + v = str(k, value); + if (v) { + partial.push(quote(k) + (gap ? ': ' : ':') + v); + } + } + } + } + +// Join all of the member texts together, separated with commas, +// and wrap them in braces. + + v = partial.length === 0 ? '{}' : + gap ? '{\n' + gap + partial.join(',\n' + gap) + '\n' + + mind + '}' : '{' + partial.join(',') + '}'; + gap = mind; + return v; + } + } + +// If the JSON object does not yet have a stringify method, give it one. + + if (typeof JSON.stringify !== 'function') { + JSON.stringify = function (value, replacer, space) { + +// The stringify method takes a value and an optional replacer, and an optional +// space parameter, and returns a JSON text. The replacer can be a function +// that can replace values, or an array of strings that will select the keys. +// A default replacer method can be provided. Use of the space parameter can +// produce text that is more easily readable. + + var i; + gap = ''; + indent = ''; + +// If the space parameter is a number, make an indent string containing that +// many spaces. + + if (typeof space === 'number') { + for (i = 0; i < space; i += 1) { + indent += ' '; + } + +// If the space parameter is a string, it will be used as the indent string. + + } else if (typeof space === 'string') { + indent = space; + } + +// If there is a replacer, it must be a function or an array. +// Otherwise, throw an error. + + rep = replacer; + if (replacer && typeof replacer !== 'function' && + (typeof replacer !== 'object' || + typeof replacer.length !== 'number')) { + throw new Error('JSON.stringify'); + } + +// Make a fake root object containing our value under the key of ''. +// Return the result of stringifying the value. + + return str('', {'': value}); + }; + } + + +// If the JSON object does not yet have a parse method, give it one. + + if (typeof JSON.parse !== 'function') { + JSON.parse = function (text, reviver) { + +// The parse method takes a text and an optional reviver function, and returns +// a JavaScript value if the text is a valid JSON text. + + var j; + + function walk(holder, key) { + +// The walk method is used to recursively walk the resulting structure so +// that modifications can be made. + + var k, v, value = holder[key]; + if (value && typeof value === 'object') { + for (k in value) { + if (Object.hasOwnProperty.call(value, k)) { + v = walk(value, k); + if (v !== undefined) { + value[k] = v; + } else { + delete value[k]; + } + } + } + } + return reviver.call(holder, key, value); + } + + +// Parsing happens in four stages. In the first stage, we replace certain +// Unicode characters with escape sequences. JavaScript handles many characters +// incorrectly, either silently deleting them, or treating them as line endings. + + text = String(text); + cx.lastIndex = 0; + if (cx.test(text)) { + text = text.replace(cx, function (a) { + return '\\u' + + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); + }); + } + +// In the second stage, we run the text against regular expressions that look +// for non-JSON patterns. We are especially concerned with '()' and 'new' +// because they can cause invocation, and '=' because it can cause mutation. +// But just to be safe, we want to reject all unexpected forms. + +// We split the second stage into 4 regexp operations in order to work around +// crippling inefficiencies in IE's and Safari's regexp engines. First we +// replace the JSON backslash pairs with '@' (a non-JSON character). Second, we +// replace all simple value tokens with ']' characters. Third, we delete all +// open brackets that follow a colon or comma or that begin the text. Finally, +// we look to see that the remaining characters are only whitespace or ']' or +// ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval. + + if (/^[\],:{}\s]*$/. +test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@'). +replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']'). +replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) { + +// In the third stage we use the eval function to compile the text into a +// JavaScript structure. The '{' operator is subject to a syntactic ambiguity +// in JavaScript: it can begin a block or an object literal. We wrap the text +// in parens to eliminate the ambiguity. + + j = eval('(' + text + ')'); + +// In the optional fourth stage, we recursively walk the new structure, passing +// each name/value pair to a reviver function for possible transformation. + + return typeof reviver === 'function' ? + walk({'': j}, '') : j; + } + +// If the text is not JSON parseable, then a SyntaxError is thrown. + + throw new SyntaxError('JSON.parse'); + }; + } +}());
http://git-wip-us.apache.org/repos/asf/couchdb/blob/013ee66e/test/javascript/oauth.js ---------------------------------------------------------------------- diff --git a/test/javascript/oauth.js b/test/javascript/oauth.js new file mode 100644 index 0000000..ada00a2 --- /dev/null +++ b/test/javascript/oauth.js @@ -0,0 +1,511 @@ +/* + * Copyright 2008 Netflix, Inc. + * + * 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. + */ + +/* Here's some JavaScript software for implementing OAuth. + + This isn't as useful as you might hope. OAuth is based around + allowing tools and websites to talk to each other. However, + JavaScript running in web browsers is hampered by security + restrictions that prevent code running on one website from + accessing data stored or served on another. + + Before you start hacking, make sure you understand the limitations + posed by cross-domain XMLHttpRequest. + + On the bright side, some platforms use JavaScript as their + language, but enable the programmer to access other web sites. + Examples include Google Gadgets, and Microsoft Vista Sidebar. + For those platforms, this library should come in handy. +*/ + +// The HMAC-SHA1 signature method calls b64_hmac_sha1, defined by +// http://pajhome.org.uk/crypt/md5/sha1.js + +/* An OAuth message is represented as an object like this: + {method: "GET", action: "http://server.com/path", parameters: ...} + + The parameters may be either a map {name: value, name2: value2} + or an Array of name-value pairs [[name, value], [name2, value2]]. + The latter representation is more powerful: it supports parameters + in a specific sequence, or several parameters with the same name; + for example [["a", 1], ["b", 2], ["a", 3]]. + + Parameter names and values are NOT percent-encoded in an object. + They must be encoded before transmission and decoded after reception. + For example, this message object: + {method: "GET", action: "http://server/path", parameters: {p: "x y"}} + ... can be transmitted as an HTTP request that begins: + GET /path?p=x%20y HTTP/1.0 + (This isn't a valid OAuth request, since it lacks a signature etc.) + Note that the object "x y" is transmitted as x%20y. To encode + parameters, you can call OAuth.addToURL, OAuth.formEncode or + OAuth.getAuthorization. + + This message object model harmonizes with the browser object model for + input elements of an form, whose value property isn't percent encoded. + The browser encodes each value before transmitting it. For example, + see consumer.setInputs in example/consumer.js. + */ +var OAuth; if (OAuth == null) OAuth = {}; + +OAuth.setProperties = function setProperties(into, from) { + if (into != null && from != null) { + for (var key in from) { + into[key] = from[key]; + } + } + return into; +} + +OAuth.setProperties(OAuth, // utility functions +{ + percentEncode: function percentEncode(s) { + if (s == null) { + return ""; + } + if (s instanceof Array) { + var e = ""; + for (var i = 0; i < s.length; ++i) { + if (e != "") e += '&'; + e += percentEncode(s[i]); + } + return e; + } + s = encodeURIComponent(s); + // Now replace the values which encodeURIComponent doesn't do + // encodeURIComponent ignores: - _ . ! ~ * ' ( ) + // OAuth dictates the only ones you can ignore are: - _ . ~ + // Source: http://developer.mozilla.org/en/docs/Core_JavaScript_1.5_Reference:Global_Functions:encodeURIComponent + s = s.replace(/\!/g, "%21"); + s = s.replace(/\*/g, "%2A"); + s = s.replace(/\'/g, "%27"); + s = s.replace(/\(/g, "%28"); + s = s.replace(/\)/g, "%29"); + return s; + } +, + decodePercent: function decodePercent(s) { + if (s != null) { + // Handle application/x-www-form-urlencoded, which is defined by + // http://www.w3.org/TR/html4/interact/forms.html#h-17.13.4.1 + s = s.replace(/\+/g, " "); + } + return decodeURIComponent(s); + } +, + /** Convert the given parameters to an Array of name-value pairs. */ + getParameterList: function getParameterList(parameters) { + if (parameters == null) { + return []; + } + if (typeof parameters != "object") { + return decodeForm(parameters + ""); + } + if (parameters instanceof Array) { + return parameters; + } + var list = []; + for (var p in parameters) { + list.push([p, parameters[p]]); + } + return list; + } +, + /** Convert the given parameters to a map from name to value. */ + getParameterMap: function getParameterMap(parameters) { + if (parameters == null) { + return {}; + } + if (typeof parameters != "object") { + return getParameterMap(decodeForm(parameters + "")); + } + if (parameters instanceof Array) { + var map = {}; + for (var p = 0; p < parameters.length; ++p) { + var key = parameters[p][0]; + if (map[key] === undefined) { // first value wins + map[key] = parameters[p][1]; + } + } + return map; + } + return parameters; + } +, + getParameter: function getParameter(parameters, name) { + if (parameters instanceof Array) { + for (var p = 0; p < parameters.length; ++p) { + if (parameters[p][0] == name) { + return parameters[p][1]; // first value wins + } + } + } else { + return OAuth.getParameterMap(parameters)[name]; + } + return null; + } +, + formEncode: function formEncode(parameters) { + var form = ""; + var list = OAuth.getParameterList(parameters); + for (var p = 0; p < list.length; ++p) { + var value = list[p][1]; + if (value == null) value = ""; + if (form != "") form += '&'; + form += OAuth.percentEncode(list[p][0]) + +'='+ OAuth.percentEncode(value); + } + return form; + } +, + decodeForm: function decodeForm(form) { + var list = []; + var nvps = form.split('&'); + for (var n = 0; n < nvps.length; ++n) { + var nvp = nvps[n]; + if (nvp == "") { + continue; + } + var equals = nvp.indexOf('='); + var name; + var value; + if (equals < 0) { + name = OAuth.decodePercent(nvp); + value = null; + } else { + name = OAuth.decodePercent(nvp.substring(0, equals)); + value = OAuth.decodePercent(nvp.substring(equals + 1)); + } + list.push([name, value]); + } + return list; + } +, + setParameter: function setParameter(message, name, value) { + var parameters = message.parameters; + if (parameters instanceof Array) { + for (var p = 0; p < parameters.length; ++p) { + if (parameters[p][0] == name) { + if (value === undefined) { + parameters.splice(p, 1); + } else { + parameters[p][1] = value; + value = undefined; + } + } + } + if (value !== undefined) { + parameters.push([name, value]); + } + } else { + parameters = OAuth.getParameterMap(parameters); + parameters[name] = value; + message.parameters = parameters; + } + } +, + setParameters: function setParameters(message, parameters) { + var list = OAuth.getParameterList(parameters); + for (var i = 0; i < list.length; ++i) { + OAuth.setParameter(message, list[i][0], list[i][1]); + } + } +, + /** Fill in parameters to help construct a request message. + This function doesn't fill in every parameter. + The accessor object should be like: + {consumerKey:'foo', consumerSecret:'bar', accessorSecret:'nurn', token:'krelm', tokenSecret:'blah'} + The accessorSecret property is optional. + */ + completeRequest: function completeRequest(message, accessor) { + if (message.method == null) { + message.method = "GET"; + } + var map = OAuth.getParameterMap(message.parameters); + if (map.oauth_consumer_key == null) { + OAuth.setParameter(message, "oauth_consumer_key", accessor.consumerKey || ""); + } + if (map.oauth_token == null && accessor.token != null) { + OAuth.setParameter(message, "oauth_token", accessor.token); + } + if (map.oauth_version == null) { + OAuth.setParameter(message, "oauth_version", "1.0"); + } + if (map.oauth_timestamp == null) { + OAuth.setParameter(message, "oauth_timestamp", OAuth.timestamp()); + } + if (map.oauth_nonce == null) { + OAuth.setParameter(message, "oauth_nonce", OAuth.nonce(6)); + } + OAuth.SignatureMethod.sign(message, accessor); + } +, + setTimestampAndNonce: function setTimestampAndNonce(message) { + OAuth.setParameter(message, "oauth_timestamp", OAuth.timestamp()); + OAuth.setParameter(message, "oauth_nonce", OAuth.nonce(6)); + } +, + addToURL: function addToURL(url, parameters) { + newURL = url; + if (parameters != null) { + var toAdd = OAuth.formEncode(parameters); + if (toAdd.length > 0) { + var q = url.indexOf('?'); + if (q < 0) newURL += '?'; + else newURL += '&'; + newURL += toAdd; + } + } + return newURL; + } +, + /** Construct the value of the Authorization header for an HTTP request. */ + getAuthorizationHeader: function getAuthorizationHeader(realm, parameters) { + var header = 'OAuth realm="' + OAuth.percentEncode(realm) + '"'; + var list = OAuth.getParameterList(parameters); + for (var p = 0; p < list.length; ++p) { + var parameter = list[p]; + var name = parameter[0]; + if (name.indexOf("oauth_") == 0) { + header += ',' + OAuth.percentEncode(name) + '="' + OAuth.percentEncode(parameter[1]) + '"'; + } + } + return header; + } +, + timestamp: function timestamp() { + var d = new Date(); + return Math.floor(d.getTime()/1000); + } +, + nonce: function nonce(length) { + var chars = OAuth.nonce.CHARS; + var result = ""; + for (var i = 0; i < length; ++i) { + var rnum = Math.floor(Math.random() * chars.length); + result += chars.substring(rnum, rnum+1); + } + return result; + } +}); + +OAuth.nonce.CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz"; + +/** Define a constructor function, + without causing trouble to anyone who was using it as a namespace. + That is, if parent[name] already existed and had properties, + copy those properties into the new constructor. + */ +OAuth.declareClass = function declareClass(parent, name, newConstructor) { + var previous = parent[name]; + parent[name] = newConstructor; + if (newConstructor != null && previous != null) { + for (var key in previous) { + if (key != "prototype") { + newConstructor[key] = previous[key]; + } + } + } + return newConstructor; +} + +/** An abstract algorithm for signing messages. */ +OAuth.declareClass(OAuth, "SignatureMethod", function OAuthSignatureMethod(){}); + +OAuth.setProperties(OAuth.SignatureMethod.prototype, // instance members +{ + /** Add a signature to the message. */ + sign: function sign(message) { + var baseString = OAuth.SignatureMethod.getBaseString(message); + var signature = this.getSignature(baseString); + OAuth.setParameter(message, "oauth_signature", signature); + return signature; // just in case someone's interested + } +, + /** Set the key string for signing. */ + initialize: function initialize(name, accessor) { + var consumerSecret; + if (accessor.accessorSecret != null + && name.length > 9 + && name.substring(name.length-9) == "-Accessor") + { + consumerSecret = accessor.accessorSecret; + } else { + consumerSecret = accessor.consumerSecret; + } + this.key = OAuth.percentEncode(consumerSecret) + +"&"+ OAuth.percentEncode(accessor.tokenSecret); + } +}); + +/* SignatureMethod expects an accessor object to be like this: + {tokenSecret: "lakjsdflkj...", consumerSecret: "QOUEWRI..", accessorSecret: "xcmvzc..."} + The accessorSecret property is optional. + */ +// Class members: +OAuth.setProperties(OAuth.SignatureMethod, // class members +{ + sign: function sign(message, accessor) { + var name = OAuth.getParameterMap(message.parameters).oauth_signature_method; + if (name == null || name == "") { + name = "HMAC-SHA1"; + OAuth.setParameter(message, "oauth_signature_method", name); + } + OAuth.SignatureMethod.newMethod(name, accessor).sign(message); + } +, + /** Instantiate a SignatureMethod for the given method name. */ + newMethod: function newMethod(name, accessor) { + var impl = OAuth.SignatureMethod.REGISTERED[name]; + if (impl != null) { + var method = new impl(); + method.initialize(name, accessor); + return method; + } + var err = new Error("signature_method_rejected"); + var acceptable = ""; + for (var r in OAuth.SignatureMethod.REGISTERED) { + if (acceptable != "") acceptable += '&'; + acceptable += OAuth.percentEncode(r); + } + err.oauth_acceptable_signature_methods = acceptable; + throw err; + } +, + /** A map from signature method name to constructor. */ + REGISTERED : {} +, + /** Subsequently, the given constructor will be used for the named methods. + The constructor will be called with no parameters. + The resulting object should usually implement getSignature(baseString). + You can easily define such a constructor by calling makeSubclass, below. + */ + registerMethodClass: function registerMethodClass(names, classConstructor) { + for (var n = 0; n < names.length; ++n) { + OAuth.SignatureMethod.REGISTERED[names[n]] = classConstructor; + } + } +, + /** Create a subclass of OAuth.SignatureMethod, with the given getSignature function. */ + makeSubclass: function makeSubclass(getSignatureFunction) { + var superClass = OAuth.SignatureMethod; + var subClass = function() { + superClass.call(this); + }; + subClass.prototype = new superClass(); + // Delete instance variables from prototype: + // delete subclass.prototype... There aren't any. + subClass.prototype.getSignature = getSignatureFunction; + subClass.prototype.constructor = subClass; + return subClass; + } +, + getBaseString: function getBaseString(message) { + var URL = message.action; + var q = URL.indexOf('?'); + var parameters; + if (q < 0) { + parameters = message.parameters; + } else { + // Combine the URL query string with the other parameters: + parameters = OAuth.decodeForm(URL.substring(q + 1)); + var toAdd = OAuth.getParameterList(message.parameters); + for (var a = 0; a < toAdd.length; ++a) { + parameters.push(toAdd[a]); + } + } + return OAuth.percentEncode(message.method.toUpperCase()) + +'&'+ OAuth.percentEncode(OAuth.SignatureMethod.normalizeUrl(URL)) + +'&'+ OAuth.percentEncode(OAuth.SignatureMethod.normalizeParameters(parameters)); + } +, + normalizeUrl: function normalizeUrl(url) { + var uri = OAuth.SignatureMethod.parseUri(url); + var scheme = uri.protocol.toLowerCase(); + var authority = uri.authority.toLowerCase(); + var dropPort = (scheme == "http" && uri.port == 80) + || (scheme == "https" && uri.port == 443); + if (dropPort) { + // find the last : in the authority + var index = authority.lastIndexOf(":"); + if (index >= 0) { + authority = authority.substring(0, index); + } + } + var path = uri.path; + if (!path) { + path = "/"; // conforms to RFC 2616 section 3.2.2 + } + // we know that there is no query and no fragment here. + return scheme + "://" + authority + path; + } +, + parseUri: function parseUri (str) { + /* This function was adapted from parseUri 1.2.1 + http://stevenlevithan.com/demo/parseuri/js/assets/parseuri.js + */ + var o = {key: ["source","protocol","authority","userInfo","user","password","host","port","relative","path","directory","file","query","anchor"], + parser: {strict: /^(?:([^:\/?#]+):)?(?:\/\/((?:(([^:@]*):?([^:@]*))?@)?([^:\/?#]*)(?::(\d*))?))?((((?:[^?#\/]*\/)*)([^?#]*))(?:\?([^#]*))?(?:#(.*))?)/ }}; + var m = o.parser.strict.exec(str); + var uri = {}; + var i = 14; + while (i--) uri[o.key[i]] = m[i] || ""; + return uri; + } +, + normalizeParameters: function normalizeParameters(parameters) { + if (parameters == null) { + return ""; + } + var list = OAuth.getParameterList(parameters); + var sortable = []; + for (var p = 0; p < list.length; ++p) { + var nvp = list[p]; + if (nvp[0] != "oauth_signature") { + sortable.push([ OAuth.percentEncode(nvp[0]) + + " " // because it comes before any character that can appear in a percentEncoded string. + + OAuth.percentEncode(nvp[1]) + , nvp]); + } + } + sortable.sort(function(a,b) { + if (a[0] < b[0]) return -1; + if (a[0] > b[0]) return 1; + return 0; + }); + var sorted = []; + for (var s = 0; s < sortable.length; ++s) { + sorted.push(sortable[s][1]); + } + return OAuth.formEncode(sorted); + } +}); + +OAuth.SignatureMethod.registerMethodClass(["PLAINTEXT", "PLAINTEXT-Accessor"], + OAuth.SignatureMethod.makeSubclass( + function getSignature(baseString) { + return this.key; + } + )); + +OAuth.SignatureMethod.registerMethodClass(["HMAC-SHA1", "HMAC-SHA1-Accessor"], + OAuth.SignatureMethod.makeSubclass( + function getSignature(baseString) { + b64pad = '='; + var signature = b64_hmac_sha1(this.key, baseString); + return signature; + } + )); http://git-wip-us.apache.org/repos/asf/couchdb/blob/013ee66e/test/javascript/replicator_db_inc.js ---------------------------------------------------------------------- diff --git a/test/javascript/replicator_db_inc.js b/test/javascript/replicator_db_inc.js new file mode 100644 index 0000000..23f8587 --- /dev/null +++ b/test/javascript/replicator_db_inc.js @@ -0,0 +1,96 @@ +// 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. + +var replicator_db = {}; +replicator_db.wait_rep_doc = 500; // number of millisecs to wait after saving a Rep Doc +replicator_db.dbA = new CouchDB("test_suite_rep_db_a", {"X-Couch-Full-Commit":"false"}); +replicator_db.dbB = new CouchDB("test_suite_rep_db_b", {"X-Couch-Full-Commit":"false"}); +replicator_db.repDb = new CouchDB("test_suite_rep_db", {"X-Couch-Full-Commit":"false"}); +replicator_db.usersDb = new CouchDB("test_suite_auth", {"X-Couch-Full-Commit":"false"}); + +replicator_db.docs1 = [ + { + _id: "foo1", + value: 11 + }, + { + _id: "foo2", + value: 22 + }, + { + _id: "foo3", + value: 33 + } +]; + +replicator_db.waitForRep = function waitForSeq(repDb, repDoc, state) { + var newRep, + t0 = new Date(), + t1, + ms = 3000; + + do { + newRep = repDb.open(repDoc._id); + t1 = new Date(); + } while (((t1 - t0) <= ms) && newRep._replication_state !== state); +} + +replicator_db.waitForSeq = function waitForSeq(sourceDb, targetDb) { + var targetSeq, + sourceSeq = sourceDb.info().update_seq, + t0 = new Date(), + t1, + ms = 3000; + + do { + targetSeq = targetDb.info().update_seq; + t1 = new Date(); + } while (((t1 - t0) <= ms) && targetSeq < sourceSeq); +} + +replicator_db.waitForDocPos = function waitForDocPos(db, docId, pos) { + var doc, curPos, t0, t1, + maxWait = 3000; + + doc = db.open(docId); + curPos = Number(doc._rev.split("-", 1)); + t0 = t1 = new Date(); + + while ((curPos < pos) && ((t1 - t0) <= maxWait)) { + doc = db.open(docId); + curPos = Number(doc._rev.split("-", 1)); + t1 = new Date(); + } + + return doc; +} + +replicator_db.wait = function wait(ms) { + var t0 = new Date(), t1; + do { + CouchDB.request("GET", "/"); + t1 = new Date(); + } while ((t1 - t0) <= ms); +} + + +replicator_db.populate_db = function populate_db(db, docs) { + if (db.name !== replicator_db.usersDb.name) { + db.deleteDb(); + db.createDb(); + } + for (var i = 0; i < docs.length; i++) { + var d = docs[i]; + delete d._rev; + T(db.save(d).ok); + } +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/couchdb/blob/013ee66e/test/javascript/run ---------------------------------------------------------------------- diff --git a/test/javascript/run b/test/javascript/run index 883fd37..3ba83b7 100755 --- a/test/javascript/run +++ b/test/javascript/run @@ -33,12 +33,12 @@ N = 3 COUCHJS = "src/couch/priv/couchjs" SCRIPTS = """ - share/www/script/json2.js - share/www/script/sha1.js - share/www/script/oauth.js - share/www/script/couch.js - share/www/script/replicator_db_inc.js - share/www/script/couch_test_runner.js + test/javascript/json2.js + test/javascript/sha1.js + test/javascript/oauth.js + test/javascript/couch.js + test/javascript/replicator_db_inc.js + test/javascript/couch_test_runner.js test/javascript/couch_http.js test/javascript/test_setup.js """.split() @@ -107,14 +107,14 @@ def main(): tests = [] if not len(args): - args = ["share/www/script/test"] + args = ["test/javascript/tests"] for name in args: if os.path.isdir(name): tests.extend(glob.glob(os.path.join(name, "*.js"))) elif os.path.isfile(name): tests.append(name) else: - pname = os.path.join("share/www/script/test", name) + pname = os.path.join("test/javascript/tests", name) if os.path.isfile(pname): tests.append(pname) elif os.path.isfile(pname + ".js"): http://git-wip-us.apache.org/repos/asf/couchdb/blob/013ee66e/test/javascript/sha1.js ---------------------------------------------------------------------- diff --git a/test/javascript/sha1.js b/test/javascript/sha1.js new file mode 100644 index 0000000..ee73a63 --- /dev/null +++ b/test/javascript/sha1.js @@ -0,0 +1,202 @@ +/* + * A JavaScript implementation of the Secure Hash Algorithm, SHA-1, as defined + * in FIPS PUB 180-1 + * Version 2.1a Copyright Paul Johnston 2000 - 2002. + * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet + * Distributed under the BSD License + * See http://pajhome.org.uk/crypt/md5 for details. + */ + +/* + * Configurable variables. You may need to tweak these to be compatible with + * the server-side, but the defaults work in most cases. + */ +var hexcase = 0; /* hex output format. 0 - lowercase; 1 - uppercase */ +var b64pad = "="; /* base-64 pad character. "=" for strict RFC compliance */ +var chrsz = 8; /* bits per input character. 8 - ASCII; 16 - Unicode */ + +/* + * These are the functions you'll usually want to call + * They take string arguments and return either hex or base-64 encoded strings + */ +function hex_sha1(s){return binb2hex(core_sha1(str2binb(s),s.length * chrsz));} +function b64_sha1(s){return binb2b64(core_sha1(str2binb(s),s.length * chrsz));} +function str_sha1(s){return binb2str(core_sha1(str2binb(s),s.length * chrsz));} +function hex_hmac_sha1(key, data){ return binb2hex(core_hmac_sha1(key, data));} +function b64_hmac_sha1(key, data){ return binb2b64(core_hmac_sha1(key, data));} +function str_hmac_sha1(key, data){ return binb2str(core_hmac_sha1(key, data));} + +/* + * Perform a simple self-test to see if the VM is working + */ +function sha1_vm_test() +{ + return hex_sha1("abc") == "a9993e364706816aba3e25717850c26c9cd0d89d"; +} + +/* + * Calculate the SHA-1 of an array of big-endian words, and a bit length + */ +function core_sha1(x, len) +{ + /* append padding */ + x[len >> 5] |= 0x80 << (24 - len % 32); + x[((len + 64 >> 9) << 4) + 15] = len; + + var w = Array(80); + var a = 1732584193; + var b = -271733879; + var c = -1732584194; + var d = 271733878; + var e = -1009589776; + + for(var i = 0; i < x.length; i += 16) + { + var olda = a; + var oldb = b; + var oldc = c; + var oldd = d; + var olde = e; + + for(var j = 0; j < 80; j++) + { + if(j < 16) w[j] = x[i + j]; + else w[j] = rol(w[j-3] ^ w[j-8] ^ w[j-14] ^ w[j-16], 1); + var t = safe_add(safe_add(rol(a, 5), sha1_ft(j, b, c, d)), + safe_add(safe_add(e, w[j]), sha1_kt(j))); + e = d; + d = c; + c = rol(b, 30); + b = a; + a = t; + } + + a = safe_add(a, olda); + b = safe_add(b, oldb); + c = safe_add(c, oldc); + d = safe_add(d, oldd); + e = safe_add(e, olde); + } + return Array(a, b, c, d, e); + +} + +/* + * Perform the appropriate triplet combination function for the current + * iteration + */ +function sha1_ft(t, b, c, d) +{ + if(t < 20) return (b & c) | ((~b) & d); + if(t < 40) return b ^ c ^ d; + if(t < 60) return (b & c) | (b & d) | (c & d); + return b ^ c ^ d; +} + +/* + * Determine the appropriate additive constant for the current iteration + */ +function sha1_kt(t) +{ + return (t < 20) ? 1518500249 : (t < 40) ? 1859775393 : + (t < 60) ? -1894007588 : -899497514; +} + +/* + * Calculate the HMAC-SHA1 of a key and some data + */ +function core_hmac_sha1(key, data) +{ + var bkey = str2binb(key); + if(bkey.length > 16) bkey = core_sha1(bkey, key.length * chrsz); + + var ipad = Array(16), opad = Array(16); + for(var i = 0; i < 16; i++) + { + ipad[i] = bkey[i] ^ 0x36363636; + opad[i] = bkey[i] ^ 0x5C5C5C5C; + } + + var hash = core_sha1(ipad.concat(str2binb(data)), 512 + data.length * chrsz); + return core_sha1(opad.concat(hash), 512 + 160); +} + +/* + * Add integers, wrapping at 2^32. This uses 16-bit operations internally + * to work around bugs in some JS interpreters. + */ +function safe_add(x, y) +{ + var lsw = (x & 0xFFFF) + (y & 0xFFFF); + var msw = (x >> 16) + (y >> 16) + (lsw >> 16); + return (msw << 16) | (lsw & 0xFFFF); +} + +/* + * Bitwise rotate a 32-bit number to the left. + */ +function rol(num, cnt) +{ + return (num << cnt) | (num >>> (32 - cnt)); +} + +/* + * Convert an 8-bit or 16-bit string to an array of big-endian words + * In 8-bit function, characters >255 have their hi-byte silently ignored. + */ +function str2binb(str) +{ + var bin = Array(); + var mask = (1 << chrsz) - 1; + for(var i = 0; i < str.length * chrsz; i += chrsz) + bin[i>>5] |= (str.charCodeAt(i / chrsz) & mask) << (32 - chrsz - i%32); + return bin; +} + +/* + * Convert an array of big-endian words to a string + */ +function binb2str(bin) +{ + var str = ""; + var mask = (1 << chrsz) - 1; + for(var i = 0; i < bin.length * 32; i += chrsz) + str += String.fromCharCode((bin[i>>5] >>> (32 - chrsz - i%32)) & mask); + return str; +} + +/* + * Convert an array of big-endian words to a hex string. + */ +function binb2hex(binarray) +{ + var hex_tab = hexcase ? "0123456789ABCDEF" : "0123456789abcdef"; + var str = ""; + for(var i = 0; i < binarray.length * 4; i++) + { + str += hex_tab.charAt((binarray[i>>2] >> ((3 - i%4)*8+4)) & 0xF) + + hex_tab.charAt((binarray[i>>2] >> ((3 - i%4)*8 )) & 0xF); + } + return str; +} + +/* + * Convert an array of big-endian words to a base-64 string + */ +function binb2b64(binarray) +{ + var tab = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + var str = ""; + for(var i = 0; i < binarray.length * 4; i += 3) + { + var triplet = (((binarray[i >> 2] >> 8 * (3 - i %4)) & 0xFF) << 16) + | (((binarray[i+1 >> 2] >> 8 * (3 - (i+1)%4)) & 0xFF) << 8 ) + | ((binarray[i+2 >> 2] >> 8 * (3 - (i+2)%4)) & 0xFF); + for(var j = 0; j < 4; j++) + { + if(i * 8 + j * 6 > binarray.length * 32) str += b64pad; + else str += tab.charAt((triplet >> 6*(3-j)) & 0x3F); + } + } + return str; +} http://git-wip-us.apache.org/repos/asf/couchdb/blob/013ee66e/test/javascript/tests/all_docs.js ---------------------------------------------------------------------- diff --git a/test/javascript/tests/all_docs.js b/test/javascript/tests/all_docs.js new file mode 100644 index 0000000..66ad880 --- /dev/null +++ b/test/javascript/tests/all_docs.js @@ -0,0 +1,142 @@ +// 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.all_docs = function(debug) { + var db = new CouchDB("test_suite_db", {"X-Couch-Full-Commit":"false"}); + db.deleteDb(); + db.createDb(); + if (debug) debugger; + + // Create some more documents. + // Notice the use of the ok member on the return result. + T(db.save({_id:"0",a:1,b:1}).ok); + T(db.save({_id:"3",a:4,b:16}).ok); + T(db.save({_id:"1",a:2,b:4}).ok); + T(db.save({_id:"2",a:3,b:9}).ok); + + // Check the all docs + var results = db.allDocs(); + var rows = results.rows; + + T(results.total_rows == results.rows.length); + + for(var i=0; i < rows.length; i++) { + T(rows[i].id >= "0" && rows[i].id <= "4"); + } + + // Check _all_docs with descending=true + var desc = db.allDocs({descending:true}); + T(desc.total_rows == desc.rows.length); + + // Check _all_docs offset + var all = db.allDocs({startkey:"2"}); + T(all.offset == 2); + + // Confirm that queries may assume raw collation. + var raw = db.allDocs({ startkey: "org.couchdb.user:", + endkey : "org.couchdb.user;" + }); + TEquals(0, raw.rows.length); + + // check that the docs show up in the seq view in the order they were created + var changes = db.changes(); + var ids = ["0","3","1","2"]; + for (var i=0; i < changes.results.length; i++) { + var row = changes.results[i]; + T(row.id == ids[i], "seq order"); + }; + + // it should work in reverse as well + changes = db.changes({descending:true}); + ids = ["2","1","3","0"]; + for (var i=0; i < changes.results.length; i++) { + var row = changes.results[i]; + T(row.id == ids[i], "descending=true"); + }; + + // check that deletions also show up right + var doc1 = db.open("1"); + var deleted = db.deleteDoc(doc1); + T(deleted.ok); + changes = db.changes(); + // the deletion should make doc id 1 have the last seq num + T(changes.results.length == 4); + T(changes.results[3].id == "1"); + T(changes.results[3].deleted); + + // do an update + var doc2 = db.open("3"); + doc2.updated = "totally"; + db.save(doc2); + changes = db.changes(); + + // the update should make doc id 3 have the last seq num + T(changes.results.length == 4); + T(changes.results[3].id == "3"); + + // ok now lets see what happens with include docs + changes = db.changes({include_docs: true}); + T(changes.results.length == 4); + T(changes.results[3].id == "3"); + T(changes.results[3].doc.updated == "totally"); + + T(changes.results[2].doc); + T(changes.results[2].doc._deleted); + + rows = db.allDocs({include_docs: true}, ["1"]).rows; + TEquals(1, rows.length); + TEquals("1", rows[0].key); + TEquals("1", rows[0].id); + TEquals(true, rows[0].value.deleted); + TEquals(null, rows[0].doc); + + // add conflicts + var conflictDoc1 = { + _id: "3", _rev: "2-aa01552213fafa022e6167113ed01087", value: "X" + }; + var conflictDoc2 = { + _id: "3", _rev: "2-ff01552213fafa022e6167113ed01087", value: "Z" + }; + T(db.save(conflictDoc1, {new_edits: false})); + T(db.save(conflictDoc2, {new_edits: false})); + + var winRev = db.open("3"); + + changes = db.changes({include_docs: true, conflicts: true, style: "all_docs"}); + TEquals("3", changes.results[3].id); + TEquals(3, changes.results[3].changes.length); + TEquals(winRev._rev, changes.results[3].changes[0].rev); + TEquals("3", changes.results[3].doc._id); + TEquals(winRev._rev, changes.results[3].doc._rev); + TEquals(true, changes.results[3].doc._conflicts instanceof Array); + TEquals(2, changes.results[3].doc._conflicts.length); + + rows = db.allDocs({include_docs: true, conflicts: true}).rows; + TEquals(3, rows.length); + TEquals("3", rows[2].key); + TEquals("3", rows[2].id); + TEquals(winRev._rev, rows[2].value.rev); + TEquals(winRev._rev, rows[2].doc._rev); + TEquals("3", rows[2].doc._id); + TEquals(true, rows[2].doc._conflicts instanceof Array); + TEquals(2, rows[2].doc._conflicts.length); + + // test the all docs collates sanely + db.save({_id: "Z", foo: "Z"}); + db.save({_id: "a", foo: "a"}); + + var rows = db.allDocs({startkey: "Z", endkey: "Z"}).rows; + T(rows.length == 1); + + // cleanup + db.deleteDb(); +}; http://git-wip-us.apache.org/repos/asf/couchdb/blob/013ee66e/test/javascript/tests/attachment_names.js ---------------------------------------------------------------------- diff --git a/test/javascript/tests/attachment_names.js b/test/javascript/tests/attachment_names.js new file mode 100644 index 0000000..c9a5fcc --- /dev/null +++ b/test/javascript/tests/attachment_names.js @@ -0,0 +1,96 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. + +couchTests.attachment_names = function(debug) { + var db = new CouchDB("test_suite_db", {"X-Couch-Full-Commit":"false"}); + db.deleteDb(); + db.createDb(); + if (debug) debugger; + + var goodDoc = { + _id: "good_doc", + _attachments: { + "ÐолÑн.txt": { + content_type:"application/octet-stream", + data: "VGhpcyBpcyBhIGJhc2U2NCBlbmNvZGVkIHRleHQ=" + } + } + }; + + var save_response = db.save(goodDoc); + T(save_response.ok); + + var xhr = CouchDB.request("GET", "/test_suite_db/good_doc/ÐолÑн.txt"); + T(xhr.responseText == "This is a base64 encoded text"); + T(xhr.getResponseHeader("Content-Type") == "application/octet-stream"); + TEquals("\"aEI7pOYCRBLTRQvvqYrrJQ==\"", xhr.getResponseHeader("Etag")); + + var binAttDoc = { + _id: "bin_doc", + _attachments:{ + "foo\x80txt": { + content_type:"text/plain", + data: "VGhpcyBpcyBhIGJhc2U2NCBlbmNvZGVkIHRleHQ=" + } + } + }; + + // inline attachments + resp = db.save(binAttDoc); + TEquals(true, resp.ok, "attachment_name: inline attachment"); + + + // standalone docs + var bin_data = "JHAPDO*AU£PN ){(3u[d 93DQ9¡â¬])} ææøo'âÆÃ¦â¤Ã§Ã¦ÏÏâ¢Â¥â«Â¶Â®#â ϶®¥Ïâ¬ÂªÂ®ËÏ8np"; + + + var xhr = (CouchDB.request("PUT", "/test_suite_db/bin_doc3/attachment\x80txt", { + headers:{"Content-Type":"text/plain;charset=utf-8"}, + body:bin_data + })); + + var resp = JSON.parse(xhr.responseText); + TEquals(201, xhr.status, "attachment_name: standalone API"); + TEquals(true, resp.ok, "attachment_name: standalone API"); + + // bulk docs + var docs = { docs: [binAttDoc] }; + + var xhr = CouchDB.request("POST", "/test_suite_db/_bulk_docs", { + body: JSON.stringify(docs) + }); + + TEquals(201, xhr.status, "attachment_name: bulk docs"); + + + // leading underscores + var binAttDoc = { + _id: "bin_doc2", + _attachments:{ + "_foo.txt": { + content_type:"text/plain", + data: "VGhpcyBpcyBhIGJhc2U2NCBlbmNvZGVkIHRleHQ=" + } + } + }; + + try { + db.save(binAttDoc); + TEquals(1, 2, "Attachment name with leading underscore saved. Should never show!"); + } catch (e) { + TEquals("bad_request", e.error, "attachment_name: leading underscore"); + TEquals("Attachment name can't start with '_'", e.reason, "attachment_name: leading underscore"); + } + + // todo: form uploads, waiting for cmlenz' test case for form uploads + +}; http://git-wip-us.apache.org/repos/asf/couchdb/blob/013ee66e/test/javascript/tests/attachment_paths.js ---------------------------------------------------------------------- diff --git a/test/javascript/tests/attachment_paths.js b/test/javascript/tests/attachment_paths.js new file mode 100644 index 0000000..3f6ffb7 --- /dev/null +++ b/test/javascript/tests/attachment_paths.js @@ -0,0 +1,153 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. + +couchTests.attachment_paths = 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(); + + // first just save a regular doc with an attachment that has a slash in the url. + // (also gonna run an encoding check case) + var binAttDoc = { + _id: "bin_doc", + _attachments:{ + "foo/bar.txt": { + content_type:"text/plain", + data: "VGhpcyBpcyBhIGJhc2U2NCBlbmNvZGVkIHRleHQ=" + }, + "foo%2Fbaz.txt": { + content_type:"text/plain", + data: "V2UgbGlrZSBwZXJjZW50IHR3byBGLg==" + } + } + }; + + T(db.save(binAttDoc).ok); + + var xhr = CouchDB.request("GET", "/"+dbName+"/bin_doc/foo/bar.txt"); + T(xhr.responseText == "This is a base64 encoded text"); + T(xhr.getResponseHeader("Content-Type") == "text/plain"); + + // lets try it with an escaped attachment id... + // weird that it's at two urls + var xhr = CouchDB.request("GET", "/"+dbName+"/bin_doc/foo%2Fbar.txt"); + T(xhr.status == 200); + // xhr.responseText == "This is a base64 encoded text" + + var xhr = CouchDB.request("GET", "/"+dbName+"/bin_doc/foo/baz.txt"); + T(xhr.status == 404); + + var xhr = CouchDB.request("GET", "/"+dbName+"/bin_doc/foo%252Fbaz.txt"); + T(xhr.status == 200); + T(xhr.responseText == "We like percent two F."); + + // require a _rev to PUT + var xhr = CouchDB.request("PUT", "/"+dbName+"/bin_doc/foo/attachment.txt", { + headers:{"Content-Type":"text/plain;charset=utf-8"}, + body:"Just some text" + }); + T(xhr.status == 409); + + var xhr = CouchDB.request("PUT", "/"+dbName+"/bin_doc/foo/bar2.txt?rev=" + binAttDoc._rev, { + body:"This is no base64 encoded text", + headers:{"Content-Type": "text/plain;charset=utf-8"} + }); + T(xhr.status == 201); + var rev = JSON.parse(xhr.responseText).rev; + + binAttDoc = db.open("bin_doc"); + + T(binAttDoc._attachments["foo/bar.txt"] !== undefined); + T(binAttDoc._attachments["foo%2Fbaz.txt"] !== undefined); + T(binAttDoc._attachments["foo/bar2.txt"] !== undefined); + TEquals("text/plain;charset=utf-8", // thank you Safari + binAttDoc._attachments["foo/bar2.txt"].content_type.toLowerCase(), + "correct content-type" + ); + T(binAttDoc._attachments["foo/bar2.txt"].length == 30); + + //// now repeat the while thing with a design doc + + // first just save a regular doc with an attachment that has a slash in the url. + // (also gonna run an encoding check case) + var binAttDoc = { + _id: "_design/bin_doc", + _attachments:{ + "foo/bar.txt": { + content_type:"text/plain", + data: "VGhpcyBpcyBhIGJhc2U2NCBlbmNvZGVkIHRleHQ=" + }, + "foo%2Fbaz.txt": { + content_type:"text/plain", + data: "V2UgbGlrZSBwZXJjZW50IHR3byBGLg==" + } + } + }; + + T(db.save(binAttDoc).ok); + + var xhr = CouchDB.request("GET", "/"+dbName+"/_design%2Fbin_doc/foo/bar.txt"); + T(xhr.responseText == "This is a base64 encoded text"); + T(xhr.getResponseHeader("Content-Type") == "text/plain"); + + // lets try it with an escaped attachment id... + // weird that it's at two urls + var xhr = CouchDB.request("GET", "/"+dbName+"/_design%2Fbin_doc/foo%2Fbar.txt"); + T(xhr.responseText == "This is a base64 encoded text"); + T(xhr.status == 200); + + // err, 3 urls + var xhr = CouchDB.request("GET", "/"+dbName+"/_design/bin_doc/foo%2Fbar.txt"); + T(xhr.responseText == "This is a base64 encoded text"); + T(xhr.status == 200); + + // I mean um, 4 urls + var xhr = CouchDB.request("GET", "/"+dbName+"/_design/bin_doc/foo/bar.txt"); + T(xhr.responseText == "This is a base64 encoded text"); + T(xhr.status == 200); + + var xhr = CouchDB.request("GET", "/"+dbName+"/_design%2Fbin_doc/foo/baz.txt"); + T(xhr.status == 404); + + var xhr = CouchDB.request("GET", "/"+dbName+"/_design%2Fbin_doc/foo%252Fbaz.txt"); + T(xhr.status == 200); + T(xhr.responseText == "We like percent two F."); + + // require a _rev to PUT + var xhr = CouchDB.request("PUT", "/"+dbName+"/_design%2Fbin_doc/foo/attachment.txt", { + headers:{"Content-Type":"text/plain;charset=utf-8"}, + body:"Just some text" + }); + T(xhr.status == 409); + + var xhr = CouchDB.request("PUT", "/"+dbName+"/_design%2Fbin_doc/foo/bar2.txt?rev=" + binAttDoc._rev, { + body:"This is no base64 encoded text", + headers:{"Content-Type": "text/plain;charset=utf-8"} + }); + T(xhr.status == 201); + var rev = JSON.parse(xhr.responseText).rev; + + binAttDoc = db.open("_design/bin_doc"); + + T(binAttDoc._attachments["foo/bar.txt"] !== undefined); + T(binAttDoc._attachments["foo/bar2.txt"] !== undefined); + TEquals("text/plain;charset=utf-8", // thank you Safari + binAttDoc._attachments["foo/bar2.txt"].content_type.toLowerCase(), + "correct content-type" + ); + T(binAttDoc._attachments["foo/bar2.txt"].length == 30); + } +}; http://git-wip-us.apache.org/repos/asf/couchdb/blob/013ee66e/test/javascript/tests/attachment_ranges.js ---------------------------------------------------------------------- diff --git a/test/javascript/tests/attachment_ranges.js b/test/javascript/tests/attachment_ranges.js new file mode 100644 index 0000000..7d9afb5 --- /dev/null +++ b/test/javascript/tests/attachment_ranges.js @@ -0,0 +1,160 @@ +// 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 cacheBust() { + return "?anti-cache=" + String(Math.round(Math.random() * 1000000)); +}; + +couchTests.attachment_ranges = function(debug) { + var db = new CouchDB("test_suite_db", { + "X-Couch-Full-Commit": "false" + }); + db.deleteDb(); + db.createDb(); + + if (debug) debugger; + + if((typeof window != "undefined") && window.navigator.userAgent.match(/Chrome/)) { + // Chrome is broken. + return; + } + + var binAttDoc = { + _id: "bin_doc", + _attachments: { + "foo.txt": { + content_type: "application/octet-stream", + data: "VGhpcyBpcyBhIGJhc2U2NCBlbmNvZGVkIHRleHQ=" + } + } + }; + + var save_response = db.save(binAttDoc); + T(save_response.ok); + + // Fetching the whole entity is a 206. + var xhr = CouchDB.request("GET", "/test_suite_db/bin_doc/foo.txt" + cacheBust(), { + headers: { + "Range": "bytes=0-28" + } + }); + TEquals(206, xhr.status, "fetch 0-28"); + TEquals("This is a base64 encoded text", xhr.responseText); + TEquals("bytes 0-28/29", xhr.getResponseHeader("Content-Range")); + TEquals("29", xhr.getResponseHeader("Content-Length")); + + // Fetch the whole entity without an end offset is a 200. + var xhr = CouchDB.request("GET", "/test_suite_db/bin_doc/foo.txt" + cacheBust(), { + headers: { + "Range": "bytes=0-" + } + }); + TEquals(200, xhr.status, "fetch 0-"); + TEquals("This is a base64 encoded text", xhr.responseText); + TEquals(null, xhr.getResponseHeader("Content-Range")); + TEquals("29", xhr.getResponseHeader("Content-Length")); + + // Even if you ask multiple times. + var xhr = CouchDB.request("GET", "/test_suite_db/bin_doc/foo.txt" + cacheBust(), { + headers: { + "Range": "bytes=0-,0-,0-" + } + }); + TEquals(200, xhr.status, "multiple 0-'s"); + + // Badly formed range header is a 200. + var xhr = CouchDB.request("GET", "/test_suite_db/bin_doc/foo.txt" + cacheBust(), { + headers: { + "Range": "bytes:0-" + } + }); + TEquals(200, xhr.status, "fetch with bad range header"); + + // Fetch the end of an entity without an end offset is a 206. + var xhr = CouchDB.request("GET", "/test_suite_db/bin_doc/foo.txt" + cacheBust(), { + headers: { + "Range": "bytes=2-" + } + }); + TEquals(206, xhr.status, "fetch 2-"); + TEquals("is is a base64 encoded text", xhr.responseText); + TEquals("bytes 2-28/29", xhr.getResponseHeader("Content-Range")); + TEquals("27", xhr.getResponseHeader("Content-Length")); + + // Fetch past the end of the entity is a 206 + var xhr = CouchDB.request("GET", "/test_suite_db/bin_doc/foo.txt" + cacheBust(), { + headers: { + "Range": "bytes=0-29" + } + }); + TEquals(206, xhr.status, "fetch 0-29"); + TEquals("bytes 0-28/29", xhr.getResponseHeader("Content-Range")); + TEquals("29", xhr.getResponseHeader("Content-Length")); + + // Fetch first part of entity is a 206 + var xhr = CouchDB.request("GET", "/test_suite_db/bin_doc/foo.txt" + cacheBust(), { + headers: { + "Range": "bytes=0-3" + } + }); + TEquals(206, xhr.status, "fetch 0-3"); + TEquals("This", xhr.responseText); + TEquals("4", xhr.getResponseHeader("Content-Length")); + TEquals("bytes 0-3/29", xhr.getResponseHeader("Content-Range")); + + // Fetch middle of entity is also a 206 + var xhr = CouchDB.request("GET", "/test_suite_db/bin_doc/foo.txt" + cacheBust(), { + headers: { + "Range": "bytes=10-15" + } + }); + TEquals(206, xhr.status, "fetch 10-15"); + TEquals("base64", xhr.responseText); + TEquals("6", xhr.getResponseHeader("Content-Length")); + TEquals("bytes 10-15/29", xhr.getResponseHeader("Content-Range")); + + // Fetch end of entity is also a 206 + var xhr = CouchDB.request("GET", "/test_suite_db/bin_doc/foo.txt" + cacheBust(), { + headers: { + "Range": "bytes=-3" + } + }); + TEquals(206, xhr.status, "fetch -3"); + TEquals("ext", xhr.responseText); + TEquals("3", xhr.getResponseHeader("Content-Length")); + TEquals("bytes 26-28/29", xhr.getResponseHeader("Content-Range")); + + // backward range is 416 + var xhr = CouchDB.request("GET", "/test_suite_db/bin_doc/foo.txt" + cacheBust(), { + headers: { + "Range": "bytes=5-3" + } + }); + TEquals(416, xhr.status, "fetch 5-3"); + + // range completely outside of entity is 416 + var xhr = CouchDB.request("GET", "/test_suite_db/bin_doc/foo.txt" + cacheBust(), { + headers: { + "Range": "bytes=300-310" + } + }); + TEquals(416, xhr.status, "fetch 300-310"); + + // We ignore a Range header with too many ranges + var xhr = CouchDB.request("GET", "/test_suite_db/bin_doc/foo.txt" + cacheBust(), { + headers: { + "Range": "bytes=0-1,0-1,0-1,0-1,0-1,0-1,0-1,0-1,0-1,0-1" + } + }); + TEquals(200, xhr.status, "too many ranges"); + +}; http://git-wip-us.apache.org/repos/asf/couchdb/blob/013ee66e/test/javascript/tests/attachment_views.js ---------------------------------------------------------------------- diff --git a/test/javascript/tests/attachment_views.js b/test/javascript/tests/attachment_views.js new file mode 100644 index 0000000..b55aabe --- /dev/null +++ b/test/javascript/tests/attachment_views.js @@ -0,0 +1,140 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. + +couchTests.attachment_views= function(debug) { + + var db = new CouchDB("test_suite_db", {"X-Couch-Full-Commit":"false"}); + db.deleteDb(); + db.createDb(); + if (debug) debugger; + + // count attachments in a view + + var attachmentData = "VGhpcyBpcyBhIGJhc2U2NCBlbmNvZGVkIHRleHQ="; + + db.bulkSave(makeDocs(0, 10)); + + db.bulkSave(makeDocs(10, 20, { + _attachments:{ + "foo.txt": { + content_type:"text/plain", + data: attachmentData + } + } + })); + + db.bulkSave(makeDocs(20, 30, { + _attachments:{ + "foo.txt": { + content_type:"text/plain", + data: attachmentData + }, + "bar.txt": { + content_type:"text/plain", + data: attachmentData + } + } + })); + + db.bulkSave(makeDocs(30, 40, { + _attachments:{ + "foo.txt": { + content_type:"text/plain", + data: attachmentData + }, + "bar.txt": { + content_type:"text/plain", + data: attachmentData + }, + "baz.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 reduceFunction = function(key, values) { + return sum(values); + }; + + var result = db.query(mapFunction, reduceFunction); + + T(result.rows.length == 1); + T(result.rows[0].value == 60); + + var result = db.query(mapFunction, reduceFunction, { + startkey:10, + endkey:19 + }); + + T(result.rows.length == 1); + T(result.rows[0].value == 10); + + var result = db.query(mapFunction, reduceFunction, { + startkey:20, + endkey:29 + }); + + T(result.rows.length == 1); + T(result.rows[0].value == 20); + + var result = db.query(mapFunction, null, { + startkey: 30, + endkey: 39, + include_docs: true + }); + + T(result.rows.length == 10); + T(result.rows[0].value == 3); + T(result.rows[0].doc._attachments['baz.txt'].stub === true); + T(result.rows[0].doc._attachments['baz.txt'].data === undefined); + T(result.rows[0].doc._attachments['baz.txt'].encoding === undefined); + T(result.rows[0].doc._attachments['baz.txt'].encoded_length === undefined); + + var result = db.query(mapFunction, null, { + startkey: 30, + endkey: 39, + include_docs: true, + attachments: true + }); + + T(result.rows.length == 10); + T(result.rows[0].value == 3); + T(result.rows[0].doc._attachments['baz.txt'].data === attachmentData); + T(result.rows[0].doc._attachments['baz.txt'].stub === undefined); + T(result.rows[0].doc._attachments['baz.txt'].encoding === undefined); + T(result.rows[0].doc._attachments['baz.txt'].encoded_length === undefined); + + var result = db.query(mapFunction, null, { + startkey: 30, + endkey: 39, + include_docs: true, + att_encoding_info: true + }); + + T(result.rows.length == 10); + T(result.rows[0].value == 3); + T(result.rows[0].doc._attachments['baz.txt'].data === undefined); + T(result.rows[0].doc._attachments['baz.txt'].stub === true); + T(result.rows[0].doc._attachments['baz.txt'].encoding === "gzip"); + T(result.rows[0].doc._attachments['baz.txt'].encoded_length === 47); +}; http://git-wip-us.apache.org/repos/asf/couchdb/blob/013ee66e/test/javascript/tests/attachments.js ---------------------------------------------------------------------- diff --git a/test/javascript/tests/attachments.js b/test/javascript/tests/attachments.js new file mode 100644 index 0000000..2fa08ee --- /dev/null +++ b/test/javascript/tests/attachments.js @@ -0,0 +1,328 @@ +// 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.attachments= function(debug) { + var db = new CouchDB("test_suite_db", {"X-Couch-Full-Commit":"false"}); + db.deleteDb(); + db.createDb(); + if (debug) debugger; + + + // MD5 Digests of compressible attachments and therefore Etags + // will vary depending on platform gzip implementation. + // These MIME types are defined in [attachments] compressible_types + var binAttDoc = { + _id: "bin_doc", + _attachments:{ + "foo.txt": { + content_type:"application/octet-stream", + data: "VGhpcyBpcyBhIGJhc2U2NCBlbmNvZGVkIHRleHQ=" + } + } + }; + + var save_response = db.save(binAttDoc); + T(save_response.ok); + + var xhr = CouchDB.request("GET", "/test_suite_db/bin_doc/foo.txt"); + T(xhr.responseText == "This is a base64 encoded text"); + T(xhr.getResponseHeader("Content-Type") == "application/octet-stream"); + TEquals("\"aEI7pOYCRBLTRQvvqYrrJQ==\"", xhr.getResponseHeader("Etag")); + + // empty attachment + var binAttDoc2 = { + _id: "bin_doc2", + _attachments:{ + "foo.txt": { + content_type:"text/plain", + data: "" + } + } + } + + T(db.save(binAttDoc2).ok); + + var xhr = CouchDB.request("GET", "/test_suite_db/bin_doc2/foo.txt"); + T(xhr.responseText.length == 0); + T(xhr.getResponseHeader("Content-Type") == "text/plain"); + + // test RESTful doc API + + var xhr = CouchDB.request("PUT", "/test_suite_db/bin_doc2/foo2.txt?rev=" + binAttDoc2._rev, { + body:"This is no base64 encoded text", + headers:{"Content-Type": "text/plain;charset=utf-8"} + }); + T(xhr.status == 201); + TEquals("/bin_doc2/foo2.txt", + xhr.getResponseHeader("Location").substr(-18), + "should return Location header to newly created or updated attachment"); + + var rev = JSON.parse(xhr.responseText).rev; + + binAttDoc2 = db.open("bin_doc2"); + + T(binAttDoc2._attachments["foo.txt"] !== undefined); + T(binAttDoc2._attachments["foo2.txt"] !== undefined); + TEqualsIgnoreCase("text/plain;charset=utf-8", binAttDoc2._attachments["foo2.txt"].content_type); + T(binAttDoc2._attachments["foo2.txt"].length == 30); + + var xhr = CouchDB.request("GET", "/test_suite_db/bin_doc2/foo2.txt"); + T(xhr.responseText == "This is no base64 encoded text"); + TEqualsIgnoreCase("text/plain;charset=utf-8", xhr.getResponseHeader("Content-Type")); + + // test without rev, should fail + var xhr = CouchDB.request("DELETE", "/test_suite_db/bin_doc2/foo2.txt"); + T(xhr.status == 409); + + // test with rev, should not fail + var xhr = CouchDB.request("DELETE", "/test_suite_db/bin_doc2/foo2.txt?rev=" + rev); + T(xhr.status == 200); + TEquals(null, xhr.getResponseHeader("Location"), + "should not return Location header on DELETE request"); + + // test binary data + var bin_data = "JHAPDO*AU£PN ){(3u[d 93DQ9¡â¬])} ææøo'âÆÃ¦â¤Ã§Ã¦ÏÏâ¢Â¥â«Â¶Â®#â ϶®¥Ïâ¬ÂªÂ®ËÏ8np"; + var xhr = CouchDB.request("PUT", "/test_suite_db/bin_doc3/attachment.txt", { + headers:{"Content-Type":"text/plain;charset=utf-8"}, + body:bin_data + }); + T(xhr.status == 201); + var rev = JSON.parse(xhr.responseText).rev; + TEquals('"' + rev + '"', xhr.getResponseHeader("Etag")); + + var xhr = CouchDB.request("GET", "/test_suite_db/bin_doc3/attachment.txt"); + T(xhr.responseText == bin_data); + TEqualsIgnoreCase("text/plain;charset=utf-8", xhr.getResponseHeader("Content-Type")); + + // without rev + var xhr = CouchDB.request("PUT", "/test_suite_db/bin_doc3/attachment.txt", { + headers:{"Content-Type":"text/plain;charset=utf-8"}, + body:bin_data + }); + T(xhr.status == 409); + + // with nonexistent rev + var xhr = CouchDB.request("PUT", "/test_suite_db/bin_doc3/attachment.txt" + "?rev=1-adae8575ecea588919bd08eb020c708e", { + headers:{"Content-Type":"text/plain;charset=utf-8"}, + body:bin_data + }); + T(xhr.status == 409); + + // with current rev + var xhr = CouchDB.request("PUT", "/test_suite_db/bin_doc3/attachment.txt?rev=" + rev, { + headers:{"Content-Type":"text/plain;charset=utf-8"}, + body:bin_data + }); + T(xhr.status == 201); + var rev = JSON.parse(xhr.responseText).rev; + TEquals('"' + rev + '"', xhr.getResponseHeader("Etag")); + + var xhr = CouchDB.request("GET", "/test_suite_db/bin_doc3/attachment.txt"); + T(xhr.responseText == bin_data); + TEqualsIgnoreCase("text/plain;charset=utf-8", xhr.getResponseHeader("Content-Type")); + + var xhr = CouchDB.request("GET", "/test_suite_db/bin_doc3/attachment.txt?rev=" + rev); + T(xhr.responseText == bin_data); + TEqualsIgnoreCase("text/plain;charset=utf-8", xhr.getResponseHeader("Content-Type")); + + var xhr = CouchDB.request("DELETE", "/test_suite_db/bin_doc3/attachment.txt?rev=" + rev); + T(xhr.status == 200); + + var xhr = CouchDB.request("GET", "/test_suite_db/bin_doc3/attachment.txt"); + T(xhr.status == 404); + + // deleted attachment is still accessible with revision + var xhr = CouchDB.request("GET", "/test_suite_db/bin_doc3/attachment.txt?rev=" + rev); + T(xhr.status == 200); + T(xhr.responseText == bin_data); + TEqualsIgnoreCase("text/plain;charset=utf-8", xhr.getResponseHeader("Content-Type")); + + // empty attachments + var xhr = CouchDB.request("PUT", "/test_suite_db/bin_doc4/attachment.txt", { + headers:{"Content-Type":"text/plain;charset=utf-8"}, + body:"" + }); + T(xhr.status == 201); + var rev = JSON.parse(xhr.responseText).rev; + + var xhr = CouchDB.request("GET", "/test_suite_db/bin_doc4/attachment.txt"); + T(xhr.status == 200); + T(xhr.responseText.length == 0); + + // overwrite previsously empty attachment + var xhr = CouchDB.request("PUT", "/test_suite_db/bin_doc4/attachment.txt?rev=" + rev, { + headers:{"Content-Type":"text/plain;charset=utf-8"}, + body:"This is a string" + }); + T(xhr.status == 201); + + var xhr = CouchDB.request("GET", "/test_suite_db/bin_doc4/attachment.txt"); + T(xhr.status == 200); + T(xhr.responseText == "This is a string"); + + // Attachment sparseness COUCHDB-220 + + var docs = []; + for (var i = 0; i < 5; i++) { + var doc = { + _id: (i).toString(), + _attachments:{ + "foo.txt": { + content_type:"text/plain", + data: "VGhpcyBpcyBhIGJhc2U2NCBlbmNvZGVkIHRleHQ=" + } + } + }; + docs.push(doc); + } + + var saved = db.bulkSave(docs); + // now delete the docs, and while we are looping over them, remove the + // '_rev' field so we can re-create after deletion. + var to_up = []; + for (i=0;i<saved.length;i++) { + to_up.push({'_id': saved[i]['id'], '_rev': saved[i]['rev'], '_deleted': true}); + delete docs[i]._rev; + } + // delete them. + var saved2 = db.bulkSave(to_up); + // re-create them + var saved3 = db.bulkSave(docs); + + var before = db.info().disk_size; + + // Compact it. + T(db.compact().ok); + T(db.last_req.status == 202); + // compaction isn't instantaneous, loop until done + while (db.info().compact_running) {}; + + var after = db.info().disk_size; + + // Compaction should reduce the database slightly, but not + // orders of magnitude (unless attachments introduce sparseness) + T(after > before * 0.1, "before: " + before + " after: " + after); + + + // test large attachments - COUCHDB-366 + var lorem = CouchDB.request("GET", "/_utils/script/test/lorem.txt").responseText; + + var xhr = CouchDB.request("PUT", "/test_suite_db/bin_doc5/lorem.txt", { + headers:{"Content-Type":"text/plain;charset=utf-8"}, + body:lorem + }); + T(xhr.status == 201); + var rev = JSON.parse(xhr.responseText).rev; + + var xhr = CouchDB.request("GET", "/test_suite_db/bin_doc5/lorem.txt"); + T(xhr.responseText == lorem); + TEqualsIgnoreCase("text/plain;charset=utf-8", xhr.getResponseHeader("Content-Type")); + + // test large inline attachment too + var lorem_b64 = CouchDB.request("GET", "/_utils/script/test/lorem_b64.txt").responseText; + var doc = db.open("bin_doc5", {attachments:true}); + T(doc._attachments["lorem.txt"].data == lorem_b64); + + // test etags for attachments. + var xhr = CouchDB.request("GET", "/test_suite_db/bin_doc5/lorem.txt"); + T(xhr.status == 200); + var etag = xhr.getResponseHeader("etag"); + xhr = CouchDB.request("GET", "/test_suite_db/bin_doc5/lorem.txt", { + headers: {"if-none-match": etag} + }); + T(xhr.status == 304); + + // test COUCHDB-497 - empty attachments + var xhr = CouchDB.request("PUT", "/test_suite_db/bin_doc5/empty.txt?rev="+rev, { + headers:{"Content-Type":"text/plain;charset=utf-8", "Content-Length": "0"}, + body:"" + }); + TEquals(201, xhr.status, "should send 201 Accepted"); + var rev = JSON.parse(xhr.responseText).rev; + var xhr = CouchDB.request("PUT", "/test_suite_db/bin_doc5/empty.txt?rev="+rev, { + headers:{"Content-Type":"text/plain;charset=utf-8"} + }); + TEquals(201, xhr.status, "should send 201 Accepted"); + + // implicit doc creation allows creating docs with a reserved id. COUCHDB-565 + var xhr = CouchDB.request("PUT", "/test_suite_db/_nonexistant/attachment.txt", { + headers: {"Content-Type":"text/plain;charset=utf-8"}, + body: "THIS IS AN ATTACHMENT. BOOYA!" + }); + TEquals(400, xhr.status, "should return error code 400 Bad Request"); + + // test COUCHDB-809 - stubs should only require the 'stub' field + var bin_doc6 = { + _id: "bin_doc6", + _attachments:{ + "foo.txt": { + content_type:"text/plain", + data: "VGhpcyBpcyBhIGJhc2U2NCBlbmNvZGVkIHRleHQ=" + } + } + }; + T(db.save(bin_doc6).ok); + // stub out the attachment + bin_doc6._attachments["foo.txt"] = { stub: true }; + T(db.save(bin_doc6).ok == true); + + // wrong rev pos specified + + // stub out the attachment with the wrong revpos + bin_doc6._attachments["foo.txt"] = { stub: true, revpos: 10}; + try { + T(db.save(bin_doc6).ok == true); + T(false && "Shouldn't get here!"); + } catch (e) { + T(e.error == "missing_stub"); + } + + // test MD5 header + var bin_data = "foo bar" + var xhr = CouchDB.request("PUT", "/test_suite_db/bin_doc7/attachment.txt", { + headers:{"Content-Type":"application/octet-stream", + "Content-MD5":"MntvB0NYESObxH4VRDUycw=="}, + body:bin_data + }); + TEquals(201, xhr.status); + + var xhr = CouchDB.request("GET", "/test_suite_db/bin_doc7/attachment.txt"); + TEquals('MntvB0NYESObxH4VRDUycw==', xhr.getResponseHeader("Content-MD5")); + + // test attachment via multipart/form-data + var bin_doc8 = { + _id: "bin_doc8" + }; + T(db.save(bin_doc8).ok); + var doc = db.open("bin_doc8"); + var body = "------TF\r\n" + + "Content-Disposition: form-data; name=\"_rev\"\r\n\r\n" + + doc._rev + "\r\n" + + "------TF\r\n" + + "Content-Disposition: form-data; name=\"_attachments\"; filename=\"file.txt\"\r\n" + + "Content-Type: text/plain\r\n\r\n" + + "contents of file.txt\r\n\r\n" + + "------TF--" + xhr = CouchDB.request("POST", "/test_suite_db/bin_doc8", { + headers: { + "Content-Type": "multipart/form-data; boundary=----TF", + "Content-Length": body.length + }, + body: body + }); + TEquals(201, xhr.status); + TEquals(true, JSON.parse(xhr.responseText).ok); + var doc = db.open("bin_doc8"); + T(doc._attachments); + T(doc._attachments['file.txt']); + +};