diff --git a/packages/backend/src/models/__snapshots__/app.test.js.snap b/packages/backend/src/models/__snapshots__/app.test.js.snap new file mode 100644 index 00000000..0ada15b4 --- /dev/null +++ b/packages/backend/src/models/__snapshots__/app.test.js.snap @@ -0,0 +1,73 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`App model > list should have list of applications keys 1`] = ` +[ + "airtable", + "appwrite", + "azure-openai", + "carbone", + "clickup", + "code", + "cryptography", + "datastore", + "deepl", + "delay", + "discord", + "disqus", + "dropbox", + "filter", + "flickr", + "flowers-software", + "formatter", + "ghost", + "github", + "gitlab", + "google-calendar", + "google-drive", + "google-forms", + "google-sheets", + "google-tasks", + "helix", + "http-request", + "hubspot", + "invoice-ninja", + "jotform", + "mailchimp", + "mailerlite", + "mattermost", + "miro", + "notion", + "ntfy", + "odoo", + "openai", + "pipedrive", + "placetel", + "postgresql", + "pushover", + "reddit", + "removebg", + "rss", + "salesforce", + "scheduler", + "self-hosted-llm", + "signalwire", + "slack", + "smtp", + "spotify", + "strava", + "stripe", + "telegram-bot", + "todoist", + "trello", + "twilio", + "twitter", + "typeform", + "vtiger-crm", + "webhook", + "wordpress", + "xero", + "you-need-a-budget", + "youtube", + "zendesk", +] +`; diff --git a/packages/backend/src/models/app.test.js b/packages/backend/src/models/app.test.js new file mode 100644 index 00000000..acd64d9f --- /dev/null +++ b/packages/backend/src/models/app.test.js @@ -0,0 +1,420 @@ +import { describe, it, expect, vi } from 'vitest'; + +import App from './app.js'; +import * as getAppModule from '../helpers/get-app.js'; +import * as appInfoConverterModule from '../helpers/app-info-converter.js'; + +describe('App model', () => { + it('folderPath should return correct path', () => { + expect(App.folderPath).toEqual( + expect.stringMatching(/\/packages\/backend\/src\/apps$/) + ); + }); + + it('list should have list of applications keys', () => { + expect(App.list).toMatchSnapshot(); + }); + + describe('findAll', () => { + it('should return all applications', async () => { + const apps = await App.findAll(); + + expect(apps.length).toBe(App.list.length); + }); + + it('should return matching applications when name argument is given', async () => { + const apps = await App.findAll('deepl'); + + expect(apps.length).toBe(1); + expect(apps[0].key).toBe('deepl'); + }); + + it('should return matching applications in plain JSON when stripFunc argument is true', async () => { + const appFindOneByNameSpy = vi.spyOn(App, 'findOneByName'); + + await App.findAll('deepl', true); + + expect(appFindOneByNameSpy).toHaveBeenCalledWith('deepl', true); + }); + }); + + describe('findOneByName', () => { + it('should return app info for given app name', async () => { + const getAppSpy = vi + .spyOn(getAppModule, 'default') + .mockImplementation(() => 'mock-app'); + + const appInfoConverterSpy = vi + .spyOn(appInfoConverterModule, 'default') + .mockImplementation(() => 'app-info'); + + const app = await App.findOneByName('DeepL'); + + expect(getAppSpy).toHaveBeenCalledWith('deepl', false); + expect(appInfoConverterSpy).toHaveBeenCalledWith('mock-app'); + expect(app).toStrictEqual('app-info'); + }); + + it('should return app info for given app name in plain JSON when stripFunc argument is true', async () => { + const getAppSpy = vi + .spyOn(getAppModule, 'default') + .mockImplementation(() => 'mock-app'); + + const appInfoConverterSpy = vi + .spyOn(appInfoConverterModule, 'default') + .mockImplementation(() => 'app-info'); + + const app = await App.findOneByName('DeepL', true); + + expect(getAppSpy).toHaveBeenCalledWith('deepl', true); + expect(appInfoConverterSpy).toHaveBeenCalledWith('mock-app'); + expect(app).toStrictEqual('app-info'); + }); + }); + + describe('findOneByKey', () => { + it('should return app info for given app key', async () => { + const getAppSpy = vi + .spyOn(getAppModule, 'default') + .mockImplementation(() => 'mock-app'); + + const appInfoConverterSpy = vi + .spyOn(appInfoConverterModule, 'default') + .mockImplementation(() => 'app-info'); + + const app = await App.findOneByKey('deepl'); + + expect(getAppSpy).toHaveBeenCalledWith('deepl', false); + expect(appInfoConverterSpy).toHaveBeenCalledWith('mock-app'); + expect(app).toStrictEqual('app-info'); + }); + + it('should return app info for given app key in plain JSON when stripFunc argument is true', async () => { + const getAppSpy = vi + .spyOn(getAppModule, 'default') + .mockImplementation(() => 'mock-app'); + + const appInfoConverterSpy = vi + .spyOn(appInfoConverterModule, 'default') + .mockImplementation(() => 'app-info'); + + const app = await App.findOneByKey('deepl', true); + + expect(getAppSpy).toHaveBeenCalledWith('deepl', true); + expect(appInfoConverterSpy).toHaveBeenCalledWith('mock-app'); + expect(app).toStrictEqual('app-info'); + }); + }); + + describe('findAuthByKey', () => { + it('should return app auth for given app key', async () => { + const getAppSpy = vi + .spyOn(getAppModule, 'default') + .mockImplementation(() => ({ auth: 'mock-auth' })); + + const appInfoConverterSpy = vi + .spyOn(appInfoConverterModule, 'default') + .mockImplementation((input) => input); + + const appAuth = await App.findAuthByKey('deepl'); + + expect(getAppSpy).toHaveBeenCalledWith('deepl', false); + expect(appInfoConverterSpy).toHaveBeenCalledWith({ auth: 'mock-auth' }); + expect(appAuth).toStrictEqual('mock-auth'); + }); + + it('should return app auth for given app key in plain JSON when stripFunc argument is true', async () => { + const getAppSpy = vi + .spyOn(getAppModule, 'default') + .mockImplementation(() => ({ auth: 'mock-auth' })); + + const appInfoConverterSpy = vi + .spyOn(appInfoConverterModule, 'default') + .mockImplementation((input) => input); + + const appAuth = await App.findAuthByKey('deepl', true); + + expect(getAppSpy).toHaveBeenCalledWith('deepl', true); + expect(appInfoConverterSpy).toHaveBeenCalledWith({ auth: 'mock-auth' }); + expect(appAuth).toStrictEqual('mock-auth'); + }); + }); + + describe('findTriggersByKey', () => { + it('should return app triggers for given app key', async () => { + const getAppSpy = vi + .spyOn(getAppModule, 'default') + .mockImplementation(() => ({ triggers: 'mock-triggers' })); + + const appInfoConverterSpy = vi + .spyOn(appInfoConverterModule, 'default') + .mockImplementation((input) => input); + + const appTriggers = await App.findTriggersByKey('deepl'); + + expect(getAppSpy).toHaveBeenCalledWith('deepl', false); + expect(appInfoConverterSpy).toHaveBeenCalledWith({ + triggers: 'mock-triggers', + }); + expect(appTriggers).toStrictEqual('mock-triggers'); + }); + + it('should return app triggers for given app key in plain JSON when stripFunc argument is true', async () => { + const getAppSpy = vi + .spyOn(getAppModule, 'default') + .mockImplementation(() => ({ triggers: 'mock-triggers' })); + + const appInfoConverterSpy = vi + .spyOn(appInfoConverterModule, 'default') + .mockImplementation((input) => input); + + const appTriggers = await App.findTriggersByKey('deepl', true); + + expect(getAppSpy).toHaveBeenCalledWith('deepl', true); + expect(appInfoConverterSpy).toHaveBeenCalledWith({ + triggers: 'mock-triggers', + }); + expect(appTriggers).toStrictEqual('mock-triggers'); + }); + }); + + describe('findTriggerSubsteps', () => { + it('should return app trigger substeps for given app key', async () => { + const getAppSpy = vi + .spyOn(getAppModule, 'default') + .mockImplementation(() => ({ + triggers: [{ key: 'mock-trigger', substeps: 'mock-substeps' }], + })); + + const appInfoConverterSpy = vi + .spyOn(appInfoConverterModule, 'default') + .mockImplementation((input) => input); + + const appTriggerSubsteps = await App.findTriggerSubsteps( + 'deepl', + 'mock-trigger' + ); + + expect(getAppSpy).toHaveBeenCalledWith('deepl', false); + expect(appInfoConverterSpy).toHaveBeenCalledWith({ + triggers: [{ key: 'mock-trigger', substeps: 'mock-substeps' }], + }); + expect(appTriggerSubsteps).toStrictEqual('mock-substeps'); + }); + + it('should return app trigger substeps for given app key in plain JSON when stripFunc argument is true', async () => { + const getAppSpy = vi + .spyOn(getAppModule, 'default') + .mockImplementation(() => ({ + triggers: [{ key: 'mock-trigger', substeps: 'mock-substeps' }], + })); + + const appInfoConverterSpy = vi + .spyOn(appInfoConverterModule, 'default') + .mockImplementation((input) => input); + + const appTriggerSubsteps = await App.findTriggerSubsteps( + 'deepl', + 'mock-trigger', + true + ); + + expect(getAppSpy).toHaveBeenCalledWith('deepl', true); + expect(appInfoConverterSpy).toHaveBeenCalledWith({ + triggers: [{ key: 'mock-trigger', substeps: 'mock-substeps' }], + }); + expect(appTriggerSubsteps).toStrictEqual('mock-substeps'); + }); + }); + + describe('findActionsByKey', () => { + it('should return app actions for given app key', async () => { + const getAppSpy = vi + .spyOn(getAppModule, 'default') + .mockImplementation(() => ({ actions: 'mock-actions' })); + + const appInfoConverterSpy = vi + .spyOn(appInfoConverterModule, 'default') + .mockImplementation((input) => input); + + const appActions = await App.findActionsByKey('deepl'); + + expect(getAppSpy).toHaveBeenCalledWith('deepl', false); + expect(appInfoConverterSpy).toHaveBeenCalledWith({ + actions: 'mock-actions', + }); + expect(appActions).toStrictEqual('mock-actions'); + }); + + it('should return app actions for given app key in plain JSON when stripFunc argument is true', async () => { + const getAppSpy = vi + .spyOn(getAppModule, 'default') + .mockImplementation(() => ({ actions: 'mock-actions' })); + + const appInfoConverterSpy = vi + .spyOn(appInfoConverterModule, 'default') + .mockImplementation((input) => input); + + const appActions = await App.findActionsByKey('deepl', true); + + expect(getAppSpy).toHaveBeenCalledWith('deepl', true); + expect(appInfoConverterSpy).toHaveBeenCalledWith({ + actions: 'mock-actions', + }); + expect(appActions).toStrictEqual('mock-actions'); + }); + }); + + describe('findActionSubsteps', () => { + it('should return app action substeps for given app key', async () => { + const getAppSpy = vi + .spyOn(getAppModule, 'default') + .mockImplementation(() => ({ + actions: [{ key: 'mock-action', substeps: 'mock-substeps' }], + })); + + const appInfoConverterSpy = vi + .spyOn(appInfoConverterModule, 'default') + .mockImplementation((input) => input); + + const appActionSubsteps = await App.findActionSubsteps( + 'deepl', + 'mock-action' + ); + + expect(getAppSpy).toHaveBeenCalledWith('deepl', false); + expect(appInfoConverterSpy).toHaveBeenCalledWith({ + actions: [{ key: 'mock-action', substeps: 'mock-substeps' }], + }); + expect(appActionSubsteps).toStrictEqual('mock-substeps'); + }); + + it('should return app action substeps for given app key in plain JSON when stripFunc argument is true', async () => { + const getAppSpy = vi + .spyOn(getAppModule, 'default') + .mockImplementation(() => ({ + actions: [{ key: 'mock-action', substeps: 'mock-substeps' }], + })); + + const appInfoConverterSpy = vi + .spyOn(appInfoConverterModule, 'default') + .mockImplementation((input) => input); + + const appActionSubsteps = await App.findActionSubsteps( + 'deepl', + 'mock-action', + true + ); + + expect(getAppSpy).toHaveBeenCalledWith('deepl', true); + expect(appInfoConverterSpy).toHaveBeenCalledWith({ + actions: [{ key: 'mock-action', substeps: 'mock-substeps' }], + }); + expect(appActionSubsteps).toStrictEqual('mock-substeps'); + }); + }); + + describe('checkAppAndAction', () => { + it('should return undefined when app and action exist', async () => { + const findOneByKeySpy = vi + .spyOn(App, 'findOneByKey') + .mockImplementation(() => ({ + actions: [ + { + key: 'translate-text', + }, + ], + })); + + const appAndActionExist = await App.checkAppAndAction( + 'deepl', + 'translate-text' + ); + + expect(findOneByKeySpy).toHaveBeenCalledWith('deepl'); + expect(appAndActionExist).toBeUndefined(); + }); + + it('should return undefined when app exists without action argument provided', async () => { + const actionFindSpy = vi.fn(); + const findOneByKeySpy = vi + .spyOn(App, 'findOneByKey') + .mockImplementation(() => ({ + actions: { + find: actionFindSpy, + }, + })); + + const appAndActionExist = await App.checkAppAndAction('deepl'); + + expect(findOneByKeySpy).toHaveBeenCalledWith('deepl'); + expect(actionFindSpy).not.toHaveBeenCalled(); + expect(appAndActionExist).toBeUndefined(); + }); + + it('should throw an error when app exists, but action does not', async () => { + const findOneByKeySpy = vi + .spyOn(App, 'findOneByKey') + .mockImplementation(() => ({ name: 'deepl' })); + + await expect(() => + App.checkAppAndAction('deepl', 'non-existing-action') + ).rejects.toThrowError( + 'deepl does not have an action with the "non-existing-action" key!' + ); + expect(findOneByKeySpy).toHaveBeenCalledWith('deepl'); + }); + }); + + describe('checkAppAndTrigger', () => { + it('should return undefined when app and trigger exist', async () => { + const findOneByKeySpy = vi + .spyOn(App, 'findOneByKey') + .mockImplementation(() => ({ + triggers: [ + { + key: 'catch-raw-webhook', + }, + ], + })); + + const appAndTriggerExist = await App.checkAppAndTrigger( + 'webhook', + 'catch-raw-webhook' + ); + + expect(findOneByKeySpy).toHaveBeenCalledWith('webhook'); + expect(appAndTriggerExist).toBeUndefined(); + }); + + it('should return undefined when app exists without trigger argument provided', async () => { + const triggerFindSpy = vi.fn(); + const findOneByKeySpy = vi + .spyOn(App, 'findOneByKey') + .mockImplementation(() => ({ + actions: { + find: triggerFindSpy, + }, + })); + + const appAndTriggerExist = await App.checkAppAndTrigger('webhook'); + + expect(findOneByKeySpy).toHaveBeenCalledWith('webhook'); + expect(triggerFindSpy).not.toHaveBeenCalled(); + expect(appAndTriggerExist).toBeUndefined(); + }); + + it('should throw an error when app exists, but trigger does not', async () => { + const findOneByKeySpy = vi + .spyOn(App, 'findOneByKey') + .mockImplementation(() => ({ name: 'webhook' })); + + await expect(() => + App.checkAppAndTrigger('webhook', 'non-existing-trigger') + ).rejects.toThrowError( + 'webhook does not have a trigger with the "non-existing-trigger" key!' + ); + expect(findOneByKeySpy).toHaveBeenCalledWith('webhook'); + }); + }); +});