diff --git a/database/layer/admin-client.js b/database/layer/admin-client.js index 84727de..228feba 100644 --- a/database/layer/admin-client.js +++ b/database/layer/admin-client.js @@ -1,12 +1,7 @@ const { PGPool } = require('./pg_pool') const pool = new PGPool() - -module.exports = { - insertClient, - getClient, - updateClientByClientId, - deleteClientByClientId -} +const { usherDb } = require('./knex') +const { pgErrorHandler } = require('../utils/pgErrorHandler') /** * @@ -19,7 +14,7 @@ module.exports = { * @param {string} secret * @returns Record with the newly created object */ -async function insertClient (tenantName, clientId, name, description, secret) { +const insertClient = async (tenantName, clientId, name, description, secret) => { try { // validate tenant name and get tenant key let sql = 'SELECT t.key from usher.tenants t WHERE t.name = $1' @@ -56,11 +51,11 @@ async function insertClient (tenantName, clientId, name, description, secret) { * @param {string} clientId The Client ID * @returns client object */ - async function getClient (clientId) { +const getClient = async (clientId) => { const sql = 'SElECT c.client_id, c.name, c.description, c.secret FROM usher.clients c WHERE c.client_id = $1' try { const results = await pool.query(sql, [clientId]) - if(results.rowCount === 0) { + if (results.rowCount === 0) { throw new Error(`No results for client_id ${clientId}`) } return results.rows[0] @@ -69,21 +64,36 @@ async function insertClient (tenantName, clientId, name, description, secret) { } } -async function updateClientByClientId (clientId, name, description, secret) { - const sql = 'UPDATE usher.clients SET name = $1, description = $2, secret = $3 WHERE client_id = $4' +/** + * Updates a client by client ID with the provided information. + * + * @param {string} clientId - The ID of the client to update. + * @param {Object} clientInfo - Object containing the updated client information. + * @param {string} clientInfo.client_id - The new client ID of the client. + * @param {string} clientInfo.name - The new name of the client. + * @param {string} clientInfo.description - The new description of the client. + * @param {string} clientInfo.secret - The new secret key of the client. + * @returns {Promise} The updated client object. + * @throws {Error} If an error occurs while updating the client. + */ +const updateClientByClientId = async (clientId, { client_id, name, description, secret }) => { try { - const results = await pool.query(sql, [name, description, secret, clientId]) - if (results.rowCount === 1) { - return 'Update successful' - } else { - return `Update failed: Client does not exist matching client_id ${clientId}` - } - } catch (error) { - return `Update failed: ${error.message}` + const [updatedClient] = await usherDb('clients') + .where({ client_id: clientId }) + .update({ + client_id, + name, + description, + secret, + updated_at: new Date(), + }).returning(['client_id', 'name', 'description', 'secret']) + return updatedClient + } catch (err) { + throw pgErrorHandler(err) } } -async function deleteClientByClientId (clientId) { +const deleteClientByClientId = async (clientId) => { const sql = 'DELETE FROM usher.clients WHERE client_id = $1' try { const results = await pool.query(sql, [clientId]) @@ -96,3 +106,10 @@ async function deleteClientByClientId (clientId) { return `Delete failed: ${error.message}` } } + +module.exports = { + insertClient, + getClient, + updateClientByClientId, + deleteClientByClientId, +} diff --git a/database/test/db-insert.test.js b/database/test/db-insert.test.js index 8d795ca..c33a759 100644 --- a/database/test/db-insert.test.js +++ b/database/test/db-insert.test.js @@ -51,8 +51,8 @@ describe('Insert Update and Delete tests', function () { }) it('Should update a single specified client', async function () { try { - await postClients.updateClientByClientId('dummy_client', 'updated_clientname', 'updated_clientdescription', 'updated_secret') - await postClients.updateClientByClientId('dummy_client', 'Dummy Client', 'Dummy client for testing', 'secretsecretdonttell') + await postClients.updateClientByClientId('dummy_client', { client_id: 'dummy_client', name: 'updated_clientname', description: 'updated_clientdescription', secret: 'updated_secret' }) + await postClients.updateClientByClientId('dummy_client', { client_id: 'dummy_client', name: 'Dummy Client', description: 'Dummy client for testing', secret: 'secretsecretdonttell' }) assert(true, 'Dummy client updated and reverted') } catch (error) { assert(false, error.message) diff --git a/server/src/api_endpoints/clients/index.js b/server/src/api_endpoints/clients/index.js index 7eb994a..d3aa9c6 100644 --- a/server/src/api_endpoints/clients/index.js +++ b/server/src/api_endpoints/clients/index.js @@ -1,5 +1,6 @@ const createError = require('http-errors') const dbAdminRole = require('database/layer/admin-client') +const { checkClientExists } = require('./utils') /** * Client Admin function to create a Client @@ -8,7 +9,7 @@ const dbAdminRole = require('database/layer/admin-client') * @param {*} res * @param {*} next */ - const createClient = async (req, res, next) => { +const createClient = async (req, res, next) => { const { tenant_name: tenantName, client_id: clientId, @@ -65,8 +66,29 @@ const getClient = async (req, res, next) => { } } +/** + * HTTP Request handler + * Update a client by client_id + * + * @param {Object} req - The request object + * @param {Object} res - The response object to send 200 statusCode and updated client on success + * @param {Function} next - The next middleware function + * @returns {Promise} - A promise that resolves to void when client is updated + */ +const updateClient = async (req, res, next) => { + try { + const { client_id: clientId } = req.params + await checkClientExists(clientId) + const updatedClient = await dbAdminRole.updateClientByClientId(clientId, req.body) + res.status(200).send(updatedClient) + } catch ({ httpStatusCode = 500, message }) { + return next(createError(httpStatusCode, { message })) + } +} + module.exports = { createClient, deleteClient, - getClient + getClient, + updateClient, } diff --git a/server/src/api_endpoints/clients/utils.js b/server/src/api_endpoints/clients/utils.js new file mode 100644 index 0000000..7ded761 --- /dev/null +++ b/server/src/api_endpoints/clients/utils.js @@ -0,0 +1,16 @@ +const dbAdminRole = require('database/layer/admin-client') + +const checkClientExists = async (clientId) => { + try { + await dbAdminRole.getClient(clientId); + } catch { + throw { + httpStatusCode: 404, + message: 'Client does not exist!', + } + } +} + +module.exports = { + checkClientExists, +} diff --git a/server/test/endpoint_clients.test.js b/server/test/endpoint_clients.test.js index 873bb86..6c95df6 100644 --- a/server/test/endpoint_clients.test.js +++ b/server/test/endpoint_clients.test.js @@ -1,9 +1,10 @@ const { describe, it } = require('mocha') const fetch = require('node-fetch') const assert = require('assert') -const { getAdmin1IdPToken } = require('./lib/tokens') +const { getAdmin1IdPToken, getTestUser1IdPToken } = require('./lib/tokens') const { getServerUrl } = require('./lib/urls') const dbAdminRole = require('database/layer/admin-client') +const { usherDb } = require('../../database/layer/knex') describe('Admin Clients Endpoint Test', () => { let userAccessToken = '' @@ -82,7 +83,6 @@ describe('Admin Clients Endpoint Test', () => { assert.strictEqual(response.status, 200, 'Expected 200 response code') assert.strictEqual(data.client_id, 'test-client1', 'Expected valid client id value') }) - it('Should allow client admin to get Client object') }) describe('Delete Client', () => { @@ -100,4 +100,86 @@ describe('Admin Clients Endpoint Test', () => { assert.strictEqual(response.status, 404, 'Expected 404 response code') }) }) + + describe('Update Client', () => { + let testClient + /** + * PUT /clients/{:client_id} + * HTTP request to Update a client by its client_id + * + * @param {string} clientId - The subject client id which needs to be updated + * @param {string} payload - The request body payload to update a client + * @param {Object} header - The request headers + * @returns {Promise} - A Promise which resolves to fetch.response + */ + const updateClient = async (clientId, payload = { client_id: testClient?.client_id, name: testClient?.name }, header = requestHeaders) => { + return await fetch(`${url}/clients/${clientId}`, { + method: 'PUT', + headers: header, + body: JSON.stringify(payload) + }) + } + + beforeEach(async () => { + testClient = (await usherDb('clients').insert({ + client_id: 'test_client_id', + name: 'test_client_name', + description: 'test_client_description', + secret: 'test_client_secret', + }).returning('*'))[0] + }) + + it('should return 200, update the client information', async () => { + const newClientInfo = { + client_id: 'updated_client_id', + name: 'updated_client_name', + description: 'updated_client_description', + secret: 'updated_client_secret', + } + const response = await updateClient(testClient.client_id, newClientInfo) + assert.equal(response.status, 200) + testClient.client_id = newClientInfo.client_id + const updatedClient = await response.json(); + Object.entries(newClientInfo).forEach(([key, val]) => { + assert.equal(updatedClient[key], val) + }) + }) + + it('should return 400, for invalid payloads', async () => { + const invalidRequestsResponses = await Promise.all([ + updateClient(testClient.client_id, ''), + updateClient(testClient.client_id, {}), + updateClient(testClient.client_id, { name: 'test' }), + updateClient(testClient.client_id, { client_id: 'test' }), + updateClient(testClient.client_id, { client_id: 'test', name: 1 }), + updateClient(testClient.client_id, { client_id: 1, name: 'test' }), + ]) + invalidRequestsResponses.forEach(({ status }) => assert.equal(status, 400)) + }) + + it('should return 401, unauthorized token', async () => { + const userAccessToken = await getTestUser1IdPToken() + const response = await updateClient(testClient.client_id, testClient, + { + ...requestHeaders, + Authorization: `Bearer ${userAccessToken}` + }) + assert.equal(response.status, 401) + }) + + it('should return 404, for invalid client_id', async () => { + const response = await updateClient('invalid_client_id') + assert.equal(response.status, 404) + }) + + it('should return 409, to update client_id if it already exist', async () => { + const { client_id: existingClientId } = await usherDb('clients').select('client_id').first() + const response = await updateClient(testClient.client_id, { ...testClient, client_id: existingClientId }) + assert.equal(response.status, 409) + }) + + afterEach(async () => { + await usherDb('clients').where({ client_id: testClient.client_id }).del() + }) + }) }) diff --git a/server/the-usher-openapi-spec.yaml b/server/the-usher-openapi-spec.yaml index 2045639..ce0bc7c 100644 --- a/server/the-usher-openapi-spec.yaml +++ b/server/the-usher-openapi-spec.yaml @@ -850,6 +850,38 @@ paths: description: Successfully deleted the Client and data 404: $ref: '#/components/responses/NotFound' + put: + 'x-swagger-router-controller': 'clients/index' + operationId: updateClient + summary: Update a client + tags: + - Client Admin APIs + security: + - bearerAdminAuth: [] + - bearerClientAdminAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Client' + responses: + 200: + description: Return the details of updated client + content: + application/json: + schema: + $ref: '#/components/schemas/Client' + 400: + $ref: '#/components/responses/BadRequest' + 401: + $ref: '#/components/responses/Unauthorized' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 500: + $ref: '#/components/responses/InternalError' /clients/{client_id}/roles: parameters: