Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pound symbols (#) don't seem to be encoded when matching on uri #29

Closed
jimSampica opened this issue Jul 25, 2022 · 7 comments
Closed

Pound symbols (#) don't seem to be encoded when matching on uri #29

jimSampica opened this issue Jul 25, 2022 · 7 comments

Comments

@jimSampica
Copy link

jimSampica commented Jul 25, 2022

When query strings have pound symbols (#) in them the decoded value does not appear to get encoded when determining if it matches the expected uri. I've tried several methods on the api but they all more or less have the same result.

MockHttp.HttpMockException : There are 1 unfulfilled expectations:
    Method: GET, RequestUri: 'http://localhost/controller?param=Hello World%23', QueryString: '?param=Hello%20World%23'
    Seen requests: Method: GET, RequestUri: 'http://localhost/controller?param=Hello World#', Version: 1.1, Content: <null>, Headers:
    {

    }

Repro:

[Fact]
public async Task PoundSymbolRepro()
{
  MockHttpHandler mockHttp = new MockHttpHandler();
  // Configure setup(s).
  mockHttp
    .When(matching => matching
      .Method("GET")
      //None of the following work as expected
      //.RequestUri("http://localhost/controller?param=Hello%20World%23")
      //.RequestUri("http://localhost/controller").QueryString(new NameValueCollection
      //{
      //    { "param", "Hello World#" }
      //})
      .RequestUri("http://localhost/controller").QueryString("?param=Hello%20World%23")
      )
    .Respond(HttpStatusCode.OK, "ok")
    .Verifiable();

    var client = new HttpClient(mockHttp);

    var response = await client.GetAsync("http://localhost/controller?param=Hello World#");
    // Verify expectations.
    mockHttp.Verify();
}
@jimSampica jimSampica changed the title Pound symbols (#) don't seem to be encoded when matching on url Pound symbols (#) don't seem to be encoded when matching on uri Jul 25, 2022
@skwasjer
Copy link
Owner

skwasjer commented Jul 25, 2022

Thanks for reporting. Could it be because the actual request URI sent via the client is not encoded and it is thus considered an URI fragment (and not part of the query string value)?

https://en.wikipedia.org/wiki/URI_fragment

eg. for:

http://localhost/controller?param=Hello World#

scheme: http
host: localhost
path: controller
query string: param=Hello World
uri fragment: empty string

Because if true, you will not be able to intercept this via MockHttp as this is considered client side and not actually sent to the server.

If not, I will have a look at it.

@jimSampica
Copy link
Author

jimSampica commented Jul 25, 2022

If we encode the request uri client side then we get spaces being decoded and pound symbols remain encoded.

e.g. if we change the above repro to be var response = await client.GetAsync("http://localhost/controller?param=Hello%20World%23");

we get this as a result...

MockHttp.HttpMockException : There are 1 unfulfilled expectations:
Method: GET, RequestUri: 'http://localhost/controller', QueryString: '?param=Hello%20World%23'
Seen requests: Method: GET, RequestUri: 'http://localhost/controller?param=Hello World%23', Version: 1.1, Content: <null>, Headers:
{
}

Note the partial decoding in the Seen request uri. I would expect to either see a full decoding or the request uri to remain encoded.

@skwasjer
Copy link
Owner

skwasjer commented Jul 25, 2022

Note the partial decoding in the Seen request uri. I would expect to either see a full decoding or the request uri to remain encoded.

The request URI matcher does consider query string encoding according to RFC-3986. Internally, everything is decoded before a match is performed. What you see in the exception details is just the way HttpRequestMessage.ToString() works.

Please note that the RequestUri matcher verifies the full request URI (see #25). This is currently preventing your test cases to pass. Add a wildcard first:

.RequestUri("http://localhost/controller?*")

It seems to work as intended (provided you do the above), as long as the URL requested via HttpClient 'is' actually encoded properly (if it needs to be part of the query string param). The space character (' ') is a bit of an exceptional case as it is allowed to be %20 or just space ' ', because Uri.UnescapeDataString() supports both. See these theories:

[Theory]
[MemberData(nameof(QueryStringTestCases))]
public async Task MatchingTestCases(Action<RequestMatching> matchQueryString)
{
    MockHttpHandler mockHttp = new MockHttpHandler();
    // Configure setup(s).
    mockHttp
        .When(matching =>
        {
            matchQueryString(matching.RequestUri("http://localhost/controller?*"));
        })
        .Respond(HttpStatusCode.OK, "ok")
        .Verifiable();

    var client = new HttpClient(mockHttp);
    HttpResponseMessage response = await client.GetAsync("http://localhost/controller?param=Hello%20World%23");

    // Verify expectations (WILL PASS).
    mockHttp.Verify();
}

[Theory]
[MemberData(nameof(QueryStringTestCases))]
public async Task NotMatchingTestCases(Action<RequestMatching> matchQueryString)
{
    MockHttpHandler mockHttp = new MockHttpHandler();
    // Configure setup(s).
    _sut
        .When(matching =>
        {
            matchQueryString(matching.RequestUri("http://localhost/controller?*"));
        })
        .Respond(HttpStatusCode.OK, "ok")
        .Verifiable();

    var client = new HttpClient(mockHttp);
    // This won't work as the # is not part of the query param value but actually a fragment
    HttpResponseMessage response = await client.GetAsync("http://localhost/controller?param=Hello World#");

    // Verify expectations (WILL BREAK).
    mockHttp.Verify();
}

public static IEnumerable<object[]> QueryStringTestCases()
{
    yield return new object[] { (Action<RequestMatching>)(m => m.QueryString("?param=Hello%20World%23")) }; // Must encode ourselves with this overload (see wiki)
    yield return new object[] { (Action<RequestMatching>)(m => m.QueryString("?param=Hello World%23")) };   // Must encode ourselves with this overload (see wiki)
    yield return new object[] { (Action<RequestMatching>)(m => m.QueryString("param", "Hello World#")) };
    yield return new object[] { (Action<RequestMatching>)(m => m.QueryString(new Dictionary<string, IEnumerable<string>> { { "param", new[] { "Hello World#" } } })) };
    yield return new object[] { (Action<RequestMatching>)(m => m.QueryString(new NameValueCollection { { "param", "Hello World#" } })) };
}

I will make note to improve the member XML docs and wiki to clarify the above nuances more. It is documented but not very evident and this can definitely be improved.

@skwasjer
Copy link
Owner

As far as this case, I will look into it further:

//.RequestUri("http://localhost/controller?param=Hello%20World%23")

But it could be similar if you were sending with this request:

var response = await client.GetAsync("http://localhost/controller?param=Hello World#");

That said, this matcher 'does' work somewhat different, so I will investigate.

@jimSampica
Copy link
Author

Thank you very much for the in depth responses and the time you've taken to look at this.

Internally, everything is decoded before a match is performed.

If this is true then something I don't understand then is why these two fail to match....

...
.RequestUri("http://localhost/controller").QueryString("?param=Hello%20World%23")
.RequestUri("http://localhost/controller").QueryString(new NameValueCollection
{
    { "param", "Hello World#" }
}
...
var response = await client.GetAsync("http://localhost/controller?param=Hello%20World%23");

The only scenario that matches here is .RequestUri("http://localhost/controller?param=Hello%20World%23")

@skwasjer
Copy link
Owner

skwasjer commented Jul 26, 2022

The issue is the same, the .RequestUri("http://localhost/controller") is not passing the matcher test, because it tests against the full request URI which includes the query string too. You may think that the second query string matcher is failing, but it doesn't even run that matcher if the prior one fails. Adding an explicit query string matcher does not mean the (full) request URI matcher will ignore any query string.

Please note that the RequestUri matcher verifies the full request URI (see #25). This is currently preventing your test cases to pass. Add a wildcard first:

.RequestUri("http://localhost/controller?*")

@jimSampica
Copy link
Author

I understand now and thanks again for your help.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants