From b3b475a0b337c337955fbdee725f839a7a79588e Mon Sep 17 00:00:00 2001 From: Daniel Ramos Date: Thu, 23 Jun 2022 10:57:05 +0100 Subject: [PATCH] fix: query string array support (#182) * fix: query string array support * fix: remove elvis operator * fix: use the objectToQueryString function at tepper runner * chore: bump version to 0.4.2 * fix: skip undefined values --- package-lock.json | 8 ++--- package.json | 4 +-- src/TepperConfig.ts | 3 +- src/TepperRunner.ts | 16 ++++++++-- src/queries/objectToQueryString.spec.ts | 31 +++++++++++++++++++ src/queries/objectToQueryString.ts | 18 +++++++++++ test/query-params.spec.ts | 41 +++++++++++++++++++++++++ 7 files changed, 110 insertions(+), 11 deletions(-) create mode 100644 src/queries/objectToQueryString.spec.ts create mode 100644 src/queries/objectToQueryString.ts diff --git a/package-lock.json b/package-lock.json index 81305f07..d08ce0a7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2554,7 +2554,7 @@ "node_modules/busboy": { "version": "0.2.14", "resolved": "https://registry.npmjs.org/busboy/-/busboy-0.2.14.tgz", - "integrity": "sha1-bCpiLvz0fFe7vh4qnDetNseSVFM=", + "integrity": "sha512-InWFDomvlkEj+xWLBfU3AvnbVYqeTWmQopiW0tWWEy5yehYm2YkGEc59sUmw/4ty5Zj/b0WHGs1LgecuBSBGrg==", "dev": true, "dependencies": { "dicer": "0.2.5", @@ -3014,7 +3014,7 @@ "node_modules/dicer": { "version": "0.2.5", "resolved": "https://registry.npmjs.org/dicer/-/dicer-0.2.5.tgz", - "integrity": "sha1-WZbAhrszIYyBLAkL3cCc0S+stw8=", + "integrity": "sha512-FDvbtnq7dzlPz0wyYlOExifDEZcu8h+rErEXgfxqmLfRfC/kJidEFh4+effJRO3P0xmfqyPbSMG0LveNRfTKVg==", "dev": true, "dependencies": { "readable-stream": "1.1.x", @@ -10722,7 +10722,7 @@ "busboy": { "version": "0.2.14", "resolved": "https://registry.npmjs.org/busboy/-/busboy-0.2.14.tgz", - "integrity": "sha1-bCpiLvz0fFe7vh4qnDetNseSVFM=", + "integrity": "sha512-InWFDomvlkEj+xWLBfU3AvnbVYqeTWmQopiW0tWWEy5yehYm2YkGEc59sUmw/4ty5Zj/b0WHGs1LgecuBSBGrg==", "dev": true, "requires": { "dicer": "0.2.5", @@ -11074,7 +11074,7 @@ "dicer": { "version": "0.2.5", "resolved": "https://registry.npmjs.org/dicer/-/dicer-0.2.5.tgz", - "integrity": "sha1-WZbAhrszIYyBLAkL3cCc0S+stw8=", + "integrity": "sha512-FDvbtnq7dzlPz0wyYlOExifDEZcu8h+rErEXgfxqmLfRfC/kJidEFh4+effJRO3P0xmfqyPbSMG0LveNRfTKVg==", "dev": true, "requires": { "readable-stream": "1.1.x", diff --git a/package.json b/package.json index 8e9eef88..5ef78b81 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tepper", - "version": "0.4.1", + "version": "0.4.2", "description": "Modern library for testing HTTP servers", "main": "dist/tepper.js", "engines": { @@ -73,4 +73,4 @@ "node-fetch": "^2.6.1", "@types/node-fetch": "2.6.2" } -} +} \ No newline at end of file diff --git a/src/TepperConfig.ts b/src/TepperConfig.ts index 95cc3117..8523919d 100644 --- a/src/TepperConfig.ts +++ b/src/TepperConfig.ts @@ -1,4 +1,3 @@ -import { ParsedUrlQueryInput } from "querystring" import { DebugOptions } from "./DebugOptions" export type TepperConfig = { @@ -6,7 +5,7 @@ export type TepperConfig = { readonly path: string readonly body: string | object | null readonly isForm: boolean - readonly query: ParsedUrlQueryInput | null + readonly query: object | null readonly redirects: number readonly expectedStatus: number | null readonly expectedBody: diff --git a/src/TepperRunner.ts b/src/TepperRunner.ts index 8744a198..978247ea 100644 --- a/src/TepperRunner.ts +++ b/src/TepperRunner.ts @@ -4,6 +4,7 @@ import fetch from "node-fetch" import qs from "querystring" import { Readable } from "stream" import { FormDataEncoder } from "form-data-encoder" +import { URLSearchParams } from "url" import { listenAppPromised, listenServerPromised } from "./utils/listenPromised" import { getBaseUrl } from "./utils/getBaseUrl" import { closePromised } from "./utils/closePromised" @@ -12,6 +13,7 @@ import { TepperConfig } from "./TepperConfig" import { TepperResult } from "./TepperResult" import { BaseUrlServerOrExpress } from "./BaseUrlServerOrExpress" import { objectToFormData } from "./forms/objectToFormData" +import { objectToQueryString } from "./queries/objectToQueryString" function isExpressApp( baseUrlServerOrExpress: BaseUrlServerOrExpress, @@ -63,9 +65,7 @@ export class TepperRunner { endpoint: string, config: TepperConfig, ): Promise { - const endpointWithQuery = config.query - ? endpoint.concat("?").concat(qs.stringify(config.query)) - : endpoint + const endpointWithQuery = this.appendQuery(endpoint, config) const { body, headers } = this.insertBodyIfPresent(config) @@ -112,6 +112,16 @@ export class TepperRunner { return result } + private static appendQuery(endpoint: string, config: TepperConfig) { + if (!config.query) { + return endpoint + } + + return endpoint + .concat("?") + .concat(objectToQueryString(config.query).toString()) + } + private static insertBodyIfPresent(config: TepperConfig): { body: any headers: object diff --git a/src/queries/objectToQueryString.spec.ts b/src/queries/objectToQueryString.spec.ts new file mode 100644 index 00000000..077db68a --- /dev/null +++ b/src/queries/objectToQueryString.spec.ts @@ -0,0 +1,31 @@ +import { objectToQueryString } from "./objectToQueryString" + +describe("objectToQueryString", () => { + it("serializes a query with strings", () => { + const query = { + hello: "world", + } + + const result = objectToQueryString(query) + + expect(result).toEqual("hello=world") + }) + + it("serializes a query with an array", () => { + const query = { + tags: ["first-tag", "second-tag"], + } + + const result = objectToQueryString(query) + + expect(result).toEqual("tags[]=first-tag&tags[]=second-tag") + }) + + it("skips undefined values", () => { + const query = { empty: undefined } + + const result = objectToQueryString(query) + + expect(result).toEqual("") + }) +}) diff --git a/src/queries/objectToQueryString.ts b/src/queries/objectToQueryString.ts new file mode 100644 index 00000000..957d6075 --- /dev/null +++ b/src/queries/objectToQueryString.ts @@ -0,0 +1,18 @@ +import { URLSearchParams } from "url" + +export function objectToQueryString(query: object) { + const params = new URLSearchParams() + const arrayIndicator = "__array_indicator__" + + for (const [key, value] of Object.entries(query)) { + if (Array.isArray(value)) { + for (const v of value) { + params.append(key + arrayIndicator, v) + } + } else if (value != null) { + params.append(key, (value && value.toString()) || "") + } + } + + return params.toString().replace(new RegExp(arrayIndicator, "g"), "[]") +} diff --git a/test/query-params.spec.ts b/test/query-params.spec.ts index d7eb35a2..44bb978d 100644 --- a/test/query-params.spec.ts +++ b/test/query-params.spec.ts @@ -14,4 +14,45 @@ describe("query params", () => { expect(body).toEqual({ hello: "world" }) }) + + it("skips undefined values", async () => { + const app = express().get("/", (req, res) => { + res.send(req.query) + }) + + const { body } = await tepper(app) + .get("") + .withQuery({ hello: undefined }) + .run() + + expect(body).toEqual({}) + }) + + describe("array params", () => { + it("parses correctly an array", async () => { + const app = express().get("/", (req, res) => { + res.send(req.query) + }) + + const { body } = await tepper(app) + .get("") + .withQuery({ tags: ["first-tag", "second-tag"] }) + .run() + + expect(body).toEqual({ tags: ["first-tag", "second-tag"] }) + }) + + it("parses correctly an array with one element", async () => { + const app = express().get("/", (req, res) => { + res.send(req.query) + }) + + const { body } = await tepper(app) + .get("") + .withQuery({ tags: ["first-tag"] }) + .run() + + expect(body).toEqual({ tags: ["first-tag"] }) + }) + }) })