Skip to content

Commit

Permalink
feat: support Express
Browse files Browse the repository at this point in the history
  • Loading branch information
cmorten committed Jan 28, 2024
1 parent 16865c8 commit 4411199
Show file tree
Hide file tree
Showing 11 changed files with 1,195 additions and 51 deletions.
44 changes: 38 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ HTTP assertions for Deno made easy via <a href="https://github.com/visionmedia/s
- [Getting Started](#getting-started)
- [About](#about)
- [Installation](#installation)
- [Example](#example)
- [Examples](#examples)
- [Documentation](#documentation)
- [API](#api)
- [Notes](#notes)
Expand Down Expand Up @@ -87,7 +87,7 @@ a package registry for Deno on the Blockchain.

> Note: All examples in this README are using the unversioned form of the import URL. In production you should always use the versioned import form such as `https://deno.land/x/superdeno@4.9.0/mod.ts`.
## Example
## Examples

You may pass a url string,
[`http.Server`](https://doc.deno.land/https/deno.land/std/http/mod.ts#Server), a
Expand Down Expand Up @@ -117,7 +117,7 @@ Here's an example of SuperDeno working with the Opine web framework:
```ts
import { superdeno } from "https://deno.land/x/superdeno/mod.ts";
import { opine } from "https://deno.land/x/opine@2.3.4/mod.ts";
export { expect } from "https://deno.land/x/expect@v0.4.0/mod.ts";
import { expect } from "https://deno.land/x/expect@v0.4.0/mod.ts";

const app = opine();

Expand All @@ -129,19 +129,49 @@ Deno.test("it should support regular expressions", async () => {
await superdeno(app)
.get("/")
.expect("Content-Type", /^application/)
.end((err) => {
.catch((err) => {
expect(err.message).toEqual(
'expected "Content-Type" matching /^application/, got "text/html; charset=utf-8"'
);
});
});
```

See more examples in the [Opine test suite](./test/superdeno.opine.test.ts).

Here's an example of SuperDeno working with the Express web framework:

```ts
import { superdeno } from "https://deno.land/x/superdeno/mod.ts";
// @deno-types="npm:@types/express@^4.17"
import express from "npm:express@4.18.2";
import { expect } from "https://deno.land/x/expect@v0.4.0/mod.ts";

Deno.test("it should support regular expressions", async () => {
const app = express();

app.get("/", (_req, res) => {
res.send("Hello Deno!");
});

await superdeno(app)
.get("/")
.expect("Content-Type", /^application/)
.catch((err) => {
expect(err.message).toEqual(
'expected "Content-Type" matching /^application/, got "text/html; charset=utf-8"'
);
});
});
```

See more examples in the [Express test suite](./test/superdeno.express.test.ts).

Here's an example of SuperDeno working with the Oak web framework:

```ts
import { superdeno } from "https://deno.land/x/superdeno/mod.ts";
import { Application, Router } from "https://deno.land/x/oak@v10.0.0/mod.ts";
import { Application, Router } from "https://deno.land/x/oak@v12.6.2/mod.ts";

const router = new Router();
router.get("/", (ctx) => {
Expand Down Expand Up @@ -171,6 +201,8 @@ Deno.test("it should support the Oak framework", () => {
});
```

See more examples in the [Oak test suite](./test/superdeno.oak.test.ts).

If you are using the [Oak](https://github.com/oakserver/oak/) web framework then
it is recommended that you use the specialized
[SuperOak](https://github.com/cmorten/superoak) assertions library for
Expand All @@ -181,7 +213,7 @@ are making use of the `app.handle()` method (for example for serverless apps)
then you can write slightly less verbose tests for Oak:

```ts
import { Application, Router } from "https://deno.land/x/oak@v10.0.0/mod.ts";
import { Application, Router } from "https://deno.land/x/oak@v12.6.2/mod.ts";
import { superdeno } from "https://deno.land/x/superdeno/mod.ts";

const router = new Router();
Expand Down
1 change: 1 addition & 0 deletions deps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export type { StatusCode } from "https://deno.land/std@0.213.0/http/status.ts";
export { assertEquals } from "https://deno.land/std@0.213.0/assert/mod.ts";
export { methods } from "https://deno.land/x/opine@2.3.4/src/methods.ts";
export { mergeDescriptors } from "https://deno.land/x/opine@2.3.4/src/utils/mergeDescriptors.ts";
export { getFreePort } from "https://deno.land/x/free_port@v1.2.0/mod.ts";

// TODO: upgrade to v8
export { default as superagent } from "https://jspm.dev/superagent@6.1.0";
140 changes: 113 additions & 27 deletions src/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,29 @@
* - https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/supertest/index.d.ts
*/

import type { ListenerLike, ServerLike } from "./types.ts";
import { assertEquals, STATUS_TEXT, StatusCode } from "../deps.ts";
import type {
ExpressListenerLike,
ExpressServerLike,
ListenerLike,
ServerLike,
} from "./types.ts";
import { assertEquals, getFreePort, STATUS_TEXT, StatusCode } from "../deps.ts";
import { superagent } from "./superagent.ts";
import { close } from "./close.ts";
import { isListener, isServer, isStdNativeServer, isString } from "./utils.ts";
import {
isExpressListener,
isExpressServer,
isListener,
isServer,
isStdNativeServer,
isString,
} from "./utils.ts";
import { exposeSham } from "./xhrSham.js";

export function random(min: number, max: number): number {
return Math.round(Math.random() * (max - min)) + min;
}

/**
* Custom expectation checker.
*/
Expand Down Expand Up @@ -201,9 +217,11 @@ export class Test extends SuperRequest {
#redirects: number;
#redirectList: string[];
#server!: ServerLike;
#serverSetupPromise: Promise<void>;
#urlSetupPromise: Promise<void>;

public app: string | ListenerLike | ServerLike;
public url: string;
public url!: string;

constructor(
app: string | ListenerLike | ServerLike,
Expand All @@ -220,8 +238,21 @@ export class Test extends SuperRequest {
this.app = app;
this.#asserts = [];

let serverSetupPromiseResolver!: () => void;
let addressSetupPromiseResolver!: () => void;

this.#serverSetupPromise = new Promise<void>((resolve) => {
serverSetupPromiseResolver = resolve;
});
this.#urlSetupPromise = new Promise<void>((resolve) => {
addressSetupPromiseResolver = resolve;
});

if (isString(app)) {
this.url = `${app}${path}`;

serverSetupPromiseResolver();
addressSetupPromiseResolver();
} else {
if (isStdNativeServer(app)) {
const listenAndServePromise = app.listenAndServe().catch((err) =>
Expand All @@ -240,18 +271,60 @@ export class Test extends SuperRequest {
addrs: app.addrs,
async listenAndServe() {},
};

serverSetupPromiseResolver();
} else if (isExpressServer(app)) {
this.#server = app as ExpressServerLike;

const expressResolver = async () => {
await new Promise((resolve) => setTimeout(resolve, 1));
serverSetupPromiseResolver();
};

if (!this.#server.listening) {
(this.#server as ExpressServerLike).once(
"listening",
expressResolver,
);
} else {
expressResolver();
}
} else if (isServer(app)) {
this.#server = app as ServerLike;

serverSetupPromiseResolver();
} else if (isExpressListener(app)) {
secure = false;

const expressResolver = async () => {
await new Promise((resolve) => setTimeout(resolve, 1));
serverSetupPromiseResolver();
};

getFreePort(random(1024, 49151)).then(
(freePort) => {
this.#server = (app as ExpressListenerLike).listen(
freePort,
expressResolver,
);
},
);
} else if (isListener(app)) {
secure = false;

this.#server = (app as ListenerLike).listen(":0");

serverSetupPromiseResolver();
} else {
serverSetupPromiseResolver();
addressSetupPromiseResolver();

throw new Error(
"superdeno is unable to identify or create a valid test server",
);
}

this.url = this.#serverAddress(path, host, secure);
this.#setServerAddress(addressSetupPromiseResolver, path, host, secure);
}
}

Expand All @@ -265,19 +338,28 @@ export class Test extends SuperRequest {
* @returns {string} URL address
* @private
*/
#serverAddress = (
#setServerAddress = async (
addressSetupPromiseResolver: () => void,
path: string,
host?: string,
secure?: boolean,
) => {
await this.#serverSetupPromise;

const address =
("addrs" in this.#server
? this.#server.addrs[0]
: "address" in this.#server
? this.#server.address()
: this.#server.listener.addr) as Deno.NetAddr;

const port = address.port;
const protocol = secure ? "https" : "http";
const url = `${protocol}://${(host || "127.0.0.1")}:${port}${path}`;

return `${protocol}://${(host || "127.0.0.1")}:${port}${path}`;
this.url = url;

addressSetupPromiseResolver();
};

/**
Expand Down Expand Up @@ -455,29 +537,33 @@ export class Test extends SuperRequest {
* @public
*/
end(callback?: CallbackHandler): this {
const self = this;
const end = SuperRequest.prototype.end;

end.call(
self,
function (err: any, res: any) {
// Before we close, ensure that we have handled all
// requested redirects
const redirect = isRedirect(res?.statusCode);
const max: number = (self as any)._maxRedirects;

if (redirect && self.#redirects++ !== max) {
return self.#redirect(res, callback);
}
Promise.allSettled([this.#serverSetupPromise, this.#urlSetupPromise]).then(
() => {
const self = this;
const end = SuperRequest.prototype.end;

end.call(
self,
function (err: any, res: any) {
// Before we close, ensure that we have handled all
// requested redirects
const redirect = isRedirect(res?.statusCode);
const max: number = (self as any)._maxRedirects;

if (redirect && self.#redirects++ !== max) {
return self.#redirect(res, callback);
}

return close(self.#server, self.app, undefined, async () => {
await completeXhrPromises();
return close(self.#server, self.app, undefined, async () => {
await completeXhrPromises();

// REF: https://github.com/denoland/deno/blob/987716798fb3bddc9abc7e12c25a043447be5280/ext/timers/01_timers.js#L353
await new Promise((resolve) => setTimeout(resolve, 20));
// REF: https://github.com/denoland/deno/blob/987716798fb3bddc9abc7e12c25a043447be5280/ext/timers/01_timers.js#L353
await new Promise((resolve) => setTimeout(resolve, 20));

self.#assert(err, res, callback);
});
self.#assert(err, res, callback);
});
},
);
},
);

Expand Down
16 changes: 15 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,22 @@ export interface NativeServerLike {
close(): void;
}

export type ServerLike = LegacyServerLike | NativeServerLike;
export interface ExpressServerLike {
address(): any;
listening: boolean;
close(): void;
once(eventName: string, listener: () => void): void;
}

export type ServerLike =
| LegacyServerLike
| NativeServerLike
| ExpressServerLike;

export interface ListenerLike {
listen(addr: string): ServerLike;
}

export interface ExpressListenerLike {
listen(port: number, callback: () => void): ServerLike;
}
17 changes: 16 additions & 1 deletion src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type {
ExpressListenerLike,
LegacyServerLike,
ListenerLike,
NativeServerLike,
Expand All @@ -11,6 +12,14 @@ export const isString = (thing: unknown): thing is string =>
export const isListener = (thing: unknown): thing is ListenerLike =>
thing instanceof Object && thing !== null && "listen" in thing;

export const isExpressListener = (
thing: unknown,
): thing is ExpressListenerLike =>
thing instanceof Object && thing !== null && "locals" in thing &&
"mountpath" in thing && "all" in thing && "engine" in thing &&
"listen" in thing && "param" in thing && "path" in thing &&
"render" in thing && "route" in thing && "set" in thing && "use" in thing;

const isCommonServer = (thing: unknown): thing is ServerLike =>
thing instanceof Object && thing !== null && "close" in thing;

Expand All @@ -22,5 +31,11 @@ export const isStdNativeServer = (thing: unknown): thing is NativeServerLike =>
isCommonServer(thing) &&
"addrs" in thing;

export const isExpressServer = (thing: unknown): thing is NativeServerLike =>
isCommonServer(thing) &&
"listening" in thing &&
"address" in thing && typeof thing.address === "function";

export const isServer = (thing: unknown): thing is ServerLike =>
isStdLegacyServer(thing) || isStdNativeServer(thing);
isStdLegacyServer(thing) || isStdNativeServer(thing) ||
isExpressServer(thing);
Loading

0 comments on commit 4411199

Please sign in to comment.