diff --git a/CHANGELOG.md b/CHANGELOG.md index 2127fe2cc..9c3bb805c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ Please see [CONTRIBUTING.md](https://github.com/cucumber/cucumber/blob/master/CO #### New Features +* Add `--publish` option to publish reports to [reports.cucumber.io](https://reports.cucumber.io) [#1424](https://github.com/cucumber/cucumber-js/pull/1424) * Add support for Gherkin's [Rule/Example syntax](https://cucumber.io/docs/gherkin/reference/#rule) * Add `transpose` method to [data table interface](docs/support_files/data_table_interface.md) * Add `log` function to world, providing a shorthand to log plain text as [attachment(s)](docs/support_files/attachments.md) diff --git a/cucumber.js b/cucumber.js index 8ebc0635e..8e7da2c9a 100644 --- a/cucumber.js +++ b/cucumber.js @@ -7,6 +7,7 @@ const feature = [ '--format rerun:@rerun.txt', '--format usage:usage.txt', '--format message:messages.ndjson', + '--publish-quiet', ].join(' ') const cck = [ @@ -24,6 +25,7 @@ const FORMATTERS_INCLUDE = [ 'parameter-types', 'rules', 'stack-traces', + '--publish-quiet', ] const formatters = [ @@ -36,6 +38,7 @@ const formatters = [ `compatibility/features/{${FORMATTERS_INCLUDE.join(',')}}/*.ts`, '--format', 'message', + '--publish-quiet', ].join(' ') module.exports = { diff --git a/features/publish.feature b/features/publish.feature new file mode 100644 index 000000000..feea1245e --- /dev/null +++ b/features/publish.feature @@ -0,0 +1,131 @@ +Feature: Publish reports + + Background: + Given a file named "features/a.feature" with: + """ + Feature: a feature + Scenario: a scenario + Given a step + """ + And a file named "features/step_definitions/steps.js" with: + """ + const {Given} = require('@cucumber/cucumber') + + Given(/^a step$/, function() {}) + """ + + @spawn + Scenario: Report is published when --publish is specified + Given a report server is running on 'http://localhost:9987' + When I run cucumber-js with arguments `--publish` and env `CUCUMBER_PUBLISH_URL=http://localhost:9987/api/reports` + Then it passes + And the server should receive the following message types: + | meta | + | source | + | gherkinDocument | + | pickle | + | stepDefinition | + | testRunStarted | + | testCase | + | testCaseStarted | + | testStepStarted | + | testStepFinished | + | testCaseFinished | + | testRunFinished | + + @spawn + Scenario: Report is published when CUCUMBER_PUBLISH_ENABLED is set + Given a report server is running on 'http://localhost:9987' + When I run cucumber-js with arguments `` and env `CUCUMBER_PUBLISH_ENABLED=1 CUCUMBER_PUBLISH_URL=http://localhost:9987/api/reports` + Then it passes + And the server should receive the following message types: + | meta | + | source | + | gherkinDocument | + | pickle | + | stepDefinition | + | testRunStarted | + | testCase | + | testCaseStarted | + | testStepStarted | + | testStepFinished | + | testCaseFinished | + | testRunFinished | + + @spawn + Scenario: Report is published when CUCUMBER_PUBLISH_TOKEN is set + Given a report server is running on 'http://localhost:9987' + When I run cucumber-js with arguments `` and env `CUCUMBER_PUBLISH_TOKEN=keyboardcat CUCUMBER_PUBLISH_URL=http://localhost:9987/api/reports` + Then it passes + And the server should receive the following message types: + | meta | + | source | + | gherkinDocument | + | pickle | + | stepDefinition | + | testRunStarted | + | testCase | + | testCaseStarted | + | testStepStarted | + | testStepFinished | + | testCaseFinished | + | testRunFinished | + And the server should receive an "Authorization" header with value "Bearer keyboardcat" + + @spawn + Scenario: a banner is displayed after publication + Given a report server is running on 'http://localhost:9987' + When I run cucumber-js with arguments `--publish` and env `CUCUMBER_PUBLISH_URL=http://localhost:9987/api/reports` + Then the error output contains the text: + """ + ┌──────────────────────────────────────────────────────────────────────────┐ + │ View your Cucumber Report at: │ + │ https://reports.cucumber.io/reports/f318d9ec-5a3d-4727-adec-bd7b69e2edd3 │ + │ │ + │ This report will self-destruct in 24h unless it is claimed or deleted. │ + └──────────────────────────────────────────────────────────────────────────┘ + """ + + @spawn + Scenario: when results are not published, a banner explains how to publish + When I run cucumber-js + Then the error output contains the text: + """ + ┌──────────────────────────────────────────────────────────────────────────┐ + │ Share your Cucumber Report with your team at https://reports.cucumber.io │ + │ │ + │ Command line option: --publish │ + │ Environment variable: CUCUMBER_PUBLISH_ENABLED=true │ + │ │ + │ More information at https://reports.cucumber.io/docs/cucumber-js │ + │ │ + │ To disable this message, add this to your ./cucumber.js: │ + │ module.exports = { default: '--publish-quiet' } │ + └──────────────────────────────────────────────────────────────────────────┘ + """ + @spawn + Scenario: the publication banner is not shown when publication is done + When I run cucumber-js with arguments `` and env `` + Then the error output does not contain the text: + """ + Share your Cucumber Report with your team at https://reports.cucumber.io + """ + + Examples: + | args | env | + | --publish | | + | | CUCUMBER_PUBLISH_ENABLED=true | + | | CUCUMBER_PUBLISH_TOKEN=123456 | + + @spawn + Scenario: the publication banner is not shown when publication is disabled + When I run cucumber-js with arguments `` and env `` + Then the error output does not contain the text: + """ + Share your Cucumber Report with your team at https://reports.cucumber.io + """ + + Examples: + | args | env | + | --publish-quiet | | + | | CUCUMBER_PUBLISH_QUIET=true | diff --git a/features/step_definitions/cli_steps.ts b/features/step_definitions/cli_steps.ts index 00838e62f..9722d3944 100644 --- a/features/step_definitions/cli_steps.ts +++ b/features/step_definitions/cli_steps.ts @@ -22,6 +22,27 @@ When( } ) +When( + /^I run cucumber-js with arguments `(|.+)` and env `(|.+)`$/, + { timeout: 10000 }, + async function (this: World, args: string, envs: string) { + const renderedArgs = Mustache.render(valueOrDefault(args, ''), this) + const stringArgs = stringArgv(renderedArgs) + const initialValue: NodeJS.ProcessEnv = {} + const env: NodeJS.ProcessEnv = (envs === null ? '' : envs) + .split(/\s+/) + .map((keyValue) => keyValue.split('=')) + .reduce((dict, pair) => { + dict[pair[0]] = pair[1] + return dict + }, initialValue) + return await this.run(this.localExecutablePath, stringArgs, { + ...process.env, + ...env, + }) + } +) + When( /^I run cucumber-js with all formatters(?: and `(|.+)`)?$/, { timeout: 10000 }, @@ -100,6 +121,15 @@ Then(/^the error output contains the text:$/, function ( expect(actualOutput).to.include(expectedOutput) }) +Then('the error output does not contain the text:', function ( + this: World, + text: string +) { + const actualOutput = normalizeText(this.lastRun.errorOutput) + const expectedOutput = normalizeText(text) + expect(actualOutput).not.to.include(expectedOutput) +}) + Then(/^I see the version of Cucumber$/, function (this: World) { const actualOutput = this.lastRun.output const expectedOutput = `${version as string}\n` diff --git a/features/step_definitions/report_server_steps.ts b/features/step_definitions/report_server_steps.ts new file mode 100644 index 000000000..6d1b7cca4 --- /dev/null +++ b/features/step_definitions/report_server_steps.ts @@ -0,0 +1,42 @@ +import { Given, Then, DataTable } from '../..' +import { World } from '../support/world' +import { expect } from 'chai' +import { URL } from 'url' +import FakeReportServer from '../../test/fake_report_server' +import assert from 'assert' + +Given('a report server is running on {string}', async function ( + this: World, + url: string +) { + const port = parseInt(new URL(url).port) + this.reportServer = new FakeReportServer(port) + await this.reportServer.start() +}) + +Then('the server should receive the following message types:', async function ( + this: World, + expectedMessageTypesTable: DataTable +) { + const expectedMessageTypes = expectedMessageTypesTable + .raw() + .map((row) => row[0]) + + const receivedBodies = await this.reportServer.stop() + const ndjson = receivedBodies.toString('utf-8').trim() + if (ndjson === '') assert.fail('Server received nothing') + + const receivedMessageTypes = ndjson + .split(/\n/) + .map((line) => JSON.parse(line)) + .map((envelope) => Object.keys(envelope)[0]) + + expect(receivedMessageTypes).to.deep.eq(expectedMessageTypes) +}) + +Then( + 'the server should receive a(n) {string} header with value {string}', + function (this: World, name: string, value: string) { + expect(this.reportServer.receivedHeaders[name.toLowerCase()]).to.eq(value) + } +) diff --git a/features/support/hooks.ts b/features/support/hooks.ts index 5c49daf43..f6860a235 100644 --- a/features/support/hooks.ts +++ b/features/support/hooks.ts @@ -106,3 +106,9 @@ After(function (this: World) { ) } }) + +After(async function (this: World) { + if (this.reportServer?.started) { + await this.reportServer.stop() + } +}) diff --git a/features/support/world.ts b/features/support/world.ts index 02401afde..67300d890 100644 --- a/features/support/world.ts +++ b/features/support/world.ts @@ -10,6 +10,7 @@ import VError from 'verror' import _ from 'lodash' import ndjsonParse from 'ndjson-parse' import { messages } from '@cucumber/messages' +import FakeReportServer from '../../test/fake_report_server' interface ILastRun { error: any @@ -32,8 +33,13 @@ export class World { public verifiedLastRunError: boolean public localExecutablePath: string public globalExecutablePath: string + public reportServer: FakeReportServer - async run(executablePath: string, inputArgs: string[]): Promise { + async run( + executablePath: string, + inputArgs: string[], + env: NodeJS.ProcessEnv = process.env + ): Promise { const messageFilename = 'message.ndjson' const args = ['node', executablePath] .concat(inputArgs, [ @@ -54,9 +60,14 @@ export class World { if (this.spawn) { result = await new Promise((resolve) => { - execFile(args[0], args.slice(1), { cwd }, (error, stdout, stderr) => { - resolve({ error, stdout, stderr }) - }) + execFile( + args[0], + args.slice(1), + { cwd, env }, + (error, stdout, stderr) => { + resolve({ error, stdout, stderr }) + } + ) }) } else { const stdout = new PassThrough() diff --git a/package.json b/package.json index 38f3f2204..29da6d1c6 100644 --- a/package.json +++ b/package.json @@ -184,6 +184,7 @@ "stack-chain": "^2.0.0", "stacktrace-js": "^2.0.2", "string-argv": "^0.3.1", + "tmp": "^0.2.1", "util-arity": "^1.1.0", "verror": "^1.10.0" }, @@ -193,6 +194,7 @@ "@types/bluebird": "3.5.32", "@types/chai": "4.2.12", "@types/dirty-chai": "2.0.2", + "@types/express": "^4.17.7", "@types/fs-extra": "9.0.1", "@types/glob": "7.1.3", "@types/lodash": "4.14.161", @@ -228,6 +230,7 @@ "eslint-plugin-prettier": "3.1.4", "eslint-plugin-promise": "4.2.1", "eslint-plugin-standard": "4.0.1", + "express": "^4.17.1", "fs-extra": "9.0.1", "mocha": "8.1.3", "mustache": "4.0.1", @@ -241,7 +244,6 @@ "sinon-chai": "3.5.0", "stream-buffers": "3.0.2", "stream-to-string": "1.2.0", - "tmp": "0.2.1", "ts-node": "9.0.0", "tsify": "5.0.2", "typescript": "4.0.2" diff --git a/src/cli/argv_parser.ts b/src/cli/argv_parser.ts index f7ec87785..ac825dedb 100644 --- a/src/cli/argv_parser.ts +++ b/src/cli/argv_parser.ts @@ -34,6 +34,8 @@ export interface IParsedArgvOptions { parallel: number predictableIds: boolean profile: string[] + publish: boolean + publishQuiet: boolean require: string[] requireModule: string[] retry: number @@ -168,6 +170,16 @@ const ArgvParser = { 'Use predictable ids in messages (option ignored if using parallel)', false ) + .option( + '--publish', + 'Publish a report to https://reports.cucumber.io', + false + ) + .option( + '--publish-quiet', + "Don't print information banner about publishing reports", + false + ) .option( '-r, --require ', 'require files before executing features (repeatable)', diff --git a/src/cli/configuration_builder.ts b/src/cli/configuration_builder.ts index adfe2dc48..e830b11c5 100644 --- a/src/cli/configuration_builder.ts +++ b/src/cli/configuration_builder.ts @@ -11,6 +11,7 @@ import glob from 'glob' import { promisify } from 'util' import { IPickleFilterOptions } from '../pickle_filter' import { IRuntimeOptions } from '../runtime' +import { valueOrDefault } from '../value_checker' export interface IConfigurationFormat { outputTo: string @@ -22,6 +23,7 @@ export interface IConfiguration { featurePaths: string[] formats: IConfigurationFormat[] formatOptions: IParsedArgvFormatOptions + publishing: boolean listI18nKeywordsFor: string listI18nLanguages: boolean order: string @@ -33,6 +35,7 @@ export interface IConfiguration { shouldExitImmediately: boolean supportCodePaths: string[] supportCodeRequiredModules: string[] + suppressPublishAdvertisement: boolean } export interface INewConfigurationBuilderOptions { @@ -40,6 +43,8 @@ export interface INewConfigurationBuilderOptions { cwd: string } +const DEFAULT_CUCUMBER_PUBLISH_URL = 'https://messages.cucumber.io/api/reports' + export default class ConfigurationBuilder { static async build( options: INewConfigurationBuilderOptions @@ -83,6 +88,7 @@ export default class ConfigurationBuilder { featurePaths, formats: this.getFormats(), formatOptions: this.options.formatOptions, + publishing: this.isPublishing(), listI18nKeywordsFor, listI18nLanguages, order: this.options.order, @@ -108,6 +114,7 @@ export default class ConfigurationBuilder { shouldExitImmediately: this.options.exit, supportCodePaths, supportCodeRequiredModules: this.options.requireModule, + suppressPublishAdvertisement: this.isPublishAdvertisementSuppressed(), } } @@ -157,15 +164,45 @@ export default class ConfigurationBuilder { return _.uniq(featureDirs) } + isPublishing(): boolean { + return ( + this.options.publish || + this.isTruthyString(process.env.CUCUMBER_PUBLISH_ENABLED) || + process.env.CUCUMBER_PUBLISH_TOKEN !== undefined + ) + } + + isPublishAdvertisementSuppressed(): boolean { + return ( + this.options.publishQuiet || + this.isTruthyString(process.env.CUCUMBER_PUBLISH_QUIET) + ) + } + getFormats(): IConfigurationFormat[] { const mapping: { [key: string]: string } = { '': 'progress' } this.options.format.forEach((format) => { const [type, outputTo] = OptionSplitter.split(format) mapping[outputTo] = type }) + if (this.isPublishing()) { + const publishUrl = valueOrDefault( + process.env.CUCUMBER_PUBLISH_URL, + DEFAULT_CUCUMBER_PUBLISH_URL + ) + + mapping[publishUrl] = 'message' + } return _.map(mapping, (type, outputTo) => ({ outputTo, type })) } + isTruthyString(s: string | undefined): boolean { + if (s === undefined) { + return false + } + return s.match(/^(false|no|0)$/i) === null + } + async getUnexpandedFeaturePaths(): Promise { if (this.args.length > 0) { const nestedFeaturePaths = await bluebird.map(this.args, async (arg) => { diff --git a/src/cli/configuration_builder_spec.ts b/src/cli/configuration_builder_spec.ts index 133f4c448..46138c50e 100644 --- a/src/cli/configuration_builder_spec.ts +++ b/src/cli/configuration_builder_spec.ts @@ -33,6 +33,7 @@ describe('Configuration', () => { featurePaths: [], formatOptions: {}, formats: [{ outputTo: '', type: 'progress' }], + publishing: false, listI18nKeywordsFor: '', listI18nLanguages: false, order: 'defined', @@ -58,6 +59,7 @@ describe('Configuration', () => { shouldExitImmediately: false, supportCodePaths: [], supportCodeRequiredModules: [], + suppressPublishAdvertisement: false, }) }) }) @@ -125,6 +127,40 @@ describe('Configuration', () => { expect(formats).to.eql([{ outputTo: '', type: 'progress' }]) }) + it('adds a message formatter with reports URL when --publish specified', async function () { + // Arrange + const cwd = await buildTestWorkingDirectory() + const argv = baseArgv.concat(['--publish']) + + // Act + const { formats } = await ConfigurationBuilder.build({ argv, cwd }) + + // Assert + expect(formats).to.eql([ + { outputTo: '', type: 'progress' }, + { + outputTo: 'https://messages.cucumber.io/api/reports', + type: 'message', + }, + ]) + }) + + it('sets publishing to true when --publish is specified', async function () { + const cwd = await buildTestWorkingDirectory() + const argv = baseArgv.concat(['--publish']) + const configuration = await ConfigurationBuilder.build({ argv, cwd }) + + expect(configuration.publishing).to.eq(true) + }) + + it('sets suppressPublishAdvertisement to true when --publish-quiet is specified', async function () { + const cwd = await buildTestWorkingDirectory() + const argv = baseArgv.concat(['--publish-quiet']) + const configuration = await ConfigurationBuilder.build({ argv, cwd }) + + expect(configuration.suppressPublishAdvertisement).to.eq(true) + }) + it('splits relative unix paths', async function () { // Arrange const cwd = await buildTestWorkingDirectory() diff --git a/src/cli/index.ts b/src/cli/index.ts index 97e71458c..9e1ad1a94 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -27,7 +27,8 @@ import { doesNotHaveValue } from '../value_checker' import { GherkinStreams } from '@cucumber/gherkin' import { ISupportCodeLibrary } from '../support_code_library_builder/types' import { IParsedArgvFormatOptions } from './argv_parser' -import { createReadStream, WriteStream } from 'fs' +import { createReadStream } from 'fs' +import HttpStream from '../formatter/http_stream' const { incrementing, uuid } = IdGenerator @@ -87,12 +88,23 @@ export default class Cli { formats, supportCodeLibrary, }: IInitializeFormattersRequest): Promise<() => Promise> { - const streamsToClose: WriteStream[] = [] + const streamsToClose: IFormatterStream[] = [] await bluebird.map(formats, async ({ type, outputTo }) => { let stream: IFormatterStream = this.stdout if (outputTo !== '') { - const fd = await fs.open(path.resolve(this.cwd, outputTo), 'w') - stream = fs.createWriteStream(null, { fd }) + if (outputTo.match(new RegExp('^https?://')) !== null) { + const headers: { [key: string]: string } = {} + if (process.env.CUCUMBER_PUBLISH_TOKEN !== undefined) { + headers.Authorization = `Bearer ${process.env.CUCUMBER_PUBLISH_TOKEN}` + } + + stream = new HttpStream(outputTo, 'GET', headers, (content) => + console.error(content) + ) + } else { + const fd = await fs.open(path.resolve(this.cwd, outputTo), 'w') + stream = fs.createWriteStream(null, { fd }) + } streamsToClose.push(stream) } const typeOptions = { diff --git a/src/cli/publish_banner.ts b/src/cli/publish_banner.ts new file mode 100644 index 000000000..53a8eefca --- /dev/null +++ b/src/cli/publish_banner.ts @@ -0,0 +1,34 @@ +import colors from 'colors/safe' +import Table from 'cli-table3' + +const underlineBoldCyan = (x: string): string => + colors.underline(colors.bold(colors.cyan(x))) + +const formattedReportUrl = underlineBoldCyan('https://reports.cucumber.io') +const formattedEnv = + colors.cyan('CUCUMBER_PUBLISH_ENABLED') + '=' + colors.cyan('true') +const formattedMoreInfoUrl = underlineBoldCyan( + 'https://reports.cucumber.io/docs/cucumber-js' +) + +const text = `\ +Share your Cucumber Report with your team at ${formattedReportUrl} + +Command line option: ${colors.cyan('--publish')} +Environment variable: ${formattedEnv} + +More information at ${formattedMoreInfoUrl} + +To disable this message, add this to your ${colors.bold('./cucumber.js')}: +${colors.bold("module.exports = { default: '--publish-quiet' }")}` + +const table = new Table({ + style: { + head: [], + border: ['green'], + }, +}) + +table.push([text]) + +export default table.toString() diff --git a/src/cli/run.ts b/src/cli/run.ts index 7f238d66c..c2253d0f2 100644 --- a/src/cli/run.ts +++ b/src/cli/run.ts @@ -1,11 +1,16 @@ import Cli, { ICliRunResult } from './' import VError from 'verror' +import publishBanner from './publish_banner' function exitWithError(error: Error): void { console.error(VError.fullStack(error)) // eslint-disable-line no-console process.exit(1) } +function displayPublishAdvertisementBanner(): void { + console.error(publishBanner) +} + export default async function run(): Promise { const cwd = process.cwd() const cli = new Cli({ @@ -21,6 +26,11 @@ export default async function run(): Promise { exitWithError(error) } + const config = await cli.getConfiguration() + if (!config.publishing && !config.suppressPublishAdvertisement) { + displayPublishAdvertisementBanner() + } + const exitCode = result.success ? 0 : 1 if (result.shouldExitImmediately) { process.exit(exitCode) diff --git a/src/formatter/http_stream.ts b/src/formatter/http_stream.ts new file mode 100644 index 000000000..aef9ced4e --- /dev/null +++ b/src/formatter/http_stream.ts @@ -0,0 +1,139 @@ +import { pipeline, Writable } from 'stream' +import tmp from 'tmp' +import fs from 'fs' +import http from 'http' +import https from 'https' +import { doesHaveValue } from '../value_checker' + +// https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods +type HttpMethod = + | 'GET' + | 'HEAD' + | 'POST' + | 'PUT' + | 'DELETE' + | 'CONNECT' + | 'OPTIONS' + | 'TRACE' + | 'PATCH' + +/** + * This Writable writes data to a HTTP/HTTPS URL. + * + * It has special handling for https://reports.cucumber.io/ + * which uses an API where the first request is a `GET`, + * and if the response is 202 with a Location header, issues + * a PUT request to that URL. + * + * 3xx redirects are not currently followed. + */ +export default class HttpStream extends Writable { + private tempFilePath: string + private tempFile: Writable + private responseBodyFromGet: string | null = null + + constructor( + private readonly url: string, + private readonly method: HttpMethod, + private readonly headers: { [name: string]: string }, + private readonly reportLocation: (content: string) => void + ) { + super() + } + + _write( + chunk: any, + encoding: BufferEncoding, + callback: (err?: Error | null) => void + ): void { + if (this.tempFile === undefined) { + tmp.file((err, name, fd) => { + if (doesHaveValue(err)) return callback(err) + + this.tempFilePath = name + this.tempFile = fs.createWriteStream(name, { fd }) + this.tempFile.write(chunk, encoding, callback) + }) + } else { + this.tempFile.write(chunk, encoding, callback) + } + } + + _final(callback: (error?: Error | null) => void): void { + this.tempFile.end(() => { + this.sendRequest( + this.url, + this.method, + (err: Error | null | undefined) => { + if (doesHaveValue(err)) return callback(err) + this.reportLocation(this.responseBodyFromGet) + callback(null) + } + ) + }) + } + + private sendRequest( + url: string, + method: HttpMethod, + callback: (err: Error | null | undefined, url?: string) => void + ): void { + const httpx = doesHaveValue(url.match(/^https:/)) ? https : http + + if (method === 'GET') { + httpx.get(url, { headers: this.headers }, (res) => { + if (res.statusCode >= 400) { + return callback( + new Error(`${method} ${url} returned status ${res.statusCode}`) + ) + } + + if (res.statusCode !== 202 || res.headers.location === undefined) { + callback(null, url) + } else { + let body = Buffer.alloc(0) + res.on('data', (chunk) => { + body = Buffer.concat([body, chunk]) + }) + res.on('end', () => { + this.responseBodyFromGet = body.toString('utf-8') + this.sendRequest(res.headers.location, 'PUT', callback) + }) + } + }) + } else { + const contentLength = fs.statSync(this.tempFilePath).size + const req = httpx.request(url, { + method, + headers: { + 'Content-Length': contentLength, + }, + }) + + req.on('response', (res) => { + if (res.statusCode >= 400) { + let body = Buffer.alloc(0) + res.on('data', (chunk) => { + body = Buffer.concat([body, chunk]) + }) + res.on('end', () => { + callback( + new Error( + `${method} ${url} returned status ${ + res.statusCode + }:\n${body.toString('utf-8')}` + ) + ) + }) + res.on('error', callback) + } else { + callback(null, url) + } + }) + + pipeline(fs.createReadStream(this.tempFilePath), req, (err) => { + if (doesHaveValue(err)) callback(err) + }) + } + } +} diff --git a/src/formatter/http_stream_spec.ts b/src/formatter/http_stream_spec.ts new file mode 100644 index 000000000..2a93fd5fd --- /dev/null +++ b/src/formatter/http_stream_spec.ts @@ -0,0 +1,120 @@ +import assert from 'assert' +import HttpStream from './http_stream' +import FakeReportServer from '../../test/fake_report_server' + +type Callback = (err?: Error | null) => void + +describe('HttpStream', () => { + const port = 8998 + let reportServer: FakeReportServer + + beforeEach(async () => { + reportServer = new FakeReportServer(port) + await reportServer.start() + }) + + it(`sends a PUT request with written data when the stream is closed`, (callback: Callback) => { + const stream = new HttpStream( + `http://localhost:${port}/s3`, + 'PUT', + {}, + () => undefined + ) + + stream.on('error', callback) + stream.on('finish', () => { + reportServer + .stop() + .then((receivedBodies) => { + try { + assert.strictEqual(receivedBodies.toString('utf-8'), 'hello work') + callback() + } catch (err) { + callback(err) + } + }) + .catch(callback) + }) + + stream.write('hello') + stream.write(' work') + stream.end() + }) + + it(`follows location from GET response, and sends body and headers in a PUT request`, (callback: Callback) => { + const stream = new HttpStream( + `http://localhost:${port}/api/reports`, + 'GET', + { Authorization: 'Bearer blablabla' }, + () => undefined + ) + + stream.on('error', callback) + stream.on('finish', () => { + reportServer + .stop() + .then((receivedBodies) => { + try { + const expectedBody = 'hello work' + assert.strictEqual(receivedBodies.toString('utf-8'), expectedBody) + assert.strictEqual( + reportServer.receivedHeaders['content-length'], + expectedBody.length.toString() + ) + assert.strictEqual( + reportServer.receivedHeaders.authorization, + 'Bearer blablabla' + ) + callback() + } catch (err) { + callback(err) + } + }) + .catch(callback) + }) + + stream.write('hello') + stream.write(' work') + stream.end() + }) + + it('outputs the body provided by the server', (callback: Callback) => { + let reported: string + + const stream = new HttpStream( + `http://localhost:${port}/api/reports`, + 'GET', + {}, + (content) => { + reported = content + } + ) + + stream.on('error', callback) + stream.on('finish', () => { + reportServer + .stop() + .then((receivedBodies) => { + try { + assert.strictEqual( + reported, + `┌──────────────────────────────────────────────────────────────────────────┐ +│ View your Cucumber Report at: │ +│ https://reports.cucumber.io/reports/f318d9ec-5a3d-4727-adec-bd7b69e2edd3 │ +│ │ +│ This report will self-destruct in 24h unless it is claimed or deleted. │ +└──────────────────────────────────────────────────────────────────────────┘ +` + ) + callback() + } catch (err) { + callback(err) + } + }) + .catch(callback) + }) + + stream.write('hello') + stream.end() + }) +}) diff --git a/src/formatter/index.ts b/src/formatter/index.ts index 696e96d29..95ca4e0fa 100644 --- a/src/formatter/index.ts +++ b/src/formatter/index.ts @@ -7,8 +7,13 @@ import { WriteStream as FsWriteStream } from 'fs' import { WriteStream as TtyWriteStream } from 'tty' import { EventEmitter } from 'events' import { IParsedArgvFormatOptions } from '../cli/argv_parser' +import HttpStream from './http_stream' -export type IFormatterStream = FsWriteStream | TtyWriteStream | PassThrough +export type IFormatterStream = + | FsWriteStream + | TtyWriteStream + | PassThrough + | HttpStream export type IFormatterLogFn = (buffer: string | Uint8Array) => void export interface IFormatterOptions { diff --git a/src/support_code_library_builder/types.ts b/src/support_code_library_builder/types.ts index 4d411544a..81440a317 100644 --- a/src/support_code_library_builder/types.ts +++ b/src/support_code_library_builder/types.ts @@ -13,10 +13,10 @@ export interface ITestCaseHookParameter { testCaseStartedId: string } -export type TestCaseHookFunctionWithoutParameter = () => void +export type TestCaseHookFunctionWithoutParameter = () => void | Promise export type TestCaseHookFunctionWithParameter = ( arg: ITestCaseHookParameter -) => void +) => void | Promise export type TestCaseHookFunction = | TestCaseHookFunctionWithoutParameter | TestCaseHookFunctionWithParameter diff --git a/test/fake_report_server.ts b/test/fake_report_server.ts new file mode 100644 index 000000000..6bd063542 --- /dev/null +++ b/test/fake_report_server.ts @@ -0,0 +1,95 @@ +import { Server, Socket } from 'net' +import express from 'express' +import { pipeline, Writable } from 'stream' +import http from 'http' +import { promisify } from 'util' +import { doesHaveValue } from '../src/value_checker' + +type Callback = (err?: Error | null) => void + +/** + * Fake implementation of the same report server that backs Cucumber Reports + * (https://messages.cucumber.io). Used for testing only. + */ +export default class FakeReportServer { + private readonly sockets = new Set() + private readonly server: Server + private receivedBodies = Buffer.alloc(0) + public receivedHeaders: http.IncomingHttpHeaders = {} + + constructor(private readonly port: number) { + const app = express() + + app.put('/s3', (req, res) => { + this.receivedHeaders = { ...this.receivedHeaders, ...req.headers } + + const captureBodyStream = new Writable({ + write: (chunk: Buffer, encoding: string, callback: Callback) => { + this.receivedBodies = Buffer.concat([this.receivedBodies, chunk]) + callback() + }, + }) + + pipeline(req, captureBodyStream, (err) => { + if (doesHaveValue(err)) return res.status(500).end(err.stack) + res.end() + }) + }) + + app.get('/api/reports', (req, res) => { + this.receivedHeaders = { ...this.receivedHeaders, ...req.headers } + + res.setHeader('Location', `http://localhost:${port}/s3`) + res.status(202) + .end(`┌──────────────────────────────────────────────────────────────────────────┐ +│ View your Cucumber Report at: │ +│ https://reports.cucumber.io/reports/f318d9ec-5a3d-4727-adec-bd7b69e2edd3 │ +│ │ +│ This report will self-destruct in 24h unless it is claimed or deleted. │ +└──────────────────────────────────────────────────────────────────────────┘ +`) + }) + + this.server = http.createServer(app) + + this.server.on('connection', (socket) => { + this.sockets.add(socket) + socket.on('close', () => { + this.sockets.delete(socket) + }) + }) + } + + async start(): Promise { + const listen = promisify(this.server.listen.bind(this.server)) + await listen(this.port) + } + + /** + * @return all the received request bodies + */ + async stop(): Promise { + // Wait for all sockets to be closed + await Promise.all( + Array.from(this.sockets).map( + // eslint-disable-next-line @typescript-eslint/promise-function-async + (socket) => + new Promise((resolve, reject) => { + if (socket.destroyed) return resolve() + socket.on('close', resolve) + socket.on('error', reject) + }) + ) + ) + return new Promise((resolve, reject) => { + this.server.close((err) => { + if (doesHaveValue(err)) return reject(err) + resolve(this.receivedBodies) + }) + }) + } + + get started(): boolean { + return this.server.listening + } +} diff --git a/yarn.lock b/yarn.lock index f975ab5d8..5879539f5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -369,6 +369,14 @@ resolved "https://registry.yarnpkg.com/@types/bluebird/-/bluebird-3.5.32.tgz#381e7b59e39f010d20bbf7e044e48f5caf1ab620" integrity sha512-dIOxFfI0C+jz89g6lQ+TqhGgPQ0MxSnh/E4xuC0blhFtyW269+mPG5QeLgbdwst/LvdP8o1y0o/Gz5EHXLec/g== +"@types/body-parser@*": + version "1.19.0" + resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.0.tgz#0685b3c47eb3006ffed117cdd55164b61f80538f" + integrity sha512-W98JrE0j2K78swW4ukqMleo8R7h/pFETjM2DQ90MF6XK2i4LO4W3gQ71Lt4w3bfm2EvVSyWHplECvB5sK22yFQ== + dependencies: + "@types/connect" "*" + "@types/node" "*" + "@types/chai-as-promised@*": version "7.1.3" resolved "https://registry.yarnpkg.com/@types/chai-as-promised/-/chai-as-promised-7.1.3.tgz#779166b90fda611963a3adbfd00b339d03b747bd" @@ -386,6 +394,13 @@ resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0" integrity sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ== +"@types/connect@*": + version "3.4.33" + resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.33.tgz#31610c901eca573b8713c3330abc6e6b9f588546" + integrity sha512-2+FrkXY4zllzTNfJth7jOqEHC+enpLeGslEhpnTAkg21GkRrWV4SsAtqchtT4YS9/nODBU2/ZfsBY2X4J/dX7A== + dependencies: + "@types/node" "*" + "@types/dirty-chai@2.0.2": version "2.0.2" resolved "https://registry.yarnpkg.com/@types/dirty-chai/-/dirty-chai-2.0.2.tgz#eeac4802329a41ed7815ac0c1a6360335bf77d0c" @@ -399,6 +414,25 @@ resolved "https://registry.yarnpkg.com/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#1ee30d79544ca84d68d4b3cdb0af4f205663dd2d" integrity sha512-OCutwjDZ4aFS6PB1UZ988C4YgwlBHJd6wCeQqaLdmadZ/7e+w79+hbMUFC1QXDNCmdyoRfAFdm0RypzwR+Qpag== +"@types/express-serve-static-core@*": + version "4.17.12" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.12.tgz#9a487da757425e4f267e7d1c5720226af7f89591" + integrity sha512-EaEdY+Dty1jEU7U6J4CUWwxL+hyEGMkO5jan5gplfegUgCUsIUWqXxqw47uGjimeT4Qgkz/XUfwoau08+fgvKA== + dependencies: + "@types/node" "*" + "@types/qs" "*" + "@types/range-parser" "*" + +"@types/express@^4.17.7": + version "4.17.8" + resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.8.tgz#3df4293293317e61c60137d273a2e96cd8d5f27a" + integrity sha512-wLhcKh3PMlyA2cNAB9sjM1BntnhPMiM0JOBwPBqttjHev2428MLEB4AYVN+d8s2iyCVZac+o41Pflm/ZH5vLXQ== + dependencies: + "@types/body-parser" "*" + "@types/express-serve-static-core" "*" + "@types/qs" "*" + "@types/serve-static" "*" + "@types/fs-extra@9.0.1": version "9.0.1" resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-9.0.1.tgz#91c8fc4c51f6d5dbe44c2ca9ab09310bd00c7918" @@ -434,6 +468,11 @@ resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.1.tgz#459c65fa1867dafe6a8f322c4c51695663cc55e9" integrity sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w== +"@types/mime@*": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@types/mime/-/mime-2.0.3.tgz#c893b73721db73699943bfc3653b1deb7faa4a3a" + integrity sha512-Jus9s4CDbqwocc5pOAnh8ShfrnMcPHuJYzVcSUU7lrh8Ni5HuIqX3oilL86p3dlTrk0LzHRCgA/GQ7uNCw6l2Q== + "@types/minimatch@*": version "3.0.3" resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d" @@ -478,6 +517,16 @@ dependencies: "@types/node" "*" +"@types/qs@*": + version "6.9.4" + resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.4.tgz#a59e851c1ba16c0513ea123830dd639a0a15cb6a" + integrity sha512-+wYo+L6ZF6BMoEjtf8zB2esQsqdV6WsjRK/GP9WOgLPrq87PbNWgIxS76dS5uvl/QXtHGakZmwTznIfcPXcKlQ== + +"@types/range-parser@*": + version "1.2.3" + resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.3.tgz#7ee330ba7caafb98090bece86a5ee44115904c2c" + integrity sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA== + "@types/resolve@1.17.1": version "1.17.1" resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-1.17.1.tgz#3afd6ad8967c77e4376c598a82ddd58f46ec45d6" @@ -490,6 +539,14 @@ resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.3.3.tgz#3ad6ed949e7487e7bda6f886b4a2434a2c3d7b1a" integrity sha512-jQxClWFzv9IXdLdhSaTf16XI3NYe6zrEbckSpb5xhKfPbWgIyAY0AFyWWWfaiDcBuj3UHmMkCIwSRqpKMTZL2Q== +"@types/serve-static@*": + version "1.13.5" + resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.13.5.tgz#3d25d941a18415d3ab092def846e135a08bbcf53" + integrity sha512-6M64P58N+OXjU432WoLLBQxbA0LRGBCRm7aAGQJ+SMC1IMl0dgRVi9EFfoDcS2a7Xogygk/eGN94CfwU9UF7UQ== + dependencies: + "@types/express-serve-static-core" "*" + "@types/mime" "*" + "@types/sinon-chai@3.2.4": version "3.2.4" resolved "https://registry.yarnpkg.com/@types/sinon-chai/-/sinon-chai-3.2.4.tgz#c425625681f4f8d3a43a7551a77f590ce1c49b21" @@ -600,6 +657,14 @@ JSONStream@^1.0.3: jsonparse "^1.2.0" through ">=2.2.7 <3" +accepts@~1.3.7: + version "1.3.7" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd" + integrity sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA== + dependencies: + mime-types "~2.1.24" + negotiator "0.6.2" + acorn-jsx@^5.2.0: version "5.3.1" resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.1.tgz#fc8661e11b7ac1539c47dbfea2e72b3af34d267b" @@ -719,6 +784,11 @@ argparse@^1.0.7: dependencies: sprintf-js "~1.0.2" +array-flatten@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" + integrity sha1-ml9pkFGx5wczKPKgCJaLZOopVdI= + array-includes@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.1.tgz#cdd67e6852bdf9c1215460786732255ed2459348" @@ -857,6 +927,22 @@ bn.js@^5.1.1: resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.1.3.tgz#beca005408f642ebebea80b042b4d18d2ac0ee6b" integrity sha512-GkTiFpjFtUzU9CbMeJ5iazkCzGL3jrhzerzZIuqLABjbwRaFt33I9tUdSNryIptM+RxDet6OKm2WnLXzW51KsQ== +body-parser@1.19.0: + version "1.19.0" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a" + integrity sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw== + dependencies: + bytes "3.1.0" + content-type "~1.0.4" + debug "2.6.9" + depd "~1.1.2" + http-errors "1.7.2" + iconv-lite "0.4.24" + on-finished "~2.3.0" + qs "6.7.0" + raw-body "2.4.0" + type-is "~1.6.17" + brace-expansion@^1.1.7: version "1.1.11" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" @@ -1044,6 +1130,11 @@ builtin-status-codes@^3.0.0: resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8" integrity sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug= +bytes@3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6" + integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg== + cached-path-relative@^1.0.0, cached-path-relative@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/cached-path-relative/-/cached-path-relative-1.0.2.tgz#a13df4196d26776220cc3356eb147a52dba2c6db" @@ -1294,6 +1385,18 @@ contains-path@^0.1.0: resolved "https://registry.yarnpkg.com/contains-path/-/contains-path-0.1.0.tgz#fe8cf184ff6670b6baef01a9d4861a5cbec4120a" integrity sha1-/ozxhP9mcLa67wGp1IYaXL7EEgo= +content-disposition@0.5.3: + version "0.5.3" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.3.tgz#e130caf7e7279087c5616c2007d0485698984fbd" + integrity sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g== + dependencies: + safe-buffer "5.1.2" + +content-type@~1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" + integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== + convert-source-map@^1.1.0, convert-source-map@^1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.7.0.tgz#17a2cb882d7f77d3490585e2ce6c524424a3a442" @@ -1306,6 +1409,16 @@ convert-source-map@~1.1.0: resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.1.3.tgz#4829c877e9fe49b3161f3bf3673888e204699860" integrity sha1-SCnId+n+SbMWHzvzZziI4gRpmGA= +cookie-signature@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" + integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw= + +cookie@0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba" + integrity sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg== + core-js@3.6.5: version "3.6.5" resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.6.5.tgz#7395dc273af37fb2e50e9bd3d9fe841285231d1a" @@ -1961,6 +2074,42 @@ evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3: md5.js "^1.3.4" safe-buffer "^5.1.1" +express@^4.17.1: + version "4.17.1" + resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134" + integrity sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g== + dependencies: + accepts "~1.3.7" + array-flatten "1.1.1" + body-parser "1.19.0" + content-disposition "0.5.3" + content-type "~1.0.4" + cookie "0.4.0" + cookie-signature "1.0.6" + debug "2.6.9" + depd "~1.1.2" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + finalhandler "~1.1.2" + fresh "0.5.2" + merge-descriptors "1.0.1" + methods "~1.1.2" + on-finished "~2.3.0" + parseurl "~1.3.3" + path-to-regexp "0.1.7" + proxy-addr "~2.0.5" + qs "6.7.0" + range-parser "~1.2.1" + safe-buffer "5.1.2" + send "0.17.1" + serve-static "1.14.1" + setprototypeof "1.1.1" + statuses "~1.5.0" + type-is "~1.6.18" + utils-merge "1.0.1" + vary "~1.1.2" + ext@^1.1.2: version "1.4.0" resolved "https://registry.yarnpkg.com/ext/-/ext-1.4.0.tgz#89ae7a07158f79d35517882904324077e4379244" @@ -2034,7 +2183,7 @@ fill-range@^7.0.1: dependencies: to-regex-range "^5.0.1" -finalhandler@1.1.2: +finalhandler@1.1.2, finalhandler@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d" integrity sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA== @@ -2129,6 +2278,11 @@ form-data@~2.3.2: combined-stream "^1.0.6" mime-types "^2.1.12" +forwarded@~0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84" + integrity sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ= + fresh@0.5.2: version "0.5.2" resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" @@ -2345,6 +2499,17 @@ htmlescape@^1.1.0: resolved "https://registry.yarnpkg.com/htmlescape/-/htmlescape-1.1.1.tgz#3a03edc2214bca3b66424a3e7959349509cb0351" integrity sha1-OgPtwiFLyjtmQko+eVk0lQnLA1E= +http-errors@1.7.2: + version "1.7.2" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.2.tgz#4f5029cf13239f31036e5b2e55292bcfbcc85c8f" + integrity sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg== + dependencies: + depd "~1.1.2" + inherits "2.0.3" + setprototypeof "1.1.1" + statuses ">= 1.5.0 < 2" + toidentifier "1.0.0" + http-errors@~1.7.2: version "1.7.3" resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.3.tgz#6c619e4f9c60308c38519498c14fbb10aacebb06" @@ -2370,6 +2535,13 @@ https-browserify@^1.0.0: resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73" integrity sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM= +iconv-lite@0.4.24: + version "0.4.24" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" + integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== + dependencies: + safer-buffer ">= 2.1.2 < 3" + ieee754@^1.1.4: version "1.1.13" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84" @@ -2449,6 +2621,11 @@ insert-module-globals@^7.0.0: undeclared-identifiers "^1.1.2" xtend "^4.0.0" +ipaddr.js@1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" + integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== + is-arguments@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.0.4.tgz#3faf966c7cba0ff437fb31f6250082fcf0448cf3" @@ -2924,6 +3101,21 @@ md5.js@^1.3.4: inherits "^2.0.1" safe-buffer "^5.1.2" +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g= + +merge-descriptors@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" + integrity sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E= + +methods@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" + integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4= + miller-rabin@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.1.tgz#f080351c865b0dc562a8462966daa53543c78a4d" @@ -2937,7 +3129,7 @@ mime-db@1.44.0: resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.44.0.tgz#fa11c5eb0aca1334b4233cb4d52f10c5a6272f92" integrity sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg== -mime-types@^2.1.12, mime-types@~2.1.19: +mime-types@^2.1.12, mime-types@~2.1.19, mime-types@~2.1.24: version "2.1.27" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.27.tgz#47949f98e279ea53119f5722e0f34e529bec009f" integrity sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w== @@ -3074,6 +3266,11 @@ ndjson-parse@1.0.4: resolved "https://registry.yarnpkg.com/ndjson-parse/-/ndjson-parse-1.0.4.tgz#65c031147ea1b5fa6f692e4fd63ab75f89dbf648" integrity sha512-xwglvz2dMbxvX4NAVKnww8xEJ4kp4+CKVseQQdtkA79yI3abPqyBYqk6A6HvNci5oS0cUsSHheMEV1c+9MWlEw== +negotiator@0.6.2: + version "0.6.2" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb" + integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw== + next-tick@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.0.0.tgz#ca86d1fe8828169b0120208e3dc8424b9db8342c" @@ -3408,6 +3605,11 @@ path-platform@~0.11.15: resolved "https://registry.yarnpkg.com/path-platform/-/path-platform-0.11.15.tgz#e864217f74c36850f0852b78dc7bf7d4a5721bf2" integrity sha1-6GQhf3TDaFDwhSt43Hv31KVyG/I= +path-to-regexp@0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" + integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w= + path-to-regexp@^1.7.0: version "1.8.0" resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.8.0.tgz#887b3ba9d84393e87a0a0b9f4cb756198b53548a" @@ -3541,6 +3743,14 @@ protobufjs@^6.10.1: "@types/node" "^13.7.0" long "^4.0.0" +proxy-addr@~2.0.5: + version "2.0.6" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.6.tgz#fdc2336505447d3f2f2c638ed272caf614bbb2bf" + integrity sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw== + dependencies: + forwarded "~0.1.2" + ipaddr.js "1.9.1" + psl@^1.1.28: version "1.8.0" resolved "https://registry.yarnpkg.com/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24" @@ -3573,6 +3783,11 @@ punycode@^2.1.0, punycode@^2.1.1: resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== +qs@6.7.0: + version "6.7.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc" + integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ== + qs@~6.5.2: version "6.5.2" resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" @@ -3608,6 +3823,16 @@ range-parser@~1.2.1: resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== +raw-body@2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.0.tgz#a1ce6fb9c9bc356ca52e89256ab59059e13d0332" + integrity sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q== + dependencies: + bytes "3.1.0" + http-errors "1.7.2" + iconv-lite "0.4.24" + unpipe "1.0.0" + read-only-stream@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/read-only-stream/-/read-only-stream-2.0.0.tgz#2724fd6a8113d73764ac288d4386270c1dbf17f0" @@ -3777,17 +4002,17 @@ ripemd160@^2.0.0, ripemd160@^2.0.1: hash-base "^3.0.0" inherits "^2.0.1" +safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.0, safe-buffer@~5.2.0: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== -safe-buffer@~5.1.0, safe-buffer@~5.1.1: - version "5.1.2" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" - integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== - -safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: +"safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: version "2.1.2" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== @@ -4313,7 +4538,7 @@ timers-browserify@^1.0.1: dependencies: process "~0.11.0" -tmp@0.2.1: +tmp@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.1.tgz#8457fc3037dcf4719c251367a1af6500ee1ccf14" integrity sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ== @@ -4439,6 +4664,14 @@ type-fest@^0.8.0, type-fest@^0.8.1: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== +type-is@~1.6.17, type-is@~1.6.18: + version "1.6.18" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" + integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== + dependencies: + media-typer "0.3.0" + mime-types "~2.1.24" + type@^1.0.1: version "1.2.0" resolved "https://registry.yarnpkg.com/type/-/type-1.2.0.tgz#848dd7698dafa3e54a6c479e759c4bc3f18847a0" @@ -4492,7 +4725,7 @@ universalify@^1.0.0: resolved "https://registry.yarnpkg.com/universalify/-/universalify-1.0.0.tgz#b61a1da173e8435b2fe3c67d29b9adf8594bd16d" integrity sha512-rb6X1W158d7pRQBg5gkR8uPaSfiids68LTJQYOtEUhoJUWBdaQHsuT/EUduxXYxcrt4r5PJ4fuHW1MHT6p0qug== -unpipe@~1.0.0: +unpipe@1.0.0, unpipe@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw= @@ -4576,6 +4809,11 @@ validate-npm-package-license@^3.0.1: spdx-correct "^3.0.0" spdx-expression-parse "^3.0.0" +vary@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw= + verror@1.10.0, verror@^1.10.0: version "1.10.0" resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400"