Bug 693864 - Implement /storage DELETE handling in test JS Sync server. r=philikon
authorRichard Newman <rnewman@mozilla.com>
Wed, 12 Oct 2011 13:57:39 -0700
changeset 79066 2c9b36620889ee79db0547b7c4f6ab576ad66138
parent 79065 f06dfa65c0beca988e13f26644be2aa498446511
child 79067 9a34387605eae0ada07567343b9ce45c7a4bba1a
push id1
push userroot
push dateMon, 20 Oct 2014 17:29:22 +0000
reviewersphilikon
bugs693864
milestone10.0a1
Bug 693864 - Implement /storage DELETE handling in test JS Sync server. r=philikon
services/sync/tests/unit/head_http_server.js
services/sync/tests/unit/test_httpd_sync_server.js
--- a/services/sync/tests/unit/head_http_server.js
+++ b/services/sync/tests/unit/head_http_server.js
@@ -706,37 +706,60 @@ SyncServer.prototype = {
     if (!(collection in userCollections)) {
       throw new Error("Unknown collection.");
     }
     userCollections[collection].insertWBO(wbo);
     return wbo;
   },
 
   /**
+   * Delete all of the collections for the named user.
+   *
+   * @param username
+   *        The name of the affected user.
+   *
+   * @return a timestamp.
+   */
+  deleteCollections: function deleteCollections(username) {
+    if (!(username in this.users)) {
+      throw new Error("Unknown user.");
+    }
+    let userCollections = this.users[username].collections;
+    for each (let [name, coll] in Iterator(userCollections)) {
+      this._log.trace("Bulk deleting " + name + " for " + username + "...");
+      coll.delete({});
+    }
+    this.users[username].collections = {};
+    return this.timestamp();
+  },
+
+  /**
    * Simple accessor to allow collective binding and abbreviation of a bunch of
    * methods. Yay!
    * Use like this:
    *
    *   let u = server.user("john");
    *   u.collection("bookmarks").wbo("abcdefg").payload;  // Etc.
    *
    * @return a proxy for the user data stored in this server.
    */
   user: function user(username) {
     let collection       = this.getCollection.bind(this, username);
     let createCollection = this.createCollection.bind(this, username);
     let createContents   = this.createContents.bind(this, username);
     let modified         = function (collectionName) {
       return collection(collectionName).timestamp;
     }
+    let deleteCollections = this.deleteCollections.bind(this, username);
     return {
-      collection:       collection,
-      createCollection: createCollection,
-      createContents:   createContents,
-      modified:         modified
+      collection:        collection,
+      createCollection:  createCollection,
+      createContents:    createContents,
+      deleteCollections: deleteCollections,
+      modified:          modified
     };
   },
 
   /*
    * Regular expressions for splitting up Sync request paths.
    * Sync URLs are of the form:
    *   /$apipath/$version/$user/$further
    * where $further is usually:
@@ -747,19 +770,19 @@ SyncServer.prototype = {
    *   info/$op
    * We assume for the sake of simplicity that $apipath is empty.
    *
    * N.B., we don't follow any kind of username spec here, because as far as I
    * can tell there isn't one. See Bug 689671. Instead we follow the Python
    * server code.
    *
    * Path: [all, version, username, first, rest]
-   * Storage: [all, collection, id?]
+   * Storage: [all, collection?, id?]
    */
