Skip to content

Commit

Permalink
feat(playwright-test): explicit proxy config will override env variab…
Browse files Browse the repository at this point in the history
…les for REST interaction

Serenity/JS Playwright Test adapter will now use the `proxy` property defined in Playwright Test
config to override any proxy-related environment variables when instantiating the ability to
CallAnApi. This will allow for consistent behaviour between the browser and the HTTP client.

Related tickets: #1949
  • Loading branch information
jan-molak committed Sep 26, 2023
1 parent dfaf8e4 commit 1c277d6
Show file tree
Hide file tree
Showing 10 changed files with 907 additions and 151 deletions.
829 changes: 753 additions & 76 deletions package-lock.json

Large diffs are not rendered by default.

47 changes: 39 additions & 8 deletions packages/playwright-test/src/api/test-api.ts
Expand Up @@ -40,8 +40,11 @@ export const fixtures: Fixtures<Omit<SerenityOptions, 'actors'> & SerenityFixtur
CallAnApi.using({
baseURL: contextOptions.baseURL,
headers: contextOptions.extraHTTPHeaders,
proxy: contextOptions.proxy && contextOptions.proxy?.server
? asProxyConfig(contextOptions.proxy)
: undefined,
}),
)))
)));
},
{ option: true },
],
Expand Down Expand Up @@ -199,14 +202,14 @@ function createTestApi<TestArgs extends Record<string, any>, WorkerArgs extends
useFixtures<T extends Record<string, any>, W extends Record<string, any> = object>(customFixtures: Fixtures<T, W, TestArgs, WorkerArgs>): TestApi<TestArgs & T, WorkerArgs & W> {
return createTestApi(baseTest.extend(customFixtures));
},
beforeAll: baseTest.beforeAll,
beforeAll: baseTest.beforeAll,
beforeEach: baseTest.beforeEach,
afterEach: baseTest.afterEach,
afterAll: baseTest.afterAll,
describe: baseTest.describe,
expect: baseTest.expect,
it: baseTest,
test: baseTest,
afterEach: baseTest.afterEach,
afterAll: baseTest.afterAll,
describe: baseTest.describe,
expect: baseTest.expect,
it: baseTest,
test: baseTest,
};
}

