@qooxdoo/framework
Version:
The JS Framework for Coders
1,038 lines (769 loc) • 25.5 kB
JavaScript
/* ************************************************************************
qooxdoo - the new era of web development
http://qooxdoo.org
Copyright:
2004-2011 1&1 Internet AG, Germany, http://www.1und1.de
License:
MIT: https://opensource.org/licenses/MIT
See the LICENSE file in the project's top-level directory for details.
Authors:
* Tristan Koch (tristankoch)
* Richard Sternagel (rsternagel)
************************************************************************ */
/* ************************************************************************
************************************************************************ */
/**
*
* @asset(qx/test/xmlhttp/*)
*/
qx.Class.define("qx.test.io.rest.Resource",
{
extend: qx.dev.unit.TestCase,
include : [qx.dev.unit.MRequirements,
qx.dev.unit.MMock],
members:
{
setUp: function() {
this.setUpDoubleRequest();
this.setUpResource();
},
setUpDoubleRequest: function() {
// Restore Xhr when wrapped before
if (typeof qx.io.request.Xhr.restore == "function") {
qx.io.request.Xhr.restore();
}
var req = this.req = new qx.io.request.Xhr();
// Stub request methods, leave event system intact
req = this.shallowStub(req, qx.io.request.AbstractRequest);
// Inject double and return
this.injectStub(qx.io.request, "Xhr", req);
// Remember request for later disposal
this.__reqs = this.__reqs || [];
this.__reqs.push(this.req);
return req;
},
setUpResource: function() {
this.res && this.res.dispose();
var res = this.res = new qx.io.rest.Resource();
// Default routes
res.map("get", "GET", "/photos");
res.map("post", "POST", "/photos");
},
tearDown: function() {
this.getSandbox().restore();
this.res.dispose();
this.__reqs.forEach(function(req) {
req.dispose();
});
},
//
// Configuration
//
"test: configure request receives pre-configured but unsent request": function() {
var res = this.res,
req = this.req;
res.configureRequest(qx.lang.Function.bind(function(req) {
this.assertCalledWith(req.setMethod, "GET");
this.assertCalled(req.setUrl, "/photos");
this.assertNotCalled(req.send);
}, this));
res.get();
},
"test: configure request receives invocation details": function() {
var res = this.res,
req = this.req,
params = {},
data = {},
callback;
callback = this.spy(qx.lang.Function.bind(function(req, _action, _params, _data) {
this.assertEquals("get", _action, "Unexpected action");
this.assertEquals(params, _params, "Unexpected params");
this.assertEquals(data, _data, "Unexpected data");
}, this));
res.configureRequest(callback);
res.get(params, data);
this.assertCalled(callback);
},
//
// Route
//
"test: map action": function() {
var res = this.res,
params;
params = res._getRequestConfig("get");
this.assertEquals("GET", params.method);
this.assertEquals("/photos", params.url);
},
"test: map action when base URL": function() {
var res = this.res,
params;
res.setBaseUrl("http://example.com");
params = res._getRequestConfig("get");
this.assertEquals("http://example.com/photos", params.url);
},
"test: map existing action": function() {
var res = this.res,
params;
res.map("post", "GET", "/articles");
params = res._getRequestConfig("post");
this.assertEquals("/articles", params.url);
},
"test: map action creates method": function() {
var res = this.res,
req = this.req;
this.assertFunction(res.get);
},
"test: map action throws when existing method": function() {
this.require(["debug"]);
var res = this.res,
req = this.req;
// For whatever reason
res.popular = function() {};
this.assertException(function() {
res.map("popular", "GET", "/photos/popular");
}, Error);
},
"test: map action does not throw when existing method is empty": function() {
this.require(["debug"]);
var res = this.res,
req = this.req;
// For documentation purposes
res.get = (function() {});
res.map("get", "GET", "/photos/popular");
},
"test: dynamically created action forwards arguments": function() {
var res = this.res,
req = this.req;
this.spy(res, "invoke");
res.get({}, 1, 2, 3);
this.assertCalledWith(res.invoke, "get", {}, 1, 2, 3);
},
"test: dynamically created action returns what invoke returns": function() {
var id = 1;
this.stub(this.res, "invoke").returns(id);
this.assertEquals(id, this.res.get());
},
"test: map actions from description": function() {
var req = this.req,
description,
res,
check = {},
params;
description =
{
get: { method: "GET", url: "/photos" },
create: { method: "POST", url: "/photos", check: check }
};
res = new qx.io.rest.Resource(description);
params = res._getRequestConfig("get");
this.assertEquals("GET", params.method);
this.assertEquals("/photos", params.url);
params = res._getRequestConfig("create");
this.assertEquals("POST", params.method);
this.assertEquals("/photos", params.url);
this.assertEquals(check, params.check);
res.dispose();
},
"test: map action from description throws with non-object": function() {
this.require(["debug"]);
this.assertException(function() {
var res = new qx.io.rest.Resource([]);
});
},
"test: map action from description throws with incomplete route": function() {
this.require(["debug"]);
this.res.dispose();
this.assertException(function() {
var description =
{
get: { method: "GET"}
};
this.res = new qx.io.rest.Resource(description);
}, Error, "URL must be string for route 'get'");
},
//
// Invoke
//
"test: invoke action generically": function() {
var res = this.res,
req = this.req,
result;
result = res.invoke("get");
this.assertSend();
},
"test: invoke action": function() {
var res = this.res,
req = this.req;
res.get();
this.assertSend();
},
"test: invoke action returns id of request": function() {
var res = this.res,
req = this.req;
this.assertNumber(res.invoke("get"));
},
"test: invoke action while other is in progress": function() {
var res = this.res,
req1, req2;
req1 = this.req;
res.get();
this.setUpDoubleRequest();
req2 = this.req;
res.post();
this.assertCalledOnce(req1.send);
this.assertCalledOnce(req2.send);
},
"test: invoke same action handles multiple requests": function() {
var res = this.res,
req1, req2,
getSuccess = this.spy();
res.addListener("getSuccess", getSuccess);
req1 = this.req;
res.get();
this.setUpDoubleRequest();
req2 = this.req;
res.get();
this.respond("", req1);
this.respond("", req2);
this.assertCalledTwice(getSuccess);
},
"test: invoke action with positional params": function() {
var res = this.res,
req = this.req;
res.map("get", "GET", "/photos/{id}");
res.get({id: "1"});
this.assertCalledWith(req.setUrl, "/photos/1");
},
"test: invoke action with positional params that evaluate to false": function() {
var res = this.res,
req = this.req;
res.map("get", "GET", "/photos/{id}");
res.get({id: 0});
this.assertCalledWith(req.setUrl, "/photos/0");
},
"test: invoke action with non-string params": function() {
var res = this.res,
req = this.req;
res.map("get", "GET", "/photos/{id}");
res.get({id: 1});
this.assertCalledWith(req.setUrl, "/photos/1");
},
"test: invoke action with params and data": function() {
var res = this.res,
req = this.req;
res.map("put", "PUT", "/articles/{id}");
res.put({id: "1"}, {article: '{title: "Affe"}'});
// Note that with method GET, parameters are appended to the URLs query part.
// Please refer to the API docs of qx.io.request.AbstractRequest#requestData.
//
// res.get({id: "1"}, {lang: "de"});
// --> /articles/1/?lang=de
this.assertCalledWith(req.setRequestData, {article: '{title: "Affe"}'});
},
"test: invoke action with multiple positional params": function() {
var res = this.res,
req = this.req;
res.map("get", "GET", "/photos/{id}/comments/{commentId}");
res.get({id: "1", commentId: "2"});
this.assertCalledWith(req.setUrl, "/photos/1/comments/2");
},
"test: invoke action with positional params in query": function() {
var res = this.res,
req = this.req;
res.map("get", "GET", "/photos/{id}/comments?id={commentId}");
res.get({id: "1", commentId: "2"});
this.assertCalledWith(req.setUrl, "/photos/1/comments?id=2");
},
"test: invoke action with undefined params": function() {
var res = this.res,
req = this.req;
res.get();
this.assertCalled(req.send);
},
"test: invoke action with null params": function() {
var res = this.res,
req = this.req;
res.get(null);
this.assertCalled(req.send);
},
"test: invoke action when content type json": function() {
var res = this.res,
req = this.req;
req.setRequestHeader.restore();
req.getRequestHeader.restore();
res.configureRequest(function(req) {
req.setRequestHeader("Content-Type", "application/json");
});
this.spy(qx.lang.Json, "stringify");
var data = {location: "Karlsruhe"};
res.map("post", "POST", "/photos/{id}/meta");
res.post({id: 1}, data);
this.assertCalledWith(req.setRequestData, '{"location":"Karlsruhe"}');
this.assertCalledWith(qx.lang.Json.stringify, data);
},
"test: invoke action when content type json and get": function() {
var res = this.res,
req = this.req;
req.setMethod.restore();
req.getMethod.restore();
this.spy(qx.lang.Json, "stringify");
req.getRequestHeader.withArgs("Content-Type").returns("application/json");
res.get();
this.assertNotCalled(qx.lang.Json.stringify);
},
"test: invoke action for url with port": function() {
var res = this.res,
req = this.req;
res.map("get", "GET", "http://example.com:8080/photos/{id}");
res.get({id: "1"});
this.assertCalledWith(req.setUrl, "http://example.com:8080/photos/1");
},
"test: invoke action for relative url": function() {
var res = this.res,
req = this.req;
res.map("get", "GET", "{page}");
res.get({page: "index"});
this.assertCalledWith(req.setUrl, "index");
},
"test: invoke action for relative url with dots": function() {
var res = this.res,
req = this.req;
res.map("get", "GET", "../{page}");
res.get({page: "index"});
this.assertCalledWith(req.setUrl, "../index");
},
"test: invoke action for route with check": function() {
var res = this.res;
res.map("get", "GET", "/photos/zoom/{id}", {id: /\d+/});
res.get({id: "123"});
this.assertSend("GET", "/photos/zoom/123");
},
"test: invoke action fills in empty string when missing param and no default": function() {
var res = this.res;
res.map("get", "GET", "/photos/{tag}");
res.get();
this.assertSend("GET", "/photos/");
},
"test: invoke action fills in default when missing param": function() {
var res = this.res;
res.map("get", "GET", "/photos/{tag=recent}/{size}");
res.get({size: "large"});
this.assertSend("GET", "/photos/recent/large");
},
"test: invoke action throws when missing required positional param": function() {
var res = this.res;
// Require positional param
res.map("get", "GET", "/photos/{tag}", {tag: qx.io.rest.Resource.REQUIRED});
this.assertException(function() {
res.get();
}, Error, "Missing parameter 'tag'");
},
"test: invoke action throws when missing required request param": function() {
var res = new qx.io.rest.Resource();
// Require request body param
res.map("post", "POST", "/photos/", {photo: qx.io.rest.Resource.REQUIRED});
this.assertException(function() {
res.post();
}, Error, "Missing parameter 'photo'");
},
"test: invoke action throws when param not match check": function() {
var res = this.res;
res.map("get", "GET", "/photos/{id}", {id: /\d+/});
this.assertException(function() {
res.get({id: "FAIL"});
}, Error, "Parameter 'id' is invalid");
},
"test: invoke action ignores invalid check in production": function() {
this.require(["debug"]);
var res = this.res;
var setting = this.stub(qx.core.Environment, "get").withArgs("qx.debug");
setting.returns(false);
// Invalid check
res.map("get", "GET", "/photos/{id}", {id: ""});
res.get({id: 1});
},
//
// Abort
//
"test: abort action": function() {
var res = this.res,
req = this.req;
res.get();
res.abort("get");
this.assertCalledOnce(req.abort);
},
"test: abort action when multiple requests": function() {
var res = this.res,
req1, req2;
req1 = this.setUpDoubleRequest();
res.get();
req2 = this.setUpDoubleRequest();
res.get();
res.abort("get");
this.assertCalledOnce(req1.abort);
this.assertCalledOnce(req2.abort);
},
"test: abort by action id": function() {
var res = this.res,
req = this.req;
var id = res.get();
res.abort(id);
this.assertCalledOnce(req.abort);
},
//
// Helper
//
"test: refresh action": function() {
var res = this.res,
req = this.req;
res.get();
this.assertSend();
res.refresh("get");
this.assertSend();
},
"test: refresh action replaying previous params": function() {
var res = this.res,
req = this.req;
res.map("get", "GET", "/photos/{id}");
res.get({id: "1"});
this.assertSend("GET", "/photos/1");
res.refresh("get");
this.assertSend("GET", "/photos/1");
},
"test: poll action": function() {
var res = this.res,
sandbox = this.getSandbox();
sandbox.useFakeTimers();
this.spy(res._resource, "refresh");
res.poll("get", 10);
this.respond();
sandbox.clock.tick(20);
this.assertCalledWith(res._resource.refresh, "get");
this.assertCalledOnce(res._resource.refresh);
},
"test: not poll action when no response received yet": function() {
var res = this.res,
sandbox = this.getSandbox();
sandbox.useFakeTimers();
this.spy(res, "refresh");
res.poll("get", 10);
sandbox.clock.tick(20);
this.assertNotCalled(res.refresh);
},
"test: poll action immediately": function() {
var res = this.res;
this.spy(res._resource, "invoke");
res.poll("get", 10, undefined, true);
this.assertCalled(res._resource.invoke);
},
"test: poll action sets initial params": function() {
var res = this.res;
res.map("get", "GET", "/photos/{id}");
this.stub(res._resource, "invoke");
res.poll("get", 10, {id: "1"}, true);
this.assertCalledWith(res._resource.invoke, "get", {id: "1"});
},
"test: poll action replaying previous params": function() {
var res = this.res,
req = this.req;
res.map("get", "GET", "/photos/{id}");
res.get({id: "1"});
this.assertSend("GET", "/photos/1");
res.poll("get");
this.assertSend("GET", "/photos/1");
},
"test: poll action repeatedly ends previous timer": function() {
var res = this.res,
sandbox = this.getSandbox(),
msg;
sandbox.useFakeTimers();
this.stub(res._resource, "refresh");
res.poll("get", 10);
this.respond();
sandbox.clock.tick(20);
res.poll("get", 100);
this.respond();
sandbox.clock.tick(100);
this.assertCalledTwice(res._resource.refresh);
},
"test: poll many actions": function() {
var res = this.res,
sandbox = this.getSandbox(),
spy,
get,
post;
this.stub(this.req, "dispose");
sandbox.useFakeTimers();
spy = this.spy(res._resource, "refresh");
get = spy.withArgs("get");
post = spy.withArgs("post");
res.poll("get", 10);
res.poll("post", 10);
this.respond();
sandbox.clock.tick(20);
this.assertCalledOnce(get);
this.assertCalledOnce(post);
this.req.dispose.restore();
this.req.dispose();
},
"test: end poll action": function() {
var res = this.res,
sandbox = this.getSandbox(),
timer;
sandbox.useFakeTimers();
this.spy(res._resource, "refresh");
timer = res.poll("get", 10);
this.respond();
// 10ms invoke, 20ms refresh, 30ms refresh
sandbox.clock.tick(30);
timer.stop();
sandbox.clock.tick(100);
this.assertCalledTwice(res._resource.refresh);
},
"test: end poll action does not end polling of other action": function() {
var res = this.res,
sandbox = this.getSandbox(),
timer,
spy;
sandbox.useFakeTimers();
spy = this.spy(res._resource, "refresh").withArgs("get");
this.respond();
res.poll("get", 10);
timer = res.poll("post", 10);
sandbox.clock.tick(20);
timer.stop();
sandbox.clock.tick(10);
this.assertCalledTwice(spy);
},
"test: restart poll action": function() {
var res = this.res,
sandbox = this.getSandbox(),
timer;
sandbox.useFakeTimers();
this.respond();
timer = res.poll("get", 10);
sandbox.clock.tick(10);
timer.stop();
this.spy(res._resource, "refresh");
timer.restart();
sandbox.clock.tick(10);
this.assertCalled(res._resource.refresh);
},
"test: long poll action": function() {
var res = this.res,
req = this.req,
responses = [];
this.stub(req, "dispose");
res.addListener("getSuccess", function(e) {
responses.push(e.getData());
}, this);
res.longPoll("get");
// longPoll() sets up new request when receiving a response
this.respond("1");
this.respond("2");
this.respond("3");
this.assertArrayEquals(["1", "2", "3"], responses);
},
"test: throttle long poll": function() {
var res = this.res,
req = this.req;
this.stub(req, "dispose");
this.spy(res, "refresh");
this.stub(qx.io.rest.Resource, "POLL_THROTTLE_COUNT", "3");
res.longPoll("get");
// A number of immediate responses, above count
for (var i=0; i < 4; i++) {
this.respond();
}
res.refresh = function() {
throw new Error("With throttling in effect, " +
"must not make new request.");
};
// Throttling
this.respond();
},
"test: not throttle long poll when not received within limit": function() {
var res = this.res,
req = this.req,
sandbox = this.getSandbox();
this.stub(req, "dispose");
sandbox.useFakeTimers();
res.longPoll("get");
// A number of delayed responses, above count
for (var i=0; i < 31; i++) {
sandbox.clock.tick(101);
this.respond();
}
this.spy(res, "refresh");
sandbox.clock.tick(101);
this.respond();
this.assertCalled(res.refresh);
},
"test: not throttle long poll when not received subsequently": function() {
var res = this.res,
req = this.req,
sandbox = this.getSandbox();
this.stub(req, "dispose");
sandbox.useFakeTimers();
res.longPoll("get");
// A number of immediate responses
for (var i=0; i < 30; i++) {
this.respond();
}
// Delayed response
sandbox.clock.tick(101);
this.respond();
// More immediate responses, total count above limit
this.spy(res, "refresh");
for (i=0; i < 10; i++) {
this.respond();
}
this.assertCallCount(res.refresh, 10);
},
"test: end long poll action": function() {
var res = this.res,
req = this.req,
handlerId,
msg;
this.stub(req, "dispose");
this.spy(res._resource, "refresh");
handlerId = res.longPoll("get");
this.respond();
this.respond();
res.removeListenerById(handlerId);
this.respond();
this.assertCalledTwice(res._resource.refresh);
},
//
// Events
//
"test: fire actionSuccess": function() {
var res = this.res,
req = this.req,
that = this;
res.get();
this.assertEventFired(res, "getSuccess", function() {
that.respond("Affe");
}, function(e) {
that.assertEquals("Affe", e.getData());
that.assertEquals("get", e.getAction());
that.assertIdentical(req, e.getRequest());
that.assertInteger(e.getId());
});
},
"test: fire success": function() {
var res = this.res,
req = this.req,
that = this;
res.get();
this.assertEventFired(res, "success", function() {
that.respond("Affe");
}, function(e) {
that.assertEquals("Affe", e.getData());
that.assertEquals("get", e.getAction());
that.assertIdentical(req, e.getRequest());
that.assertInteger(e.getId());
});
},
"test: fire actionError": function() {
var res = this.res,
req = this.req,
that = this;
res.get();
this.assertEventFired(res, "getError", function() {
that.respondError("statusError");
}, function(e) {
that.assertEquals("statusError", e.getPhase());
that.assertIdentical(req, e.getRequest());
});
},
"test: fire error": function() {
var res = this.res,
req = this.req,
that = this;
res.get();
this.assertEventFired(res, "error", function() {
that.respondError("statusError");
}, function(e) {
that.assertEquals("statusError", e.getPhase());
that.assertIdentical(req, e.getRequest());
});
},
//
// Dispose
//
"test: dispose requests": function() {
var res = this.res,
req1, req2;
req1 = this.req;
res.get();
this.setUpDoubleRequest();
req2 = this.req;
res.post();
this.spy(req1, "dispose");
this.spy(req2, "dispose");
res.dispose();
this.assertCalled(req1.dispose);
this.assertCalled(req2.dispose);
},
"test: dispose requests of same action": function() {
var res = this.res,
req1, req2;
req1 = this.req;
res.get();
this.setUpDoubleRequest();
req2 = this.req;
res.get();
this.spy(req1, "dispose");
this.spy(req2, "dispose");
res.dispose();
this.assertCalled(req1.dispose);
this.assertCalled(req2.dispose);
},
"test: dispose request on loadEnd": function() {
var res = this.res,
req = this.req;
this.spy(req, "dispose");
res.get();
this.respond();
setTimeout(function() {
this.resume(function() {
this.assertCalledOnce(req.dispose);
}, this);
}.bind(this), 100);
this.wait();
},
assertSend: function(method, url) {
var req = this.req;
method = method || "GET";
url = url || "/photos";
this.assertCalledWith(req.setMethod, method);
this.assertCalledWith(req.setUrl, url);
this.assertCalled(req.send);
},
skip: function(msg) {
throw new qx.dev.unit.RequirementError(null, msg);
},
hasDebug: function() {
return qx.core.Environment.get("qx.debug");
},
// Fake response
respond: function(response, req) {
var req = req || this.req;
response = response || "";
req.isDone.returns(true);
req.getPhase.returns("success");
req.getResponse.returns(response);
req.fireEvent("success");
req.fireEvent("loadEnd");
},
// Fake erroneous response
respondError: function(phase) {
var req = this.req;
phase = phase || "statusError";
req.getPhase.returns(phase);
req.fireEvent("fail");
req.fireEvent("loadEnd");
}
}
});