From 5fff5236708cfb00924fd7f3f42a53fbf38f702e Mon Sep 17 00:00:00 2001 From: olav Date: Fri, 24 Jun 2022 15:29:27 +0200 Subject: [PATCH] refactor: add schemas to client application registration (#1746) --- src/lib/openapi/index.ts | 2 + .../client-application-schema.test.ts.snap | 18 ++++ .../spec/client-application-schema.test.ts | 92 +++++++++++++++++++ .../openapi/spec/client-application-schema.ts | 41 +++++++++ src/lib/routes/client-api/register.ts | 41 +++++++-- src/lib/types/model.ts | 2 +- .../__snapshots__/openapi.e2e.test.ts.snap | 67 ++++++++++++++ 7 files changed, 253 insertions(+), 10 deletions(-) create mode 100644 src/lib/openapi/spec/__snapshots__/client-application-schema.test.ts.snap create mode 100644 src/lib/openapi/spec/client-application-schema.test.ts create mode 100644 src/lib/openapi/spec/client-application-schema.ts diff --git a/src/lib/openapi/index.ts b/src/lib/openapi/index.ts index c5824086b9..d08ee0e2a1 100644 --- a/src/lib/openapi/index.ts +++ b/src/lib/openapi/index.ts @@ -79,6 +79,7 @@ import { emailSchema } from './spec/email-schema'; import { strategySchema } from './spec/strategy-schema'; import { strategiesSchema } from './spec/strategies-schema'; import { upsertStrategySchema } from './spec/upsert-strategy-schema'; +import { clientApplicationSchema } from './spec/client-application-schema'; // All schemas in `openapi/spec` should be listed here. export const schemas = { @@ -90,6 +91,7 @@ export const schemas = { apiTokensSchema, applicationSchema, applicationsSchema, + clientApplicationSchema, cloneFeatureSchema, changePasswordSchema, constraintSchema, diff --git a/src/lib/openapi/spec/__snapshots__/client-application-schema.test.ts.snap b/src/lib/openapi/spec/__snapshots__/client-application-schema.test.ts.snap new file mode 100644 index 0000000000..84869df44b --- /dev/null +++ b/src/lib/openapi/spec/__snapshots__/client-application-schema.test.ts.snap @@ -0,0 +1,18 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`clientApplicationSchema no fields 1`] = ` +Object { + "errors": Array [ + Object { + "instancePath": "", + "keyword": "required", + "message": "must have required property 'appName'", + "params": Object { + "missingProperty": "appName", + }, + "schemaPath": "#/required", + }, + ], + "schema": "#/components/schemas/clientApplicationSchema", +} +`; diff --git a/src/lib/openapi/spec/client-application-schema.test.ts b/src/lib/openapi/spec/client-application-schema.test.ts new file mode 100644 index 0000000000..ca55ed3411 --- /dev/null +++ b/src/lib/openapi/spec/client-application-schema.test.ts @@ -0,0 +1,92 @@ +import { validateSchema } from '../validate'; +import { ClientApplicationSchema } from './client-application-schema'; + +test('clientApplicationSchema no fields', () => { + expect( + validateSchema('#/components/schemas/clientApplicationSchema', {}), + ).toMatchSnapshot(); +}); + +test('clientApplicationSchema required fields', () => { + const data: ClientApplicationSchema = { + appName: '', + interval: 0, + started: 0, + strategies: [''], + }; + + expect( + validateSchema('#/components/schemas/clientApplicationSchema', data), + ).toBeUndefined(); +}); + +test('clientApplicationSchema all fields', () => { + const data: ClientApplicationSchema = { + appName: '', + instanceId: '', + sdkVersion: '', + environment: '', + interval: 0, + started: 0, + strategies: [''], + }; + + expect( + validateSchema('#/components/schemas/clientApplicationSchema', data), + ).toBeUndefined(); +}); + +test('clientApplicationSchema go-sdk request', () => { + const json = `{ + "appName": "x", + "instanceId": "y", + "sdkVersion": "unleash-client-go:3.3.1", + "strategies": [ + "default", + "applicationHostname", + "gradualRolloutRandom", + "gradualRolloutSessionId", + "gradualRolloutUserId", + "remoteAddress", + "userWithId", + "flexibleRollout" + ], + "started": "2022-06-24T09:59:12.822607943+02:00", + "interval": 1 + }`; + + expect( + validateSchema( + '#/components/schemas/clientApplicationSchema', + JSON.parse(json), + ), + ).toBeUndefined(); +}); + +test('clientApplicationSchema node-sdk request', () => { + const json = `{ + "appName": "unleash-test-node-appName2", + "instanceId": "unleash-test-node-instanceId", + "sdkVersion": "unleash-client-node:3.11.0", + "strategies": [ + "p", + "default", + "applicationHostname", + "gradualRolloutRandom", + "gradualRolloutUserId", + "gradualRolloutSessionId", + "userWithId", + "remoteAddress", + "flexibleRollout" + ], + "started": "2022-06-24T09:54:03.649Z", + "interval": 1000 + }`; + + expect( + validateSchema( + '#/components/schemas/clientApplicationSchema', + JSON.parse(json), + ), + ).toBeUndefined(); +}); diff --git a/src/lib/openapi/spec/client-application-schema.ts b/src/lib/openapi/spec/client-application-schema.ts new file mode 100644 index 0000000000..ad6aee6bc1 --- /dev/null +++ b/src/lib/openapi/spec/client-application-schema.ts @@ -0,0 +1,41 @@ +import { FromSchema } from 'json-schema-to-ts'; + +export const clientApplicationSchema = { + $id: '#/components/schemas/clientApplicationSchema', + type: 'object', + required: ['appName', 'interval', 'started', 'strategies'], + properties: { + appName: { + type: 'string', + }, + instanceId: { + type: 'string', + }, + sdkVersion: { + type: 'string', + }, + environment: { + type: 'string', + }, + interval: { + type: 'number', + }, + started: { + oneOf: [ + { type: 'string', format: 'date-time' }, + { type: 'number' }, + ], + }, + strategies: { + type: 'array', + items: { + type: 'string', + }, + }, + }, + components: {}, +} as const; + +export type ClientApplicationSchema = FromSchema< + typeof clientApplicationSchema +>; diff --git a/src/lib/routes/client-api/register.ts b/src/lib/routes/client-api/register.ts index 792e659a7f..62bc57ae5f 100644 --- a/src/lib/routes/client-api/register.ts +++ b/src/lib/routes/client-api/register.ts @@ -9,27 +9,47 @@ import { IClientApp } from '../../types/model'; import ApiUser from '../../types/api-user'; import { ALL } from '../../types/models/api-token'; import { NONE } from '../../types/permissions'; +import { OpenApiService } from '../../services/openapi-service'; +import { emptyResponse } from '../../openapi/spec/empty-response'; +import { createRequestSchema } from '../../openapi'; +import { ClientApplicationSchema } from '../../openapi/spec/client-application-schema'; export default class RegisterController extends Controller { logger: Logger; - metrics: ClientInstanceService; + clientInstanceService: ClientInstanceService; + + openApiService: OpenApiService; constructor( { clientInstanceService, - }: Pick, + openApiService, + }: Pick, config: IUnleashConfig, ) { super(config); this.logger = config.getLogger('/api/client/register'); - this.metrics = clientInstanceService; + this.clientInstanceService = clientInstanceService; + this.openApiService = openApiService; - // NONE permission is not optimal here in terms of readability. - this.post('/', this.handleRegister, NONE); + this.route({ + method: 'post', + path: '', + handler: this.registerClientApplication, + permission: NONE, + middleware: [ + openApiService.validPath({ + tags: ['client'], + operationId: 'registerClientApplication', + requestBody: createRequestSchema('clientApplicationSchema'), + responses: { 202: emptyResponse }, + }), + ], + }); } - private resolveEnvironment(user: User, data: IClientApp) { + private static resolveEnvironment(user: User, data: Partial) { if (user instanceof ApiUser) { if (user.environment !== ALL) { return user.environment; @@ -40,10 +60,13 @@ export default class RegisterController extends Controller { return 'default'; } - async handleRegister(req: IAuthRequest, res: Response): Promise { + async registerClientApplication( + req: IAuthRequest, + res: Response, + ): Promise { const { body: data, ip: clientIp, user } = req; - data.environment = this.resolveEnvironment(user, data); - await this.metrics.registerClient(data, clientIp); + data.environment = RegisterController.resolveEnvironment(user, data); + await this.clientInstanceService.registerClient(data, clientIp); return res.status(202).end(); } } diff --git a/src/lib/types/model.ts b/src/lib/types/model.ts index c934dfbadd..596109d3d1 100644 --- a/src/lib/types/model.ts +++ b/src/lib/types/model.ts @@ -280,7 +280,7 @@ export interface IClientApp { strategies?: string[] | Record[]; bucket?: any; count?: number; - started?: number | Date; + started?: string | number | Date; interval?: number; icon?: string; description?: string; diff --git a/src/test/e2e/api/openapi/__snapshots__/openapi.e2e.test.ts.snap b/src/test/e2e/api/openapi/__snapshots__/openapi.e2e.test.ts.snap index e1f5bf6709..f4459bb11f 100644 --- a/src/test/e2e/api/openapi/__snapshots__/openapi.e2e.test.ts.snap +++ b/src/test/e2e/api/openapi/__snapshots__/openapi.e2e.test.ts.snap @@ -309,6 +309,49 @@ Object { ], "type": "object", }, + "clientApplicationSchema": Object { + "properties": Object { + "appName": Object { + "type": "string", + }, + "environment": Object { + "type": "string", + }, + "instanceId": Object { + "type": "string", + }, + "interval": Object { + "type": "number", + }, + "sdkVersion": Object { + "type": "string", + }, + "started": Object { + "oneOf": Array [ + Object { + "format": "date-time", + "type": "string", + }, + Object { + "type": "number", + }, + ], + }, + "strategies": Object { + "items": Object { + "type": "string", + }, + "type": "array", + }, + }, + "required": Array [ + "appName", + "interval", + "started", + "strategies", + ], + "type": "object", + }, "cloneFeatureSchema": Object { "properties": Object { "name": Object { @@ -4908,6 +4951,30 @@ Object { ], }, }, + "/api/client/register": Object { + "post": Object { + "operationId": "registerClientApplication", + "requestBody": Object { + "content": Object { + "application/json": Object { + "schema": Object { + "$ref": "#/components/schemas/clientApplicationSchema", + }, + }, + }, + "description": "clientApplicationSchema", + "required": true, + }, + "responses": Object { + "202": Object { + "description": "emptyResponse", + }, + }, + "tags": Array [ + "client", + ], + }, + }, "/auth/reset/password": Object { "post": Object { "operationId": "changePassword",