UNPKG

@qooxdoo/framework

Version:

The JS Framework for Coders

1,166 lines (883 loc) 27.8 kB
/* ************************************************************************ qooxdoo - the new era of web development http://qooxdoo.org Copyright: 2013 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: * Richard Sternagel (rsternagel) ************************************************************************ */ /** * @asset(qx/test/xmlhttp/*) */ qx.Class.define("qx.test.bom.rest.Resource", { extend: qx.dev.unit.TestCase, include: [qx.dev.unit.MRequirements, qx.dev.unit.MMock], members: { setUp() { this.setUpDoubleRequest(); this.setUpResource(); }, setUpDoubleRequest() { // Restore Xhr when wrapped before if (typeof qx.bom.request.SimpleXhr.restore == "function") { qx.bom.request.SimpleXhr.restore(); } var req = (this.req = new qx.bom.request.SimpleXhr()); // Stub request methods, but // - leave event system intact (addListenerOnce) // - leave disposable intact, cause test methods stub it themselves (dispose) req = this.shallowStub(req, qx.bom.request.SimpleXhr, [ "dispose", "addListenerOnce", "getTransport" ]); // Inject double and return this.injectStub(qx.bom.request, "SimpleXhr", req); // Remember request for later disposal this.__reqs = this.__reqs || []; this.__reqs.push(this.req); return req; }, setUpResource() { this.res && this.res.dispose(); var res = (this.res = new qx.bom.rest.Resource()); // Default routes res.map("get", "GET", "/photos"); res.map("post", "POST", "/photos"); }, tearDown() { this.getSandbox().restore(); this.res.dispose(); this.__reqs.forEach(function (req) { req.dispose(); }); }, // // Configuration // "test: configure request receives pre-configured but unsent request"() { var res = this.res, req = this.req; res.configureRequest( qx.lang.Function.bind(function (req) { this.assertCalledWith(req.setMethod, "GET"); this.assertCalled(req.setUrl); this.assertNotCalled(req.send); }, this) ); res.get(); }, "test: configure request receives invocation details"() { 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"() { 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"() { 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"() { var res = this.res, params; res.map("post", "GET", "/articles"); params = res._getRequestConfig("post"); this.assertEquals("/articles", params.url); }, "test: map action creates method"() { var res = this.res, req = this.req; this.assertFunction(res.get); }, "test: map action throws when existing method"() { 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"() { 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"() { 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"() { var id = 1; this.stub(this.res, "invoke").returns(id); this.assertEquals(id, this.res.get()); }, "test: map actions from description"() { var req = this.req, description, res, check = {}, params; description = { get: { method: "GET", url: "/photos" }, create: { method: "POST", url: "/photos", check: check } }; res = new qx.bom.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"() { this.require(["debug"]); this.assertException(function () { var res = new qx.bom.rest.Resource([]); }); }, "test: map action from description throws with incomplete route"() { this.require(["debug"]); this.res.dispose(); this.assertException( function () { var description = { get: { method: "GET" } }; this.res = new qx.bom.rest.Resource(description); }, Error, "URL must be string for route 'get'" ); }, // // Invoke // "test: invoke action generically"() { var res = this.res, req = this.req, result; result = res.invoke("get"); this.assertSend(); }, "test: invoke action"() { var res = this.res, req = this.req; res.get(); this.assertSend(); }, "test: invoke action returns id of request"() { var res = this.res, req = this.req; req.toHashCode.restore(); this.assertNumber(res.invoke("get")); }, "test: invoke action while other is in progress"() { 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"() { 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"() { 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"() { 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"() { 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"() { 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"() { 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"() { 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"() { var res = this.res, req = this.req; res.get(); this.assertCalled(req.send); }, "test: invoke action with null params"() { var res = this.res, req = this.req; res.get(null); this.assertCalled(req.send); }, "test: invoke action when content type json"() { 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"() { 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"() { 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"() { 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"() { 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"() { 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"() { 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"() { 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"() { var res = this.res; // Require positional param res.map("get", "GET", "/photos/{tag}", { tag: qx.bom.rest.Resource.REQUIRED }); this.assertException( function () { res.get(); }, Error, "Missing parameter 'tag'" ); }, "test: invoke action throws when missing required request param"() { var res = new qx.bom.rest.Resource(); // Require request body param res.map("post", "POST", "/photos/", { photo: qx.bom.rest.Resource.REQUIRED }); this.assertException( function () { res.post(); }, Error, "Missing parameter 'photo'" ); }, "test: invoke action throws when param not match check"() { 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"() { this.skip("needs runtime enviroment checks!"); 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"() { var res = this.res, req = this.req; res.get(); res.abort("get"); this.assertCalledOnce(req.abort); }, "test: abort action when multiple requests"() { 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"() { var res = this.res, req = this.req; req.toHashCode.restore(); var id = res.get(); res.abort(id); this.assertCalledOnce(req.abort); }, // // Helper // "test: refresh action"() { var res = this.res, req = this.req; res.get(); this.assertSend(); res.refresh("get"); this.assertSend(); }, "test: refresh action replaying previous params"() { 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"() { var res = this.res, sandbox = this.getSandbox(); sandbox.useFakeTimers(); this.spy(res, "refresh"); res.poll("get", 10); this.respond(); sandbox.clock.tick(20); this.assertCalledWith(res.refresh, "get"); this.assertCalledOnce(res.refresh); }, "test: not poll action when no response received yet"() { 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"() { var res = this.res; this.spy(res, "invoke"); res.poll("get", 10, undefined, true); this.assertCalled(res.invoke); }, "test: poll action sets initial params"() { var res = this.res; res.map("get", "GET", "/photos/{id}"); this.stub(res, "invoke"); res.poll("get", 10, { id: "1" }, true); this.assertCalledWith(res.invoke, "get", { id: "1" }); }, "test: poll action replaying previous params"() { 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"() { var res = this.res, sandbox = this.getSandbox(), msg; sandbox.useFakeTimers(); this.stub(res, "refresh"); res.poll("get", 10); this.respond(); sandbox.clock.tick(20); res.poll("get", 100); this.respond(); sandbox.clock.tick(100); this.assertCalledTwice(res.refresh); }, "test: poll many actions"() { var res = this.res, sandbox = this.getSandbox(), spy, get, post; this.stub(this.req, "dispose"); sandbox.useFakeTimers(); spy = this.spy(res, "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"() { var res = this.res, sandbox = this.getSandbox(), timer, numCalled; sandbox.useFakeTimers(); this.spy(res, "refresh"); res.poll("get", 10); this.respond(); // 10ms invoke, 20ms refresh, 30ms refresh sandbox.clock.tick(30); res.stopPollByAction("get"); sandbox.clock.tick(100); this.assertCalledTwice(res.refresh); }, "test: end poll action does not end polling of other action"() { var res = this.res, sandbox = this.getSandbox(), timer, spy; sandbox.useFakeTimers(); spy = this.spy(res, "refresh").withArgs("get"); this.respond(); res.poll("get", 10); res.poll("post", 10); sandbox.clock.tick(20); res.stopPollByAction("post"); sandbox.clock.tick(10); this.assertCalledTwice(spy); }, "test: restart poll action"() { var res = this.res, sandbox = this.getSandbox(), timer; sandbox.useFakeTimers(); this.respond(); res.poll("get", 10); sandbox.clock.tick(10); res.stopPollByAction("get"); this.spy(res, "refresh"); res.restartPollByAction("get"); sandbox.clock.tick(10); this.assertCalled(res.refresh); }, "test: long poll action"() { var res = this.res, req = this.req, responses = []; // undo this line from setUp() ... // this.injectStub(qx.bom.request, "SimpleXhr", req); // ... in order to have unique reqs instead of always // the same stubbed req from the setUp method. qx.bom.request.SimpleXhr.restore(); this.stub(req, "dispose"); res.addListener("getSuccess", e => { responses.push(e.response); }); res.longPoll("get"); // longPoll() sets up new request when receiving a response this.respondSubsequent("1", 0, true); this.respondSubsequent("2", 1, true); this.respondSubsequent("3", 2, true); this.assertArrayEquals(["1", "2", "3"], responses); }, "test: throttle long poll"() { var res = this.res, req = this.req; this.stub(req, "dispose"); this.spy(res, "refresh"); this.stub(qx.bom.rest.Resource, "POLL_THROTTLE_COUNT").value("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"() { var res = this.res, req = this.req, sandbox = this.getSandbox(); // undo this line from setUp() ... // this.injectStub(qx.bom.request, "SimpleXhr", req); // ... in order to have unique reqs instead of always // the same stubbed req from the setUp method. qx.bom.request.SimpleXhr.restore(); 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.respondSubsequent(null, i); } this.spy(res, "refresh"); sandbox.clock.tick(101); this.respondSubsequent(null, i); this.assertCalled(res.refresh); }, "test: not throttle long poll when not received subsequently"() { var res = this.res, req = this.req, sandbox = this.getSandbox(); // undo this line from setUp() ... // this.injectStub(qx.bom.request, "SimpleXhr", req); // ... in order to have unique reqs instead of always // the same stubbed req from the setUp method. qx.bom.request.SimpleXhr.restore(); this.stub(req, "dispose"); sandbox.useFakeTimers(); res.longPoll("get"); // A number of immediate responses for (var i = 0; i < 30; i++) { this.respondSubsequent(null, i); } // Delayed response sandbox.clock.tick(101); this.respondSubsequent(null, i++); // // More immediate responses, total count above limit this.spy(res, "refresh"); for (var j = 0; j < 10; j++) { this.respondSubsequent(null, i + j); } this.assertCallCount(res.refresh, 10); }, "test: end long poll action"() { var res = this.res, req = this.req, handlerId, msg; // undo this line from setUp() ... // this.injectStub(qx.bom.request, "SimpleXhr", req); // ... in order to have unique reqs instead of always // the same stubbed req from the setUp method. qx.bom.request.SimpleXhr.restore(); this.stub(req, "dispose"); this.spy(res, "refresh"); handlerId = res.longPoll("get"); this.respondSubsequent(null, 0); this.respondSubsequent(null, 1); res.removeListenerById(handlerId); this.respondSubsequent(null, 2); this.assertCalledTwice(res.refresh); }, // // Events // "test: fire actionSuccess"() { 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.response); that.assertIdentical(req, e.request); that.assertEquals("get", e.action); } ); }, "test: fire success"() { 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.response); that.assertIdentical(req, e.request); that.assertEquals("get", e.action); } ); }, "test: fire actionError"() { var res = this.res, req = this.req, that = this; res.get(); this.assertEventFired( res, "getError", function () { that.respondError(); }, function (e) { that.assertIdentical(req, e.request); that.assertEquals("get", e.action); } ); }, "test: fire error"() { var res = this.res, req = this.req, that = this; res.get(); this.assertEventFired( res, "error", function () { that.respondError(); }, function (e) { that.assertIdentical(req, e.request); that.assertEquals("get", e.action); } ); }, "test: fire started"() { qx.bom.request.SimpleXhr.restore(); var res = this.res, req = this.req, that = this; var listener = this.spy(); res.on("started", listener); res.get(); window.setTimeout( function () { this.resume(function () { this.assertTrue(listener.calledOnce); }, this); }.bind(this), 200 ); this.wait(500); }, // // Dispose // "test: dispose requests"() { 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"() { 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"() { var res = this.res, req = this.req; this.spy(req, "dispose"); res.get(); this.respond(); window.setTimeout( function () { this.resume(function () { this.assertCalledOnce(req.dispose); }, this); }.bind(this), 100 ); this.wait(200); }, assertSend(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); }, // Fake response respond(response, req) { req = req || this.req; response = response || ""; req.isDone.returns(true); req.getResponse.returns(response); req.emit("success"); req.emit("loadEnd"); }, // Fake response but find and manipulate matching requests *within* res // which is important for tests with more than one request (e.g. poll and long poll) respondSubsequent(response, reqIdx, shouldStubResp) { var response = response || "", validReqIdx = reqIdx !== undefined; // this.res.__requests isn't available after 'privates' optimization // so find it by some kind of feature detection - this isn't beautiful, // but adding a protected getter just for that is worse var requests = ""; Object.keys(this.res).forEach(function (propName) { if ( propName.indexOf("__") === 0 && "get" in this.res[propName] && qx.lang.Type.isArray(this.res[propName].get) && qx.lang.Type.isObject(this.res[propName].get[0]) && "$$hash" in this.res[propName].get[0] ) { requests = propName; } }, this); if (validReqIdx && requests) { var reqWithin = this.res[requests].get[reqIdx]; if (shouldStubResp) { this.stub(reqWithin, "isDone"); this.stub(reqWithin, "getResponse"); reqWithin.isDone.returns(true); reqWithin.getResponse.returns(response); } reqWithin.emit("success"); reqWithin.emit("loadEnd"); this.res[requests].get[reqIdx] = reqWithin; } }, // Fake erroneous response respondError() { var req = this.req; req.emit("fail"); req.emit("loadEnd"); } } });