cmorten / superoak

HTTP assertions for Oak made easy via SuperDeno. ๐Ÿฟ ๐Ÿฆ•
https://cmorten.github.io/superoak/
MIT License
121 stars 8 forks source link

How to test cookie based sessions? #21

Closed thesmart closed 3 years ago

thesmart commented 3 years ago

How to test cookie based sessions?

Setup:

Details

Hello, I'm trying to figure out how to use superoak to test multiple routes that would sign-up and sign-in a user via session cookie. Is there any way to read headers from response and pass them to a request?

cmorten commented 3 years ago

Hi @thesmart ๐Ÿ‘‹

superoak ultimately wraps superdeno and superagent to expose the API for Oak servers.

Setting headers can be performed via the .set() method https://visionmedia.github.io/superagent/#setting-header-fields and accessible via the response object in the .end() or .then() methods https://github.com/asos-craigmorten/superdeno#endfn, https://visionmedia.github.io/superagent/#response-properties.

superoak does not save cookies by default in keeping with superagent for Node https://visionmedia.github.io/superagent/#agents-for-global-state. If having a persistent cookie jar is what youโ€™re requesting please let me know and can look into that.

I think what you want can be achieved by reading the header response from your first request, extracting the value and then passing as a header to your second request ๐Ÿ™‚ though being honest Iโ€™m not 100% clear if that is what you want to achieve ( need coffee! ) ๐Ÿ˜… if so then could start with that, and exposing an agent which can automatically handle cookies for you in future can be the next goal for this project.

If Iโ€™m completely off the mark, please can you provide a small code snippet ( code block or gist etc ) for the server ( or sim ) youโ€™re trying to test?

thesmart commented 3 years ago

Thanks, @cmorten! I didn't realize how SuperOak, SuperDeno and SuperAgent overlapped, but now it seems obvious from your great answer and I wonder now why I couldn't figure this out last night. Probably too tired. ๐Ÿ˜… Your answer makes it clear how I can move forward. ๐Ÿ‘

I think I confused myself when I realized the superoak(app) instances can't be reused due to the network is offline issue mentioned in docs. This morning, I was curious if I could patch that network is offline issue, but realized it's probably due to Oak's architecture. Oak relies on serve to produce requests, and those requests are processed only after .listen() is awaited on. So SuperOak has to close via abort signal in order for the assertion chain to be evaluated. This is SuperAwkward (ha!). It would be great if Oak didn't require a network connection for testing.

Example:

class MockServer {
  serverClosed = false;
  requestStack: ServerRequest[];

  constructor(requestStack: ServerRequest[]) {
    this.requestStack = requestStack;
  }

  close(): void {
    this.serverClosed = true;
  }

  async *[Symbol.asyncIterator]() {
    for await (const request of this.requestStack) {
      yield request;
    }
  }
}

/**
 * Make a mock Oak application that can process requests
 * and middleware for testing purposes.
 *
 * ```js
 * const app = new MockApplication();
 * app.use(middleware);
 * app.makeRequest("/"
 * ```
 */
export class MockApplication extends Application {
  requestStack: ServerRequest[] = [];
  responseStack: ServerResponse[] = [];

  constructor() {
    super({
      serve: (addr: string | ListenOptions): Server => {
        return new MockServer(this.requestStack) as Server;
      }
    });
  }

  makeRequest(
    url = "/",
    headersInit: string[][] = [["host", "example.com"]]
  ) {
    this.requestStack.push({
      url,
      headers: new Headers(headersInit),
      respond: (response: ServerResponse) => {
        this.responseStack.push(response);
        return Promise.resolve();
      },
      proto: "HTTP/1.1"
    } as any);
  }

  async listen() {
    // this port doesn't actually get used
    await super.listen(":1337")
    // clear the request stack after handling them
    while (this.requestStack.pop()) {};
  }
}

Deno.test("MockApplication can be used to test middleware.", async function() {
  const app = new MockApplication();
  let middlewareCalled = 0;
  app.use((ctx: Context, next: () => Promise<void>) => {
    middlewareCalled += 1;
  })

  app.makeRequest();
  app.makeRequest();
  await app.listen();
  assertEquals(middlewareCalled, 2);

  app.makeRequest();
  await app.listen();
  assertEquals(middlewareCalled, 3);
})

This is still pretty awkward, but maybe could be made better by composing the assertions like how SuperAgent does it.

thesmart commented 3 years ago

Closing this issue because it's not an "issue" and more of a conversation.