implement PUT endpoint to update a token

This commit is contained in:
Dieter Stinglhamber 2021-09-06 13:30:52 +02:00
parent ca668c78e8
commit f7bd99cb74
7 changed files with 300 additions and 3 deletions

View File

@ -198,4 +198,89 @@ describe('API Token Controller', () => {
expect(notFound).toHaveBeenCalledWith('API Token not found');
});
});
describe('Update API Token', () => {
const body = {
name: 'api-token_tests-name',
description: 'api-token_tests-description',
type: 'read-only',
};
const id = 1;
test('Fails if the name is already taken', async () => {
const getById = jest.fn(() => ({ id, ...body }));
const exists = jest.fn(() => true);
const badRequest = jest.fn();
const ctx = createContext({ body, params: { id } }, { badRequest });
global.strapi = {
admin: {
services: {
'api-token': {
exists,
getById,
},
},
},
};
await apiTokenController.update(ctx);
expect(exists).toHaveBeenCalledWith({ name: body.name });
expect(badRequest).toHaveBeenCalledWith('Name already taken');
});
test('Fails if the token does not exist', async () => {
const getById = jest.fn(() => null);
const notFound = jest.fn();
const ctx = createContext({ body, params: { id } }, { notFound });
global.strapi = {
admin: {
services: {
'api-token': {
getById,
},
},
},
};
await apiTokenController.update(ctx);
expect(getById).toHaveBeenCalledWith(id);
expect(notFound).toHaveBeenCalledWith('API token not found');
});
test('Updates API Token Successfully', async () => {
const update = jest.fn().mockResolvedValue(body);
const getById = jest.fn(() => ({ id, ...body }));
const exists = jest.fn(() => false);
const badRequest = jest.fn();
const notFound = jest.fn();
const send = jest.fn();
const ctx = createContext({ body, params: { id } }, { badRequest, notFound, send });
global.strapi = {
admin: {
services: {
'api-token': {
getById,
exists,
update,
},
},
},
};
await apiTokenController.update(ctx);
expect(getById).toHaveBeenCalledWith(id);
expect(exists).toHaveBeenCalledWith({ name: body.name });
expect(badRequest).not.toHaveBeenCalled();
expect(notFound).not.toHaveBeenCalled();
expect(update).toHaveBeenCalledWith(id, body);
expect(send).toHaveBeenCalled();
});
});
});

View File

@ -1,8 +1,12 @@
'use strict';
const { trim } = require('lodash/fp');
const has = require('lodash/has');
const { getService } = require('../utils');
const { validateApiTokenCreationInput } = require('../validation/api-tokens');
const {
validateApiTokenCreationInput,
validateApiTokenUpdateInput,
} = require('../validation/api-tokens');
module.exports = {
async create(ctx) {
@ -63,4 +67,42 @@ module.exports = {
ctx.send({ data: apiToken });
},
async update(ctx) {
const { body } = ctx.request;
const { id } = ctx.params;
const apiTokenService = getService('api-token');
/**
* We trim both field to avoid having issues with either:
* - having a space at the end or start of the value.
* - having only spaces as value;
*/
const attributes = {
name: trim(body.name),
description: trim(body.description),
type: body.type,
};
try {
await validateApiTokenUpdateInput(attributes);
} catch (err) {
return ctx.badRequest('ValidationError', err);
}
const apiTokenExists = await apiTokenService.getById(id);
if (!apiTokenExists) {
return ctx.notFound('API token not found');
}
if (has(attributes, 'name')) {
const nameAlreadyTaken = await apiTokenService.exists({ name: attributes.name });
if (nameAlreadyTaken) {
return ctx.badRequest('Name already taken');
}
}
const apiToken = await apiTokenService.update(id, attributes);
ctx.send({ data: apiToken });
},
};

View File

@ -45,4 +45,15 @@ module.exports = [
],
},
},
{
method: 'PUT',
path: '/api-tokens/:id',
handler: 'api-token.update',
config: {
policies: [
'admin::isAuthenticatedAdmin',
{ name: 'admin::hasPermissions', options: { actions: ['admin::api-tokens.update'] } },
],
},
},
];

View File

@ -243,4 +243,35 @@ describe('API Token', () => {
expect(res).toEqual(null);
});
});
describe('update', () => {
test('Updates a token', async () => {
const update = jest.fn(({ data }) => Promise.resolve(data));
global.strapi = {
query() {
return { update };
},
config: {
get: jest.fn(() => ''),
},
};
const id = 1;
const attributes = {
name: 'api-token_tests-updated-name',
description: 'api-token_tests-description',
type: 'read-only',
};
const res = await apiTokenService.update(id, attributes);
expect(update).toHaveBeenCalledWith({
select: ['id', 'name', 'description', 'type'],
where: { id },
data: attributes,
});
expect(res).toEqual(attributes);
});
});
});

