Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
feat(rest): new interaction to set a request header for all the subse…
…quent requests
  • Loading branch information
jan-molak committed Jan 27, 2021
1 parent a769d15 commit c1c9be0
Show file tree
Hide file tree
Showing 3 changed files with 156 additions and 31 deletions.
81 changes: 66 additions & 15 deletions packages/rest/spec/screenplay/interactions/ChangeAPIConfig.spec.ts
Expand Up @@ -54,31 +54,82 @@ describe('ChangeApiConfig', () => {
* @test {ChangeApiConfig}
* @test {ChangeApiConfig.setPortTo}
*/
it('changes the base URL used by any subsequent requests', () => actor.attemptsTo(
Send.a(GetRequest.to('/')),
Ensure.that(LastResponse.status(), equals(500)),
it('changes the base URL used by any subsequent requests', () =>
actor.attemptsTo(
Send.a(GetRequest.to('/')),
Ensure.that(LastResponse.status(), equals(500)),

ChangeApiConfig.setPortTo(8080),
Send.a(GetRequest.to('/')),
Ensure.that(LastResponse.status(), equals(200)),
));
ChangeApiConfig.setPortTo(8080),
Send.a(GetRequest.to('/')),
Ensure.that(LastResponse.status(), equals(200)),
));

/**
* @test {ChangeApiConfig}
* @test {ChangeApiConfig.setPortTo}
*/
it('complains if the url has not been set prior to attempted port change', () => expect(actor.attemptsTo(
ChangeApiConfig.setUrlTo(undefined),
ChangeApiConfig.setPortTo(8080),
)).to.be.rejectedWith(LogicError, `Can't change the port of a baseURL that has not been set.`));
it('complains if the url has not been set prior to attempted port change', () =>
expect(actor.attemptsTo(
ChangeApiConfig.setUrlTo(undefined),
ChangeApiConfig.setPortTo(8080),
)).to.be.rejectedWith(LogicError, `Can't change the port of a baseURL that has not been set.`));

/**
* @test {ChangeApiConfig}
* @test {ChangeApiConfig.setPortTo}
*/
it('complains if the url to be changed is invalid', () => expect(actor.attemptsTo(
ChangeApiConfig.setUrlTo('invalid'),
ChangeApiConfig.setPortTo(8080),
)).to.be.rejectedWith(LogicError, `Could not change the API port`));
it('complains if the url to be changed is invalid', () =>
expect(actor.attemptsTo(
ChangeApiConfig.setUrlTo('invalid'),
ChangeApiConfig.setPortTo(8080),
)).to.be.rejectedWith(LogicError, `Could not change the API port`));
});

describe('when setting a request header', () => {

const { actor, mock } = actorUsingAMockedAxiosInstance({ baseURL: originalUrl });

beforeEach(() => {
const dataMatcher = undefined;
mock.onGet(originalUrl, dataMatcher).replyOnce(401);
mock.onGet(originalUrl, dataMatcher, {
'Accept': 'application/json, text/plain, */*',
'Authorization': 'my-token'
}).replyOnce(200);
});

afterEach(() => mock.reset());

/**
* @test {ChangeApiConfig}
* @test {ChangeApiConfig.setHeader}
*/
it('sets a header to be used by any subsequent requests', () =>
actor.attemptsTo(
Send.a(GetRequest.to('/')),
Ensure.that(LastResponse.status(), equals(401)),

ChangeApiConfig.setHeader('Authorization', 'my-token'),
Send.a(GetRequest.to('/')),
Ensure.that(LastResponse.status(), equals(200)),
));

/**
* @test {ChangeApiConfig}
* @test {ChangeApiConfig.setHeader}
*/
it('complains if the name of the header is empty', () =>
expect(actor.attemptsTo(
ChangeApiConfig.setHeader('', 'value'),
)).to.be.rejectedWith(LogicError, `Looks like the name of the header is missing, "" given`));

/**
* @test {ChangeApiConfig}
* @test {ChangeApiConfig.setHeader}
*/
it('complains if the name of the header is undefined', () =>
expect(actor.attemptsTo(
ChangeApiConfig.setHeader(undefined, 'value'),
)).to.be.rejectedWith(LogicError, `Looks like the name of the header is missing, "undefined" given`));
});
});
14 changes: 8 additions & 6 deletions packages/rest/src/model/HTTPRequest.ts
Expand Up @@ -56,12 +56,14 @@ export abstract class HTTPRequest extends Question<Promise<AxiosRequestConfig>>
{ method: this.httpMethodName() },
),
)
.then(config => Object.keys(config).reduce((acc, key) => {
if (!! config[key]) {
acc[key] = config[key];
}
return acc;
}, {}));
.then(config =>
Object.keys(config).reduce((acc, key) => {
if (!! config[key]) {
acc[key] = config[key];
}
return acc;
}, {})
);
}

