diff --git a/.github/workflows/publish-docs.yml b/.github/workflows/publish-docs.yml index c8cf714..4f89e0b 100644 --- a/.github/workflows/publish-docs.yml +++ b/.github/workflows/publish-docs.yml @@ -13,17 +13,17 @@ jobs: - name: Use Node.js uses: actions/setup-node@v1 with: - node-version: 12 + node-version: 20 - name: Install deps run: make deps - name: Use Deno uses: denolib/setup-deno@v2 with: - deno-version: 1.19.3 + deno-version: 1.40.2 - run: make typedoc - run: make ci - name: Publish Updated Type Docs uses: stefanzweifel/git-auto-commit-action@v4 with: commit_message: publish typedocs - push_options: --force \ No newline at end of file + push_options: --force diff --git a/.github/workflows/publish-egg.yml b/.github/workflows/publish-egg.yml index 1971234..60baf6a 100644 --- a/.github/workflows/publish-egg.yml +++ b/.github/workflows/publish-egg.yml @@ -11,7 +11,7 @@ jobs: - uses: actions/checkout@v2 - uses: denolib/setup-deno@v2 with: - deno-version: 1.19.3 + deno-version: 1.40.2 - run: deno install -A -f --unstable -n eggs https://x.nest.land/eggs@0.3.10/eggs.ts - run: | export PATH="/home/runner/.deno/bin:$PATH" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 61882ff..5430b14 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,7 +11,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, macos-latest] - deno-version: [1.19.3] + deno-version: [1.40.2] runs-on: ${{ matrix.os }} @@ -27,7 +27,7 @@ jobs: strategy: matrix: os: [windows-latest] - deno-version: [1.19.3] + deno-version: [1.40.2] runs-on: ${{ matrix.os }} diff --git a/Makefile b/Makefile index cc3c70b..9422de7 100644 --- a/Makefile +++ b/Makefile @@ -12,7 +12,7 @@ ci: @make test deps: - @npm install -g typescript typedoc@0.19.2 + @npm install -g typescript@4 typedoc@0.19.2 doc: @deno doc ./mod.ts @@ -24,7 +24,7 @@ fmt-check: @deno fmt --check ${FILES_TO_FORMAT} lint: - @deno lint --unstable ${FILES_TO_FORMAT} + @deno lint ${FILES_TO_FORMAT} precommit: @make typedoc diff --git a/README.md b/README.md index ec14d72..46c193b 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ HTTP assertions for Deno made easy via SuperDeno latest /x/ version - Minimum supported Deno version + Minimum supported Deno version SuperDeno dependency count SuperDeno dependency outdatedness SuperDeno cached size @@ -31,7 +31,7 @@ HTTP assertions for Deno made easy via 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.8.0/mod.ts`. +> 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 @@ -116,8 +116,8 @@ 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@1.9.1/mod.ts"; -export { expect } from "https://deno.land/x/expect@v0.2.9/mod.ts"; +import { opine } from "https://deno.land/x/opine@2.3.4/mod.ts"; +import { expect } from "https://deno.land/x/expect@v0.4.0/mod.ts"; const app = opine(); @@ -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"', + '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) => { @@ -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 @@ -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(); @@ -194,15 +226,16 @@ const app = new Application(); app.use(router.routes()); app.use(router.allowedMethods()); -Deno.test("it should support the Oak framework `app.handle` method", async () => { - /** - * Note that we have to bind `app` to the function otherwise `app.handle` - * doesn't preserve the `this` context from `app`. - */ - await superdeno(app.handle.bind(app)) - .get("/") - .expect("Hello Deno!"); -}); +Deno.test( + "it should support the Oak framework `app.handle` method", + async () => { + /** + * Note that we have to bind `app` to the function otherwise `app.handle` + * doesn't preserve the `this` context from `app`. + */ + await superdeno(app.handle.bind(app)).get("/").expect("Hello Deno!"); + } +); ``` In this case, SuperDeno handles the setup and closing of the server for you, so diff --git a/deps.ts b/deps.ts index 24c6bca..7b7a725 100644 --- a/deps.ts +++ b/deps.ts @@ -1,6 +1,10 @@ -export { Server } from "https://deno.land/std@0.129.0/http/server.ts"; -export { STATUS_TEXT } from "https://deno.land/std@0.129.0/http/http_status.ts"; -export { assertEquals } from "https://deno.land/std@0.129.0/testing/asserts.ts"; -export { methods } from "https://deno.land/x/opine@2.1.2/src/methods.ts"; -export { mergeDescriptors } from "https://deno.land/x/opine@2.1.2/src/utils/mergeDescriptors.ts"; +export { Server } from "https://deno.land/std@0.213.0/http/server.ts"; +export { STATUS_TEXT } from "https://deno.land/std@0.213.0/http/status.ts"; +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"; diff --git a/docs/classes/_test_.test.html b/docs/classes/_test_.test.html index 9afa94a..8114c18 100644 --- a/docs/classes/_test_.test.html +++ b/docs/classes/_test_.test.html @@ -179,7 +179,7 @@

constructor

Parameters

@@ -213,7 +213,7 @@

Private #asserts

#asserts: any[]
@@ -223,7 +223,7 @@

Private #redirectList

#redirectList: string[]
@@ -233,7 +233,7 @@

Private #redirects

#redirects: number
@@ -243,7 +243,7 @@

Private #server

#server: ServerLike
@@ -253,7 +253,7 @@

app

app: string | ListenerLike | ServerLike
@@ -264,7 +264,7 @@

cookies

@@ -275,7 +275,7 @@

method

@@ -286,7 +286,7 @@

url

@@ -303,7 +303,7 @@

