1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-05-03 01:18:43 +02:00

fix: Remove idNumberMiddleware and change to use parameters in validPath method instead (#8734)

## About the changes

- Remove `idNumberMiddleware` method and change to use `parameters`
field in `openApiService.validPath` method for the flexibility.
- Remove unnecessary `Number` type converting method and change them to
use `<{id: number}>` to specify the type.

### Reference

The changed response looks like the one below.

```JSON
{
   "id":"8174a692-7427-4d35-b7b9-6543b9d3db6e",
   "name":"BadDataError",
   "message":"Request validation failed: your request body or params contain invalid data. Refer to the `details` list for more information.",
   "details":[
      {
         "message":"The `/params/id` property must be integer. You sent undefined.",
         "path":"/params/id"
      }
   ]
}
```

I think it might be better to customize the error response, especially
`"You sent undefined."`, on another pull request if this one is
accepted. I prefer to separate jobs to divide the context and believe
that it helps reviewer easier to understand.
This commit is contained in:
weekwith.me 2024-11-18 23:08:07 +09:00 committed by GitHub
parent 18591dd017
commit 695873132e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 123 additions and 83 deletions

View File

@ -204,14 +204,8 @@ test('should create segments', async () => {
expect(segments.map((s) => s.name)).toEqual(['a', 'b', 'c']); expect(segments.map((s) => s.name)).toEqual(['a', 'b', 'c']);
}); });
test('should return 400 for non numeeric segment ID', async () => { test('should return 400 for non numeric segment ID', async () => {
const { body } = await app.request await app.request.get(`${SEGMENTS_BASE_PATH}/stringName`).expect(400);
.get(`${SEGMENTS_BASE_PATH}/stringName`)
.expect(400);
expect(body).toMatchObject({
message:
'Request validation failed: your request body or params contain invalid data: ID should be an integer',
});
}); });
test('should update segments', async () => { test('should update segments', async () => {

View File

@ -41,7 +41,6 @@ import {
import { anonymiseKeys, extractUserIdFromUser } from '../../util'; import { anonymiseKeys, extractUserIdFromUser } from '../../util';
import { BadDataError } from '../../error'; import { BadDataError } from '../../error';
import idNumberMiddleware from '../../middleware/id-number-middleware';
type IUpdateFeatureStrategySegmentsRequest = IAuthRequest< type IUpdateFeatureStrategySegmentsRequest = IAuthRequest<
{}, {},
@ -156,11 +155,21 @@ export class SegmentsController extends Controller {
summary: 'Get strategies that reference segment', summary: 'Get strategies that reference segment',
description: description:
'Retrieve all strategies that reference the specified segment.', 'Retrieve all strategies that reference the specified segment.',
parameters: [
{
name: 'id',
in: 'path',
required: true,
schema: {
type: 'integer',
},
description: 'a segment id',
},
],
responses: { responses: {
200: createResponseSchema('segmentStrategiesSchema'), 200: createResponseSchema('segmentStrategiesSchema'),
}, },
}), }),
idNumberMiddleware(),
], ],
}); });
@ -175,6 +184,17 @@ export class SegmentsController extends Controller {
summary: 'Deletes a segment by id', summary: 'Deletes a segment by id',
description: description:
'Deletes a segment by its id, if not found returns a 409 error', 'Deletes a segment by its id, if not found returns a 409 error',
parameters: [
{
name: 'id',
in: 'path',
required: true,
schema: {
type: 'integer',
},
description: 'a segment id',
},
],
tags: ['Segments'], tags: ['Segments'],
operationId: 'removeSegment', operationId: 'removeSegment',
responses: { responses: {
@ -182,7 +202,6 @@ export class SegmentsController extends Controller {
...getStandardResponses(401, 403, 409), ...getStandardResponses(401, 403, 409),
}, },
}), }),
idNumberMiddleware(),
], ],
}); });
@ -198,13 +217,23 @@ export class SegmentsController extends Controller {
'Updates the content of the segment with the provided payload. Requires `name` and `constraints` to be present. If `project` is not present, it will be set to `null`. Any other fields not specified will be left untouched.', 'Updates the content of the segment with the provided payload. Requires `name` and `constraints` to be present. If `project` is not present, it will be set to `null`. Any other fields not specified will be left untouched.',
tags: ['Segments'], tags: ['Segments'],
operationId: 'updateSegment', operationId: 'updateSegment',
parameters: [
{
name: 'id',
in: 'path',
required: true,
schema: {
type: 'integer',
},
description: 'a segment id',
},
],
requestBody: createRequestSchema('upsertSegmentSchema'), requestBody: createRequestSchema('upsertSegmentSchema'),
responses: { responses: {
204: emptyResponse, 204: emptyResponse,
...getStandardResponses(400, 401, 403, 409, 415), ...getStandardResponses(400, 401, 403, 409, 415),
}, },
}), }),
idNumberMiddleware(),
], ],
}); });
@ -219,12 +248,22 @@ export class SegmentsController extends Controller {
description: 'Retrieves a segment based on its ID.', description: 'Retrieves a segment based on its ID.',
tags: ['Segments'], tags: ['Segments'],
operationId: 'getSegment', operationId: 'getSegment',
parameters: [
{
name: 'id',
in: 'path',
required: true,
schema: {
type: 'integer',
},
description: 'a segment id',
},
],
responses: { responses: {
200: createResponseSchema('adminSegmentSchema'), 200: createResponseSchema('adminSegmentSchema'),
...getStandardResponses(404), ...getStandardResponses(404),
}, },
}), }),
idNumberMiddleware(),
], ],
}); });
@ -353,7 +392,7 @@ export class SegmentsController extends Controller {
req: IAuthRequest<{ id: number }>, req: IAuthRequest<{ id: number }>,
res: Response<SegmentStrategiesSchema>, res: Response<SegmentStrategiesSchema>,
): Promise<void> { ): Promise<void> {
const id = Number(req.params.id); const id = req.params.id;
const { user } = req; const { user } = req;
const strategies = await this.segmentService.getVisibleStrategies( const strategies = await this.segmentService.getVisibleStrategies(
id, id,
@ -386,10 +425,10 @@ export class SegmentsController extends Controller {
} }
async removeSegment( async removeSegment(
req: IAuthRequest<{ id: string }>, req: IAuthRequest<{ id: number }>,
res: Response, res: Response,
): Promise<void> { ): Promise<void> {
const id = Number(req.params.id); const id = req.params.id;
let segmentIsInUse = false; let segmentIsInUse = false;
segmentIsInUse = await this.segmentService.isInUse(id); segmentIsInUse = await this.segmentService.isInUse(id);
@ -403,10 +442,10 @@ export class SegmentsController extends Controller {
} }
async updateSegment( async updateSegment(
req: IAuthRequest<{ id: string }>, req: IAuthRequest<{ id: number }>,
res: Response, res: Response,
): Promise<void> { ): Promise<void> {
const id = Number(req.params.id); const id = req.params.id;
const updateRequest: UpsertSegmentSchema = { const updateRequest: UpsertSegmentSchema = {
name: req.body.name, name: req.body.name,
description: req.body.description, description: req.body.description,
@ -423,10 +462,10 @@ export class SegmentsController extends Controller {
} }
async getSegment( async getSegment(
req: Request<{ id: string }>, req: Request<{ id: number }>,
res: Response, res: Response,
): Promise<void> { ): Promise<void> {
const id = Number(req.params.id); const id = req.params.id;
const segment = await this.segmentService.get(id); const segment = await this.segmentService.get(id);
if (this.flagResolver.isEnabled('anonymiseEventLog')) { if (this.flagResolver.isEnabled('anonymiseEventLog')) {
res.json(anonymiseKeys(segment, ['createdBy'])); res.json(anonymiseKeys(segment, ['createdBy']));

View File

@ -1,32 +0,0 @@
import express from 'express';
import supertest from 'supertest';
import idNumberMiddleware from './id-number-middleware';
describe('idNumberMiddleware', () => {
it('should pass when id is a valid integer', async () => {
const app = express();
app.use('/:id', idNumberMiddleware());
app.get('/:id', (req, res) => {
res.status(200).send('Valid ID');
});
await supertest(app)
.get('/123')
.expect(200)
.expect((res) => {
expect(res.text).toBe('Valid ID');
});
});
it('should throw BadDataError when id is not a valid integer', async () => {
const app = express();
app.use('/:id', idNumberMiddleware());
app.get('/:id', (req, res) => {
res.status(200).send('This should not be executed');
});
const { body } = await supertest(app).get('/abc').expect(400);
expect(body).toMatchObject({
details: [{ message: 'ID should be an integer' }],
});
});
});

View File

@ -1,16 +0,0 @@
import { BadDataError } from '../error';
const idNumberMiddleware = (): any => {
return async (req, res, next) => {
const { id } = req.params;
if (!Number.isInteger(Number(id))) {
res.status(400).send(
new BadDataError('ID should be an integer').toJSON(),
);
return;
}
next();
};
};
export default idNumberMiddleware;

View File

@ -55,7 +55,6 @@ import {
type CreateUserResponseSchema, type CreateUserResponseSchema,
} from '../../openapi/spec/create-user-response-schema'; } from '../../openapi/spec/create-user-response-schema';
import type { IRoleWithPermissions } from '../../types/stores/access-store'; import type { IRoleWithPermissions } from '../../types/stores/access-store';
import idNumberMiddleware from '../../middleware/id-number-middleware';
export default class UserAdminController extends Controller { export default class UserAdminController extends Controller {
private flagResolver: IFlagResolver; private flagResolver: IFlagResolver;
@ -144,6 +143,17 @@ export default class UserAdminController extends Controller {
operationId: 'changeUserPassword', operationId: 'changeUserPassword',
summary: 'Change password for a user', summary: 'Change password for a user',
description: 'Change password for a user as an admin', description: 'Change password for a user as an admin',
parameters: [
{
name: 'id',
in: 'path',
required: true,
schema: {
type: 'integer',
},
description: 'a user id',
},
],
requestBody: createRequestSchema('passwordSchema'), requestBody: createRequestSchema('passwordSchema'),
responses: { responses: {
200: emptyResponse, 200: emptyResponse,
@ -256,6 +266,15 @@ export default class UserAdminController extends Controller {
description: description:
'Gets a list of permissions for a user, additional project and environment can be specified.', 'Gets a list of permissions for a user, additional project and environment can be specified.',
parameters: [ parameters: [
{
name: 'id',
in: 'path',
required: true,
schema: {
type: 'integer',
},
description: 'a user id',
},
{ {
name: 'project', name: 'project',
in: 'query', in: 'query',
@ -341,12 +360,22 @@ export default class UserAdminController extends Controller {
operationId: 'getUser', operationId: 'getUser',
summary: 'Get user', summary: 'Get user',
description: 'Will return a single user by id', description: 'Will return a single user by id',
parameters: [
{
name: 'id',
in: 'path',
required: true,
schema: {
type: 'integer',
},
description: 'a user id',
},
],
responses: { responses: {
200: createResponseSchema('userSchema'), 200: createResponseSchema('userSchema'),
...getStandardResponses(400, 401, 404), ...getStandardResponses(400, 401, 404),
}, },
}), }),
idNumberMiddleware(),
], ],
}); });
@ -367,9 +396,11 @@ export default class UserAdminController extends Controller {
{ {
name: 'id', name: 'id',
in: 'path', in: 'path',
required: true,
schema: { schema: {
type: 'integer', type: 'integer',
}, },
description: 'a user id',
}, },
], ],
responses: { responses: {
@ -377,7 +408,6 @@ export default class UserAdminController extends Controller {
...getStandardResponses(400, 401, 403, 404), ...getStandardResponses(400, 401, 403, 404),
}, },
}), }),
idNumberMiddleware(),
], ],
}); });
@ -393,12 +423,22 @@ export default class UserAdminController extends Controller {
operationId: 'deleteUser', operationId: 'deleteUser',
summary: 'Delete a user', summary: 'Delete a user',
description: 'Deletes the user with the given userId', description: 'Deletes the user with the given userId',
parameters: [
{
name: 'id',
in: 'path',
required: true,
schema: {
type: 'integer',
},
description: 'a user id',
},
],
responses: { responses: {
200: emptyResponse, 200: emptyResponse,
...getStandardResponses(401, 403, 404), ...getStandardResponses(401, 403, 404),
}, },
}), }),
idNumberMiddleware(),
], ],
}); });
} }
@ -507,9 +547,12 @@ export default class UserAdminController extends Controller {
); );
} }
async getUser(req: Request, res: Response<UserSchema>): Promise<void> { async getUser(
req: Request<{ id: number }>,
res: Response<UserSchema>,
): Promise<void> {
const { id } = req.params; const { id } = req.params;
const user = await this.userService.getUser(Number(id)); const user = await this.userService.getUser(id);
this.openApiService.respondWithValidation( this.openApiService.respondWithValidation(
200, 200,
@ -568,7 +611,7 @@ export default class UserAdminController extends Controller {
async updateUser( async updateUser(
req: IAuthRequest< req: IAuthRequest<
{ id: string }, { id: number },
CreateUserResponseSchema, CreateUserResponseSchema,
UpdateUserSchema UpdateUserSchema
>, >,
@ -578,14 +621,14 @@ export default class UserAdminController extends Controller {
const { id } = params; const { id } = params;
const { name, email, rootRole } = body; const { name, email, rootRole } = body;
await this.throwIfScimUser({ id: Number(id) }); await this.throwIfScimUser({ id });
const normalizedRootRole = Number.isInteger(Number(rootRole)) const normalizedRootRole = Number.isInteger(Number(rootRole))
? Number(rootRole) ? Number(rootRole)
: (rootRole as RoleName); : (rootRole as RoleName);
const updateUser = await this.userService.updateUser( const updateUser = await this.userService.updateUser(
{ {
id: Number(id), id,
name, name,
email, email,
rootRole: normalizedRootRole, rootRole: normalizedRootRole,
@ -604,11 +647,14 @@ export default class UserAdminController extends Controller {
); );
} }
async deleteUser(req: IAuthRequest, res: Response): Promise<void> { async deleteUser(
req: IAuthRequest<{ id: number }>,
res: Response,
): Promise<void> {
const { user, params } = req; const { user, params } = req;
const { id } = params; const { id } = params;
await this.throwIfScimUser({ id: Number(id) }); await this.throwIfScimUser({ id });
await this.userService.deleteUser(+id, req.audit); await this.userService.deleteUser(+id, req.audit);
res.status(200).send(); res.status(200).send();
@ -625,13 +671,13 @@ export default class UserAdminController extends Controller {
} }
async changeUserPassword( async changeUserPassword(
req: IAuthRequest<{ id: string }, unknown, PasswordSchema>, req: IAuthRequest<{ id: number }, unknown, PasswordSchema>,
res: Response, res: Response,
): Promise<void> { ): Promise<void> {
const { id } = req.params; const { id } = req.params;
const { password } = req.body; const { password } = req.body;
await this.throwIfScimUser({ id: Number(id) }); await this.throwIfScimUser({ id });
await this.userService.changePassword(+id, password); await this.userService.changePassword(+id, password);
res.status(200).send(); res.status(200).send();

View File

@ -28,7 +28,6 @@ import {
createPatSchema, createPatSchema,
} from '../../../openapi/spec/create-pat-schema'; } from '../../../openapi/spec/create-pat-schema';
import { ForbiddenError, NotFoundError } from '../../../error'; import { ForbiddenError, NotFoundError } from '../../../error';
import idNumberMiddleware from '../../../middleware/id-number-middleware';
export default class PatController extends Controller { export default class PatController extends Controller {
private patService: PatService; private patService: PatService;
@ -107,12 +106,22 @@ export default class PatController extends Controller {
'Delete a personal access token (PAT) for the current user.', 'Delete a personal access token (PAT) for the current user.',
description: description:
'Deletes a [personal access token](https://docs.getunleash.io/how-to/how-to-create-personal-access-tokens) (PAT) belonging to the current user.', 'Deletes a [personal access token](https://docs.getunleash.io/how-to/how-to-create-personal-access-tokens) (PAT) belonging to the current user.',
parameters: [
{
name: 'id',
in: 'path',
required: true,
schema: {
type: 'integer',
},
description: 'a personal access token id',
},
],
responses: { responses: {
200: emptyResponse, 200: emptyResponse,
...getStandardResponses(401, 403, 404), ...getStandardResponses(401, 403, 404),
}, },
}), }),
idNumberMiddleware(),
], ],
}); });
} }