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

Access request within HttpTest RespondWith for dynamic response #673

Open
kkd83 opened this issue Feb 3, 2022 · 4 comments
Open

Access request within HttpTest RespondWith for dynamic response #673

kkd83 opened this issue Feb 3, 2022 · 4 comments

Comments

@kkd83
Copy link

kkd83 commented Feb 3, 2022

Not all responses can be purely static, some are dynamic based on the request.

For example, a user defines a request identifier in the body/header and expects the response to include this.

using var httpTest = new HttpTest();
httpTest
    .ForCallsTo("github.com")
    .RespondWith(HttpRequestMessage request => {
       // create a body based on the request
       var body = request.Headers["RequestIdentifier"]
       return new CapturedStringContent(body);
    })

There are lots of good methods for filtering the request but none as far as I could see for creating a dynamic response.

Potential changes required

No breaking changes are needed

FakeHttpMessageHandler:

    var resp = setup.GetNextResponse(request) ?? ...

HttpTestSetup:

    private readonly List<Func<HttpRequestMessage, HttpResponseMessage>> _responses = new List<Func<HttpRequestMessage, HttpResponseMessage>>();
...
    internal HttpResponseMessage GetNextResponse(HttpRequestMessage request) {
        if (!_responses.Any())
            return null;
            // atomically get the next response in the list, or the last one if we're past the end
            return _responses[Math.Min(Interlocked.Increment(ref _respIndex), _responses.Count) - 1](request);
    }

    // removal of null from buildContent
    public HttpTestSetup RespondWith(Func<HttpContent> buildContent, int status = 200, object headers = null, object cookies = null, bool replaceUnderscoreWithHyphen = true) {
        return RespondWith(request => buildContent?.Invoke(), status, headers, cookies, replaceUnderscoreWithHyphen);
    }

    // new method
    public HttpTestSetup RespondWith(Func<HttpRequestMessage, HttpContent> buildContent = null, int status = 200, object headers = null, object cookies = null, bool replaceUnderscoreWithHyphen = true) {
    return RespondWith(request => {
        var response = new HttpResponseMessage {
            StatusCode = (HttpStatusCode)status,
            Content = buildContent?.Invoke(request)
        };
    }
    
    // absolute base method
    public HttpTestSetup RespondWith(Func<HttpRequestMessage, HttpResponseMessage> buildResponse) {
        _responses.Add(buildResponse);
        return this;
    }
kkd83 added a commit to kkd83/Flurl that referenced this issue Feb 15, 2022
Allow HttpTest.RespondWith to access the request when constructing the response.
@tmenier
Copy link
Owner

tmenier commented May 6, 2022

Not all responses can be purely static, some are dynamic based on the request.

I'm not sure I agree with your premise, because we're specifically within the context of testing, where your inputs are "known" (usually hard-coded), as are your expected results. If you know the details of the request that will be made (and in a test you should), then you should be able to set up the fake response "statically". Maybe I'm missing something?

@kkd83
Copy link
Author

kkd83 commented May 7, 2022

I agree that the request and response should be well known but may include data that is automatically generated, that needs to be included in the response.
Consider the surname service:
GET /pls-give-me-a-surname
{
“request-id”: “0a72836b-5b75-402f-8417-2e20e92a0fcc”,
“name”: “bob”
}
Response can’t be purely static, as it must include that request-id which isn’t know about before.
{
“request-id”: “????”,
“name”: “bob”,
“surname”: “Smith”
}

@tmenier
Copy link
Owner

tmenier commented May 12, 2022

That makes sense. Could be scenarios where this simplifies test setup too. I don't know that I love how RespondWith deals with raw HttpRequestMessage and HttpResponseMessage since Flurl provides wrappers, but that's partly my fault for how I did the existing implementation. So this could be a 4.0 thing (breaking change), which I plan to start on soon.

@RytisLT
Copy link

RytisLT commented May 15, 2024

I have another example where this is relevant. The code that I'm testing is parallel, even though the responses are known, I do not known the order in which they will be called. The following code illustrates the problem. I'm basically making 100 parallel calls and saying that response body should equal to request body

public class FlurlTest
{
    [Fact]
    public async Task AsyncCalls()
    {
        var httpTest = new HttpTest();

        httpTest
            .ForCallsTo("http://example.com/give-me")
            .WithVerb(HttpMethod.Post)
            .RespondWith(() =>
            {
                var body = HttpTest.Current.CallLog.Last().RequestBody;
                var value = JsonConvert.DeserializeObject<string>(body);
                return new StringContent(value!);
            });

        var tasks = new List<Task<string>>();
        for (var i = 0; i < 100; i++)
        {
            var body = i;
            var task = Task.Run(() =>
            {
                var response = "http://example.com/give-me".PostJsonAsync($"{body}").Result;
                var result = response.GetStringAsync().Result;
                return result!;
            });
            tasks.Add(task);
        }


        await Task.WhenAll(tasks);

        for (var i = 0; i < tasks.Count; i++)
        {
            var response = await tasks[i];
            Assert.Equal(i.ToString(), response);
        }
    }
}

Ideally I would like to receive HttpRequest or FlurlCall in RespondWith like so .RespondWith( request => { ... } );

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

No branches or pull requests

3 participants