-  pathRE: /^\/([0-9]+(?:\.[0-9]+)?)\/([-._a-zA-Z0-9]+)\/([^\/]+)\/(.*)$/,
+  pathRE: /^\/([0-9]+(?:\.[0-9]+)?)\/([-._a-zA-Z0-9]+)(?:\/([^\/]+)(?:\/(.+))?)?$/,
   storageRE: /^([-_a-zA-Z0-9]+)(?:\/([-_a-zA-Z0-9]+)\/?)?$/,
 
   defaultHeaders: {},
 
   /**
    * HTTP response utility.
    */
   respond: function respond(req, resp, code, status, body, headers) {
@@ -821,24 +844,42 @@ SyncServer.prototype = {
     return responseObject;
   },
 
   /**
    * Collection of the handler methods we use for top-level path components.
    */
   toplevelHandlers: {
     "storage": function handleStorage(handler, req, resp, version, username, rest) {
+      let respond = this.respond.bind(this, req, resp);
+      if (!rest || !rest.length) {
+        this._log.debug("SyncServer: top-level storage " +
+                        req.method + " request.");
+
+        // TODO: verify if this is spec-compliant.
+        if (req.method != "DELETE") {
+          respond(405, "Method Not Allowed", "[]", {"Allow": "DELETE"});
+          return;
+        }
+
+        // Delete all collections and track the timestamp for the response.
+        let timestamp = this.user(username).deleteCollections();
+
+        // Return timestamp and OK for deletion.
+        respond(200, "OK", JSON.stringify(timestamp));
+        return;
+      }
+
       let match = this.storageRE.exec(rest);
       if (!match) {
         this._log.warn("SyncServer: Unknown storage operation " + rest);
         throw HTTP_404;
       }
       let [all, collection, wboID] = match;
       let coll = this.getCollection(username, collection);
-      let respond = this.respond.bind(this, req, resp);
       switch (req.method) {
         case "GET":
           if (!coll) {
             // *cries inside*: Bug 687299.
             respond(200, "OK", "[]");
             return;
           }
           if (!wboID) {
--- a/services/sync/tests/unit/test_httpd_sync_server.js
+++ b/services/sync/tests/unit/test_httpd_sync_server.js
@@ -1,9 +1,11 @@
 function run_test() {
+  Log4Moz.repository.getLogger("Sync.Test.Server").level = Log4Moz.Level.Trace;
+  initTestLogging();
   run_next_test();
 }
 
 add_test(function test_creation() {
   // Explicit callback for this one.
   let s = new SyncServer({
     __proto__: SyncServerCallback,
   });
@@ -11,23 +13,54 @@ add_test(function test_creation() {
   s.start(null, function () {
     _("Started on " + s.port);
     s.stop(run_next_test);
   });
 });
 
 add_test(function test_url_parsing() {
   let s = new SyncServer();
+
+  // Check that we can parse a WBO URI.
   let parts = s.pathRE.exec("/1.1/johnsmith/storage/crypto/keys");
   let [all, version, username, first, rest] = parts;
+  do_check_eq(all, "/1.1/johnsmith/storage/crypto/keys");
   do_check_eq(version, "1.1");
   do_check_eq(username, "johnsmith");
   do_check_eq(first, "storage");
   do_check_eq(rest, "crypto/keys");
   do_check_eq(null, s.pathRE.exec("/nothing/else"));
+
+  // Check that we can parse a collection URI.
+  parts = s.pathRE.exec("/1.1/johnsmith/storage/crypto");
+  let [all, version, username, first, rest] = parts;
+  do_check_eq(all, "/1.1/johnsmith/storage/crypto");
+  do_check_eq(version, "1.1");
+  do_check_eq(username, "johnsmith");
+  do_check_eq(first, "storage");
+  do_check_eq(rest, "crypto");
+
+  // We don't allow trailing slash on storage URI.
+  parts = s.pathRE.exec("/1.1/johnsmith/storage/");
+  do_check_eq(parts, undefined);
+
+  // storage alone is a valid request.
+  parts = s.pathRE.exec("/1.1/johnsmith/storage");
+  let [all, version, username, first, rest] = parts;
+  do_check_eq(all, "/1.1/johnsmith/storage");
+  do_check_eq(version, "1.1");
+  do_check_eq(username, "johnsmith");
+  do_check_eq(first, "storage");
+  do_check_eq(rest, undefined);
+
+  parts = s.storageRE.exec("storage");
+  let [all, storage, collection, id] = parts;
+  do_check_eq(all, "storage");
+  do_check_eq(collection, undefined);
+
   run_next_test();
 });
 
 Cu.import("resource://services-sync/rest.js");
 function localRequest(path) {
   _("localRequest: " + path);
   let url = "http://127.0.0.1:8080" + path;
   _("url: " + url);
@@ -104,16 +137,18 @@ add_test(function test_info_collections(
       });
     });
   });
 });
 
 add_test(function test_storage_request() {
   let keysURL = "/1.1/john/storage/crypto/keys?foo=bar";
   let foosURL = "/1.1/john/storage/crypto/foos";
+  let storageURL = "/1.1/john/storage";
+
   let s = new SyncServer();
   let creation = s.timestamp();
   s.registerUser("john", "password");
 
   s.createContents("john", {
     crypto: {foos: {foo: "bar"}}
   });
   let coll = s.user("john").collection("crypto");
@@ -140,22 +175,46 @@ add_test(function test_storage_request()
       _("Modified is " + this.response.newModified);
       let parsedBody = JSON.parse(this.response.body);
       do_check_eq(parsedBody.id, "foos");
       do_check_eq(parsedBody.modified, coll.wbo("foos").modified);
       do_check_eq(JSON.parse(parsedBody.payload).foo, "bar");
       Utils.nextTick(next);
     });
   }
+  function deleteStorage(next) {
+    _("Testing DELETE on /storage.");
+    let now = s.timestamp();
+    _("Timestamp: " + now);
+    let req = localRequest(storageURL);
+    req.delete(function (err) {
+      _("Body is " + this.response.body);
+      _("Modified is " + this.response.newModified);
+      let parsedBody = JSON.parse(this.response.body);
+      do_check_true(parsedBody >= now);
+      do_check_empty(s.users["john"].collections);
+      Utils.nextTick(next);
+    });
+  }
+  function getStorageFails(next) {
+    _("Testing that GET on /storage fails.");
+    let req = localRequest(storageURL);
+    req.get(function (err) {
+      do_check_eq(this.response.status, 405);
+      do_check_eq(this.response.headers["allow"], "DELETE");
+      Utils.nextTick(next);
+    });
+  }
   s.start(8080, function () {
     retrieveWBONotExists(
-      retrieveWBOExists.bind(this, function () {
-        s.stop(run_next_test);
-      })
-    );
+      retrieveWBOExists.bind(this,
+        getStorageFails.bind(this,
+          deleteStorage.bind(this, function () {
+            s.stop(run_next_test);
+          }))));
   });
 });
 
 add_test(function test_x_weave_records() {
   let s = new SyncServer();
   s.registerUser("john", "password");
 
   s.createContents("john", {