Expand Down Expand Up @@ -500,3 +503,31 @@ function asDuration(maybeDuration: number | Duration): Duration {
function asCast(maybeCast: unknown): Cast {
return ensure('actors', maybeCast as Cast, property('prepare', isFunction()));
}

/**
* @private
* @param proxy
*/
function asProxyConfig(proxy: PlaywrightTestOptions['proxy']): {
host: string,
port?: number,
auth?: { username: string, password: string }
} {

// Playwright defaults to http when proxy.server does not define the protocol
// See https://playwright.dev/docs/api/class-testoptions#test-options-proxy
const hasProtocol = /[\dA-Za-z]+:\/\//.test(proxy.server);
const proxyUrl = hasProtocol
? new URL(proxy.server)
: new URL(`http://${ proxy.server }`);

const host = proxyUrl.hostname;
const port = proxyUrl.port
? Number(proxyUrl.port)
: undefined;
const auth = proxy.username
? { username: proxy.username, password: proxy.password || '' }
: undefined;

return { host, port, auth };
}
9 changes: 9 additions & 0 deletions packages/rest/spec/screenplay/abilities/CallAnApi.spec.ts
Expand Up @@ -31,6 +31,15 @@ describe('CallAnApi', () => {
});

it('provides a convenient factory method to be used when no custom configuration is required', () => {
const
baseURL = 'https://mycompany.com/api',
callAnApi = CallAnApi.at(new URL(baseURL));

expect(callAnApi).to.be.instanceOf(CallAnApi);
expect((callAnApi as any).axiosInstance.defaults.baseURL).to.equal(baseURL);
});

it('allows for the ability to be instantiated using a URL object instead of a string', () => {
const
baseURL = 'https://mycompany.com/api',
callAnApi = CallAnApi.at(baseURL);
Expand Down
@@ -0,0 +1,39 @@
import { expect } from '@integration/testing-tools';
import axios from 'axios';
import { describe, it } from 'mocha';

import { axiosProxyOverridesFor } from '../../../../src/screenplay/abilities/proxy';
import { ProxyAgent } from '../../../../src/screenplay/abilities/proxy/ProxyAgent';

describe('axiosProxyOverridesFor', () => {

describe('when no proxy overrides are defined', () => {

it('overrides Axios built-in proxy mechanism to use env variables', () => {

const overrides = axiosProxyOverridesFor(axios.create({}).defaults);

expect(overrides.proxy).to.equal(false);
expect(overrides.httpAgent).to.be.instanceof(ProxyAgent);
expect(overrides.httpsAgent).to.be.instanceof(ProxyAgent);
});
});

describe('when proxy overrides are defined', () => {

it('creates an Axios instance that overrides Axios built-in proxy mechanism', () => {

const overrides = axiosProxyOverridesFor(axios.create({
proxy: {
protocol: 'http',
host: 'proxy.mycompany.com',
port: 9000,
}
}).defaults);

expect(overrides.proxy).to.equal(false);
expect(overrides.httpAgent).to.be.instanceof(ProxyAgent);
expect(overrides.httpsAgent).to.be.instanceof(ProxyAgent);
});
});
});

This file was deleted.

@@ -0,0 +1,13 @@
import { type CreateAxiosDefaults } from 'axios';

export type AxiosRequestConfigDefaults<Data = any> = Omit<CreateAxiosDefaults<Data>, 'proxy'> & {
proxy?: {
host: string;
port?: number; // SOCKS proxy doesn't require port number
auth?: {
username: string;
password: string;
};
protocol?: string;
}
}
61 changes: 49 additions & 12 deletions packages/rest/src/screenplay/abilities/CallAnApi.ts
@@ -1,7 +1,16 @@
import { Ability, ConfigurationError, Duration, LogicError, TestCompromisedError } from '@serenity-js/core';
import axios, { Axios, type AxiosDefaults, type AxiosError, type AxiosInstance, type AxiosRequestConfig, type AxiosResponse, type CreateAxiosDefaults } from 'axios';
import axios, {
Axios,
type AxiosDefaults,
type AxiosError,
type AxiosInstance,
type AxiosRequestConfig,
type AxiosResponse,
type CreateAxiosDefaults,
} from 'axios';

import { withProxySupport } from './proxy';
import type { AxiosRequestConfigDefaults } from './AxiosRequestConfigDefaults';
import { axiosProxyOverridesFor } from './proxy';

/**
* An {@apilink Ability} that wraps [axios client](https://axios-http.com/docs/api_intro) and enables
Expand Down Expand Up @@ -319,7 +328,7 @@ export class CallAnApi extends Ability {

private static readonly defaults: CreateAxiosDefaults<any> = {
timeout: Duration.ofSeconds(10).inMilliseconds(),
}
};

/**
* Produces an {@apilink Ability|ability} to call a REST API at a specified `baseURL`;
Expand All @@ -328,8 +337,12 @@ export class CallAnApi extends Ability {
*
* @param baseURL
*/
static at(baseURL: string): CallAnApi {
return CallAnApi.using({ baseURL });
static at(baseURL: URL | string): CallAnApi {
return CallAnApi.using({
baseURL: baseURL instanceof URL
? baseURL.toString()
: baseURL
});
}

/**
Expand All @@ -344,21 +357,32 @@ export class CallAnApi extends Ability {
*
* When you provide an Axios instance, it's enhanced with proxy support and no other modifications are made.
*
* If you don't want Serenity/JS to augment or modify your Axios instance in any way, please use the {@apilink CallAnApi.constructor}
* directly.
* If you don't want Serenity/JS to augment or modify your Axios instance in any way,
* please use the {@apilink CallAnApi.constructor} directly.
*
* @param axiosInstanceOrConfig
*/
static using(axiosInstanceOrConfig: AxiosInstance | CreateAxiosDefaults): CallAnApi {
static using(axiosInstanceOrConfig: AxiosInstance | AxiosRequestConfigDefaults): CallAnApi {

const axiosInstanceGiven = isAxiosInstance(axiosInstanceOrConfig);

const instance = isAxiosInstance(axiosInstanceOrConfig)
const axiosInstance = axiosInstanceGiven
? axiosInstanceOrConfig
: axios.create({
...CallAnApi.defaults,
...axiosInstanceOrConfig
...omit(axiosInstanceOrConfig, 'proxy'),
});

return new CallAnApi(withProxySupport(instance));
const proxyConfig = axiosInstanceGiven
? axiosInstanceOrConfig.defaults.proxy
: axiosInstanceOrConfig.proxy

const proxyOverrides = axiosProxyOverridesFor({
...axiosInstance.defaults,
...proxyConfig
});

return new CallAnApi(withOverrides(axiosInstance, proxyOverrides));
}

/**
Expand Down Expand Up @@ -410,7 +434,7 @@ export class CallAnApi extends Ability {

return this.lastResponse;
}
catch(error) {
catch (error) {
const description = `${ config.method.toUpperCase() } ${ url || config.url }`;

switch (true) {
Expand Down Expand Up @@ -469,3 +493,16 @@ function isAxiosInstance(axiosInstanceOrConfig: any): axiosInstanceOrConfig is A
return axiosInstanceOrConfig
&& (axiosInstanceOrConfig instanceof Axios || axiosInstanceOrConfig.defaults);
}

function withOverrides<Data = any>(axiosInstance: AxiosInstance, overrides: AxiosRequestConfig<Data>): AxiosInstance {
for (const [key, override] of Object.entries(overrides)) {
axiosInstance.defaults[key] = override;
}

return axiosInstance;
}

function omit<T extends object, K extends keyof T>(record: T, key: K): Omit<T, K> {
const { [key]: omitted_, ...rest } = record;
return rest;
}
1 change: 1 addition & 0 deletions packages/rest/src/screenplay/abilities/index.ts
@@ -1 +1,2 @@
export * from './AxiosRequestConfigDefaults';
export * from './CallAnApi';
@@ -1,24 +1,14 @@
import { type AxiosInstance, type CreateAxiosDefaults } from 'axios';
import { type CreateAxiosDefaults } from 'axios';
import type * as http from 'http';
import { ensure, isDefined } from 'tiny-types';

import { createUrl } from './createUrl';
import { ProxyAgent } from './ProxyAgent';

/**
* @param axiosInstance
* @param options
*/
export function withProxySupport(axiosInstance: AxiosInstance): AxiosInstance {
const overrides = proxyConfigUsing(axiosInstance.defaults);

for (const [ key, override ] of Object.entries(overrides)) {
axiosInstance.defaults[key] = override;
}

return axiosInstance;
}

function proxyConfigUsing<Data = any>(options: CreateAxiosDefaults<Data>): {
export function axiosProxyOverridesFor<Data = any>(options: CreateAxiosDefaults<Data>): {
proxy: false, httpAgent: http.Agent, httpsAgent: http.Agent
} {
const envProxyOverride: string | false = options.proxy
Expand Down
2 changes: 1 addition & 1 deletion packages/rest/src/screenplay/abilities/proxy/index.ts
@@ -1 +1 @@
export * from './withProxySupport';
export * from './axiosProxyOverridesFor';

0 comments on commit 1c277d6

Please sign in to comment.