diff --git a/packages/backend/src/models/__snapshots__/connection.test.js.snap b/packages/backend/src/models/__snapshots__/connection.test.js.snap new file mode 100644 index 00000000..9fc77caf --- /dev/null +++ b/packages/backend/src/models/__snapshots__/connection.test.js.snap @@ -0,0 +1,51 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`Connection model > jsonSchema should have correct validations 1`] = ` +{ + "properties": { + "appAuthClientId": { + "format": "uuid", + "type": "string", + }, + "createdAt": { + "type": "string", + }, + "data": { + "type": "string", + }, + "deletedAt": { + "type": "string", + }, + "draft": { + "type": "boolean", + }, + "formattedData": { + "type": "object", + }, + "id": { + "format": "uuid", + "type": "string", + }, + "key": { + "maxLength": 255, + "minLength": 1, + "type": "string", + }, + "updatedAt": { + "type": "string", + }, + "userId": { + "format": "uuid", + "type": "string", + }, + "verified": { + "default": false, + "type": "boolean", + }, + }, + "required": [ + "key", + ], + "type": "object", +} +`; diff --git a/packages/backend/src/models/connection.js b/packages/backend/src/models/connection.js index 26d139e7..225d31c6 100644 --- a/packages/backend/src/models/connection.js +++ b/packages/backend/src/models/connection.js @@ -160,35 +160,6 @@ class Connection extends Base { return this; } - // TODO: Make another abstraction like beforeSave instead of using - // beforeInsert and beforeUpdate separately for the same operation. - async $beforeInsert(queryContext) { - await super.$beforeInsert(queryContext); - - await this.checkEligibilityForCreation(); - - this.encryptData(); - } - - async $beforeUpdate(opt, queryContext) { - await super.$beforeUpdate(opt, queryContext); - this.encryptData(); - } - - async $afterFind() { - this.decryptData(); - } - - async $afterInsert(queryContext) { - await super.$afterInsert(queryContext); - Telemetry.connectionCreated(this); - } - - async $afterUpdate(opt, queryContext) { - await super.$afterUpdate(opt, queryContext); - Telemetry.connectionUpdated(this); - } - async getApp() { if (!this.key) return null; @@ -278,6 +249,35 @@ class Connection extends Base { }, }); } + + // TODO: Make another abstraction like beforeSave instead of using + // beforeInsert and beforeUpdate separately for the same operation. + async $beforeInsert(queryContext) { + await super.$beforeInsert(queryContext); + + await this.checkEligibilityForCreation(); + + this.encryptData(); + } + + async $beforeUpdate(opt, queryContext) { + await super.$beforeUpdate(opt, queryContext); + this.encryptData(); + } + + async $afterFind() { + this.decryptData(); + } + + async $afterInsert(queryContext) { + await super.$afterInsert(queryContext); + Telemetry.connectionCreated(this); + } + + async $afterUpdate(opt, queryContext) { + await super.$afterUpdate(opt, queryContext); + Telemetry.connectionUpdated(this); + } } export default Connection; diff --git a/packages/backend/src/models/connection.test.js b/packages/backend/src/models/connection.test.js new file mode 100644 index 00000000..d9ce4795 --- /dev/null +++ b/packages/backend/src/models/connection.test.js @@ -0,0 +1,163 @@ +import { describe, it, expect, vi } from 'vitest'; +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 AppConfig from './app-config.js'; +import Base from './base.js'; +import Connection from './connection'; +import Step from './step.js'; +import User from './user.js'; + +describe('Connection model', () => { + it('tableName should return correct name', () => { + expect(Connection.tableName).toBe('connections'); + }); + + it('jsonSchema should have correct validations', () => { + expect(Connection.jsonSchema).toMatchSnapshot(); + }); + + it('virtualAttributes should return correct attributes', () => { + const virtualAttributes = Connection.virtualAttributes; + + const expectedAttributes = ['reconnectable']; + + expect(virtualAttributes).toStrictEqual(expectedAttributes); + }); + + it('relationMappings should return correct associations', () => { + const relationMappings = Connection.relationMappings(); + + 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', + }, + }, + triggerSteps: { + relation: Base.HasManyRelation, + modelClass: Step, + join: { + from: 'connections.id', + to: 'steps.connection_id', + }, + filter: expect.any(Function), + }, + 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', + }, + }, + }; + + expect(relationMappings).toStrictEqual(expectedRelations); + }); + + describe.todo('reconnectable'); + + describe('encryptData', () => { + it('should return undefined if eligibleForEncryption is not true', async () => { + vi.spyOn(Connection.prototype, 'eligibleForEncryption').mockReturnValue( + false + ); + + const connection = new Connection(); + + expect(connection.encryptData()).toBeUndefined(); + }); + + it('should encrypt formattedData and set it to data', async () => { + vi.spyOn(Connection.prototype, 'eligibleForEncryption').mockReturnValue( + true + ); + + const formattedData = { + key: 'value', + }; + + const connection = new Connection(); + connection.formattedData = formattedData; + connection.encryptData(); + + const expectedDecryptedValue = JSON.parse( + AES.decrypt(connection.data, appConfig.encryptionKey).toString(enc) + ); + + expect(formattedData).toStrictEqual(expectedDecryptedValue); + expect(connection.data).not.toEqual(formattedData); + }); + + it('should encrypt formattedData and remove formattedData', async () => { + vi.spyOn(Connection.prototype, 'eligibleForEncryption').mockReturnValue( + true + ); + + const formattedData = { + key: 'value', + }; + + const connection = new Connection(); + connection.formattedData = formattedData; + connection.encryptData(); + + expect(connection.formattedData).not.toBeDefined(); + }); + }); + + describe('decryptData', () => { + it('should return undefined if eligibleForDecryption is not true', () => { + vi.spyOn(Connection.prototype, 'eligibleForDecryption').mockReturnValue( + false + ); + + const connection = new Connection(); + + expect(connection.decryptData()).toBeUndefined(); + }); + + it('should decrypt data and set it to formattedData', async () => { + vi.spyOn(Connection.prototype, 'eligibleForDecryption').mockReturnValue( + true + ); + + const formattedData = { + key: 'value', + }; + + const data = AES.encrypt( + JSON.stringify(formattedData), + appConfig.encryptionKey + ).toString(); + + const connection = new Connection(); + connection.data = data; + connection.decryptData(); + + expect(connection.formattedData).toStrictEqual(formattedData); + expect(connection.data).not.toEqual(formattedData); + }); + }); +});