diff --git a/packages/backend/src/controllers/api/v1/connections/update-connection.js b/packages/backend/src/controllers/api/v1/connections/update-connection.js new file mode 100644 index 00000000..540d6679 --- /dev/null +++ b/packages/backend/src/controllers/api/v1/connections/update-connection.js @@ -0,0 +1,19 @@ +import { renderObject } from '../../../../helpers/renderer.js'; + +export default async (request, response) => { + let connection = await request.currentUser + .$relatedQuery('connections') + .findOne({ + id: request.params.connectionId, + }) + .throwIfNotFound(); + + connection = await connection.update(connectionParams(request)); + + renderObject(response, connection); +}; + +const connectionParams = (request) => { + const { formattedData, appAuthClientId } = request.body; + return { formattedData, appAuthClientId }; +}; diff --git a/packages/backend/src/controllers/api/v1/connections/update-connection.test.js b/packages/backend/src/controllers/api/v1/connections/update-connection.test.js new file mode 100644 index 00000000..988da4fa --- /dev/null +++ b/packages/backend/src/controllers/api/v1/connections/update-connection.test.js @@ -0,0 +1,117 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import request from 'supertest'; +import Crypto from 'crypto'; +import app from '../../../../app.js'; +import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id.js'; +import { createUser } from '../../../../../test/factories/user.js'; +import { createConnection } from '../../../../../test/factories/connection.js'; +import { createPermission } from '../../../../../test/factories/permission.js'; +import updateConnectionMock from '../../../../../test/mocks/rest/api/v1/connections/update-connection.js'; + +describe('PATCH /api/v1/connections/:connectionId', () => { + let currentUser, currentUserRole, token; + + beforeEach(async () => { + currentUser = await createUser(); + currentUserRole = await currentUser.$relatedQuery('role'); + + token = await createAuthTokenByUserId(currentUser.id); + }); + + it('should update the connection with valid data for current user', async () => { + const connectionData = { + userId: currentUser.id, + key: 'deepl', + verified: true, + formattedData: { + screenName: 'Connection name', + clientSecret: 'secret', + clientId: 'id', + token: 'token', + }, + }; + + const currentUserConnection = await createConnection(connectionData); + + await createPermission({ + action: 'update', + subject: 'Connection', + roleId: currentUserRole.id, + conditions: ['isCreator'], + }); + + const response = await request(app) + .patch(`/api/v1/connections/${currentUserConnection.id}`) + .set('Authorization', token) + .send({ + formattedData: { + screenName: 'New connection name', + clientSecret: 'new secret', + clientId: 'new id', + token: 'new token', + }, + }) + .expect(200); + + const refetchedCurrentUserConnection = await currentUserConnection.$query(); + + const expectedPayload = updateConnectionMock({ + ...refetchedCurrentUserConnection, + reconnectable: refetchedCurrentUserConnection.reconnectable, + }); + + expect(response.body).toStrictEqual(expectedPayload); + }); + + it('should return not found response for another user', async () => { + const anotherUser = await createUser(); + + const anotherUserConnection = await createConnection({ + userId: anotherUser.id, + key: 'deepl', + verified: true, + }); + + await createPermission({ + action: 'update', + subject: 'Connection', + roleId: currentUserRole.id, + conditions: [], + }); + + await request(app) + .patch(`/api/v1/connections/${anotherUserConnection.id}`) + .set('Authorization', token) + .expect(404); + }); + + it('should return not found response for not existing connection UUID', async () => { + const notExistingConnectionUUID = Crypto.randomUUID(); + + await createPermission({ + action: 'update', + subject: 'Connection', + roleId: currentUserRole.id, + conditions: ['isCreator'], + }); + + await request(app) + .patch(`/api/v1/connections/${notExistingConnectionUUID}`) + .set('Authorization', token) + .expect(404); + }); + + it('should return bad request response for invalid UUID', async () => { + await createPermission({ + action: 'update', + subject: 'Connection', + roleId: currentUserRole.id, + conditions: ['isCreator'], + }); + + await request(app) + .patch('/api/v1/connections/invalidConnectionUUID') + .set('Authorization', token) + .expect(400); + }); +}); diff --git a/packages/backend/src/helpers/authorization.js b/packages/backend/src/helpers/authorization.js index 67b1e305..95cd3f0c 100644 --- a/packages/backend/src/helpers/authorization.js +++ b/packages/backend/src/helpers/authorization.js @@ -85,6 +85,10 @@ const authorizationList = { action: 'update', subject: 'Flow', }, + 'PATCH /api/v1/connections/:connectionId': { + action: 'update', + subject: 'Connection', + }, 'DELETE /api/v1/connections/:connectionId': { action: 'delete', subject: 'Connection', diff --git a/packages/backend/src/models/connection.js b/packages/backend/src/models/connection.js index aa1f5dd6..26d139e7 100644 --- a/packages/backend/src/models/connection.js +++ b/packages/backend/src/models/connection.js @@ -261,6 +261,23 @@ class Connection extends Base { return updatedConnection; } + + async update({ formattedData, appAuthClientId }) { + if (appAuthClientId) { + const appAuthClient = await AppAuthClient.query() + .findById(appAuthClientId) + .throwIfNotFound(); + + formattedData = appAuthClient.formattedAuthDefaults; + } + + return await this.$query().patchAndFetch({ + formattedData: { + ...this.formattedData, + ...formattedData, + }, + }); + } } export default Connection; diff --git a/packages/backend/src/routes/api/v1/connections.js b/packages/backend/src/routes/api/v1/connections.js index 458e9b53..16e7f738 100644 --- a/packages/backend/src/routes/api/v1/connections.js +++ b/packages/backend/src/routes/api/v1/connections.js @@ -5,6 +5,7 @@ import getFlowsAction from '../../../controllers/api/v1/connections/get-flows.js import testConnectionAction from '../../../controllers/api/v1/connections/test-connection.js'; import verifyConnectionAction from '../../../controllers/api/v1/connections/verify-connection.js'; import deleteConnectionAction from '../../../controllers/api/v1/connections/delete-connection.js'; +import updateConnectionAction from '../../../controllers/api/v1/connections/update-connection.js'; import generateAuthUrlAction from '../../../controllers/api/v1/connections/generate-auth-url.js'; import resetConnectionAction from '../../../controllers/api/v1/connections/reset-connection.js'; @@ -17,6 +18,13 @@ router.delete( deleteConnectionAction ); +router.patch( + '/:connectionId', + authenticateUser, + authorizeUser, + updateConnectionAction +); + router.get( '/:connectionId/flows', authenticateUser, diff --git a/packages/backend/test/mocks/rest/api/v1/connections/update-connection.js b/packages/backend/test/mocks/rest/api/v1/connections/update-connection.js new file mode 100644 index 00000000..b059d27e --- /dev/null +++ b/packages/backend/test/mocks/rest/api/v1/connections/update-connection.js @@ -0,0 +1,27 @@ +const updateConnectionMock = (connection) => { + const data = { + id: connection.id, + key: connection.key, + verified: connection.verified, + reconnectable: connection.reconnectable, + appAuthClientId: connection.appAuthClientId, + formattedData: { + screenName: connection.formattedData.screenName, + }, + createdAt: connection.createdAt.getTime(), + updatedAt: connection.updatedAt.getTime(), + }; + + return { + data: data, + meta: { + count: 1, + currentPage: null, + isArray: false, + totalPages: null, + type: 'Connection', + }, + }; +}; + +export default updateConnectionMock;