Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(oas): support for vendor extensions for example values in swagger #225

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions packages/oas/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,18 @@ const requests = await oas2har(response.data);
console.log(requests);
```

Some specifications may incorporate example values for parameters provided in vendor extension fields, to include such examples in output use the `oas2har` function as follows:

```js
import schema from './swagger.json' assert { type: 'json' };
import { oas2har } from '@har-sdk/oas';

const requests = await oas2har(schema, { includeVendorExamples: true });
console.log(requests);
```

Notice the `includeVendorExamples` option affects Swagger specifications only.

## License

Copyright © 2023 [Bright Security](https://brightsec.com/).
Expand Down
73 changes: 54 additions & 19 deletions packages/oas/src/converter/Sampler.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
import { ConvertError } from '../errors';
import { sample, Schema } from '@har-sdk/openapi-sampler';
import { isOASV2 } from '../utils';
import {
Options,
sample,
Schema,
VendorExtensions
} from '@har-sdk/openapi-sampler';
import pointer from 'json-pointer';
import type { OpenAPI } from '@har-sdk/core';

export class Sampler {
constructor(private readonly options: Options) {}
pmstss marked this conversation as resolved.
Show resolved Hide resolved

public sampleParam(
param: OpenAPI.Parameter,
context: {
Expand All @@ -12,23 +20,15 @@ export class Sampler {
tokens: string[];
}
): any {
return this.sample(
'schema' in param
? {
...param.schema,
...(param.example !== undefined ? { example: param.example } : {})
}
: param,
{
spec: context.spec,
jsonPointer: pointer.compile([
...context.tokens,
'parameters',
context.idx.toString(),
...('schema' in param ? ['schema'] : [])
])
}
);
return this.sample(this.createParamSchema(param), {
spec: context.spec,
jsonPointer: pointer.compile([
...context.tokens,
'parameters',
context.idx.toString(),
...('schema' in param ? ['schema'] : [])
])
});
}

/**
Expand All @@ -43,9 +43,44 @@ export class Sampler {
}
): any | undefined {
try {
return sample(schema, { skipReadOnly: true, quiet: true }, context?.spec);
const { includeVendorExamples } = this.options;
const options = {
...this.options,
skipReadOnly: true,
quiet: true,
includeVendorExamples:
context?.spec && isOASV2(context?.spec)
? includeVendorExamples
: false
};

return sample(schema, options, context?.spec);
} catch (e) {
throw new ConvertError(e.message, context?.jsonPointer);
}
}

private createParamSchema(param: OpenAPI.Parameter): Schema {
if ('schema' in param) {
const { schema, example, ...rest } = param;

return {
...schema,
...(example !== undefined ? { example } : {}),
...this.extractVendorExamples(rest)
};
}

return param as Schema;
}

private extractVendorExamples(param: OpenAPI.Parameter) {
return [VendorExtensions.X_EXAMPLE, VendorExtensions.X_EXAMPLES].reduce(
(acc, prop) => ({
...acc,
...(param[prop] !== undefined ? { [prop]: param[prop] } : {})
}),
{}
);
}
}
7 changes: 5 additions & 2 deletions packages/oas/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,15 @@ import type { OpenAPI, Request } from '@har-sdk/core';

export * from './errors';

export const oas2har = (collection: OpenAPI.Document): Promise<Request[]> => {
export const oas2har = (
collection: OpenAPI.Document,
options: { includeVendorExamples?: boolean } = {}
): Promise<Request[]> => {
if (!collection) {
throw new TypeError('Please provide a valid OAS specification.');
}

const sampler = new Sampler();
const sampler = new Sampler(options);
const baseUrlParser = new BaseUrlParser(sampler);
const subConverterFactory = new SubConverterFactory(sampler);
const subConverterRegistry = new SubConverterRegistry(subConverterFactory);
Expand Down
146 changes: 123 additions & 23 deletions packages/oas/tests/DefaultConverter.spec.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,32 @@
import { ConvertError, oas2har } from '../src';
import yaml, { load } from 'js-yaml';
import { load } from 'js-yaml';
import { OpenAPIV2, Request } from '@har-sdk/core';
import { resolve } from 'path';
import { readFile, readFileSync } from 'fs';
import { readFile } from 'fs';
import { promisify } from 'util';

describe('DefaultConverter', () => {
const readFileAsync = promisify(readFile);

const loadFile = async (fileName: string) => {
const filePath = resolve(__dirname, fileName);

const content = await readFileAsync(filePath, 'utf-8');

return content.endsWith('.json') ? JSON.parse(content) : load(content);
};

const createFixture = async ({
inputFile,
expectedFile
}: {
inputFile: string;
expectedFile: string;
}) => ({
inputDoc: await loadFile(inputFile),
expectedDoc: await loadFile(expectedFile)
});

describe('convert', () => {
[
{
Expand Down Expand Up @@ -183,24 +204,14 @@ describe('DefaultConverter', () => {
}
].forEach(({ input: inputFile, expected: expectedFile, message }) => {
it(message, async () => {
const content = readFileSync(
resolve(__dirname, `./fixtures/${inputFile}`),
'utf-8'
);
const input = inputFile.endsWith('json')
? JSON.parse(content)
: load(content);

const expected = JSON.parse(
readFileSync(
resolve(__dirname, `./fixtures/${expectedFile}`),
'utf-8'
)
);
const { inputDoc, expectedDoc } = await createFixture({
inputFile: `./fixtures/${inputFile}`,
expectedFile: `./fixtures/${expectedFile}`
});

const result: Request[] = await oas2har(input as any);
const result: Request[] = await oas2har(inputDoc as any);

expect(result).toStrictEqual(expected);
expect(result).toStrictEqual(expectedDoc);
});
});

Expand Down Expand Up @@ -271,18 +282,107 @@ describe('DefaultConverter', () => {
/^convert-error-on-(.+)\.(.+)\.yaml$/,
'$1 ($2)'
)}`, async () => {
const content: string = await promisify(readFile)(
resolve(__dirname, `./fixtures/${input}`),
'utf8'
);
const inputDoc = await loadFile(`./fixtures/${input}`);

const result = oas2har(yaml.load(content) as OpenAPIV2.Document);
const result = oas2har(inputDoc as OpenAPIV2.Document);

await expect(result).rejects.toThrow(ConvertError);
await expect(result).rejects.toMatchObject({
jsonPointer: expected
});
})
);

