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

Add thenCallback method #2

Merged
merged 10 commits into from
Mar 4, 2018
Merged

Conversation

APshenkin
Copy link

@APshenkin APshenkin commented Feb 11, 2018

thenCallback method allow to complete some operations with request before sending response.

example of usage:

await server.get("/mocked-endpoint").thenCallback(req => {
 if (req.body.myVar = 'foo') {
  return {status: 200, body: {obj: req.body.myVar}, headers: {myHeader: 'foo'}}
 }
 return {status: 404, body: 'not found'}
});

Wait for your thought and suggestions

this PR should be merged after #1

Copy link
Member

@pimterry pimterry left a comment

Choose a reason for hiding this comment

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

Nice, I can definitely see how this would be useful, it's been in my plan for a little while but I haven't had any time to get to it, thanks for taking a look! I've added a few comments.

I'd like to ensure as much as possible that users don't need to use this though, so if there are cases where you're hitting this, do file an issue for the kind of handler you'd need to be built in, and we can look at that in future. E.g. in your example above we could have a json matcher for the body, and a json template responder like:

mockServer.get('/mocked-endpoint')
.withJsonMatching({ myVar: 'foo' })
.thenReplyWithTemplate(200, {
  obj: '{{ body.myVar }}'
});

Exact template format needs thought, but you get the idea. A real handler like this can be built to run faster, we can make it work more consistently across platforms etc etc etc, but mainly it just avoids lots of people all reimplementing the same kinds of callbacks.

There's an extra bonus issue here too, that I suspect you'll hit when you try to add tests: this is quite tricky to implement in browsers. To do this well you need some kind of persistent connection, where the server can ask the browser what to do, and hold the request until it responds.

For this first step we can ignore that imo (wrap tests with nodeOnly(() => { ... }), mention this in the docs, and make sure it throws something helpful if you try to use it in a browser). As long as we make sure the interface is something we can implement for browsers in future I'm fine with that, and I think this should work pretty easily as is. Nice work!

@@ -27,7 +29,16 @@ export class SimpleHandlerData {

constructor(
public status: number,
public data?: string
public data?: string,
public headers?: http.OutgoingHttpHeaders
Copy link
Member

Choose a reason for hiding this comment

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

Looks like this is still on top of the headers PR - if you rebase on the latest master now these changes should disappear.

@@ -25,7 +25,8 @@ import {
WildcardMatcherData
} from "./matchers";

import { SimpleHandlerData, PassThroughHandlerData } from "./handlers";
Copy link
Member

Choose a reason for hiding this comment

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

Just fyi - partial mock rule has now been renamed to mock rule builder (git's conflict resolution should be able to magically handle that for you if you rebase), but there might be some conflicts because I've added jsdoc to all the methods through here. Should be easy to sort out, just a heads up.

};

return this.addRule(rule);
}

thenCallback(callback: Function): Promise<MockedEndpoint> {
const rule: MockRuleData= {
Copy link
Member

Choose a reason for hiding this comment

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

A space before the = here would be nice


let response = await fetch(server.urlFor("/mocked-endpoint"));

expect(await response.headers.get("myHeader")).to.equal("foo");
Copy link
Member

Choose a reason for hiding this comment

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

This is just the test for the headers - it'd be good to add one for thenCallback too. There's now a separate handlers.spec.ts for that (which I've moved this header test into).

formData = await request.body.asFormData();
} catch (err) {
formData = undefined;
}
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 we should have all this body parsing logic here. It'd be nice to have the callback be given requests in basically the same format as request verification, and to share the code to do that. Right now that's over here:

https://github.com/pimterry/mockttp/blob/4b21b6b9f401d371abbcee3c3557b37bea2de4b8/src/rules/mock-rule.ts#L60-L74

We carefully avoid doing this before the handler normally, because if we're passing the request through we want to avoid reading the entire body into memory unnecessarily, and it means we can start matching & responding before the request body has even arrived. Here though that's less important of course, as most callbacks are going to do that anyway.

I think we should pull that logic out to turn this into request = waitForCompletedRequest(ongoingRequest), and put the shared logic to somewhere else (maybe in parse-body?), so we can use it both here and in the above code in mock rule. That saves us a bunch of duplication, and makes the whole interface a bit more consistent.

path: request.path,
headers: request.headers,
body: { buffer, text, json, formData }
}
Copy link
Member

Choose a reason for hiding this comment

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

Why does the request need cleaning?

Copy link
Author

Choose a reason for hiding this comment

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

this is a bad name for variables :) Will rename it

try {
ourResponse = await callback(cleanRequest);
} catch (err) {
throw err;
Copy link
Member

Choose a reason for hiding this comment

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

We should return a clear response with the error here, so it's easier to tell what's gone wrong. This error will appear in the server logs, but it's much easier to debug if we send it as a response (with a failing status code) so it appears in people's test failure message.

Copy link
Author

Choose a reason for hiding this comment

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

What failing status code will be best in this case?

throw err;
}
if (typeof ourResponse.body === 'object') {
ourResponse.body = JSON.stringify(ourResponse.body);
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 we should automatically encode bodies like this. In future there might be other object structured bodies we'd like to support (like XML or something), and it's a bit too magic. I'd be fine with something a bit more explicit though, like a .json property:

if (!!ourResponse.json) {
    outResponse.headers['Content-Type'] = outResponse.headers['Content-Type'] || 'application/json';
	ourResponse.body = JSON.stringify(ourResponse.json);
}

readonly type: 'callback' = 'callback';

constructor(
public callback: Function
Copy link
Member

Choose a reason for hiding this comment

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

We should be more specific here than just Function, so we can check usage and properly document what you're allowed here. I'd suggest:

public callback: (request: CompletedRequest) => {
  status?: number;
  body?: string;
  json?: any;
  headers: {
    [key: string]: string;
  };
}

@APshenkin
Copy link
Author

Just one more tip about why this feature is necessary.

If you want to mock any SOAP service, than you have to implement logic in callback to receive different responses in different cases. thenReplyWithTemplate will not allow to do that. Because of this I created this PR. In our project we want to mock SOAP service, but currently it's not possible(

@APshenkin
Copy link
Author

@pimterry is there any blockers to merge this feature and update mockttp?

@pimterry
Copy link
Member

pimterry commented Mar 2, 2018

No specific blockers, I just need time to sit down and review it again. Life's got a bit hectic, but I should be able to sort this this weekend.

@pimterry
Copy link
Member

pimterry commented Mar 4, 2018

I've made a couple more tweaks here, but otherwise this all looks good to me now - I'll merge in a sec once the tests pass.

@pimterry pimterry changed the title [WIP] Add thenCallback method Add thenCallback method Mar 4, 2018
@pimterry pimterry merged commit a12a44c into httptoolkit:master Mar 4, 2018
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

Successfully merging this pull request may close these issues.

2 participants