Compare commits

..

54 Commits

Author SHA1 Message Date
Jakub P.
d31309a92d test: add delete flow tests 2024-10-16 23:33:43 +02:00
Ali BARIN
0234b4ad81 Merge pull request #2125 from automatisch/aut-1322
test(connection): write model tests
2024-10-16 13:23:17 +02:00
Ömer Faruk Aydın
59ee9c21f3 Merge pull request #2111 from automatisch/aut-1301
test(app): write model tests
2024-10-16 13:17:35 +02:00
Faruk AYDIN
00317fed24 refactor: Use endsWith for app folder path test 2024-10-16 12:56:17 +02:00
Ali BARIN
42f6311ca8 test(app): write model tests 2024-10-16 12:56:17 +02:00
Ali BARIN
0f77a1ec03 test(connection): write model tests 2024-10-16 09:38:48 +00:00
Ali BARIN
8268cd4d09 Merge pull request #2123 from automatisch/aut-1319
test(config): write model tests
2024-10-14 13:28:09 +02:00
Ali BARIN
c868070337 test(config): write model tests 2024-10-14 11:19:29 +00:00
Ömer Faruk Aydın
623ec66a79 Merge pull request #2121 from automatisch/usage-data-tests
test: Implement UsageData model tests
2024-10-10 13:03:09 +02:00
Faruk AYDIN
b51bae14ec test: Implement UsageData model tests 2024-10-10 12:08:41 +02:00
Ömer Faruk Aydın
9250456e7b Merge pull request #2119 from automatisch/execution-step-test
tests: Implement tests for ExecutionStep model
2024-10-09 16:27:39 +02:00
Faruk AYDIN
163d6a7a28 refactor: Do not use persisted instance for execution step is failed 2024-10-09 15:43:33 +02:00
Faruk AYDIN
4ce9976dbc refactor: Use isSucceededNonTestRun naming for execution steps 2024-10-09 15:43:33 +02:00
Faruk AYDIN
f6490990de tests: Implement tests for ExecutionStep model 2024-10-09 15:43:33 +02:00
Ali BARIN
f24ff606ac Merge pull request #2113 from automatisch/AUT-1196
fix: use env var for authDocUrl
2024-10-09 15:15:09 +02:00
Jakub P.
3ff53744b5 fix: use env var for authDocUrl 2024-10-09 15:02:59 +02:00
Ali BARIN
0b33c10ed8 Merge pull request #2029 from automatisch/AUT-1190
fix: limit resetting form on defaultValues change
2024-10-09 14:54:05 +02:00
Ömer Faruk Aydın
3fa87701ed Merge pull request #2114 from automatisch/AUT-929
feat: fix primary app color value and how it is displayed in the app
2024-10-09 11:02:03 +02:00
Ali BARIN
e5e0c6fa2a Merge pull request #2117 from automatisch/aut-1191-2
fix: bring back missing custom logo
2024-10-08 21:54:12 +02:00
Jakub P.
0b38a0b6af fix: bring back missing custom logo 2024-10-08 17:56:13 +00:00
Ömer Faruk Aydın
44b228777a Merge pull request #2093 from automatisch/aut-1191
feat(config): make data structure horizontal
2024-10-08 18:15:33 +02:00
Ali BARIN
dd4f658d14 test: cover static config in config controller test 2024-10-08 14:41:02 +00:00
Ali BARIN
9c66f47bca test: write config serializer cases 2024-10-08 14:34:14 +00:00
Ali BARIN
138a34d6a4 feat(config): incorporate static config as virtual attributes 2024-10-08 14:19:17 +00:00
Ali BARIN
8f2af2e863 chore: rename horizontal config migration as make_config_single_record 2024-10-08 14:18:38 +00:00
Ali BARIN
c732fe16b3 refactor(update-config): introduce updateFirstOrInsert via query builder 2024-10-08 13:18:42 +00:00
Ali BARIN
9be75e56e7 refactor(migration): simplify make_config_horizontal_scale script 2024-10-08 13:18:42 +00:00
Ali BARIN
e7d1f26034 feat(serializers): add config serializer 2024-10-08 13:18:42 +00:00
Ali BARIN
c554fff048 feat(config): add missing schema properties 2024-10-08 13:18:42 +00:00
Ali BARIN
0268521aaa test(config): fix typo in palette primary light 2024-10-08 13:18:42 +00:00
Ali BARIN
89b1cb9353 feat(web): adapt to horizontal config structure 2024-10-08 13:18:42 +00:00
Ali BARIN
8a17c5eaab feat(config): make data structure horizontal 2024-10-08 13:18:42 +00:00
Ömer Faruk Aydın
f4a1ad6c8c Merge pull request #2109 from automatisch/aut-1292
test(app-auth-client): write model tests
2024-10-08 15:09:41 +02:00
Jakub P.
93d76d8d79 feat: add hash in color to the examples in docs 2024-10-07 16:39:51 +02:00
Ali BARIN
0b956a71b9 test(models/app-auth-client): improve decrypt data test cases 2024-10-07 13:25:04 +00:00
Faruk AYDIN
26be72f76d test: Improve encrypt data tests for AppAuthClient model 2024-10-07 14:21:14 +02:00
Ali BARIN
737391c721 test(app-auth-client): write model tests 2024-10-07 09:14:18 +00:00
kasia.oczkowska
9c1d21fd1b feat: fix primary app color value and how it is displayed in the app 2024-10-04 12:34:50 +01:00
kasia.oczkowska
ef4a4c8611 fix: limit resetting form on defaultValues change 2024-10-04 11:46:12 +01:00
Ömer Faruk Aydın
13c0a8ceaa Merge pull request #2112 from automatisch/test-subscription
test: Implement subscription model tests
2024-10-02 13:53:46 +03:00
Faruk AYDIN
c2976080f6 test: Implement subscription model tests 2024-10-02 13:44:21 +03:00
Faruk AYDIN
0a1b6931af test: Add tests for Identity model 2024-10-02 10:52:05 +03:00
Ömer Faruk Aydın
5009319f91 Merge pull request #2108 from automatisch/aut-1282
test(access-token): write model tests
2024-10-02 10:16:49 +03:00
Ali BARIN
4db8683bd6 test(access-token): write model tests 2024-10-01 10:37:43 +00:00
Ali BARIN
d35cf8d31e test: add access token factory 2024-10-01 09:55:36 +00:00
Ali BARIN
5fef16131a test: add identity factory 2024-10-01 09:55:26 +00:00
Ömer Faruk Aydın
8b2235ee26 Merge pull request #2107 from automatisch/execution-tests
test: Implement execution model tests
2024-09-30 17:33:28 +03:00
Faruk AYDIN
2d8faf849e test: Implement execution model tests 2024-09-30 17:25:15 +03:00
Ali BARIN
c9de9fa185 Merge pull request #2096 from automatisch/aut-1276
test(SamlAuthProvidersRoleMapping): assert model properties
2024-09-30 16:24:46 +02:00
Ali BARIN
0618877d58 test(models/saml-auth-providers-role-mapping): assert model properties 2024-09-30 14:14:02 +00:00
Ömer Faruk Aydın
f6b4e7eef8 Merge pull request #2095 from automatisch/datastore-tests
feat: Implement datastore model tests
2024-09-30 17:12:11 +03:00
Faruk AYDIN
7abe44da19 refactor: Use matchsnapshot for datastore tests 2024-09-30 16:43:06 +03:00
Faruk AYDIN
9006a0c25f refactor: Update required validations with custom assertion 2024-09-30 16:39:06 +03:00
Faruk AYDIN
f8389ff8ab feat: Implement datastore model tests 2024-09-30 16:39:06 +03:00
138 changed files with 2926 additions and 526 deletions

View File

@@ -12,6 +12,9 @@ on:
workflow_dispatch:
env:
BULLMQ_DASHBOARD_USERNAME: root
BULLMQ_DASHBOARD_PASSWORD: sample
ENABLE_BULLMQ_DASHBOARD: true
ENCRYPTION_KEY: sample_encryption_key
WEBHOOK_SECRET_KEY: sample_webhook_secret_key
APP_SECRET_KEY: sample_app_secret_key
@@ -22,6 +25,7 @@ env:
POSTGRES_PASSWORD: automatisch_password
REDIS_HOST: localhost
APP_ENV: production
PORT: 3000
LICENSE_KEY: dummy_license_key
jobs:

View File

