diff --git a/packages/dredd/lib/general.ts b/packages/dredd/lib/general.ts new file mode 100644 index 000000000..07e669fe1 --- /dev/null +++ b/packages/dredd/lib/general.ts @@ -0,0 +1,82 @@ +export enum HTTPMethod { + CONNECT = 'CONNECT', + OPTIONS = 'OPTIONS', + POST = 'POST', + GET = 'GET', + HEAD = 'HEAD', + PUT = 'PUT', + PATCH = 'PATCH', + DELETE = 'DELETE', + TRACE = 'TRACE', +} + +export enum BodyEncoding { + 'utf-8', + 'base64', +} + +export enum TransactionTestStatus { + 'pass', + 'fail', + 'skip', +} + +export interface Transaction { + id: string; + name: string; + origin: TransactionOrigin; + host: string; + port: number; + protocol: 'http:' | 'https:'; + fullPath: string; + request: TransactionRequest; + expected: { + statusCode: number; + headers: Record; + body: string; + bodySchema: Record; + }; + real: { + statusCode: string; + headers: Record; + body: string; + bodyEncoding: BodyEncoding; + }; + skip: boolean; + fail: boolean; + + test: TransactionTest; +} + +export interface TransactionRequest { + method: HTTPMethod; + url: string; + body?: string; + bodyEncoding?: BodyEncoding; + headers?: Record; +} + +export interface TransactionOrigin { + filename: string; + apiName: string; + resourceGroupName: string; + resourceName: string; + actionName: string; + exampleName: string; +} + +export interface TransactionTest { + start: Date; + end: Date; + duration: number; + startedAt: number; + title: string; + request: TransactionRequest; + actual: any; + expected: any; + status: TransactionTestStatus; + message: string; + results: any; + valid: boolean; + origin: TransactionOrigin; +} diff --git a/packages/dredd/lib/isURL.js b/packages/dredd/lib/isURL.ts similarity index 52% rename from packages/dredd/lib/isURL.js rename to packages/dredd/lib/isURL.ts index 2f27a30f9..12178f6c8 100644 --- a/packages/dredd/lib/isURL.js +++ b/packages/dredd/lib/isURL.ts @@ -1,8 +1,6 @@ /** * Decides whether given string is a URL or not - * @param {string} location - * @returns {boolean} */ -export default function isURL(location) { +export default function isURL(location: string): boolean { return /^http(s)?:\/\//.test(location); } diff --git a/packages/dredd/lib/resolveLocations.js b/packages/dredd/lib/resolveLocations.ts similarity index 73% rename from packages/dredd/lib/resolveLocations.js rename to packages/dredd/lib/resolveLocations.ts index 4fffdafd2..bf0292b32 100644 --- a/packages/dredd/lib/resolveLocations.js +++ b/packages/dredd/lib/resolveLocations.ts @@ -7,19 +7,18 @@ import isURL from './isURL'; * * Keeps URLs intact. Keeps the original order. Throws in case there's a glob * pattern which doesn't resolve to any existing files. - * - * @param {string} workingDirectory - * @param {string[]} locations - * @returns {string[]} */ -export default function resolveLocations(workingDirectory, locations) { +export default function resolveLocations( + workingDirectory: string, + locations: string[], +): string[] { const resolvedLocations = locations // resolves paths to local files, produces an array of arrays .map((location) => isURL(location) ? [location] : resolvePaths(workingDirectory, [location]), ) // flattens the array of arrays - .reduce((flatArray, array) => flatArray.concat(array), []); + .reduce((flatArray, array) => flatArray.concat(array), []); return Array.from(new Set(resolvedLocations)); } diff --git a/packages/dredd/lib/resolveModule.js b/packages/dredd/lib/resolveModule.ts similarity index 69% rename from packages/dredd/lib/resolveModule.js rename to packages/dredd/lib/resolveModule.ts index f521862ca..96a2b63dc 100644 --- a/packages/dredd/lib/resolveModule.js +++ b/packages/dredd/lib/resolveModule.ts @@ -1,7 +1,10 @@ import fs from 'fs'; import path from 'path'; -export default function resolveModule(workingDirectory, moduleName) { +export default function resolveModule( + workingDirectory: string, + moduleName: string, +): string { const absolutePath = path.resolve(workingDirectory, moduleName); return fs.existsSync(absolutePath) || fs.existsSync(`${absolutePath}.js`) ? absolutePath diff --git a/packages/dredd/lib/resolvePaths.js b/packages/dredd/lib/resolvePaths.ts similarity index 89% rename from packages/dredd/lib/resolvePaths.js rename to packages/dredd/lib/resolvePaths.ts index c7e164a2c..1c8797d19 100644 --- a/packages/dredd/lib/resolvePaths.js +++ b/packages/dredd/lib/resolvePaths.ts @@ -6,7 +6,7 @@ import glob from 'glob'; const basename = process.platform === 'win32' ? path.win32.basename : path.basename; -function resolveGlob(workingDirectory, pattern) { +function resolveGlob(workingDirectory: string, pattern: string): string[] { // 'glob.sync()' does not resolve paths, only glob patterns if (glob.hasMagic(pattern)) { return glob @@ -23,7 +23,10 @@ function resolveGlob(workingDirectory, pattern) { * Resolves glob patterns and sorts the files alphabetically by their basename. * Throws in case there's a pattern which doesn't resolve to any existing files. */ -export default function resolvePaths(workingDirectory, patterns) { +export default function resolvePaths( + workingDirectory: string, + patterns: string[], +) { if (!patterns || patterns.length < 1) { return []; } diff --git a/packages/dredd/lib/sortTransactions.js b/packages/dredd/lib/sortTransactions.js deleted file mode 100644 index 73a6bb814..000000000 --- a/packages/dredd/lib/sortTransactions.js +++ /dev/null @@ -1,42 +0,0 @@ -// Often, API description is arranged with a sequence of methods that lends -// itself to understanding by the human reading the documentation. -// -// However, the sequence of methods may not be appropriate for the machine -// reading the documentation in order to test the API. -// -// By sorting the transactions by their methods, it is possible to ensure that -// objects are created before they are read, updated, or deleted. -export default function sortTransactions(arr) { - arr.forEach((a, i) => { - a._index = i; - }); - - arr.sort((a, b) => { - const sortedMethods = [ - 'CONNECT', - 'OPTIONS', - 'POST', - 'GET', - 'HEAD', - 'PUT', - 'PATCH', - 'DELETE', - 'TRACE', - ]; - - const methodIndexA = sortedMethods.indexOf(a.request.method); - const methodIndexB = sortedMethods.indexOf(b.request.method); - - if (methodIndexA < methodIndexB) { - return -1; - } - if (methodIndexA > methodIndexB) { - return 1; - } - return a._index - b._index; - }); - - arr.forEach((a) => delete a._index); - - return arr; -} diff --git a/packages/dredd/lib/sortTransactions.ts b/packages/dredd/lib/sortTransactions.ts new file mode 100644 index 000000000..3abe6447a --- /dev/null +++ b/packages/dredd/lib/sortTransactions.ts @@ -0,0 +1,61 @@ +import { HTTPMethod, Transaction } from './general'; + +const sortedMethods: HTTPMethod[] = [ + HTTPMethod.CONNECT, + HTTPMethod.OPTIONS, + HTTPMethod.POST, + HTTPMethod.GET, + HTTPMethod.HEAD, + HTTPMethod.PUT, + HTTPMethod.PATCH, + HTTPMethod.DELETE, + HTTPMethod.TRACE, +]; + +// Often, API description is arranged with a sequence of methods that lends +// itself to understanding by the human reading the documentation. +// +// However, the sequence of methods may not be appropriate for the machine +// reading the documentation in order to test the API. +// +// By sorting the transactions by their methods, it is possible to ensure that +// objects are created before they are read, updated, or deleted. +export default function sortTransactions( + transactions: Transaction[], +): Transaction[] { + // Convert the list of transactions into a list of tuples + // that hold each trasnaction index and details. + const tempTransactions: Array<[number, Transaction]> = transactions.map( + (transaction, index) => [index, transaction], + ); + + tempTransactions.sort( + ([leftIndex, leftTransaction], [rightIndex, rightTransaction]) => { + const methodIndexA = sortedMethods.indexOf( + leftTransaction.request.method, + ); + const methodIndexB = sortedMethods.indexOf( + rightTransaction.request.method, + ); + + // Sort transactions according to the transaction's request method + if (methodIndexA < methodIndexB) { + return -1; + } + + if (methodIndexA > methodIndexB) { + return 1; + } + + // In case two transactions' request methods are the same, + // preserve the original order of those transactions + return leftIndex - rightIndex; + }, + ); + + const cleanTransactions = tempTransactions.map( + ([_, transaction]) => transaction, + ); + + return cleanTransactions; +} diff --git a/packages/dredd/test/tsconfig.json b/packages/dredd/test/tsconfig.json index 102f42c8d..70b87cd4a 100644 --- a/packages/dredd/test/tsconfig.json +++ b/packages/dredd/test/tsconfig.json @@ -5,7 +5,8 @@ "target": "esnext", "allowSyntheticDefaultImports": true, "allowJs": true, - "noEmit": true + "noEmit": true, + "rootDir": "../" }, "include": ["./**/*.ts"] } diff --git a/packages/dredd/test/unit/sortTransactions-test.ts b/packages/dredd/test/unit/sortTransactions-test.ts new file mode 100644 index 000000000..30db8cbd4 --- /dev/null +++ b/packages/dredd/test/unit/sortTransactions-test.ts @@ -0,0 +1,104 @@ +import R from 'ramda'; +import { expect } from 'chai'; +import sortTransactions from '../../lib/sortTransactions'; +import { Transaction, HTTPMethod } from '../../lib/general'; + +const createTransaction = (transaction: Partial) => { + return R.mergeDeepRight>({ + protocol: 'http:', + host: 'localhost', + })(transaction); +}; + +const transactions = Object.keys(HTTPMethod).reduce< + Record +>( + (acc, method: HTTPMethod) => { + return R.assoc( + method, + createTransaction({ + request: { + url: '/endpoint', + method, + }, + }), + acc, + ); + }, + {} as any, +); + +describe('sortTransactions', () => { + describe('given transactions list in arbitrary order', () => { + const sorted = sortTransactions([ + transactions.GET, + transactions.TRACE, + transactions.OPTIONS, + transactions.HEAD, + transactions.DELETE, + transactions.POST, + transactions.PATCH, + transactions.PUT, + transactions.CONNECT, + ]); + + it('should return transactions list sorted', () => { + expect(sorted).to.deep.equal([ + transactions.CONNECT, + transactions.OPTIONS, + transactions.POST, + transactions.GET, + transactions.HEAD, + transactions.PUT, + transactions.PATCH, + transactions.DELETE, + transactions.TRACE, + ]); + }); + }); + + describe('given multiple transactions with the same method', () => { + const getOne = createTransaction({ + id: 'one', + request: { + method: HTTPMethod.GET, + url: '/endpoint', + }, + }); + + const getTwo = createTransaction({ + id: 'two', + request: { + method: HTTPMethod.GET, + url: '/endpoint', + }, + }); + + // This doesn't assert the identity of transactions. + const sorted = sortTransactions([getOne as any, getTwo]); + + it('should sort transactions by occurence (asc)', () => { + expect(sorted).to.deep.equal([getOne, getTwo]); + }); + }); + + describe('given transactions list sorted properly', () => { + const transactionsList = [ + transactions.CONNECT, + transactions.OPTIONS, + transactions.POST, + transactions.POST, + transactions.GET, + transactions.HEAD, + transactions.PUT, + transactions.PATCH, + transactions.DELETE, + transactions.TRACE, + ]; + const sorted = sortTransactions(transactionsList); + + it('should return transactions list as-is', () => { + expect(sorted).to.deep.equal(transactionsList); + }); + }); +});