Private #assert

  • @@ -337,7 +337,7 @@

    Private #assertBody

  • @@ -372,7 +372,7 @@

    Private #assertFunction
    @@ -407,7 +407,7 @@

    Private #assertHeader

  • @@ -450,7 +450,7 @@

    Private #assertStatus

  • @@ -485,7 +485,7 @@

    Private #redirect

  • Parameters

    @@ -511,7 +511,7 @@

    Private #serverAddress

    @@ -551,7 +551,7 @@

    abort

    Returns void

    @@ -569,7 +569,7 @@

    accept

    Parameters

    @@ -593,7 +593,7 @@

    agent

    Parameters

    @@ -617,7 +617,7 @@

    attach

    Parameters

    @@ -648,7 +648,7 @@

    auth

    Parameters

    @@ -674,7 +674,7 @@

    Returns this

    Inherited from IRequest.auth

    Parameters

    @@ -706,7 +706,7 @@

    buffer

    Parameters

    @@ -730,7 +730,7 @@

    ca

    Parameters

    @@ -754,7 +754,7 @@

    catch

    @@ -793,7 +793,7 @@

    cert

    Parameters

    @@ -817,7 +817,7 @@

    clearTimeout

    Returns this

    @@ -835,7 +835,7 @@

    disableTLSCerts

    Returns this

    @@ -853,7 +853,7 @@

    end

    @@ -887,7 +887,7 @@

    expect

  • @@ -910,7 +910,7 @@

    Returns this
    @@ -937,7 +937,7 @@

    Returns this
    @@ -967,7 +967,7 @@

    Returns this
    @@ -995,7 +995,7 @@

    Returns this
    @@ -1036,7 +1036,7 @@

    field

    Parameters

    @@ -1054,7 +1054,7 @@

    Returns this

    Inherited from IRequest.field

    Parameters

    @@ -1083,7 +1083,7 @@

    get

    Parameters

    @@ -1107,7 +1107,7 @@

    http2

    Parameters

    @@ -1131,7 +1131,7 @@

    key

    Parameters

    @@ -1155,7 +1155,7 @@

    maxResponseSize

    Parameters

    @@ -1179,7 +1179,7 @@

    ok

    Parameters

    @@ -1224,7 +1224,7 @@

    on

    Parameters

    @@ -1260,7 +1260,7 @@

    Returns this

    Inherited from IRequest.on

    Parameters

    @@ -1296,7 +1296,7 @@

    Returns this

    Inherited from IRequest.on

    Parameters

    @@ -1332,7 +1332,7 @@

    Returns this

    Inherited from IRequest.on

    Parameters

    @@ -1377,7 +1377,7 @@

    parse

    Parameters

    @@ -1401,7 +1401,7 @@

    part

    Returns this

    @@ -1419,7 +1419,7 @@

    pfx

    Parameters

    @@ -1443,7 +1443,7 @@

    pipe

    Parameters

    @@ -1470,7 +1470,7 @@

    query

    Parameters

    @@ -1494,7 +1494,7 @@

    redirects

    Parameters

    @@ -1518,7 +1518,7 @@

    responseType

    Parameters

    @@ -1542,7 +1542,7 @@

    retry

    Parameters

    @@ -1569,7 +1569,7 @@

    send

    Parameters

    @@ -1593,7 +1593,7 @@

    serialize

    Parameters

    @@ -1619,7 +1619,7 @@

    set

    Parameters

    @@ -1634,7 +1634,7 @@

    Returns this

    Inherited from IRequest.set

    Parameters

    @@ -1652,7 +1652,7 @@

    Returns this

    Inherited from IRequest.set

    Parameters

    @@ -1679,7 +1679,7 @@

    then

    @@ -1727,7 +1727,7 @@

    timeout

    Parameters

    @@ -1751,7 +1751,7 @@

    trustLocalhost

    Parameters

    @@ -1775,7 +1775,7 @@

    type

    Parameters

    @@ -1799,7 +1799,7 @@

    unset

    Parameters

    @@ -1823,7 +1823,7 @@

    use

    Parameters

    @@ -1847,7 +1847,7 @@

    withCredentials

    Returns this

    @@ -1865,7 +1865,7 @@

    write

    Parameters

    diff --git a/docs/index.html b/docs/index.html index b88f56a..1e76906 100644 --- a/docs/index.html +++ b/docs/index.html @@ -82,7 +82,7 @@

    SuperDeno

    SuperDeno latest /x/ version - Minimum supported Deno version + Minimum supported Deno version SuperDeno dependency count SuperDeno dependency outdatedness SuperDeno cached size @@ -143,7 +143,7 @@

    Installation

    SuperDeno is also available on nest.land, 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.8.0/mod.ts.

    +

    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

    diff --git a/docs/interfaces/_superdeno_.superdeno.html b/docs/interfaces/_superdeno_.superdeno.html index cf3b193..ff4153e 100644 --- a/docs/interfaces/_superdeno_.superdeno.html +++ b/docs/interfaces/_superdeno_.superdeno.html @@ -133,7 +133,7 @@

    checkout

  • Parameters

    @@ -156,7 +156,7 @@

    connect

  • Parameters

    @@ -179,7 +179,7 @@

    copy

  • Parameters

    @@ -202,7 +202,7 @@

    delete

  • Parameters

    @@ -225,7 +225,7 @@

    get

  • Parameters

    @@ -248,7 +248,7 @@

    head

  • Parameters

    @@ -271,7 +271,7 @@

    lock

  • Parameters

    @@ -294,7 +294,7 @@

    m-search

  • Parameters

    @@ -317,7 +317,7 @@

    merge

  • Parameters

    @@ -340,7 +340,7 @@

    mkactivity

  • Parameters

    @@ -363,7 +363,7 @@

    mkcol

  • Parameters

    @@ -386,7 +386,7 @@

    move

  • Parameters

    @@ -409,7 +409,7 @@

    notify

  • Parameters

    @@ -432,7 +432,7 @@

    options

  • Parameters

    @@ -455,7 +455,7 @@

    patch

  • Parameters

    @@ -478,7 +478,7 @@

    post

  • Parameters

    @@ -501,7 +501,7 @@

    propfind

  • Parameters

    @@ -524,7 +524,7 @@

    proppatch

  • Parameters

    @@ -547,7 +547,7 @@

    purge

  • Parameters

    @@ -570,7 +570,7 @@

    put

  • Parameters

    @@ -593,7 +593,7 @@

    report

  • Parameters

    @@ -616,7 +616,7 @@

    search

  • Parameters

    @@ -639,7 +639,7 @@

    subscribe

  • Parameters

    @@ -662,7 +662,7 @@

    trace

  • Parameters

    @@ -685,7 +685,7 @@

    unlock

  • Parameters

    @@ -708,7 +708,7 @@

    unsubscribe

  • Parameters

    diff --git a/docs/interfaces/_test_.httperror.html b/docs/interfaces/_test_.httperror.html index f7dc713..431879f 100644 --- a/docs/interfaces/_test_.httperror.html +++ b/docs/interfaces/_test_.httperror.html @@ -121,7 +121,7 @@

    Error

    Error: ErrorConstructor
    @@ -132,7 +132,7 @@

    message

    @@ -142,7 +142,7 @@

    method

    method: string
    @@ -153,7 +153,7 @@

    name

    @@ -163,7 +163,7 @@

    path

    path: string
    @@ -174,7 +174,7 @@

    Optional stack

    @@ -184,7 +184,7 @@

    status

    status: number
    @@ -194,7 +194,7 @@

    text

    text: string
    diff --git a/docs/interfaces/_test_.irequest.html b/docs/interfaces/_test_.irequest.html index 8a62831..51cf887 100644 --- a/docs/interfaces/_test_.irequest.html +++ b/docs/interfaces/_test_.irequest.html @@ -161,7 +161,7 @@

    constructor

  • @@ -195,7 +195,7 @@

    cookies

    cookies: string
    @@ -205,7 +205,7 @@

    method

    method: string
    @@ -215,7 +215,7 @@

    url

    url: string
    @@ -232,7 +232,7 @@

    abort

  • Returns void

    @@ -249,7 +249,7 @@

    accept

  • Parameters

    @@ -272,7 +272,7 @@

    agent

  • Parameters

    @@ -295,7 +295,7 @@

    attach

  • Parameters

    @@ -325,7 +325,7 @@

    auth

  • Parameters

    @@ -350,7 +350,7 @@

    Returns this

    Parameters

    @@ -381,7 +381,7 @@

    buffer

  • Parameters

    @@ -404,7 +404,7 @@

    ca

  • Parameters

    @@ -428,7 +428,7 @@

    catch

    @@ -466,7 +466,7 @@

    cert

  • Parameters

    @@ -489,7 +489,7 @@

    clearTimeout

  • Returns this

    @@ -506,7 +506,7 @@

    disableTLSCerts

  • Returns this

    @@ -523,7 +523,7 @@

    end

  • Parameters

    @@ -547,7 +547,7 @@

    field

  • Parameters

    @@ -564,7 +564,7 @@

    Returns this

    Parameters

    @@ -592,7 +592,7 @@

    get

  • Parameters

    @@ -615,7 +615,7 @@

    http2

  • Parameters

    @@ -638,7 +638,7 @@

    key

  • Parameters

    @@ -661,7 +661,7 @@

    maxResponseSize

  • Parameters

    @@ -684,7 +684,7 @@

    ok

  • Parameters

    @@ -728,7 +728,7 @@

    on

  • Parameters

    @@ -763,7 +763,7 @@

    Returns this

    Parameters

    @@ -798,7 +798,7 @@

    Returns this

    Parameters

    @@ -833,7 +833,7 @@

    Returns this

    Parameters

    @@ -877,7 +877,7 @@

    parse

  • Parameters

    @@ -900,7 +900,7 @@

    part

  • Returns this

    @@ -917,7 +917,7 @@

    pfx

  • Parameters

    @@ -940,7 +940,7 @@

    pipe

  • Parameters

    @@ -966,7 +966,7 @@

    query

  • Parameters

    @@ -989,7 +989,7 @@

    redirects

  • Parameters

    @@ -1012,7 +1012,7 @@

    responseType

  • Parameters

    @@ -1035,7 +1035,7 @@

    retry

  • Parameters

    @@ -1061,7 +1061,7 @@

    send

  • Parameters

    @@ -1084,7 +1084,7 @@

    serialize

  • Parameters

    @@ -1109,7 +1109,7 @@

    set

  • Parameters

    @@ -1123,7 +1123,7 @@

    Returns this

    Parameters

    @@ -1140,7 +1140,7 @@

    Returns this

    Parameters

    @@ -1167,7 +1167,7 @@

    then

    @@ -1214,7 +1214,7 @@

    timeout

  • Parameters

    @@ -1237,7 +1237,7 @@

    trustLocalhost

  • Parameters

    @@ -1260,7 +1260,7 @@

    type

  • Parameters

    @@ -1283,7 +1283,7 @@

    unset

  • Parameters

    @@ -1306,7 +1306,7 @@

    use

  • Parameters

    @@ -1329,7 +1329,7 @@

    withCredentials

  • Returns this

    @@ -1346,7 +1346,7 @@

    write

  • Parameters

    diff --git a/docs/interfaces/_test_.iresponse.html b/docs/interfaces/_test_.iresponse.html index 18caec5..7101b69 100644 --- a/docs/interfaces/_test_.iresponse.html +++ b/docs/interfaces/_test_.iresponse.html @@ -128,7 +128,7 @@

    accepted

    accepted: boolean
    @@ -138,7 +138,7 @@

    badRequest

    badRequest: boolean
    @@ -148,7 +148,7 @@

    body

    body: any
    @@ -158,7 +158,7 @@

    charset

    charset: string
    @@ -168,7 +168,7 @@

    clientError

    clientError: boolean
    @@ -178,7 +178,7 @@

    error

    error: false | HTTPError
    @@ -188,7 +188,7 @@

    files

    files: any
    @@ -198,7 +198,7 @@

    forbidden

    forbidden: boolean
    @@ -208,7 +208,7 @@

    header

    header: Header
    @@ -218,7 +218,7 @@

    headers

    headers: Header
    @@ -228,7 +228,7 @@

    info

    info: boolean
    @@ -238,7 +238,7 @@

    links

    links: object
    @@ -248,7 +248,7 @@

    noContent

    noContent: boolean
    @@ -258,7 +258,7 @@

    notAcceptable

    notAcceptable: boolean
    @@ -268,7 +268,7 @@

    notFound

    notFound: boolean
    @@ -278,7 +278,7 @@

    ok

    ok: boolean
    @@ -288,7 +288,7 @@

    redirect

    redirect: boolean
    @@ -298,7 +298,7 @@

    redirects

    redirects: string[]
    @@ -308,7 +308,7 @@

    serverError

    serverError: boolean
    @@ -318,7 +318,7 @@

    status

    status: number
    @@ -328,7 +328,7 @@

    statusCode

    statusCode: number
    @@ -338,7 +338,7 @@

    statusText

    statusText: string
    @@ -348,7 +348,7 @@

    statusType

    statusType: number
    @@ -358,7 +358,7 @@

    text

    text: string
    @@ -368,7 +368,7 @@

    type

    type: string
    @@ -378,7 +378,7 @@

    unauthorized

    unauthorized: boolean
    @@ -388,7 +388,7 @@

    xhr

    @@ -405,7 +405,7 @@

    get

  • Parameters

    diff --git a/docs/interfaces/_types_.legacyserverlike.html b/docs/interfaces/_types_.legacyserverlike.html index b658d86..dc4e352 100644 --- a/docs/interfaces/_types_.legacyserverlike.html +++ b/docs/interfaces/_types_.legacyserverlike.html @@ -102,7 +102,7 @@

    listener

    listener: Deno.Listener
    @@ -119,7 +119,7 @@

    close

  • Returns void

    diff --git a/docs/interfaces/_types_.listenerlike.html b/docs/interfaces/_types_.listenerlike.html index 06bea79..8d09ebb 100644 --- a/docs/interfaces/_types_.listenerlike.html +++ b/docs/interfaces/_types_.listenerlike.html @@ -100,7 +100,7 @@

    listen

  • Parameters

    diff --git a/docs/interfaces/_types_.nativeserverlike.html b/docs/interfaces/_types_.nativeserverlike.html index 3d483b9..5bd5bb4 100644 --- a/docs/interfaces/_types_.nativeserverlike.html +++ b/docs/interfaces/_types_.nativeserverlike.html @@ -103,7 +103,7 @@

    Readonly addrs

    addrs: Deno.Addr[]
    @@ -120,7 +120,7 @@

    close

  • Returns void

    @@ -137,7 +137,7 @@

    listenAndServe

  • Returns Promise<void>

    diff --git a/docs/interfaces/_types_.requesthandlerlike.html b/docs/interfaces/_types_.requesthandlerlike.html index d8939bd..d9c1d99 100644 --- a/docs/interfaces/_types_.requesthandlerlike.html +++ b/docs/interfaces/_types_.requesthandlerlike.html @@ -84,7 +84,7 @@

    Callable

  • Parameters

    diff --git a/docs/modules/_close_.html b/docs/modules/_close_.html index fad4257..cc64f6f 100644 --- a/docs/modules/_close_.html +++ b/docs/modules/_close_.html @@ -89,7 +89,7 @@

    Private
    diff --git a/docs/modules/_superagent_.html b/docs/modules/_superagent_.html index 8903fd2..07ab632 100644 --- a/docs/modules/_superagent_.html +++ b/docs/modules/_superagent_.html @@ -91,7 +91,7 @@

    Const superagent

    superagent: any = _superagent
    @@ -108,7 +108,7 @@

    getXHR

  • Returns any

    diff --git a/docs/modules/_superdeno_.html b/docs/modules/_superdeno_.html index 0f0bbbd..d4bd0a7 100644 --- a/docs/modules/_superdeno_.html +++ b/docs/modules/_superdeno_.html @@ -96,7 +96,7 @@

    startManagedServer

  • Parameters

    @@ -122,7 +122,7 @@

    superdeno

  • diff --git a/docs/modules/_test_.html b/docs/modules/_test_.html index c3169f1..23bd464 100644 --- a/docs/modules/_test_.html +++ b/docs/modules/_test_.html @@ -125,7 +125,7 @@

    CallbackHandler

    CallbackHandler: (err: any, res: IResponse) => void
    @@ -164,7 +164,7 @@

    ExpectChecker

    ExpectChecker: (res: IResponse) => any
    @@ -200,7 +200,7 @@

    Header

    Header: {}
    @@ -218,7 +218,7 @@

    HeaderValue

    HeaderValue: string | string[]
    @@ -228,7 +228,7 @@

    MultipartValue

    @@ -238,7 +238,7 @@

    MultipartValueSingle

    MultipartValueSingle: Blob | Uint8Array | Deno.Reader | string | boolean | number
    @@ -248,7 +248,7 @@

    Parser

    Parser: (str: string) => any
    @@ -279,7 +279,7 @@

    Plugin

    Plugin: (req: IRequest) => void
    @@ -310,7 +310,7 @@

    Serializer

    Serializer: (obj: any) => string
    @@ -344,7 +344,7 @@

    Const SHAM_SYMBOL

    SHAM_SYMBOL: any = Symbol("SHAM_SYMBOL")
    @@ -360,7 +360,7 @@

    Const SuperRequest

    SuperRequest: IRequest = (superagent as any).Request
    @@ -382,7 +382,7 @@

    Private cleanHeader

  • @@ -416,7 +416,7 @@

    Private completeXhrPr
  • @@ -438,7 +438,7 @@

    Private error

  • @@ -474,7 +474,7 @@

    Private initHeaders

  • @@ -505,7 +505,7 @@

    Private isRedirect

  • diff --git a/docs/modules/_types_.html b/docs/modules/_types_.html index 953d63a..fcfd293 100644 --- a/docs/modules/_types_.html +++ b/docs/modules/_types_.html @@ -94,7 +94,7 @@

    ServerLike

    diff --git a/docs/modules/_utils_.html b/docs/modules/_utils_.html index ae1e7e0..c9e38ce 100644 --- a/docs/modules/_utils_.html +++ b/docs/modules/_utils_.html @@ -94,7 +94,7 @@

    Const isCommonServer

    Parameters

    @@ -117,7 +117,7 @@

    Const isListener

  • Parameters

    @@ -140,7 +140,7 @@

    Const isServer

  • Parameters

    @@ -163,7 +163,7 @@

    Const isStdLegacyS
  • Parameters

    @@ -186,7 +186,7 @@

    Const isStdNativeS
  • Parameters

    @@ -209,7 +209,7 @@

    Const isString

  • Parameters

    diff --git a/egg.json b/egg.json index 44d4eb0..1ac3c6e 100644 --- a/egg.json +++ b/egg.json @@ -1,7 +1,7 @@ { "name": "superdeno", "description": "HTTP assertions for Deno made easy via superagent.", - "version": "4.8.0", + "version": "4.9.0", "repository": "https://github.com/cmorten/superdeno", "stable": true, "checkFormat": false, diff --git a/src/test.ts b/src/test.ts index 5b7fa10..6857ba3 100644 --- a/src/test.ts +++ b/src/test.ts @@ -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 } 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. */ @@ -201,9 +217,11 @@ export class Test extends SuperRequest { #redirects: number; #redirectList: string[]; #server!: ServerLike; + #serverSetupPromise: Promise; + #urlSetupPromise: Promise; public app: string | ListenerLike | ServerLike; - public url: string; + public url!: string; constructor( app: string | ListenerLike | ServerLike, @@ -220,8 +238,21 @@ export class Test extends SuperRequest { this.app = app; this.#asserts = []; + let serverSetupPromiseResolver!: () => void; + let addressSetupPromiseResolver!: () => void; + + this.#serverSetupPromise = new Promise((resolve) => { + serverSetupPromiseResolver = resolve; + }); + this.#urlSetupPromise = new Promise((resolve) => { + addressSetupPromiseResolver = resolve; + }); + if (isString(app)) { this.url = `${app}${path}`; + + serverSetupPromiseResolver(); + addressSetupPromiseResolver(); } else { if (isStdNativeServer(app)) { const listenAndServePromise = app.listenAndServe().catch((err) => @@ -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); } } @@ -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(); }; /** @@ -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); + }); + }, + ); }, ); @@ -618,8 +704,8 @@ export class Test extends SuperRequest { */ #assertStatus = (status: number, res: IResponse): Error | void => { if (res.status !== status) { - const a = STATUS_TEXT.get(status); - const b = STATUS_TEXT.get(res.status); + const a = STATUS_TEXT[status as StatusCode]; + const b = STATUS_TEXT[res.status as StatusCode]; return new Error(`expected ${status} "${a}", got ${res.status} "${b}"`); } diff --git a/src/types.ts b/src/types.ts index 6ad2382..e45b384 100644 --- a/src/types.ts +++ b/src/types.ts @@ -3,6 +3,7 @@ export interface RequestHandlerLike { ( req: any, + ...args: any[] ): Promise | Promise | any | void; } @@ -17,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; +} diff --git a/src/utils.ts b/src/utils.ts index ea41146..d638d4e 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,4 +1,5 @@ import type { + ExpressListenerLike, LegacyServerLike, ListenerLike, NativeServerLike, @@ -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; @@ -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); diff --git a/src/xhrSham.js b/src/xhrSham.js index 0cfa310..a9c8820 100644 --- a/src/xhrSham.js +++ b/src/xhrSham.js @@ -4,9 +4,9 @@ const decoder = new TextDecoder("utf-8"); let SHAM_SYMBOL = Symbol("SHAM_SYMBOL"); function setupSham(symbol) { - window[symbol] = window[symbol] || {}; - window[symbol].idSequence = 0; - window[symbol].promises = {}; + globalThis[symbol] = globalThis[symbol] || {}; + globalThis[symbol].idSequence = 0; + globalThis[symbol].promises = {}; } setupSham(SHAM_SYMBOL); @@ -25,7 +25,7 @@ export const exposeSham = (symbol) => { */ export class XMLHttpRequestSham { constructor() { - this.id = (++window[SHAM_SYMBOL].idSequence).toString(36); + this.id = (++globalThis[SHAM_SYMBOL].idSequence).toString(36); this.origin = null; this.onreadystatechange = () => {}; this.readyState = 0; @@ -205,7 +205,7 @@ export class XMLHttpRequestSham { // in this implementation. TBC whether this is accurate. // To prevent a memory leak we clean up our promise from the // cache now that it _must_ be resolved. - delete window[SHAM_SYMBOL].promises[self.id]; + delete globalThis[SHAM_SYMBOL].promises[self.id]; xhrResponse.responseHeaders = xhrResponse.getAllResponseHeaders(); onStateChange(xhrResponse); @@ -278,7 +278,7 @@ export class XMLHttpRequestSham { // so that superdeno can await these promises before ending. // Not doing such results in Deno test complaining of unhandled // async operations. - window[SHAM_SYMBOL].promises[self.id] = fetch(options.url, { + globalThis[SHAM_SYMBOL].promises[self.id] = fetch(options.url, { method: options.method, headers: options.requestHeaders, body, @@ -295,7 +295,7 @@ export class XMLHttpRequestSham { }); // Wait on the response, and then read the buffer. - response = await window[SHAM_SYMBOL].promises[self.id]; + response = await globalThis[SHAM_SYMBOL].promises[self.id]; // Manually transfer over properties, getPropertyDescriptors / prototype access now // restricted in Deno. REF: https://github.com/denoland/deno/releases/tag/v1.9.0 diff --git a/test/deps.ts b/test/deps.ts index b76caa5..1edc171 100644 --- a/test/deps.ts +++ b/test/deps.ts @@ -1,5 +1,9 @@ -export { dirname, join } from "https://deno.land/std@0.129.0/path/mod.ts"; -export { expect } from "https://deno.land/x/expect@v0.2.9/mod.ts"; -export * as Opine from "https://deno.land/x/opine@2.1.2/mod.ts"; -export * as Oak from "https://deno.land/x/oak@v10.4.0/mod.ts"; -export { getFreePort } from "https://deno.land/x/free_port@v1.2.0/mod.ts"; +export { dirname, join } from "https://deno.land/std@0.213.0/path/mod.ts"; +export { expect } from "https://deno.land/x/expect@v0.4.0/mod.ts"; +export * as Opine from "https://deno.land/x/opine@2.3.4/mod.ts"; + +// TODO: upgrade to v13.0.0 - appear to be getting error when using AbortController +export * as Oak from "https://deno.land/x/oak@v12.6.2/mod.ts"; + +// @deno-types="npm:@types/express@^4.17" +export { default as express } from "npm:express@4.18.2"; diff --git a/test/superdeno.express.test.ts b/test/superdeno.express.test.ts new file mode 100644 index 0000000..343e6eb --- /dev/null +++ b/test/superdeno.express.test.ts @@ -0,0 +1,964 @@ +import { getFreePort } from "../deps.ts"; +import { expect, express } from "./deps.ts"; +import { describe, it, random } from "./utils.ts"; +import { superdeno, Test } from "../mod.ts"; + +const { json } = express; + +describe("superdeno(url)", () => { + it("superdeno(url): should support `superdeno(url)`", async (done) => { + const app = express(); + + app.get("/", (_req, res) => { + res.send("hello"); + }); + + const freePort = await getFreePort(random(1024, 49151)); + const server = app.listen(freePort); + const address = server.address(); + const url = `http://localhost:${address.port}`; + + superdeno(url) + .get("/") + .expect("hello", () => { + server.close(); + done(); + }); + }); + + describe(".end(cb)", () => { + it("superdeno(url): .end(cb): should set `this` to the test object when calling the `cb` in `.end(cb)`", async (done) => { + const app = express(); + + app.get("/", (_req, res) => { + res.send("hello"); + }); + + const freePort = await getFreePort(random(1024, 49151)); + const server = app.listen(freePort); + const address = server.address(); + const url = `http://localhost:${address.port}`; + + const test = superdeno(url).get("/"); + + test.end(function (this: Test, _err, _res) { + expect(test).toEqual(this); + server.close(); + done(); + }); + }); + }); +}); + +describe("superdeno(app)", () => { + it("superdeno(app): should fire up the app on an ephemeral port", (done) => { + const app = express(); + + app.get("/", (_req, res) => { + res.send("hey"); + }); + + superdeno(app) + .get("/") + .end((_err, res) => { + expect(res.status).toEqual(200); + expect(res.text).toEqual("hey"); + done(); + }); + }); + + it("superdeno(app): should work with an active server", async (done) => { + const app = express(); + + app.get("/", (_req, res) => { + res.send("hey"); + }); + + const freePort = await getFreePort(random(1024, 49151)); + const server = app.listen(freePort); + + superdeno(server) + .get("/") + .end((_err, res) => { + expect(res.status).toEqual(200); + expect(res.text).toEqual("hey"); + done(); + }); + }); + + it("superdeno(app): should work with remote server", async (done) => { + const app = express(); + + app.get("/", (_req, res) => { + res.send("hey"); + }); + + const freePort = await getFreePort(random(1024, 49151)); + const server = app.listen(freePort); + + superdeno(`http://localhost:${freePort}`) + .get("/") + .end((err, res) => { + if (err) throw err; + expect(res.status).toEqual(200); + expect(res.text).toEqual("hey"); + server.close(); + done(); + }); + }); + + it("superdeno(app): should work with .send() on POST", (done) => { + const app = express(); + + app.use(json()); + + app.post("/", (req, res) => { + res.send(req.body.name); + }); + + superdeno(app) + .post("/") + .send({ name: "john" }) + .expect("john", done); + }); + + it("superdeno(app): should handle headers correctly", (done) => { + const app = express(); + + app.get("/", (_req, res) => { + res.cookie("foo", "bar"); + res.cookie("user", "deno"); + res.append("Set-Cookie", "fizz=buzz"); + res.set("X-Tested-With", "SuperDeno"); + res.type("application/json"); + res.send(); + }); + + superdeno(app) + .get("/") + .expect("content-length", "0") + .expect("content-type", "application/json; charset=utf-8") + .expect("set-cookie", "foo=bar; Path=/,user=deno; Path=/,fizz=buzz") + .expect("x-powered-by", "Express") + .expect("x-tested-with", "SuperDeno") + .expect(200, done); + }); + + it("superdeno(app): should work when unbuffered", (done) => { + const app = express(); + + app.get("/", (_req, res) => { + res.send("Hello"); + }); + + superdeno(app) + .get("/") + .expect("Hello", done); + }); + + it("superdeno(app): should default redirects to 0", (done) => { + const app = express(); + + app.get("/", (_req, res) => { + res.redirect("/login"); + }); + + superdeno(app) + .get("/") + .expect(302, done); + }); + + it("superdeno(app): promise form: should default redirects to 0", async () => { + const app = express(); + + app.get("/", (_req, res) => { + res.redirect("/login"); + }); + + await superdeno(app) + .get("/") + .expect(302); + }); + + it("superdeno(app): .redirects(n): should handle intermediate redirects", (done) => { + const app = express(); + + app.get("/login", (_req, res) => { + res.send("Login"); + }); + + app.get("/redirect", (_req, res) => { + res.redirect("/login"); + }); + + app.get("/", (_req, res) => { + res.redirect("/redirect"); + }); + + superdeno(app) + .get("/") + .redirects(1) + .expect(302, done); + }); + + it("superdeno(app): .redirects(n): promise form: should handle intermediate redirects", async () => { + const app = express(); + + app.get("/login", (_req, res) => { + res.send("Login"); + }); + + app.get("/redirect", (_req, res) => { + res.redirect("/login"); + }); + + app.get("/", (_req, res) => { + res.redirect("/redirect"); + }); + + await superdeno(app) + .get("/") + .redirects(1) + .expect(302); + }); + + it("superdeno(app): .redirects(n): should handle full redirects", (done) => { + const app = express(); + + app.get("/login", (_req, res) => { + res.send("Login"); + }); + + app.get("/redirect", (_req, res) => { + res.redirect("/login"); + }); + + app.get("/", (_req, res) => { + res.redirect("/redirect"); + }); + + superdeno(app) + .get("/") + .redirects(2) + .end((_err, res) => { + expect(res).toBeDefined(); + expect(res.status).toEqual(200); + expect(res.text).toEqual("Login"); + done(); + }); + }); + + it("superdeno(app): .redirects(n): promise form: should handle full redirects", async () => { + const app = express(); + + app.get("/login", (_req, res) => { + res.send("Login"); + }); + + app.get("/redirect", (_req, res) => { + res.redirect("/login"); + }); + + app.get("/", (_req, res) => { + res.redirect("/redirect"); + }); + + const res = await superdeno(app) + .get("/") + .redirects(2); + + expect(res).toBeDefined(); + expect(res.status).toEqual(200); + expect(res.text).toEqual("Login"); + }); + + // TODO: figure out the equivalent error scenario for Deno setup + // it('should handle socket errors', (done) => { + // const app = express(); + + // app.get('/', (req, res) => { + // res.destroy(); + // }); + + // superdeno(app) + // .get('/') + // .end((err) => { + // expect(err).toBeDefined(); + // done(); + // }); + // }); + + describe(".end(fn)", () => { + it("superdeno(app): .end(fn): should close server", (done) => { + const app = express(); + + app.get("/", (_req, res) => { + res.send("superdeno FTW!"); + }); + + let doneCount = 0; + + superdeno(app) + .get("/") + .end(() => { + doneCount++; + + if (doneCount === 2) { + done(); + } + }); + + app.on("close", () => { + doneCount++; + + if (doneCount === 2) { + done(); + } + }); + }); + + it("superdeno(app): .end(fn): should wait for server to close before invoking fn", (done) => { + const app = express(); + let closed = false; + + app.get("/", (_req, res) => { + res.send("superdeno FTW!"); + }); + + superdeno(app) + .get("/") + .end(() => { + expect(closed).toBeTruthy(); + done(); + }); + + app.on("close", () => { + closed = true; + }); + }); + + it("superdeno(app): .end(fn): should support nested requests", (done) => { + const app = express(); + const test = superdeno(app); + + app.get("/", (_req, res) => { + res.send("superdeno FTW!"); + }); + + test + .get("/") + .end(() => { + test + .get("/") + .end((err, res) => { + expect(err).toBeNull(); + expect(res.status).toEqual(200); + expect(res.text).toEqual("superdeno FTW!"); + done(); + }); + }); + }); + + it("superdeno(app): .end(fn): should include the response in the error callback", (done) => { + const app = express(); + + app.get("/", (_req, res) => { + res.send("whatever"); + }); + + superdeno(app) + .get("/") + .expect(() => { + throw new Error("Some error"); + }) + .end((err, res) => { + expect(err).toBeDefined(); + expect(res).toBeDefined(); + // Duck-typing response, just in case. + expect(res.status).toEqual(200); + done(); + }); + }); + + it("superdeno(app): .end(fn): should set `this` to the test object when calling the error callback", (done) => { + const app = express(); + + app.get("/", (_req, res) => { + res.send("whatever"); + }); + + const test = superdeno(app).get("/"); + + test.expect(() => { + throw new Error("Some error"); + }).end(function (this: Test, err, _res) { + expect(err).toBeDefined(); + expect(this).toEqual(test); + done(); + }); + }); + + it("superdeno(app): .end(fn): should handle an undefined Response", (done) => { + const app = express(); + + let timeoutPromise: Promise; + + app.get("/", async (_req, res) => { + timeoutPromise = new Promise((resolve) => { + setTimeout(async () => { + try { + await res.send(); + } catch (_) { + // swallow + } + + resolve(true); + }, 20); + }); + + await timeoutPromise; + }); + + const server = app.listen(); + const address = server.address(); + const url = `http://localhost:${address.port}`; + + superdeno(url).get("/").timeout(1) + .expect(200, async (err, _res) => { + expect(err).toBeInstanceOf(Error); + server.close(); + await timeoutPromise; + done(); + }); + }); + }); + + describe(".expect(status[, fn])", () => { + it("superdeno(app): .expect(status[, fn]): should assert the response status", (done) => { + const app = express(); + + app.get("/", (_req, res) => { + res.send("hey"); + }); + + superdeno(app) + .get("/") + .expect(404) + .end((err, _res) => { + expect(err.message).toEqual('expected 404 "Not Found", got 200 "OK"'); + done(); + }); + }); + }); + + describe(".expect(status)", () => { + it("superdeno(app): .expect(status): should assert only status", (done) => { + const app = express(); + + app.get("/", (_req, res) => { + res.send("hey"); + }); + + superdeno(app) + .get("/") + .expect(200) + .end(done); + }); + + it("superdeno(app): .expect(status): should assert only error status'", (done) => { + const app = express(); + + app.get("/", (_req, res) => { + res.sendStatus(400); + }); + + superdeno(app) + .get("/") + .expect(400) + .end(done); + }); + }); + + describe(".expect(status, body[, fn])", () => { + it("superdeno(app): .expect(status, body[, fn]): should assert the response body and status", (done) => { + const app = express(); + + app.get("/", (_req, res) => { + res.send("foo"); + }); + + superdeno(app) + .get("/") + .expect(200, "foo", done); + }); + + it("superdeno(app): .expect(status, body[, fn]): should assert the response body and error status'", (done) => { + const app = express(); + + app.get("/", (_req, res) => { + res.status(400).send("foo"); + }); + + superdeno(app) + .get("/") + .expect(400, "foo", done); + }); + + describe("when the body argument is an empty string", () => { + it("superdeno(app): .expect(status, body[, fn]): should not quietly pass on failure", (done) => { + const app = express(); + + app.get("/", (_req, res) => { + res.send("foo"); + }); + + superdeno(app) + .get("/") + .expect(200, "") + .end((err, _res) => { + expect(err.message).toEqual('expected "" response body, got "foo"'); + done(); + }); + }); + }); + }); + + describe(".expect(body[, fn])", () => { + it("superdeno(app): .expect(body[, fn]): should assert the response body", (done) => { + const app = express(); + + app.set("json spaces", 0); + + app.get("/", (_req, res) => { + res.send({ foo: "bar" }); + }); + + superdeno(app) + .get("/") + .expect("hey") + .end((err, _res) => { + expect(err.message).toEqual( + 'expected "hey" response body, got \'{"foo":"bar"}\'', + ); + done(); + }); + }); + + it("superdeno(app): .expect(body[, fn]): should assert the status before the body", (done) => { + const app = express(); + + app.set("json spaces", 0); + + app.get("/", (_req, res) => { + res.status(500).send({ message: "something went wrong" }); + }); + + superdeno(app) + .get("/") + .expect(200) + .expect("hey") + .end((err, _res) => { + expect(err.message).toEqual( + 'expected 200 "OK", got 500 "Internal Server Error"', + ); + done(); + }); + }); + + it("superdeno(app): .expect(body[, fn]): should assert the response text", (done) => { + const app = express(); + + app.set("json spaces", 0); + + app.get("/", (_req, res) => { + res.send({ foo: "bar" }); + }); + + superdeno(app) + .get("/") + .expect('{"foo":"bar"}', done); + }); + + it("superdeno(app): .expect(body[, fn]): should assert the parsed response body", (done) => { + const app = express(); + + app.set("json spaces", 0); + + app.get("/", (_req, res) => { + res.send({ foo: "bar" }); + }); + + superdeno(app) + .get("/") + .expect({ foo: "baz" }) + .end((err, _res) => { + expect(err.message).toEqual( + 'expected { foo: "baz" } response body, got { foo: "bar" }', + ); + + superdeno(app) + .get("/") + .expect({ foo: "bar" }) + .end(done); + }); + }); + + it("superdeno(app): .expect(body[, fn]): should test response object types", (done) => { + const app = express(); + app.get("/", (_req, res) => { + res.status(200).json({ stringValue: "foo", numberValue: 3 }); + }); + + superdeno(app) + .get("/") + .expect({ stringValue: "foo", numberValue: 3 }, done); + }); + + it("superdeno(app): .expect(body[, fn]): should deep test response object types", (done) => { + const app = express(); + app.get("/", (_req, res) => { + res.status(200) + .json( + { + stringValue: "foo", + numberValue: 3, + nestedObject: { innerString: "5" }, + }, + ); + }); + + superdeno(app) + .get("/") + .expect( + { + stringValue: "foo", + numberValue: 3, + nestedObject: { innerString: 5 }, + }, + ) + .end((err, _res) => { + expect(err.message).toEqual( + 'expected {\n stringValue: "foo",\n numberValue: 3,\n nestedObject: { innerString: 5 }\n} response body, got {\n stringValue: "foo",\n numberValue: 3,\n nestedObject: { innerString: "5" }\n}', + ); // eslint-disable-line max-len + + superdeno(app) + .get("/") + .expect( + { + stringValue: "foo", + numberValue: 3, + nestedObject: { innerString: "5" }, + }, + ) + .end(done); + }); + }); + + it("superdeno(app): .expect(body[, fn]): should support regular expressions", (done) => { + const app = express(); + + app.get("/", (_req, res) => { + res.send("foobar"); + }); + + superdeno(app) + .get("/") + .expect(/^bar/) + .end((err, _res) => { + expect(err.message).toEqual('expected body "foobar" to match /^bar/'); + done(); + }); + }); + + it("superdeno(app): .expect(body[, fn]): should assert response body multiple times", (done) => { + const app = express(); + + app.get("/", (_req, res) => { + res.send("hey deno"); + }); + + superdeno(app) + .get("/") + .expect(/deno/) + .expect("hey") + .expect("hey deno") + .end((err, _res) => { + expect(err.message).toEqual( + 'expected "hey" response body, got "hey deno"', + ); + done(); + }); + }); + + it("superdeno(app): .expect(body[, fn]): should assert response body multiple times with no exception", (done) => { + const app = express(); + + app.get("/", (_req, res) => { + res.send("hey deno"); + }); + + superdeno(app) + .get("/") + .expect(/deno/) + .expect(/^hey/) + .expect("hey deno", done); + }); + }); + + describe(".expect(field, value[, fn])", () => { + it("superdeno(app): .expect(field, value[, fn]): should assert the header field presence", (done) => { + const app = express(); + + app.get("/", (_req, res) => { + res.send({ foo: "bar" }); + }); + + superdeno(app) + .get("/") + .expect("Content-Foo", "bar") + .end((err, _res) => { + expect(err.message).toEqual('expected "Content-Foo" header field'); + done(); + }); + }); + + it("superdeno(app): .expect(field, value[, fn]): should assert the header field value", (done) => { + const app = express(); + + app.get("/", (_req, res) => { + res.send({ foo: "bar" }); + }); + + superdeno(app) + .get("/") + .expect("Content-Type", "text/html") + .end((err, _res) => { + expect(err.message).toEqual( + 'expected "Content-Type" of "text/html", ' + + 'got "application/json; charset=utf-8"', + ); + done(); + }); + }); + + it("superdeno(app): .expect(field, value[, fn]): should assert multiple fields", (done) => { + const app = express(); + + app.get("/", (_req, res) => { + res.send("hey"); + }); + + superdeno(app) + .get("/") + .expect("Content-Type", "text/html; charset=utf-8") + .expect("Content-Length", "3") + .end(done); + }); + + it("superdeno(app): .expect(field, value[, fn]): should support regular expressions", (done) => { + const app = express(); + + app.get("/", (_req, res) => { + res.send("hey"); + }); + + superdeno(app) + .get("/") + .expect("Content-Type", /^application/) + .end((err) => { + expect(err.message).toEqual( + 'expected "Content-Type" matching /^application/, ' + + 'got "text/html; charset=utf-8"', + ); + done(); + }); + }); + + it("superdeno(app): .expect(field, value[, fn]): should support numbers", (done) => { + const app = express(); + + app.get("/", (_req, res) => { + res.send("hey"); + }); + + superdeno(app) + .get("/") + .expect("Content-Length", 4) + .end((err) => { + expect(err.message).toEqual( + 'expected "Content-Length" of "4", got "3"', + ); + done(); + }); + }); + + describe("handling arbitrary expect functions", () => { + const app = express(); + app.get("/", (_req, res) => { + res.send("hey"); + }); + + it("superdeno(app): .expect(field, value[, fn]): reports errors", (done) => { + superdeno(app).get("/") + .expect((_res) => { + throw new Error("failed"); + }) + .end((err) => { + expect(err.message).toEqual("failed"); + done(); + }); + }); + + it( + "superdeno(app): .expect(field, value[, fn]): ensures truthy non-errors returned from asserts are not promoted to errors", + (done) => { + superdeno(app).get("/") + .expect((_res) => { + return "some descriptive error"; + }) + .end((err) => { + expect(err).toBeNull(); + done(); + }); + }, + ); + + it("superdeno(app): .expect(field, value[, fn]): ensures truthy errors returned from asserts are throw to end", (done) => { + superdeno(app).get("/") + .expect((_res) => { + return new Error("some descriptive error"); + }) + .end((err) => { + expect(err.message).toEqual("some descriptive error"); + expect(err).toBeInstanceOf(Error); + done(); + }); + }); + + it("superdeno(app): .expect(field, value[, fn]): doesn't create false negatives", (done) => { + superdeno(app).get("/") + .expect((_res) => { + }) + .end(done); + }); + + it("superdeno(app): .expect(field, value[, fn]): handles multiple asserts", (done) => { + const calls: number[] = []; + + superdeno(app).get("/") + .expect((_res) => { + calls[0] = 1; + }) + .expect((_res) => { + calls[1] = 1; + }) + .expect((_res) => { + calls[2] = 1; + }) + .end(() => { + const callCount = [0, 1, 2].reduce((count, i) => { + return count + calls[i]; + }, 0); + expect(callCount).toEqual(3); + done(); + }); + }); + + it("superdeno(app): .expect(field, value[, fn]): plays well with normal assertions - no false positives", (done) => { + superdeno(app).get("/") + .expect((_res) => { + }) + .expect("Content-Type", /json/) + .end((err) => { + expect(err.message).toMatch(/Content-Type/); + done(); + }); + }); + + it("superdeno(app): .expect(field, value[, fn]): plays well with normal assertions - no false negatives", (done) => { + superdeno(app).get("/") + .expect((_res) => { + }) + .expect("Content-Type", /html/) + .expect((_res) => { + }) + .expect("Content-Type", /text/) + .end(done); + }); + }); + + describe("handling multiple assertions per field", () => { + it("superdeno(app): .expect(field, value[, fn]): should work", (done) => { + const app = express(); + app.get("/", (_req, res) => { + res.send("hey"); + }); + + superdeno(app) + .get("/") + .expect("Content-Type", /text/) + .expect("Content-Type", /html/) + .end(done); + }); + + it("superdeno(app): .expect(field, value[, fn]): should return an error if the first one fails", (done) => { + const app = express(); + app.get("/", (_req, res) => { + res.send("hey"); + }); + + superdeno(app) + .get("/") + .expect("Content-Type", /bloop/) + .expect("Content-Type", /html/) + .end((err) => { + expect(err.message).toEqual( + 'expected "Content-Type" matching /bloop/, ' + + 'got "text/html; charset=utf-8"', + ); + done(); + }); + }); + + it("superdeno(app): .expect(field, value[, fn]): should return an error if a middle one fails", (done) => { + const app = express(); + app.get("/", (_req, res) => { + res.send("hey"); + }); + + superdeno(app) + .get("/") + .expect("Content-Type", /text/) + .expect("Content-Type", /bloop/) + .expect("Content-Type", /html/) + .end((err) => { + expect(err.message).toEqual( + 'expected "Content-Type" matching /bloop/, ' + + 'got "text/html; charset=utf-8"', + ); + done(); + }); + }); + + it("superdeno(app): .expect(field, value[, fn]): should return an error if the last one fails", (done) => { + const app = express(); + app.get("/", (_req, res) => { + res.send("hey"); + }); + + superdeno(app) + .get("/") + .expect("Content-Type", /text/) + .expect("Content-Type", /html/) + .expect("Content-Type", /bloop/) + .end((err) => { + expect(err.message).toEqual( + 'expected "Content-Type" matching /bloop/, ' + + 'got "text/html; charset=utf-8"', + ); + done(); + }); + }); + }); + }); +}); diff --git a/test/superdeno.oak.test.ts b/test/superdeno.oak.test.ts index dac0dc8..d1f7387 100644 --- a/test/superdeno.oak.test.ts +++ b/test/superdeno.oak.test.ts @@ -1,15 +1,12 @@ // deno-lint-ignore-file no-explicit-any -import { expect, getFreePort, Oak } from "./deps.ts"; -import { describe, it } from "./utils.ts"; +import { getFreePort } from "../deps.ts"; +import { expect, Oak } from "./deps.ts"; +import { describe, it, random } from "./utils.ts"; import { superdeno, Test } from "../mod.ts"; const { Application, Router } = Oak; -function random(min: number, max: number): number { - return Math.round(Math.random() * (max - min)) + min; -} - const bootstrapOakServerTest = async ( { configureApp, assertionsDelegate, done }: { configureApp: ( @@ -35,7 +32,7 @@ const bootstrapOakServerTest = async ( app.use(router.allowedMethods()); const controller = new AbortController(); - const { signal } = controller; + const signal = controller.signal; const freePort = await getFreePort(random(1024, 49151)); app.addEventListener("listen", ({ hostname, port, secure }: any) => { @@ -52,7 +49,7 @@ describe("Oak: superdeno(url)", () => { it("Oak: superdeno(url): should support open `superdeno(url)` format for web frameworks such as Oak", async (done) => { await bootstrapOakServerTest({ configureApp: ({ router }) => { - router.get("/", (ctx: Oak.RouterContext) => { + router.get("/", (ctx) => { ctx.response.body = "hello"; }); }, @@ -71,7 +68,7 @@ describe("Oak: superdeno(url)", () => { it("Oak: superdeno(url): .expect(status, body[, fn]): should assert the response body and status", async (done) => { await bootstrapOakServerTest({ configureApp: ({ router }) => { - router.get("/", (ctx: Oak.RouterContext) => { + router.get("/", (ctx) => { ctx.response.body = "foo"; }); }, @@ -89,7 +86,7 @@ describe("Oak: superdeno(url)", () => { it("superdeno(app): .expect(status, body[, fn]): should assert the response body and error status'", async (done) => { await bootstrapOakServerTest({ configureApp: ({ router }) => { - router.get("/", (ctx: Oak.RouterContext) => { + router.get("/", (ctx) => { ctx.throw(400, "foo"); }); }, @@ -109,7 +106,7 @@ describe("Oak: superdeno(url)", () => { it("Oak: superdeno(url): .end(cb): should set `this` to the test object when calling the `cb` in `.end(cb)`", async (done) => { await bootstrapOakServerTest({ configureApp: ({ router }) => { - router.get("/", (ctx: Oak.RouterContext) => { + router.get("/", (ctx) => { ctx.response.body = "hello"; }); }, @@ -133,7 +130,7 @@ describe("Oak: superdeno(app.handle)", () => { const router = new Router(); const app = new Application(); - router.get("/", (ctx: Oak.RouterContext) => { + router.get("/", (ctx) => { ctx.response.body = "Hello Deno!"; }); diff --git a/test/superdeno.opine.test.ts b/test/superdeno.opine.test.ts index 2aa6f8a..db30990 100644 --- a/test/superdeno.opine.test.ts +++ b/test/superdeno.opine.test.ts @@ -659,7 +659,7 @@ describe("superdeno(app)", () => { ) .end((err, _res) => { expect(err.message).toEqual( - 'expected { stringValue: "foo", numberValue: 3, nestedObject: { innerString: 5 } } response body, got { stringValue: "foo", numberValue: 3, nestedObject: { innerString: "5" } }', + 'expected {\n stringValue: "foo",\n numberValue: 3,\n nestedObject: { innerString: 5 }\n} response body, got {\n stringValue: "foo",\n numberValue: 3,\n nestedObject: { innerString: "5" }\n}', ); // eslint-disable-line max-len superdeno(app) diff --git a/test/utils.ts b/test/utils.ts index 0fdc505..57b2137 100644 --- a/test/utils.ts +++ b/test/utils.ts @@ -3,6 +3,17 @@ */ export const TEST_TIMEOUT = 3000; +/** + * Random number generator for a given range. + * + * @param min Lower bound + * @param max Upper bound + * @returns A random int in the range + */ +export function random(min: number, max: number): number { + return Math.round(Math.random() * (max - min)) + min; +} + /** * A no-op _describe_ method. * diff --git a/version.ts b/version.ts index 78ed505..cebf4c4 100644 --- a/version.ts +++ b/version.ts @@ -1,9 +1,9 @@ /** * Version of SuperDeno. */ -export const VERSION = "4.8.0"; +export const VERSION = "4.9.0"; /** * Supported versions of Deno. */ -export const DENO_SUPPORTED_VERSIONS = ["1.19.3"]; +export const DENO_SUPPORTED_VERSIONS = ["1.40.2"];