it('should ignore x-example when includeVendorExamples is true (oas)', async () => {
// arrange
const { inputDoc, expectedDoc } = await createFixture({
inputFile: `./fixtures/x-example.oas.yaml`,
expectedFile: `./fixtures/x-example.oas.result.json`
});

// act
const result: Request[] = await oas2har(inputDoc as any, {
includeVendorExamples: true
});

// assert
expect(result).toStrictEqual(expectedDoc);
});

it.each(['path', 'query', 'header', 'form-data'])(
'should ignore %s parameter vendor example when vendor examples inclusion disabled (swagger)',
async (input) => {
// arrange
const { inputDoc, expectedDoc } = await createFixture({
inputFile: `./fixtures/x-example.${input}.swagger.yaml`,
expectedFile: `./fixtures/x-example.${input}.disabled.swagger.result.json`
});

// act
const result: Request[] = await oas2har(inputDoc as any, {
includeVendorExamples: false
});

// assert
expect(result).toStrictEqual(expectedDoc);
}
);

it.each(['path', 'query', 'header', 'form-data'])(
'should use %s parameter vendor example when vendor examples inclusion enabled (swagger)',
async (input) => {
// arrange
const { inputDoc, expectedDoc } = await createFixture({
inputFile: `./fixtures/x-example.${input}.swagger.yaml`,
expectedFile: `./fixtures/x-example.${input}.swagger.result.json`
});

// act
const result: Request[] = await oas2har(inputDoc as any, {
includeVendorExamples: true
});

// assert
expect(result).toStrictEqual(expectedDoc);
}
);

it.each(['schemathesis', 'redocly', 'api-connect', 'smartbear'])(
'should ignore body parameter vendor example when vendor examples inclusion disabled (swagger, %s)',
async (input) => {
// arrange
const { inputDoc, expectedDoc } = await createFixture({
inputFile: `./fixtures/x-example.body.${input}.swagger.yaml`,
expectedFile: `./fixtures/x-example.body.disabled.swagger.result.json`
});

// act
const result: Request[] = await oas2har(inputDoc as any, {
includeVendorExamples: false
});

// assert
expect(result).toStrictEqual(expectedDoc);
}
);

it.each(['schemathesis', 'redocly', 'api-connect', 'smartbear'])(
'should use body parameter vendor example when vendor examples inclusion enabled (swagger, %s)',
async (input) => {
// arrange
const { inputDoc, expectedDoc } = await createFixture({
inputFile: `./fixtures/x-example.body.${input}.swagger.yaml`,
expectedFile: `./fixtures/x-example.body.${input}.swagger.result.json`
});

// act
const result: Request[] = await oas2har(inputDoc as any, {
includeVendorExamples: true
});

// assert
expect(result).toStrictEqual(expectedDoc);
}
);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
[
{
"queryString": [],
"cookies": [],
"method": "POST",
"headers": [
{
"value": "application/json",
"name": "content-type"
},
{
"value": "application/json",
"name": "accept"
}
],
"httpVersion": "HTTP/1.1",
"headersSize": 0,
"bodySize": 0,
"postData": {
"mimeType": "application/json",
"text": "[{\"name\":\"x_example_name\",\"age\":30}]"
},
"url": "https://example.com/sample"
},
{
"queryString": [],
"cookies": [],
"method": "PUT",
"headers": [
{
"value": "application/json",
"name": "content-type"
},
{
"value": "application/json",
"name": "accept"
}
],
"httpVersion": "HTTP/1.1",
"headersSize": 0,
"bodySize": 0,
"postData": {
"mimeType": "application/json",
"text": "{\"name\":\"x_example_name\",\"age\":30}"
},
"url": "https://example.com/sample/123"
}
]
Loading
Loading