Files
automatisch/packages/backend/src/models/connection.test.js

804 lines
23 KiB
JavaScript

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 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 Telemetry from '../helpers/telemetry/index.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', () => {
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);
});
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',
},
},
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);
});
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('reconnectable', () => {
it('should return active status of app auth client when created via app auth client', async () => {
const appAuthClient = await createAppAuthClient({
active: true,
formattedAuthDefaults: {
clientId: 'sample-id',
},
});
const connection = await createConnection({
appAuthClientId: appAuthClient.id,
formattedData: {
token: 'sample-token',
},
});
const connectionWithAppAuthClient = await connection
.$query()
.withGraphFetched({
appAuthClient: true,
});
expect(connectionWithAppAuthClient.reconnectable).toBe(true);
});
it('should return true when app config is not disabled and allows custom connection', async () => {
const appConfig = await createAppConfig({
key: 'gitlab',
disabled: false,
customConnectionAllowed: true,
});
const connection = await createConnection({
key: appConfig.key,
formattedData: {
token: 'sample-token',
},
});
const connectionWithAppAuthClient = await connection
.$query()
.withGraphFetched({
appConfig: true,
});
expect(connectionWithAppAuthClient.reconnectable).toBe(true);
});
it('should return false when app config is disabled or does not allow custom connection', async () => {
const connection = await createConnection({
key: 'gitlab',
formattedData: {
token: 'sample-token',
},
});
await createAppConfig({
key: 'gitlab',
disabled: true,
customConnectionAllowed: false,
});
const connectionWithAppAuthClient = await connection
.$query()
.withGraphFetched({
appConfig: true,
});
expect(connectionWithAppAuthClient.reconnectable).toBe(false);
});
});
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.toStrictEqual(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.toStrictEqual(formattedData);
});
});
describe('eligibleForEncryption', () => {
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 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('checkEligibilityForCreation', () => {
it('should return connection if no app config exists', async () => {
vi.spyOn(Connection.prototype, 'getApp').mockResolvedValue({
name: 'gitlab',
});
vi.spyOn(Connection.prototype, 'getAppConfig').mockResolvedValue();
const connection = new Connection();
expect(await connection.checkEligibilityForCreation()).toBe(connection);
});
it('should throw an error when app does not exist', async () => {
vi.spyOn(Connection.prototype, 'getApp').mockRejectedValue(
new Error(
`An application with the "unexisting-app" key couldn't be found.`
)
);
vi.spyOn(Connection.prototype, 'getAppConfig').mockResolvedValue();
const connection = new Connection();
await expect(() =>
connection.checkEligibilityForCreation()
).rejects.toThrow(
`An application with the "unexisting-app" key couldn't be found.`
);
});
it('should throw an error when app config is disabled', async () => {
vi.spyOn(Connection.prototype, 'getApp').mockResolvedValue({
name: 'gitlab',
});
vi.spyOn(Connection.prototype, 'getAppConfig').mockResolvedValue({
disabled: true,
});
const connection = new Connection();
await expect(() =>
connection.checkEligibilityForCreation()
).rejects.toThrow(
'The application has been disabled for new connections!'
);
});
it('should throw an error when app config does not allow custom connection with formatted data', async () => {
vi.spyOn(Connection.prototype, 'getApp').mockResolvedValue({
name: 'gitlab',
});
vi.spyOn(Connection.prototype, 'getAppConfig').mockResolvedValue({
disabled: false,
customConnectionAllowed: false,
});
const connection = new Connection();
connection.formattedData = {};
await expect(() =>
connection.checkEligibilityForCreation()
).rejects.toThrow(
'New custom connections have been disabled for gitlab!'
);
});
it('should throw an error when app config is not shared with app auth client', async () => {
vi.spyOn(Connection.prototype, 'getApp').mockResolvedValue({
name: 'gitlab',
});
vi.spyOn(Connection.prototype, 'getAppConfig').mockResolvedValue({
disabled: false,
shared: false,
});
const connection = new Connection();
connection.appAuthClientId = 'sample-id';
await expect(() =>
connection.checkEligibilityForCreation()
).rejects.toThrow(
'The connection with the given app auth client is not allowed!'
);
});
it('should apply app auth client auth defaults when creating with shared app auth client', async () => {
await createAppConfig({
key: 'gitlab',
disabled: false,
customConnectionAllowed: true,
shared: true,
});
const appAuthClient = await createAppAuthClient({
appKey: 'gitlab',
active: true,
formattedAuthDefaults: {
clientId: 'sample-id',
},
});
const connection = await createConnection({
key: 'gitlab',
appAuthClientId: appAuthClient.id,
formattedData: null,
});
await connection.checkEligibilityForCreation();
expect(connection.formattedData).toStrictEqual({
clientId: 'sample-id',
});
});
});
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('should unverify connection and persist it', async () => {
const connection = await createConnection({ verified: true });
const isStillVerifiedSpy = vi
.fn()
.mockRejectedValue(new Error('Wrong credentials!'));
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(false);
});
});
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',
});
});
});
describe('$beforeInsert', () => {
it('should call super.$beforeInsert', async () => {
const superBeforeInsertSpy = vi
.spyOn(Base.prototype, '$beforeInsert')
.mockResolvedValue();
await createConnection();
expect(superBeforeInsertSpy).toHaveBeenCalledOnce();
});
it('should call checkEligibilityForCreation', async () => {
const checkEligibilityForCreationSpy = vi
.spyOn(Connection.prototype, 'checkEligibilityForCreation')
.mockResolvedValue();
await createConnection();
expect(checkEligibilityForCreationSpy).toHaveBeenCalledOnce();
});
it('should call encryptData', async () => {
const encryptDataSpy = vi
.spyOn(Connection.prototype, 'encryptData')
.mockResolvedValue();
await createConnection();
expect(encryptDataSpy).toHaveBeenCalledOnce();
});
});
describe('$beforeUpdate', () => {
it('should call super.$beforeUpdate', async () => {
const superBeforeUpdateSpy = vi
.spyOn(Base.prototype, '$beforeUpdate')
.mockResolvedValue();
const connection = await createConnection();
await connection.$query().patch({ verified: false });
expect(superBeforeUpdateSpy).toHaveBeenCalledOnce();
});
it('should call encryptData', async () => {
const connection = await createConnection();
const encryptDataSpy = vi
.spyOn(Connection.prototype, 'encryptData')
.mockResolvedValue();
await connection.$query().patch({ verified: false });
expect(encryptDataSpy).toHaveBeenCalledOnce();
});
});
describe('$afterFind', () => {
it('should call decryptData', async () => {
const connection = await createConnection();
const decryptDataSpy = vi
.spyOn(Connection.prototype, 'decryptData')
.mockResolvedValue();
await connection.$query();
expect(decryptDataSpy).toHaveBeenCalledOnce();
});
});
describe('$afterInsert', () => {
it('should call super.$afterInsert', async () => {
const superAfterInsertSpy = vi.spyOn(Base.prototype, '$afterInsert');
await createConnection();
expect(superAfterInsertSpy).toHaveBeenCalledOnce();
});
it('should call Telemetry.connectionCreated', async () => {
const telemetryConnectionCreatedSpy = vi
.spyOn(Telemetry, 'connectionCreated')
.mockImplementation(() => {});
const connection = await createConnection();
expect(telemetryConnectionCreatedSpy).toHaveBeenCalledWith(connection);
});
});
describe('$afterUpdate', () => {
it('should call super.$afterUpdate', async () => {
const superAfterInsertSpy = vi.spyOn(Base.prototype, '$afterUpdate');
const connection = await createConnection();
await connection.$query().patch({ verified: false });
expect(superAfterInsertSpy).toHaveBeenCalledOnce();
});
it('should call Telemetry.connectionUpdated', async () => {
const telemetryconnectionUpdatedSpy = vi
.spyOn(Telemetry, 'connectionCreated')
.mockImplementation(() => {});
const connection = await createConnection();
await connection.$query().patch({ verified: false });
expect(telemetryconnectionUpdatedSpy).toHaveBeenCalledWith(connection);
});
});
});