Skip to content

Commit

Permalink
Added support for request headers.
Browse files Browse the repository at this point in the history
Signed-off-by: dblock <dblock@amazon.com>
  • Loading branch information
dblock committed Aug 1, 2024
1 parent 6e88e70 commit c42291a
Show file tree
Hide file tree
Showing 10 changed files with 139 additions and 17 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,8 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
- Added `plugins` to NodeInfoSettings ([#442](https://github.com/opensearch-project/opensearch-api-specification/pull/442))
- Added test coverage ([#443](https://github.com/opensearch-project/opensearch-api-specification/pull/443))
- Added `--opensearch-version` to `merger` that excludes schema elements per semver ([#428](https://github.com/opensearch-project/opensearch-api-specification/pull/428))
- Added `retry` to `tester` to support asynchronous tasks ([453](https://github.com/opensearch-project/opensearch-api-specification/pull/453))
- Added `retry` to `tester` to support asynchronous tasks ([#453](https://github.com/opensearch-project/opensearch-api-specification/pull/453))
- Added support for request headers in tests [#461](https://github.com/opensearch-project/opensearch-api-specification/pull/461)

### Changed

Expand Down
2 changes: 2 additions & 0 deletions TESTING_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ chapters:
parameters: # All parameters are validated against their schemas in the spec.
index: books
request: # The request.
headers: # Optional headers.
user-agent: OpenSearch API Spec/1.0
payload: # The request body is validated against the schema of the requestBody in the spec.
mappings:
properties:
Expand Down
11 changes: 10 additions & 1 deletion json_schemas/test_story.schema.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -123,9 +123,12 @@ definitions:
content_type:
type: string
default: application/json
headers:
type: object
additionalProperties:
$ref: '#/definitions/Header'
payload:
$ref: '#/definitions/Payload'
required: [payload]
additionalProperties: false

ExpectedResponse:
Expand Down Expand Up @@ -161,6 +164,12 @@ definitions:
required: [content_type, payload, status]
additionalProperties: false

Header:
anyOf:
- type: string
- type: number
- type: boolean

Payload:
anyOf:
- type: object
Expand Down
2 changes: 1 addition & 1 deletion spec/schemas/security._common.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ components:
type: array
items:
type: string
log_request:
log_request_body:
type: boolean
resolve_indices:
type: boolean
Expand Down
2 changes: 1 addition & 1 deletion tools/src/tester/ChapterEvaluator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ export default class ChapterEvaluator {
}

#evaluate_request(chapter: Chapter, operation: ParsedOperation): Evaluation {
if (!chapter.request) return { result: Result.PASSED }
if (chapter.request?.payload === undefined) return { result: Result.PASSED }
const content_type = chapter.request.content_type ?? APPLICATION_JSON
const schema = operation.requestBody?.content[content_type]?.schema
if (schema == null) return { result: Result.FAILED, message: `Schema for "${content_type}" request body not found in the spec.` }
Expand Down
25 changes: 20 additions & 5 deletions tools/src/tester/ChapterReader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import YAML from 'yaml'
import CBOR from 'cbor'
import SMILE from 'smile-js'
import { APPLICATION_CBOR, APPLICATION_JSON, APPLICATION_SMILE, APPLICATION_YAML, TEXT_PLAIN } from "./MimeTypes";
import _ from 'lodash'

export default class ChapterReader {
private readonly _client: OpenSearchHttpClient
Expand All @@ -31,16 +32,16 @@ export default class ChapterReader {
const response: Record<string, any> = {}
const resolved_params = story_outputs.resolve_params(chapter.parameters ?? {})
const [url_path, params] = this.#parse_url(chapter.path, resolved_params)
const content_type = chapter.request?.content_type ?? APPLICATION_JSON
const [headers, content_type] = this.#serialize_headers(chapter.request?.headers, chapter.request?.content_type)
const request_data = chapter.request?.payload !== undefined ? this.#serialize_payload(
story_outputs.resolve_value(chapter.request.payload),
content_type
) : undefined
this.logger.info(`=> ${chapter.method} ${url_path} (${to_json(params)}) [${content_type}] | ${to_json(request_data)}`)
this.logger.info(`=> ${chapter.method} ${url_path} (${to_json(params)}) [${content_type}] ${_.compact([to_json(headers), to_json(request_data)]).join(' | ')}`)
await this._client.request({
url: url_path,
method: chapter.method,
headers: { 'Content-Type' : content_type },
headers: { 'Content-Type' : content_type, ...headers },
params,
data: request_data,
paramsSerializer: (params) => { // eslint-disable-line @typescript-eslint/naming-convention
Expand All @@ -49,7 +50,8 @@ export default class ChapterReader {
}).then(r => {
response.status = r.status
response.content_type = r.headers['content-type']?.split(';')[0]
response.payload = this.#deserialize_payload(r.data, response.content_type)
const payload = this.#deserialize_payload(r.data, response.content_type)
if (payload !== undefined) response.payload = payload
this.logger.info(`<= ${r.status} (${r.headers['content-type']}) | ${to_json(response.payload)}`)
}).catch(e => {
if (e.response == null) {
Expand All @@ -59,7 +61,7 @@ export default class ChapterReader {
response.status = e.response.status
response.content_type = e.response.headers['content-type']?.split(';')[0]
const payload = this.#deserialize_payload(e.response.data, response.content_type)
response.payload = payload?.error
if (payload !== undefined) response.payload = payload.error
response.message = payload.error?.reason ?? e.response.statusText
response.error = e

Expand All @@ -68,6 +70,19 @@ export default class ChapterReader {
return response as ActualResponse
}

#serialize_headers(headers?: Record<string, any>, content_type?: string): [Record<string, any> | undefined, string] {
headers = _.cloneDeep(headers)
content_type = content_type ?? APPLICATION_JSON
if (!headers) return [headers, content_type]
_.forEach(headers, (v, k) => {
if (k.toLowerCase() == 'content-type') {
content_type = v.toString()
if (headers) delete headers[k]
}
})
return [headers, content_type]
}

#serialize_payload(payload: any, content_type: string): any {
if (payload === undefined) return undefined
switch (content_type) {
Expand Down
10 changes: 9 additions & 1 deletion tools/src/tester/types/story.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ export type SupplementalChapter = ChapterRequest & {
* via the `definition` "Parameter".
*/
export type Parameter = (string | number | boolean)[] | string | number | boolean;
/**
* This interface was referenced by `Story`'s JSON-Schema
* via the `definition` "Header".
*/
export type Header = string | number | boolean;
/**
* This interface was referenced by `Story`'s JSON-Schema
* via the `definition` "Payload".
Expand Down Expand Up @@ -114,7 +119,10 @@ export interface ChapterRequest {
*/
export interface Request {
content_type?: string;
payload: Payload;
headers?: {
[k: string]: Header;
};
payload?: Payload;
}
/**
* Describes output for a chapter.
Expand Down
75 changes: 68 additions & 7 deletions tools/tests/tester/ChapterReader.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ describe('ChapterReader', () => {
output: undefined
}, new StoryOutputs())

expect(result).toEqual({ status: 200, content_type: 'application/json', payload: undefined })
expect(result).toStrictEqual({ status: 200, content_type: 'application/json' })
expect(mocked_axios.request.mock.calls).toEqual([
[{
url: 'path',
Expand All @@ -71,15 +71,15 @@ describe('ChapterReader', () => {
output: undefined
}, new StoryOutputs())

expect(result).toEqual({ status: 200, content_type: 'application/json', payload: undefined })
expect(result).toEqual({ status: 200, content_type: 'application/json' })
expect(mocked_axios.request.mock.calls).toEqual([
[{
url: 'books/path',
method: 'GET',
data: undefined,
headers: { 'Content-Type': 'application/json' },
params: {},
paramsSerializer: expect.any(Function),
data: undefined
paramsSerializer: expect.any(Function)
}]
])
})
Expand All @@ -94,7 +94,7 @@ describe('ChapterReader', () => {
output: undefined
}, new StoryOutputs())

expect(result).toEqual({ status: 200, content_type: 'application/json', payload: undefined })
expect(result).toEqual({ status: 200, content_type: 'application/json' })
expect(mocked_axios.request.mock.calls).toEqual([
[{
url: '/path',
Expand All @@ -120,7 +120,7 @@ describe('ChapterReader', () => {
output: undefined
}, new StoryOutputs())

expect(result).toEqual({ status: 200, content_type: 'application/json', payload: undefined })
expect(result).toEqual({ status: 200, content_type: 'application/json' })
expect(mocked_axios.request.mock.calls).toEqual([
[{
url: 'path',
Expand All @@ -146,7 +146,7 @@ describe('ChapterReader', () => {
output: undefined
}, new StoryOutputs())

expect(result).toEqual({ status: 200, content_type: 'application/json', payload: undefined })
expect(result).toEqual({ status: 200, content_type: 'application/json' })
expect(mocked_axios.request.mock.calls).toEqual([
[{
url: 'path',
Expand All @@ -158,6 +158,67 @@ describe('ChapterReader', () => {
}]
])
})

it('sends headers', async () => {
const result = await reader.read({
id: 'id',
path: 'path',
method: 'GET',
request: {
headers: {
'string': 'bar',
'number': 1,
'boolean': true
},
},
output: undefined
}, new StoryOutputs())

expect(result).toStrictEqual({ status: 200, content_type: 'application/json' })
expect(mocked_axios.request.mock.calls).toStrictEqual([
[{
url: 'path',
method: 'GET',
data: undefined,
headers: {
'Content-Type': 'application/json',
'string': 'bar',
'number': 1,
'boolean': true
},
params: {},
paramsSerializer: expect.any(Function)
}]
])
})

it('overwrites case-insensitive content-type', async () => {
const result = await reader.read({
id: 'id',
path: 'path',
method: 'GET',
request: {
headers: {
'content-type': 'application/overwritten'
},
},
output: undefined
}, new StoryOutputs())

expect(result).toStrictEqual({ status: 200, content_type: 'application/json' })
expect(mocked_axios.request.mock.calls).toStrictEqual([
[{
url: 'path',
method: 'GET',
data: undefined,
headers: {
'Content-Type': 'application/overwritten',
},
params: {},
paramsSerializer: expect.any(Function)
}]
])
})
})

describe('deserialize_payload', () => {
Expand Down
17 changes: 17 additions & 0 deletions tools/tests/tester/fixtures/evals/passed.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,23 @@ chapters:
result: PASSED
output_values:
result: SKIPPED
- title: This GET /_cat chapter with a header should pass.
overall:
result: PASSED
path: GET /_cat
request:
parameters: {}
request:
result: PASSED
response:
status:
result: PASSED
payload_body:
result: PASSED
payload_schema:
result: PASSED
output_values:
result: SKIPPED
- title: This GET /_cat/health chapter returns application/json and should pass.
overall:
result: PASSED
Expand Down
9 changes: 9 additions & 0 deletions tools/tests/tester/fixtures/stories/passed.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,15 @@ chapters:
response:
status: 200
content_type: text/plain
- synopsis: This GET /_cat chapter with a header should pass.
path: /_cat
method: GET
request:
headers:
User-Agent: OpenSearch API Spec/1.0
response:
status: 200
content_type: text/plain
- synopsis: This GET /_cat/health chapter returns application/json and should pass.
path: /_cat/health
parameters:
Expand Down

0 comments on commit c42291a

Please sign in to comment.