Skip to content

Commit

Permalink
feat(cli): include chaos option in Spectra config
Browse files Browse the repository at this point in the history
  • Loading branch information
P0lip committed Nov 27, 2024
1 parent 4a8ee14 commit f7e9e2e
Show file tree
Hide file tree
Showing 5 changed files with 272 additions and 3 deletions.
78 changes: 78 additions & 0 deletions packages/cli/src/util/__tests__/config.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { assertValidConfig, type Config } from '../config';

describe('assertValidConfig', () => {
it('should not throw an error for a valid config', () => {
const validConfig: Config = {
ignoreExamples: true,
dynamic: false,
jsonSchemaFakerFillProperties: true,
chaos: {
enabled: true,
rate: 50,
codes: [500, 502],
},
};

expect(assertValidConfig.bind(null, validConfig)).not.toThrow();
});

it('should throw an error for an invalid config', () => {
const invalidConfig = {
ignoreExamples: 'true', // invalid type
dynamic: false,
jsonSchemaFakerFillProperties: true,
chaos: {
enabled: true,
rate: 50,
codes: [500, 502],
},
};

expect(assertValidConfig.bind(null, invalidConfig)).toThrow();
});

it('should throw an error for a config with missing required properties', () => {
const invalidConfig = {
dynamic: false,
jsonSchemaFakerFillProperties: true,
chaos: {
enabled: true,
rate: 50,
},
};

expect(assertValidConfig.bind(null, invalidConfig)).toThrow();
});

it('should throw an error for a config with additional properties', () => {
const invalidConfig = {
ignoreExamples: true,
dynamic: false,
jsonSchemaFakerFillProperties: true,
extraProperty: 'not allowed',
};

expect(assertValidConfig.bind(null, invalidConfig)).toThrow();
});

it('should throw an error for a config with enabled chaos and no/empty codes', () => {
expect(
assertValidConfig.bind(null, {
chaos: {
enabled: true,
rate: 50,
},
})
).toThrow();

expect(
assertValidConfig.bind(null, {
chaos: {
enabled: true,
rate: 50,
codes: [],
},
})
).toThrow();
});
});
69 changes: 66 additions & 3 deletions packages/cli/src/util/config.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,29 @@
import { promises as fs } from 'fs';
import * as Ajv from 'ajv';
import * as Ajv from 'ajv/dist/2020';
import type * as pino from 'pino';
import type { CreateMockServerOptions } from './createServer';
import type { Observable } from './observable';

const ajv = new Ajv.Ajv({
export type Config = {
ignoreExamples?: boolean;
dynamic?: boolean;
jsonSchemaFakerFillProperties?: boolean;
chaos?: {
enabled?: boolean;
rate?: number;
codes?: number[];
} & (
| {
enabled: true;
codes: [number, ...number[]];
}
| {
enabled?: false;
}
);
};

const ajv = new Ajv.Ajv2020({
strict: true,
allErrors: true,
});
Expand All @@ -15,10 +34,54 @@ const validate = ajv.compile({
ignoreExamples: { type: 'boolean' },
dynamic: { type: 'boolean' },
jsonSchemaFakerFillProperties: { type: 'boolean' },
} satisfies Partial<Record<keyof CreateMockServerOptions, unknown>>,
chaos: {
type: 'object',
unevaluatedProperties: false,
properties: {
enabled: { type: 'boolean' },
rate: {
type: 'number',
minimum: 0,
maximum: 100,
},
codes: {
type: 'array',
items: {
type: 'integer',
minimum: 400,
exclusiveMaximum: 600,
},
uniqueItems: true,
},
},
oneOf: [
{
properties: {
enabled: { const: true },
codes: {
type: 'array',
minItems: 1,
},
},
required: ['enabled', 'codes'],
},
{
properties: {
enabled: { const: false },
},
},
],
},
} satisfies Partial<Record<keyof Config, unknown>>,
additionalProperties: false,
});

export function assertValidConfig(input: unknown): asserts input is Config {
if (!validate(input)) {
throw new Error(ajv.errorsText(validate.errors));
}
}

export async function safeApplyConfig(
logInstance: pino.Logger,
defaultConfig: CreateMockServerOptions,
Expand Down
36 changes: 36 additions & 0 deletions packages/cli/src/util/createServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ import * as chalk from 'chalk';
import * as cluster from 'cluster';
import * as E from 'fp-ts/Either';
import { pipe } from 'fp-ts/function';
import * as O from 'fp-ts/Option';
import * as pino from 'pino';
import * as signale from 'signale';
import * as split from 'split2';

import { PassThrough, Readable } from 'stream';
import { LOG_COLOR_MAP } from '../const/options';
import { CreatePrism } from './runner';
Expand Down Expand Up @@ -94,6 +96,27 @@ async function createPrismServerWithLogger(options: Observable<CreateBaseServerO
errors: options.errors,
upstreamProxy: undefined,
mock: {
get chaos(): IHttpConfig['mock']['chaos'] {
const rate = options.chaos?.rate ?? 10;
return pipe(
O.fromNullable(options.chaos),
O.fold(
() => ({ enabled: false, rate, codes: [] }),
chaos =>
chaos.enabled === true
? {
enabled: true,
rate,
codes: chaos.codes,
}
: {
enabled: false,
rate,
codes: chaos.codes ?? [],
}
)
);
},
get dynamic() {
return options.dynamic;
},
Expand Down Expand Up @@ -162,6 +185,19 @@ type CreateBaseServerOptions = {
config?: string;
dynamic: boolean;
cors: boolean;
chaos?: {
enabled?: boolean;
rate?: number;
codes?: number[];
} & (
| {
enabled: true;
codes: [number, ...number[]];
}
| {
enabled?: false;
}
);
host: string;
port: number;
document: string;
Expand Down
50 changes: 50 additions & 0 deletions test-harness/specs/config/chaos.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
====test====
Given I mock and specify a config with chaos enabled
When I send a request to an operation
Then the config should influence the response
====spec====
openapi: "3.1.0"
info:
version: "0.0"
title: Config Test
paths:
/pets/{petId}:
get:
description: Get a pet by ID
responses:
"200":
description: A pet
content:
application/json:
schema:
type: object
properties:
name:
const: Odie
"401":
description: Unauthorized
content:
application/json:
schema:
type: object
properties:
message:
const: Unauthorized

====config====
{
"chaos": {
"enabled": true,
"rate": 100,
"codes": [401]
}
}
====server====
mock -p 4010 --config ${config} ${document}
====command====
curl -i http://localhost:4010/pets/2
====expect====
HTTP/1.1 401 OK
content-type: application/json

{"message":"Unauthorized"}
42 changes: 42 additions & 0 deletions test-harness/specs/config/chaos_broken.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
====test====
Given I mock and specify a broken config
When I send a request to an operation
Then the config should not influence the response
====spec====
openapi: "3.1.0"
info:
version: "0.0"
title: Config Test
paths:
/pets/{petId}:
get:
description: Get a pet by ID
responses:
"200":
description: A pet
content:
application/json:
schema:
type: object
properties:
name:
type: string
"401":
description: Unauthorized
====config====
{
"chaos": {
"enabled": true,
"rate": 100,
"codes": []
}
}
====server====
mock -p 4010 --config ${config} ${document}
====command====
curl -i http://localhost:4010/pets/2
====expect====
HTTP/1.1 200 OK
content-type: application/json

{"name":"string"}

0 comments on commit f7e9e2e

Please sign in to comment.