From 7dcfb1081b71cfa71119bc64eb1b8bdb056b94ae Mon Sep 17 00:00:00 2001 From: Ali BARIN Date: Tue, 24 Sep 2024 09:28:17 +0000 Subject: [PATCH 01/12] feat(useAuthenticateApp): use REST API endpoint to create auth url --- .../src/helpers/add-authentication-steps.js | 7 +------ .../web/src/helpers/computeAuthStepVariables.js | 4 ++-- packages/web/src/hooks/useAuthenticateApp.ee.js | 7 +++++++ .../web/src/hooks/useCreateConnectionAuthUrl.js | 17 +++++++++++++++++ 4 files changed, 27 insertions(+), 8 deletions(-) create mode 100644 packages/web/src/hooks/useCreateConnectionAuthUrl.js diff --git a/packages/backend/src/helpers/add-authentication-steps.js b/packages/backend/src/helpers/add-authentication-steps.js index 631e6055..8ab6f6d7 100644 --- a/packages/backend/src/helpers/add-authentication-steps.js +++ b/packages/backend/src/helpers/add-authentication-steps.js @@ -54,12 +54,7 @@ const authenticationStepsWithAuthUrl = [ { type: 'mutation', name: 'generateAuthUrl', - arguments: [ - { - name: 'id', - value: '{createConnection.id}', - }, - ], + arguments: [], }, { type: 'openWithPopup', diff --git a/packages/web/src/helpers/computeAuthStepVariables.js b/packages/web/src/helpers/computeAuthStepVariables.js index d49bdb5f..49714b54 100644 --- a/packages/web/src/helpers/computeAuthStepVariables.js +++ b/packages/web/src/helpers/computeAuthStepVariables.js @@ -6,7 +6,7 @@ const computeAuthStepVariables = (variableSchema, aggregatedData) => { if (variable.properties) { variables[variable.name] = computeAuthStepVariables( variable.properties, - aggregatedData + aggregatedData, ); continue; } @@ -17,7 +17,7 @@ const computeAuthStepVariables = (variableSchema, aggregatedData) => { continue; } const computedVariable = template(variable.value, { interpolate })( - aggregatedData + aggregatedData, ); variables[variable.name] = computedVariable; } diff --git a/packages/web/src/hooks/useAuthenticateApp.ee.js b/packages/web/src/hooks/useAuthenticateApp.ee.js index c140c3e7..c4572363 100644 --- a/packages/web/src/hooks/useAuthenticateApp.ee.js +++ b/packages/web/src/hooks/useAuthenticateApp.ee.js @@ -9,6 +9,7 @@ import computeAuthStepVariables from 'helpers/computeAuthStepVariables'; import useAppAuth from './useAppAuth'; import useCreateConnection from './useCreateConnection'; import useFormatMessage from './useFormatMessage'; +import useCreateConnectionAuthUrl from './useCreateConnectionAuthUrl'; function getSteps(auth, hasConnection, useShared) { if (hasConnection) { @@ -29,6 +30,7 @@ export default function useAuthenticateApp(payload) { const { appKey, appAuthClientId, connectionId, useShared = false } = payload; const { data: auth } = useAppAuth(appKey); const { mutateAsync: createConnection } = useCreateConnection(appKey); + const { mutateAsync: createConnectionAuthUrl } = useCreateConnectionAuthUrl(); const [authenticationInProgress, setAuthenticationInProgress] = React.useState(false); const formatMessage = useFormatMessage(); @@ -70,6 +72,11 @@ export default function useAuthenticateApp(payload) { if (step.name === 'createConnection') { const stepResponse = await createConnection(variables); response[step.name] = stepResponse?.data; + } else if (step.name === 'generateAuthUrl') { + const stepResponse = await createConnectionAuthUrl( + response.createConnection.id, + ); + response[step.name] = stepResponse?.data; } else { const stepResponse = await processMutation(step.name, variables); response[step.name] = stepResponse; diff --git a/packages/web/src/hooks/useCreateConnectionAuthUrl.js b/packages/web/src/hooks/useCreateConnectionAuthUrl.js new file mode 100644 index 00000000..cfdc450f --- /dev/null +++ b/packages/web/src/hooks/useCreateConnectionAuthUrl.js @@ -0,0 +1,17 @@ +import { useMutation } from '@tanstack/react-query'; + +import api from 'helpers/api'; + +export default function useCreateConnectionAuthUrl() { + const query = useMutation({ + mutationFn: async (connectionId) => { + const { data } = await api.post( + `/v1/connections/${connectionId}/auth-url`, + ); + + return data; + }, + }); + + return query; +} From 37c78e6bbdbea3ecafa467dfffb03268a2d0c74e Mon Sep 17 00:00:00 2001 From: Ali BARIN Date: Tue, 24 Sep 2024 09:46:56 +0000 Subject: [PATCH 02/12] chore: remove redundant generate auth url mutation --- .../backend/src/graphql/mutation-resolvers.js | 2 -- .../graphql/mutations/generate-auth-url.js | 30 ------------------- packages/backend/src/graphql/schema.graphql | 20 +------------ .../graphql/mutations/generate-auth-url.js | 8 ----- packages/web/src/graphql/mutations/index.js | 3 +- 5 files changed, 2 insertions(+), 61 deletions(-) delete mode 100644 packages/backend/src/graphql/mutations/generate-auth-url.js delete mode 100644 packages/web/src/graphql/mutations/generate-auth-url.js diff --git a/packages/backend/src/graphql/mutation-resolvers.js b/packages/backend/src/graphql/mutation-resolvers.js index f29b5f57..e02841fa 100644 --- a/packages/backend/src/graphql/mutation-resolvers.js +++ b/packages/backend/src/graphql/mutation-resolvers.js @@ -1,11 +1,9 @@ // Converted mutations import verifyConnection from './mutations/verify-connection.js'; -import generateAuthUrl from './mutations/generate-auth-url.js'; import resetConnection from './mutations/reset-connection.js'; import updateConnection from './mutations/update-connection.js'; const mutationResolvers = { - generateAuthUrl, resetConnection, updateConnection, verifyConnection, diff --git a/packages/backend/src/graphql/mutations/generate-auth-url.js b/packages/backend/src/graphql/mutations/generate-auth-url.js deleted file mode 100644 index 17948c14..00000000 --- a/packages/backend/src/graphql/mutations/generate-auth-url.js +++ /dev/null @@ -1,30 +0,0 @@ -import globalVariable from '../../helpers/global-variable.js'; -import App from '../../models/app.js'; - -const generateAuthUrl = async (_parent, params, context) => { - context.currentUser.can('create', 'Connection'); - - const connection = await context.currentUser - .$relatedQuery('connections') - .findOne({ - id: params.input.id, - }) - .throwIfNotFound(); - - if (!connection.formattedData) { - return null; - } - - const authInstance = ( - await import(`../../apps/${connection.key}/auth/index.js`) - ).default; - - const app = await App.findOneByKey(connection.key); - - const $ = await globalVariable({ connection, app }); - await authInstance.generateAuthUrl($); - - return connection.formattedData; -}; - -export default generateAuthUrl; diff --git a/packages/backend/src/graphql/schema.graphql b/packages/backend/src/graphql/schema.graphql index d3b753d3..329803ef 100644 --- a/packages/backend/src/graphql/schema.graphql +++ b/packages/backend/src/graphql/schema.graphql @@ -1,23 +1,13 @@ type Query { placeholderQuery(name: String): Boolean } + type Mutation { - generateAuthUrl(input: GenerateAuthUrlInput): AuthLink resetConnection(input: ResetConnectionInput): Connection updateConnection(input: UpdateConnectionInput): Connection verifyConnection(input: VerifyConnectionInput): Connection } -""" -Exposes a URL that specifies the behaviour of this scalar. -""" -directive @specifiedBy( - """ - The URL that specifies the behaviour of this scalar. - """ - url: String! -) on SCALAR - type Trigger { name: String key: String @@ -130,10 +120,6 @@ type AuthenticationStepProperty { value: String } -type AuthLink { - url: String -} - type Connection { id: String key: String @@ -200,10 +186,6 @@ type SamlAuthProvidersRoleMapping { remoteRoleName: String } -input GenerateAuthUrlInput { - id: String! -} - input UpdateConnectionInput { id: String! formattedData: JSONObject diff --git a/packages/web/src/graphql/mutations/generate-auth-url.js b/packages/web/src/graphql/mutations/generate-auth-url.js deleted file mode 100644 index 44ce0431..00000000 --- a/packages/web/src/graphql/mutations/generate-auth-url.js +++ /dev/null @@ -1,8 +0,0 @@ -import { gql } from '@apollo/client'; -export const GENERATE_AUTH_URL = gql` - mutation generateAuthUrl($input: GenerateAuthUrlInput) { - generateAuthUrl(input: $input) { - url - } - } -`; diff --git a/packages/web/src/graphql/mutations/index.js b/packages/web/src/graphql/mutations/index.js index 07e46710..e00ad910 100644 --- a/packages/web/src/graphql/mutations/index.js +++ b/packages/web/src/graphql/mutations/index.js @@ -1,12 +1,11 @@ import { UPDATE_CONNECTION } from './update-connection'; import { VERIFY_CONNECTION } from './verify-connection'; import { RESET_CONNECTION } from './reset-connection'; -import { GENERATE_AUTH_URL } from './generate-auth-url'; const mutations = { updateConnection: UPDATE_CONNECTION, verifyConnection: VERIFY_CONNECTION, resetConnection: RESET_CONNECTION, - generateAuthUrl: GENERATE_AUTH_URL, }; + export default mutations; From 5e20ac07d1ebc8ed5a32e15ad125199d08730bbb Mon Sep 17 00:00:00 2001 From: Ali BARIN Date: Tue, 24 Sep 2024 10:26:05 +0000 Subject: [PATCH 03/12] chore(add-authentication-steps): remove unnecessary id arguments --- .../src/helpers/add-authentication-steps.js | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/packages/backend/src/helpers/add-authentication-steps.js b/packages/backend/src/helpers/add-authentication-steps.js index 8ab6f6d7..f3c288bb 100644 --- a/packages/backend/src/helpers/add-authentication-steps.js +++ b/packages/backend/src/helpers/add-authentication-steps.js @@ -70,10 +70,6 @@ const authenticationStepsWithAuthUrl = [ type: 'mutation', name: 'updateConnection', arguments: [ - { - name: 'id', - value: '{createConnection.id}', - }, { name: 'formattedData', value: '{openAuthPopup.all}', @@ -110,12 +106,7 @@ const sharedAuthenticationStepsWithAuthUrl = [ { type: 'mutation', name: 'generateAuthUrl', - arguments: [ - { - name: 'id', - value: '{createConnection.id}', - }, - ], + arguments: [], }, { type: 'openWithPopup', @@ -131,10 +122,6 @@ const sharedAuthenticationStepsWithAuthUrl = [ type: 'mutation', name: 'updateConnection', arguments: [ - { - name: 'id', - value: '{createConnection.id}', - }, { name: 'formattedData', value: '{openAuthPopup.all}', From 24d09fda4c35fd0c2d82f5abb62fc3f2f6ccd381 Mon Sep 17 00:00:00 2001 From: Ali BARIN Date: Tue, 24 Sep 2024 11:10:40 +0000 Subject: [PATCH 04/12] feat(useAuthenticateApp): use REST API endpoint to update connection --- .../src/helpers/computeAuthStepVariables.js | 8 +++++++ .../web/src/hooks/useAuthenticateApp.ee.js | 22 +++++++++++++++++-- packages/web/src/hooks/useUpdateConnection.js | 18 +++++++++++++++ 3 files changed, 46 insertions(+), 2 deletions(-) create mode 100644 packages/web/src/hooks/useUpdateConnection.js diff --git a/packages/web/src/helpers/computeAuthStepVariables.js b/packages/web/src/helpers/computeAuthStepVariables.js index 49714b54..8c3e5f5e 100644 --- a/packages/web/src/helpers/computeAuthStepVariables.js +++ b/packages/web/src/helpers/computeAuthStepVariables.js @@ -1,27 +1,35 @@ import template from 'lodash/template'; const interpolate = /{([\s\S]+?)}/g; + const computeAuthStepVariables = (variableSchema, aggregatedData) => { const variables = {}; + for (const variable of variableSchema) { if (variable.properties) { variables[variable.name] = computeAuthStepVariables( variable.properties, aggregatedData, ); + continue; } + if (variable.value) { if (variable.value.endsWith('.all}')) { const key = variable.value.replace('{', '').replace('.all}', ''); variables[variable.name] = aggregatedData[key]; + continue; } + const computedVariable = template(variable.value, { interpolate })( aggregatedData, ); + variables[variable.name] = computedVariable; } } + return variables; }; export default computeAuthStepVariables; diff --git a/packages/web/src/hooks/useAuthenticateApp.ee.js b/packages/web/src/hooks/useAuthenticateApp.ee.js index c4572363..6a70366e 100644 --- a/packages/web/src/hooks/useAuthenticateApp.ee.js +++ b/packages/web/src/hooks/useAuthenticateApp.ee.js @@ -6,10 +6,11 @@ import { processPopupMessage, } from 'helpers/authenticationSteps'; import computeAuthStepVariables from 'helpers/computeAuthStepVariables'; +import useFormatMessage from './useFormatMessage'; import useAppAuth from './useAppAuth'; import useCreateConnection from './useCreateConnection'; -import useFormatMessage from './useFormatMessage'; import useCreateConnectionAuthUrl from './useCreateConnectionAuthUrl'; +import useUpdateConnection from './useUpdateConnection'; function getSteps(auth, hasConnection, useShared) { if (hasConnection) { @@ -31,6 +32,7 @@ export default function useAuthenticateApp(payload) { const { data: auth } = useAppAuth(appKey); const { mutateAsync: createConnection } = useCreateConnection(appKey); const { mutateAsync: createConnectionAuthUrl } = useCreateConnectionAuthUrl(); + const { mutateAsync: updateConnection } = useUpdateConnection(); const [authenticationInProgress, setAuthenticationInProgress] = React.useState(false); const formatMessage = useFormatMessage(); @@ -76,6 +78,13 @@ export default function useAuthenticateApp(payload) { const stepResponse = await createConnectionAuthUrl( response.createConnection.id, ); + response[step.name] = stepResponse?.data; + } else if (step.name === 'updateConnection') { + const stepResponse = await updateConnection({ + ...variables, + connectionId: response.createConnection.id, + }); + response[step.name] = stepResponse?.data; } else { const stepResponse = await processMutation(step.name, variables); @@ -98,7 +107,16 @@ export default function useAuthenticateApp(payload) { setAuthenticationInProgress(false); } }; - }, [steps, appKey, appAuthClientId, connectionId, formatMessage]); + }, [ + steps, + appKey, + appAuthClientId, + connectionId, + formatMessage, + createConnection, + createConnectionAuthUrl, + updateConnection, + ]); return { authenticate, diff --git a/packages/web/src/hooks/useUpdateConnection.js b/packages/web/src/hooks/useUpdateConnection.js new file mode 100644 index 00000000..37d87bc4 --- /dev/null +++ b/packages/web/src/hooks/useUpdateConnection.js @@ -0,0 +1,18 @@ +import { useMutation } from '@tanstack/react-query'; + +import api from 'helpers/api'; + +export default function useUpdateConnection() { + const query = useMutation({ + mutationFn: async ({ connectionId, formattedData, appAuthClientId }) => { + const { data } = await api.patch(`/v1/connections/${connectionId}`, { + formattedData, + appAuthClientId, + }); + + return data; + }, + }); + + return query; +} From bc0e18d074f596bb48ff9ad2e85253a1e3c23942 Mon Sep 17 00:00:00 2001 From: Ali BARIN Date: Tue, 24 Sep 2024 11:10:56 +0000 Subject: [PATCH 05/12] chore: remove redundant update connection mutation --- .../backend/src/graphql/mutation-resolvers.js | 5 ++- .../graphql/mutations/update-connection.js | 33 ------------------- packages/backend/src/graphql/schema.graphql | 7 ---- packages/web/src/graphql/mutations/index.js | 2 -- .../graphql/mutations/update-connection.js | 13 -------- 5 files changed, 4 insertions(+), 56 deletions(-) delete mode 100644 packages/backend/src/graphql/mutations/update-connection.js delete mode 100644 packages/web/src/graphql/mutations/update-connection.js diff --git a/packages/backend/src/graphql/mutation-resolvers.js b/packages/backend/src/graphql/mutation-resolvers.js index e02841fa..741bf98b 100644 --- a/packages/backend/src/graphql/mutation-resolvers.js +++ b/packages/backend/src/graphql/mutation-resolvers.js @@ -1,11 +1,14 @@ // Converted mutations import verifyConnection from './mutations/verify-connection.js'; import resetConnection from './mutations/reset-connection.js'; -import updateConnection from './mutations/update-connection.js'; const mutationResolvers = { resetConnection, +<<<<<<< HEAD updateConnection, +======= + updateCurrentUser, +>>>>>>> 82cde73e (chore: remove redundant update connection mutation) verifyConnection, }; diff --git a/packages/backend/src/graphql/mutations/update-connection.js b/packages/backend/src/graphql/mutations/update-connection.js deleted file mode 100644 index bc8cdc58..00000000 --- a/packages/backend/src/graphql/mutations/update-connection.js +++ /dev/null @@ -1,33 +0,0 @@ -import AppAuthClient from '../../models/app-auth-client.js'; - -const updateConnection = async (_parent, params, context) => { - context.currentUser.can('create', 'Connection'); - - let connection = await context.currentUser - .$relatedQuery('connections') - .findOne({ - id: params.input.id, - }) - .throwIfNotFound(); - - let formattedData = params.input.formattedData; - - if (params.input.appAuthClientId) { - const appAuthClient = await AppAuthClient.query() - .findById(params.input.appAuthClientId) - .throwIfNotFound(); - - formattedData = appAuthClient.formattedAuthDefaults; - } - - connection = await connection.$query().patchAndFetch({ - formattedData: { - ...connection.formattedData, - ...formattedData, - }, - }); - - return connection; -}; - -export default updateConnection; diff --git a/packages/backend/src/graphql/schema.graphql b/packages/backend/src/graphql/schema.graphql index 329803ef..ce9655fb 100644 --- a/packages/backend/src/graphql/schema.graphql +++ b/packages/backend/src/graphql/schema.graphql @@ -4,7 +4,6 @@ type Query { type Mutation { resetConnection(input: ResetConnectionInput): Connection - updateConnection(input: UpdateConnectionInput): Connection verifyConnection(input: VerifyConnectionInput): Connection } @@ -186,12 +185,6 @@ type SamlAuthProvidersRoleMapping { remoteRoleName: String } -input UpdateConnectionInput { - id: String! - formattedData: JSONObject - appAuthClientId: String -} - input ResetConnectionInput { id: String! } diff --git a/packages/web/src/graphql/mutations/index.js b/packages/web/src/graphql/mutations/index.js index e00ad910..7cbc753a 100644 --- a/packages/web/src/graphql/mutations/index.js +++ b/packages/web/src/graphql/mutations/index.js @@ -1,9 +1,7 @@ -import { UPDATE_CONNECTION } from './update-connection'; import { VERIFY_CONNECTION } from './verify-connection'; import { RESET_CONNECTION } from './reset-connection'; const mutations = { - updateConnection: UPDATE_CONNECTION, verifyConnection: VERIFY_CONNECTION, resetConnection: RESET_CONNECTION, }; diff --git a/packages/web/src/graphql/mutations/update-connection.js b/packages/web/src/graphql/mutations/update-connection.js deleted file mode 100644 index 6b70cebe..00000000 --- a/packages/web/src/graphql/mutations/update-connection.js +++ /dev/null @@ -1,13 +0,0 @@ -import { gql } from '@apollo/client'; -export const UPDATE_CONNECTION = gql` - mutation UpdateConnection($input: UpdateConnectionInput) { - updateConnection(input: $input) { - id - key - verified - formattedData { - screenName - } - } - } -`; From 5e6f4bfb8840381be9dcfb6dbc8b28efa6068649 Mon Sep 17 00:00:00 2001 From: Ali BARIN Date: Tue, 24 Sep 2024 11:40:35 +0000 Subject: [PATCH 06/12] feat(useAuthenticateApp): use REST API endpoint to reset connection --- .../src/helpers/add-authentication-steps.js | 21 ++--------- .../src/helpers/add-reconnection-steps.js | 35 ++----------------- .../web/src/hooks/useAuthenticateApp.ee.js | 22 +++++++----- packages/web/src/hooks/useResetConnection.js | 15 ++++++++ 4 files changed, 34 insertions(+), 59 deletions(-) create mode 100644 packages/web/src/hooks/useResetConnection.js diff --git a/packages/backend/src/helpers/add-authentication-steps.js b/packages/backend/src/helpers/add-authentication-steps.js index f3c288bb..5e7a462a 100644 --- a/packages/backend/src/helpers/add-authentication-steps.js +++ b/packages/backend/src/helpers/add-authentication-steps.js @@ -27,12 +27,7 @@ const authenticationStepsWithoutAuthUrl = [ { type: 'mutation', name: 'verifyConnection', - arguments: [ - { - name: 'id', - value: '{createConnection.id}', - }, - ], + arguments: [], }, ]; @@ -79,12 +74,7 @@ const authenticationStepsWithAuthUrl = [ { type: 'mutation', name: 'verifyConnection', - arguments: [ - { - name: 'id', - value: '{createConnection.id}', - }, - ], + arguments: [], }, ]; @@ -131,12 +121,7 @@ const sharedAuthenticationStepsWithAuthUrl = [ { type: 'mutation', name: 'verifyConnection', - arguments: [ - { - name: 'id', - value: '{createConnection.id}', - }, - ], + arguments: [], }, ]; diff --git a/packages/backend/src/helpers/add-reconnection-steps.js b/packages/backend/src/helpers/add-reconnection-steps.js index 58b2a530..728498e0 100644 --- a/packages/backend/src/helpers/add-reconnection-steps.js +++ b/packages/backend/src/helpers/add-reconnection-steps.js @@ -1,54 +1,23 @@ import cloneDeep from 'lodash/cloneDeep.js'; -const connectionIdArgument = { - name: 'id', - value: '{connection.id}', -}; - const resetConnectionStep = { type: 'mutation', name: 'resetConnection', - arguments: [connectionIdArgument], + arguments: [], }; -function replaceCreateConnection(string) { - return string.replace('{createConnection.id}', '{connection.id}'); -} - function removeAppKeyArgument(args) { return args.filter((argument) => argument.name !== 'key'); } -function addConnectionId(step) { - step.arguments = step.arguments.map((argument) => { - if (typeof argument.value === 'string') { - argument.value = replaceCreateConnection(argument.value); - } - - if (argument.properties) { - argument.properties = argument.properties.map((property) => { - return { - name: property.name, - value: replaceCreateConnection(property.value), - }; - }); - } - - return argument; - }); - - return step; -} - function replaceCreateConnectionsWithUpdate(steps) { const updatedSteps = cloneDeep(steps); return updatedSteps.map((step) => { - const updatedStep = addConnectionId(step); + const updatedStep = { ...step }; if (step.name === 'createConnection') { updatedStep.name = 'updateConnection'; updatedStep.arguments = removeAppKeyArgument(updatedStep.arguments); - updatedStep.arguments.unshift(connectionIdArgument); return updatedStep; } diff --git a/packages/web/src/hooks/useAuthenticateApp.ee.js b/packages/web/src/hooks/useAuthenticateApp.ee.js index 6a70366e..ebb8c97a 100644 --- a/packages/web/src/hooks/useAuthenticateApp.ee.js +++ b/packages/web/src/hooks/useAuthenticateApp.ee.js @@ -11,6 +11,7 @@ import useAppAuth from './useAppAuth'; import useCreateConnection from './useCreateConnection'; import useCreateConnectionAuthUrl from './useCreateConnectionAuthUrl'; import useUpdateConnection from './useUpdateConnection'; +import useResetConnection from './useResetConnection'; function getSteps(auth, hasConnection, useShared) { if (hasConnection) { @@ -33,6 +34,7 @@ export default function useAuthenticateApp(payload) { const { mutateAsync: createConnection } = useCreateConnection(appKey); const { mutateAsync: createConnectionAuthUrl } = useCreateConnectionAuthUrl(); const { mutateAsync: updateConnection } = useUpdateConnection(); + const { mutateAsync: resetConnection } = useResetConnection(); const [authenticationInProgress, setAuthenticationInProgress] = React.useState(false); const formatMessage = useFormatMessage(); @@ -48,9 +50,7 @@ export default function useAuthenticateApp(payload) { const response = { key: appKey, appAuthClientId: appAuthClientId || payload.appAuthClientId, - connection: { - id: connectionId, - }, + connectionId, fields, }; let stepIndex = 0; @@ -73,19 +73,24 @@ export default function useAuthenticateApp(payload) { if (step.type === 'mutation') { if (step.name === 'createConnection') { const stepResponse = await createConnection(variables); - response[step.name] = stepResponse?.data; + response[step.name] = stepResponse.data; + response.connectionId = stepResponse.data.id; } else if (step.name === 'generateAuthUrl') { const stepResponse = await createConnectionAuthUrl( - response.createConnection.id, + response.connectionId, ); - response[step.name] = stepResponse?.data; + response[step.name] = stepResponse.data; } else if (step.name === 'updateConnection') { const stepResponse = await updateConnection({ ...variables, - connectionId: response.createConnection.id, + connectionId: response.connectionId, }); - response[step.name] = stepResponse?.data; + response[step.name] = stepResponse.data; + } else if (step.name === 'resetConnection') { + const stepResponse = await resetConnection(response.connectionId); + + response[step.name] = stepResponse.data; } else { const stepResponse = await processMutation(step.name, variables); response[step.name] = stepResponse; @@ -116,6 +121,7 @@ export default function useAuthenticateApp(payload) { createConnection, createConnectionAuthUrl, updateConnection, + resetConnection, ]); return { diff --git a/packages/web/src/hooks/useResetConnection.js b/packages/web/src/hooks/useResetConnection.js new file mode 100644 index 00000000..39bdf908 --- /dev/null +++ b/packages/web/src/hooks/useResetConnection.js @@ -0,0 +1,15 @@ +import { useMutation } from '@tanstack/react-query'; + +import api from 'helpers/api'; + +export default function useResetConnection() { + const query = useMutation({ + mutationFn: async (connectionId) => { + const { data } = await api.post(`/v1/connections/${connectionId}/reset`); + + return data; + }, + }); + + return query; +} From dc0273148cd078c1ec3530fc79f672fd26a2a852 Mon Sep 17 00:00:00 2001 From: Ali BARIN Date: Tue, 24 Sep 2024 11:44:42 +0000 Subject: [PATCH 07/12] chore: remove redundant reset connection mutation --- .../backend/src/graphql/mutation-resolvers.js | 7 ------ .../src/graphql/mutations/reset-connection.js | 22 ------------------- packages/backend/src/graphql/schema.graphql | 5 ----- packages/web/src/graphql/mutations/index.js | 2 -- .../src/graphql/mutations/reset-connection.js | 8 ------- 5 files changed, 44 deletions(-) delete mode 100644 packages/backend/src/graphql/mutations/reset-connection.js delete mode 100644 packages/web/src/graphql/mutations/reset-connection.js diff --git a/packages/backend/src/graphql/mutation-resolvers.js b/packages/backend/src/graphql/mutation-resolvers.js index 741bf98b..8492caa6 100644 --- a/packages/backend/src/graphql/mutation-resolvers.js +++ b/packages/backend/src/graphql/mutation-resolvers.js @@ -1,14 +1,7 @@ // Converted mutations import verifyConnection from './mutations/verify-connection.js'; -import resetConnection from './mutations/reset-connection.js'; const mutationResolvers = { - resetConnection, -<<<<<<< HEAD - updateConnection, -======= - updateCurrentUser, ->>>>>>> 82cde73e (chore: remove redundant update connection mutation) verifyConnection, }; diff --git a/packages/backend/src/graphql/mutations/reset-connection.js b/packages/backend/src/graphql/mutations/reset-connection.js deleted file mode 100644 index 212cede7..00000000 --- a/packages/backend/src/graphql/mutations/reset-connection.js +++ /dev/null @@ -1,22 +0,0 @@ -const resetConnection = async (_parent, params, context) => { - context.currentUser.can('create', 'Connection'); - - let connection = await context.currentUser - .$relatedQuery('connections') - .findOne({ - id: params.input.id, - }) - .throwIfNotFound(); - - if (!connection.formattedData) { - return null; - } - - connection = await connection.$query().patchAndFetch({ - formattedData: { screenName: connection.formattedData.screenName }, - }); - - return connection; -}; - -export default resetConnection; diff --git a/packages/backend/src/graphql/schema.graphql b/packages/backend/src/graphql/schema.graphql index ce9655fb..84dd1bb4 100644 --- a/packages/backend/src/graphql/schema.graphql +++ b/packages/backend/src/graphql/schema.graphql @@ -3,7 +3,6 @@ type Query { } type Mutation { - resetConnection(input: ResetConnectionInput): Connection verifyConnection(input: VerifyConnectionInput): Connection } @@ -185,10 +184,6 @@ type SamlAuthProvidersRoleMapping { remoteRoleName: String } -input ResetConnectionInput { - id: String! -} - input VerifyConnectionInput { id: String! } diff --git a/packages/web/src/graphql/mutations/index.js b/packages/web/src/graphql/mutations/index.js index 7cbc753a..c0921b27 100644 --- a/packages/web/src/graphql/mutations/index.js +++ b/packages/web/src/graphql/mutations/index.js @@ -1,9 +1,7 @@ import { VERIFY_CONNECTION } from './verify-connection'; -import { RESET_CONNECTION } from './reset-connection'; const mutations = { verifyConnection: VERIFY_CONNECTION, - resetConnection: RESET_CONNECTION, }; export default mutations; diff --git a/packages/web/src/graphql/mutations/reset-connection.js b/packages/web/src/graphql/mutations/reset-connection.js deleted file mode 100644 index adc02b24..00000000 --- a/packages/web/src/graphql/mutations/reset-connection.js +++ /dev/null @@ -1,8 +0,0 @@ -import { gql } from '@apollo/client'; -export const RESET_CONNECTION = gql` - mutation ResetConnection($input: ResetConnectionInput) { - resetConnection(input: $input) { - id - } - } -`; From 4d5fc50f1acf31f674b550d1500798dba3e34a0b Mon Sep 17 00:00:00 2001 From: "kasia.oczkowska" Date: Fri, 20 Sep 2024 13:05:21 +0100 Subject: [PATCH 08/12] feat: refactor verify connection mutation with the REST API endpoint --- .../backend/src/graphql/mutation-resolvers.js | 7 +---- .../graphql/mutations/verify-connection.js | 29 ----------------- packages/backend/src/graphql/schema.graphql | 6 +--- packages/web/src/graphql/cache.js | 31 +------------------ packages/web/src/graphql/mutations/index.js | 6 +--- .../graphql/mutations/verify-connection.js | 17 ---------- .../web/src/hooks/useAuthenticateApp.ee.js | 5 +++ packages/web/src/hooks/useVerifyConnection.js | 28 +++++++++++++++++ 8 files changed, 37 insertions(+), 92 deletions(-) delete mode 100644 packages/backend/src/graphql/mutations/verify-connection.js delete mode 100644 packages/web/src/graphql/mutations/verify-connection.js create mode 100644 packages/web/src/hooks/useVerifyConnection.js diff --git a/packages/backend/src/graphql/mutation-resolvers.js b/packages/backend/src/graphql/mutation-resolvers.js index 8492caa6..fa95c17c 100644 --- a/packages/backend/src/graphql/mutation-resolvers.js +++ b/packages/backend/src/graphql/mutation-resolvers.js @@ -1,8 +1,3 @@ -// Converted mutations -import verifyConnection from './mutations/verify-connection.js'; - -const mutationResolvers = { - verifyConnection, -}; +const mutationResolvers = {}; export default mutationResolvers; diff --git a/packages/backend/src/graphql/mutations/verify-connection.js b/packages/backend/src/graphql/mutations/verify-connection.js deleted file mode 100644 index fdd95764..00000000 --- a/packages/backend/src/graphql/mutations/verify-connection.js +++ /dev/null @@ -1,29 +0,0 @@ -import App from '../../models/app.js'; -import globalVariable from '../../helpers/global-variable.js'; - -const verifyConnection = async (_parent, params, context) => { - context.currentUser.can('create', 'Connection'); - - let connection = await context.currentUser - .$relatedQuery('connections') - .findOne({ - id: params.input.id, - }) - .throwIfNotFound(); - - const app = await App.findOneByKey(connection.key); - const $ = await globalVariable({ connection, app }); - await app.auth.verifyCredentials($); - - connection = await connection.$query().patchAndFetch({ - verified: true, - draft: false, - }); - - return { - ...connection, - app, - }; -}; - -export default verifyConnection; diff --git a/packages/backend/src/graphql/schema.graphql b/packages/backend/src/graphql/schema.graphql index 84dd1bb4..cade7b74 100644 --- a/packages/backend/src/graphql/schema.graphql +++ b/packages/backend/src/graphql/schema.graphql @@ -3,7 +3,7 @@ type Query { } type Mutation { - verifyConnection(input: VerifyConnectionInput): Connection + placeholderQuery(name: String): Boolean } type Trigger { @@ -184,10 +184,6 @@ type SamlAuthProvidersRoleMapping { remoteRoleName: String } -input VerifyConnectionInput { - id: String! -} - input UserRoleInput { id: String } diff --git a/packages/web/src/graphql/cache.js b/packages/web/src/graphql/cache.js index cfa5eded..5d44dc73 100644 --- a/packages/web/src/graphql/cache.js +++ b/packages/web/src/graphql/cache.js @@ -6,36 +6,7 @@ const cache = new InMemoryCache({ }, Mutation: { mutationType: true, - fields: { - verifyConnection: { - merge(existing, verifiedConnection, { readField, cache }) { - const appKey = readField('key', verifiedConnection); - const appCacheId = cache.identify({ - __typename: 'App', - key: appKey, - }); - cache.modify({ - id: appCacheId, - fields: { - connections: (existingConnections) => { - const existingConnectionIndex = existingConnections.findIndex( - (connection) => { - return connection.__ref === verifiedConnection.__ref; - } - ); - const connectionExists = existingConnectionIndex !== -1; - // newly created and verified connection - if (!connectionExists) { - return [verifiedConnection, ...existingConnections]; - } - return existingConnections; - }, - }, - }); - return verifiedConnection; - }, - }, - }, + fields: {}, }, }, }); diff --git a/packages/web/src/graphql/mutations/index.js b/packages/web/src/graphql/mutations/index.js index c0921b27..ea2bcc28 100644 --- a/packages/web/src/graphql/mutations/index.js +++ b/packages/web/src/graphql/mutations/index.js @@ -1,7 +1,3 @@ -import { VERIFY_CONNECTION } from './verify-connection'; - -const mutations = { - verifyConnection: VERIFY_CONNECTION, -}; +const mutations = {}; export default mutations; diff --git a/packages/web/src/graphql/mutations/verify-connection.js b/packages/web/src/graphql/mutations/verify-connection.js deleted file mode 100644 index 954eed0b..00000000 --- a/packages/web/src/graphql/mutations/verify-connection.js +++ /dev/null @@ -1,17 +0,0 @@ -import { gql } from '@apollo/client'; -export const VERIFY_CONNECTION = gql` - mutation VerifyConnection($input: VerifyConnectionInput) { - verifyConnection(input: $input) { - id - key - verified - formattedData { - screenName - } - createdAt - app { - key - } - } - } -`; diff --git a/packages/web/src/hooks/useAuthenticateApp.ee.js b/packages/web/src/hooks/useAuthenticateApp.ee.js index ebb8c97a..81d8e6fb 100644 --- a/packages/web/src/hooks/useAuthenticateApp.ee.js +++ b/packages/web/src/hooks/useAuthenticateApp.ee.js @@ -12,6 +12,7 @@ import useCreateConnection from './useCreateConnection'; import useCreateConnectionAuthUrl from './useCreateConnectionAuthUrl'; import useUpdateConnection from './useUpdateConnection'; import useResetConnection from './useResetConnection'; +import useVerifyConnection from './useVerifyConnection'; function getSteps(auth, hasConnection, useShared) { if (hasConnection) { @@ -39,6 +40,7 @@ export default function useAuthenticateApp(payload) { React.useState(false); const formatMessage = useFormatMessage(); const steps = getSteps(auth?.data, !!connectionId, useShared); + const { mutateAsync: verifyConnection } = useVerifyConnection(); const authenticate = React.useMemo(() => { if (!steps?.length) return; @@ -91,6 +93,9 @@ export default function useAuthenticateApp(payload) { const stepResponse = await resetConnection(response.connectionId); response[step.name] = stepResponse.data; + } else if (step.name === 'verifyConnection') { + const stepResponse = await verifyConnection(variables?.id); + response[step.name] = stepResponse?.data; } else { const stepResponse = await processMutation(step.name, variables); response[step.name] = stepResponse; diff --git a/packages/web/src/hooks/useVerifyConnection.js b/packages/web/src/hooks/useVerifyConnection.js new file mode 100644 index 00000000..bafc7c40 --- /dev/null +++ b/packages/web/src/hooks/useVerifyConnection.js @@ -0,0 +1,28 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import api from 'helpers/api'; + +export default function useVerifyConnection() { + const queryClient = useQueryClient(); + + const query = useMutation({ + mutationFn: async (connectionId) => { + try { + const { data } = await api.post( + `/v1/connections/${connectionId}/verify`, + ); + + return data; + } catch { + throw new Error('Failed while verifying connection!'); + } + }, + onSuccess: (data) => { + const appKey = data?.data.key; + queryClient.invalidateQueries({ + queryKey: ['apps', appKey, 'connections'], + }); + }, + }); + + return query; +} From 244eeeb816ee98794b5c65f4ac99a83ec1828768 Mon Sep 17 00:00:00 2001 From: Ali BARIN Date: Tue, 24 Sep 2024 12:11:02 +0000 Subject: [PATCH 09/12] feat(useAuthenticateApp): centralize invalidating queries --- .../web/src/hooks/useAuthenticateApp.ee.js | 27 ++++++++++++++----- packages/web/src/hooks/useCreateConnection.js | 10 +------ packages/web/src/hooks/useVerifyConnection.js | 10 +------ 3 files changed, 23 insertions(+), 24 deletions(-) diff --git a/packages/web/src/hooks/useAuthenticateApp.ee.js b/packages/web/src/hooks/useAuthenticateApp.ee.js index 81d8e6fb..86184d1a 100644 --- a/packages/web/src/hooks/useAuthenticateApp.ee.js +++ b/packages/web/src/hooks/useAuthenticateApp.ee.js @@ -1,4 +1,5 @@ import * as React from 'react'; +import { useQueryClient } from '@tanstack/react-query'; import { processMutation, @@ -32,6 +33,7 @@ function getSteps(auth, hasConnection, useShared) { export default function useAuthenticateApp(payload) { const { appKey, appAuthClientId, connectionId, useShared = false } = payload; const { data: auth } = useAppAuth(appKey); + const queryClient = useQueryClient(); const { mutateAsync: createConnection } = useCreateConnection(appKey); const { mutateAsync: createConnectionAuthUrl } = useCreateConnectionAuthUrl(); const { mutateAsync: updateConnection } = useUpdateConnection(); @@ -94,7 +96,9 @@ export default function useAuthenticateApp(payload) { response[step.name] = stepResponse.data; } else if (step.name === 'verifyConnection') { - const stepResponse = await verifyConnection(variables?.id); + const stepResponse = await verifyConnection( + response.connectionId, + ); response[step.name] = stepResponse?.data; } else { const stepResponse = await processMutation(step.name, variables); @@ -107,26 +111,37 @@ export default function useAuthenticateApp(payload) { } catch (err) { console.log(err); setAuthenticationInProgress(false); + + queryClient.invalidateQueries({ + queryKey: ['apps', appKey, 'connections'], + }); + throw err; } - stepIndex++; - if (stepIndex === steps.length) { - return response; - } - setAuthenticationInProgress(false); + stepIndex++; } + + await queryClient.invalidateQueries({ + queryKey: ['apps', appKey, 'connections'], + }); + + setAuthenticationInProgress(false); + + return response; }; }, [ steps, appKey, appAuthClientId, connectionId, + queryClient, formatMessage, createConnection, createConnectionAuthUrl, updateConnection, resetConnection, + verifyConnection, ]); return { diff --git a/packages/web/src/hooks/useCreateConnection.js b/packages/web/src/hooks/useCreateConnection.js index afcbe458..6ba59f05 100644 --- a/packages/web/src/hooks/useCreateConnection.js +++ b/packages/web/src/hooks/useCreateConnection.js @@ -1,10 +1,8 @@ -import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { useMutation } from '@tanstack/react-query'; import api from 'helpers/api'; export default function useCreateConnection(appKey) { - const queryClient = useQueryClient(); - const query = useMutation({ mutationFn: async ({ appAuthClientId, formattedData }) => { const { data } = await api.post(`/v1/apps/${appKey}/connections`, { @@ -14,12 +12,6 @@ export default function useCreateConnection(appKey) { return data; }, - - onSuccess: () => { - queryClient.invalidateQueries({ - queryKey: ['apps', appKey, 'connections'], - }); - }, }); return query; diff --git a/packages/web/src/hooks/useVerifyConnection.js b/packages/web/src/hooks/useVerifyConnection.js index bafc7c40..e47a2b8b 100644 --- a/packages/web/src/hooks/useVerifyConnection.js +++ b/packages/web/src/hooks/useVerifyConnection.js @@ -1,9 +1,7 @@ -import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { useMutation } from '@tanstack/react-query'; import api from 'helpers/api'; export default function useVerifyConnection() { - const queryClient = useQueryClient(); - const query = useMutation({ mutationFn: async (connectionId) => { try { @@ -16,12 +14,6 @@ export default function useVerifyConnection() { throw new Error('Failed while verifying connection!'); } }, - onSuccess: (data) => { - const appKey = data?.data.key; - queryClient.invalidateQueries({ - queryKey: ['apps', appKey, 'connections'], - }); - }, }); return query; From 589fe0f5f3ddccf4c897423e94abe249d1d3193b Mon Sep 17 00:00:00 2001 From: Ali BARIN Date: Tue, 24 Sep 2024 12:12:29 +0000 Subject: [PATCH 10/12] feat(useAuthenticateApp): remove redundant graphql implementation --- packages/web/src/graphql/mutations/index.js | 3 --- packages/web/src/helpers/authenticationSteps.js | 17 ----------------- packages/web/src/hooks/useAuthenticateApp.ee.js | 4 ---- 3 files changed, 24 deletions(-) delete mode 100644 packages/web/src/graphql/mutations/index.js diff --git a/packages/web/src/graphql/mutations/index.js b/packages/web/src/graphql/mutations/index.js deleted file mode 100644 index ea2bcc28..00000000 --- a/packages/web/src/graphql/mutations/index.js +++ /dev/null @@ -1,3 +0,0 @@ -const mutations = {}; - -export default mutations; diff --git a/packages/web/src/helpers/authenticationSteps.js b/packages/web/src/helpers/authenticationSteps.js index a6194d14..3da27fb1 100644 --- a/packages/web/src/helpers/authenticationSteps.js +++ b/packages/web/src/helpers/authenticationSteps.js @@ -1,20 +1,3 @@ -import apolloClient from 'graphql/client'; -import MUTATIONS from 'graphql/mutations'; - -export const processMutation = async (mutationName, variables) => { - const mutation = MUTATIONS[mutationName]; - const mutationResponse = await apolloClient.mutate({ - mutation, - variables: { input: variables }, - context: { - autoSnackbar: false, - }, - }); - const responseData = mutationResponse.data[mutationName]; - - return responseData; -}; - const parseUrlSearchParams = (event) => { const searchParams = new URLSearchParams(event.data.payload.search); const hashParams = new URLSearchParams(event.data.payload.hash.substring(1)); diff --git a/packages/web/src/hooks/useAuthenticateApp.ee.js b/packages/web/src/hooks/useAuthenticateApp.ee.js index 86184d1a..b0b99b09 100644 --- a/packages/web/src/hooks/useAuthenticateApp.ee.js +++ b/packages/web/src/hooks/useAuthenticateApp.ee.js @@ -2,7 +2,6 @@ import * as React from 'react'; import { useQueryClient } from '@tanstack/react-query'; import { - processMutation, processOpenWithPopup, processPopupMessage, } from 'helpers/authenticationSteps'; @@ -100,9 +99,6 @@ export default function useAuthenticateApp(payload) { response.connectionId, ); response[step.name] = stepResponse?.data; - } else { - const stepResponse = await processMutation(step.name, variables); - response[step.name] = stepResponse; } } else if (step.type === 'openWithPopup') { const stepResponse = await processPopupMessage(popup); From d6e78a48a0173d07799afdd9dbd56f8335f9e2aa Mon Sep 17 00:00:00 2001 From: Ali BARIN Date: Tue, 24 Sep 2024 14:38:38 +0000 Subject: [PATCH 11/12] feat(AddAppConnection): show meaningful error messages only --- packages/backend/src/helpers/error-handler.js | 2 +- .../src/components/AddAppConnection/index.jsx | 24 +++++++++---------- packages/web/src/hooks/useVerifyConnection.js | 8 +++++-- 3 files changed, 19 insertions(+), 15 deletions(-) diff --git a/packages/backend/src/helpers/error-handler.js b/packages/backend/src/helpers/error-handler.js index 63c80bbb..7dcac4a3 100644 --- a/packages/backend/src/helpers/error-handler.js +++ b/packages/backend/src/helpers/error-handler.js @@ -50,7 +50,7 @@ const errorHandler = (error, request, response, next) => { }, }; - response.status(200).json(httpErrorPayload); + response.status(422).json(httpErrorPayload); } if (error instanceof NotAuthorizedError) { diff --git a/packages/web/src/components/AddAppConnection/index.jsx b/packages/web/src/components/AddAppConnection/index.jsx index bb019cc6..d80191d7 100644 --- a/packages/web/src/components/AddAppConnection/index.jsx +++ b/packages/web/src/components/AddAppConnection/index.jsx @@ -26,7 +26,8 @@ function AddAppConnection(props) { const navigate = useNavigate(); const [searchParams] = useSearchParams(); const formatMessage = useFormatMessage(); - const [error, setError] = React.useState(null); + const [errorMessage, setErrorMessage] = React.useState(null); + const [errorDetails, setErrorDetails] = React.useState(null); const [inProgress, setInProgress] = React.useState(false); const hasConnection = Boolean(connectionId); const useShared = searchParams.get('shared') === 'true'; @@ -76,7 +77,8 @@ function AddAppConnection(props) { async (data) => { if (!authenticate) return; setInProgress(true); - setError(null); + setErrorMessage(null); + setErrorDetails(null); try { const response = await authenticate({ fields: data, @@ -85,21 +87,19 @@ function AddAppConnection(props) { await queryClient.invalidateQueries({ queryKey: ['apps', key, 'connections'], }); + onClose(response); } catch (err) { const error = err; console.log(error); - if (error.message) { - setError(error); - } else { - setError(error.graphQLErrors?.[0]); - } + setErrorMessage(error.message); + setErrorDetails(error?.response?.data?.errors); } finally { setInProgress(false); } }, - [authenticate], + [authenticate, key, onClose, queryClient], ); if (useShared) @@ -134,16 +134,16 @@ function AddAppConnection(props) { )} - {error && ( + {(errorMessage || errorDetails) && ( - {error.message} - {error.details && ( + {!errorDetails && errorMessage} + {errorDetails && (
-              {JSON.stringify(error.details, null, 2)}
+              {JSON.stringify(errorDetails, null, 2)}
             
)}
diff --git a/packages/web/src/hooks/useVerifyConnection.js b/packages/web/src/hooks/useVerifyConnection.js index e47a2b8b..78e4a87b 100644 --- a/packages/web/src/hooks/useVerifyConnection.js +++ b/packages/web/src/hooks/useVerifyConnection.js @@ -10,8 +10,12 @@ export default function useVerifyConnection() { ); return data; - } catch { - throw new Error('Failed while verifying connection!'); + } catch (err) { + if (err.response.status === 500) { + throw new Error('Failed while verifying connection!'); + } + + throw err; } }, }); From b1f2727bebb2e3005d40dc17e557770e6edec9c6 Mon Sep 17 00:00:00 2001 From: Ali BARIN Date: Wed, 25 Sep 2024 09:51:48 +0000 Subject: [PATCH 12/12] test(create-dynamic-data): use 422 for error response --- .../src/controllers/api/v1/steps/create-dynamic-data.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/src/controllers/api/v1/steps/create-dynamic-data.test.js b/packages/backend/src/controllers/api/v1/steps/create-dynamic-data.test.js index a83ac23b..b8028ccb 100644 --- a/packages/backend/src/controllers/api/v1/steps/create-dynamic-data.test.js +++ b/packages/backend/src/controllers/api/v1/steps/create-dynamic-data.test.js @@ -169,7 +169,7 @@ describe('POST /api/v1/steps/:stepId/dynamic-data', () => { dynamicDataKey: 'listRepos', parameters: {}, }) - .expect(200); + .expect(422); expect(response.body.errors).toEqual(errors); });