@@ -36,7 +36,7 @@
"crypto-js": "^4.1.1",
"debug": "~2.6.9",
"dotenv": "^10.0.0",
"express": "~4.20.0",
"express": "~4.18.2",
"express-async-errors": "^3.1.1",
"express-basic-auth": "^1.2.1",
"fast-xml-parser": "^4.0.11",

View File

@@ -12,7 +12,7 @@ export default defineApp({
apiBaseUrl: 'https://api.airtable.com',
iconUrl: '{BASE_URL}/apps/airtable/assets/favicon.svg',
authDocUrl: '{DOCS_URL}/apps/airtable/connection',
primaryColor: 'FFBF00',
primaryColor: '#FFBF00',
supportsConnections: true,
beforeRequest: [addAuthHeader],
auth,

View File

@@ -12,7 +12,7 @@ export default defineApp({
apiBaseUrl: 'https://cloud.appwrite.io',
iconUrl: '{BASE_URL}/apps/appwrite/assets/favicon.svg',
authDocUrl: '{DOCS_URL}/apps/appwrite/connection',
primaryColor: 'FD366E',
primaryColor: '#FD366E',
supportsConnections: true,
beforeRequest: [setBaseUrl, addAuthHeader],
auth,

View File

@@ -12,7 +12,7 @@ export default defineApp({
apiBaseUrl: '',
iconUrl: '{BASE_URL}/apps/azure-openai/assets/favicon.svg',
authDocUrl: '{DOCS_URL}/apps/azure-openai/connection',
primaryColor: '000000',
primaryColor: '#000000',
supportsConnections: true,
beforeRequest: [setBaseUrl, addAuthHeader],
auth,

View File

@@ -11,7 +11,7 @@ export default defineApp({
supportsConnections: true,
baseUrl: 'https://carbone.io',
apiBaseUrl: 'https://api.carbone.io',
primaryColor: '6f42c1',
primaryColor: '#6f42c1',
beforeRequest: [addAuthHeader],
auth,
actions,

View File

@@ -12,8 +12,8 @@ export default defineApp({
baseUrl: 'https://clickup.com',
apiBaseUrl: 'https://api.clickup.com/api',
iconUrl: '{BASE_URL}/apps/clickup/assets/favicon.svg',
authDocUrl: 'https://automatisch.io/docs/apps/clickup/connection',
primaryColor: 'FD71AF',
authDocUrl: '{DOCS_URL}/apps/clickup/connection',
primaryColor: '#FD71AF',
supportsConnections: true,
beforeRequest: [addAuthHeader],
auth,

View File

@@ -8,7 +8,7 @@ export default defineApp({
apiBaseUrl: '',
iconUrl: '{BASE_URL}/apps/code/assets/favicon.svg',
authDocUrl: '{DOCS_URL}/apps/code/connection',
primaryColor: '000000',
primaryColor: '#000000',
supportsConnections: false,
actions,
});

View File

@@ -9,6 +9,6 @@ export default defineApp({
supportsConnections: false,
baseUrl: '',
apiBaseUrl: '',
primaryColor: '001F52',
primaryColor: '#001F52',
actions,
});

View File

@@ -9,6 +9,6 @@ export default defineApp({
supportsConnections: false,
baseUrl: '',
apiBaseUrl: '',
primaryColor: '001F52',
primaryColor: '#001F52',
actions,
});

View File

@@ -11,7 +11,7 @@ export default defineApp({
supportsConnections: true,
baseUrl: 'https://deepl.com',
apiBaseUrl: 'https://api.deepl.com',
primaryColor: '0d2d45',
primaryColor: '#0d2d45',
beforeRequest: [addAuthHeader],
auth,
actions,

View File

@@ -9,6 +9,6 @@ export default defineApp({
supportsConnections: false,
baseUrl: '',
apiBaseUrl: '',
primaryColor: '001F52',
primaryColor: '#001F52',
actions,
});

View File

@@ -14,7 +14,7 @@ export default defineApp({
supportsConnections: true,
baseUrl: 'https://discord.com',
apiBaseUrl: 'https://discord.com/api',
primaryColor: '5865f2',
primaryColor: '#5865f2',
beforeRequest: [addAuthHeader],
auth,
dynamicData,

View File

@@ -11,7 +11,7 @@ export default defineApp({
apiBaseUrl: 'https://disqus.com/api',
iconUrl: '{BASE_URL}/apps/disqus/assets/favicon.svg',
authDocUrl: '{DOCS_URL}/apps/disqus/connection',
primaryColor: '2E9FFF',
primaryColor: '#2E9FFF',
supportsConnections: true,
beforeRequest: [addAuthHeader],
auth,

View File

@@ -11,7 +11,7 @@ export default defineApp({
supportsConnections: true,
baseUrl: 'https://dropbox.com',
apiBaseUrl: 'https://api.dropboxapi.com',
primaryColor: '0061ff',
primaryColor: '#0061ff',
beforeRequest: [addAuthHeader],
auth,
actions,

View File

@@ -9,6 +9,6 @@ export default defineApp({
supportsConnections: false,
baseUrl: '',
apiBaseUrl: '',
primaryColor: '001F52',
primaryColor: '#001F52',
actions,
});

View File

@@ -10,7 +10,7 @@ export default defineApp({
iconUrl: '{BASE_URL}/apps/flickr/assets/favicon.svg',
authDocUrl: '{DOCS_URL}/apps/flickr/connection',
docUrl: 'https://automatisch.io/docs/flickr',
primaryColor: '000000',
primaryColor: '#000000',
supportsConnections: true,
baseUrl: 'https://www.flickr.com/',
apiBaseUrl: 'https://www.flickr.com/services',

View File

@@ -11,7 +11,7 @@ export default defineApp({
supportsConnections: true,
baseUrl: 'https://flowers-software.com',
apiBaseUrl: 'https://webapp.flowers-software.com/api',
primaryColor: '02AFC7',
primaryColor: '#02AFC7',
beforeRequest: [addAuthHeader],
auth,
triggers,

View File

@@ -10,7 +10,7 @@ export default defineApp({
supportsConnections: false,
baseUrl: '',
apiBaseUrl: '',
primaryColor: '001F52',
primaryColor: '#001F52',
actions,
dynamicFields,
});

View File

@@ -11,7 +11,7 @@ export default defineApp({
apiBaseUrl: '',
iconUrl: '{BASE_URL}/apps/ghost/assets/favicon.svg',
authDocUrl: '{DOCS_URL}/apps/ghost/connection',
primaryColor: '15171A',
primaryColor: '#15171A',
supportsConnections: true,
beforeRequest: [setBaseUrl, addAuthHeader],
auth,

View File

@@ -12,7 +12,7 @@ export default defineApp({
apiBaseUrl: 'https://api.github.com',
iconUrl: '{BASE_URL}/apps/github/assets/favicon.svg',
authDocUrl: '{DOCS_URL}/apps/github/connection',
primaryColor: '000000',
primaryColor: '#000000',
supportsConnections: true,
beforeRequest: [addAuthHeader],
auth,

View File

@@ -12,7 +12,7 @@ export default defineApp({
apiBaseUrl: 'https://gitlab.com',
iconUrl: '{BASE_URL}/apps/gitlab/assets/favicon.svg',
authDocUrl: '{DOCS_URL}/apps/gitlab/connection',
primaryColor: 'FC6D26',
primaryColor: '#FC6D26',
supportsConnections: true,
beforeRequest: [setBaseUrl, addAuthHeader],
auth,

View File

@@ -11,7 +11,7 @@ export default defineApp({
apiBaseUrl: 'https://www.googleapis.com/calendar',
iconUrl: '{BASE_URL}/apps/google-calendar/assets/favicon.svg',
authDocUrl: '{DOCS_URL}/apps/google-calendar/connection',
primaryColor: '448AFF',
primaryColor: '#448AFF',
supportsConnections: true,
beforeRequest: [addAuthHeader],
auth,

View File

@@ -11,7 +11,7 @@ export default defineApp({
apiBaseUrl: 'https://www.googleapis.com/drive',
iconUrl: '{BASE_URL}/apps/google-drive/assets/favicon.svg',
authDocUrl: '{DOCS_URL}/apps/google-drive/connection',
primaryColor: '1FA463',
primaryColor: '#1FA463',
supportsConnections: true,
beforeRequest: [addAuthHeader],
auth,

View File

@@ -11,7 +11,7 @@ export default defineApp({
apiBaseUrl: 'https://forms.googleapis.com',
iconUrl: '{BASE_URL}/apps/google-forms/assets/favicon.svg',
authDocUrl: '{DOCS_URL}/apps/google-forms/connection',
primaryColor: '673AB7',
primaryColor: '#673AB7',
supportsConnections: true,
beforeRequest: [addAuthHeader],
auth,

View File

@@ -13,7 +13,7 @@ export default defineApp({
apiBaseUrl: 'https://sheets.googleapis.com',
iconUrl: '{BASE_URL}/apps/google-sheets/assets/favicon.svg',
authDocUrl: '{DOCS_URL}/apps/google-sheets/connection',
primaryColor: '0F9D58',
primaryColor: '#0F9D58',
supportsConnections: true,
beforeRequest: [addAuthHeader],
auth,

View File

@@ -12,7 +12,7 @@ export default defineApp({
apiBaseUrl: 'https://tasks.googleapis.com',
iconUrl: '{BASE_URL}/apps/google-tasks/assets/favicon.svg',
authDocUrl: '{DOCS_URL}/apps/google-tasks/connection',
primaryColor: '0066DA',
primaryColor: '#0066DA',
supportsConnections: true,
beforeRequest: [addAuthHeader],
auth,

View File

@@ -11,7 +11,7 @@ export default defineApp({
apiBaseUrl: 'https://app.tryhelix.ai',
iconUrl: '{BASE_URL}/apps/helix/assets/favicon.svg',
authDocUrl: '{DOCS_URL}/apps/helix/connection',
primaryColor: '000000',
primaryColor: '#000000',
supportsConnections: true,
beforeRequest: [setBaseUrl, addAuthHeader],
auth,

View File

@@ -9,6 +9,6 @@ export default defineApp({
supportsConnections: false,
baseUrl: '',
apiBaseUrl: '',
primaryColor: '000000',
primaryColor: '#000000',
actions,
});

View File

@@ -11,7 +11,7 @@ export default defineApp({
supportsConnections: true,
baseUrl: 'https://www.hubspot.com',
apiBaseUrl: 'https://api.hubapi.com',
primaryColor: 'F95C35',
primaryColor: '#F95C35',
beforeRequest: [addAuthHeader],
auth,
actions,

View File

@@ -13,7 +13,7 @@ export default defineApp({
apiBaseUrl: 'https://invoicing.co/api',
iconUrl: '{BASE_URL}/apps/invoice-ninja/assets/favicon.svg',
authDocUrl: '{DOCS_URL}/apps/invoice-ninja/connection',
primaryColor: '000000',
primaryColor: '#000000',
supportsConnections: true,
beforeRequest: [setBaseUrl, addAuthHeader],
auth,

View File

@@ -9,11 +9,11 @@ export default defineApp({
name: 'Jotform',
key: 'jotform',
iconUrl: '{BASE_URL}/apps/jotform/assets/favicon.svg',
authDocUrl: 'https://automatisch.io/docs/apps/jotform/connection',
authDocUrl: '{DOCS_URL}/apps/jotform/connection',
supportsConnections: true,
baseUrl: 'https://www.jotform.com',
apiBaseUrl: 'https://api.jotform.com',
primaryColor: 'FF6100',
primaryColor: '#FF6100',
beforeRequest: [setBaseUrl, addAuthHeader],
auth,
triggers,

View File

@@ -12,8 +12,8 @@ export default defineApp({
baseUrl: 'https://mailchimp.com',
apiBaseUrl: '',
iconUrl: '{BASE_URL}/apps/mailchimp/assets/favicon.svg',
authDocUrl: 'https://automatisch.io/docs/apps/mailchimp/connection',
primaryColor: '000000',
authDocUrl: '{DOCS_URL}/apps/mailchimp/connection',
primaryColor: '#000000',
supportsConnections: true,
beforeRequest: [setBaseUrl, addAuthHeader],
auth,

View File

@@ -7,11 +7,11 @@ export default defineApp({
name: 'MailerLite',
key: 'mailerlite',
iconUrl: '{BASE_URL}/apps/mailerlite/assets/favicon.svg',
authDocUrl: 'https://automatisch.io/docs/apps/mailerlite/connection',
authDocUrl: '{DOCS_URL}/apps/mailerlite/connection',
supportsConnections: true,
baseUrl: 'https://www.mailerlite.com',
apiBaseUrl: 'https://connect.mailerlite.com/api',
primaryColor: '09C269',
primaryColor: '#09C269',
beforeRequest: [addAuthHeader],
auth,
triggers,

View File

@@ -13,7 +13,7 @@ export default defineApp({
authDocUrl: '{DOCS_URL}/apps/mattermost/connection',
baseUrl: 'https://mattermost.com',
apiBaseUrl: '', // there is no cloud version of this app, user always need to provide address of own instance when creating connection
primaryColor: '4a154b',
primaryColor: '#4a154b',
supportsConnections: true,
beforeRequest: [setBaseUrl, addXRequestedWithHeader, addAuthHeader],
auth,

View File

@@ -11,7 +11,7 @@ export default defineApp({
apiBaseUrl: 'https://api.miro.com',
iconUrl: '{BASE_URL}/apps/miro/assets/favicon.svg',
authDocUrl: '{DOCS_URL}/apps/miro/connection',
primaryColor: 'F2CA02',
primaryColor: '#F2CA02',
supportsConnections: true,
beforeRequest: [addAuthHeader],
auth,

View File

@@ -13,7 +13,7 @@ export default defineApp({
apiBaseUrl: 'https://api.notion.com',
iconUrl: '{BASE_URL}/apps/notion/assets/favicon.svg',
authDocUrl: '{DOCS_URL}/apps/notion/connection',
primaryColor: '000000',
primaryColor: '#000000',
supportsConnections: true,
beforeRequest: [addAuthHeader, addNotionVersionHeader],
auth,

View File

@@ -11,7 +11,7 @@ export default defineApp({
supportsConnections: true,
baseUrl: 'https://ntfy.sh',
apiBaseUrl: 'https://ntfy.sh',
primaryColor: '56bda8',
primaryColor: '#56bda8',
beforeRequest: [addAuthHeader],
auth,
actions,

View File

@@ -10,7 +10,7 @@ export default defineApp({
supportsConnections: true,
baseUrl: 'https://odoo.com',
apiBaseUrl: '',
primaryColor: '9c5789',
primaryColor: '#9c5789',
auth,
actions,
});

View File

@@ -11,7 +11,7 @@ export default defineApp({
apiBaseUrl: 'https://api.openai.com',
iconUrl: '{BASE_URL}/apps/openai/assets/favicon.svg',
authDocUrl: '{DOCS_URL}/apps/openai/connection',
primaryColor: '000000',
primaryColor: '#000000',
supportsConnections: true,
beforeRequest: [addAuthHeader],
auth,

View File

@@ -13,7 +13,7 @@ export default defineApp({
apiBaseUrl: '',
iconUrl: '{BASE_URL}/apps/pipedrive/assets/favicon.svg',
authDocUrl: '{DOCS_URL}/apps/pipedrive/connection',
primaryColor: 'FFFFFF',
primaryColor: '#FFFFFF',
supportsConnections: true,
beforeRequest: [setBaseUrl, addAuthHeader],
auth,

View File

@@ -12,7 +12,7 @@ export default defineApp({
supportsConnections: true,
baseUrl: 'https://placetel.de',
apiBaseUrl: 'https://api.placetel.de',
primaryColor: '069dd9',
primaryColor: '#069dd9',
beforeRequest: [addAuthHeader],
auth,
triggers,

View File

@@ -10,7 +10,7 @@ export default defineApp({
supportsConnections: true,
baseUrl: '',
apiBaseUrl: '',
primaryColor: '336791',
primaryColor: '#336791',
auth,
actions,
});

View File

@@ -10,7 +10,7 @@ export default defineApp({
apiBaseUrl: 'https://api.pushover.net',
iconUrl: '{BASE_URL}/apps/pushover/assets/favicon.svg',
authDocUrl: '{DOCS_URL}/apps/pushover/connection',
primaryColor: '249DF1',
primaryColor: '#249DF1',
supportsConnections: true,
auth,
actions,

View File

@@ -11,7 +11,7 @@ export default defineApp({
apiBaseUrl: 'https://oauth.reddit.com',
iconUrl: '{BASE_URL}/apps/reddit/assets/favicon.svg',
authDocUrl: '{DOCS_URL}/apps/reddit/connection',
primaryColor: 'FF4500',
primaryColor: '#FF4500',
supportsConnections: true,
beforeRequest: [addAuthHeader],
auth,

View File

@@ -11,7 +11,7 @@ export default defineApp({
supportsConnections: true,
baseUrl: 'https://www.remove.bg',
apiBaseUrl: 'https://api.remove.bg/v1.0',
primaryColor: '55636c',
primaryColor: '#55636c',
beforeRequest: [addAuthHeader],
auth,
actions,

View File

@@ -9,6 +9,6 @@ export default defineApp({
supportsConnections: false,
baseUrl: '',
apiBaseUrl: '',
primaryColor: 'ff8800',
primaryColor: '#ff8800',
triggers,
});

View File

@@ -13,7 +13,7 @@ export default defineApp({
supportsConnections: true,
baseUrl: 'https://salesforce.com',
apiBaseUrl: '',
primaryColor: '00A1E0',
primaryColor: '#00A1E0',
beforeRequest: [addAuthHeader],
auth,
triggers,

View File

@@ -9,7 +9,7 @@ export default defineApp({
authDocUrl: '{DOCS_URL}/apps/scheduler/connection',
baseUrl: '',
apiBaseUrl: '',
primaryColor: '0059F7',
primaryColor: '#0059F7',
supportsConnections: false,
triggers,
});

View File

@@ -12,7 +12,7 @@ export default defineApp({
apiBaseUrl: '',
iconUrl: '{BASE_URL}/apps/self-hosted-llm/assets/favicon.svg',
authDocUrl: '{DOCS_URL}/apps/self-hosted-llm/connection',
primaryColor: '000000',
primaryColor: '#000000',
supportsConnections: true,
beforeRequest: [setBaseUrl, addAuthHeader],
auth,

View File

@@ -13,7 +13,7 @@ export default defineApp({
supportsConnections: true,
baseUrl: 'https://signalwire.com',
apiBaseUrl: '',
primaryColor: '044cf6',
primaryColor: '#044cf6',
beforeRequest: [addAuthHeader],
auth,
triggers,

View File

@@ -13,7 +13,7 @@ export default defineApp({
supportsConnections: true,
baseUrl: 'https://slack.com',
apiBaseUrl: 'https://slack.com/api',
primaryColor: '4a154b',
primaryColor: '#4a154b',
beforeRequest: [addAuthHeader],
auth,
actions,

View File

@@ -10,7 +10,7 @@ export default defineApp({
supportsConnections: true,
baseUrl: '',
apiBaseUrl: '',
primaryColor: '2DAAE1',
primaryColor: '#2DAAE1',
auth,
actions,
});

View File

@@ -11,7 +11,7 @@ export default defineApp({
supportsConnections: true,
baseUrl: 'https://spotify.com',
apiBaseUrl: 'https://api.spotify.com',
primaryColor: '000000',
primaryColor: '#000000',
beforeRequest: [addAuthHeader],
auth,
actions,

View File

@@ -11,7 +11,7 @@ export default defineApp({
supportsConnections: true,
baseUrl: 'https://www.strava.com',
apiBaseUrl: 'https://www.strava.com/api',
primaryColor: 'fc4c01',
primaryColor: '#fc4c01',
beforeRequest: [addAuthHeader],
auth,
actions,

View File

@@ -11,7 +11,7 @@ export default defineApp({
supportsConnections: true,
baseUrl: 'https://stripe.com',
apiBaseUrl: 'https://api.stripe.com',
primaryColor: '635bff',
primaryColor: '#635bff',
beforeRequest: [addAuthHeader],
auth,
triggers,

View File

@@ -11,7 +11,7 @@ export default defineApp({
supportsConnections: true,
baseUrl: 'https://telegram.org',
apiBaseUrl: 'https://api.telegram.org',
primaryColor: '2AABEE',
primaryColor: '#2AABEE',
beforeRequest: [addAuthHeader],
auth,
actions,

View File

@@ -13,7 +13,7 @@ export default defineApp({
supportsConnections: true,
baseUrl: 'https://todoist.com',
apiBaseUrl: 'https://api.todoist.com/rest/v2',
primaryColor: 'e44332',
primaryColor: '#e44332',
beforeRequest: [addAuthHeader],
auth,
triggers,

View File

@@ -12,7 +12,7 @@ export default defineApp({
iconUrl: '{BASE_URL}/apps/trello/assets/favicon.svg',
authDocUrl: '{DOCS_URL}/apps/trello/connection',
supportsConnections: true,
primaryColor: '0079bf',
primaryColor: '#0079bf',
beforeRequest: [addAuthHeader],
auth,
actions,

View File

@@ -13,7 +13,7 @@ export default defineApp({
supportsConnections: true,
baseUrl: 'https://twilio.com',
apiBaseUrl: 'https://api.twilio.com',
primaryColor: 'e1000f',
primaryColor: '#e1000f',
beforeRequest: [addAuthHeader],
auth,
triggers,

View File

@@ -12,7 +12,7 @@ export default defineApp({
supportsConnections: true,
baseUrl: 'https://twitter.com',
apiBaseUrl: 'https://api.twitter.com',
primaryColor: '1da1f2',
primaryColor: '#1da1f2',
beforeRequest: [addAuthHeader],
auth,
triggers,

View File

@@ -12,7 +12,7 @@ export default defineApp({
supportsConnections: true,
baseUrl: 'https://typeform.com',
apiBaseUrl: 'https://api.typeform.com',
primaryColor: '262627',
primaryColor: '#262627',
beforeRequest: [addAuthHeader],
auth,
triggers,

View File

@@ -14,7 +14,7 @@ export default defineApp({
supportsConnections: true,
baseUrl: '',
apiBaseUrl: '',
primaryColor: '39a86d',
primaryColor: '#39a86d',
beforeRequest: [setBaseUrl, addAuthHeader],
auth,
triggers,

View File

@@ -10,7 +10,7 @@ export default defineApp({
supportsConnections: false,
baseUrl: '',
apiBaseUrl: '',
primaryColor: '0059F7',
primaryColor: '#0059F7',
actions,
triggers,
});

View File

@@ -13,7 +13,7 @@ export default defineApp({
supportsConnections: true,
baseUrl: 'https://wordpress.com',
apiBaseUrl: '',
primaryColor: '464342',
primaryColor: '#464342',
beforeRequest: [setBaseUrl, addAuthHeader],
auth,
triggers,

View File

@@ -11,7 +11,7 @@ export default defineApp({
apiBaseUrl: 'https://api.xero.com',
iconUrl: '{BASE_URL}/apps/xero/assets/favicon.svg',
authDocUrl: '{DOCS_URL}/apps/xero/connection',
primaryColor: '13B5EA',
primaryColor: '#13B5EA',
supportsConnections: true,
beforeRequest: [addAuthHeader],
auth,

View File

@@ -10,7 +10,7 @@ export default defineApp({
apiBaseUrl: 'https://api.ynab.com/v1',
iconUrl: '{BASE_URL}/apps/you-need-a-budget/assets/favicon.svg',
authDocUrl: '{DOCS_URL}/apps/you-need-a-budget/connection',
primaryColor: '19223C',
primaryColor: '#19223C',
supportsConnections: true,
beforeRequest: [addAuthHeader],
auth,

View File

@@ -10,7 +10,7 @@ export default defineApp({
apiBaseUrl: 'https://www.googleapis.com/youtube',
iconUrl: '{BASE_URL}/apps/youtube/assets/favicon.svg',
authDocUrl: '{DOCS_URL}/apps/youtube/connection',
primaryColor: 'FF0000',
primaryColor: '#FF0000',
supportsConnections: true,
beforeRequest: [addAuthHeader],
auth,

View File

@@ -12,7 +12,7 @@ export default defineApp({
apiBaseUrl: '',
iconUrl: '{BASE_URL}/apps/zendesk/assets/favicon.svg',
authDocUrl: '{DOCS_URL}/apps/zendesk/connection',
primaryColor: '17494d',
primaryColor: '#17494d',
supportsConnections: true,
beforeRequest: [addAuthHeader],
auth,

View File

@@ -1,23 +1,28 @@
import pick from 'lodash/pick.js';
import { renderObject } from '../../../../../helpers/renderer.js';
import Config from '../../../../../models/config.js';
export default async (request, response) => {
const config = configParams(request);
await Config.batchUpdate(config);
const config = await Config.query().updateFirstOrInsert(
configParams(request)
);
renderObject(response, config);
};
const configParams = (request) => {
const updatableConfigurationKeys = [
'logo.svgData',
'palette.primary.dark',
'palette.primary.light',
'palette.primary.main',
'title',
];
const {
logoSvgData,
palettePrimaryDark,
palettePrimaryLight,
palettePrimaryMain,
title,
} = request.body;
return pick(request.body, updatableConfigurationKeys);
return {
logoSvgData,
palettePrimaryDark,
palettePrimaryLight,
palettePrimaryMain,
title,
};
};

View File

@@ -5,7 +5,7 @@ import app from '../../../../../app.js';
import createAuthTokenByUserId from '../../../../../helpers/create-auth-token-by-user-id.js';
import { createUser } from '../../../../../../test/factories/user.js';
import { createRole } from '../../../../../../test/factories/role.js';
import { createBulkConfig } from '../../../../../../test/factories/config.js';
import { updateConfig } from '../../../../../../test/factories/config.js';
import * as license from '../../../../../helpers/license.ee.js';
describe('PATCH /api/v1/admin/config', () => {
@@ -30,13 +30,13 @@ describe('PATCH /api/v1/admin/config', () => {
const appConfig = {
title,
'palette.primary.main': palettePrimaryMain,
'palette.primary.dark': palettePrimaryDark,
'palette.primary.light': palettePrimaryLight,
'logo.svgData': logoSvgData,
palettePrimaryMain: palettePrimaryMain,
palettePrimaryDark: palettePrimaryDark,
palettePrimaryLight: palettePrimaryLight,
logoSvgData: logoSvgData,
};
await createBulkConfig(appConfig);
await updateConfig(appConfig);
const newTitle = 'Updated title';
@@ -51,7 +51,7 @@ describe('PATCH /api/v1/admin/config', () => {
.expect(200);
expect(response.body.data.title).toEqual(newTitle);
expect(response.body.meta.type).toEqual('Object');
expect(response.body.meta.type).toEqual('Config');
});
it('should return created config for unexisting config', async () => {
@@ -68,7 +68,7 @@ describe('PATCH /api/v1/admin/config', () => {
.expect(200);
expect(response.body.data.title).toEqual(newTitle);
expect(response.body.meta.type).toEqual('Object');
expect(response.body.meta.type).toEqual('Config');
});
it('should return null for deleted config entry', async () => {
@@ -83,6 +83,6 @@ describe('PATCH /api/v1/admin/config', () => {
.expect(200);
expect(response.body.data.title).toBeNull();
expect(response.body.meta.type).toEqual('Object');
expect(response.body.meta.type).toEqual('Config');
});
});

View File

@@ -1,25 +1,8 @@
import appConfig from '../../../../config/app.js';
import Config from '../../../../models/config.js';
import { renderObject } from '../../../../helpers/renderer.js';
export default async (request, response) => {
const defaultConfig = {
disableNotificationsPage: appConfig.disableNotificationsPage,
disableFavicon: appConfig.disableFavicon,
additionalDrawerLink: appConfig.additionalDrawerLink,
additionalDrawerLinkIcon: appConfig.additionalDrawerLinkIcon,
additionalDrawerLinkText: appConfig.additionalDrawerLinkText,
};
let config = await Config.query().orderBy('key', 'asc');
config = config.reduce((computedConfig, configEntry) => {
const { key, value } = configEntry;
computedConfig[key] = value?.data;
return computedConfig;
}, defaultConfig);
const config = await Config.get();
renderObject(response, config);
};

View File

@@ -1,66 +1,47 @@
import { vi, expect, describe, it } from 'vitest';
import request from 'supertest';
import { createConfig } from '../../../../../test/factories/config.js';
import { updateConfig } from '../../../../../test/factories/config.js';
import app from '../../../../app.js';
import configMock from '../../../../../test/mocks/rest/api/v1/automatisch/config.js';
import * as license from '../../../../helpers/license.ee.js';
import appConfig from '../../../../config/app.js';
describe('GET /api/v1/automatisch/config', () => {
it('should return Automatisch config', async () => {
it('should return Automatisch config along with static config', async () => {
vi.spyOn(license, 'hasValidLicense').mockResolvedValue(true);
vi.spyOn(appConfig, 'disableNotificationsPage', 'get').mockReturnValue(
true
);
vi.spyOn(appConfig, 'disableFavicon', 'get').mockReturnValue(true);
vi.spyOn(appConfig, 'additionalDrawerLink', 'get').mockReturnValue('link');
vi.spyOn(appConfig, 'additionalDrawerLinkIcon', 'get').mockReturnValue(
'icon'
);
vi.spyOn(appConfig, 'additionalDrawerLinkText', 'get').mockReturnValue(
'text'
);
const logoConfig = await createConfig({
key: 'logo.svgData',
value: { data: '<svg>Sample</svg>' },
});
const primaryDarkConfig = await createConfig({
key: 'palette.primary.dark',
value: { data: '#001F52' },
});
const primaryLightConfig = await createConfig({
key: 'palette.primary.light',
value: { data: '#4286FF' },
});
const primaryMainConfig = await createConfig({
key: 'palette.primary.main',
value: { data: '#0059F7' },
});
const titleConfig = await createConfig({
key: 'title',
value: { data: 'Sample Title' },
const config = await updateConfig({
logoSvgData: '<svg>Sample</svg>',
palettePrimaryDark: '#001f52',
palettePrimaryLight: '#4286FF',
palettePrimaryMain: '#0059F7',
title: 'Sample Title',
});
const response = await request(app)
.get('/api/v1/automatisch/config')
.expect(200);
const expectedPayload = configMock(
logoConfig,
primaryDarkConfig,
primaryLightConfig,
primaryMainConfig,
titleConfig
);
const expectedPayload = configMock({
...config,
disableNotificationsPage: true,
disableFavicon: true,
additionalDrawerLink: 'link',
additionalDrawerLinkIcon: 'icon',
additionalDrawerLinkText: 'text',
});
expect(response.body).toEqual(expectedPayload);
});
it('should return additional environment variables', async () => {
vi.spyOn(appConfig, 'disableNotificationsPage', 'get').mockReturnValue(true);
vi.spyOn(appConfig, 'disableFavicon', 'get').mockReturnValue(true);
vi.spyOn(appConfig, 'additionalDrawerLink', 'get').mockReturnValue('link');
vi.spyOn(appConfig, 'additionalDrawerLinkIcon', 'get').mockReturnValue('icon');
vi.spyOn(appConfig, 'additionalDrawerLinkText', 'get').mockReturnValue('text');
expect(appConfig.disableNotificationsPage).toEqual(true);
expect(appConfig.disableFavicon).toEqual(true);
expect(appConfig.additionalDrawerLink).toEqual('link');
expect(appConfig.additionalDrawerLinkIcon).toEqual('icon');
expect(appConfig.additionalDrawerLinkText).toEqual('text');
expect(response.body).toStrictEqual(expectedPayload);
});
});

View File

@@ -5,7 +5,7 @@ import Config from '../../../../../models/config.js';
import User from '../../../../../models/user.js';
import { createRole } from '../../../../../../test/factories/role';
import { createUser } from '../../../../../../test/factories/user';
import { createInstallationCompletedConfig } from '../../../../../../test/factories/config';
import { markInstallationCompleted } from '../../../../../../test/factories/config';
describe('POST /api/v1/installation/users', () => {
let adminRole;
@@ -59,7 +59,7 @@ describe('POST /api/v1/installation/users', () => {
describe('for completed installations', () => {
beforeEach(async () => {
await createInstallationCompletedConfig();
await markInstallationCompleted();
});
it('should respond with HTTP 403 when installation completed', async () => {

View File

@@ -0,0 +1,105 @@
export async function up(knex) {
await knex.schema.alterTable('config', (table) => {
table.dropUnique('key');
table.string('key').nullable().alter();
table.boolean('installation_completed').defaultTo(false);
table.text('logo_svg_data');
table.text('palette_primary_dark');
table.text('palette_primary_light');
table.text('palette_primary_main');
table.string('title');
});
const config = await knex('config').select('key', 'value');
const newConfigData = {
logo_svg_data: getValueForKey(config, 'logo.svgData'),
palette_primary_dark: getValueForKey(config, 'palette.primary.dark'),
palette_primary_light: getValueForKey(config, 'palette.primary.light'),
palette_primary_main: getValueForKey(config, 'palette.primary.main'),
title: getValueForKey(config, 'title'),
installation_completed: getValueForKey(config, 'installation.completed'),
};
const [configEntry] = await knex('config')
.insert(newConfigData)
.select('id')
.returning('id');
await knex('config').where('id', '!=', configEntry.id).delete();
await knex.schema.alterTable('config', (table) => {
table.dropColumn('key');
table.dropColumn('value');
});
}
export async function down(knex) {
await knex.schema.alterTable('config', (table) => {
table.string('key');
table.jsonb('value').notNullable().defaultTo({});
});
const configRow = await knex('config').first();
const config = [
{
key: 'logo.svgData',
value: {
data: configRow.logo_svg_data,
},
},
{
key: 'palette.primary.dark',
value: {
data: configRow.palette_primary_dark,
},
},
{
key: 'palette.primary.light',
value: {
data: configRow.palette_primary_light,
},
},
{
key: 'palette.primary.main',
value: {
data: configRow.palette_primary_main,
},
},
{
key: 'title',
value: {
data: configRow.title,
},
},
{
key: 'installation.completed',
value: {
data: configRow.installation_completed,
},
},
];
await knex('config').insert(config).returning('id');
await knex('config').where('id', '=', configRow.id).delete();
await knex.schema.alterTable('config', (table) => {
table.dropColumn('installation_completed');
table.dropColumn('logo_svg_data');
table.dropColumn('palette_primary_dark');
table.dropColumn('palette_primary_light');
table.dropColumn('palette_primary_main');
table.dropColumn('title');
table.string('key').unique().notNullable().alter();
});
}
function getValueForKey(rows, key) {
const row = rows.find((row) => row.key === key);
return row?.value?.data || null;
}

View File

@@ -0,0 +1,41 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`AccessToken model > jsonSchema should have correct validations 1`] = `
{
"properties": {
"expiresIn": {
"type": "integer",
},
"id": {
"format": "uuid",
"type": "string",
},
"revokedAt": {
"format": "date-time",
"type": [
"string",
"null",
],
},
"samlSessionId": {
"type": [
"string",
"null",
],
},
"token": {
"minLength": 32,
"type": "string",
},
"userId": {
"format": "uuid",
"type": "string",
},
},
"required": [
"token",
"expiresIn",
],
"type": "object",
}
`;

View File

@@ -0,0 +1,39 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`AppAuthClient model > jsonSchema should have correct validations 1`] = `
{
"properties": {
"active": {
"type": "boolean",
},
"appKey": {
"type": "string",
},
"authDefaults": {
"type": [
"string",
"null",
],
},
"createdAt": {
"type": "string",
},
"formattedAuthDefaults": {
"type": "object",
},
"id": {
"format": "uuid",
"type": "string",
},
"updatedAt": {
"type": "string",
},
},
"required": [
"name",
"appKey",
"formattedAuthDefaults",
],
"type": "object",
}
`;

View File

@@ -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",
]
`;

View File

@@ -0,0 +1,52 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`Config model > jsonSchema should have correct validations 1`] = `
{
"properties": {
"createdAt": {
"type": "string",
},
"id": {
"format": "uuid",
"type": "string",
},
"installationCompleted": {
"type": "boolean",
},
"logoSvgData": {
"type": [
"string",
"null",
],
},
"palettePrimaryDark": {
"type": [
"string",
"null",
],
},
"palettePrimaryLight": {
"type": [
"string",
"null",
],
},
"palettePrimaryMain": {
"type": [
"string",
"null",
],
},
"title": {
"type": [
"string",
"null",
],
},
"updatedAt": {
"type": "string",
},
},
"type": "object",
}
`;

View File

@@ -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",
}
`;

View File

@@ -0,0 +1,36 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`Datastore model > jsonSchema should have correct validations 1`] = `
{
"properties": {
"id": {
"format": "uuid",
"type": "string",
},
"key": {
"minLength": 1,
"type": "string",
},
"scope": {
"default": "flow",
"enum": [
"flow",
],
"type": "string",
},
"scopeId": {
"format": "uuid",
"type": "string",
},
"value": {
"type": "string",
},
},
"required": [
"key",
"value",
"scopeId",
],
"type": "object",
}
`;

View File

@@ -0,0 +1,54 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`ExecutionStep model > jsonSchema should have correct validations 1`] = `
{
"properties": {
"createdAt": {
"type": "string",
},
"dataIn": {
"type": [
"object",
"null",
],
},
"dataOut": {
"type": [
"object",
"null",
],
},
"deletedAt": {
"type": "string",
},
"errorDetails": {
"type": [
"object",
"null",
],
},
"executionId": {
"format": "uuid",
"type": "string",
},
"id": {
"format": "uuid",
"type": "string",
},
"status": {
"enum": [
"success",
"failure",
],
"type": "string",
},
"stepId": {
"type": "string",
},
"updatedAt": {
"type": "string",
},
},
"type": "object",
}
`;

View File

@@ -0,0 +1,33 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`Execution model > jsonSchema should have correct validations 1`] = `
{
"properties": {
"createdAt": {
"type": "string",
},
"deletedAt": {
"type": "string",
},
"flowId": {
"format": "uuid",
"type": "string",
},
"id": {
"format": "uuid",
"type": "string",
},
"internalId": {
"type": "string",
},
"testRun": {
"default": false,
"type": "boolean",
},
"updatedAt": {
"type": "string",
},
},
"type": "object",
}
`;

View File

@@ -0,0 +1,37 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`Identity model > jsonSchema should have correct validations 1`] = `
{
"properties": {
"id": {
"format": "uuid",
"type": "string",
},
"providerId": {
"format": "uuid",
"type": "string",
},
"providerType": {
"enum": [
"saml",
],
"type": "string",
},
"remoteId": {
"minLength": 1,
"type": "string",
},
"userId": {
"format": "uuid",
"type": "string",
},
},
"required": [
"providerId",
"remoteId",
"userId",
"providerType",
],
"type": "object",
}
`;

View File

@@ -0,0 +1,41 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`SamlAuthProvidersRoleMapping model > jsonSchema should have the correct schema 1`] = `
{
"properties": {
"id": {
"format": "uuid",
"type": "string",
},
"remoteRoleName": {
"minLength": 1,
"type": "string",
},
"roleId": {
"format": "uuid",
"type": "string",
},
"samlAuthProviderId": {
"format": "uuid",
"type": "string",
},
},
"required": [
"samlAuthProviderId",
"roleId",
"remoteRoleName",
],
"type": "object",
}
`;
exports[`SamlAuthProvidersRoleMapping model > relationMappings should have samlAuthProvider relation 1`] = `
{
"join": {
"from": "saml_auth_providers_role_mappings.saml_auth_provider_id",
"to": "saml_auth_providers.id",
},
"modelClass": [Function],
"relation": [Function],
}
`;

View File

@@ -0,0 +1,63 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`Subscription model > jsonSchema should have correct validations 1`] = `
{
"properties": {
"cancelUrl": {
"type": "string",
},
"cancellationEffectiveDate": {
"type": "string",
},
"createdAt": {
"type": "string",
},
"deletedAt": {
"type": "string",
},
"id": {
"format": "uuid",
"type": "string",
},
"lastBillDate": {
"type": "string",
},
"nextBillAmount": {
"type": "string",
},
"nextBillDate": {
"type": "string",
},
"paddlePlanId": {
"type": "string",
},
"paddleSubscriptionId": {
"type": "string",
},
"status": {
"type": "string",
},
"updateUrl": {
"type": "string",
},
"updatedAt": {
"type": "string",
},
"userId": {
"format": "uuid",
"type": "string",
},
},
"required": [
"userId",
"paddleSubscriptionId",
"paddlePlanId",
"updateUrl",
"cancelUrl",
"status",
"nextBillAmount",
"nextBillDate",
],
"type": "object",
}
`;

View File

@@ -0,0 +1,41 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`UsageData model > jsonSchema should have correct validations 1`] = `
{
"properties": {
"consumedTaskCount": {
"type": "integer",
},
"createdAt": {
"type": "string",
},
"deletedAt": {
"type": "string",
},
"id": {
"format": "uuid",
"type": "string",
},
"nextResetAt": {
"type": "string",
},
"subscriptionId": {
"format": "uuid",
"type": "string",
},
"updatedAt": {
"type": "string",
},
"userId": {
"format": "uuid",
"type": "string",
},
},
"required": [
"userId",
"consumedTaskCount",
"nextResetAt",
],
"type": "object",
}
`;

View File

@@ -34,24 +34,25 @@ class AccessToken extends Base {
return;
}
const user = await this
.$relatedQuery('user');
const user = await this.$relatedQuery('user');
const firstIdentity = await user
.$relatedQuery('identities')
.first();
const firstIdentity = await user.$relatedQuery('identities').first();
const samlAuthProvider = await firstIdentity
.$relatedQuery('samlAuthProvider')
.throwIfNotFound();
const response = await samlAuthProvider.terminateRemoteSession(this.samlSessionId);
const response = await samlAuthProvider.terminateRemoteSession(
this.samlSessionId
);
return response;
}
async revoke() {
const response = await this.$query().patch({ revokedAt: new Date().toISOString() });
const response = await this.$query().patch({
revokedAt: new Date().toISOString(),
});
try {
await this.terminateRemoteSamlSession();

View File

@@ -0,0 +1,84 @@
import { describe, it, expect, vi } from 'vitest';
import AccessToken from './access-token.js';
import User from './user.js';
import Base from './base.js';
import SamlAuthProvider from './saml-auth-provider.ee.js';
import { createAccessToken } from '../../test/factories/access-token.js';
import { createUser } from '../../test/factories/user.js';
import { createIdentity } from '../../test/factories/identity.js';
describe('AccessToken model', () => {
it('tableName should return correct name', () => {
expect(AccessToken.tableName).toBe('access_tokens');
});
it('jsonSchema should have correct validations', () => {
expect(AccessToken.jsonSchema).toMatchSnapshot();
});
it('relationMappings should return correct associations', () => {
const relationMappings = AccessToken.relationMappings();
const expectedRelations = {
user: {
relation: Base.BelongsToOneRelation,
modelClass: User,
join: {
from: 'access_tokens.user_id',
to: 'users.id',
},
},
};
expect(relationMappings).toStrictEqual(expectedRelations);
});
it('revoke should set revokedAt and terminate remote SAML session', async () => {
const accessToken = await createAccessToken();
const terminateRemoteSamlSessionSpy = vi
.spyOn(accessToken, 'terminateRemoteSamlSession')
.mockImplementation(() => {});
await accessToken.revoke();
expect(terminateRemoteSamlSessionSpy).toHaveBeenCalledOnce();
expect(accessToken.revokedAt).not.toBeUndefined();
});
describe('terminateRemoteSamlSession', () => {
it('should terminate remote SAML session when exists', async () => {
const user = await createUser();
const accessToken = await createAccessToken({
userId: user.id,
samlSessionId: 'random-remote-session-id',
});
await createIdentity({ userId: user.id });
const terminateRemoteSamlSessionSpy = vi
.spyOn(SamlAuthProvider.prototype, 'terminateRemoteSession')
.mockImplementation(() => {});
await accessToken.terminateRemoteSamlSession();
expect(terminateRemoteSamlSessionSpy).toHaveBeenCalledWith(
accessToken.samlSessionId
);
});
it(`should return undefined when remote SALM session doesn't exist`, async () => {
const user = await createUser();
const accessToken = await createAccessToken({ userId: user.id });
await createIdentity({ userId: user.id });
const terminateRemoteSamlSessionSpy = vi
.spyOn(SamlAuthProvider.prototype, 'terminateRemoteSession')
.mockImplementation(() => {});
const expected = await accessToken.terminateRemoteSamlSession();
expect(terminateRemoteSamlSessionSpy).not.toHaveBeenCalledOnce();
expect(expected).toBeUndefined();
});
});
});

View File

@@ -31,6 +31,7 @@ class AppAuthClient extends Base {
delete this.formattedAuthDefaults;
}
decryptData() {
if (!this.eligibleForDecryption()) return;

View File

@@ -0,0 +1,179 @@
import { describe, it, expect, vi } from 'vitest';
import AES from 'crypto-js/aes.js';
import enc from 'crypto-js/enc-utf8.js';
import AppAuthClient from './app-auth-client.js';
import appConfig from '../config/app.js';
import { createAppAuthClient } from '../../test/factories/app-auth-client.js';
describe('AppAuthClient model', () => {
it('tableName should return correct name', () => {
expect(AppAuthClient.tableName).toBe('app_auth_clients');
});
it('jsonSchema should have correct validations', () => {
expect(AppAuthClient.jsonSchema).toMatchSnapshot();
});
describe('encryptData', () => {
it('should return undefined if eligibleForEncryption is not true', async () => {
vi.spyOn(
AppAuthClient.prototype,
'eligibleForEncryption'
).mockReturnValue(false);
const appAuthClient = new AppAuthClient();
expect(appAuthClient.encryptData()).toBeUndefined();
});
it('should encrypt formattedAuthDefaults and set it to authDefaults', async () => {
vi.spyOn(
AppAuthClient.prototype,
'eligibleForEncryption'
).mockReturnValue(true);
const formattedAuthDefaults = {
key: 'value',
};
const appAuthClient = new AppAuthClient();
appAuthClient.formattedAuthDefaults = formattedAuthDefaults;
appAuthClient.encryptData();
const expectedDecryptedValue = JSON.parse(
AES.decrypt(
appAuthClient.authDefaults,
appConfig.encryptionKey
).toString(enc)
);
expect(formattedAuthDefaults).toStrictEqual(expectedDecryptedValue);
expect(appAuthClient.authDefaults).not.toEqual(formattedAuthDefaults);
});
it('should encrypt formattedAuthDefaults and remove formattedAuthDefaults', async () => {
vi.spyOn(
AppAuthClient.prototype,
'eligibleForEncryption'
).mockReturnValue(true);
const formattedAuthDefaults = {
key: 'value',
};
const appAuthClient = new AppAuthClient();
appAuthClient.formattedAuthDefaults = formattedAuthDefaults;
appAuthClient.encryptData();
expect(appAuthClient.formattedAuthDefaults).not.toBeDefined();
});
});
describe('decryptData', () => {
it('should return undefined if eligibleForDecryption is not true', () => {
vi.spyOn(
AppAuthClient.prototype,
'eligibleForDecryption'
).mockReturnValue(false);
const appAuthClient = new AppAuthClient();
expect(appAuthClient.decryptData()).toBeUndefined();
});
it('should decrypt authDefaults and set it to formattedAuthDefaults', async () => {
vi.spyOn(
AppAuthClient.prototype,
'eligibleForDecryption'
).mockReturnValue(true);
const formattedAuthDefaults = {
key: 'value',
};
const authDefaults = AES.encrypt(
JSON.stringify(formattedAuthDefaults),
appConfig.encryptionKey
).toString();
const appAuthClient = new AppAuthClient();
appAuthClient.authDefaults = authDefaults;
appAuthClient.decryptData();
expect(appAuthClient.formattedAuthDefaults).toStrictEqual(
formattedAuthDefaults
);
expect(appAuthClient.authDefaults).not.toEqual(formattedAuthDefaults);
});
});
describe('eligibleForEncryption', () => {
it('should return true when formattedAuthDefaults property exists', async () => {
const appAuthClient = await createAppAuthClient();
expect(appAuthClient.eligibleForEncryption()).toBe(true);
});
it("should return false when formattedAuthDefaults property doesn't exist", async () => {
const appAuthClient = await createAppAuthClient();
delete appAuthClient.formattedAuthDefaults;
expect(appAuthClient.eligibleForEncryption()).toBe(false);
});
});
describe('eligibleForDecryption', () => {
it('should return true when authDefaults property exists', async () => {
const appAuthClient = await createAppAuthClient();
expect(appAuthClient.eligibleForDecryption()).toBe(true);
});
it("should return false when authDefaults property doesn't exist", async () => {
const appAuthClient = await createAppAuthClient();
delete appAuthClient.authDefaults;
expect(appAuthClient.eligibleForDecryption()).toBe(false);
});
});
it('$beforeInsert should call AppAuthClient.encryptData', async () => {
const appAuthClientBeforeInsertSpy = vi.spyOn(
AppAuthClient.prototype,
'encryptData'
);
await createAppAuthClient();
expect(appAuthClientBeforeInsertSpy).toHaveBeenCalledOnce();
});
it('$beforeUpdate should call AppAuthClient.encryptData', async () => {
const appAuthClient = await createAppAuthClient();
const appAuthClientBeforeUpdateSpy = vi.spyOn(
AppAuthClient.prototype,
'encryptData'
);
await appAuthClient.$query().patchAndFetch({ name: 'sample' });
expect(appAuthClientBeforeUpdateSpy).toHaveBeenCalledOnce();
});
it('$afterFind should call AppAuthClient.decryptData', async () => {
const appAuthClient = await createAppAuthClient();
const appAuthClientAfterFindSpy = vi.spyOn(
AppAuthClient.prototype,
'decryptData'
);
await appAuthClient.$query();
expect(appAuthClientAfterFindSpy).toHaveBeenCalledOnce();
});
});

View File

@@ -8,6 +8,7 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
class App {
static folderPath = join(__dirname, '../apps');
static list = fs
.readdirSync(this.folderPath)
.filter((file) => fs.statSync(join(this.folderPath, file)).isDirectory());

View File

@@ -0,0 +1,418 @@
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.endsWith('/packages/backend/src/apps')).toBe(true);
});
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');
});
});
});

View File

@@ -1,3 +1,4 @@
import appConfig from '../config/app.js';
import Base from './base.js';
class Config extends Base {
@@ -5,68 +6,79 @@ class Config extends Base {
static jsonSchema = {
type: 'object',
required: ['key', 'value'],
properties: {
id: { type: 'string', format: 'uuid' },
key: { type: 'string', minLength: 1 },
value: { type: 'object' },
installationCompleted: { type: 'boolean' },
logoSvgData: { type: ['string', 'null'] },
palettePrimaryDark: { type: ['string', 'null'] },
palettePrimaryLight: { type: ['string', 'null'] },
palettePrimaryMain: { type: ['string', 'null'] },
title: { type: ['string', 'null'] },
createdAt: { type: 'string' },
updatedAt: { type: 'string' },
},
};
static get virtualAttributes() {
return [
'disableNotificationsPage',
'disableFavicon',
'additionalDrawerLink',
'additionalDrawerLinkIcon',
'additionalDrawerLinkText',
];
}
get disableNotificationsPage() {
return appConfig.disableNotificationsPage;
}
get disableFavicon() {
return appConfig.disableFavicon;
}
get additionalDrawerLink() {
return appConfig.additionalDrawerLink;
}
get additionalDrawerLinkIcon() {
return appConfig.additionalDrawerLinkIcon;
}
get additionalDrawerLinkText() {
return appConfig.additionalDrawerLinkText;
}
static async get() {
const existingConfig = await this.query().limit(1).first();
if (!existingConfig) {
return await this.query().insertAndFetch({});
}
return existingConfig;
}
static async update(config) {
const configEntry = await this.get();
return await configEntry.$query().patchAndFetch(config);
}
static async isInstallationCompleted() {
const installationCompletedEntry = await this.query()
.where({
key: 'installation.completed',
})
.first();
const config = await this.get();
const installationCompleted =
installationCompletedEntry?.value?.data === true;
return installationCompleted;
return config.installationCompleted;
}
static async markInstallationCompleted() {
return await this.query().insert({
key: 'installation.completed',
value: {
data: true,
},
const config = await this.get();
return await config.$query().patchAndFetch({
installationCompleted: true,
});
}
static async batchUpdate(config) {
const configKeys = Object.keys(config);
const updates = [];
for (const key of configKeys) {
const newValue = config[key];
if (newValue) {
const entryUpdate = Config.query()
.insert({
key,
value: {
data: newValue,
},
})
.onConflict('key')
.merge({
value: {
data: newValue,
},
});
updates.push(entryUpdate);
} else {
const entryUpdate = Config.query().findOne({ key }).delete();
updates.push(entryUpdate);
}
}
return await Promise.all(updates);
}
}
export default Config;

View File

@@ -0,0 +1,137 @@
import { describe, it, expect, vi } from 'vitest';
import appConfig from '../config/app.js';
import Config from './config';
import { createConfig } from '../../test/factories/config.js';
describe('Config model', () => {
it('tableName should return correct name', () => {
expect(Config.tableName).toBe('config');
});
it('jsonSchema should have correct validations', () => {
expect(Config.jsonSchema).toMatchSnapshot();
});
it('virtualAttributes should return correct attributes', () => {
const virtualAttributes = Config.virtualAttributes;
const expectedAttributes = [
'disableNotificationsPage',
'disableFavicon',
'additionalDrawerLink',
'additionalDrawerLinkIcon',
'additionalDrawerLinkText',
];
expect(virtualAttributes).toStrictEqual(expectedAttributes);
});
it('disableNotificationsPage should return its value in appConfig', async () => {
const disableNotificationsPageSpy = vi.spyOn(
appConfig,
'disableNotificationsPage',
'get'
);
new Config().disableNotificationsPage;
expect(disableNotificationsPageSpy).toHaveBeenCalledOnce();
});
it('disableFavicon should return its value in appConfig', async () => {
const disableFaviconSpy = vi
.spyOn(appConfig, 'disableFavicon', 'get')
.mockReturnValue(true);
new Config().disableFavicon;
expect(disableFaviconSpy).toHaveBeenCalledOnce();
});
it('additionalDrawerLink should return its value in appConfig', async () => {
const additionalDrawerLinkSpy = vi
.spyOn(appConfig, 'additionalDrawerLink', 'get')
.mockReturnValue('https://automatisch.io');
new Config().additionalDrawerLink;
expect(additionalDrawerLinkSpy).toHaveBeenCalledOnce();
});
it('additionalDrawerLinkIcon should return its value in appConfig', async () => {
const additionalDrawerLinkIconSpy = vi
.spyOn(appConfig, 'additionalDrawerLinkIcon', 'get')
.mockReturnValue('SampleIcon');
new Config().additionalDrawerLinkIcon;
expect(additionalDrawerLinkIconSpy).toHaveBeenCalledOnce();
});
it('additionalDrawerLinkText should return its value in appConfig', async () => {
const additionalDrawerLinkTextSpy = vi
.spyOn(appConfig, 'additionalDrawerLinkText', 'get')
.mockReturnValue('Go back to Automatisch');
new Config().additionalDrawerLinkText;
expect(additionalDrawerLinkTextSpy).toHaveBeenCalledOnce();
});
describe('get', () => {
it('should return single config record when it exists', async () => {
const createdConfig = await createConfig({
title: 'Automatisch',
});
const config = await Config.get();
expect(config).toStrictEqual(createdConfig);
});
it('should create config record and return when it does not exist', async () => {
const configBefore = await Config.query().first();
expect(configBefore).toBeUndefined();
const config = await Config.get();
expect(config).toBeTruthy();
});
});
it('update should update existing single record', async () => {
const patchAndFetchSpy = vi
.fn()
.mockImplementation((newConfig) => newConfig);
vi.spyOn(Config, 'get').mockImplementation(() => ({
$query: () => ({
patchAndFetch: patchAndFetchSpy,
}),
}));
const config = await Config.update({ title: 'Automatisch' });
expect(patchAndFetchSpy).toHaveBeenCalledWith({ title: 'Automatisch' });
expect(config).toStrictEqual({ title: 'Automatisch' });
});
it('isInstallationCompleted should return installationCompleted value', async () => {
const configGetSpy = vi.spyOn(Config, 'get').mockImplementation(() => ({
installationCompleted: true,
}));
await Config.isInstallationCompleted();
expect(configGetSpy).toHaveBeenCalledOnce();
});
it('markInstallationCompleted should update installationCompleted as true', async () => {
await Config.update({ installationCompleted: false });
const config = await Config.markInstallationCompleted();
expect(config.installationCompleted).toBe(true);
});
});

View File

@@ -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;

View File

@@ -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);
});
});
});

View File

@@ -5,7 +5,7 @@ class Datastore extends Base {
static jsonSchema = {
type: 'object',
required: ['key', 'value', 'scope', 'scopeId'],
required: ['key', 'value', 'scopeId'],
properties: {
id: { type: 'string', format: 'uuid' },

View File

@@ -0,0 +1,12 @@
import { describe, it, expect } from 'vitest';
import Datastore from './datastore';
describe('Datastore model', () => {
it('tableName should return correct name', () => {
expect(Datastore.tableName).toBe('datastore');
});
it('jsonSchema should have correct validations', () => {
expect(Datastore.jsonSchema).toMatchSnapshot();
});
});

View File

@@ -47,21 +47,31 @@ class ExecutionStep extends Base {
return this.status === 'failure';
}
async isSucceededNonTestRun() {
const execution = await this.$relatedQuery('execution');
return !execution.testRun && !this.isFailed;
}
async updateUsageData() {
const execution = await this.$relatedQuery('execution');
const flow = await execution.$relatedQuery('flow');
const user = await flow.$relatedQuery('user');
const usageData = await user.$relatedQuery('currentUsageData');
await usageData.increaseConsumedTaskCountByOne();
}
async increaseUsageCount() {
if (appConfig.isCloud && this.isSucceededNonTestRun()) {
await this.updateUsageData();
}
}
async $afterInsert(queryContext) {
await super.$afterInsert(queryContext);
Telemetry.executionStepCreated(this);
if (appConfig.isCloud) {
const execution = await this.$relatedQuery('execution');
if (!execution.testRun && !this.isFailed) {
const flow = await execution.$relatedQuery('flow');
const user = await flow.$relatedQuery('user');
const usageData = await user.$relatedQuery('currentUsageData');
await usageData.increaseConsumedTaskCountByOne();
}
}
await this.increaseUsageCount();
}
}

Some files were not shown because too many files have changed in this diff Show More