From 7caa055e0046295432c8cdc0518c579ca1a03f11 Mon Sep 17 00:00:00 2001 From: Ali BARIN Date: Thu, 17 Oct 2024 08:07:50 +0000 Subject: [PATCH 1/2] test(connection): write model tests --- .../api/v1/connections/update-connection.js | 2 +- packages/backend/src/models/connection.js | 26 +- .../backend/src/models/connection.test.js | 414 ++++++++++++++++-- 3 files changed, 389 insertions(+), 53 deletions(-) diff --git a/packages/backend/src/controllers/api/v1/connections/update-connection.js b/packages/backend/src/controllers/api/v1/connections/update-connection.js index 540d6679..5d84e797 100644 --- a/packages/backend/src/controllers/api/v1/connections/update-connection.js +++ b/packages/backend/src/controllers/api/v1/connections/update-connection.js @@ -8,7 +8,7 @@ export default async (request, response) => { }) .throwIfNotFound(); - connection = await connection.update(connectionParams(request)); + connection = await connection.updateFormattedData(connectionParams(request)); renderObject(response, connection); }; diff --git a/packages/backend/src/models/connection.js b/packages/backend/src/models/connection.js index 225d31c6..c743bbdc 100644 --- a/packages/backend/src/models/connection.js +++ b/packages/backend/src/models/connection.js @@ -122,10 +122,20 @@ class Connection extends Base { return this.data ? true : false; } - async checkEligibilityForCreation() { - const app = await App.findOneByKey(this.key); + async getApp() { + if (!this.key) return null; - const appConfig = await AppConfig.query().findOne({ key: this.key }); + return await App.findOneByKey(this.key); + } + + async getAppConfig() { + return await AppConfig.query().findOne({ key: this.key }); + } + + async checkEligibilityForCreation() { + const app = await this.getApp(); + + const appConfig = await this.getAppConfig(); if (appConfig) { if (appConfig.disabled) { @@ -160,12 +170,6 @@ class Connection extends Base { return this; } - async getApp() { - if (!this.key) return null; - - return await App.findOneByKey(this.key); - } - async testAndUpdateConnection() { const app = await this.getApp(); const $ = await globalVariable({ connection: this, app }); @@ -224,7 +228,7 @@ class Connection extends Base { async reset() { const formattedData = this?.formattedData?.screenName ? { screenName: this.formattedData.screenName } - : null; + : {}; const updatedConnection = await this.$query().patchAndFetch({ formattedData, @@ -233,7 +237,7 @@ class Connection extends Base { return updatedConnection; } - async update({ formattedData, appAuthClientId }) { + async updateFormattedData({ formattedData, appAuthClientId }) { if (appAuthClientId) { const appAuthClient = await AppAuthClient.query() .findById(appAuthClientId) diff --git a/packages/backend/src/models/connection.test.js b/packages/backend/src/models/connection.test.js index d9ce4795..35abb18e 100644 --- a/packages/backend/src/models/connection.test.js +++ b/packages/backend/src/models/connection.test.js @@ -3,11 +3,15 @@ import AES from 'crypto-js/aes.js'; import enc from 'crypto-js/enc-utf8.js'; import appConfig from '../config/app.js'; import AppAuthClient from './app-auth-client.js'; +import App from './app.js'; import AppConfig from './app-config.js'; import Base from './base.js'; import Connection from './connection'; import Step from './step.js'; import User from './user.js'; +import { createConnection } from '../../test/factories/connection.js'; +import { createAppConfig } from '../../test/factories/app-config.js'; +import { createAppAuthClient } from '../../test/factories/app-auth-client.js'; describe('Connection model', () => { it('tableName should return correct name', () => { @@ -26,54 +30,65 @@ describe('Connection model', () => { expect(virtualAttributes).toStrictEqual(expectedAttributes); }); - it('relationMappings should return correct associations', () => { - const relationMappings = Connection.relationMappings(); + describe('relationMappings', () => { + it('should return correct associations', () => { + const relationMappings = Connection.relationMappings(); - const expectedRelations = { - user: { - relation: Base.BelongsToOneRelation, - modelClass: User, - join: { - from: 'connections.user_id', - to: 'users.id', + const expectedRelations = { + user: { + relation: Base.BelongsToOneRelation, + modelClass: User, + join: { + from: 'connections.user_id', + to: 'users.id', + }, }, - }, - steps: { - relation: Base.HasManyRelation, - modelClass: Step, - join: { - from: 'connections.id', - to: 'steps.connection_id', + steps: { + relation: Base.HasManyRelation, + modelClass: Step, + join: { + from: 'connections.id', + to: 'steps.connection_id', + }, }, - }, - triggerSteps: { - relation: Base.HasManyRelation, - modelClass: Step, - join: { - from: 'connections.id', - to: 'steps.connection_id', + triggerSteps: { + relation: Base.HasManyRelation, + modelClass: Step, + join: { + from: 'connections.id', + to: 'steps.connection_id', + }, + filter: expect.any(Function), }, - filter: expect.any(Function), - }, - appConfig: { - relation: Base.BelongsToOneRelation, - modelClass: AppConfig, - join: { - from: 'connections.key', - to: 'app_configs.key', + appConfig: { + relation: Base.BelongsToOneRelation, + modelClass: AppConfig, + join: { + from: 'connections.key', + to: 'app_configs.key', + }, }, - }, - appAuthClient: { - relation: Base.BelongsToOneRelation, - modelClass: AppAuthClient, - join: { - from: 'connections.app_auth_client_id', - to: 'app_auth_clients.id', + appAuthClient: { + relation: Base.BelongsToOneRelation, + modelClass: AppAuthClient, + join: { + from: 'connections.app_auth_client_id', + to: 'app_auth_clients.id', + }, }, - }, - }; + }; - expect(relationMappings).toStrictEqual(expectedRelations); + expect(relationMappings).toStrictEqual(expectedRelations); + }); + + it('triggerSteps should return only trigger typed steps', () => { + const relations = Connection.relationMappings(); + const whereSpy = vi.fn(); + + relations.triggerSteps.filter({ where: whereSpy }); + + expect(whereSpy).toHaveBeenCalledWith('type', '=', 'trigger'); + }); }); describe.todo('reconnectable'); @@ -160,4 +175,321 @@ describe('Connection model', () => { expect(connection.data).not.toEqual(formattedData); }); }); + + describe('eligibleForEncryption', () => { + it('should access formattedData', async () => { + const connection = new Connection(); + connection.formattedData = { clientId: 'sample-id' }; + + const spy = vi.spyOn(connection, 'formattedData', 'get'); + + connection.eligibleForEncryption(); + + expect(spy).toHaveBeenCalledOnce(); + }); + + it('should return true when formattedData property exists', async () => { + const connection = new Connection(); + connection.formattedData = { clientId: 'sample-id' }; + + expect(connection.eligibleForEncryption()).toBe(true); + }); + + it("should return false when formattedData property doesn't exist", async () => { + const connection = new Connection(); + connection.formattedData = undefined; + + expect(connection.eligibleForEncryption()).toBe(false); + }); + }); + + describe('eligibleForDecryption', () => { + it('should access formattedData', async () => { + const connection = new Connection(); + connection.data = 'encrypted-data'; + + const spy = vi.spyOn(connection, 'data', 'get'); + + connection.eligibleForDecryption(); + + expect(spy).toHaveBeenCalledOnce(); + }); + + it('should return true when data property exists', async () => { + const connection = new Connection(); + connection.data = 'encrypted-data'; + + expect(connection.eligibleForDecryption()).toBe(true); + }); + + it("should return false when data property doesn't exist", async () => { + const connection = new Connection(); + connection.data = undefined; + + expect(connection.eligibleForDecryption()).toBe(false); + }); + }); + + describe('getApp', () => { + it('should return connection app when valid key exists', async () => { + const connection = new Connection(); + connection.key = 'gitlab'; + + const connectionApp = await connection.getApp(); + const app = await App.findOneByKey('gitlab'); + + expect(connectionApp).toStrictEqual(app); + }); + + it('should throw an error when invalid key exists', async () => { + const connection = new Connection(); + connection.key = 'invalid-key'; + + await expect(() => connection.getApp()).rejects.toThrowError( + `An application with the "invalid-key" key couldn't be found.` + ); + }); + + it('should return null when no key exists', async () => { + const connection = new Connection(); + + await expect(connection.getApp()).resolves.toBe(null); + }); + }); + + it('getAppConfig should return connection app config', async () => { + const connection = new Connection(); + connection.key = 'gitlab'; + + const appConfig = await createAppConfig({ key: 'gitlab' }); + + const connectionAppConfig = await connection.getAppConfig(); + + expect(connectionAppConfig).toStrictEqual(appConfig); + }); + + describe.todo('checkEligibilityForCreation', async () => {}); + + describe('testAndUpdateConnection', () => { + it('should verify connection and persist it', async () => { + const connection = await createConnection({ verified: false }); + + const isStillVerifiedSpy = vi.fn().mockReturnValue(true); + + const originalApp = await connection.getApp(); + + const getAppSpy = vi + .spyOn(connection, 'getApp') + .mockImplementation(() => { + return { + ...originalApp, + auth: { + ...originalApp.auth, + isStillVerified: isStillVerifiedSpy, + }, + }; + }); + + const updatedConnection = await connection.testAndUpdateConnection(); + + expect(getAppSpy).toHaveBeenCalledOnce(); + expect(isStillVerifiedSpy).toHaveBeenCalledOnce(); + expect(updatedConnection.verified).toBe(true); + }); + + it.todo('should unverify connection and persist it'); + }); + + describe('verifyAndUpdateConnection', () => { + it('should verify connection with valid token', async () => { + const connection = await createConnection({ + verified: false, + draft: true, + }); + + const verifyCredentialsSpy = vi.fn().mockResolvedValue(true); + + const originalApp = await connection.getApp(); + + vi.spyOn(connection, 'getApp').mockImplementation(() => { + return { + ...originalApp, + auth: { + ...originalApp.auth, + verifyCredentials: verifyCredentialsSpy, + }, + }; + }); + + const updatedConnection = await connection.verifyAndUpdateConnection(); + + expect(verifyCredentialsSpy).toHaveBeenCalledOnce(); + expect(updatedConnection.verified).toBe(true); + expect(updatedConnection.draft).toBe(false); + }); + + it('should throw an error with invalid token', async () => { + const connection = await createConnection({ + verified: false, + draft: true, + }); + + const verifyCredentialsSpy = vi + .fn() + .mockRejectedValue(new Error('Invalid token!')); + + const originalApp = await connection.getApp(); + + vi.spyOn(connection, 'getApp').mockImplementation(() => { + return { + ...originalApp, + auth: { + ...originalApp.auth, + verifyCredentials: verifyCredentialsSpy, + }, + }; + }); + + await expect(() => + connection.verifyAndUpdateConnection() + ).rejects.toThrowError('Invalid token!'); + expect(verifyCredentialsSpy).toHaveBeenCalledOnce(); + }); + }); + + describe('verifyWebhook', () => { + it('should verify webhook on remote', async () => { + const connection = await createConnection({ key: 'typeform' }); + + const verifyWebhookSpy = vi.fn().mockResolvedValue('verified-webhook'); + + const originalApp = await connection.getApp(); + + vi.spyOn(connection, 'getApp').mockImplementation(() => { + return { + ...originalApp, + auth: { + ...originalApp.auth, + verifyWebhook: verifyWebhookSpy, + }, + }; + }); + + expect(await connection.verifyWebhook()).toBe('verified-webhook'); + }); + + it('should return true if connection does not have value in key property', async () => { + const connection = await createConnection({ key: null }); + + expect(await connection.verifyWebhook()).toBe(true); + }); + + it('should throw an error at failed webhook verification', async () => { + const connection = await createConnection({ key: 'typeform' }); + + const verifyWebhookSpy = vi.fn().mockRejectedValue('unverified-webhook'); + + const originalApp = await connection.getApp(); + + vi.spyOn(connection, 'getApp').mockImplementation(() => { + return { + ...originalApp, + auth: { + ...originalApp.auth, + verifyWebhook: verifyWebhookSpy, + }, + }; + }); + + await expect(() => connection.verifyWebhook()).rejects.toThrowError( + 'unverified-webhook' + ); + }); + }); + + it('generateAuthUrl should return authentication url', async () => { + const connection = await createConnection({ + key: 'typeform', + formattedData: { + url: 'https://automatisch.io/authentication-url', + }, + }); + + const generateAuthUrlSpy = vi.fn(); + + const originalApp = await connection.getApp(); + + vi.spyOn(connection, 'getApp').mockImplementation(() => { + return { + ...originalApp, + auth: { + ...originalApp.auth, + generateAuthUrl: generateAuthUrlSpy, + }, + }; + }); + + expect(await connection.generateAuthUrl()).toStrictEqual({ + url: 'https://automatisch.io/authentication-url', + }); + }); + + describe('reset', () => { + it('should keep screen name when exists and reset the rest of the formatted data', async () => { + const connection = await createConnection({ + formattedData: { + screenName: 'Sample connection', + token: 'sample-token', + }, + }); + + await connection.reset(); + + const refetchedConnection = await connection.$query(); + + expect(refetchedConnection.formattedData).toStrictEqual({ + screenName: 'Sample connection', + }); + }); + + it('should empty formatted data object when screen name does not exist', async () => { + const connection = await createConnection({ + formattedData: { + token: 'sample-token', + }, + }); + + await connection.reset(); + + const refetchedConnection = await connection.$query(); + + expect(refetchedConnection.formattedData).toStrictEqual({}); + }); + }); + + describe('updateFormattedData', () => { + it('should extend connection data with app auth client auth defaults', async () => { + const appAuthClient = await createAppAuthClient({ + formattedAuthDefaults: { + clientId: 'sample-id', + }, + }); + + const connection = await createConnection({ + appAuthClientId: appAuthClient.id, + formattedData: { + token: 'sample-token', + }, + }); + + const updatedConnection = await connection.updateFormattedData({ + appAuthClientId: appAuthClient.id, + }); + + expect(updatedConnection.formattedData).toStrictEqual({ + clientId: 'sample-id', + token: 'sample-token', + }); + }); + }); }); From a49c8602d1e0b22be58ff5e4b9312195d081c2fe Mon Sep 17 00:00:00 2001 From: Faruk AYDIN Date: Mon, 21 Oct 2024 12:32:46 +0200 Subject: [PATCH 2/2] refactor: Remove redundant test cases for connection model --- .../backend/src/models/connection.test.js | 22 ------------------- 1 file changed, 22 deletions(-) diff --git a/packages/backend/src/models/connection.test.js b/packages/backend/src/models/connection.test.js index 35abb18e..56abf594 100644 --- a/packages/backend/src/models/connection.test.js +++ b/packages/backend/src/models/connection.test.js @@ -177,17 +177,6 @@ describe('Connection model', () => { }); describe('eligibleForEncryption', () => { - it('should access formattedData', async () => { - const connection = new Connection(); - connection.formattedData = { clientId: 'sample-id' }; - - const spy = vi.spyOn(connection, 'formattedData', 'get'); - - connection.eligibleForEncryption(); - - expect(spy).toHaveBeenCalledOnce(); - }); - it('should return true when formattedData property exists', async () => { const connection = new Connection(); connection.formattedData = { clientId: 'sample-id' }; @@ -204,17 +193,6 @@ describe('Connection model', () => { }); describe('eligibleForDecryption', () => { - it('should access formattedData', async () => { - const connection = new Connection(); - connection.data = 'encrypted-data'; - - const spy = vi.spyOn(connection, 'data', 'get'); - - connection.eligibleForDecryption(); - - expect(spy).toHaveBeenCalledOnce(); - }); - it('should return true when data property exists', async () => { const connection = new Connection(); connection.data = 'encrypted-data';