From 0d347669831fa6e2953345f4de82dbd7320a5922 Mon Sep 17 00:00:00 2001 From: Richard Newman Date: Mon, 30 Mar 2015 15:02:51 -0700 Subject: [PATCH] Bug 1148933 - Remove old storageservice.js and tests. r=gps --- services/common/moz.build | 1 - services/common/services-common.js | 1 - services/common/storageservice.js | 2222 ----------------- .../common/tests/unit/test_load_modules.js | 1 - .../tests/unit/test_storageservice_bso.js | 124 - .../tests/unit/test_storageservice_client.js | 1381 ---------- services/common/tests/unit/xpcshell.ini | 6 - 7 files changed, 3736 deletions(-) delete mode 100644 services/common/storageservice.js delete mode 100644 services/common/tests/unit/test_storageservice_bso.js delete mode 100644 services/common/tests/unit/test_storageservice_client.js diff --git a/services/common/moz.build b/services/common/moz.build index 29b3aa45770..41c855d1e2a 100644 --- a/services/common/moz.build +++ b/services/common/moz.build @@ -17,7 +17,6 @@ EXTRA_JS_MODULES['services-common'] += [ 'hawkclient.js', 'hawkrequest.js', 'logmanager.js', - 'storageservice.js', 'stringbundle.js', 'tokenserverclient.js', 'utils.js', diff --git a/services/common/services-common.js b/services/common/services-common.js index 106a5848059..bc37d4028f4 100644 --- a/services/common/services-common.js +++ b/services/common/services-common.js @@ -8,5 +8,4 @@ pref("services.common.log.logger.rest.request", "Debug"); pref("services.common.log.logger.rest.response", "Debug"); -pref("services.common.storageservice.sendVersionInfo", true); pref("services.common.log.logger.tokenserverclient", "Debug"); diff --git a/services/common/storageservice.js b/services/common/storageservice.js deleted file mode 100644 index 3f73555ad75..00000000000 --- a/services/common/storageservice.js +++ /dev/null @@ -1,2222 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this file, - * You can obtain one at http://mozilla.org/MPL/2.0/. */ - -/** - * This file contains APIs for interacting with the Storage Service API. - * - * The specification for the service is available at. - * http://docs.services.mozilla.com/storage/index.html - * - * Nothing about the spec or the service is Sync-specific. And, that is how - * these APIs are implemented. Instead, it is expected that consumers will - * create a new type inheriting or wrapping those provided by this file. - * - * STORAGE SERVICE OVERVIEW - * - * The storage service is effectively a key-value store where each value is a - * well-defined envelope that stores specific metadata along with a payload. - * These values are called Basic Storage Objects, or BSOs. BSOs are organized - * into named groups called collections. - * - * The service also provides ancillary APIs not related to storage, such as - * looking up the set of stored collections, current quota usage, etc. - */ - -"use strict"; - -this.EXPORTED_SYMBOLS = [ - "BasicStorageObject", - "StorageServiceClient", - "StorageServiceRequestError", -]; - -const {classes: Cc, interfaces: Ci, results: Cr, utils: Cu} = Components; - -Cu.import("resource://gre/modules/Preferences.jsm"); -Cu.import("resource://services-common/async.js"); -Cu.import("resource://gre/modules/Log.jsm"); -Cu.import("resource://services-common/rest.js"); -Cu.import("resource://services-common/utils.js"); - -const Prefs = new Preferences("services.common.storageservice."); - -/** - * The data type stored in the storage service. - * - * A Basic Storage Object (BSO) is the primitive type stored in the storage - * service. BSO's are simply maps with a well-defined set of keys. - * - * BSOs belong to named collections. - * - * A single BSO consists of the following fields: - * - * id - An identifying string. This is how a BSO is uniquely identified within - * a single collection. - * modified - Integer milliseconds since Unix epoch BSO was modified. - * payload - String contents of BSO. The format of the string is undefined - * (although JSON is typically used). - * ttl - The number of seconds to keep this record. - * sortindex - Integer indicating relative importance of record within the - * collection. - * - * The constructor simply creates an empty BSO having the specified ID (which - * can be null or undefined). It also takes an optional collection. This is - * purely for convenience. - * - * This type is meant to be a dumb container and little more. - * - * @param id - * (string) ID of BSO. Can be null. - * (string) Collection BSO belongs to. Can be null; - */ -this.BasicStorageObject = - function BasicStorageObject(id=null, collection=null) { - this.data = {}; - this.id = id; - this.collection = collection; -} -BasicStorageObject.prototype = { - id: null, - collection: null, - data: null, - - // At the time this was written, the convention for constructor arguments - // was not adopted by Harmony. It could break in the future. We have test - // coverage that will break if SpiderMonkey changes, just in case. - _validKeys: new Set(["id", "payload", "modified", "sortindex", "ttl"]), - - /** - * Get the string payload as-is. - */ - get payload() { - return this.data.payload; - }, - - /** - * Set the string payload to a new value. - */ - set payload(value) { - this.data.payload = value; - }, - - /** - * Get the modified time of the BSO in milliseconds since Unix epoch. - * - * You can convert this to a native JS Date instance easily: - * - * let date = new Date(bso.modified); - */ - get modified() { - return this.data.modified; - }, - - /** - * Sets the modified time of the BSO in milliseconds since Unix epoch. - * - * Please note that if this value is sent to the server it will be ignored. - * The server will use its time at the time of the operation when storing the - * BSO. - */ - set modified(value) { - this.data.modified = value; - }, - - get sortindex() { - if (this.data.sortindex) { - return this.data.sortindex || 0; - } - - return 0; - }, - - set sortindex(value) { - if (!value && value !== 0) { - delete this.data.sortindex; - return; - } - - this.data.sortindex = value; - }, - - get ttl() { - return this.data.ttl; - }, - - set ttl(value) { - if (!value && value !== 0) { - delete this.data.ttl; - return; - } - - this.data.ttl = value; - }, - - /** - * Deserialize JSON or another object into this instance. - * - * The argument can be a string containing serialized JSON or an object. - * - * If the JSON is invalid or if the object contains unknown fields, an - * exception will be thrown. - * - * @param json - * (string|object) Value to construct BSO from. - */ - deserialize: function deserialize(input) { - let data; - - if (typeof(input) == "string") { - data = JSON.parse(input); - if (typeof(data) != "object") { - throw new Error("Supplied JSON is valid but is not a JS-Object."); - } - } - else if (typeof(input) == "object") { - data = input; - } else { - throw new Error("Argument must be a JSON string or object: " + - typeof(input)); - } - - for each (let key in Object.keys(data)) { - if (key == "id") { - this.id = data.id; - continue; - } - - if (!this._validKeys.has(key)) { - throw new Error("Invalid key in object: " + key); - } - - this.data[key] = data[key]; - } - }, - - /** - * Serialize the current BSO to JSON. - * - * @return string - * The JSON representation of this BSO. - */ - toJSON: function toJSON() { - let obj = {}; - - for (let [k, v] in Iterator(this.data)) { - obj[k] = v; - } - - if (this.id) { - obj.id = this.id; - } - - return obj; - }, - - toString: function toString() { - return "{ " + - "id: " + this.id + " " + - "modified: " + this.modified + " " + - "ttl: " + this.ttl + " " + - "index: " + this.sortindex + " " + - "payload: " + this.payload + - " }"; - }, -}; - -/** - * Represents an error encountered during a StorageServiceRequest request. - * - * Instances of this will be passed to the onComplete callback for any request - * that did not succeed. - * - * This type effectively wraps other error conditions. It is up to the client - * to determine the appropriate course of action for each error type - * encountered. - * - * The following error "classes" are defined by properties on each instance: - * - * serverModified - True if the request to modify data was conditional and - * the server rejected the request because it has newer data than the - * client. - * - * notFound - True if the requested URI or resource does not exist. - * - * conflict - True if the server reported that a resource being operated on - * was in conflict. If this occurs, the client should typically wait a - * little and try the request again. - * - * requestTooLarge - True if the request was too large for the server. If - * this happens on batch requests, the client should retry the request with - * smaller batches. - * - * network - A network error prevented this request from succeeding. If set, - * it will be an Error thrown by the Gecko network stack. If set, it could - * mean that the request could not be performed or that an error occurred - * when the request was in flight. It is also possible the request - * succeeded on the server but the response was lost in transit. - * - * authentication - If defined, an authentication error has occurred. If - * defined, it will be an Error instance. If seen, the client should not - * retry the request without first correcting the authentication issue. - * - * client - An error occurred which was the client's fault. This typically - * means the code in this file is buggy. - * - * server - An error occurred on the server. In the ideal world, this should - * never happen. But, it does. If set, this will be an Error which - * describes the error as reported by the server. - */ -this.StorageServiceRequestError = function StorageServiceRequestError() { - this.serverModified = false; - this.notFound = false; - this.conflict = false; - this.requestToolarge = false; - this.network = null; - this.authentication = null; - this.client = null; - this.server = null; -} - -/** - * Represents a single request to the storage service. - * - * Instances of this type are returned by the APIs on StorageServiceClient. - * They should not be created outside of StorageServiceClient. - * - * This type encapsulates common storage API request and response handling. - * Metadata required to perform the request is stored inside each instance and - * should be treated as invisible by consumers. - * - * A number of "public" properties are exposed to allow clients to further - * customize behavior. These are documented below. - * - * Some APIs in StorageServiceClient define their own types which inherit from - * this one. Read the API documentation to see which types those are and when - * they apply. - * - * This type wraps RESTRequest rather than extending it. The reason is mainly - * to avoid the fragile base class problem. We implement considerable extra - * functionality on top of RESTRequest and don't want this to accidentally - * trample on RESTRequest's members. - * - * If this were a C++ class, it and StorageServiceClient would be friend - * classes. Each touches "protected" APIs of the other. Thus, each should be - * considered when making changes to the other. - * - * Usage - * ===== - * - * When you obtain a request instance, it is waiting to be dispatched. It may - * have additional settings available for tuning. See the documentation in - * StorageServiceClient for more. - * - * There are essentially two types of requests: "basic" and "streaming." - * "Basic" requests encapsulate the traditional request-response paradigm: - * a request is issued and we get a response later once the full response - * is available. Most of the APIs in StorageServiceClient issue these "basic" - * requests. Streaming requests typically involve the transport of multiple - * BasicStorageObject instances. When a new BSO instance is available, a - * callback is fired. - * - * For basic requests, the general flow looks something like: - * - * // Obtain a new request instance. - * let request = client.getCollectionInfo(); - * - * // Install a handler which provides callbacks for request events. The most - * // important is `onComplete`, which is called when the request has - * // finished and the response is completely received. - * request.handler = { - * onComplete: function onComplete(error, request) { - * // Do something. - * } - * }; - * - * // Send the request. - * request.dispatch(); - * - * Alternatively, we can install the onComplete handler when calling dispatch: - * - * let request = client.getCollectionInfo(); - * request.dispatch(function onComplete(error, request) { - * // Handle response. - * }); - * - * Please note that installing an `onComplete` handler as the argument to - * `dispatch()` will overwrite an existing `handler`. - * - * In both of the above example, the two `request` variables are identical. The - * original `StorageServiceRequest` is passed into the callback so callers - * don't need to rely on closures. - * - * Most of the complexity for onComplete handlers is error checking. - * - * The first thing you do in your onComplete handler is ensure no error was - * seen: - * - * function onComplete(error, request) { - * if (error) { - * // Handle error. - * } - * } - * - * If `error` is defined, it will be an instance of - * `StorageServiceRequestError`. An error will be set if the request didn't - * complete successfully. This means the transport layer must have succeeded - * and the application protocol (HTTP) must have returned a successful status - * code (2xx and some 3xx). Please see the documentation for - * `StorageServiceRequestError` for more. - * - * A robust error handler would look something like: - * - * function onComplete(error, request) { - * if (error) { - * if (error.network) { - * // Network error encountered! - * } else if (error.server) { - * // Something went wrong on the server (HTTP 5xx). - * } else if (error.authentication) { - * // Server rejected request due to bad credentials. - * } else if (error.serverModified) { - * // The conditional request was rejected because the server has newer - * // data than what the client reported. - * } else if (error.conflict) { - * // The server reported that the operation could not be completed - * // because another client is also updating it. - * } else if (error.requestTooLarge) { - * // The server rejected the request because it was too large. - * } else if (error.notFound) { - * // The requested resource was not found. - * } else if (error.client) { - * // Something is wrong with the client's request. You should *never* - * // see this, as it means this client is likely buggy. It could also - * // mean the server is buggy or misconfigured. Either way, something - * // is buggy. - * } - * - * return; - * } - * - * // Handle successful case. - * } - * - * If `error` is null, the request completed successfully. There may or may not - * be additional data available on the request instance. - * - * For requests that obtain data, this data is typically made available through - * the `resultObj` property on the request instance. The API that was called - * will install its own response hander and ensure this property is decoded to - * what you expect. - * - * Conditional Requests - * -------------------- - * - * Many of the APIs on `StorageServiceClient` support conditional requests. - * That is, the client defines the last version of data it has (the version - * comes from a previous response from the server) and sends this as part of - * the request. - * - * For query requests, if the server hasn't changed, no new data will be - * returned. If issuing a conditional query request, the caller should check - * the `notModified` property on the request in the response callback. If this - * property is true, the server has no new data and there is obviously no data - * on the response. - * - * For example: - * - * let request = client.getCollectionInfo(); - * request.locallyModifiedVersion = Date.now() - 60000; - * request.dispatch(function onComplete(error, request) { - * if (error) { - * // Handle error. - * return; - * } - * - * if (request.notModified) { - * return; - * } - * - * let info = request.resultObj; - * // Do stuff. - * }); - * - * For modification requests, if the server has changed, the request will be - * rejected. When this happens, `error`will be defined and the `serverModified` - * property on it will be true. - * - * For example: - * - * let request = client.setBSO(bso); - * request.locallyModifiedVersion = bso.modified; - * request.dispatch(function onComplete(error, request) { - * if (error) { - * if (error.serverModified) { - * // Server data is newer! We should probably fetch it and apply - * // locally. - * } - * - * return; - * } - * - * // Handle success. - * }); - * - * Future Features - * --------------- - * - * The current implementation does not support true streaming for things like - * multi-BSO retrieval. However, the API supports it, so we should be able - * to implement it transparently. - */ -function StorageServiceRequest() { - this._log = Log.repository.getLogger("Sync.StorageService.Request"); - this._log.level = Log.Level[Prefs.get("log.level")]; - - this.notModified = false; - - this._client = null; - this._request = null; - this._method = null; - this._handler = {}; - this._data = null; - this._error = null; - this._resultObj = null; - this._locallyModifiedVersion = null; - this._allowIfModified = false; - this._allowIfUnmodified = false; -} -StorageServiceRequest.prototype = { - /** - * The StorageServiceClient this request came from. - */ - get client() { - return this._client; - }, - - /** - * The underlying RESTRequest instance. - * - * This should be treated as read only and should not be modified - * directly by external callers. While modification would probably work, this - * would defeat the purpose of the API and the abstractions it is meant to - * provide. - * - * If a consumer needs to modify the underlying request object, it is - * recommended for them to implement a new type that inherits from - * StorageServiceClient and override the necessary APIs to modify the request - * there. - * - * This accessor may disappear in future versions. - */ - get request() { - return this._request; - }, - - /** - * The RESTResponse that resulted from the RESTRequest. - */ - get response() { - return this._request.response; - }, - - /** - * HTTP status code from response. - */ - get statusCode() { - let response = this.response; - return response ? response.status : null; - }, - - /** - * Holds any error that has occurred. - * - * If a network error occurred, that will be returned. If no network error - * occurred, the client error will be returned. If no error occurred (yet), - * null will be returned. - */ - get error() { - return this._error; - }, - - /** - * The result from the request. - * - * This stores the object returned from the server. The type of object depends - * on the request type. See the per-API documentation in StorageServiceClient - * for details. - */ - get resultObj() { - return this._resultObj; - }, - - /** - * Define the local version of the entity the client has. - * - * This is used to enable conditional requests. Depending on the request - * type, the value set here could be reflected in the X-If-Modified-Since or - * X-If-Unmodified-Since headers. - * - * This attribute is not honoured on every request. See the documentation - * in the client API to learn where it is valid. - */ - set locallyModifiedVersion(value) { - // Will eventually become a header, so coerce to string. - this._locallyModifiedVersion = "" + value; - }, - - /** - * Object which holds callbacks and state for this request. - * - * The handler is installed by users of this request. It is simply an object - * containing 0 or more of the following properties: - * - * onComplete - A function called when the request has completed and all - * data has been received from the server. The function receives the - * following arguments: - * - * (StorageServiceRequestError) Error encountered during request. null - * if no error was encountered. - * (StorageServiceRequest) The request that was sent (this instance). - * Response information is available via properties and functions. - * - * Unless the call to dispatch() throws before returning, this callback - * is guaranteed to be invoked. - * - * Every client almost certainly wants to install this handler. - * - * onDispatch - A function called immediately before the request is - * dispatched. This hook can be used to inspect or modify the request - * before it is issued. - * - * The called function receives the following arguments: - * - * (StorageServiceRequest) The request being issued (this request). - * - * onBSORecord - When retrieving multiple BSOs from the server, this - * function is invoked when a new BSO record has been read. This function - * will be invoked 0 to N times before onComplete is invoked. onComplete - * signals that the last BSO has been processed or that an error - * occurred. The function receives the following arguments: - * - * (StorageServiceRequest) The request that was sent (this instance). - * (BasicStorageObject|string) The received BSO instance (when in full - * mode) or the string ID of the BSO (when not in full mode). - * - * Callers are free to (and encouraged) to store extra state in the supplied - * handler. - */ - set handler(value) { - if (typeof(value) != "object") { - throw new Error("Invalid handler. Must be an Object."); - } - - this._handler = value; - - if (!value.onComplete) { - this._log.warn("Handler does not contain an onComplete callback!"); - } - }, - - get handler() { - return this._handler; - }, - - //--------------- - // General APIs | - //--------------- - - /** - * Start the request. - * - * The request is dispatched asynchronously. The installed handler will have - * one or more of its callbacks invoked as the state of the request changes. - * - * The `onComplete` argument is optional. If provided, the supplied function - * will be installed on a *new* handler before the request is dispatched. This - * is equivalent to calling: - * - * request.handler = {onComplete: value}; - * request.dispatch(); - * - * Please note that any existing handler will be replaced if onComplete is - * provided. - * - * @param onComplete - * (function) Callback to be invoked when request has completed. - */ - dispatch: function dispatch(onComplete) { - if (onComplete) { - this.handler = {onComplete: onComplete}; - } - - // Installing the dummy callback makes implementation easier in _onComplete - // because we can then blindly call. - this._dispatch(function _internalOnComplete(error) { - this._onComplete(error); - this.completed = true; - }.bind(this)); - }, - - /** - * This is a synchronous version of dispatch(). - * - * THIS IS AN EVIL FUNCTION AND SHOULD NOT BE CALLED. It is provided for - * legacy reasons to support evil, synchronous clients. - * - * Please note that onComplete callbacks are executed from this JS thread. - * We dispatch the request, spin the event loop until it comes back. Then, - * we execute callbacks ourselves then return. In other words, there is no - * potential for spinning between callback execution and this function - * returning. - * - * The `onComplete` argument has the same behavior as for `dispatch()`. - * - * @param onComplete - * (function) Callback to be invoked when request has completed. - */ - dispatchSynchronous: function dispatchSynchronous(onComplete) { - if (onComplete) { - this.handler = {onComplete: onComplete}; - } - - let cb = Async.makeSyncCallback(); - this._dispatch(cb); - let error = Async.waitForSyncCallback(cb); - - this._onComplete(error); - this.completed = true; - }, - - //------------------------------------------------------------------------- - // HIDDEN APIS. DO NOT CHANGE ANYTHING UNDER HERE FROM OUTSIDE THIS TYPE. | - //------------------------------------------------------------------------- - - /** - * Data to include in HTTP request body. - */ - _data: null, - - /** - * StorageServiceRequestError encountered during dispatchy. - */ - _error: null, - - /** - * Handler to parse response body into another object. - * - * This is installed by the client API. It should return the value the body - * parses to on success. If a failure is encountered, an exception should be - * thrown. - */ - _completeParser: null, - - /** - * Dispatch the request. - * - * This contains common functionality for dispatching requests. It should - * ideally be part of dispatch, but since dispatchSynchronous exists, we - * factor out common code. - */ - _dispatch: function _dispatch(onComplete) { - // RESTRequest throws if the request has already been dispatched, so we - // need not bother checking. - - // Inject conditional headers into request if they are allowed and if a - // value is set. Note that _locallyModifiedVersion is always a string and - // if("0") is true. - if (this._allowIfModified && this._locallyModifiedVersion) { - this._log.trace("Making request conditional."); - this._request.setHeader("X-If-Modified-Since", - this._locallyModifiedVersion); - } else if (this._allowIfUnmodified && this._locallyModifiedVersion) { - this._log.trace("Making request conditional."); - this._request.setHeader("X-If-Unmodified-Since", - this._locallyModifiedVersion); - } - - // We have both an internal and public hook. - // If these throw, it is OK since we are not in a callback. - if (this._onDispatch) { - this._onDispatch(); - } - - if (this._handler.onDispatch) { - this._handler.onDispatch(this); - } - - this._client.runListeners("onDispatch", this); - - this._log.info("Dispatching request: " + this._method + " " + - this._request.uri.asciiSpec); - - this._request.dispatch(this._method, this._data, onComplete); - }, - - /** - * RESTRequest onComplete handler for all requests. - * - * This provides common logic for all response handling. - */ - _onComplete: function(error) { - let onCompleteCalled = false; - - let callOnComplete = function callOnComplete() { - onCompleteCalled = true; - - if (!this._handler.onComplete) { - this._log.warn("No onComplete installed in handler!"); - return; - } - - try { - this._handler.onComplete(this._error, this); - } catch (ex) { - this._log.warn("Exception when invoking handler's onComplete: " + - CommonUtils.exceptionStr(ex)); - throw ex; - } - }.bind(this); - - try { - if (error) { - this._error = new StorageServiceRequestError(); - this._error.network = error; - this._log.info("Network error during request: " + error); - this._client.runListeners("onNetworkError", this._client, this, error); - callOnComplete(); - return; - } - - let response = this._request.response; - this._log.info(response.status + " " + this._request.uri.asciiSpec); - - this._processHeaders(); - - if (response.status == 200) { - this._resultObj = this._completeParser(response); - callOnComplete(); - return; - } - - if (response.status == 201) { - callOnComplete(); - return; - } - - if (response.status == 204) { - callOnComplete(); - return; - } - - if (response.status == 304) { - this.notModified = true; - callOnComplete(); - return; - } - - // TODO handle numeric response code from server. - if (response.status == 400) { - this._error = new StorageServiceRequestError(); - this._error.client = new Error("Client error!"); - callOnComplete(); - return; - } - - if (response.status == 401) { - this._error = new StorageServiceRequestError(); - this._error.authentication = new Error("401 Received."); - this._client.runListeners("onAuthFailure", this._error.authentication, - this); - callOnComplete(); - return; - } - - if (response.status == 404) { - this._error = new StorageServiceRequestError(); - this._error.notFound = true; - callOnComplete(); - return; - } - - if (response.status == 409) { - this._error = new StorageServiceRequestError(); - this._error.conflict = true; - callOnComplete(); - return; - } - - if (response.status == 412) { - this._error = new StorageServiceRequestError(); - this._error.serverModified = true; - callOnComplete(); - return; - } - - if (response.status == 413) { - this._error = new StorageServiceRequestError(); - this._error.requestTooLarge = true; - callOnComplete(); - return; - } - - // If we see this, either the client or the server is buggy. We should - // never see this. - if (response.status == 415) { - this._log.error("415 HTTP response seen from server! This should " + - "never happen!"); - this._error = new StorageServiceRequestError(); - this._error.client = new Error("415 Unsupported Media Type received!"); - callOnComplete(); - return; - } - - if (response.status >= 500 && response.status <= 599) { - this._log.error(response.status + " seen from server!"); - this._error = new StorageServiceRequestError(); - this._error.server = new Error(response.status + " status code."); - callOnComplete(); - return; - } - - callOnComplete(); - - } catch (ex) { - this._clientError = ex; - this._log.info("Exception when processing _onComplete: " + ex); - - if (!onCompleteCalled) { - this._log.warn("Exception in internal response handling logic!"); - try { - callOnComplete(); - } catch (ex) { - this._log.warn("An additional exception was encountered when " + - "calling the handler's onComplete: " + ex); - } - } - } - }, - - _processHeaders: function _processHeaders() { - let headers = this._request.response.headers; - - if (headers["x-timestamp"]) { - this.serverTime = parseFloat(headers["x-timestamp"]); - } - - if (headers["x-backoff"]) { - this.backoffInterval = 1000 * parseInt(headers["x-backoff"], 10); - } - - if (headers["retry-after"]) { - this.backoffInterval = 1000 * parseInt(headers["retry-after"], 10); - } - - if (this.backoffInterval) { - let failure = this._request.response.status == 503; - this._client.runListeners("onBackoffReceived", this._client, this, - this.backoffInterval, !failure); - } - - if (headers["x-quota-remaining"]) { - this.quotaRemaining = parseInt(headers["x-quota-remaining"], 10); - this._client.runListeners("onQuotaRemaining", this._client, this, - this.quotaRemaining); - } - }, -}; - -/** - * Represents a request to fetch from a collection. - * - * These requests are highly configurable so they are given their own type. - * This type inherits from StorageServiceRequest and provides additional - * controllable parameters. - * - * By default, requests are issued in "streaming" mode. As the client receives - * data from the server, it will invoke the caller-supplied onBSORecord - * callback for each record as it is ready. When all records have been received, - * it will invoke onComplete as normal. To change this behavior, modify the - * "streaming" property before the request is dispatched. - */ -function StorageCollectionGetRequest() { - StorageServiceRequest.call(this); -} -StorageCollectionGetRequest.prototype = { - __proto__: StorageServiceRequest.prototype, - - _namedArgs: {}, - - _streaming: true, - - /** - * Control whether streaming mode is in effect. - * - * Read the type documentation above for more details. - */ - set streaming(value) { - this._streaming = !!value; - }, - - /** - * Define the set of IDs to fetch from the server. - */ - set ids(value) { - this._namedArgs.ids = value.join(","); - }, - - /** - * Only retrieve BSOs that were modified strictly before this time. - * - * Defined in milliseconds since UNIX epoch. - */ - set older(value) { - this._namedArgs.older = value; - }, - - /** - * Only retrieve BSOs that were modified strictly after this time. - * - * Defined in milliseconds since UNIX epoch. - */ - set newer(value) { - this._namedArgs.newer = value; - }, - - /** - * If set to a truthy value, return full BSO information. - * - * If not set (the default), the request will only return the set of BSO - * ids. - */ - set full(value) { - if (value) { - this._namedArgs.full = "1"; - } else { - delete this._namedArgs["full"]; - } - }, - - /** - * Limit the max number of returned BSOs to this integer number. - */ - set limit(value) { - this._namedArgs.limit = value; - }, - - /** - * If set with any value, sort the results based on modification time, oldest - * first. - */ - set sortOldest(value) { - this._namedArgs.sort = "oldest"; - }, - - /** - * If set with any value, sort the results based on modification time, newest - * first. - */ - set sortNewest(value) { - this._namedArgs.sort = "newest"; - }, - - /** - * If set with any value, sort the results based on sortindex value, highest - * first. - */ - set sortIndex(value) { - this._namedArgs.sort = "index"; - }, - - _onDispatch: function _onDispatch() { - let qs = this._getQueryString(); - if (!qs.length) { - return; - } - - this._request.uri = CommonUtils.makeURI(this._request.uri.asciiSpec + "?" + - qs); - }, - - _getQueryString: function _getQueryString() { - let args = []; - for (let [k, v] in Iterator(this._namedArgs)) { - args.push(encodeURIComponent(k) + "=" + encodeURIComponent(v)); - } - - return args.join("&"); - }, - - _completeParser: function _completeParser(response) { - let obj = JSON.parse(response.body); - let items = obj.items; - - if (!Array.isArray(items)) { - throw new Error("Unexpected JSON response. items is missing or not an " + - "array!"); - } - - if (!this.handler.onBSORecord) { - return; - } - - for (let bso of items) { - this.handler.onBSORecord(this, bso); - } - }, -}; - -/** - * Represents a request that sets data in a collection - * - * Instances of this type are returned by StorageServiceClient.setBSOs(). - */ -function StorageCollectionSetRequest() { - StorageServiceRequest.call(this); - - this.size = 0; - - // TODO Bug 775781 convert to Set and Map once iterable. - this.successfulIDs = []; - this.failures = {}; - - this._lines = []; -} -StorageCollectionSetRequest.prototype = { - __proto__: StorageServiceRequest.prototype, - - get count() { - return this._lines.length; - }, - - /** - * Add a BasicStorageObject to this request. - * - * Please note that the BSO content is retrieved when the BSO is added to - * the request. If the BSO changes after it is added to a request, those - * changes will not be reflected in the request. - * - * @param bso - * (BasicStorageObject) BSO to add to the request. - */ - addBSO: function addBSO(bso) { - if (!bso instanceof BasicStorageObject) { - throw new Error("argument must be a BasicStorageObject instance."); - } - - if (!bso.id) { - throw new Error("Passed BSO must have id defined."); - } - - this.addLine(JSON.stringify(bso)); - }, - - /** - * Add a BSO (represented by its serialized newline-delimited form). - * - * You probably shouldn't use this. It is used for batching. - */ - addLine: function addLine(line) { - // This is off by 1 in the larger direction. We don't care. - this.size += line.length + 1; - this._lines.push(line); - }, - - _onDispatch: function _onDispatch() { - this._data = this._lines.join("\n"); - this.size = this._data.length; - }, - - _completeParser: function _completeParser(response) { - let result = JSON.parse(response.body); - - for (let id of result.success) { - this.successfulIDs.push(id); - } - - this.allSucceeded = true; - - for (let [id, reasons] in Iterator(result.failed)) { - this.failures[id] = reasons; - this.allSucceeded = false; - } - }, -}; - -/** - * Represents a batch upload of BSOs to an individual collection. - * - * This is a more intelligent way to upload may BSOs to the server. It will - * split the uploaded data into multiple requests so size limits, etc aren't - * exceeded. - * - * Once a client obtains an instance of this type, it calls `addBSO` for each - * BSO to be uploaded. When the client is done providing BSOs to be uploaded, - * it calls `finish`. When `finish` is called, no more BSOs can be added to the - * batch. When all requests created from this batch have finished, the callback - * provided to `finish` will be invoked. - * - * Clients can also explicitly flush pending outgoing BSOs via `flush`. This - * allows callers to control their own batching/chunking. - * - * Interally, this maintains a queue of StorageCollectionSetRequest to be - * issued. At most one request is allowed to be in-flight at once. This is to - * avoid potential conflicts on the server. And, in the case of conditional - * requests, it prevents requests from being declined due to the server being - * updated by another request issued by us. - * - * If a request errors for any reason, all queued uploads are abandoned and the - * `finish` callback is invoked as soon as possible. The `successfulIDs` and - * `failures` properties will contain data from all requests that had this - * response data. In other words, the IDs have BSOs that were never sent to the - * server are not lumped in to either property. - * - * Requests can be made conditional by setting `locallyModifiedVersion` to the - * most recent version of server data. As responses from the server are seen, - * the last server version is carried forward to subsequent requests. - * - * The server version from the last request is available in the - * `serverModifiedVersion` property. It should only be accessed during or - * after the callback passed to `finish`. - * - * @param client - * (StorageServiceClient) Client instance to use for uploading. - * - * @param collection - * (string) Collection the batch operation will upload to. - */ -function StorageCollectionBatchedSet(client, collection) { - this.client = client; - this.collection = collection; - - this._log = client._log; - - this.locallyModifiedVersion = null; - this.serverModifiedVersion = null; - - // TODO Bug 775781 convert to Set and Map once iterable. - this.successfulIDs = []; - this.failures = {}; - - // Request currently being populated. - this._stagingRequest = client.setBSOs(this.collection); - - // Requests ready to be sent over the wire. - this._outgoingRequests = []; - - // Whether we are waiting for a response. - this._requestInFlight = false; - - this._onFinishCallback = null; - this._finished = false; - this._errorEncountered = false; -} -StorageCollectionBatchedSet.prototype = { - /** - * Add a BSO to be uploaded as part of this batch. - */ - addBSO: function addBSO(bso) { - if (this._errorEncountered) { - return; - } - - let line = JSON.stringify(bso); - - if (line.length > this.client.REQUEST_SIZE_LIMIT) { - throw new Error("BSO is larger than allowed limit: " + line.length + - " > " + this.client.REQUEST_SIZE_LIMIT); - } - - if (this._stagingRequest.size + line.length > this.client.REQUEST_SIZE_LIMIT) { - this._log.debug("Sending request because payload size would be exceeded"); - this._finishStagedRequest(); - - this._stagingRequest.addLine(line); - return; - } - - // We are guaranteed to fit within size limits. - this._stagingRequest.addLine(line); - - if (this._stagingRequest.count >= this.client.REQUEST_BSO_COUNT_LIMIT) { - this._log.debug("Sending request because BSO count threshold reached."); - this._finishStagedRequest(); - return; - } - }, - - finish: function finish(cb) { - if (this._finished) { - throw new Error("Batch request has already been finished."); - } - - this.flush(); - - this._onFinishCallback = cb; - this._finished = true; - this._stagingRequest = null; - }, - - flush: function flush() { - if (this._finished) { - throw new Error("Batch request has been finished."); - } - - if (!this._stagingRequest.count) { - return; - } - - this._finishStagedRequest(); - }, - - _finishStagedRequest: function _finishStagedRequest() { - this._outgoingRequests.push(this._stagingRequest); - this._sendOutgoingRequest(); - this._stagingRequest = this.client.setBSOs(this.collection); - }, - - _sendOutgoingRequest: function _sendOutgoingRequest() { - if (this._requestInFlight || this._errorEncountered) { - return; - } - - if (!this._outgoingRequests.length) { - return; - } - - let request = this._outgoingRequests.shift(); - - if (this.locallyModifiedVersion) { - request.locallyModifiedVersion = this.locallyModifiedVersion; - } - - request.dispatch(this._onBatchComplete.bind(this)); - this._requestInFlight = true; - }, - - _onBatchComplete: function _onBatchComplete(error, request) { - this._requestInFlight = false; - - this.serverModifiedVersion = request.serverTime; - - // Only update if we had a value before. Otherwise, this breaks - // unconditional requests! - if (this.locallyModifiedVersion) { - this.locallyModifiedVersion = request.serverTime; - } - - for (let id of request.successfulIDs) { - this.successfulIDs.push(id); - } - - for (let [id, reason] in Iterator(request.failures)) { - this.failures[id] = reason; - } - - if (request.error) { - this._errorEncountered = true; - } - - this._checkFinish(); - }, - - _checkFinish: function _checkFinish() { - if (this._outgoingRequests.length && !this._errorEncountered) { - this._sendOutgoingRequest(); - return; - } - - if (!this._onFinishCallback) { - return; - } - - try { - this._onFinishCallback(this); - } catch (ex) { - this._log.warn("Exception when calling finished callback: " + - CommonUtils.exceptionStr(ex)); - } - }, -}; -Object.freeze(StorageCollectionBatchedSet.prototype); - -/** - * Manages a batch of BSO deletion requests. - * - * A single instance of this virtual request allows deletion of many individual - * BSOs without having to worry about server limits. - * - * Instances are obtained by calling `deleteBSOsBatching` on - * StorageServiceClient. - * - * Usage is roughly the same as StorageCollectionBatchedSet. Callers obtain - * an instance and select individual BSOs for deletion by calling `addID`. - * When the caller is finished marking BSOs for deletion, they call `finish` - * with a callback which will be invoked when all deletion requests finish. - * - * When the finished callback is invoked, any encountered errors will be stored - * in the `errors` property of this instance (which is passed to the callback). - * This will be an empty array if no errors were encountered. Else, it will - * contain the errors from the `onComplete` handler of request instances. The - * set of succeeded and failed IDs is not currently available. - * - * Deletes can be made conditional by setting `locallyModifiedVersion`. The - * behavior is the same as request types. The only difference is that the - * updated version from the server as a result of requests is carried forward - * to subsequent requests. - * - * The server version from the last request is stored in the - * `serverModifiedVersion` property. It is not safe to access this until the - * callback from `finish`. - * - * Like StorageCollectionBatchedSet, requests are issued serially to avoid - * race conditions on the server. - * - * @param client - * (StorageServiceClient) Client request is associated with. - * @param collection - * (string) Collection being operated on. - */ -function StorageCollectionBatchedDelete(client, collection) { - this.client = client; - this.collection = collection; - - this._log = client._log; - - this.locallyModifiedVersion = null; - this.serverModifiedVersion = null; - this.errors = []; - - this._pendingIDs = []; - this._requestInFlight = false; - this._finished = false; - this._finishedCallback = null; -} -StorageCollectionBatchedDelete.prototype = { - addID: function addID(id) { - if (this._finished) { - throw new Error("Cannot add IDs to a finished instance."); - } - - // If we saw errors already, don't do any work. This is an optimization - // and isn't strictly required, as _sendRequest() should no-op. - if (this.errors.length) { - return; - } - - this._pendingIDs.push(id); - - if (this._pendingIDs.length >= this.client.REQUEST_BSO_DELETE_LIMIT) { - this._sendRequest(); - } - }, - - /** - * Finish this batch operation. - * - * No more IDs can be added to this operation. Existing IDs are flushed as - * a request. The passed callback will be called when all requests have - * finished. - */ - finish: function finish(cb) { - if (this._finished) { - throw new Error("Batch delete instance has already been finished."); - } - - this._finished = true; - this._finishedCallback = cb; - - if (this._pendingIDs.length) { - this._sendRequest(); - } - }, - - _sendRequest: function _sendRequest() { - // Only allow 1 active request at a time and don't send additional - // requests if one has failed. - if (this._requestInFlight || this.errors.length) { - return; - } - - let ids = this._pendingIDs.splice(0, this.client.REQUEST_BSO_DELETE_LIMIT); - let request = this.client.deleteBSOs(this.collection, ids); - - if (this.locallyModifiedVersion) { - request.locallyModifiedVersion = this.locallyModifiedVersion; - } - - request.dispatch(this._onRequestComplete.bind(this)); - this._requestInFlight = true; - }, - - _onRequestComplete: function _onRequestComplete(error, request) { - this._requestInFlight = false; - - if (error) { - // We don't currently track metadata of what failed. This is an obvious - // feature that could be added. - this._log.warn("Error received from server: " + error); - this.errors.push(error); - } - - this.serverModifiedVersion = request.serverTime; - - // If performing conditional requests, carry forward the new server version - // so subsequent conditional requests work. - if (this.locallyModifiedVersion) { - this.locallyModifiedVersion = request.serverTime; - } - - if (this._pendingIDs.length && !this.errors.length) { - this._sendRequest(); - return; - } - - if (!this._finishedCallback) { - return; - } - - try { - this._finishedCallback(this); - } catch (ex) { - this._log.warn("Exception when invoking finished callback: " + - CommonUtils.exceptionStr(ex)); - } - }, -}; -Object.freeze(StorageCollectionBatchedDelete.prototype); - -/** - * Construct a new client for the SyncStorage API, version 2.0. - * - * Clients are constructed against a base URI. This URI is typically obtained - * from the token server via the endpoint component of a successful token - * response. - * - * The purpose of this type is to serve as a middleware between a client's core - * logic and the HTTP API. It hides the details of how the storage API is - * implemented but exposes important events, such as when auth goes bad or the - * server requests the client to back off. - * - * All request APIs operate by returning a StorageServiceRequest instance. The - * caller then installs the appropriate callbacks on each instance and then - * dispatches the request. - * - * Each client instance also serves as a controller and coordinator for - * associated requests. Callers can install listeners for common events on the - * client and take the appropriate action whenever any associated request - * observes them. For example, you will only need to register one listener for - * backoff observation as opposed to one on each request. - * - * While not currently supported, a future goal of this type is to support - * more advanced transport channels - such as SPDY - to allow for faster and - * more efficient API calls. The API is thus designed to abstract transport - * specifics away from the caller. - * - * Storage API consumers almost certainly have added functionality on top of the - * storage service. It is encouraged to create a child type which adds - * functionality to this layer. - * - * @param baseURI - * (string) Base URI for all requests. - */ -this.StorageServiceClient = function StorageServiceClient(baseURI) { - this._log = Log.repository.getLogger("Services.Common.StorageServiceClient"); - this._log.level = Log.Level[Prefs.get("log.level")]; - - this._baseURI = baseURI; - - if (this._baseURI[this._baseURI.length-1] != "/") { - this._baseURI += "/"; - } - - this._log.info("Creating new StorageServiceClient under " + this._baseURI); - - this._listeners = []; -} -StorageServiceClient.prototype = { - /** - * The user agent sent with every request. - * - * You probably want to change this. - */ - userAgent: "StorageServiceClient", - - /** - * Maximum size of entity bodies. - * - * TODO this should come from the server somehow. See bug 769759. - */ - REQUEST_SIZE_LIMIT: 512000, - - /** - * Maximum number of BSOs in requests. - * - * TODO this should come from the server somehow. See bug 769759. - */ - REQUEST_BSO_COUNT_LIMIT: 100, - - /** - * Maximum number of BSOs that can be deleted in a single DELETE. - * - * TODO this should come from the server. See bug 769759. - */ - REQUEST_BSO_DELETE_LIMIT: 100, - - _baseURI: null, - _log: null, - - _listeners: null, - - //---------------------------- - // Event Listener Management | - //---------------------------- - - /** - * Adds a listener to this client instance. - * - * Listeners allow other parties to react to and influence execution of the - * client instance. - * - * An event listener is simply an object that exposes functions which get - * executed during client execution. Objects can expose 0 or more of the - * following keys: - * - * onDispatch - Callback notified immediately before a request is - * dispatched. This gets called for every outgoing request. The function - * receives as its arguments the client instance and the outgoing - * StorageServiceRequest. This listener is useful for global - * authentication handlers, which can modify the request before it is - * sent. - * - * onAuthFailure - This is called when any request has experienced an - * authentication failure. - * - * This callback receives the following arguments: - * - * (StorageServiceClient) Client that encountered the auth failure. - * (StorageServiceRequest) Request that encountered the auth failure. - * - * onBackoffReceived - This is called when a backoff request is issued by - * the server. Backoffs are issued either when the service is completely - * unavailable (and the client should abort all activity) or if the server - * is under heavy load (and has completed the current request but is - * asking clients to be kind and stop issuing requests for a while). - * - * This callback receives the following arguments: - * - * (StorageServiceClient) Client that encountered the backoff. - * (StorageServiceRequest) Request that received the backoff. - * (number) Integer milliseconds the server is requesting us to back off - * for. - * (bool) Whether the request completed successfully. If false, the - * client should cease sending additional requests immediately, as - * they will likely fail. If true, the client is allowed to continue - * to put the server in a proper state. But, it should stop and heed - * the backoff as soon as possible. - * - * onNetworkError - This is called for every network error that is - * encountered. - * - * This callback receives the following arguments: - * - * (StorageServiceClient) Client that encountered the network error. - * (StorageServiceRequest) Request that encountered the error. - * (Error) Error passed in to RESTRequest's onComplete handler. It has - * a result property, which is a Components.Results enumeration. - * - * onQuotaRemaining - This is called if any request sees updated quota - * information from the server. This provides an update mechanism so - * listeners can immediately find out quota changes as soon as they - * are made. - * - * This callback receives the following arguments: - * - * (StorageServiceClient) Client that encountered the quota change. - * (StorageServiceRequest) Request that received the quota change. - * (number) Integer number of kilobytes remaining for the user. - */ - addListener: function addListener(listener) { - if (!listener) { - throw new Error("listener argument must be an object."); - } - - if (this._listeners.indexOf(listener) != -1) { - return; - } - - this._listeners.push(listener); - }, - - /** - * Remove a previously-installed listener. - */ - removeListener: function removeListener(listener) { - this._listeners = this._listeners.filter(function(a) { - return a != listener; - }); - }, - - /** - * Invoke listeners for a specific event. - * - * @param name - * (string) The name of the listener to invoke. - * @param args - * (array) Arguments to pass to listener functions. - */ - runListeners: function runListeners(name, ...args) { - for (let listener of this._listeners) { - try { - if (name in listener) { - listener[name].apply(listener, args); - } - } catch (ex) { - this._log.warn("Listener threw an exception during " + name + ": " - + ex); - } - } - }, - - //----------------------------- - // Information/Metadata APIs | - //----------------------------- - - /** - * Obtain a request that fetches collection info. - * - * On successful response, the result is placed in the resultObj property - * of the request object. - * - * The result value is a map of strings to numbers. The string keys represent - * collection names. The number values are integer milliseconds since Unix - * epoch that hte collection was last modified. - * - * This request can be made conditional by defining `locallyModifiedVersion` - * on the returned object to the last known version on the client. - * - * Example Usage: - * - * let request = client.getCollectionInfo(); - * request.dispatch(function onComplete(error, request) { - * if (!error) { - * return; - * } - * - * for (let [collection, milliseconds] in Iterator(this.resultObj)) { - * // ... - * } - * }); - */ - getCollectionInfo: function getCollectionInfo() { - return this._getJSONGETRequest("info/collections"); - }, - - /** - * Fetch quota information. - * - * The result in the callback upon success is a map containing quota - * metadata. It will have the following keys: - * - * usage - Number of bytes currently utilized. - * quota - Number of bytes available to account. - * - * The request can be made conditional by populating `locallyModifiedVersion` - * on the returned request instance with the most recently known version of - * server data. - */ - getQuota: function getQuota() { - return this._getJSONGETRequest("info/quota"); - }, - - /** - * Fetch information on how much data each collection uses. - * - * The result on success is a map of strings to numbers. The string keys - * are collection names. The values are numbers corresponding to the number - * of kilobytes used by that collection. - */ - getCollectionUsage: function getCollectionUsage() { - return this._getJSONGETRequest("info/collection_usage"); - }, - - /** - * Fetch the number of records in each collection. - * - * The result on success is a map of strings to numbers. The string keys are - * collection names. The values are numbers corresponding to the integer - * number of items in that collection. - */ - getCollectionCounts: function getCollectionCounts() { - return this._getJSONGETRequest("info/collection_counts"); - }, - - //-------------------------- - // Collection Interaction | - // ------------------------- - - /** - * Obtain a request to fetch collection information. - * - * The returned request instance is a StorageCollectionGetRequest instance. - * This is a sub-type of StorageServiceRequest and offers a number of setters - * to control how the request is performed. See the documentation for that - * type for more. - * - * The request can be made conditional by setting `locallyModifiedVersion` - * on the returned request instance. - * - * Example usage: - * - * let request = client.getCollection("testcoll"); - * - * // Obtain full BSOs rather than just IDs. - * request.full = true; - * - * // Only obtain BSOs modified in the last minute. - * request.newer = Date.now() - 60000; - * - * // Install handler. - * request.handler = { - * onBSORecord: function onBSORecord(request, bso) { - * let id = bso.id; - * let payload = bso.payload; - * - * // Do something with BSO. - * }, - * - * onComplete: function onComplete(error, req) { - * if (error) { - * // Handle error. - * return; - * } - * - * // Your onBSORecord handler has processed everything. Now is where - * // you typically signal that everything has been processed and to move - * // on. - * } - * }; - * - * request.dispatch(); - * - * @param collection - * (string) Name of collection to operate on. - */ - getCollection: function getCollection(collection) { - if (!collection) { - throw new Error("collection argument must be defined."); - } - - let uri = this._baseURI + "storage/" + collection; - - let request = this._getRequest(uri, "GET", { - accept: "application/json", - allowIfModified: true, - requestType: StorageCollectionGetRequest - }); - - return request; - }, - - /** - * Fetch a single Basic Storage Object (BSO). - * - * On success, the BSO may be available in the resultObj property of the - * request as a BasicStorageObject instance. - * - * The request can be made conditional by setting `locallyModifiedVersion` - * on the returned request instance.* - * - * Example usage: - * - * let request = client.getBSO("meta", "global"); - * request.dispatch(function onComplete(error, request) { - * if (!error) { - * return; - * } - * - * if (request.notModified) { - * return; - * } - * - * let bso = request.bso; - * let payload = bso.payload; - * - * ... - * }; - * - * @param collection - * (string) Collection to fetch from - * @param id - * (string) ID of BSO to retrieve. - * @param type - * (constructor) Constructor to call to create returned object. This - * is optional and defaults to BasicStorageObject. - */ - getBSO: function fetchBSO(collection, id, type=BasicStorageObject) { - if (!collection) { - throw new Error("collection argument must be defined."); - } - - if (!id) { - throw new Error("id argument must be defined."); - } - - let uri = this._baseURI + "storage/" + collection + "/" + id; - - return this._getRequest(uri, "GET", { - accept: "application/json", - allowIfModified: true, - completeParser: function completeParser(response) { - let record = new type(id, collection); - record.deserialize(response.body); - - return record; - }, - }); - }, - - /** - * Add or update a BSO in a collection. - * - * To make the request conditional (i.e. don't allow server changes if the - * server has a newer version), set request.locallyModifiedVersion to the - * last known version of the BSO. While this could be done automatically by - * this API, it is intentionally omitted because there are valid conditions - * where a client may wish to forcefully update the server. - * - * If a conditional request fails because the server has newer data, the - * StorageServiceRequestError passed to the callback will have the - * `serverModified` property set to true. - * - * Example usage: - * - * let bso = new BasicStorageObject("foo", "coll"); - * bso.payload = "payload"; - * bso.modified = Date.now(); - * - * let request = client.setBSO(bso); - * request.locallyModifiedVersion = bso.modified; - * - * request.dispatch(function onComplete(error, req) { - * if (error) { - * if (error.serverModified) { - * // Handle conditional set failure. - * return; - * } - * - * // Handle other errors. - * return; - * } - * - * // Record that set worked. - * }); - * - * @param bso - * (BasicStorageObject) BSO to upload. The BSO instance must have the - * `collection` and `id` properties defined. - */ - setBSO: function setBSO(bso) { - if (!bso) { - throw new Error("bso argument must be defined."); - } - - if (!bso.collection) { - throw new Error("BSO instance does not have collection defined."); - } - - if (!bso.id) { - throw new Error("BSO instance does not have ID defined."); - } - - let uri = this._baseURI + "storage/" + bso.collection + "/" + bso.id; - let request = this._getRequest(uri, "PUT", { - contentType: "application/json", - allowIfUnmodified: true, - data: JSON.stringify(bso), - }); - - return request; - }, - - /** - * Add or update multiple BSOs. - * - * This is roughly equivalent to calling setBSO multiple times except it is - * much more effecient because there is only 1 round trip to the server. - * - * The request can be made conditional by setting `locallyModifiedVersion` - * on the returned request instance. - * - * This function returns a StorageCollectionSetRequest instance. This type - * has additional functions and properties specific to this operation. See - * its documentation for more. - * - * Most consumers interested in submitting multiple BSOs to the server will - * want to use `setBSOsBatching` instead. That API intelligently splits up - * requests as necessary, etc. - * - * Example usage: - * - * let request = client.setBSOs("collection0"); - * let bso0 = new BasicStorageObject("id0"); - * bso0.payload = "payload0"; - * - * let bso1 = new BasicStorageObject("id1"); - * bso1.payload = "payload1"; - * - * request.addBSO(bso0); - * request.addBSO(bso1); - * - * request.dispatch(function onComplete(error, req) { - * if (error) { - * // Handle error. - * return; - * } - * - * let successful = req.successfulIDs; - * let failed = req.failed; - * - * // Do additional processing. - * }); - * - * @param collection - * (string) Collection to operate on. - * @return - * (StorageCollectionSetRequest) Request instance. - */ - setBSOs: function setBSOs(collection) { - if (!collection) { - throw new Error("collection argument must be defined."); - } - - let uri = this._baseURI + "storage/" + collection; - let request = this._getRequest(uri, "POST", { - requestType: StorageCollectionSetRequest, - contentType: "application/newlines", - accept: "application/json", - allowIfUnmodified: true, - }); - - return request; - }, - - /** - * This is a batching variant of setBSOs. - * - * Whereas `setBSOs` is a 1:1 mapping between function calls and HTTP - * requests issued, this one is a 1:N mapping. It will intelligently break - * up outgoing BSOs into multiple requests so size limits, etc aren't - * exceeded. - * - * Please see the documentation for `StorageCollectionBatchedSet` for - * usage info. - * - * @param collection - * (string) Collection to operate on. - * @return - * (StorageCollectionBatchedSet) Batched set instance. - */ - setBSOsBatching: function setBSOsBatching(collection) { - if (!collection) { - throw new Error("collection argument must be defined."); - } - - return new StorageCollectionBatchedSet(this, collection); - }, - - /** - * Deletes a single BSO from a collection. - * - * The request can be made conditional by setting `locallyModifiedVersion` - * on the returned request instance. - * - * @param collection - * (string) Collection to operate on. - * @param id - * (string) ID of BSO to delete. - */ - deleteBSO: function deleteBSO(collection, id) { - if (!collection) { - throw new Error("collection argument must be defined."); - } - - if (!id) { - throw new Error("id argument must be defined."); - } - - let uri = this._baseURI + "storage/" + collection + "/" + id; - return this._getRequest(uri, "DELETE", { - allowIfUnmodified: true, - }); - }, - - /** - * Delete multiple BSOs from a specific collection. - * - * This is functional equivalent to calling deleteBSO() for every ID but - * much more efficient because it only results in 1 round trip to the server. - * - * The request can be made conditional by setting `locallyModifiedVersion` - * on the returned request instance. - * - * If the number of BSOs to delete is potentially large, it is preferred to - * use `deleteBSOsBatching`. That API automatically splits the operation into - * multiple requests so server limits aren't exceeded. - * - * @param collection - * (string) Name of collection to delete BSOs from. - * @param ids - * (iterable of strings) Set of BSO IDs to delete. - */ - deleteBSOs: function deleteBSOs(collection, ids) { - // In theory we should URL encode. However, IDs are supposed to be URL - // safe. If we get garbage in, we'll get garbage out and the server will - // reject it. - let s = ids.join(","); - - let uri = this._baseURI + "storage/" + collection + "?ids=" + s; - - return this._getRequest(uri, "DELETE", { - allowIfUnmodified: true, - }); - }, - - /** - * Bulk deletion of BSOs with no size limit. - * - * This allows a large amount of BSOs to be deleted easily. It will formulate - * multiple `deleteBSOs` queries so the client does not exceed server limits. - * - * @param collection - * (string) Name of collection to delete BSOs from. - * @return StorageCollectionBatchedDelete - */ - deleteBSOsBatching: function deleteBSOsBatching(collection) { - if (!collection) { - throw new Error("collection argument must be defined."); - } - - return new StorageCollectionBatchedDelete(this, collection); - }, - - /** - * Deletes a single collection from the server. - * - * The request can be made conditional by setting `locallyModifiedVersion` - * on the returned request instance. - * - * @param collection - * (string) Name of collection to delete. - */ - deleteCollection: function deleteCollection(collection) { - let uri = this._baseURI + "storage/" + collection; - - return this._getRequest(uri, "DELETE", { - allowIfUnmodified: true - }); - }, - - /** - * Deletes all collections data from the server. - */ - deleteCollections: function deleteCollections() { - let uri = this._baseURI + "storage"; - - return this._getRequest(uri, "DELETE", {}); - }, - - /** - * Helper that wraps _getRequest for GET requests that return JSON. - */ - _getJSONGETRequest: function _getJSONGETRequest(path) { - let uri = this._baseURI + path; - - return this._getRequest(uri, "GET", { - accept: "application/json", - allowIfModified: true, - completeParser: this._jsonResponseParser, - }); - }, - - /** - * Common logic for obtaining an HTTP request instance. - * - * @param uri - * (string) URI to request. - * @param method - * (string) HTTP method to issue. - * @param options - * (object) Additional options to control request and response - * handling. Keys influencing behavior are: - * - * completeParser - Function that parses a HTTP response body into a - * value. This function receives the RESTResponse object and - * returns a value that is added to a StorageResponse instance. - * If the response cannot be parsed or is invalid, this function - * should throw an exception. - * - * data - Data to be sent in HTTP request body. - * - * accept - Value for Accept request header. - * - * contentType - Value for Content-Type request header. - * - * requestType - Function constructor for request type to initialize. - * Defaults to StorageServiceRequest. - * - * allowIfModified - Whether to populate X-If-Modified-Since if the - * request contains a locallyModifiedVersion. - * - * allowIfUnmodified - Whether to populate X-If-Unmodified-Since if - * the request contains a locallyModifiedVersion. - */ - _getRequest: function _getRequest(uri, method, options) { - if (!options.requestType) { - options.requestType = StorageServiceRequest; - } - - let request = new RESTRequest(uri); - - if (Prefs.get("sendVersionInfo", true)) { - let ua = this.userAgent + Prefs.get("client.type", "desktop"); - request.setHeader("user-agent", ua); - } - - if (options.accept) { - request.setHeader("accept", options.accept); - } - - if (options.contentType) { - request.setHeader("content-type", options.contentType); - } - - let result = new options.requestType(); - result._request = request; - result._method = method; - result._client = this; - result._data = options.data; - - if (options.completeParser) { - result._completeParser = options.completeParser; - } - - result._allowIfModified = !!options.allowIfModified; - result._allowIfUnmodified = !!options.allowIfUnmodified; - - return result; - }, - - _jsonResponseParser: function _jsonResponseParser(response) { - let ct = response.headers["content-type"]; - if (!ct) { - throw new Error("No Content-Type response header! Misbehaving server!"); - } - - if (ct != "application/json" && ct.indexOf("application/json;") != 0) { - throw new Error("Non-JSON media type: " + ct); - } - - return JSON.parse(response.body); - }, -}; diff --git a/services/common/tests/unit/test_load_modules.js b/services/common/tests/unit/test_load_modules.js index ca3b4922c76..a84eac31d3a 100644 --- a/services/common/tests/unit/test_load_modules.js +++ b/services/common/tests/unit/test_load_modules.js @@ -6,7 +6,6 @@ const modules = [ "bagheeraclient.js", "logmanager.js", "rest.js", - "storageservice.js", "stringbundle.js", "tokenserverclient.js", "utils.js", diff --git a/services/common/tests/unit/test_storageservice_bso.js b/services/common/tests/unit/test_storageservice_bso.js deleted file mode 100644 index 05b9bbecda7..00000000000 --- a/services/common/tests/unit/test_storageservice_bso.js +++ /dev/null @@ -1,124 +0,0 @@ -/* Any copyright is dedicated to the Public Domain. - * http://creativecommons.org/publicdomain/zero/1.0/ */ - -Cu.import("resource://services-common/storageservice.js"); - -function run_test() { - initTestLogging("Trace"); - - run_next_test(); -} - -add_test(function test_bso_constructor() { - _("Ensure created BSO instances are initialized properly."); - - let bso = new BasicStorageObject(); - do_check_eq(bso.id, null); - do_check_eq(bso.collection, null); - do_check_attribute_count(bso.data, 0); - do_check_eq(bso.payload, null); - do_check_eq(bso.modified, null); - do_check_eq(bso.sortindex, 0); - do_check_eq(bso.ttl, null); - - bso = new BasicStorageObject("foobar"); - do_check_eq(bso.id, "foobar"); - do_check_eq(bso.collection, null); - do_check_attribute_count(bso.data, 0); - - bso = new BasicStorageObject("foo", "coll"); - do_check_eq(bso.id, "foo"); - do_check_eq(bso.collection, "coll"); - do_check_attribute_count(bso.data, 0); - - run_next_test(); -}); - -add_test(function test_bso_attributes() { - _("Ensure attribute getters and setters work."); - - let bso = new BasicStorageObject("foobar"); - bso.payload = "pay"; - do_check_eq(bso.payload, "pay"); - - bso.modified = 35423; - do_check_eq(bso.modified, 35423); - - bso.sortindex = 10; - do_check_eq(bso.sortindex, 10); - - bso.ttl = 60; - do_check_eq(bso.ttl, 60); - - run_next_test(); -}); - -add_test(function test_bso_deserialize() { - _("Ensure that deserialize() works."); - - _("A simple working test."); - let json = '{"id": "foobar", "payload": "pay", "modified": 1223145532}'; - let bso = new BasicStorageObject(); - bso.deserialize(json); - do_check_neq(bso, null); - do_check_eq(bso.id, "foobar"); - do_check_eq(bso.payload, "pay"); - do_check_eq(bso.modified, 1223145532); - - _("Invalid JSON."); - json = '{id: "foobar}'; - bso = new BasicStorageObject(); - try { - bso.deserialize(json); - do_check_true(false); - } catch (ex) { - do_check_eq(ex.name, "SyntaxError"); - } - - _("Invalid key in JSON."); - json = '{"id": "foo", "payload": "pay", "BADKEY": "irrelevant"}'; - bso = new BasicStorageObject(); - try { - bso.deserialize(json); - do_check_true(false); - } catch (ex) { - do_check_eq(ex.name, "Error"); - do_check_eq(ex.message.indexOf("Invalid key"), 0); - } - - _("Loading native JS objects works."); - bso = new BasicStorageObject(); - bso.deserialize({id: "foo", payload: "pay"}); - do_check_neq(bso, null); - do_check_eq(bso.id, "foo"); - do_check_eq(bso.payload, "pay"); - - _("Passing invalid type is caught."); - bso = new BasicStorageObject(); - try { - bso.deserialize(["foo", "bar"]); - do_check_true(false); - } catch (ex) { - do_check_eq(ex.name, "Error"); - } - - run_next_test(); -}); - -add_test(function test_bso_toJSON() { - _("Ensure JSON serialization works."); - - let bso = new BasicStorageObject(); - do_check_attribute_count(bso.toJSON(), 0); - - bso.id = "foo"; - bso.payload = "pay"; - let json = bso.toJSON(); - let original = json; - - do_check_attribute_count(original, 2); - do_check_eq(original.id, "foo"); - do_check_eq(original.payload, "pay"); - - run_next_test(); -}); diff --git a/services/common/tests/unit/test_storageservice_client.js b/services/common/tests/unit/test_storageservice_client.js deleted file mode 100644 index a0cdaae9c63..00000000000 --- a/services/common/tests/unit/test_storageservice_client.js +++ /dev/null @@ -1,1381 +0,0 @@ -/* Any copyright is dedicated to the Public Domain. - * http://creativecommons.org/publicdomain/zero/1.0/ */ - -Cu.import("resource://services-common/storageservice.js"); -Cu.import("resource://testing-common/services/common/storageserver.js"); - -function run_test() { - initTestLogging("Trace"); - - run_next_test(); -} - -function getRandomUser() { - return "" + (Math.floor(Math.random() * 100000) + 1); -} - -function getEmptyServer(user=getRandomUser(), password="password") { - let users = {}; - users[user] = password; - - return storageServerForUsers(users, { - meta: {}, - clients: {}, - crypto: {}, - }); -} - -function getClient(server, user=getRandomUser(), password="password") { - let identity = server.server.identity; - let url = identity.primaryScheme + "://" + identity.primaryHost + ":" + - identity.primaryPort + "/2.0/" + user; - let client = new StorageServiceClient(url); - client.addListener({ - onDispatch: function onDispatch(request) { - let up = user + ":" + password; - request.request.setHeader("authorization", "Basic " + btoa(up)); - } - }); - - return client; -} - -function getServerAndClient(user=getRandomUser(), password="password") { - let server = getEmptyServer(user, password); - let client = getClient(server, user, password); - - return [server, client, user, password]; -} - -add_test(function test_auth_failure_listener() { - _("Ensure the onAuthFailure listener is invoked."); - - let server = getEmptyServer(); - let client = getClient(server, "324", "INVALID"); - client.addListener({ - onAuthFailure: function onAuthFailure(client, request) { - _("onAuthFailure"); - server.stop(run_next_test); - } - }); - - let request = client.getCollectionInfo(); - request.dispatch(); -}); - -add_test(function test_duplicate_listeners() { - _("Ensure that duplicate listeners aren't installed multiple times."); - - let server = getEmptyServer(); - let client = getClient(server, "1234567", "BAD_PASSWORD"); - - let invokeCount = 0; - let listener = { - onAuthFailure: function onAuthFailure() { - invokeCount++; - } - }; - - client.addListener(listener); - // No error expected. - client.addListener(listener); - - let request = client.getCollectionInfo(); - request.dispatch(function onComplete() { - do_check_eq(invokeCount, 1); - - server.stop(run_next_test); - }); -}); - -add_test(function test_handler_object() { - _("Ensure that installed handlers get their callbacks invoked."); - - let [server, client] = getServerAndClient(); - - let onCompleteCount = 0; - let onDispatchCount = 0; - - let handler = { - onComplete: function onComplete() { - onCompleteCount++; - - do_check_eq(onDispatchCount, 1); - do_check_eq(onCompleteCount, 1); - - server.stop(run_next_test); - }, - - onDispatch: function onDispatch() { - onDispatchCount++; - }, - }; - let request = client.getCollectionInfo(); - request.handler = handler; - request.dispatch(); -}); - -add_test(function test_info_collections() { - _("Ensure requests to /info/collections work as expected."); - - let [server, client] = getServerAndClient(); - - let request = client.getCollectionInfo(); - request.dispatch(function onComplete(error, req) { - do_check_null(error); - do_check_eq("object", typeof req.resultObj); - do_check_attribute_count(req.resultObj, 3); - do_check_true("meta" in req.resultObj); - - server.stop(run_next_test); - }); -}); - -add_test(function test_info_collections_conditional_not_modified() { - _("Ensure conditional getCollectionInfo requests work."); - - let [server, client, username] = getServerAndClient(); - let user = server.user(username); - - let now = Date.now(); - - user.createCollection("testcoll", { - foo: new ServerBSO("foo", "payload", now) - }); - - let request = client.getCollectionInfo(); - request.locallyModifiedVersion = now + 10; - request.dispatch(function onComplete(error, req) { - do_check_null(error); - do_check_true(req.notModified); - - server.stop(run_next_test); - }); -}); - -add_test(function test_info_collections_conditional_modified() { - _("Ensure conditional getCollectionInfo requests work."); - - let [server, client, username] = getServerAndClient(); - let user = server.user(username); - - let now = Date.now(); - - user.createCollection("testcoll", { - foo: new ServerBSO("foo", "payload", now) - }); - - let request = client.getCollectionInfo(); - request.locallyModifiedVersion = now - 10; - request.dispatch(function onComplete(error, req) { - do_check_null(error); - do_check_false(req.notModified); - - server.stop(run_next_test); - }); -}); - -add_test(function test_get_quota() { - _("Ensure quota requests work."); - - let [server, client] = getServerAndClient(); - - let request = client.getQuota(); - request.dispatch(function onComplete(error, req) { - do_check_null(error); - - do_check_eq(req.resultObj.quota, 1048576); - - server.stop(run_next_test); - }); -}); - -add_test(function test_get_quota_conditional_not_modified() { - _("Ensure conditional getQuota requests work."); - - let [server, client, username] = getServerAndClient(); - let user = server.user(username); - - let now = Date.now(); - - user.createCollection("testcoll", { - foo: new ServerBSO("foo", "payload", now) - }); - - let request = client.getQuota(); - request.locallyModifiedVersion = now + 10; - request.dispatch(function onComplete(error, req) { - do_check_null(error); - do_check_true(req.notModified); - - server.stop(run_next_test); - }); -}); - -add_test(function test_get_quota_conditional_modified() { - _("Ensure conditional getQuota requests work."); - - let [server, client, username] = getServerAndClient(); - let user = server.user(username); - - let now = Date.now(); - - user.createCollection("testcoll", { - foo: new ServerBSO("foo", "payload", now) - }); - - let request = client.getQuota(); - request.locallyModifiedVersion = now - 10; - request.dispatch(function onComplete(error, req) { - do_check_null(error); - do_check_false(req.notModified); - - server.stop(run_next_test); - }); -}); - -add_test(function test_get_collection_usage() { - _("Ensure info/collection_usage requests work."); - - let [server, client, username] = getServerAndClient(); - let user = server.user(username); - - user.createCollection("testcoll", { - abc123: new ServerBSO("abc123", "payload", Date.now()) - }); - - let request = client.getCollectionUsage(); - request.dispatch(function onComplete(error, req) { - do_check_null(error); - - let usage = req.resultObj; - do_check_true("testcoll" in usage); - do_check_eq(usage.testcoll, "payload".length); - - server.stop(run_next_test); - }); -}); - -add_test(function test_get_usage_conditional_not_modified() { - _("Ensure conditional getCollectionUsage requests work."); - - let [server, client, username] = getServerAndClient(); - let user = server.user(username); - - let now = Date.now(); - - user.createCollection("testcoll", { - foo: new ServerBSO("foo", "payload", now) - }); - - let request = client.getCollectionUsage(); - request.locallyModifiedVersion = now + 10; - request.dispatch(function onComplete(error, req) { - do_check_null(error); - do_check_true(req.notModified); - - server.stop(run_next_test); - }); -}); - -add_test(function test_get_usage_conditional_modified() { - _("Ensure conditional getCollectionUsage requests work."); - - let [server, client, username] = getServerAndClient(); - let user = server.user(username); - - let now = Date.now(); - - user.createCollection("testcoll", { - foo: new ServerBSO("foo", "payload", now) - }); - - let request = client.getCollectionUsage(); - request.locallyModifiedVersion = now - 10; - request.dispatch(function onComplete(error, req) { - do_check_null(error); - do_check_false(req.notModified); - - server.stop(run_next_test); - }); -}); - -add_test(function test_get_collection_counts() { - _("Ensure info/collection_counts requests work."); - - let [server, client, username] = getServerAndClient(); - let user = server.user(username); - - user.createCollection("testcoll", { - foo: new ServerBSO("foo", "payload0", Date.now()), - bar: new ServerBSO("bar", "payload1", Date.now()) - }); - - let request = client.getCollectionCounts(); - request.dispatch(function onComplete(error, req) { - do_check_null(error); - - let counts = req.resultObj; - do_check_true("testcoll" in counts); - do_check_eq(counts.testcoll, 2); - - server.stop(run_next_test); - }); -}); - -add_test(function test_get_counts_conditional_not_modified() { - _("Ensure conditional getCollectionCounts requests work."); - - let [server, client, username] = getServerAndClient(); - let user = server.user(username); - - let now = Date.now(); - - user.createCollection("testcoll", { - foo: new ServerBSO("foo", "payload", now) - }); - - let request = client.getCollectionCounts(); - request.locallyModifiedVersion = now + 10; - request.dispatch(function onComplete(error, req) { - do_check_null(error); - do_check_true(req.notModified); - - server.stop(run_next_test); - }); -}); - -add_test(function test_get_counts_conditional_modified() { - _("Ensure conditional getCollectionCounts requests work."); - - let [server, client, username] = getServerAndClient(); - let user = server.user(username); - - let now = Date.now(); - - user.createCollection("testcoll", { - foo: new ServerBSO("foo", "payload", now) - }); - - let request = client.getCollectionCounts(); - request.locallyModifiedVersion = now - 10; - request.dispatch(function onComplete(error, req) { - do_check_null(error); - do_check_false(req.notModified); - - server.stop(run_next_test); - }); -}); - -add_test(function test_get_collection_simple() { - _("Ensure basic collection retrieval works."); - - let [server, client, username] = getServerAndClient(); - let user = server.user(username); - - user.createCollection("testcoll", { - foo: new ServerBSO("foo", "payload0", Date.now()), - bar: new ServerBSO("bar", "payload1", Date.now()) - }); - - let request = client.getCollection("testcoll"); - let bsos = []; - request.handler = { - onBSORecord: function onBSORecord(request, bso) { - bsos.push(bso); - }, - - onComplete: function onComplete(error, request) { - do_check_null(error); - - do_check_eq(bsos.length, 2); - do_check_eq(bsos[0], "foo"); - do_check_eq(bsos[1], "bar"); - - server.stop(run_next_test); - } - }; - request.dispatch(); -}); - -add_test(function test_get_collection_conditional_not_modified() { - _("Ensure conditional requests with no new data to getCollection work."); - - let [server, client, username] = getServerAndClient(); - let user = server.user(username); - - let now = Date.now(); - - user.createCollection("testcoll", { - foo: new ServerBSO("foo", "payload0", now) - }); - - let request = client.getCollection("testcoll"); - request.locallyModifiedVersion = now + 1; - - request.dispatch(function onComplete(error, req) { - do_check_null(error); - - do_check_true(req.notModified); - - server.stop(run_next_test); - }); -}); - -add_test(function test_get_collection_conditional_modified() { - _("Ensure conditional requests with new data to getCollection work."); - - let [server, client, username] = getServerAndClient(); - let user = server.user(username); - - let now = Date.now(); - - user.createCollection("testcoll", { - foo: new ServerBSO("foo", "payload0", now) - }); - - let request = client.getCollection("testcoll"); - request.locallyModifiedVersion = now - 1; - - let bsoCount = 0; - request.handler = { - onBSORecord: function onBSORecord() { - bsoCount++; - }, - - onComplete: function onComplete(error, req) { - do_check_null(error); - - do_check_false(req.notModified); - do_check_eq(bsoCount, 1); - - server.stop(run_next_test); - } - }; - request.dispatch(); -}); - -// This is effectively a sanity test for query string generation. -add_test(function test_get_collection_newer() { - _("Ensure query string for newer and full work together."); - - let [server, client, username] = getServerAndClient(); - - let date0 = Date.now(); - let date1 = date0 + 500; - - let user = server.user(username); - user.createCollection("testcoll", { - foo: new ServerBSO("foo", "payload0", date0), - bar: new ServerBSO("bar", "payload1", date1) - }); - - let request = client.getCollection("testcoll"); - request.full = true; - request.newer = date0; - - let bsos = []; - request.handler = { - onBSORecord: function onBSORecord(request, bso) { - bsos.push(bso); - }, - - onComplete: function onComplete(error, req) { - do_check_null(error); - - do_check_eq(bsos.length, 1); - let bso = bsos[0]; - - do_check_eq(bso.id, "bar"); - do_check_eq(bso.payload, "payload1"); - - server.stop(run_next_test); - } - }; - request.dispatch(); -}); - -add_test(function test_get_bso() { - _("Ensure that simple BSO fetches work."); - - let [server, client, username] = getServerAndClient(); - - server.createCollection(username, "testcoll", { - abc123: new ServerBSO("abc123", "payload", Date.now()) - }); - - let request = client.getBSO("testcoll", "abc123"); - request.dispatch(function(error, req) { - do_check_null(error); - do_check_true(req.resultObj instanceof BasicStorageObject); - - let bso = req.resultObj; - do_check_eq(bso.id, "abc123"); - do_check_eq(bso.payload, "payload"); - - server.stop(run_next_test); - }); -}); - -add_test(function test_bso_conditional() { - _("Ensure conditional requests for an individual BSO work."); - - let [server, client, username] = getServerAndClient(); - - let user = server.user(username); - let now = Date.now(); - user.createCollection("testcoll", { - foo: new ServerBSO("foo", "payload", now) - }); - - let request = client.getBSO("testcoll", "foo"); - request.locallyModifiedVersion = now; - - request.dispatch(function onComplete(error, req) { - do_check_null(error); - do_check_true(req.notModified); - - server.stop(run_next_test); - }); -}); - -add_test(function test_set_bso() { - _("Ensure simple BSO PUT works."); - - let [server, client] = getServerAndClient(); - - let id = "mnas08h3f3r2351"; - - let bso = new BasicStorageObject(id, "testcoll"); - bso.payload = "my test payload"; - - let request = client.setBSO(bso); - request.dispatch(function(error, req) { - do_check_eq(error, null); - do_check_eq(req.resultObj, null); - - server.stop(run_next_test); - }); -}); - - -add_test(function test_set_bso_conditional() { - _("Ensure conditional setting a BSO is properly rejected."); - - let [server, client, username] = getServerAndClient(); - let user = server.user(username); - - let now = Date.now(); - user.createCollection("testcoll", { - foo: new ServerBSO("foo", "payload0", now + 1000) - }); - - // Should get an mtime newer than server's. - let bso = new BasicStorageObject("foo", "testcoll"); - bso.payload = "payload1"; - - let request = client.setBSO(bso); - request.locallyModifiedVersion = now; - request.dispatch(function onComplete(error, req) { - do_check_true(error instanceof StorageServiceRequestError); - do_check_true(error.serverModified); - - server.stop(run_next_test); - }); -}); - -add_test(function test_set_bso_argument_errors() { - _("Ensure BSO set detects invalid arguments."); - - let server = getEmptyServer(); - let bso = new BasicStorageObject(); - let client = getClient(server); - - let threw = false; - try { - client.setBSO(bso); - } catch (ex) { - threw = true; - do_check_eq(ex.name, "Error"); - do_check_neq(ex.message.indexOf("does not have collection defined"), -1); - } finally { - do_check_true(threw); - threw = false; - } - - bso = new BasicStorageObject("id"); - try { - client.setBSO(bso); - } catch (ex) { - threw = true; - do_check_eq(ex.name, "Error"); - do_check_neq(ex.message.indexOf("does not have collection defined"), -1); - } finally { - do_check_true(threw); - threw = false; - } - - bso = new BasicStorageObject(null, "coll"); - try { - client.setBSO(bso); - } catch (ex) { - threw = true; - do_check_eq(ex.name, "Error"); - do_check_neq(ex.message.indexOf("does not have ID defined"), -1); - } finally { - do_check_true(threw); - threw = false; - } - - server.stop(run_next_test); -}); - -add_test(function test_set_bsos_simple() { - _("Ensure setBSOs with basic options works."); - - let [server, client, username] = getServerAndClient(); - let user = server.user(username); - - let bso0 = new BasicStorageObject("foo"); - bso0.payload = "payload0"; - - let bso1 = new BasicStorageObject("bar"); - bso1.payload = "payload1"; - - let request = client.setBSOs("testcollection"); - request.addBSO(bso0); - request.addBSO(bso1); - - request.dispatch(function onComplete(error, req) { - do_check_null(error); - - let successful = req.successfulIDs; - do_check_eq(successful.length, 2); - do_check_eq(successful.indexOf(bso0.id), 0); - do_check_true(successful.indexOf(bso1.id), 1); - - server.stop(run_next_test); - }); -}); - -add_test(function test_set_bsos_invalid_bso() { - _("Ensure that adding an invalid BSO throws."); - - let server = getEmptyServer(); - let client = getClient(server); - let request = client.setBSOs("testcoll"); - - let threw = false; - - // Empty argument is invalid. - try { - request.addBSO(null); - } catch (ex) { - threw = true; - } finally { - do_check_true(threw); - threw = false; - } - - try { - let bso = new BasicStorageObject(); - request.addBSO(bso); - } catch (ex) { - threw = true; - } finally { - do_check_true(threw); - threw = false; - } - - server.stop(run_next_test); -}); - -add_test(function test_set_bsos_newline() { - _("Ensure that newlines in BSO payloads are formatted properly."); - - let [server, client, username] = getServerAndClient(); - let user = server.user(username); - - let request = client.setBSOs("testcoll"); - - let bso0 = new BasicStorageObject("bso0"); - bso0.payload = "hello\nworld"; - request.addBSO(bso0); - - let bso1 = new BasicStorageObject("bso1"); - bso1.payload = "foobar"; - request.addBSO(bso1); - - request.dispatch(function onComplete(error, request) { - do_check_null(error); - do_check_eq(request.successfulIDs.length, 2); - - let coll = user.collection("testcoll"); - do_check_eq(coll.bso("bso0").payload, bso0.payload); - do_check_eq(coll.bso("bso1").payload, bso1.payload); - - server.stop(run_next_test); - }); -}); - -add_test(function test_delete_bso_simple() { - _("Ensure deletion of individual BSOs works."); - - let [server, client, username] = getServerAndClient(); - - let user = server.user(username); - let coll = user.createCollection("testcoll", { - foo: new ServerBSO("foo", "payload0", Date.now()), - bar: new ServerBSO("bar", "payload1", Date.now()) - }); - - let request = client.deleteBSO("testcoll", "foo"); - request.dispatch(function onComplete(error, req) { - do_check_null(error); - do_check_eq(req.statusCode, 204); - - do_check_eq(coll.count(), 1); - - server.stop(run_next_test); - }); -}); - -add_test(function test_delete_bso_conditional_failed() { - _("Ensure deletion of an individual BSO with older modification fails."); - - let [server, client, username] = getServerAndClient(); - let user = server.user(username); - - let now = Date.now(); - user.createCollection("testcoll", { - foo: new ServerBSO("foo", "payload0", now) - }); - - let request = client.deleteBSO("testcoll", "foo"); - request.locallyModifiedVersion = now - 10; - - request.dispatch(function onComplete(error, req) { - do_check_true(error instanceof StorageServiceRequestError); - do_check_true(error.serverModified); - - server.stop(run_next_test); - }); -}); - -add_test(function test_delete_bso_conditional_success() { - _("Ensure deletion of an individual BSO with newer modification works."); - - let [server, client, username] = getServerAndClient(); - let user = server.user(username); - - let now = Date.now(); - user.createCollection("testcoll", { - foo: new ServerBSO("foo", "payload0", now) - }); - - let request = client.deleteBSO("testcoll", "foo"); - request.locallyModifiedVersion = now; - - request.dispatch(function onComplete(error, req) { - do_check_null(error); - do_check_eq(req.statusCode, 204); - - server.stop(run_next_test); - }); -}); - -add_test(function test_delete_bsos_simple() { - _("Ensure deletion of multiple BSOs works."); - - let [server, client, username] = getServerAndClient(); - let user = server.user(username); - - let coll = user.createCollection("testcoll", { - foo: new ServerBSO("foo", "payload0", Date.now()), - bar: new ServerBSO("bar", "payload1", Date.now()), - baz: new ServerBSO("baz", "payload2", Date.now()) - }); - - let request = client.deleteBSOs("testcoll", ["foo", "baz"]); - request.dispatch(function onComplete(error, req) { - do_check_null(error); - do_check_eq(req.statusCode, 204); - - do_check_eq(coll.count(), 1); - - server.stop(run_next_test); - }); -}); - -add_test(function test_delete_bsos_conditional_failed() { - _("Ensure deletion of BSOs with server modifications fails."); - - let [server, client, username] = getServerAndClient(); - let user = server.user(username); - - let now = Date.now(); - let coll = user.createCollection("testcoll", { - foo: new ServerBSO("foo", "payload0", now) - }); - - let request = client.deleteBSOs("testcoll", ["foo"]); - request.locallyModifiedVersion = coll.timestamp - 1; - - request.dispatch(function onComplete(error, req) { - do_check_true(error instanceof StorageServiceRequestError); - do_check_true(error.serverModified); - - server.stop(run_next_test); - }); -}); - -add_test(function test_delete_bsos_conditional_success() { - _("Ensure conditional deletion of BSOs without server modifications works."); - - let [server, client, username] = getServerAndClient(); - let user = server.user(username); - - let now = Date.now(); - let coll = user.createCollection("testcoll", { - foo: new ServerBSO("foo", "payload0", now), - bar: new ServerBSO("bar", "payload1", now - 10) - }); - - let request = client.deleteBSOs("testcoll", ["bar"]); - request.locallyModifiedVersion = coll.timestamp; - - request.dispatch(function onComplete(error, req) { - do_check_null(error); - - do_check_eq(req.statusCode, 204); - - server.stop(run_next_test); - }); -}); - -add_test(function test_delete_collection() { - _("Ensure deleteCollection() works."); - - let [server, client, username] = getServerAndClient(); - let user = server.user(username); - - user.createCollection("testcoll", { - foo: new ServerBSO("foo", "payload0", Date.now()) - }); - - let request = client.deleteCollection("testcoll"); - request.dispatch(function onComplete(error, req) { - do_check_null(error); - - do_check_eq(user.collection("testcoll", undefined)); - - server.stop(run_next_test); - }); -}); - -add_test(function test_delete_collection_conditional_failed() { - _("Ensure conditional deletes with server modifications fail."); - - let [server, client, username] = getServerAndClient(); - let user = server.user(username); - - let now = Date.now(); - - let coll = user.createCollection("testcoll", { - foo: new ServerBSO("foo", "payload0", now) - }); - - let request = client.deleteCollection("testcoll"); - request.locallyModifiedVersion = coll.timestamp - 1; - - request.dispatch(function onComplete(error, req) { - do_check_true(error instanceof StorageServiceRequestError); - do_check_true(error.serverModified); - - server.stop(run_next_test); - }); -}); - -add_test(function test_delete_collection_conditional_success() { - _("Ensure conditional delete of collection works when it's supposed to."); - - let [server, client, username] = getServerAndClient(); - let user = server.user(username); - - let now = Date.now(); - - let coll = user.createCollection("testcoll", { - foo: new ServerBSO("foo", "payload0", now) - }); - - let request = client.deleteCollection("testcoll"); - request.locallyModifiedVersion = coll.timestamp; - - request.dispatch(function onComplete(error, req) { - do_check_null(error); - - do_check_eq(user.collection("testcoll"), undefined); - - server.stop(run_next_test); - }); -}); - -add_test(function test_delete_collections() { - _("Ensure deleteCollections() works."); - - let [server, client, username] = getServerAndClient(); - let user = server.user(username); - - user.createCollection("testColl", { - foo: new ServerBSO("foo", "payload0", Date.now()) - }); - - let request = client.deleteCollections(); - request.dispatch(function onComplete(error, req) { - do_check_null(error); - - do_check_eq(user.collection("testcoll"), undefined); - - server.stop(run_next_test); - }); -}); - -add_test(function test_network_error_captured() { - _("Ensure network errors are captured."); - - // Network errors should result in .networkError being set on request. - let client = new StorageServiceClient("http://rnewman-is-splendid.badtld/"); - - let request = client.getCollectionInfo(); - request.dispatch(function(error, req) { - do_check_neq(error, null); - do_check_neq(error.network, null); - - run_next_test(); - }); -}); - -add_test(function test_network_error_listener() { - _("Ensure the onNetworkError listener is invoked on network errors."); - - let listenerCalled = false; - - let client = new StorageServiceClient("http://philikon-is-too.badtld/"); - client.addListener({ - onNetworkError: function(client, request) { - listenerCalled = true; - } - }); - let request = client.getCollectionInfo(); - request.dispatch(function() { - do_check_true(listenerCalled); - run_next_test(); - }); -}); - -add_test(function test_batching_set_too_large() { - _("Ensure we throw when attempting to add a BSO that is too large to fit."); - - let [server, client, username] = getServerAndClient(); - - let request = client.setBSOsBatching("testcoll"); - let payload = ""; - - // The actual length of the payload is a little less. But, this ensures we - // exceed it. - for (let i = 0; i < client.REQUEST_SIZE_LIMIT; i++) { - payload += i; - } - - let bso = new BasicStorageObject("bso"); - bso.payload = payload; - do_check_throws(function add() { request.addBSO(bso); }); - - server.stop(run_next_test); -}); - -add_test(function test_batching_set_basic() { - _("Ensure batching set works with single requests."); - - let [server, client, username] = getServerAndClient(); - - let request = client.setBSOsBatching("testcoll"); - for (let i = 0; i < 10; i++) { - let bso = new BasicStorageObject("bso" + i); - bso.payload = "payload" + i; - request.addBSO(bso); - } - - request.finish(function onFinish(request) { - do_check_eq(request.successfulIDs.length, 10); - - let collection = server.user(username).collection("testcoll"); - do_check_eq(collection.timestamp, request.serverModifiedVersion); - - server.stop(run_next_test); - }); -}); - -add_test(function test_batching_set_batch_count() { - _("Ensure multiple outgoing request batching works when count is exceeded."); - - let [server, client, username] = getServerAndClient(); - let requestCount = 0; - server.callback.onRequest = function onRequest() { - requestCount++; - } - - let request = client.setBSOsBatching("testcoll"); - for (let i = 1; i <= 300; i++) { - let bso = new BasicStorageObject("bso" + i); - bso.payload = "XXXXXXX"; - request.addBSO(bso); - } - - request.finish(function onFinish(request) { - do_check_eq(request.successfulIDs.length, 300); - do_check_eq(requestCount, 3); - - let collection = server.user(username).collection("testcoll"); - do_check_eq(collection.timestamp, request.serverModifiedVersion); - - server.stop(run_next_test); - }); -}); - -add_test(function test_batching_set_batch_size() { - _("Ensure outgoing requests batch when size is exceeded."); - - let [server, client, username] = getServerAndClient(); - let requestCount = 0; - server.callback.onRequest = function onRequest() { - requestCount++; - }; - - let limit = client.REQUEST_SIZE_LIMIT; - - let request = client.setBSOsBatching("testcoll"); - - // JavaScript: Y U NO EASY REPETITION FUNCTIONALITY? - let data = []; - for (let i = (limit / 2) - 100; i; i -= 1) { - data.push("X"); - } - - let payload = data.join(""); - - for (let i = 0; i < 4; i++) { - let bso = new BasicStorageObject("bso" + i); - bso.payload = payload; - request.addBSO(bso); - } - - request.finish(function onFinish(request) { - do_check_eq(request.successfulIDs.length, 4); - do_check_eq(requestCount, 2); - - let collection = server.user(username).collection("testcoll"); - do_check_eq(collection.timestamp, request.serverModifiedVersion); - - server.stop(run_next_test); - }); -}); - -add_test(function test_batching_set_flush() { - _("Ensure flushing batch sets works."); - - let [server, client, username] = getServerAndClient(); - - let requestCount = 0; - server.callback.onRequest = function onRequest() { - requestCount++; - } - - let request = client.setBSOsBatching("testcoll"); - for (let i = 1; i < 101; i++) { - let bso = new BasicStorageObject("bso" + i); - bso.payload = "foo"; - request.addBSO(bso); - - if (i % 10 == 0) { - request.flush(); - } - } - - request.finish(function onFinish(request) { - do_check_eq(request.successfulIDs.length, 100); - do_check_eq(requestCount, 10); - - let collection = server.user(username).collection("testcoll"); - do_check_eq(collection.timestamp, request.serverModifiedVersion); - - server.stop(run_next_test); - }); -}); - -add_test(function test_batching_set_conditional_success() { - _("Ensure conditional requests for batched sets work properly."); - - let [server, client, username] = getServerAndClient(); - - let collection = server.user(username).createCollection("testcoll"); - - let lastServerVersion = Date.now(); - collection.insertBSO(new ServerBSO("foo", "bar", lastServerVersion)); - collection.timestamp = lastServerVersion; - do_check_eq(collection.timestamp, lastServerVersion); - - let requestCount = 0; - server.callback.onRequest = function onRequest() { - requestCount++; - } - - let request = client.setBSOsBatching("testcoll"); - request.locallyModifiedVersion = collection.timestamp; - - for (let i = 1; i < 251; i++) { - let bso = new BasicStorageObject("bso" + i); - bso.payload = "foo" + i; - request.addBSO(bso); - } - - request.finish(function onFinish(request) { - do_check_eq(requestCount, 3); - - do_check_eq(collection.timestamp, request.serverModifiedVersion); - do_check_eq(collection.timestamp, request.locallyModifiedVersion); - - server.stop(run_next_test); - }); -}); - -add_test(function test_batching_set_initial_failure() { - _("Ensure that an initial request failure setting BSOs is handled properly."); - - let [server, client, username] = getServerAndClient(); - - let collection = server.user(username).createCollection("testcoll"); - collection.timestamp = Date.now(); - - let requestCount = 0; - server.callback.onRequest = function onRequest() { - requestCount++; - } - - let request = client.setBSOsBatching("testcoll"); - request.locallyModifiedVersion = collection.timestamp - 1; - - for (let i = 1; i < 250; i++) { - let bso = new BasicStorageObject("bso" + i); - bso.payload = "foo" + i; - request.addBSO(bso); - } - - request.finish(function onFinish(request) { - do_check_eq(requestCount, 1); - - do_check_eq(request.successfulIDs.length, 0); - do_check_eq(Object.keys(request.failures).length, 0); - - server.stop(run_next_test); - }); -}); - -add_test(function test_batching_set_subsequent_failure() { - _("Ensure a non-initial failure during batching set is handled properly."); - - let [server, client, username] = getServerAndClient(); - let collection = server.user(username).createCollection("testcoll"); - collection.timestamp = Date.now(); - - let requestCount = 0; - server.callback.onRequest = function onRequest() { - requestCount++; - - if (requestCount == 1) { - return; - } - - collection.timestamp++; - } - - let request = client.setBSOsBatching("testcoll"); - request.locallyModifiedVersion = collection.timestamp; - - for (let i = 0; i < 250; i++) { - let bso = new BasicStorageObject("bso" + i); - bso.payload = "foo" + i; - request.addBSO(bso); - } - - request.finish(function onFinish(request) { - do_check_eq(requestCount, 2); - do_check_eq(request.successfulIDs.length, 100); - do_check_eq(Object.keys(request.failures).length, 0); - - server.stop(run_next_test); - }); -}); - -function getBatchedDeleteData(collection="testcoll") { - let [server, client, username] = getServerAndClient(); - - let serverBSOs = {}; - for (let i = 1000; i; i -= 1) { - serverBSOs["bso" + i] = new ServerBSO("bso" + i, "payload" + i); - } - - let user = server.user(username); - user.createCollection(collection, serverBSOs); - - return [server, client, username, collection]; -} - -add_test(function test_batched_delete_single() { - _("Ensure batched delete with single request works."); - - let [server, client, username, collection] = getBatchedDeleteData(); - - let requestCount = 0; - server.callback.onRequest = function onRequest() { - requestCount += 1; - } - - let request = client.deleteBSOsBatching(collection); - for (let i = 1; i < 51; i += 1) { - request.addID("bso" + i); - } - - request.finish(function onFinish(request) { - do_check_eq(requestCount, 1); - do_check_eq(request.errors.length, 0); - - let coll = server.user(username).collection(collection); - do_check_eq(coll.count(), 950); - - do_check_eq(request.serverModifiedVersion, coll.timestamp); - - server.stop(run_next_test); - }); -}); - -add_test(function test_batched_delete_multiple() { - _("Ensure batched delete splits requests properly."); - - let [server, client, username, collection] = getBatchedDeleteData(); - - let requestCount = 0; - server.callback.onRequest = function onRequest() { - requestCount += 1; - } - - let request = client.deleteBSOsBatching(collection); - for (let i = 1; i < 251; i += 1) { - request.addID("bso" + i); - } - - request.finish(function onFinish(request) { - do_check_eq(requestCount, 3); - do_check_eq(request.errors.length, 0); - - let coll = server.user(username).collection(collection); - do_check_eq(coll.count(), 750); - - do_check_eq(request.serverModifiedVersion, coll.timestamp); - - server.stop(run_next_test); - }); -}); - -add_test(function test_batched_delete_conditional_success() { - _("Ensure conditional batched delete all work."); - - let [server, client, username, collection] = getBatchedDeleteData(); - - let requestCount = 0; - server.callback.onRequest = function onRequest() { - requestCount++; - } - - let serverCollection = server.user(username).collection(collection); - let initialTimestamp = serverCollection.timestamp; - - let request = client.deleteBSOsBatching(collection); - request.locallyModifiedVersion = initialTimestamp; - - for (let i = 1; i < 251; i += 1) { - request.addID("bso" + 1); - } - - request.finish(function onFinish(request) { - do_check_eq(requestCount, 3); - do_check_eq(request.errors.length, 0); - - do_check_true(request.locallyModifiedVersion > initialTimestamp); - - server.stop(run_next_test); - }); -}); - -add_test(function test_batched_delete_conditional_initial_failure() { - _("Ensure conditional batched delete failure on initial request works."); - - // The client needs to issue multiple requests but the first one was - // rejected. The client should only issue that initial request. - let [server, client, username, collection] = getBatchedDeleteData(); - - let requestCount = 0; - server.callback.onRequest = function onRequest() { - requestCount++; - } - - let serverCollection = server.user(username).collection(collection); - let request = client.deleteBSOsBatching(collection); - request.locallyModifiedVersion = serverCollection.timestamp - 1; - - for (let i = 1; i < 251; i += 1) { - request.addID("bso" + i); - } - - request.finish(function onFinish(request) { - do_check_eq(requestCount, 1); - do_check_eq(request.errors.length, 1); - - server.stop(run_next_test); - }); -}); - -add_test(function test_batched_delete_conditional_subsequent_failure() { - _("Ensure conditional batched delete failure on non-initial request."); - - let [server, client, username, collection] = getBatchedDeleteData(); - - let serverCollection = server.user(username).collection(collection); - - let requestCount = 0; - server.callback.onRequest = function onRequest() { - requestCount++; - - if (requestCount <= 1) { - return; - } - - // Advance collection's timestamp on subsequent requests so request is - // rejected. - serverCollection.timestamp++; - } - - let request = client.deleteBSOsBatching(collection); - request.locallyModifiedVersion = serverCollection.timestamp; - - for (let i = 1; i < 251; i += 1) { - request.addID("bso" + i); - } - - request.finish(function onFinish(request) { - do_check_eq(requestCount, 2); - do_check_eq(request.errors.length, 1); - - server.stop(run_next_test); - }); -}); diff --git a/services/common/tests/unit/xpcshell.ini b/services/common/tests/unit/xpcshell.ini index a2b01449c24..083ec60a6a0 100644 --- a/services/common/tests/unit/xpcshell.ini +++ b/services/common/tests/unit/xpcshell.ini @@ -34,12 +34,6 @@ skip-if = toolkit == 'gonk' [test_restrequest.js] [test_tokenauthenticatedrequest.js] -# Storage service APIs -[test_storageservice_bso.js] -[test_storageservice_client.js] -# Bug 969624: Intermittent fail on Android 2.3 and Android 4.3 emulators -skip-if = android_version == "10" || android_version == "18" - [test_tokenserverclient.js] skip-if = os == "android"