Skip to content

Commit

Permalink
feat(rest): all HTTP requests accept DynamicRecord<AxiosRequestConfig>
Browse files Browse the repository at this point in the history
This allows for the configuration to be defined as a regular JS object with nested `Question`s. This
is particularly useful when specifying request headers.

re #463
  • Loading branch information
jan-molak committed May 13, 2022
1 parent 6a66778 commit c28b47c
Show file tree
Hide file tree
Showing 17 changed files with 183 additions and 40 deletions.
21 changes: 21 additions & 0 deletions packages/core/src/screenplay/DynamicRecord.ts
@@ -0,0 +1,21 @@
import { Answerable } from './Answerable';
import { Question } from './Question';

/* eslint-disable @typescript-eslint/indent */

/**
* @experimental
*/
export type DynamicRecord<
T extends Exclude<
Record<any, any>,
Question<Promise<any>> | Question<any> | Promise<any>
>
> = {
[K in keyof T]:
T[K] extends Record<any, any>
? Answerable<DynamicRecord<T[K]>>
: Answerable<T[K]>;
}

/* eslint-enable @typescript-eslint/indent */
1 change: 1 addition & 0 deletions packages/core/src/screenplay/index.ts
Expand Up @@ -4,6 +4,7 @@ export * from './AbilityType';
export * from './Activity';
export * from './actor';
export * from './Answerable';
export * from './DynamicRecord';
export * from './Interaction';
export * from './interactions';
export * from './notes';
Expand Down
8 changes: 1 addition & 7 deletions packages/core/src/screenplay/questions/Dictionary.ts
Expand Up @@ -3,15 +3,9 @@ import { isPlainObject, Success } from 'tiny-types';
import { LogicError } from '../../errors';
import { AnswersQuestions, UsesAbilities } from '../actor';
import { Answerable } from '../Answerable';
import { DynamicRecord } from '../DynamicRecord';
import { Question, QuestionAdapter } from '../Question';

export type DynamicRecord<T extends object> = {
[K in keyof T]:
T[K] extends object
? Answerable<DynamicRecord<T[K]>>
: Answerable<T[K]>;
}

