Skip to content
This repository has been archived by the owner on Apr 12, 2024. It is now read-only.

fix(ngMock/$httpBackend): correctly ignore query params in {expect,when}Route #16589

Closed
wants to merge 1 commit into from

Conversation

thorn0
Copy link
Contributor

@thorn0 thorn0 commented Jun 3, 2018

What kind of change does this PR introduce? (Bug fix, feature, docs update, ...)

fix

What is the current behavior? (You can also link to an open issue here)

#14173

Does this PR introduce a breaking change?

no

Please check if the PR fulfills these requirements

  • The commit message follows our guidelines
  • Fix/Feature: Docs have been added/updated
  • Fix/Feature: Tests have been added; existing tests pass

Copy link
Member

@gkalpak gkalpak left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, I think it would be better to re-use the method used in ngRoute (see #14173 (comment)).
It would be similar to how we re-use shallowCopy() (configured in angularFiles.js).

@@ -1680,14 +1681,14 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
+ (optional ? '' : slash)
+ '(?:'
+ (optional ? slash : '')
+ (star && '(.+?)' || '([^/]+)')
+ (star ? '(.+?)' : '([^/?]+)')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe also add # (just in case).

@thorn0
Copy link
Contributor Author

thorn0 commented Jun 4, 2018

@gkalpak Where is it better to place the file with this function? src? src/ngRoute?

@gkalpak
Copy link
Member

gkalpak commented Jun 4, 2018

I would put it in src/ (since it will be used in different modules).

@thorn0 thorn0 force-pushed the whenRoute branch 9 times, most recently from f984b29 to 04024ed Compare June 6, 2018 08:55
@thorn0
Copy link
Contributor Author

thorn0 commented Jun 7, 2018

@gkalpak PTAL

Copy link
Member

@gkalpak gkalpak left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great 👍
Just a couple of minor things.

@@ -1481,8 +1483,9 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
* ```
* – The respond method takes a set of static data to be returned or a function that can
* return an array containing response status (number), response data (Array|Object|string),
* response headers (Object), and the text for the status (string). The respond method returns
* the `requestHandler` object for possible overrides.
* response headers (Object), HTTP status text (string), and XMLHttpRequest status (string:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it get XHR status? AFAICT it doesn't (because it is auto-generated).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It doesn't get it. It returns it. See http://jsbin.com/vumefiz/edit?html,js,output

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I misread that 😳

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, our createResponse() method isn't consistently treating XHR status. It does not accept one when passing the data directly to .respond() (and it automatically uses completeas the XHR status), but it does accept one when a function is passed to createResponse().

I think this is a bug (or at least unintentional). createResponse() should look something like:

 function createResponse(status, data, headers, statusText) {
-  if (angular.isFunction(status)) return status;
+  if (angular.isFunction(status)) {
+    return function(method, url, data, headers, params) {
+      var response = status(method, url, data, headers, params);
+      response[4] = 'complete';
+      return response;
+    };
+  }

   return function() {
     return angular.isNumber(status)
         ? [status, data, headers, statusText, 'complete']
         : [200, status, data, headers, 'complete'];
   };
 }

But this is unrelated to this PR and would probably be a breaking change now 😞

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you think so? What if we want to test the behavior of our app with XHR statuses other than complete (e.g. error)?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But (like I said), this is not something to worry about in this PR. It is not related (and that ship has probably sailed 😁).

Copy link
Contributor Author

@thorn0 thorn0 Jun 8, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can't have error with a "normal" response (it is something that will not come up in the actual app

I believe it can. It happens in case of network error, doesn't it?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that's not a "normal" response. I.e. an XHR status other than complete can only happen if the XHR's .onabort/.onerror/.ontimeout is called. In that case, you would have a status of 0 and empty values for other fields (statusText, headers, etc).

But the mock $httpBackend is not really designed to let you test these scenarios. Sure you can emulate it by returning the correct values from your response function, you need to know $httpBackend internals. E.g. returning [200, 'OK', ..., 'aborted'] is a scenario that will never happen in the actual app and will lead to unexpected results (because aborted will have been called with a "success" status).

So, ideally, we should not let users specify the XHR status, but instead infer it from other params.
Even more ideally, $httpBackend could support testing scenarions when an XHR errored, or timed out or was aborted (and again automatically setting the correct XHR status; not letting the user specify it).

But as it is now, $httpBackend is better used with "normal" responses (unless you really know what you are doing) 😛

Copy link
Contributor Author

@thorn0 thorn0 Jun 8, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In a separate PR, we can add simple checks and throw in cases like [200, 'OK', ..., 'abort']. What are your thoughts on that?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wouldn't be opposed to that 😃

* – The respond method takes a set of static data to be returned or a function that can
* return an array containing response status (number), response data (Array|Object|string),
* response headers (Object), and the text for the status (string). The respond method returns
* the `requestHandler` object for possible overrides.
* response headers (Object), HTTP status text (string), and XMLHttpRequest status (string:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, I don't think it gets the XHR status.

.replace(/(\/)?:(\w+)(\*\?|[?*])?/g, function(_, slash, key, option) {
var optional = option === '?' || option === '*?';
var star = option === '*' || option === '*?';
keys.push({ name: key, optional: !!optional });
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

!!optional --> optional (now that optional is boolean)

* Inspired by pathRexp in visionmedia/express/lib/utils.js.
*/
function routeToRegExp(path, opts) {
if (!angular.isString(path)) path = '';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AFAICT, this function would never be called with something that is not a string (if people followed the docs 😁).
I suggest we remove this line (and fail hard if path is not a string, so that we can detect issues quicker), instead of covering it up with a route that does not correspond to the passed in path.

hb[routeShortcut](this, '/route/:id').respond(function(method, url, data, headers, params) {
paramsSpy(params);
// status, response, headers, statusText, xhrStatus
return [200, 'path', { 'x-header': 'foo' }, 'OK', 'complete'];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think complete is used (here and below).

Copy link
Contributor Author

@thorn0 thorn0 Jun 7, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm verifying that params are correct. Without the fix, paramSpy is called with {id: '123?q=str&foo=bar', q: 'str', foo: 'bar'}. So I'm testing exactly what is said in the name of the test: "should ignore query params when matching".

@@ -1943,10 +1943,30 @@ describe('ngMock', function() {
);
they('should ignore query param when matching in ' + routeShortcut + ' $prop method', methods,
function() {
hb[routeShortcut](this, '/route/:id').respond('path');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why the changes in this test? What are you trying to verify?

they('should ignore query param when matching eager parameters in ' + routeShortcut + ' $prop method', methods,
function() {
var paramsSpy = jasmine.createSpy('paramsSpy');
hb[routeShortcut](this, '/route/:id*').respond(function(method, url, data, headers, params) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about adding a test without the * (for good measure)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I misread this question before. There already is a test without *. I added a test with *. I thought you were asking about a test without any routing parameters at all.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🙈

@thorn0
Copy link
Contributor Author

thorn0 commented Jun 8, 2018

@gkalpak Should the URL /route/123/ match the route /route/:id?

@gkalpak
Copy link
Member

gkalpak commented Jun 8, 2018

I don't think it matters much (for tests). I don't have a strong opinion (unless changing the current behavior is a breaking change).
(Let's keep the current behavior in this PR though.)

@thorn0
Copy link
Contributor Author

thorn0 commented Jun 8, 2018

The current behavior is a bit unexpected:

ret.regexp = new RegExp('^' + url, 'i');

Which means even /route/123/bla/bla/bla matches the route /route/:id. Do we really want to keep it?

@gkalpak
Copy link
Member

gkalpak commented Jun 8, 2018

No, I meant the behavior wrt trailing /. Matching URLs both with and without a trailing / is reasonable (and is also how $route works by default - which is unrelated, but still 😁).

Matching the bla/bla/bla part is a bug imo. If one wants to do that, they can use a :rest*? segment at the end.

So, I woud vote for matching with and without trailing /, but not match with trailing segments (e.g. bla/bla/bla). WDYT?

@thorn0
Copy link
Contributor Author

thorn0 commented Jun 8, 2018

Okay. Matching the bla/bla/bla part isn't documented, so we can consider it a bug.

@thorn0 thorn0 force-pushed the whenRoute branch 3 times, most recently from ad838e1 to 82785e6 Compare June 9, 2018 09:33
@thorn0
Copy link
Contributor Author

thorn0 commented Jun 9, 2018

@gkalpak PTAL

@gkalpak gkalpak closed this in 840b5f0 Jun 18, 2018
@gkalpak
Copy link
Member

gkalpak commented Jun 18, 2018

Thx, @thorn0 ❤️
(Added some more info in the commit message and merged.)

gkalpak pushed a commit that referenced this pull request Jun 18, 2018
…en}Route

Previously, a route definition such as
`$httpBackend.whenRoute('GET', '/route/:id')` matched against a URL with
query params, for example `/route/1?q=foo`, would incorrectly include
the query params in `id`: `{id: '1?q=foo', q: 'foo'}`.

This commit fixes it, so that the extracted `params` will now be:
`{id: '1', q: 'foo'}`.

Fixes #14173

Closes #16589
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants