diff --git a/examples/tests-bundle.zzb b/examples/tests-bundle.zzb index 1c9b261..fa67a15 100644 --- a/examples/tests-bundle.zzb +++ b/examples/tests-bundle.zzb @@ -5,7 +5,7 @@ common: baseUrl: https://postman-echo.com - headers: + headers: Content-type: application/json tests: status: 200 @@ -17,14 +17,13 @@ variables: addressVar: streetAddress: 7th street city: Nara - postalCode: "560001" # string. This will make a difference when used in the body + postalCode: "560001" # string. This will make a difference when used in the body getUrl: /get fooVar: bar agrostar: https://test-farmerapp.agrostar.in pi: 3.14 requests: - simple-get: { method: GET, url: /get } get-with-params: @@ -33,7 +32,7 @@ requests: params: foo1: bar1 foo2: bar2 - tests: # old way of specifying json tests + tests: # old way of specifying json tests json: $.args.foo1: bar1 $.args.foo2: bar2 @@ -44,7 +43,7 @@ requests: params: foo1: bar1 foo2: - tests: # new way of specifying json + tests: # new way of specifying json $.url: "https://postman-echo.com/get?foo1=bar1&foo2" get-with-params-as-array: @@ -56,7 +55,7 @@ requests: - { name: foo2, value: multi-2 } tests: $.args.foo1: bar1 - $.args.foo2: { $eq: ["multi-1","multi-2"] } + $.args.foo2: { $eq: ["multi-1", "multi-2"] } get-with-params-values-as-array: method: GET @@ -64,11 +63,11 @@ requests: params: foo1: bar1 foo2: - - multi-1 - - multi-2 + - multi-1 + - multi-2 tests: $.args.foo1: bar1 - $.args.foo2: { $eq: ["multi-1","multi-2"] } + $.args.foo2: { $eq: ["multi-1", "multi-2"] } post-header-merge: url: /post @@ -84,7 +83,7 @@ requests: post-header-override: url: /post method: POST - headers: + headers: Content-Type: text/plain body: { foo: bar } tests: @@ -94,14 +93,14 @@ requests: status-404: url: /notfound method: GET - tests: + tests: status: 404 $h.content-type: { $exists: false } status-401: url: /status/401 method: GET - tests: { status: 401, { $.status: 401 } } + tests: { status: 401, $.status: 401 } encoding: method: GET @@ -118,7 +117,7 @@ requests: params: foo: 30%25+of+200 options: - raw: true + rawParams: true tests: $.url: https://postman-echo.com/get?foo=30%25+of+200 $.args.foo: 30% of 200 @@ -129,7 +128,7 @@ requests: url: /cookies/set tests: status: 302 - headers: { Location: /cookies, Content-Type: { $ne: application/json } } + headers: { Location: /cookies, Content-Type: { $ne: application/json } } redirects: options: @@ -145,7 +144,7 @@ requests: url: /encoding/utf8 tests: body: { $regex: unicode demo, $options: i } - headers: + headers: content-type: text/html; charset=utf-8 response-headers: @@ -185,7 +184,7 @@ requests: variables-in-body: method: POST url: /post - body: | # Alternate way of supplying the JSON string or any raw content + body: | # Alternate way of supplying the JSON string or any raw content { "foo1": "$(fooVar)", "foo2": $pi @@ -208,7 +207,7 @@ requests: header-case: method: GET url: $agrostar/userservice/ping/ - tests: + tests: headers: content-type: application/json @@ -227,7 +226,7 @@ requests: phoneNumbers: - type: mobile number: 0123-4567-8888 - available: [ 7, 22 ] + available: [7, 22] - type: home number: 0123-4567-8910 available: [18, 22] @@ -246,8 +245,8 @@ requests: $.data.middleName: null $.data.otherName: { $exists: false, $type: undefined } $.data.phoneNumbers[*].type: mobile - $.data.phoneNumbers[*].available: { $eq: [7,22] } - $.data.phoneNumbers.0.available: { $eq: [ 7, 22 ], $type: array } + $.data.phoneNumbers[*].available: { $eq: [7, 22] } + $.data.phoneNumbers.0.available: { $eq: [7, 22], $type: array } $.data.phoneNumbers.1.available: { $eq: "[18,22]", $type: array } # stress: ensure corner cases don't crash $.data.otherName.value: { $exists: false } # don't recurse down undefined @@ -261,7 +260,7 @@ requests: age: 26 name: John address: 1, example street - numbers: [ 444, 222 ] + numbers: [444, 222] object: { foo: bar } tests: status: { $ne: 200 } @@ -273,10 +272,10 @@ requests: $.data.numbers.0: "444" # 444 is not same as "444". We use === for $eq and !== for $neq # stress: ensure corner cases don't crash - $.data.age: { $size: 2 } # .length not supported for type: number + $.data.age: { $size: 2 } # .length not supported for type: number $.data.numbers[?(.@)]: 4 # invalid path - $.data.age.something: 55 # jsonpath should take care of this. - $.data.numbers[5]: 0 # jsonpath should take care of this + $.data.age.something: 55 # jsonpath should take care of this. + $.data.numbers[5]: 0 # jsonpath should take care of this # This request should tests should all fail due to bad tests schema # Ensure we don't crash on these. @@ -285,12 +284,12 @@ requests: url: /post body: address: 1, example street - numbers: [ 444, 222 ] + numbers: [444, 222] object: { foo: bar } tests: status: { $ne: 200 } headers: - content-type: {$exists: false} + content-type: { $exists: false } # Uncomment the following to run tests. Schema validation makes these invalid. # $.data.operator: { badop: any } # invalid operator badop. If you want to match an entire object/array, use it as the value of the $eq operator. # $.data.numbers: [444, 222] @@ -344,7 +343,7 @@ requests: tests: $.data.name: Tom $.data.address.city: Bangalore - $.data.address.pincode: 560001 # this time it is NOT a string. + $.data.address.pincode: 560001 # this time it is NOT a string. capture-checks-object-option-2: method: POST @@ -355,4 +354,3 @@ requests: tests: $.data.name: Tom $.data.address: { $eq: { city: Bangalore, pincode: 560001 } } - diff --git a/package-lock.json b/package-lock.json index 0a58037..95538d2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,16 +1,17 @@ { "name": "zzapi", - "version": "1.0.0", + "version": "1.0.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "zzapi", - "version": "1.0.0", + "version": "1.0.2", "license": "MIT", "dependencies": { "got": "11.8.6", - "jsonpath": "^1.1.1" + "jsonpath": "^1.1.1", + "yaml": "^2.3.4" }, "devDependencies": { "@types/jest": "^29.5.11", @@ -5459,7 +5460,6 @@ "version": "2.3.4", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.4.tgz", "integrity": "sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==", - "dev": true, "engines": { "node": ">= 14" } diff --git a/src/checkTypes.ts b/src/checkTypes.ts index c48cbc3..33009fc 100644 --- a/src/checkTypes.ts +++ b/src/checkTypes.ts @@ -152,7 +152,7 @@ const VALID_OPTIONS: { [type: string]: boolean } = { verifySSL: true, keepRawJSON: true, showHeaders: true, - raw: true, + rawParams: true, }; function checkOptions(obj: any): string | undefined { let ret = checkObjIsDict(obj, "options"); diff --git a/src/constructCurl.ts b/src/constructCurl.ts index 8ec86be..c6f6c41 100644 --- a/src/constructCurl.ts +++ b/src/constructCurl.ts @@ -1,4 +1,6 @@ -import { getBody, getParamsForUrl, getURL } from "./executeRequest"; +import { getStringValueIfDefined } from "./utils/typeUtils"; + +import { getParamsForUrl, getURL } from "./executeRequest"; import { RequestSpec } from "./models"; export function getCurlRequest(request: RequestSpec): string { @@ -12,7 +14,8 @@ export function getCurlRequest(request: RequestSpec): string { } let bodyFlag = ""; - if (request.httpRequest.body !== undefined) bodyFlag += ` -d '${getBody(request.httpRequest.body)}'`; + if (request.httpRequest.body !== undefined) + bodyFlag += ` -d '${getStringValueIfDefined(request.httpRequest.body)}'`; let followRedirectFlag = ""; if (request.options.follow) followRedirectFlag = " -L"; @@ -23,7 +26,7 @@ export function getCurlRequest(request: RequestSpec): string { const url = ` '${getURL( request.httpRequest.baseUrl, request.httpRequest.url, - getParamsForUrl(request.httpRequest.params, request.options.raw) + getParamsForUrl(request.httpRequest.params, request.options.rawParams) )}'`; const finalCurl = diff --git a/src/executeRequest.ts b/src/executeRequest.ts index 32da0a7..f21eeae 100644 --- a/src/executeRequest.ts +++ b/src/executeRequest.ts @@ -1,35 +1,28 @@ -import got, { Method } from "got"; +import got, { Method, OptionsOfTextResponseBody } from "got"; import { getStringValueIfDefined } from "./utils/typeUtils"; import { GotRequest, Param, RequestSpec } from "./models"; export function constructGotRequest(allData: RequestSpec): GotRequest { - const completeUrl = getURL( + const completeUrl: string = getURL( allData.httpRequest.baseUrl, allData.httpRequest.url, - getParamsForUrl(allData.httpRequest.params, allData.options.raw) + getParamsForUrl(allData.httpRequest.params, allData.options.rawParams) ); - const options = { + const options: OptionsOfTextResponseBody = { method: allData.httpRequest.method.toLowerCase() as Method, - body: getBody(allData.httpRequest.body), + body: getStringValueIfDefined(allData.httpRequest.body), headers: allData.httpRequest.headers, - followRedirect: allData.options?.follow, + followRedirect: allData.options.follow, + https: { rejectUnauthorized: allData.options.verifySSL }, retry: { limit: 0 }, - - https: { - rejectUnauthorized: allData.options?.verifySSL, - }, }; return got(completeUrl, options); } -export function getBody(body: any): string | undefined { - return getStringValueIfDefined(body); -} - export async function executeGotRequest(httpRequest: GotRequest): Promise<{ response: { [key: string]: any }; executionTime: number; @@ -48,11 +41,7 @@ export async function executeGotRequest(httpRequest: GotRequest): Promise<{ const res = e.response; if (res) { responseObject = res; - if (res.body) { - size = Buffer.byteLength(res.body); - } else { - size = 0; - } + size = res.body ? Buffer.byteLength(res.body) : 0; } else { responseObject = {}; if (e.code === "ERR_INVALID_URL") { @@ -68,18 +57,16 @@ export async function executeGotRequest(httpRequest: GotRequest): Promise<{ return { response: responseObject, executionTime: executionTime, byteLength: size, error: error }; } -export function getParamsForUrl(paramsArray: Param[] | undefined, raw: boolean): string { - if (!paramsArray) return ""; +export function getParamsForUrl(params: Param[] | undefined, rawParams: boolean): string { + if (!params) return ""; - let params: Param[] = paramsArray; let paramArray: string[] = []; - params.forEach((param) => { const key = param.name; let value = param.value; if (value == undefined) { paramArray.push(key); - } else if (raw === true) { + } else if (rawParams) { paramArray.push(`${key}=${getStringValueIfDefined(value)}`); } else { paramArray.push(`${key}=${encodeURIComponent(getStringValueIfDefined(value))}`); diff --git a/src/index.ts b/src/index.ts index 1302290..890c5c5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,10 @@ -export { RequestPosition, ResponseData, RequestSpec, GotRequest, TestResult } from "./models"; +export { + RequestPosition, + ResponseData, + RequestSpec, + GotRequest, + TestResult +} from "./models"; export { getRequestPositions, getAllRequestSpecs, getRequestSpec } from "./parseBundle"; diff --git a/src/mergeData.ts b/src/mergeData.ts index 246fc01..22fa6f0 100644 --- a/src/mergeData.ts +++ b/src/mergeData.ts @@ -72,9 +72,9 @@ function getMergedOptions(cOptions: RawOptions = {}, rOptions: RawOptions = {}): const verifySSL = options.verifySSL == true; const keepRawJSON = options.keepRawJSON == true; const showHeaders = options.showHeaders == true; - const raw = options.raw == true; + const rawParams = options.rawParams == true; - return { follow, verifySSL, keepRawJSON, showHeaders, raw }; + return { follow, verifySSL, keepRawJSON, showHeaders, rawParams }; } function getMergedSetVars( diff --git a/src/models.ts b/src/models.ts index 9c20217..9a075cd 100644 --- a/src/models.ts +++ b/src/models.ts @@ -1,4 +1,4 @@ -import { CancelableRequest, Response, Method } from "got"; +import { CancelableRequest, Response, Method, OptionsOfTextResponseBody } from "got"; export interface Header { name: string; @@ -15,7 +15,7 @@ export interface Options { verifySSL: boolean; keepRawJSON: boolean; showHeaders: boolean; - raw: boolean; + rawParams: boolean; } export type Assertion = number | boolean | string | null | { [op: string]: any }; @@ -49,7 +49,7 @@ export interface RawOptions { verifySSL?: boolean; keepRawJSON?: boolean; showHeaders?: boolean; - raw?: boolean; + rawParams?: boolean; } export interface RawTests { @@ -121,8 +121,6 @@ export interface RequestPosition { end: { line: number; col: number }; } -export type GotRequest = CancelableRequest>; - export interface TestResult { pass: boolean; spec: string; @@ -131,3 +129,5 @@ export interface TestResult { received: any; message?: string; } + +export type GotRequest = CancelableRequest>; diff --git a/src/replaceVars.ts b/src/replaceVars.ts index c7a7ed9..2a830f4 100644 --- a/src/replaceVars.ts +++ b/src/replaceVars.ts @@ -1,56 +1,17 @@ -import { getStrictStringValue } from "./utils/typeUtils"; +import { getStrictStringValue, isArrayOrDict } from "./utils/typeUtils"; import { Variables } from "./variables"; import { RequestSpec } from "./models"; -function replaceVariables(data: any, variables: Variables): { data: any; undefinedVars: string[] } { - if (typeof data === "object" && data !== null) { - return replaceVariablesInNonScalar(data, variables); - } - if (typeof data === "string") { - return replaceVariablesInString(data, variables); - } - return { data: data, undefinedVars: [] }; -} - -function replaceVariablesInNonScalar( - data: { [key: string]: any } | any[], - variables: Variables -): { data: any; undefinedVars: string[] } { - if (Array.isArray(data)) { - return replaceVariablesInArray(data, variables); - } else { - return replaceVariablesInObject(data, variables); - } -} - function replaceVariablesInArray( data: any[], variables: Variables ): { data: any[]; undefinedVars: string[] } { let newData: any[] = []; - const undefs: string[] = []; + let undefs: string[] = []; data.forEach((item) => { - let newItem: any | any[]; - let newUndefs: string[] = []; - if (typeof item === "object") { - if (Array.isArray(item)) { - const replacedData = replaceVariablesInArray(item, variables); - newItem = replacedData.data; - newUndefs = replacedData.undefinedVars; - } else { - const replacedData = replaceVariablesInObject(item, variables); - newItem = replacedData.data; - newUndefs = replacedData.undefinedVars; - } - } else if (typeof item === "string") { - const replacedData = replaceVariablesInString(item, variables); - newItem = replacedData.data; - newUndefs = replacedData.undefinedVars; - } else { - newItem = item; - } + const { data: newItem, undefinedVars: newUndefs } = replaceVariables(item, variables); newData.push(newItem); undefs.push(...newUndefs); @@ -59,30 +20,32 @@ function replaceVariablesInArray( return { data: newData, undefinedVars: undefs }; } -function replaceVariablesInObject( +function replaceVariablesInDict( obj: { [key: string]: any }, variables: Variables ): { data: { [key: string]: any }; undefinedVars: string[] } { - const undefs: string[] = []; + let newData: { [key: string]: any } = {}; + let undefs: string[] = []; + for (const key in obj) { - let replacedData = undefined; - if (typeof obj[key] === "object") { - if (Array.isArray(obj[key])) { - replacedData = replaceVariablesInArray(obj[key], variables); - } else { - replacedData = replaceVariablesInObject(obj[key], variables); - } - } else if (typeof obj[key] === "string") { - replacedData = replaceVariablesInString(obj[key], variables); - } + const { data: newItem, undefinedVars: newUndefs } = replaceVariables(obj[key], variables); - if (replacedData !== undefined) { - obj[key] = replacedData.data; - const newUndefs = replacedData.undefinedVars; - undefs.push(...newUndefs); - } + newData[key] = newItem; + undefs.push(...newUndefs); + } + + return { data: newData, undefinedVars: undefs }; +} + +function replaceVariablesInObject( + data: { [key: string]: any } | any[], + variables: Variables +): { data: any; undefinedVars: string[] } { + if (Array.isArray(data)) { + return replaceVariablesInArray(data, variables); + } else { + return replaceVariablesInDict(data, variables); } - return { data: obj, undefinedVars: undefs }; } /** @@ -124,7 +87,7 @@ function replaceVariablesInString( let variableIsFullText: boolean = false; const undefs: string[] = []; - function replaceVariable(match: string, varName: any): string { + function replaceVar(match: string, varName: any): string { if (typeof varName === "string") { if (variables.hasOwnProperty(varName)) { const varVal = variables[varName]; @@ -137,19 +100,19 @@ function replaceVariablesInString( undefs.push(varName); } } - return match; // if varName is undefined (not string), or is not a valid variable + return match; // if varName is not defined well (not string), or is not a valid variable } // todo: make a complete match regex and return native type immediately. const outputText = text .replace(VAR_REGEX_WITH_BRACES, (match, varName) => { - return replaceVariable(match, varName); + return replaceVar(match, varName); }) .replace(VAR_REGEX_WITHOUT_BRACES, (match) => { if (match.length <= 1) return match; // this would lead to an invalid slice const varName = match.slice(1); - return replaceVariable(match, varName); + return replaceVar(match, varName); }); if (variableIsFullText) { @@ -159,6 +122,16 @@ function replaceVariablesInString( } } +function replaceVariables(data: any, variables: Variables): { data: any; undefinedVars: string[] } { + if (isArrayOrDict(data)) { + return replaceVariablesInObject(data, variables); + } else if (typeof data === "string") { + return replaceVariablesInString(data, variables); + } else { + return { data: data, undefinedVars: [] }; + } +} + export function replaceVariablesInRequest(request: RequestSpec, variables: Variables): string[] { const undefs: string[] = []; diff --git a/src/variableParser.ts b/src/variableParser.ts index d45ea0f..d5b73a4 100644 --- a/src/variableParser.ts +++ b/src/variableParser.ts @@ -51,7 +51,7 @@ export function loadVariables( let envVars = {}; varFileContents.forEach((fileContents) => { const parsedData = YAML.parse(fileContents); - if (parsedData && parsedData.hasOwnProperty(envName)) { + if (parsedData && isDict(parsedData[envName])) { Object.assign(envVars, parsedData[envName]); } });