Skip to content

Commit

Permalink
feat: add type support to responses (#219)
Browse files Browse the repository at this point in the history
  • Loading branch information
DanielRamosAcosta authored Aug 25, 2022
1 parent 4b4b99b commit 91c4d03
Show file tree
Hide file tree
Showing 8 changed files with 4,119 additions and 26 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "tepper",
"version": "0.4.3",
"version": "1.0.0",
"description": "Modern library for testing HTTP servers",
"main": "dist/tepper.js",
"engines": {
Expand Down
34 changes: 21 additions & 13 deletions src/TepperBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,46 +4,54 @@ import { DebugOptions } from "./DebugOptions"
import { TepperConfig } from "./TepperConfig"
import { TepperResult } from "./TepperResult"
import { TepperRunner } from "./TepperRunner"
export class TepperBuilder {

type StandardError = {
error: {
status: number,
code: string,
message: string
}
}
export class TepperBuilder<ExpectedResponse, ErrorType = StandardError> {
public constructor(
private readonly baseUrlServerOrExpress: BaseUrlServerOrExpress,
private readonly config: TepperConfig,
) {}

public get(path: string) {
return new TepperBuilder(this.baseUrlServerOrExpress, {
public get<ExpectedResponse = any, ErrorType = StandardError>(path: string) {
return new TepperBuilder<ExpectedResponse, ErrorType>(this.baseUrlServerOrExpress, {
...this.config,
method: "GET",
path,
})
}

public post(path: string) {
return new TepperBuilder(this.baseUrlServerOrExpress, {
public post<ExpectedResponse = any, ErrorType = StandardError>(path: string) {
return new TepperBuilder<ExpectedResponse, ErrorType>(this.baseUrlServerOrExpress, {
...this.config,
method: "POST",
path,
})
}

public put(path: string) {
return new TepperBuilder(this.baseUrlServerOrExpress, {
public put<ExpectedResponse = any, ErrorType = StandardError>(path: string) {
return new TepperBuilder<ExpectedResponse, ErrorType>(this.baseUrlServerOrExpress, {
...this.config,
method: "PUT",
path,
})
}

public patch(path: string) {
return new TepperBuilder(this.baseUrlServerOrExpress, {
public patch<ExpectedResponse = any, ErrorType = StandardError>(path: string) {
return new TepperBuilder<ExpectedResponse, ErrorType>(this.baseUrlServerOrExpress, {
...this.config,
method: "PATCH",
path,
})
}

public delete(path: string) {
return new TepperBuilder(this.baseUrlServerOrExpress, {
public delete<ExpectedResponse = any, ErrorType = StandardError>(path: string) {
return new TepperBuilder<ExpectedResponse, ErrorType>(this.baseUrlServerOrExpress, {
...this.config,
method: "DELETE",
path,
Expand Down Expand Up @@ -151,8 +159,8 @@ export class TepperBuilder {
})
}

public async run(): Promise<TepperResult> {
return TepperRunner.launchServerAndRun(
public async run(): Promise<TepperResult<ExpectedResponse, ErrorType>> {
return TepperRunner.launchServerAndRun<ExpectedResponse, ErrorType>(
this.baseUrlServerOrExpress,
this.config,
)
Expand Down
4 changes: 2 additions & 2 deletions src/TepperResult.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { Headers } from "node-fetch"

export type TepperResult = {
export type TepperResult<ExpectedResponse, ErrorType> = {
text: string
// eslint-disable-next-line @typescript-eslint/no-explicit-any
body: any
body: ExpectedResponse & ErrorType
status: number
headers: Headers
}
22 changes: 13 additions & 9 deletions src/TepperRunner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,10 @@ export class TepperRunner {
return await listenAppPromised(baseUrlServerOrExpress, 0, "127.0.0.1")
}

public static async launchServerAndRun(
public static async launchServerAndRun<ExpectedResponse, ErrorType>(
baseUrlServerOrExpress: BaseUrlServerOrExpress,
config: TepperConfig,
): Promise<TepperResult> {
): Promise<TepperResult<ExpectedResponse, ErrorType>> {
if (isExpressApp(baseUrlServerOrExpress)) {
const server = await this.instantiateExpress(baseUrlServerOrExpress)

Expand All @@ -59,12 +59,11 @@ export class TepperRunner {
return this.run(endpoint, config)
}

private static async run(
private static async run<ExpectedResponse, ErrorType>(
endpoint: string,
config: TepperConfig,
): Promise<TepperResult> {
): Promise<TepperResult<ExpectedResponse, ErrorType>> {
const endpointWithQuery = this.appendQuery(endpoint, config)

const { body, headers } = this.insertBodyIfPresent(config)

const cookies = this.parseCookies(config.cookies)
Expand All @@ -84,11 +83,13 @@ export class TepperRunner {

const text = await response.text()

const result: TepperResult = {
const result: TepperResult<ExpectedResponse, ErrorType> = {
status: response.status,
headers: response.headers,
text,
body: safeJsonParse(text) || null,
body: safeJsonParse<ExpectedResponse & ErrorType>(
text,
) as ExpectedResponse & ErrorType,
}

if (config.debug && config.debug.body) {
Expand All @@ -108,7 +109,7 @@ export class TepperRunner {
})
}

this.runExpectations(result, config)
this.runExpectations<ExpectedResponse, ErrorType>(result, config)

return result
}
Expand Down Expand Up @@ -155,7 +156,10 @@ export class TepperRunner {
.join("; ")
}

private static runExpectations(result: TepperResult, config: TepperConfig) {
private static runExpectations<ExpectedResponse, ErrorType>(
result: TepperResult<ExpectedResponse, ErrorType>,
config: TepperConfig,
) {
if (config.expectedBody) {
if (typeof config.expectedBody === "string") {
expect(result.text).toEqual(config.expectedBody)
Expand Down
2 changes: 1 addition & 1 deletion src/utils/safeJsonParse.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export function safeJsonParse(text: string): Record<string, unknown> | null {
export function safeJsonParse<T>(text: string): T | null {
try {
return JSON.parse(text)
} catch {
Expand Down
32 changes: 32 additions & 0 deletions test/expectations.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,38 @@ describe("expectations", () => {
await tepper(app).get("/").expect({ status: "ok" }).run()
})

it("supports typing the response", async () => {
const app = express()
.use(express.json())
.get("/", (_req, res) => {
res.json({ status: "ok" })
})

const { body } = await tepper(app).get<{ status: string }>("/").run()

expect(body.status).toBe("ok")
})

it("has default error typing", async () => {
const app = express()
.use(express.json())
.get("/", (_req, res) => {
res.status(400).json({
error: {
code: "INVALID_EMAIL",
message: "The provided email is invalid",
status: 400,
},
})
})

const { body } = await tepper(app).get("/").run()

expect(body.error.code).toBe("INVALID_EMAIL")
expect(body.error.message).toBe("The provided email is invalid")
expect(body.error.status).toBe(400)
})

it("asserts with a json body when is wrong", async () => {
const app = express()
.use(express.json())
Expand Down
11 changes: 11 additions & 0 deletions test/headers.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,15 @@ describe("headers", () => {

expect(text).toEqual(CUSTOM_HEADER_VALUE)
})

it("does not add headers if they are not provided", async () => {
const CUSTOM_HEADER = "X-Custom-Header"
const app = express().post("/", (req, res) => {
res.send(req.headers[CUSTOM_HEADER.toLowerCase()])
})

const { text } = await tepper(app).post("/").send("foo").run()

expect(text).toEqual("")
})
})
Loading

0 comments on commit 91c4d03

Please sign in to comment.