View File

@ -116,6 +116,21 @@ const getById = async id => {
return strapi.query('admin::api-token').findOne({ select: SELECT_FIELDS, where: { id } });
};
/**
* @param {string|number} id
* @param {Object} attributes
* @param {TokenType} attributes.type
* @param {string} attributes.name
* @param {string} [attributes.description]
*
* @returns {Promise<Omit<ApiToken, 'accessKey'>>}
*/
const update = async (id, attributes) => {
return strapi
.query('admin::api-token')
.update({ where: { id }, data: attributes, select: SELECT_FIELDS });
};
module.exports = {
create,
exists,
@ -124,4 +139,5 @@ module.exports = {
list,
revoke,
getById,
update,
};

View File

@ -18,6 +18,10 @@ const { createAuthRequest } = require('../../../../../test/helpers/request');
* 8. Does not return an error if the ressource to delete does not exist
* 9. Retrieves a token (successfully)
* 10. Returns a 404 if the ressource to retrieve does not exist
* 11. Updates a token (successfully)
* 12. Returns a 404 if the ressource to update does not exist
* 13. Fails to creates an api token (missing parameters from the body)
* 14. Fails to creates an api token (invalid `type` in the body)
*/
describe('Admin API Token CRUD (e2e)', () => {
@ -37,7 +41,7 @@ describe('Admin API Token CRUD (e2e)', () => {
await strapi.destroy();
});
test('1. Fails to creates an api token (missing parameters from the body)', async () => {
test('1. Fails to create an api token (missing parameters from the body)', async () => {
const body = {
name: 'api-token_tests-name',
description: 'api-token_tests-description',
@ -60,7 +64,7 @@ describe('Admin API Token CRUD (e2e)', () => {
});
});
test('2. Fails to creates an api token (invalid `type` in the body)', async () => {
test('2. Fails to create an api token (invalid `type` in the body)', async () => {
const body = {
name: 'api-token_tests-name',
description: 'api-token_tests-description',
@ -237,4 +241,90 @@ describe('Admin API Token CRUD (e2e)', () => {
expect(res.statusCode).toBe(404);
expect(res.body.data).toBeUndefined();
});
test('11. Updates a token (successfully)', async () => {
const body = {
name: 'api-token_tests-updated-name',
description: 'api-token_tests-description',
type: 'read-only',
};
const res = await rq({
url: `/admin/api-tokens/${apiTokens[0].id}`,
method: 'PUT',
body,
});
expect(res.statusCode).toBe(200);
expect(res.body.data).toStrictEqual({
name: body.name,
description: body.description,
type: body.type,
id: apiTokens[0].id,
});
});
test('12. Returns a 404 if the ressource to update does not exist', async () => {
const body = {
name: 'api-token_tests-updated-name',
description: 'api-token_tests-description',
type: 'read-only',
};
const res = await rq({
url: '/admin/api-tokens/42',
method: 'PUT',
body,
});
expect(res.statusCode).toBe(404);
expect(res.body.data).toBeUndefined();
});
test('13. Fails to update an api token (missing parameters from the body)', async () => {
const body = {
name: 'api-token_tests-updated-name',
description: 'api-token_tests-description',
};
const res = await rq({
url: '/admin/api-tokens/1',
method: 'PUT',
body,
});
expect(res.statusCode).toBe(400);
expect(res.body).toMatchObject({
statusCode: 400,
error: 'Bad Request',
message: 'ValidationError',
data: {
type: ['type is a required field'],
},
});
});
test('14. Fails to update an api token (invalid `type` in the body)', async () => {
const body = {
name: 'api-token_tests-name',
description: 'api-token_tests-description',
type: 'invalid-type',
};
const res = await rq({
url: '/admin/api-tokens/1',
method: 'PUT',
body,
});
expect(res.statusCode).toBe(400);
expect(res.body).toMatchObject({
statusCode: 400,
error: 'Bad Request',
message: 'ValidationError',
data: {
type: ['type must be one of the following values: read-only, full-access'],
},
});
});
});

View File

@ -26,6 +26,28 @@ const validateApiTokenCreationInput = async data => {
.catch(handleReject);
};
const apiTokenUpdateSchema = yup
.object()
.shape({
name: yup
.string()
.min(1)
.required(),
description: yup.string().optional(),
type: yup
.string()
.oneOf(Object.values(constants.API_TOKEN_TYPE))
.required(),
})
.noUnknown();
const validateApiTokenUpdateInput = async data => {
return apiTokenUpdateSchema
.validate(data, { strict: true, abortEarly: false })
.catch(handleReject);
};
module.exports = {
validateApiTokenCreationInput,
validateApiTokenUpdateInput,
};