/**
Expand Down
92 changes: 82 additions & 10 deletions packages/rest/src/screenplay/interactions/ChangeApiConfig.ts
Expand Up @@ -8,15 +8,15 @@ import { CallAnApi } from '../abilities';
* the {@link @serenity-js/core/lib/screenplay/actor~Actor}
* executing this {@link @serenity-js/core/lib/screenplay~Interaction} has been configured with.
*
* @example <caption>Changing API URL</caption>
* @example <caption>Changing API URL for all subsequent requests</caption>
* import { Actor } from '@serenity-js/core';
* import { Navigate, Target, Text } from '@serenity-js/protractor';
* import { CallAnApi, ChangeApiConfig, GetRequest, LastResponse, Send } from '@serenity-js/rest'
* import { protractor, by } from 'protractor';
*
* import axios from 'axios';
* import axios from 'axios';
*
* const actor = Actor.named('Apisit').whoCan(
* const actor = Actor.named('Apisitt').whoCan(
* BrowseTheWeb.using(protractor.browser),
*
* // Note: no default base URL is given when the axios instance is created
Expand All @@ -41,21 +41,39 @@ import { CallAnApi } from '../abilities';
* Ensure.that(LastResponse.status(), equals(200)),
* );
*
* @example <caption>Changing API port</caption>
* @example <caption>Changing API port for all subsequent requests</caption>
* import { Actor } from '@serenity-js/core';
* import { LocalServer, ManageALocalServer, StartLocalServer } from '@serenity-js/local-server';
* import { CallAnApi, ChangeApiConfig, GetRequest, LastResponse, Send } from '@serenity-js/rest'
*
* const actor = Actor.named('Apisit').whoCan(
* const actor = Actor.named('Apisitt').whoCan(
* ManageALocalServer.runningAHttpListener(someServer),
* CallAnApi.at(http://localhost),
* CallAnApi.at('http://localhost'),
* );
*
* actor.attemptsTo(
* StartALocalServer.onRandomPort(),
* ChangeApiConfig.setPortTo(LocalServer.port()),
* Send.a(GetRequest.to('/api')),
* Ensure.that(LastResponse.status(), equals(200)),
* StartALocalServer.onRandomPort(),
* ChangeApiConfig.setPortTo(LocalServer.port()),
* Send.a(GetRequest.to('/api')),
* Ensure.that(LastResponse.status(), equals(200)),
* );
*
* @example <caption>Setting a header for all subsequent requests</caption>
* import { Actor, Question } from '@serenity-js/core';
* import { CallAnApi, ChangeApiConfig, GetRequest, LastResponse, Send } from '@serenity-js/rest'
*
* const actor = Actor.named('Apisitt').whoCan(
* CallAnApi.at('http://localhost'),
* );
*
* // A sample Question reading Node process environment variable
* const EnvVar = (var_name: string) =>
* Question.about(`${ name } environment variable`, actor => process.env[var_name]);
*
* actor.attemptsTo(
* ChangeApiConfig.header('Authorization', EnvVar('TOKEN')),
* Send.a(GetRequest.to('/api')),
* Ensure.that(LastResponse.status(), equals(200)),
* );
*/
export class ChangeApiConfig {
Expand Down Expand Up @@ -83,6 +101,23 @@ export class ChangeApiConfig {
static setPortTo(newApiPort: Answerable<number>): Interaction {
return new ChangeApiConfigSetPort(newApiPort)
}

/**
* @desc
* Instructs the {@link @serenity-js/core/lib/screenplay/actor~Actor}
* to modify the configuration of the {@link AxiosInstance}
* used by {@link CallAnApi} {@link @serenity-js/core/lib/screenplay~Ability}
* and set a HTTP request header for any subsequent {@link HTTPRequest}
* issued via {@link Send}.
*
* @param {@serenity-js/core/lib/screenplay~Answerable<string>} name
* @param {@serenity-js/core/lib/screenplay~Answerable<string>} value
*
* @returns {@serenity-js/core/lib/screenplay~Interaction}
*/
static setHeader(name: Answerable<string>, value: Answerable<string>): Interaction {
return new ChangeApiConfigSetHeader(name, value);
}
}

/**
Expand Down Expand Up @@ -134,3 +169,40 @@ class ChangeApiConfigSetPort extends Interaction {
}
}

/**
* @package
*
* @see https://github.com/axios/axios#custom-instance-defaults
*/
class ChangeApiConfigSetHeader extends Interaction {

constructor(
private readonly name: Answerable<string>,
private readonly value: Answerable<string>
) {
super();
}

performAs(actor: UsesAbilities & CollectsArtifacts & AnswersQuestions): Promise<void> {
return Promise.all([
actor.answer(this.name),
actor.answer(this.value),
]).
then(([ name, value ]) => {
if (! name) {
throw new LogicError(`Looks like the name of the header is missing, "${ name }" given`);
}

// A header with an empty value might still be valid so we don't validate the value
// see: https://www.w3.org/Protocols/rfc2616/rfc2616-sec2.html#sec2.1

return CallAnApi.as(actor).modifyConfig(config => {
config.headers.common[name] = value;
});
});
}

toString() {
return `#actor changes API URL and sets header "${ this.name }" to "${ this.value }"`;
}
}

0 comments on commit c1c9be0

Please sign in to comment.