/**
* @desc
* Represents a Screenplay-style [Dictionary](https://en.wikipedia.org/wiki/Associative_array),
Expand Down
21 changes: 21 additions & 0 deletions packages/rest/spec/model/DeleteRequest.spec.ts
@@ -1,5 +1,7 @@
import 'mocha';

import { q, Question } from '@serenity-js/core';

import { DeleteRequest } from '../../src/model';
import { actorUsingAMockedAxiosInstance } from '../actors';
import { expect } from '../expect';
Expand Down Expand Up @@ -35,6 +37,25 @@ describe('DeleteRequest', () => {
},
}));

it('accepts dynamic records', () =>
expect(
actor.answer(DeleteRequest.to('/products/2')
.using({
headers: {
Authorization: q`Bearer ${ Question.about('token', actor => 'some-token') }`,
},
})
)
).
to.eventually.deep.equal({
method: 'DELETE',
url: '/products/2',
headers: {
Authorization: 'Bearer some-token',
},
})
);

/** @test {DeleteRequest#toString} */
it('provides a sensible description of the interaction being performed', () => {
expect(DeleteRequest.to('/products/2').toString())
Expand Down
21 changes: 21 additions & 0 deletions packages/rest/spec/model/GetRequest.spec.ts
@@ -1,5 +1,7 @@
import 'mocha';

import { q, Question } from '@serenity-js/core';

import { GetRequest } from '../../src/model';
import { actorUsingAMockedAxiosInstance } from '../actors';
import { expect } from '../expect';
Expand Down Expand Up @@ -37,6 +39,25 @@ describe('GetRequest', () => {
maxRedirects: 0,
}));

it('accepts dynamic records', () =>
expect(
actor.answer(GetRequest.to('/products/2')
.using({
headers: {
Authorization: q`Bearer ${ Question.about('token', actor => 'some-token') }`,
},
})
)
).
to.eventually.deep.equal({
method: 'GET',
url: '/products/2',
headers: {
Authorization: 'Bearer some-token',
},
})
);

/** @test {GetRequest#toString} */
it('provides a sensible description of the interaction being performed', () => {
expect(GetRequest.to('/products/2').toString())
Expand Down
21 changes: 21 additions & 0 deletions packages/rest/spec/model/HeadRequest.spec.ts
@@ -1,5 +1,7 @@
import 'mocha';

import { q, Question } from '@serenity-js/core';

import { HeadRequest } from '../../src/model';
import { actorUsingAMockedAxiosInstance } from '../actors';
import { expect } from '../expect';
Expand Down Expand Up @@ -35,6 +37,25 @@ describe('HeadRequest', () => {
},
}));

it('accepts dynamic records', () =>
expect(
actor.answer(HeadRequest.to('/products/2')
.using({
headers: {
Authorization: q`Bearer ${ Question.about('token', actor => 'some-token') }`,
},
})
)
).
to.eventually.deep.equal({
method: 'HEAD',
url: '/products/2',
headers: {
Authorization: 'Bearer some-token',
},
})
);

/** @test {HeadRequest#toString} */
it('provides a sensible description of the interaction being performed', () => {
expect(HeadRequest.to('/products/2').toString())
Expand Down
21 changes: 21 additions & 0 deletions packages/rest/spec/model/OptionsRequest.spec.ts
@@ -1,5 +1,7 @@
import 'mocha';

import { q, Question } from '@serenity-js/core';

import { OptionsRequest } from '../../src/model';
import { actorUsingAMockedAxiosInstance } from '../actors';
import { expect } from '../expect';
Expand Down Expand Up @@ -35,6 +37,25 @@ describe('OptionsRequest', () => {
},
}));

it('accepts dynamic records', () =>
expect(
actor.answer(OptionsRequest.to('/products/2')
.using({
headers: {
Authorization: q`Bearer ${ Question.about('token', actor => 'some-token') }`,
},
})
)
).
to.eventually.deep.equal({
method: 'OPTIONS',
url: '/products/2',
headers: {
Authorization: 'Bearer some-token',
},
})
);

/** @test {Options#toString} */
it('provides a sensible description of the interaction being performed', () => {
expect(OptionsRequest.to('/products/2').toString())
Expand Down
22 changes: 22 additions & 0 deletions packages/rest/spec/model/PatchRequest.spec.ts
@@ -1,5 +1,7 @@
import 'mocha';

import { q, Question } from '@serenity-js/core';

import { PatchRequest } from '../../src/model';
import { actorUsingAMockedAxiosInstance } from '../actors';
import { expect } from '../expect';
Expand Down Expand Up @@ -51,6 +53,26 @@ describe('PatchRequest', () => {
})
);

it('accepts dynamic records', () =>
expect(
actor.answer(PatchRequest.to('/products/2').with({ name: 'apple' })
.using({
headers: {
Authorization: q`Bearer ${ Question.about('token', actor => 'some-token') }`,
},
})
)
).
to.eventually.deep.equal({
method: 'PATCH',
url: '/products/2',
headers: {
Authorization: 'Bearer some-token',
},
data: { name: 'apple' },
})
);

/** @test {PatchRequest#toString} */
it('provides a sensible description of the interaction being performed', () => {
expect(PatchRequest.to('/products/2').toString())
Expand Down
9 changes: 4 additions & 5 deletions packages/rest/spec/model/PostRequest.spec.ts
@@ -1,7 +1,6 @@
import 'mocha';

import { Dictionary, q, Question } from '@serenity-js/core';
import { AxiosRequestConfig } from 'axios';
import { q, Question } from '@serenity-js/core';

import { PostRequest } from '../../src/model';
import { actorUsingAMockedAxiosInstance } from '../actors';
Expand Down Expand Up @@ -45,14 +44,14 @@ describe('PostRequest', () => {
})
);

it('works with a Dictionary', () =>
it('accepts dynamic records', () =>
expect(
actor.answer(PostRequest.to('/products/2').with({ name: 'apple' })
.using(Dictionary.of<AxiosRequestConfig>({
.using({
headers: {
Authorization: q`Bearer ${ Question.about('token', actor => 'some-token') }`,
},
}))
})
)
).
to.eventually.deep.equal({
Expand Down
22 changes: 22 additions & 0 deletions packages/rest/spec/model/PutRequest.spec.ts
@@ -1,5 +1,7 @@
import 'mocha';

import { q, Question } from '@serenity-js/core';

import { PutRequest } from '../../src/model';
import { actorUsingAMockedAxiosInstance } from '../actors';
import { expect } from '../expect';
Expand Down Expand Up @@ -51,6 +53,26 @@ describe('PutRequest', () => {
})
);

it('accepts dynamic records', () =>
expect(
actor.answer(PutRequest.to('/products/2').with({ name: 'apple' })
.using({
headers: {
Authorization: q`Bearer ${ Question.about('token', actor => 'some-token') }`,
},
})
)
).
to.eventually.deep.equal({
method: 'PUT',
url: '/products/2',
headers: {
Authorization: 'Bearer some-token',
},
data: { name: 'apple' },
})
);

/** @test {PutRequest#toString} */
it('provides a sensible description of the interaction being performed', () => {
expect(PutRequest.to('/products/2').toString())
Expand Down
8 changes: 4 additions & 4 deletions packages/rest/src/model/DeleteRequest.ts
@@ -1,4 +1,4 @@
import { Answerable } from '@serenity-js/core';
import { Answerable, Dictionary, DynamicRecord } from '@serenity-js/core';
import { AxiosRequestConfig } from 'axios';

import { HTTPRequest } from './HTTPRequest';
Expand Down Expand Up @@ -60,12 +60,12 @@ export class DeleteRequest extends HTTPRequest {
* Overrides the default Axios request configuration provided
* when {@link CallAnApi} {@link @serenity-js/core/lib/screenplay~Ability} was instantiated.
*
* @param {@serenity-js/core/lib/screenplay~Answerable<AxiosRequestConfig>} config
* @param {Answerable<DynamicRecord<AxiosRequestConfig>>} config
* Axios request configuration overrides
*
* @returns {DeleteRequest}
*/
using(config: Answerable<AxiosRequestConfig>): DeleteRequest {
return new DeleteRequest(this.resourceUri, undefined, config);
using(config: Answerable<DynamicRecord<AxiosRequestConfig>>): DeleteRequest {
return new DeleteRequest(this.resourceUri, undefined, Dictionary.of(config));
}
}
8 changes: 4 additions & 4 deletions packages/rest/src/model/GetRequest.ts
@@ -1,4 +1,4 @@
import { Answerable } from '@serenity-js/core';
import { Answerable, Dictionary, DynamicRecord } from '@serenity-js/core';
import { AxiosRequestConfig } from 'axios';

import { HTTPRequest } from './HTTPRequest';
Expand Down Expand Up @@ -67,12 +67,12 @@ export class GetRequest extends HTTPRequest {
* Overrides the default Axios request configuration provided
* when {@link CallAnApi} {@link @serenity-js/core/lib/screenplay~Ability} was instantiated.
*
* @param {@serenity-js/core/lib/screenplay~Answerable<AxiosRequestConfig>} config
* @param {Answerable<DynamicRecord<AxiosRequestConfig>>} config
* Axios request configuration overrides
*
* @returns {GetRequest}
*/
using(config: Answerable<AxiosRequestConfig>): GetRequest {
return new GetRequest(this.resourceUri, undefined, config);
using(config: Answerable<DynamicRecord<AxiosRequestConfig>>): GetRequest {
return new GetRequest(this.resourceUri, undefined, Dictionary.of(config));
}
}
8 changes: 4 additions & 4 deletions packages/rest/src/model/HeadRequest.ts
@@ -1,4 +1,4 @@
import { Answerable } from '@serenity-js/core';
import { Answerable, Dictionary, DynamicRecord } from '@serenity-js/core';
import { AxiosRequestConfig } from 'axios';

import { HTTPRequest } from './HTTPRequest';
Expand Down Expand Up @@ -52,12 +52,12 @@ export class HeadRequest extends HTTPRequest {
* Overrides the default Axios request configuration provided
* when {@link CallAnApi} {@link @serenity-js/core/lib/screenplay~Ability} was instantiated.
*
* @param {@serenity-js/core/lib/screenplay~Answerable<AxiosRequestConfig>} config
* @param {Answerable<DynamicRecord<AxiosRequestConfig>>} config
* Axios request configuration overrides
*
* @returns {HeadRequest}
*/
using(config: Answerable<AxiosRequestConfig>): HeadRequest {
return new HeadRequest(this.resourceUri, undefined, config);
using(config: Answerable<DynamicRecord<AxiosRequestConfig>>): HeadRequest {
return new HeadRequest(this.resourceUri, undefined, Dictionary.of(config));
}
}
8 changes: 4 additions & 4 deletions packages/rest/src/model/OptionsRequest.ts
@@ -1,4 +1,4 @@
import { Answerable } from '@serenity-js/core';
import { Answerable, Dictionary, DynamicRecord } from '@serenity-js/core';
import { AxiosRequestConfig } from 'axios';

import { HTTPRequest } from './HTTPRequest';
Expand Down Expand Up @@ -55,12 +55,12 @@ export class OptionsRequest extends HTTPRequest {
* Overrides the default Axios request configuration provided
* when {@link CallAnApi} {@link @serenity-js/core/lib/screenplay~Ability} was instantiated.
*
* @param {@serenity-js/core/lib/screenplay~Answerable<AxiosRequestConfig>} config
* @param {Answerable<DynamicRecord<AxiosRequestConfig>>} config
* Axios request configuration overrides
*
* @returns {OptionsRequest}
*/
using(config: Answerable<AxiosRequestConfig>): OptionsRequest {
return new OptionsRequest(this.resourceUri, undefined, config);
using(config: Answerable<DynamicRecord<AxiosRequestConfig>>): OptionsRequest {
return new OptionsRequest(this.resourceUri, undefined, Dictionary.of(config));
}
}
8 changes: 4 additions & 4 deletions packages/rest/src/model/PatchRequest.ts
@@ -1,4 +1,4 @@
import { Answerable } from '@serenity-js/core';
import { Answerable, Dictionary, DynamicRecord } from '@serenity-js/core';
import { AxiosRequestConfig } from 'axios';

import { HTTPRequest } from './HTTPRequest';
Expand Down Expand Up @@ -65,12 +65,12 @@ export class PatchRequest extends HTTPRequest {
* Overrides the default Axios request configuration provided
* when {@link CallAnApi} {@link @serenity-js/core/lib/screenplay~Ability} was instantiated.
*
* @param {@serenity-js/core/lib/screenplay~Answerable<AxiosRequestConfig>} config
* @param {Answerable<DynamicRecord<AxiosRequestConfig>>} config
* Axios request configuration overrides
*
* @returns {PatchRequest}
*/
using(config: Answerable<AxiosRequestConfig>): PatchRequest {
return new PatchRequest(this.resourceUri, this.data, config);
using(config: Answerable<DynamicRecord<AxiosRequestConfig>>): PatchRequest {
return new PatchRequest(this.resourceUri, this.data, Dictionary.of(config));
}
}

0 comments on commit c28b47c

Please sign in to comment.