Compare commits

...

199 Commits

Author SHA1 Message Date
Ali BARIN
f36155f3a5 chore(coverage): add redundant code to test coverage threshold 2024-11-25 09:01:26 +00:00
Ali BARIN
09335fcd79 chore: add test coverage 2024-11-25 09:01:05 +00:00
Ömer Faruk Aydın
823a2c8b73 Merge pull request #2199 from automatisch/aut-1350-startTrialPeriod
test(user): write test for startTrialPeriod
2024-11-21 17:52:42 +03:00
Ali BARIN
741866e742 test(user): use luxon DateTime with zone over Date 2024-11-21 17:42:42 +03:00
Ali BARIN
41622678b0 test(user): write test for startTrialPeriod 2024-11-21 17:42:40 +03:00
Ömer Faruk Aydın
449b953401 Merge pull request #2198 from automatisch/aut-1350-generateHash
test(user): write tests for generateHash
2024-11-21 17:41:06 +03:00
Ali BARIN
6345ce5195 test(user): write tests for generateHash 2024-11-21 17:29:13 +03:00
Ömer Faruk Aydın
95651f6163 Merge pull request #2197 from automatisch/aut-1350-isInvitationTokenValid
test(user): write tests for isInvitationTokenValid
2024-11-21 17:23:10 +03:00
Ali BARIN
b02c1545b7 test(user): use luxon DateTime with zone over Date 2024-11-21 17:15:00 +03:00
Ali BARIN
2deaab9b24 test(user): write tests for isInvitationTokenValid 2024-11-21 17:14:56 +03:00
Ömer Faruk Aydın
f0d4853533 Merge pull request #2188 from automatisch/aut-1350-sendInvitationEmail
test(user): write test for sendInvitationEmail
2024-11-21 16:46:35 +03:00
Ali BARIN
af81ae812f Merge pull request #2222 from automatisch/fix-devcontainer
fix(devcontainer): remove yarn workspaces
2024-11-21 14:44:40 +01:00
Ali BARIN
bae76064e5 test(user): use luxon DateTime with zone over Date 2024-11-21 13:33:30 +00:00
Ali BARIN
07d9198cc8 test(user): write test for sendInvitationEmail 2024-11-21 13:31:41 +00:00
Ömer Faruk Aydın
a2e07ea2f7 Merge pull request #2187 from automatisch/aut-1350-isResetPasswordTokenValid
test(user): write test cases for isResetPasswordTokenValid
2024-11-21 16:29:50 +03:00
Ali BARIN
864c762fe2 test(user): use luxon DateTime with zone over Date 2024-11-21 12:49:33 +00:00
Ali BARIN
167bb4e8a0 test(user): write test cases for isResetPasswordTokenValid 2024-11-21 12:34:08 +00:00
Ömer Faruk Aydın
4cf64ede74 Merge pull request #2185 from automatisch/aut-1350-sendResetPasswordEmail
test(user): write test for sendResetPasswordEmail
2024-11-21 14:57:42 +03:00
Ali BARIN
bb309fea6f test(user): write test for sendResetPasswordEmail 2024-11-21 14:47:18 +03:00
Ömer Faruk Aydın
1133362028 Merge pull request #2184 from automatisch/aut-1350-softRemove
test(user): write test for softRemove
2024-11-21 14:34:53 +03:00
Ali BARIN
eb9226bd4a test(user): write test for softRemove 2024-11-21 14:23:29 +03:00
Ömer Faruk Aydın
a9abdcc37e Merge pull request #2212 from automatisch/remove-lerna
Remove lerna and yarn workspaces
2024-11-21 13:51:57 +03:00
Ali BARIN
6ace93bdbf fix(devcontainer): remove yarn workspaces 2024-11-21 10:17:43 +00:00
Faruk AYDIN
b89197939a docs: Adjust development setup page for new package setup 2024-11-21 12:40:31 +03:00
Faruk AYDIN
da788106af feat: Add separate gitignore file for docs package 2024-11-21 12:40:25 +03:00
Faruk AYDIN
49e92e6f1d fix: Adjust build web CI job to use cache dependency path 2024-11-19 20:50:15 +03:00
Faruk AYDIN
a6c3276104 chore: Remove root package.json file 2024-11-19 20:46:13 +03:00
Faruk AYDIN
6388bfc714 docs: Adjust repository structure page to reflect individual packages 2024-11-19 20:37:08 +03:00
Faruk AYDIN
bebc3b181d chore: Remove lerna debug file from .gitignore 2024-11-19 20:36:48 +03:00
Faruk AYDIN
5a6d561c1a refactor: Adjust dockerfile to work without lerna 2024-11-19 18:31:39 +03:00
Faruk AYDIN
5ba575fdfd fix: Add knex individually to e2e-tests package 2024-11-19 16:04:21 +03:00
Faruk AYDIN
dcf8bbd804 chore: Add axios package to e2e-tests package 2024-11-19 15:56:10 +03:00
Faruk AYDIN
ff93ffd0b1 fix: Adjust e2e-tests CI job working directories 2024-11-19 15:49:25 +03:00
Faruk AYDIN
395c09df92 fix: Use named ClickAwayListener import statement for web 2024-11-19 15:41:59 +03:00
Faruk AYDIN
4c903cd08b fix: Adjust backend start server CI job working directory 2024-11-19 15:36:01 +03:00
Faruk AYDIN
64cb98717c fix: Adjust backend start worker CI job working directory 2024-11-19 15:35:21 +03:00
Faruk AYDIN
b0e4ce54fb fix: Add fakerjs individually to backend package 2024-11-19 15:29:36 +03:00
Faruk AYDIN
d67a37002f fix: Use working directory for backend tests CI job 2024-11-19 15:24:40 +03:00
Faruk AYDIN
965ff8bc3f fix: Add axios individually to web package 2024-11-19 15:20:35 +03:00
Faruk AYDIN
400a495ad2 chore: Remove cache dependency path for build web CI job 2024-11-19 15:14:06 +03:00
Faruk AYDIN
09d0822a8d fix: Adjust working directory for build-web CI job 2024-11-19 15:09:46 +03:00
Faruk AYDIN
7016c20ccc fix: Change working directory for linter CI job 2024-11-19 15:03:54 +03:00
Faruk AYDIN
df54895805 fix: Linter command to work without workspaces 2024-11-19 15:00:23 +03:00
Faruk AYDIN
62d5e6fe51 chore: Remove root package dev dependencies 2024-11-19 14:57:39 +03:00
Faruk AYDIN
4615a0b7ea chore: Remove workspaces from root package.json file 2024-11-19 14:51:53 +03:00
Faruk AYDIN
280d603b14 chore: Remove lerna.json file 2024-11-18 20:21:20 +03:00
Faruk AYDIN
36271f0749 refactor: Explicitly state different packages for lerna 2024-11-18 20:16:20 +03:00
Ali BARIN
579638f932 Merge pull request #2210 from automatisch/release/v0.14.0
Update version to 0.14.0
2024-11-18 17:32:18 +01:00
Faruk AYDIN
48871c82a6 Update version to 0.14.0 2024-11-18 15:43:23 +00:00
Ali BARIN
14056c42ef Merge pull request #2211 from automatisch/upgrade-lerna
chore(deps): upgrade lerna with version 8
2024-11-18 16:42:08 +01:00
Ali BARIN
90fe1576de chore(lerna): remove bootstrap command usages 2024-11-18 15:00:49 +00:00
Ali BARIN
d61cf13985 chore(deps): upgrade lerna with version 8 2024-11-18 14:47:54 +00:00
Ali BARIN
dfe6dfd0c6 Merge pull request #2196 from automatisch/AUT-1364
feat: inline login form error and unify Alert font weight
2024-11-18 11:08:58 +01:00
Ali BARIN
c138c7d0e9 Merge pull request #2190 from automatisch/AUT-1347
feat: persist pagination and search value on flows page
2024-11-18 11:07:34 +01:00
Ali BARIN
d542be947e Merge pull request #2186 from automatisch/test-saml-auth-provider
test: Implement tests for saml auth provider url methods
2024-11-15 14:45:46 +01:00
kasia.oczkowska
c76366e72e feat: introduce improvements 2024-11-15 10:37:18 +00:00
kasia.oczkowska
75abfda783 feat: inline login form error and unify Alert font weight 2024-11-14 14:10:07 +00:00
Ali BARIN
f3d8d7d4ad Merge pull request #2191 from automatisch/AUT-1280
feat: make Wordpress instanceUrl field required
2024-11-14 12:31:43 +01:00
Ömer Faruk Aydın
7255eccb22 Merge pull request #2183 from automatisch/aut-1350-updatePassword
test(user): write tests for updatePassword
2024-11-13 19:31:28 +01:00
Ali BARIN
a0decb70cc test(user): write tests for updatePassword 2024-11-13 19:22:04 +01:00
kasia.oczkowska
532f562495 feat: make Wordpress instanceUrl field required 2024-11-13 14:32:16 +00:00
kasia.oczkowska
27e58ae925 feat: persist pagination and search value on flows page 2024-11-13 13:44:05 +00:00
Ali BARIN
abf30dfc1a Merge pull request #2167 from automatisch/AUT-1152
fix: variable chip label disappearing
2024-11-12 13:18:22 +01:00
Ali BARIN
218b8ce86e Merge pull request #2143 from automatisch/AUT-1325
feat: introduce uniqueness validation for remote role name
2024-11-12 13:18:07 +01:00
Faruk AYDIN
4867ffcb4b test: Implement tests for saml auth provider url methods 2024-11-12 13:12:00 +01:00
Ömer Faruk Aydın
e34c3b411d Merge pull request #2182 from automatisch/aut-1350-acceptInvitation
test(user): write test for acceptInvitation
2024-11-12 13:03:32 +01:00
Ali BARIN
c91b8be1a6 test(user): write test for acceptInvitation 2024-11-12 12:54:10 +01:00
Ömer Faruk Aydın
9cb41644a1 Merge pull request #2181 from automatisch/aut-1350-resetPassword
test(user): write test for resetPassword
2024-11-12 12:47:43 +01:00
Ali BARIN
8c01cea147 test(user): write test for resetPassword 2024-11-12 12:39:19 +01:00
Ömer Faruk Aydın
58eb55e90a Merge pull request #2180 from automatisch/aut-1350-generateInvitationToken
test(user): write test for generateInvitationToken
2024-11-12 12:37:31 +01:00
Ali BARIN
bb05e82e15 test(user): write test for generateInvitationToken 2024-11-12 12:18:28 +01:00
Ömer Faruk Aydın
5ab95ea175 Merge pull request #2179 from automatisch/aut-1350-generateResetPasswordToken
test(user): write test for generateResetPasswordToken
2024-11-12 12:16:42 +01:00
Ali BARIN
a25c4f1d1e test(user): write test for generateResetPasswordToken 2024-11-12 12:08:42 +01:00
Ömer Faruk Aydın
15287de8af Merge pull request #2178 from automatisch/aut-1350-login
test(user): write tests for login
2024-11-12 12:04:55 +01:00
Ali BARIN
49b4d6b511 test(user): write tests for login 2024-11-12 09:14:17 +00:00
Ömer Faruk Aydın
d5b4a5d4ac Merge pull request #2170 from automatisch/aut-1350-1
test(user): write tests for authorizedFlows
2024-11-11 18:16:03 +01:00
Ali BARIN
de480b491c test(user): write tests for authorizedExecutions 2024-11-11 18:06:59 +01:00
Ali BARIN
a949fda1fc test(user): write tests for authorizedConnections 2024-11-11 18:06:59 +01:00
Ali BARIN
3e28af670c test(user): write tests for authorizedSteps 2024-11-11 18:06:59 +01:00
Faruk AYDIN
b5310afb90 refactor: Use single quote for user can method error 2024-11-11 18:06:59 +01:00
Ali BARIN
da81ecf915 feat(user): add not authorized error message in permission check 2024-11-11 18:06:59 +01:00
Ali BARIN
f597066d16 test(user): write tests for authorizedFlows 2024-11-11 18:06:57 +01:00
Ömer Faruk Aydın
ec30606b24 Merge pull request #2177 from automatisch/aut-1350-authenticate
test(user): write tests for authenticate
2024-11-11 18:05:06 +01:00
Ali BARIN
20dce14f17 test(user): write tests for authenticate 2024-11-11 17:56:35 +01:00
Ömer Faruk Aydın
821742de85 Merge pull request #2176 from automatisch/aut-1350-acceptInvitationUrl
test(user): write test for acceptInvitationUrl
2024-11-11 17:55:23 +01:00
Ali BARIN
74dc108f62 test(user): write test for acceptInvitationUrl 2024-11-11 10:22:58 +00:00
Ali BARIN
a05fe856bb Merge pull request #2169 from automatisch/saml-auth-provider-test
test: Implement initial tests for SamlAuthProvider model
2024-11-08 13:04:09 +01:00
Ali BARIN
d13f51a32d Merge pull request #2168 from automatisch/aut-1350
test(user): write tests for tableName, jsonSchema, relationMappings
2024-11-08 13:01:02 +01:00
Faruk AYDIN
3dbe599cb3 test: Implement initial tests for SamlAuthProvider model 2024-11-08 12:55:37 +01:00
Ali BARIN
cf966dd83c test(user): write tests for tableName, jsonSchema, relationMappings 2024-11-08 11:52:50 +00:00
Ömer Faruk Aydın
4e62f3654f Merge pull request #2166 from automatisch/aut-1349-7
test(step): write test for updateFor method in model
2024-11-08 12:42:15 +01:00
Ali BARIN
970d926563 test(step): write test for updateFor method in model 2024-11-08 11:34:04 +00:00
Ali BARIN
ff49c747ba Merge pull request #2124 from automatisch/AUT-1232
fix: use correct default values when editing a role
2024-11-08 12:24:31 +01:00
kasia.oczkowska
c46b8a5f4f refactor: remove unnecessary prop types 2024-11-08 11:11:07 +00:00
kasia.oczkowska
485324e204 feat: improve syncing isCreator value 2024-11-08 10:01:49 +00:00
kasia.oczkowska
4696a03db1 feat: sync isCreator value when editing role settings 2024-11-08 10:01:49 +00:00
kasia.oczkowska
7885de36a9 fix: add isCreator role by default when creating new role 2024-11-08 10:01:49 +00:00
Ali BARIN
fac4339207 Merge pull request #2151 from automatisch/AUT-1167
feat: check if cancellationEffectiveDateObject is a valid date
2024-11-08 10:53:23 +01:00
Ali BARIN
9c70519021 Merge pull request #2152 from automatisch/AUT-1168
feat: introduce fallback for title value in the BillingCard
2024-11-08 10:53:02 +01:00
kasia.oczkowska
9ae77ecd5d fix: variable chip label disappearing 2024-11-08 08:20:33 +00:00
Ali BARIN
1c8e6f278d Merge pull request #2164 from automatisch/aut-1349-6
test(step): write tests for getSetupFields and delete
2024-11-06 14:08:26 +01:00
Ali BARIN
c0a190a9f2 test(step): write tests for getSetupFields and delete 2024-11-06 12:57:17 +00:00
Ali BARIN
e29e2a62f0 Merge pull request #2165 from automatisch/aut-1349-hooks
test(step): write tests for lifecycle hooks in model
2024-11-06 13:53:25 +01:00
Ali BARIN
1580640a35 test(step): write tests for lifecycle hooks in model 2024-11-06 12:45:18 +00:00
Ömer Faruk Aydın
33c84b7fcc Merge pull request #2163 from automatisch/aut-1349-5
test(step): write tests for getTriggerCommand and getActionCommand
2024-11-06 12:30:45 +01:00
Faruk AYDIN
9773ce75b0 fix: Typo for the step test description 2024-11-06 12:20:49 +01:00
Ali BARIN
c310e8d152 test(step): write tests for getTriggerCommand and getActionCommand 2024-11-06 08:45:24 +00:00
Ömer Faruk Aydın
af251c7b81 Merge pull request #2162 from automatisch/aut-1349-4
test(step): write tests for getApp, test, getLastExecutionStep and getNextStep
2024-11-05 13:19:48 +01:00
Faruk AYDIN
122483de0c refactor: Rename test method description for tests 2024-11-05 13:01:42 +01:00
Ali BARIN
42c2131144 test(step): write tests for getApp, test, getLastExecutionStep and getNextStep 2024-11-04 14:18:37 +00:00
Ömer Faruk Aydın
71bc7a62c2 Merge pull request #2161 from automatisch/aut-1349-3
test(step): write tests for isTrigger, isAction, getWebhookUrl
2024-11-04 15:11:00 +01:00
Ali BARIN
87bfff07db test(step): write tests for isTrigger, isAction, getWebhookUrl 2024-11-04 12:43:24 +00:00
Ömer Faruk Aydın
1cb5b780d2 Merge pull request #2160 from automatisch/aut-1349-2
test(step): write tests for webhookUrl and iconUrl
2024-11-04 13:28:25 +01:00
Ömer Faruk Aydın
2f6acd4d6e Merge pull request #2159 from automatisch/aut-1349
test(step): cover tableName, jsonSchema, virtualAttributes, relationMappings in model
2024-11-04 13:27:40 +01:00
Ömer Faruk Aydın
c2e2351505 Merge pull request #2158 from automatisch/aut-1338-isPaused
test(flow): write test cases for isPaused method
2024-11-04 13:26:40 +01:00
Ali BARIN
d847b5480b Merge pull request #2156 from automatisch/aut-1338-delete
refactor(flow): split delete method and write test
2024-11-01 15:41:52 +01:00
Ali BARIN
32749ee58e test(step): write tests for webhookUrl and iconUrl 2024-11-01 14:32:38 +00:00
Ali BARIN
a531b8b5fe test(step): cover tableName, jsonSchema, virtualAttributes, relationMappings in model 2024-11-01 15:29:35 +01:00
Ali BARIN
148a0c5bb0 test(flow): write test cases for isPaused method 2024-11-01 15:29:17 +01:00
Ali BARIN
39f9a58200 refactor(flow): split delete method and write test 2024-11-01 14:27:19 +00:00
Ömer Faruk Aydın
edd113d344 Merge pull request #2153 from automatisch/aut-1338-createActionStep
refactor(flow): distribute createActionStep logic to different methods in model
2024-11-01 15:03:58 +01:00
Faruk AYDIN
c641e8729b refactor: Rename createActionStepAfterStepId as createAfterStep for flow 2024-11-01 14:24:07 +01:00
Faruk AYDIN
2c4b13e4b5 refactor: Start updateStepPositionsFrom test from step position 2 2024-11-01 14:17:46 +01:00
Faruk AYDIN
48fcf4dda7 refactor: Rename alignStepsPositionsAsOfPosition as updateStepPositionsFrom for flow 2024-11-01 14:16:13 +01:00
Ömer Faruk Aydın
acfd980d4f Merge pull request #2150 from automatisch/aut-1338-after-find
refactor(flow): restructure afterFind hook in model
2024-10-31 16:12:08 +01:00
Ömer Faruk Aydın
db9bfab812 Merge pull request #2149 from automatisch/aut-1338-3
test(flow): write tests for model lifecycle hooks
2024-10-31 16:04:37 +01:00
Ali BARIN
d32820ee09 refactor(flow): distribute createActionStep logic to different methods in model 2024-10-31 10:19:26 +01:00
Ali BARIN
0f823fd19e refactor(flow): restructure afterFind hook in model 2024-10-31 10:19:09 +01:00
Ali BARIN
4308ed5850 test(flow): write tests for model lifecycle hooks 2024-10-31 10:18:56 +01:00
Ali BARIN
b9cd7c3983 Merge pull request #2148 from automatisch/aut-1338-2
test(flow): write model tests
2024-10-31 10:18:42 +01:00
Ömer Faruk Aydın
fa607aa961 Merge pull request #2154 from automatisch/update-saml-auth-providers-role-mapping-test-snapshot
chore(saml-auth-providers-role-mapping): remove unused test snapshot
2024-10-30 15:05:19 +01:00
Ali BARIN
6900b71841 test(flow): write model tests 2024-10-30 13:56:47 +00:00
Ömer Faruk Aydın
bb230d67e8 Merge pull request #2139 from automatisch/aut-1338
test(flow): write model tests
2024-10-30 14:55:21 +01:00
Faruk AYDIN
4f076ec3e3 refactor: Use static IncompleteStepsError from flow before update hook 2024-10-30 14:42:30 +01:00
Faruk AYDIN
96a6cbfb95 refactor: Add empty lines to improve readibility for flow tests 2024-10-30 14:39:06 +01:00
Faruk AYDIN
5bdc5aed72 refactor: Use static method for IncompleteStepsError 2024-10-30 14:39:06 +01:00
Faruk AYDIN
d38b0f088b test: Add multiple executions explicitly for last internal id test 2024-10-30 14:39:06 +01:00
Faruk AYDIN
892710f705 chore: Add empty lines for flow test to improve readability 2024-10-30 14:39:06 +01:00
Ali BARIN
fbf898be64 test(flow): write model tests 2024-10-30 14:39:06 +01:00
Ali BARIN
e3e2ecc1e1 chore(saml-auth-providers-role-mapping): remove unused test snapshot 2024-10-30 13:05:52 +00:00
kasia.oczkowska
b59807d221 feat: introduce fallback for title value in the BillingCard 2024-10-30 12:45:24 +00:00
kasia.oczkowska
163ad52285 feat: check if cancellationEffectiveDateObject is a valid date 2024-10-30 10:35:55 +00:00
Ömer Faruk Aydın
4023a6d1cc Merge pull request #2146 from automatisch/aut-1332-new-2
test(role): write remaining model tests
2024-10-29 17:45:45 +01:00
Ömer Faruk Aydın
ec827e5dc0 Merge pull request #2145 from automatisch/aut-1332-new
test(role): write model tests
2024-10-28 16:24:35 +01:00
Ali BARIN
a8f4fb7c22 test(role): write remaining model tests 2024-10-28 14:05:07 +00:00
Ali BARIN
bc195ed452 test(role): write model tests 2024-10-28 13:50:32 +00:00
Ali BARIN
41568904ab Merge pull request #2142 from automatisch/refactor-tests
refactor: Use toStrictEqual instead of toEqual for tests
2024-10-28 08:49:34 +01:00
kasia.oczkowska
79050af391 feat: introduce uniqueness validation for remote role name 2024-10-25 15:44:28 +01:00
Faruk AYDIN
2e5b44c424 refactor: Use toStrictEqual instead of toEqual for tests 2024-10-25 12:33:39 +02:00
Faruk AYDIN
7a437660d1 refactor: Remove id column from app config serializer 2024-10-25 11:47:41 +02:00
Ömer Faruk Aydın
47510e24d5 Merge pull request #2110 from automatisch/aut-1293
test(app-config): write model tests
2024-10-25 01:18:26 +02:00
Faruk AYDIN
91c9ef3068 fix: Pass app config parameters to be used for hooks 2024-10-25 01:07:44 +02:00
Faruk AYDIN
240854e4ac fix: Use uuid for down migration of app config id removal 2024-10-25 00:59:00 +02:00
Jakub P.
0e4fc7efbc fix: use key instead of id in appConfig 2024-10-24 19:42:54 +02:00
Faruk AYDIN
b47e859225 test: Add additional cases for triggerAppConfigUpdate method 2024-10-24 17:10:47 +02:00
Faruk AYDIN
62a1072682 fix: Use triggerAppConfigUpdate spy instead of 2024-10-24 17:07:23 +02:00
Faruk AYDIN
c6f2a97591 test: Add missing associations test for app auth client 2024-10-24 17:04:45 +02:00
Faruk AYDIN
d66be231b3 refactor: Remove redundant updateConnectionAllowedProperty 2024-10-24 17:00:47 +02:00
Faruk AYDIN
f73ffc8711 test: Add idColumn test to app config model 2024-10-24 16:59:17 +02:00
Faruk AYDIN
e4c17c1bc7 refactor: Use belongsTo relation for app config association 2024-10-24 16:59:17 +02:00
Faruk AYDIN
997e729535 refactor: Use hooks with refetched record for app config 2024-10-24 16:59:17 +02:00
Faruk AYDIN
e0e313b8d1 refactor: Remove id from app config factory 2024-10-24 16:59:17 +02:00
Faruk AYDIN
f0bd763e72 feat: Remove id field from app config model 2024-10-24 16:59:17 +02:00
Faruk AYDIN
6a7a90536b feat: Make key field primary key for app config model 2024-10-24 16:59:17 +02:00
Ali BARIN
ac8ddedfb5 test(connection): use new properties from app-config 2024-10-24 16:59:17 +02:00
Ali BARIN
6fcd126ff8 test(app-auth-client): cover lifecycle hooks and triggerAppConfigUpdate method 2024-10-24 16:59:17 +02:00
Faruk AYDIN
55d0966d48 fix: Pass app key while triggering app config update 2024-10-24 16:59:17 +02:00
Faruk AYDIN
2583e08f7a fix: Don't compute connectionAllowed column twice 2024-10-24 16:59:17 +02:00
Faruk AYDIN
de72e62470 fix: Pass app config key to fix associations 2024-10-24 16:59:17 +02:00
Faruk AYDIN
91993dbb07 refactor: AppConfig model and corresponding tests 2024-10-24 16:59:17 +02:00
Faruk AYDIN
d87ee4daa3 refactor: Move and adjust getApp tests for app config model 2024-10-24 16:59:17 +02:00
Ali BARIN
6791e002ff test(app-config): remove redundant virtual attributes test case 2024-10-24 16:59:17 +02:00
Ali BARIN
4ca84aa515 refactor(app-auth-client): remove redundant column selection 2024-10-24 16:59:17 +02:00
Ali BARIN
8189cbc171 fix(app-config): use correct case in connection_allowed 2024-10-24 16:59:17 +02:00
Ali BARIN
73edb45ff7 refactor(app-config): rename allowCustomConnection as customConnectionAllowed 2024-10-24 16:59:17 +02:00
Ali BARIN
0bbe362660 refactor(app-config): rename allow_custom_connection as custom_connection_allowed 2024-10-24 16:59:17 +02:00
Ali BARIN
a76bee51fc refactor(app-config): remove canCustomConnect virtual attribute 2024-10-24 16:59:17 +02:00
Ali BARIN
6e42b52414 refactor(app-config): rename canConnect as connectionAllowed 2024-10-24 16:59:17 +02:00
Ali BARIN
aed61209fa feat(app-config): update canConnect upon dependent changes 2024-10-24 16:59:17 +02:00
Ali BARIN
f5d796ea77 feat(app-config): persist relational virtual attrs 2024-10-24 16:59:17 +02:00
Ali BARIN
ecb04b4ba9 test(app-config): write model tests 2024-10-24 16:59:17 +02:00
Ali BARIN
dabb01e237 Merge pull request #2127 from automatisch/AUT-1231
feat: add error snackbar when creating or updating saml auth provider
2024-10-22 12:13:05 +02:00
Ali BARIN
c2d27d0fd4 Merge pull request #2136 from automatisch/aut-1322
test(connection): write remaining model tests
2024-10-21 17:41:31 +02:00
Ali BARIN
e62bd75fdf Merge pull request #2122 from automatisch/AUT-1239
feat: add error snackbar for updating role mappings
2024-10-21 16:44:24 +02:00
Ali BARIN
2e917bd62b Merge pull request #2120 from automatisch/AUT-1097
feat: allow both number and string values as sampleValue
2024-10-21 16:41:58 +02:00
Ali BARIN
e0492c4264 Merge pull request #2129 from automatisch/AUT-1253
feat: add error snackbar for errors originating from registerUser function
2024-10-21 16:41:31 +02:00
Ali BARIN
7db68e2f96 test(connection): write remaining model tests 2024-10-21 13:10:29 +00:00
Ömer Faruk Aydın
e9b05a37d1 Merge pull request #2135 from automatisch/aut-1322-2
test(connection): cover model lifecycle hooks
2024-10-21 12:49:49 +02:00
Ali BARIN
5613259536 test(connection): cover model lifecycle hooks 2024-10-21 12:41:57 +02:00
Ömer Faruk Aydın
3209ff16ac Merge pull request #2130 from automatisch/aut-1322
test(connection): write model tests
2024-10-21 12:41:04 +02:00
Faruk AYDIN
a49c8602d1 refactor: Remove redundant test cases for connection model 2024-10-21 12:32:46 +02:00
Ali BARIN
7caa055e00 test(connection): write model tests 2024-10-21 10:27:43 +02:00
Ömer Faruk Aydın
0d62bc6c78 Merge pull request #2134 from automatisch/permission-tests
Permission tests
2024-10-18 17:33:57 +02:00
Faruk AYDIN
bc0861fd9e test: Implement tests for permission model 2024-10-18 15:34:06 +02:00
Faruk AYDIN
f280052d93 refactor: Permission model sanitize method 2024-10-18 15:33:47 +02:00
kasia.oczkowska
21da49f79d feat: add error snackbar for errors originating from registerUser function 2024-10-18 13:41:57 +01:00
kasia.oczkowska
19a5ccf942 feat: add error snackbar when creating or updating saml auth provider 2024-10-16 14:31:05 +01:00
kasia.oczkowska
2981fa5946 feat: add error snackbar for updating role mappings 2024-10-10 13:34:40 +01:00
kasia.oczkowska
05a3016557 feat: allow both number and string values as sampleValue 2024-10-10 09:21:03 +01:00
186 changed files with 23109 additions and 17817 deletions

View File

@@ -5,8 +5,11 @@ BACKEND_PORT=3000
WEB_PORT=3001
echo "Configuring backend environment variables..."
cd packages/backend
rm -rf .env
echo "
PORT=$BACKEND_PORT
WEB_APP_URL=http://localhost:$WEB_PORT
@@ -21,24 +24,35 @@ WEBHOOK_SECRET_KEY=sample_webhook_secret_key
APP_SECRET_KEY=sample_app_secret_key
REDIS_HOST=redis
SERVE_WEB_APP_SEPARATELY=true" >> .env
echo "Installing backend dependencies..."
yarn
cd $CURRENT_DIR
echo "Configuring web environment variables..."
cd packages/web
rm -rf .env
echo "
PORT=$WEB_PORT
REACT_APP_BACKEND_URL=http://localhost:$BACKEND_PORT
" >> .env
echo "Installing web dependencies..."
yarn
cd $CURRENT_DIR
echo "Installing and linking dependencies..."
yarn
yarn lerna bootstrap
echo "Migrating database..."
cd packages/backend
yarn db:migrate
yarn db:seed:user
echo "Done!"
echo "Done!"

View File

@@ -41,8 +41,11 @@ jobs:
with:
node-version: 18
- name: Install dependencies
run: cd packages/backend && yarn
run: yarn
working-directory: packages/backend
- name: Copy .env-example.test file to .env.test
run: cd packages/backend && cp .env-example.test .env.test
run: cp .env-example.test .env.test
working-directory: packages/backend
- name: Run tests
run: cd packages/backend && yarn test
run: yarn test:coverage
working-directory: packages/backend

View File

@@ -18,11 +18,13 @@ jobs:
with:
node-version: '18'
cache: 'yarn'
cache-dependency-path: yarn.lock
cache-dependency-path: packages/backend/yarn.lock
- run: echo "💡 The ${{ github.repository }} repository has been cloned to the runner."
- run: echo "🖥️ The workflow is now ready to test your code on the runner."
- run: yarn --frozen-lockfile
- run: cd packages/backend && yarn lint
working-directory: packages/backend
- run: yarn lint
working-directory: packages/backend
- run: echo "🍏 This job's status is ${{ job.status }}."
start-backend-server:
runs-on: ubuntu-latest
@@ -35,11 +37,13 @@ jobs:
with:
node-version: '18'
cache: 'yarn'
cache-dependency-path: yarn.lock
cache-dependency-path: packages/backend/yarn.lock
- run: echo "💡 The ${{ github.repository }} repository has been cloned to the runner."
- run: echo "🖥️ The workflow is now ready to test your code on the runner."
- run: yarn --frozen-lockfile && yarn lerna bootstrap
- run: cd packages/backend && yarn start
- run: yarn --frozen-lockfile
working-directory: packages/backend
- run: yarn start
working-directory: packages/backend
env:
ENCRYPTION_KEY: sample_encryption_key
WEBHOOK_SECRET_KEY: sample_webhook_secret_key
@@ -55,11 +59,13 @@ jobs:
with:
node-version: '18'
cache: 'yarn'
cache-dependency-path: yarn.lock
cache-dependency-path: packages/backend/yarn.lock
- run: echo "💡 The ${{ github.repository }} repository has been cloned to the runner."
- run: echo "🖥️ The workflow is now ready to test your code on the runner."
- run: yarn --frozen-lockfile && yarn lerna bootstrap
- run: cd packages/backend && yarn start:worker
- run: yarn --frozen-lockfile
working-directory: packages/backend
- run: yarn start:worker
working-directory: packages/backend
env:
ENCRYPTION_KEY: sample_encryption_key
WEBHOOK_SECRET_KEY: sample_webhook_secret_key
@@ -75,11 +81,13 @@ jobs:
with:
node-version: '18'
cache: 'yarn'
cache-dependency-path: yarn.lock
cache-dependency-path: packages/web/yarn.lock
- run: echo "💡 The ${{ github.repository }} repository has been cloned to the runner."
- run: echo "🖥️ The workflow is now ready to test your code on the runner."
- run: yarn --frozen-lockfile && yarn lerna bootstrap
- run: cd packages/web && yarn build
- run: yarn --frozen-lockfile
working-directory: packages/web
- run: yarn build
working-directory: packages/web
env:
CI: false
- run: echo "🍏 This job's status is ${{ job.status }}."

View File

@@ -58,13 +58,21 @@ jobs:
- uses: actions/setup-node@v3
with:
node-version: 18
- name: Install dependencies
run: yarn && yarn lerna bootstrap
- name: Install web dependencies
run: yarn
working-directory: ./packages/web
- name: Install backend dependencies
run: yarn
working-directory: ./packages/backend
- name: Install e2e-tests dependencies
run: yarn
working-directory: ./packages/e2e-tests
- name: Install Playwright Browsers
run: yarn playwright install --with-deps
working-directory: ./packages/e2e-tests
- name: Build Automatisch web
working-directory: ./packages/web
run: yarn build
working-directory: ./packages/web
env:
# Keep this until we clean up warnings in build processes
CI: false

1
.gitignore vendored
View File

@@ -4,7 +4,6 @@ logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)

View File

@@ -11,10 +11,12 @@ WORKDIR /automatisch
# copy the app, note .dockerignore
COPY . /automatisch
RUN yarn
RUN cd packages/web && yarn
RUN cd packages/web && yarn build
RUN cd packages/backend && yarn --production
RUN \
rm -rf /usr/local/share/.cache/ && \
apk del build-dependencies

View File

@@ -1,13 +0,0 @@
{
"packages": [
"packages/*"
],
"version": "0.10.0",
"npmClient": "yarn",
"useWorkspaces": true,
"command": {
"add": {
"exact": true
}
}
}

View File

@@ -1,32 +0,0 @@
{
"name": "@automatisch/root",
"license": "See LICENSE file",
"private": true,
"scripts": {
"start": "lerna run --stream --parallel --scope=@*/{web,backend} dev",
"start:web": "lerna run --stream --scope=@*/web dev",
"start:backend": "lerna run --stream --scope=@*/backend dev",
"build:docs": "cd ./packages/docs && yarn install && yarn build"
},
"workspaces": {
"packages": [
"packages/*"
],
"nohoist": [
"**/babel-loader",
"**/webpack",
"**/@automatisch/web",
"**/ajv"
]
},
"devDependencies": {
"eslint": "^8.13.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^4.0.0",
"lerna": "^4.0.0",
"prettier": "^2.5.1"
},
"publishConfig": {
"access": "public"
}
}

View File

@@ -12,6 +12,7 @@
"pretest": "APP_ENV=test node ./test/setup/prepare-test-env.js",
"test": "APP_ENV=test vitest run",
"test:watch": "APP_ENV=test vitest watch",
"test:coverage": "yarn test --coverage",
"lint": "eslint .",
"db:create": "node ./bin/database/create.js",
"db:seed:user": "node ./bin/database/seed-user.js",
@@ -23,6 +24,7 @@
"dependencies": {
"@bull-board/express": "^3.10.1",
"@casl/ability": "^6.5.0",
"@faker-js/faker": "^9.2.0",
"@node-saml/passport-saml": "^4.0.4",
"@rudderstack/rudder-sdk-node": "^1.1.2",
"@sentry/node": "^7.42.0",
@@ -36,6 +38,9 @@
"crypto-js": "^4.1.1",
"debug": "~2.6.9",
"dotenv": "^10.0.0",
"eslint": "^8.13.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^4.0.0",
"express": "~4.18.2",
"express-async-errors": "^3.1.1",
"express-basic-auth": "^1.2.1",
@@ -61,6 +66,7 @@
"pg": "^8.7.1",
"php-serialize": "^4.0.2",
"pluralize": "^8.0.0",
"prettier": "^2.5.1",
"raw-body": "^2.5.2",
"showdown": "^2.1.0",
"uuid": "^9.0.1",
@@ -92,10 +98,11 @@
"url": "https://github.com/automatisch/automatisch/issues"
},
"devDependencies": {
"@vitest/coverage-v8": "^2.1.5",
"node-gyp": "^10.1.0",
"nodemon": "^2.0.13",
"supertest": "^6.3.3",
"vitest": "^1.1.3"
"vitest": "^2.1.5"
},
"publishConfig": {
"access": "public"

View File

@@ -8,7 +8,7 @@ export default {
key: 'instanceUrl',
label: 'WordPress instance URL',
type: 'string',
required: false,
required: true,
readOnly: false,
value: null,
placeholder: null,

View File

@@ -52,7 +52,7 @@ const appConfig = {
isDev: appEnv === 'development',
isTest: appEnv === 'test',
isProd: appEnv === 'production',
version: '0.13.1',
version: '0.14.0',
postgresDatabase: process.env.POSTGRES_DATABASE || 'automatisch_development',
postgresSchema: process.env.POSTGRES_SCHEMA || 'public',
postgresPort: parseInt(process.env.POSTGRES_PORT || '5432'),

View File

@@ -32,7 +32,7 @@ describe('POST /api/v1/access-tokens', () => {
})
.expect(422);
expect(response.body.errors.general).toEqual([
expect(response.body.errors.general).toStrictEqual([
'Incorrect email or password.',
]);
});

View File

@@ -83,7 +83,7 @@ describe('POST /api/v1/admin/apps/:appKey/auth-clients', () => {
.send(appAuthClient)
.expect(422);
expect(response.body.meta.type).toEqual('ModelValidation');
expect(response.body.meta.type).toStrictEqual('ModelValidation');
expect(response.body.errors).toMatchObject({
name: ["must have required property 'name'"],
formattedAuthDefaults: [

View File

@@ -10,11 +10,11 @@ export default async (request, response) => {
};
const appConfigParams = (request) => {
const { allowCustomConnection, shared, disabled } = request.body;
const { customConnectionAllowed, shared, disabled } = request.body;
return {
key: request.params.appKey,
allowCustomConnection,
customConnectionAllowed,
shared,
disabled,
};

View File

@@ -23,7 +23,7 @@ describe('POST /api/v1/admin/apps/:appKey/config', () => {
it('should return created app config', async () => {
const appConfig = {
allowCustomConnection: true,
customConnectionAllowed: true,
shared: true,
disabled: false,
};
@@ -44,7 +44,7 @@ describe('POST /api/v1/admin/apps/:appKey/config', () => {
it('should return HTTP 422 for already existing app config', async () => {
const appConfig = {
key: 'gitlab',
allowCustomConnection: true,
customConnectionAllowed: true,
shared: true,
disabled: false,
};
@@ -59,7 +59,7 @@ describe('POST /api/v1/admin/apps/:appKey/config', () => {
})
.expect(422);
expect(response.body.meta.type).toEqual('UniqueViolationError');
expect(response.body.meta.type).toStrictEqual('UniqueViolationError');
expect(response.body.errors).toMatchObject({
key: ["'key' must be unique."],
});

View File

@@ -32,7 +32,7 @@ describe('GET /api/v1/admin/apps/:appKey/auth-clients/:appAuthClientId', () => {
.expect(200);
const expectedPayload = getAppAuthClientMock(currentAppAuthClient);
expect(response.body).toEqual(expectedPayload);
expect(response.body).toStrictEqual(expectedPayload);
});
it('should return not found response for not existing app auth client ID', async () => {

View File

@@ -39,6 +39,6 @@ describe('GET /api/v1/admin/apps/:appKey/auth-clients', () => {
appAuthClientOne,
]);
expect(response.body).toEqual(expectedPayload);
expect(response.body).toStrictEqual(expectedPayload);
});
});

View File

@@ -8,16 +8,19 @@ export default async (request, response) => {
})
.throwIfNotFound();
await appConfig.$query().patchAndFetch(appConfigParams(request));
await appConfig.$query().patchAndFetch({
...appConfigParams(request),
key: request.params.appKey,
});
renderObject(response, appConfig);
};
const appConfigParams = (request) => {
const { allowCustomConnection, shared, disabled } = request.body;
const { customConnectionAllowed, shared, disabled } = request.body;
return {
allowCustomConnection,
customConnectionAllowed,
shared,
disabled,
};

View File

@@ -24,7 +24,7 @@ describe('PATCH /api/v1/admin/apps/:appKey/config', () => {
it('should return updated app config', async () => {
const appConfig = {
key: 'gitlab',
allowCustomConnection: true,
customConnectionAllowed: true,
shared: true,
disabled: false,
};
@@ -34,7 +34,7 @@ describe('PATCH /api/v1/admin/apps/:appKey/config', () => {
const newAppConfigValues = {
shared: false,
disabled: true,
allowCustomConnection: false,
customConnectionAllowed: false,
};
const response = await request(app)
@@ -55,7 +55,7 @@ describe('PATCH /api/v1/admin/apps/:appKey/config', () => {
const appConfig = {
shared: false,
disabled: true,
allowCustomConnection: false,
customConnectionAllowed: false,
};
await request(app)
@@ -68,7 +68,7 @@ describe('PATCH /api/v1/admin/apps/:appKey/config', () => {
it('should return HTTP 422 for invalid app config data', async () => {
const appConfig = {
key: 'gitlab',
allowCustomConnection: true,
customConnectionAllowed: true,
shared: true,
disabled: false,
};
@@ -83,7 +83,7 @@ describe('PATCH /api/v1/admin/apps/:appKey/config', () => {
})
.expect(422);
expect(response.body.meta.type).toEqual('ModelValidation');
expect(response.body.meta.type).toStrictEqual('ModelValidation');
expect(response.body.errors).toMatchObject({
disabled: ['must be boolean'],
});

View File

@@ -50,8 +50,8 @@ describe('PATCH /api/v1/admin/config', () => {
.send(newConfigValues)
.expect(200);
expect(response.body.data.title).toEqual(newTitle);
expect(response.body.meta.type).toEqual('Config');
expect(response.body.data.title).toStrictEqual(newTitle);
expect(response.body.meta.type).toStrictEqual('Config');
});
it('should return created config for unexisting config', async () => {
@@ -67,8 +67,8 @@ describe('PATCH /api/v1/admin/config', () => {
.send(newConfigValues)
.expect(200);
expect(response.body.data.title).toEqual(newTitle);
expect(response.body.meta.type).toEqual('Config');
expect(response.body.data.title).toStrictEqual(newTitle);
expect(response.body.meta.type).toStrictEqual('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('Config');
expect(response.body.meta.type).toStrictEqual('Config');
});
});

View File

@@ -27,6 +27,6 @@ describe('GET /api/v1/admin/permissions/catalog', () => {
const expectedPayload = await getPermissionsCatalogMock();
expect(response.body).toEqual(expectedPayload);
expect(response.body).toStrictEqual(expectedPayload);
});
});

View File

@@ -58,7 +58,7 @@ describe('POST /api/v1/admin/roles', () => {
]
);
expect(response.body).toEqual(expectedPayload);
expect(response.body).toStrictEqual(expectedPayload);
});
it('should return unprocessable entity response for invalid role data', async () => {

View File

@@ -92,21 +92,4 @@ describe('DELETE /api/v1/admin/roles/:roleId', () => {
},
});
});
it('should not delete role and permissions on unsuccessful response', async () => {
const role = await createRole();
const permission = await createPermission({ roleId: role.id });
await createUser({ roleId: role.id });
await request(app)
.delete(`/api/v1/admin/roles/${role.id}`)
.set('Authorization', token)
.expect(422);
const refetchedRole = await role.$query();
const refetchedPermission = await permission.$query();
expect(refetchedRole).toStrictEqual(role);
expect(refetchedPermission).toStrictEqual(permission);
});
});

View File

@@ -34,7 +34,7 @@ describe('GET /api/v1/admin/roles/:roleId', () => {
permissionTwo,
]);
expect(response.body).toEqual(expectedPayload);
expect(response.body).toStrictEqual(expectedPayload);
});
it('should return not found response for not existing role UUID', async () => {

View File

@@ -28,6 +28,6 @@ describe('GET /api/v1/admin/roles', () => {
const expectedPayload = await getRolesMock([roleOne, roleTwo]);
expect(response.body).toEqual(expectedPayload);
expect(response.body).toStrictEqual(expectedPayload);
});
});

View File

@@ -46,6 +46,6 @@ describe('GET /api/v1/admin/saml-auth-providers/:samlAuthProviderId/role-mapping
roleMappingTwo,
]);
expect(response.body).toEqual(expectedPayload);
expect(response.body).toStrictEqual(expectedPayload);
});
});

View File

@@ -30,7 +30,7 @@ describe('GET /api/v1/admin/saml-auth-provider/:samlAuthProviderId', () => {
const expectedPayload = await getSamlAuthProviderMock(samlAuthProvider);
expect(response.body).toEqual(expectedPayload);
expect(response.body).toStrictEqual(expectedPayload);
});
it('should return not found response for not existing saml auth provider UUID', async () => {

View File

@@ -34,6 +34,6 @@ describe('GET /api/v1/admin/saml-auth-providers', () => {
samlAuthProviderOne,
]);
expect(response.body).toEqual(expectedPayload);
expect(response.body).toStrictEqual(expectedPayload);
});
});

View File

@@ -30,7 +30,7 @@ describe('GET /api/v1/admin/users/:userId', () => {
.expect(200);
const expectedPayload = getUserMock(anotherUser, anotherUserRole);
expect(response.body).toEqual(expectedPayload);
expect(response.body).toStrictEqual(expectedPayload);
});
it('should return not found response for not existing user UUID', async () => {

View File

@@ -40,6 +40,6 @@ describe('GET /api/v1/admin/users', () => {
[anotherUserRole, currentUserRole]
);
expect(response.body).toEqual(expectedResponsePayload);
expect(response.body).toStrictEqual(expectedResponsePayload);
});
});

View File

@@ -61,7 +61,8 @@ describe('PATCH /api/v1/admin/users/:userId', () => {
.send(anotherUserUpdatedData)
.expect(422);
expect(response.body.meta.type).toEqual('ModelValidation');
expect(response.body.meta.type).toStrictEqual('ModelValidation');
expect(response.body.errors).toMatchObject({
email: ['must be string'],
fullName: ['must be string'],

View File

@@ -155,7 +155,7 @@ describe('POST /api/v1/apps/:appKey/connections', () => {
await createAppConfig({
key: 'gitlab',
disabled: false,
allowCustomConnection: true,
customConnectionAllowed: true,
});
});
@@ -218,7 +218,7 @@ describe('POST /api/v1/apps/:appKey/connections', () => {
await createAppConfig({
key: 'gitlab',
disabled: false,
allowCustomConnection: false,
customConnectionAllowed: false,
});
});

View File

@@ -29,7 +29,7 @@ describe('GET /api/v1/apps/:appKey/actions/:actionKey/substeps', () => {
.expect(200);
const expectedPayload = getActionSubstepsMock(exampleAction.substeps);
expect(response.body).toEqual(expectedPayload);
expect(response.body).toStrictEqual(expectedPayload);
});
it('should return not found response for invalid app key', async () => {
@@ -47,6 +47,6 @@ describe('GET /api/v1/apps/:appKey/actions/:actionKey/substeps', () => {
.set('Authorization', token)
.expect(200);
expect(response.body.data).toEqual([]);
expect(response.body.data).toStrictEqual([]);
});
});

View File

@@ -23,7 +23,7 @@ describe('GET /api/v1/apps/:appKey/actions', () => {
.expect(200);
const expectedPayload = getActionsMock(exampleApp.actions);
expect(response.body).toEqual(expectedPayload);
expect(response.body).toStrictEqual(expectedPayload);
});
it('should return not found response for invalid app key', async () => {

View File

@@ -23,7 +23,7 @@ describe('GET /api/v1/apps/:appKey', () => {
.expect(200);
const expectedPayload = getAppMock(exampleApp);
expect(response.body).toEqual(expectedPayload);
expect(response.body).toStrictEqual(expectedPayload);
});
it('should return not found response for invalid app key', async () => {

View File

@@ -22,7 +22,7 @@ describe('GET /api/v1/apps', () => {
.expect(200);
const expectedPayload = getAppsMock(apps);
expect(response.body).toEqual(expectedPayload);
expect(response.body).toStrictEqual(expectedPayload);
});
it('should return all apps filtered by name', async () => {
@@ -34,7 +34,7 @@ describe('GET /api/v1/apps', () => {
.expect(200);
const expectedPayload = getAppsMock(appsWithNameGit);
expect(response.body).toEqual(expectedPayload);
expect(response.body).toStrictEqual(expectedPayload);
});
it('should return only the apps with triggers', async () => {
@@ -46,7 +46,7 @@ describe('GET /api/v1/apps', () => {
.expect(200);
const expectedPayload = getAppsMock(appsWithTriggers);
expect(response.body).toEqual(expectedPayload);
expect(response.body).toStrictEqual(expectedPayload);
});
it('should return only the apps with actions', async () => {
@@ -58,6 +58,6 @@ describe('GET /api/v1/apps', () => {
.expect(200);
const expectedPayload = getAppsMock(appsWithActions);
expect(response.body).toEqual(expectedPayload);
expect(response.body).toStrictEqual(expectedPayload);
});
});

View File

@@ -29,7 +29,7 @@ describe('GET /api/v1/apps/:appKey/auth-clients/:appAuthClientId', () => {
.expect(200);
const expectedPayload = getAppAuthClientMock(currentAppAuthClient);
expect(response.body).toEqual(expectedPayload);
expect(response.body).toStrictEqual(expectedPayload);
});
it('should return not found response for not existing app auth client ID', async () => {

View File

@@ -37,6 +37,6 @@ describe('GET /api/v1/apps/:appKey/auth-clients', () => {
appAuthClientOne,
]);
expect(response.body).toEqual(expectedPayload);
expect(response.body).toStrictEqual(expectedPayload);
});
});

View File

@@ -23,7 +23,7 @@ describe('GET /api/v1/apps/:appKey/auth', () => {
.expect(200);
const expectedPayload = getAuthMock(exampleApp.auth);
expect(response.body).toEqual(expectedPayload);
expect(response.body).toStrictEqual(expectedPayload);
});
it('should return not found response for invalid app key', async () => {

View File

@@ -17,7 +17,7 @@ describe('GET /api/v1/apps/:appKey/config', () => {
appConfig = await createAppConfig({
key: 'deepl',
allowCustomConnection: true,
customConnectionAllowed: true,
shared: true,
disabled: false,
});
@@ -32,7 +32,7 @@ describe('GET /api/v1/apps/:appKey/config', () => {
.expect(200);
const expectedPayload = getAppConfigMock(appConfig);
expect(response.body).toEqual(expectedPayload);
expect(response.body).toStrictEqual(expectedPayload);
});
it('should return not found response for not existing app key', async () => {

View File

@@ -47,7 +47,7 @@ describe('GET /api/v1/apps/:appKey/connections', () => {
currentUserConnectionOne,
]);
expect(response.body).toEqual(expectedPayload);
expect(response.body).toStrictEqual(expectedPayload);
});
it('should return the connections data of specified app for another user', async () => {
@@ -82,7 +82,7 @@ describe('GET /api/v1/apps/:appKey/connections', () => {
anotherUserConnectionOne,
]);
expect(response.body).toEqual(expectedPayload);
expect(response.body).toStrictEqual(expectedPayload);
});
it('should return not found response for invalid connection UUID', async () => {

View File

@@ -62,7 +62,7 @@ describe('GET /api/v1/apps/:appKey/flows', () => {
[triggerStepFlowOne, actionStepFlowOne]
);
expect(response.body).toEqual(expectedPayload);
expect(response.body).toStrictEqual(expectedPayload);
});
it('should return the flows data of specified app for another user', async () => {
@@ -110,7 +110,7 @@ describe('GET /api/v1/apps/:appKey/flows', () => {
[triggerStepFlowOne, actionStepFlowOne]
);
expect(response.body).toEqual(expectedPayload);
expect(response.body).toStrictEqual(expectedPayload);
});
it('should return not found response for invalid app key', async () => {

View File

@@ -29,7 +29,7 @@ describe('GET /api/v1/apps/:appKey/triggers/:triggerKey/substeps', () => {
.expect(200);
const expectedPayload = getTriggerSubstepsMock(exampleTrigger.substeps);
expect(response.body).toEqual(expectedPayload);
expect(response.body).toStrictEqual(expectedPayload);
});
it('should return not found response for invalid app key', async () => {
@@ -47,6 +47,6 @@ describe('GET /api/v1/apps/:appKey/triggers/:triggerKey/substeps', () => {
.set('Authorization', token)
.expect(200);
expect(response.body.data).toEqual([]);
expect(response.body.data).toStrictEqual([]);
});
});

View File

@@ -23,7 +23,7 @@ describe('GET /api/v1/apps/:appKey/triggers', () => {
.expect(200);
const expectedPayload = getTriggersMock(exampleApp.triggers);
expect(response.body).toEqual(expectedPayload);
expect(expectedPayload).toMatchObject(response.body);
});
it('should return not found response for invalid app key', async () => {

View File

@@ -20,6 +20,6 @@ describe('GET /api/v1/automatisch/info', () => {
const expectedPayload = infoMock();
expect(response.body).toEqual(expectedPayload);
expect(response.body).toStrictEqual(expectedPayload);
});
});

View File

@@ -18,6 +18,6 @@ describe('GET /api/v1/automatisch/license', () => {
const expectedPayload = licenseMock();
expect(response.body).toEqual(expectedPayload);
expect(response.body).toStrictEqual(expectedPayload);
});
});

View File

@@ -10,7 +10,7 @@ describe('GET /api/v1/automatisch/version', () => {
const expectedPayload = {
data: {
version: '0.13.1',
version: '0.14.0',
},
meta: {
count: 1,
@@ -21,6 +21,6 @@ describe('GET /api/v1/automatisch/version', () => {
},
};
expect(response.body).toEqual(expectedPayload);
expect(response.body).toStrictEqual(expectedPayload);
});
});

View File

@@ -69,7 +69,7 @@ describe('GET /api/v1/connections/:connectionId/flows', () => {
[triggerStepFlowOne, actionStepFlowOne]
);
expect(response.body).toEqual(expectedPayload);
expect(response.body).toStrictEqual(expectedPayload);
});
it('should return the flows data of specified connection for another user', async () => {
@@ -123,6 +123,6 @@ describe('GET /api/v1/connections/:connectionId/flows', () => {
[triggerStepFlowOne, actionStepFlowOne]
);
expect(response.body).toEqual(expectedPayload);
expect(response.body).toStrictEqual(expectedPayload);
});
});

View File

@@ -43,7 +43,7 @@ describe('POST /api/v1/connections/:connectionId/test', () => {
.set('Authorization', token)
.expect(200);
expect(response.body.data.verified).toEqual(false);
expect(response.body.data.verified).toStrictEqual(false);
});
it('should update the connection as not verified for another user', async () => {
@@ -74,7 +74,7 @@ describe('POST /api/v1/connections/:connectionId/test', () => {
.set('Authorization', token)
.expect(200);
expect(response.body.data.verified).toEqual(false);
expect(response.body.data.verified).toStrictEqual(false);
});
it('should return not found response for not existing connection UUID', async () => {

View File

@@ -8,7 +8,7 @@ export default async (request, response) => {
})
.throwIfNotFound();
connection = await connection.update(connectionParams(request));
connection = await connection.updateFormattedData(connectionParams(request));
renderObject(response, connection);
};

View File

@@ -47,7 +47,7 @@ describe('POST /api/v1/connections/:connectionId/verify', () => {
.set('Authorization', token)
.expect(200);
expect(response.body.data.verified).toEqual(true);
expect(response.body.data.verified).toStrictEqual(true);
});
it('should return not found response for not existing connection UUID', async () => {

View File

@@ -69,7 +69,7 @@ describe('GET /api/v1/executions/:executionId/execution-steps', () => {
[stepOne, stepTwo]
);
expect(response.body).toEqual(expectedPayload);
expect(response.body).toStrictEqual(expectedPayload);
});
it('should return the execution steps of another user execution', async () => {
@@ -118,7 +118,7 @@ describe('GET /api/v1/executions/:executionId/execution-steps', () => {
[stepOne, stepTwo]
);
expect(response.body).toEqual(expectedPayload);
expect(response.body).toStrictEqual(expectedPayload);
});
it('should return not found response for not existing execution step UUID', async () => {

View File

@@ -57,7 +57,7 @@ describe('GET /api/v1/executions/:executionId', () => {
[stepOne, stepTwo]
);
expect(response.body).toEqual(expectedPayload);
expect(response.body).toStrictEqual(expectedPayload);
});
it('should return the execution data of another user', async () => {
@@ -99,7 +99,7 @@ describe('GET /api/v1/executions/:executionId', () => {
[stepOne, stepTwo]
);
expect(response.body).toEqual(expectedPayload);
expect(response.body).toStrictEqual(expectedPayload);
});
it('should return not found response for not existing execution UUID', async () => {

View File

@@ -66,7 +66,7 @@ describe('GET /api/v1/executions', () => {
[stepOne, stepTwo]
);
expect(response.body).toEqual(expectedPayload);
expect(response.body).toStrictEqual(expectedPayload);
});
it('should return the executions of another user', async () => {
@@ -114,6 +114,6 @@ describe('GET /api/v1/executions', () => {
[stepOne, stepTwo]
);
expect(response.body).toEqual(expectedPayload);
expect(response.body).toStrictEqual(expectedPayload);
});
});

View File

@@ -1,11 +1,11 @@
import { renderObject } from '../../../../helpers/renderer.js';
export default async (request, response) => {
let flow = await request.currentUser.$relatedQuery('flows').insert({
const flow = await request.currentUser.$relatedQuery('flows').insertAndFetch({
name: 'Name your flow',
});
flow = await flow.createInitialSteps();
await flow.createInitialSteps();
renderObject(response, flow, { status: 201 });
};

View File

@@ -6,7 +6,7 @@ export default async (request, response) => {
.findById(request.params.flowId)
.throwIfNotFound();
const createdActionStep = await flow.createActionStep(
const createdActionStep = await flow.createStepAfter(
request.body.previousStepId
);

View File

@@ -41,7 +41,7 @@ describe('GET /api/v1/flows/:flowId', () => {
actionStep,
]);
expect(response.body).toEqual(expectedPayload);
expect(response.body).toStrictEqual(expectedPayload);
});
it('should return the flow data of another user', async () => {
@@ -67,7 +67,7 @@ describe('GET /api/v1/flows/:flowId', () => {
actionStep,
]);
expect(response.body).toEqual(expectedPayload);
expect(response.body).toStrictEqual(expectedPayload);
});
it('should return not found response for not existing flow UUID', async () => {

View File

@@ -63,7 +63,7 @@ describe('GET /api/v1/flows', () => {
]
);
expect(response.body).toEqual(expectedPayload);
expect(response.body).toStrictEqual(expectedPayload);
});
it('should return the flows data of another user', async () => {
@@ -113,6 +113,6 @@ describe('GET /api/v1/flows', () => {
]
);
expect(response.body).toEqual(expectedPayload);
expect(response.body).toStrictEqual(expectedPayload);
});
});

View File

@@ -53,7 +53,7 @@ describe('POST /api/v1/installation/users', () => {
const usersCountAfter = await User.query().resultSize();
expect(usersCountBefore).toEqual(usersCountAfter);
expect(usersCountBefore).toStrictEqual(usersCountAfter);
});
});

View File

@@ -28,6 +28,6 @@ describe('GET /api/v1/payment/paddle-info', () => {
const expectedResponsePayload = await getPaddleInfoMock();
expect(response.body).toEqual(expectedResponsePayload);
expect(response.body).toStrictEqual(expectedResponsePayload);
});
});

View File

@@ -24,6 +24,6 @@ describe('GET /api/v1/payment/plans', () => {
const expectedResponsePayload = await getPaymentPlansMock();
expect(response.body).toEqual(expectedResponsePayload);
expect(response.body).toStrictEqual(expectedResponsePayload);
});
});

View File

@@ -25,6 +25,6 @@ describe('GET /api/v1/saml-auth-providers', () => {
samlAuthProviderOne,
]);
expect(response.body).toEqual(expectedPayload);
expect(response.body).toStrictEqual(expectedPayload);
});
});

View File

@@ -78,7 +78,7 @@ describe('POST /api/v1/steps/:stepId/dynamic-data', () => {
})
.expect(200);
expect(response.body.data).toEqual(repositories);
expect(response.body.data).toStrictEqual(repositories);
});
it('of the another users step', async () => {
@@ -117,7 +117,7 @@ describe('POST /api/v1/steps/:stepId/dynamic-data', () => {
})
.expect(200);
expect(response.body.data).toEqual(repositories);
expect(response.body.data).toStrictEqual(repositories);
});
});
@@ -171,7 +171,7 @@ describe('POST /api/v1/steps/:stepId/dynamic-data', () => {
})
.expect(422);
expect(response.body.errors).toEqual(errors);
expect(response.body.errors).toStrictEqual(errors);
});
});

View File

@@ -56,7 +56,7 @@ describe('POST /api/v1/steps/:stepId/dynamic-fields', () => {
const expectedPayload = await createDynamicFieldsMock();
expect(response.body).toEqual(expectedPayload);
expect(response.body).toStrictEqual(expectedPayload);
});
it('should return dynamically created fields of the another users step', async () => {
@@ -97,7 +97,7 @@ describe('POST /api/v1/steps/:stepId/dynamic-fields', () => {
const expectedPayload = await createDynamicFieldsMock();
expect(response.body).toEqual(expectedPayload);
expect(response.body).toStrictEqual(expectedPayload);
});
it('should return not found response for not existing step UUID', async () => {

View File

@@ -43,7 +43,7 @@ describe('GET /api/v1/steps/:stepId/connection', () => {
const expectedPayload = await getConnectionMock(currentUserConnection);
expect(response.body).toEqual(expectedPayload);
expect(response.body).toStrictEqual(expectedPayload);
});
it('should return the current user connection data of specified step', async () => {
@@ -70,7 +70,7 @@ describe('GET /api/v1/steps/:stepId/connection', () => {
const expectedPayload = await getConnectionMock(anotherUserConnection);
expect(response.body).toEqual(expectedPayload);
expect(response.body).toStrictEqual(expectedPayload);
});
it('should return not found response for not existing step without connection', async () => {

View File

@@ -70,7 +70,7 @@ describe('GET /api/v1/steps/:stepId/previous-steps', () => {
[executionStepOne, executionStepTwo]
);
expect(response.body).toEqual(expectedPayload);
expect(response.body).toStrictEqual(expectedPayload);
});
it('should return the previous steps of the specified step of another user', async () => {
@@ -124,7 +124,7 @@ describe('GET /api/v1/steps/:stepId/previous-steps', () => {
[executionStepOne, executionStepTwo]
);
expect(response.body).toEqual(expectedPayload);
expect(response.body).toStrictEqual(expectedPayload);
});
it('should return not found response for not existing step UUID', async () => {

View File

@@ -79,7 +79,7 @@ describe('GET /api/v1/users/:userId/apps', () => {
.expect(200);
const expectedPayload = getAppsMock();
expect(response.body).toEqual(expectedPayload);
expect(response.body).toStrictEqual(expectedPayload);
});
it('should return all apps of the another user', async () => {
@@ -143,7 +143,7 @@ describe('GET /api/v1/users/:userId/apps', () => {
.expect(200);
const expectedPayload = getAppsMock();
expect(response.body).toEqual(expectedPayload);
expect(response.body).toStrictEqual(expectedPayload);
});
it('should return specified app of the current user', async () => {
@@ -204,7 +204,7 @@ describe('GET /api/v1/users/:userId/apps', () => {
.set('Authorization', token)
.expect(200);
expect(response.body.data.length).toEqual(1);
expect(response.body.data[0].key).toEqual('deepl');
expect(response.body.data.length).toStrictEqual(1);
expect(response.body.data[0].key).toStrictEqual('deepl');
});
});

View File

@@ -39,6 +39,6 @@ describe('GET /api/v1/users/me', () => {
permissionTwo,
]);
expect(response.body).toEqual(expectedPayload);
expect(response.body).toStrictEqual(expectedPayload);
});
});

View File

@@ -29,6 +29,6 @@ describe('GET /api/v1/user/invoices', () => {
const expectedPayload = await getInvoicesMock(invoices);
expect(response.body).toEqual(expectedPayload);
expect(response.body).toStrictEqual(expectedPayload);
});
});

View File

@@ -36,7 +36,7 @@ describe('GET /api/v1/users/:userId/plan-and-usage', () => {
},
};
expect(response.body.data).toEqual(expectedResponseData);
expect(response.body.data).toStrictEqual(expectedResponseData);
});
it('should return current plan and usage data', async () => {
@@ -63,6 +63,6 @@ describe('GET /api/v1/users/:userId/plan-and-usage', () => {
},
};
expect(response.body.data).toEqual(expectedResponseData);
expect(response.body.data).toStrictEqual(expectedResponseData);
});
});

View File

@@ -33,7 +33,7 @@ describe('GET /api/v1/users/:userId/subscription', () => {
const expectedPayload = getSubscriptionMock(subscription);
expect(response.body).toEqual(expectedPayload);
expect(response.body).toStrictEqual(expectedPayload);
});
it('should return not found response if there is no current subscription', async () => {

View File

@@ -32,7 +32,7 @@ describe('GET /api/v1/users/:userId/trial', () => {
.expect(200);
const expectedResponsePayload = await getUserTrialMock(user);
expect(response.body).toEqual(expectedResponsePayload);
expect(response.body).toStrictEqual(expectedResponsePayload);
});
});
});

View File

@@ -43,7 +43,7 @@ describe('PATCH /api/v1/users/:userId/password', () => {
.send(userData)
.expect(422);
expect(response.body.meta.type).toEqual('ValidationError');
expect(response.body.meta.type).toStrictEqual('ValidationError');
expect(response.body.errors).toMatchObject({
currentPassword: ['is incorrect.'],
});

View File

@@ -47,7 +47,8 @@ describe('PATCH /api/v1/users/:userId', () => {
.send(userData)
.expect(422);
expect(response.body.meta.type).toEqual('ModelValidation');
expect(response.body.meta.type).toStrictEqual('ModelValidation');
expect(response.body.errors).toMatchObject({
email: ['must be string'],
fullName: ['must be string'],

View File

@@ -0,0 +1,37 @@
export async function up(knex) {
await knex.schema.alterTable('app_configs', (table) => {
table.boolean('connection_allowed').defaultTo(false);
});
const appConfigs = await knex('app_configs').select('*');
for (const appConfig of appConfigs) {
const appAuthClients = await knex('app_auth_clients').where(
'app_key',
appConfig.key
);
const hasSomeActiveAppAuthClients = !!appAuthClients?.some(
(appAuthClient) => appAuthClient.active
);
const shared = appConfig.shared;
const active = appConfig.disabled === false;
const connectionAllowedConditions = [
hasSomeActiveAppAuthClients,
shared,
active,
];
const connectionAllowed = connectionAllowedConditions.every(Boolean);
await knex('app_configs')
.where('id', appConfig.id)
.update({ connection_allowed: connectionAllowed });
}
}
export async function down(knex) {
await knex.schema.alterTable('app_configs', (table) => {
table.dropColumn('connection_allowed');
});
}

View File

@@ -0,0 +1,11 @@
export async function up(knex) {
return knex.schema.alterTable('app_configs', (table) => {
table.renameColumn('allow_custom_connection', 'custom_connection_allowed');
});
}
export async function down(knex) {
return knex.schema.alterTable('app_configs', (table) => {
table.renameColumn('custom_connection_allowed', 'allow_custom_connection');
});
}

View File

@@ -0,0 +1,13 @@
export async function up(knex) {
return knex.schema.alterTable('app_configs', function (table) {
table.dropPrimary();
table.primary('key');
});
}
export async function down(knex) {
return knex.schema.alterTable('app_configs', function (table) {
table.dropPrimary();
table.primary('id');
});
}

View File

@@ -0,0 +1,11 @@
export async function up(knex) {
return knex.schema.alterTable('app_configs', function (table) {
table.dropColumn('id');
});
}
export async function down(knex) {
return knex.schema.alterTable('app_configs', function (table) {
table.uuid('id').defaultTo(knex.raw('gen_random_uuid()'));
});
}

View File

@@ -0,0 +1,41 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`AppConfig model > jsonSchema should have correct validations 1`] = `
{
"properties": {
"connectionAllowed": {
"default": false,
"type": "boolean",
},
"createdAt": {
"type": "string",
},
"customConnectionAllowed": {
"default": false,
"type": "boolean",
},
"disabled": {
"default": false,
"type": "boolean",
},
"id": {
"format": "uuid",
"type": "string",
},
"key": {
"type": "string",
},
"shared": {
"default": false,
"type": "boolean",
},
"updatedAt": {
"type": "string",
},
},
"required": [
"key",
],
"type": "object",
}
`;

View File

@@ -0,0 +1,42 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`Flow model > jsonSchema should have correct validations 1`] = `
{
"properties": {
"active": {
"type": "boolean",
},
"createdAt": {
"type": "string",
},
"deletedAt": {
"type": "string",
},
"id": {
"format": "uuid",
"type": "string",
},
"name": {
"minLength": 1,
"type": "string",
},
"publishedAt": {
"type": "string",
},
"remoteWebhookId": {
"type": "string",
},
"updatedAt": {
"type": "string",
},
"userId": {
"format": "uuid",
"type": "string",
},
},
"required": [
"name",
],
"type": "object",
}
`;

View File

@@ -0,0 +1,42 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`Permission model > jsonSchema should have correct validations 1`] = `
{
"properties": {
"action": {
"minLength": 1,
"type": "string",
},
"conditions": {
"items": {
"type": "string",
},
"type": "array",
},
"createdAt": {
"type": "string",
},
"id": {
"format": "uuid",
"type": "string",
},
"roleId": {
"format": "uuid",
"type": "string",
},
"subject": {
"minLength": 1,
"type": "string",
},
"updatedAt": {
"type": "string",
},
},
"required": [
"roleId",
"action",
"subject",
],
"type": "object",
}
`;

View File

@@ -0,0 +1,33 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`Role model > jsonSchema should have correct validations 1`] = `
{
"properties": {
"createdAt": {
"type": "string",
},
"description": {
"maxLength": 255,
"type": [
"string",
"null",
],
},
"id": {
"format": "uuid",
"type": "string",
},
"name": {
"minLength": 1,
"type": "string",
},
"updatedAt": {
"type": "string",
},
},
"required": [
"name",
],
"type": "object",
}
`;

View File

@@ -0,0 +1,72 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`SamlAuthProvider model > jsonSchema should have the correct schema 1`] = `
{
"properties": {
"active": {
"type": "boolean",
},
"certificate": {
"minLength": 1,
"type": "string",
},
"defaultRoleId": {
"format": "uuid",
"type": "string",
},
"emailAttributeName": {
"minLength": 1,
"type": "string",
},
"entryPoint": {
"minLength": 1,
"type": "string",
},
"firstnameAttributeName": {
"minLength": 1,
"type": "string",
},
"id": {
"format": "uuid",
"type": "string",
},
"issuer": {
"minLength": 1,
"type": "string",
},
"name": {
"minLength": 1,
"type": "string",
},
"roleAttributeName": {
"minLength": 1,
"type": "string",
},
"signatureAlgorithm": {
"enum": [
"sha1",
"sha256",
"sha512",
],
"type": "string",
},
"surnameAttributeName": {
"minLength": 1,
"type": "string",
},
},
"required": [
"name",
"certificate",
"signatureAlgorithm",
"entryPoint",
"issuer",
"firstnameAttributeName",
"surnameAttributeName",
"emailAttributeName",
"roleAttributeName",
"defaultRoleId",
],
"type": "object",
}
`;

View File

@@ -28,14 +28,3 @@ exports[`SamlAuthProvidersRoleMapping model > jsonSchema should have the correct
"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,77 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`Step model > jsonSchema should have correct validations 1`] = `
{
"properties": {
"appKey": {
"maxLength": 255,
"minLength": 1,
"type": [
"string",
"null",
],
},
"connectionId": {
"format": "uuid",
"type": [
"string",
"null",
],
},
"createdAt": {
"type": "string",
},
"deletedAt": {
"type": "string",
},
"flowId": {
"format": "uuid",
"type": "string",
},
"id": {
"format": "uuid",
"type": "string",
},
"key": {
"type": [
"string",
"null",
],
},
"parameters": {
"type": "object",
},
"position": {
"type": "integer",
},
"status": {
"default": "incomplete",
"enum": [
"incomplete",
"completed",
],
"type": "string",
},
"type": {
"enum": [
"action",
"trigger",
],
"type": "string",
},
"updatedAt": {
"type": "string",
},
"webhookPath": {
"type": [
"string",
"null",
],
},
},
"required": [
"type",
],
"type": "object",
}
`;

View File

@@ -0,0 +1,81 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`User model > jsonSchema should have correct validations 1`] = `
{
"properties": {
"createdAt": {
"type": "string",
},
"deletedAt": {
"type": "string",
},
"email": {
"format": "email",
"maxLength": 255,
"minLength": 1,
"type": "string",
},
"fullName": {
"minLength": 1,
"type": "string",
},
"id": {
"format": "uuid",
"type": "string",
},
"invitationToken": {
"type": [
"string",
"null",
],
},
"invitationTokenSentAt": {
"format": "date-time",
"type": [
"string",
"null",
],
},
"password": {
"minLength": 6,
"type": "string",
},
"resetPasswordToken": {
"type": [
"string",
"null",
],
},
"resetPasswordTokenSentAt": {
"format": "date-time",
"type": [
"string",
"null",
],
},
"roleId": {
"format": "uuid",
"type": "string",
},
"status": {
"default": "active",
"enum": [
"active",
"invited",
],
"type": "string",
},
"trialExpiryDate": {
"type": "string",
},
"updatedAt": {
"type": "string",
},
},
"required": [
"fullName",
"email",
],
"type": "object",
}
`;

View File

@@ -2,6 +2,7 @@ import AES from 'crypto-js/aes.js';
import enc from 'crypto-js/enc-utf8.js';
import appConfig from '../config/app.js';
import Base from './base.js';
import AppConfig from './app-config.js';
class AppAuthClient extends Base {
static tableName = 'app_auth_clients';
@@ -21,6 +22,17 @@ class AppAuthClient extends Base {
},
};
static relationMappings = () => ({
appConfig: {
relation: Base.BelongsToOneRelation,
modelClass: AppConfig,
join: {
from: 'app_auth_clients.app_key',
to: 'app_configs.key',
},
},
});
encryptData() {
if (!this.eligibleForEncryption()) return;
@@ -48,6 +60,17 @@ class AppAuthClient extends Base {
return this.authDefaults ? true : false;
}
async triggerAppConfigUpdate() {
const appConfig = await this.$relatedQuery('appConfig');
// This is a workaround to update connection allowed column for AppConfig
await appConfig?.$query().patch({
key: appConfig.key,
shared: appConfig.shared,
disabled: appConfig.disabled,
});
}
// TODO: Make another abstraction like beforeSave instead of using
// beforeInsert and beforeUpdate separately for the same operation.
async $beforeInsert(queryContext) {
@@ -55,11 +78,23 @@ class AppAuthClient extends Base {
this.encryptData();
}
async $afterInsert(queryContext) {
await super.$afterInsert(queryContext);
await this.triggerAppConfigUpdate();
}
async $beforeUpdate(opt, queryContext) {
await super.$beforeUpdate(opt, queryContext);
this.encryptData();
}
async $afterUpdate(opt, queryContext) {
await super.$afterUpdate(opt, queryContext);
await this.triggerAppConfigUpdate();
}
async $afterFind() {
this.decryptData();
}

View File

@@ -2,9 +2,12 @@ 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 './app-config.js';
import AppAuthClient from './app-auth-client.js';
import Base from './base.js';
import appConfig from '../config/app.js';
import { createAppAuthClient } from '../../test/factories/app-auth-client.js';
import { createAppConfig } from '../../test/factories/app-config.js';
describe('AppAuthClient model', () => {
it('tableName should return correct name', () => {
@@ -15,6 +18,23 @@ describe('AppAuthClient model', () => {
expect(AppAuthClient.jsonSchema).toMatchSnapshot();
});
it('relationMappings should return correct associations', () => {
const relationMappings = AppAuthClient.relationMappings();
const expectedRelations = {
appConfig: {
relation: Base.BelongsToOneRelation,
modelClass: AppConfig,
join: {
from: 'app_auth_clients.app_key',
to: 'app_configs.key',
},
},
};
expect(relationMappings).toStrictEqual(expectedRelations);
});
describe('encryptData', () => {
it('should return undefined if eligibleForEncryption is not true', async () => {
vi.spyOn(
@@ -49,7 +69,9 @@ describe('AppAuthClient model', () => {
);
expect(formattedAuthDefaults).toStrictEqual(expectedDecryptedValue);
expect(appAuthClient.authDefaults).not.toEqual(formattedAuthDefaults);
expect(appAuthClient.authDefaults).not.toStrictEqual(
formattedAuthDefaults
);
});
it('should encrypt formattedAuthDefaults and remove formattedAuthDefaults', async () => {
@@ -104,7 +126,9 @@ describe('AppAuthClient model', () => {
expect(appAuthClient.formattedAuthDefaults).toStrictEqual(
formattedAuthDefaults
);
expect(appAuthClient.authDefaults).not.toEqual(formattedAuthDefaults);
expect(appAuthClient.authDefaults).not.toStrictEqual(
formattedAuthDefaults
);
});
});
@@ -140,6 +164,63 @@ describe('AppAuthClient model', () => {
});
});
describe('triggerAppConfigUpdate', () => {
it('should trigger an update in related app config', async () => {
await createAppConfig({ key: 'gitlab' });
const appAuthClient = await createAppAuthClient({
appKey: 'gitlab',
});
const appConfigBeforeUpdateSpy = vi.spyOn(
AppConfig.prototype,
'$beforeUpdate'
);
await appAuthClient.triggerAppConfigUpdate();
expect(appConfigBeforeUpdateSpy).toHaveBeenCalledOnce();
});
it('should update related AppConfig after creating an instance', async () => {
const appConfig = await createAppConfig({
key: 'gitlab',
disabled: false,
shared: true,
});
await createAppAuthClient({
appKey: 'gitlab',
active: true,
});
const refetchedAppConfig = await appConfig.$query();
expect(refetchedAppConfig.connectionAllowed).toBe(true);
});
it('should update related AppConfig after updating an instance', async () => {
const appConfig = await createAppConfig({
key: 'gitlab',
disabled: false,
shared: true,
});
const appAuthClient = await createAppAuthClient({
appKey: 'gitlab',
active: false,
});
let refetchedAppConfig = await appConfig.$query();
expect(refetchedAppConfig.connectionAllowed).toBe(false);
await appAuthClient.$query().patchAndFetch({ active: true });
refetchedAppConfig = await appConfig.$query();
expect(refetchedAppConfig.connectionAllowed).toBe(true);
});
});
it('$beforeInsert should call AppAuthClient.encryptData', async () => {
const appAuthClientBeforeInsertSpy = vi.spyOn(
AppAuthClient.prototype,
@@ -151,6 +232,17 @@ describe('AppAuthClient model', () => {
expect(appAuthClientBeforeInsertSpy).toHaveBeenCalledOnce();
});
it('$afterInsert should call AppAuthClient.triggerAppConfigUpdate', async () => {
const appAuthClientAfterInsertSpy = vi.spyOn(
AppAuthClient.prototype,
'triggerAppConfigUpdate'
);
await createAppAuthClient();
expect(appAuthClientAfterInsertSpy).toHaveBeenCalledOnce();
});
it('$beforeUpdate should call AppAuthClient.encryptData', async () => {
const appAuthClient = await createAppAuthClient();
@@ -164,6 +256,19 @@ describe('AppAuthClient model', () => {
expect(appAuthClientBeforeUpdateSpy).toHaveBeenCalledOnce();
});
it('$afterUpdate should call AppAuthClient.triggerAppConfigUpdate', async () => {
const appAuthClient = await createAppAuthClient();
const appAuthClientAfterUpdateSpy = vi.spyOn(
AppAuthClient.prototype,
'triggerAppConfigUpdate'
);
await appAuthClient.$query().patchAndFetch({ name: 'sample' });
expect(appAuthClientAfterUpdateSpy).toHaveBeenCalledOnce();
});
it('$afterFind should call AppAuthClient.decryptData', async () => {
const appAuthClient = await createAppAuthClient();

View File

@@ -5,6 +5,10 @@ import Base from './base.js';
class AppConfig extends Base {
static tableName = 'app_configs';
static get idColumn() {
return 'key';
}
static jsonSchema = {
type: 'object',
required: ['key'],
@@ -12,7 +16,8 @@ class AppConfig extends Base {
properties: {
id: { type: 'string', format: 'uuid' },
key: { type: 'string' },
allowCustomConnection: { type: 'boolean', default: false },
connectionAllowed: { type: 'boolean', default: false },
customConnectionAllowed: { type: 'boolean', default: false },
shared: { type: 'boolean', default: false },
disabled: { type: 'boolean', default: false },
createdAt: { type: 'string' },
@@ -31,31 +36,44 @@ class AppConfig extends Base {
},
});
static get virtualAttributes() {
return ['canConnect', 'canCustomConnect'];
}
get canCustomConnect() {
return !this.disabled && this.allowCustomConnection;
}
get canConnect() {
const hasSomeActiveAppAuthClients = !!this.appAuthClients?.some(
(appAuthClient) => appAuthClient.active
);
const shared = this.shared;
const active = this.disabled === false;
const conditions = [hasSomeActiveAppAuthClients, shared, active];
return conditions.every(Boolean);
}
async getApp() {
if (!this.key) return null;
return await App.findOneByKey(this.key);
}
async computeAndAssignConnectionAllowedProperty() {
this.connectionAllowed = await this.computeConnectionAllowedProperty();
}
async computeConnectionAllowedProperty() {
const appAuthClients = await this.$relatedQuery('appAuthClients');
const hasSomeActiveAppAuthClients =
appAuthClients?.some((appAuthClient) => appAuthClient.active) || false;
const conditions = [
hasSomeActiveAppAuthClients,
this.shared,
!this.disabled,
];
const connectionAllowed = conditions.every(Boolean);
return connectionAllowed;
}
async $beforeInsert(queryContext) {
await super.$beforeInsert(queryContext);
await this.computeAndAssignConnectionAllowedProperty();
}
async $beforeUpdate(opt, queryContext) {
await super.$beforeUpdate(opt, queryContext);
await this.computeAndAssignConnectionAllowedProperty();
}
}
export default AppConfig;

View File

@@ -0,0 +1,180 @@
import { vi, describe, it, expect } from 'vitest';
import Base from './base.js';
import AppConfig from './app-config.js';
import App from './app.js';
import AppAuthClient from './app-auth-client.js';
import { createAppConfig } from '../../test/factories/app-config.js';
import { createAppAuthClient } from '../../test/factories/app-auth-client.js';
describe('AppConfig model', () => {
it('tableName should return correct name', () => {
expect(AppConfig.tableName).toBe('app_configs');
});
it('idColumn should return key field', () => {
expect(AppConfig.idColumn).toBe('key');
});
it('jsonSchema should have correct validations', () => {
expect(AppConfig.jsonSchema).toMatchSnapshot();
});
it('relationMappings should return correct associations', () => {
const relationMappings = AppConfig.relationMappings();
const expectedRelations = {
appAuthClients: {
relation: Base.HasManyRelation,
modelClass: AppAuthClient,
join: {
from: 'app_configs.key',
to: 'app_auth_clients.app_key',
},
},
};
expect(relationMappings).toStrictEqual(expectedRelations);
});
describe('getApp', () => {
it('getApp should return null if there is no key', async () => {
const appConfig = new AppConfig();
const app = await appConfig.getApp();
expect(app).toBeNull();
});
it('getApp should return app with provided key', async () => {
const appConfig = new AppConfig();
appConfig.key = 'deepl';
const app = await appConfig.getApp();
const expectedApp = await App.findOneByKey(appConfig.key);
expect(app).toStrictEqual(expectedApp);
});
});
describe('computeAndAssignConnectionAllowedProperty', () => {
it('should call computeConnectionAllowedProperty and assign the result', async () => {
const appConfig = await createAppConfig();
const computeConnectionAllowedPropertySpy = vi
.spyOn(appConfig, 'computeConnectionAllowedProperty')
.mockResolvedValue(true);
await appConfig.computeAndAssignConnectionAllowedProperty();
expect(computeConnectionAllowedPropertySpy).toHaveBeenCalled();
expect(appConfig.connectionAllowed).toBe(true);
});
});
describe('computeConnectionAllowedProperty', () => {
it('should return true when app is enabled, shared and allows custom connection with an active app auth client', async () => {
await createAppAuthClient({
appKey: 'deepl',
active: true,
});
await createAppAuthClient({
appKey: 'deepl',
active: false,
});
const appConfig = await createAppConfig({
disabled: false,
customConnectionAllowed: true,
shared: true,
key: 'deepl',
});
const connectionAllowed =
await appConfig.computeConnectionAllowedProperty();
expect(connectionAllowed).toBe(true);
});
it('should return false if there is no active app auth client', async () => {
await createAppAuthClient({
appKey: 'deepl',
active: false,
});
const appConfig = await createAppConfig({
disabled: false,
customConnectionAllowed: true,
shared: true,
key: 'deepl',
});
const connectionAllowed =
await appConfig.computeConnectionAllowedProperty();
expect(connectionAllowed).toBe(false);
});
it('should return false if there is no app auth clients', async () => {
const appConfig = await createAppConfig({
disabled: false,
customConnectionAllowed: true,
shared: true,
key: 'deepl',
});
const connectionAllowed =
await appConfig.computeConnectionAllowedProperty();
expect(connectionAllowed).toBe(false);
});
it('should return false when app is disabled', async () => {
const appConfig = await createAppConfig({
disabled: true,
customConnectionAllowed: true,
});
const connectionAllowed =
await appConfig.computeConnectionAllowedProperty();
expect(connectionAllowed).toBe(false);
});
it(`should return false when app doesn't allow custom connection`, async () => {
const appConfig = await createAppConfig({
disabled: false,
customConnectionAllowed: false,
});
const connectionAllowed =
await appConfig.computeConnectionAllowedProperty();
expect(connectionAllowed).toBe(false);
});
});
it('$beforeInsert should call computeAndAssignConnectionAllowedProperty', async () => {
const computeAndAssignConnectionAllowedPropertySpy = vi
.spyOn(AppConfig.prototype, 'computeAndAssignConnectionAllowedProperty')
.mockResolvedValue(true);
await createAppConfig();
expect(computeAndAssignConnectionAllowedPropertySpy).toHaveBeenCalledOnce();
});
it('$beforeUpdate should call computeAndAssignConnectionAllowedProperty', async () => {
const appConfig = await createAppConfig();
const computeAndAssignConnectionAllowedPropertySpy = vi
.spyOn(AppConfig.prototype, 'computeAndAssignConnectionAllowedProperty')
.mockResolvedValue(true);
await appConfig.$query().patch({
key: 'deepl',
});
expect(computeAndAssignConnectionAllowedPropertySpy).toHaveBeenCalledOnce();
});
});

View File

@@ -89,7 +89,7 @@ class Connection extends Base {
}
if (this.appConfig) {
return !this.appConfig.disabled && this.appConfig.allowCustomConnection;
return !this.appConfig.disabled && this.appConfig.customConnectionAllowed;
}
return true;
@@ -122,10 +122,20 @@ class Connection extends Base {
return this.data ? true : false;
}
async checkEligibilityForCreation() {
const app = await App.findOneByKey(this.key);
async getApp() {
if (!this.key) return null;
const appConfig = await AppConfig.query().findOne({ key: this.key });
return await App.findOneByKey(this.key);
}
async getAppConfig() {
return await AppConfig.query().findOne({ key: this.key });
}
async checkEligibilityForCreation() {
const app = await this.getApp();
const appConfig = await this.getAppConfig();
if (appConfig) {
if (appConfig.disabled) {
@@ -134,7 +144,7 @@ class Connection extends Base {
);
}
if (!appConfig.allowCustomConnection && this.formattedData) {
if (!appConfig.customConnectionAllowed && this.formattedData) {
throw new NotAuthorizedError(
`New custom connections have been disabled for ${app.name}!`
);
@@ -160,12 +170,6 @@ class Connection extends Base {
return this;
}
async getApp() {
if (!this.key) return null;
return await App.findOneByKey(this.key);
}
async testAndUpdateConnection() {
const app = await this.getApp();
const $ = await globalVariable({ connection: this, app });
@@ -224,7 +228,7 @@ class Connection extends Base {
async reset() {
const formattedData = this?.formattedData?.screenName
? { screenName: this.formattedData.screenName }
: null;
: {};
const updatedConnection = await this.$query().patchAndFetch({
formattedData,
@@ -233,7 +237,7 @@ class Connection extends Base {
return updatedConnection;
}
async update({ formattedData, appAuthClientId }) {
async updateFormattedData({ formattedData, appAuthClientId }) {
if (appAuthClientId) {
const appAuthClient = await AppAuthClient.query()
.findById(appAuthClientId)

View File

@@ -3,11 +3,16 @@ import AES from 'crypto-js/aes.js';
import enc from 'crypto-js/enc-utf8.js';
import appConfig from '../config/app.js';
import AppAuthClient from './app-auth-client.js';
import App from './app.js';
import AppConfig from './app-config.js';
import Base from './base.js';
import Connection from './connection';
import Step from './step.js';
import User from './user.js';
import Telemetry from '../helpers/telemetry/index.js';
import { createConnection } from '../../test/factories/connection.js';
import { createAppConfig } from '../../test/factories/app-config.js';
import { createAppAuthClient } from '../../test/factories/app-auth-client.js';
describe('Connection model', () => {
it('tableName should return correct name', () => {
@@ -26,57 +31,138 @@ describe('Connection model', () => {
expect(virtualAttributes).toStrictEqual(expectedAttributes);
});
it('relationMappings should return correct associations', () => {
const relationMappings = Connection.relationMappings();
describe('relationMappings', () => {
it('should return correct associations', () => {
const relationMappings = Connection.relationMappings();
const expectedRelations = {
user: {
relation: Base.BelongsToOneRelation,
modelClass: User,
join: {
from: 'connections.user_id',
to: 'users.id',
const expectedRelations = {
user: {
relation: Base.BelongsToOneRelation,
modelClass: User,
join: {
from: 'connections.user_id',
to: 'users.id',
},
},
},
steps: {
relation: Base.HasManyRelation,
modelClass: Step,
join: {
from: 'connections.id',
to: 'steps.connection_id',
steps: {
relation: Base.HasManyRelation,
modelClass: Step,
join: {
from: 'connections.id',
to: 'steps.connection_id',
},
},
},
triggerSteps: {
relation: Base.HasManyRelation,
modelClass: Step,
join: {
from: 'connections.id',
to: 'steps.connection_id',
triggerSteps: {
relation: Base.HasManyRelation,
modelClass: Step,
join: {
from: 'connections.id',
to: 'steps.connection_id',
},
filter: expect.any(Function),
},
filter: expect.any(Function),
},
appConfig: {
relation: Base.BelongsToOneRelation,
modelClass: AppConfig,
join: {
from: 'connections.key',
to: 'app_configs.key',
appConfig: {
relation: Base.BelongsToOneRelation,
modelClass: AppConfig,
join: {
from: 'connections.key',
to: 'app_configs.key',
},
},
},
appAuthClient: {
relation: Base.BelongsToOneRelation,
modelClass: AppAuthClient,
join: {
from: 'connections.app_auth_client_id',
to: 'app_auth_clients.id',
appAuthClient: {
relation: Base.BelongsToOneRelation,
modelClass: AppAuthClient,
join: {
from: 'connections.app_auth_client_id',
to: 'app_auth_clients.id',
},
},
},
};
};
expect(relationMappings).toStrictEqual(expectedRelations);
expect(relationMappings).toStrictEqual(expectedRelations);
});
it('triggerSteps should return only trigger typed steps', () => {
const relations = Connection.relationMappings();
const whereSpy = vi.fn();
relations.triggerSteps.filter({ where: whereSpy });
expect(whereSpy).toHaveBeenCalledWith('type', '=', 'trigger');
});
});
describe.todo('reconnectable');
describe('reconnectable', () => {
it('should return active status of app auth client when created via app auth client', async () => {
const appAuthClient = await createAppAuthClient({
active: true,
formattedAuthDefaults: {
clientId: 'sample-id',
},
});
const connection = await createConnection({
appAuthClientId: appAuthClient.id,
formattedData: {
token: 'sample-token',
},
});
const connectionWithAppAuthClient = await connection
.$query()
.withGraphFetched({
appAuthClient: true,
});
expect(connectionWithAppAuthClient.reconnectable).toBe(true);
});
it('should return true when app config is not disabled and allows custom connection', async () => {
const appConfig = await createAppConfig({
key: 'gitlab',
disabled: false,
customConnectionAllowed: true,
});
const connection = await createConnection({
key: appConfig.key,
formattedData: {
token: 'sample-token',
},
});
const connectionWithAppAuthClient = await connection
.$query()
.withGraphFetched({
appConfig: true,
});
expect(connectionWithAppAuthClient.reconnectable).toBe(true);
});
it('should return false when app config is disabled or does not allow custom connection', async () => {
const connection = await createConnection({
key: 'gitlab',
formattedData: {
token: 'sample-token',
},
});
await createAppConfig({
key: 'gitlab',
disabled: true,
customConnectionAllowed: false,
});
const connectionWithAppAuthClient = await connection
.$query()
.withGraphFetched({
appConfig: true,
});
expect(connectionWithAppAuthClient.reconnectable).toBe(false);
});
});
describe('encryptData', () => {
it('should return undefined if eligibleForEncryption is not true', async () => {
@@ -107,7 +193,7 @@ describe('Connection model', () => {
);
expect(formattedData).toStrictEqual(expectedDecryptedValue);
expect(connection.data).not.toEqual(formattedData);
expect(connection.data).not.toStrictEqual(formattedData);
});
it('should encrypt formattedData and remove formattedData', async () => {
@@ -157,7 +243,561 @@ describe('Connection model', () => {
connection.decryptData();
expect(connection.formattedData).toStrictEqual(formattedData);
expect(connection.data).not.toEqual(formattedData);
expect(connection.data).not.toStrictEqual(formattedData);
});
});
describe('eligibleForEncryption', () => {
it('should return true when formattedData property exists', async () => {
const connection = new Connection();
connection.formattedData = { clientId: 'sample-id' };
expect(connection.eligibleForEncryption()).toBe(true);
});
it("should return false when formattedData property doesn't exist", async () => {
const connection = new Connection();
connection.formattedData = undefined;
expect(connection.eligibleForEncryption()).toBe(false);
});
});
describe('eligibleForDecryption', () => {
it('should return true when data property exists', async () => {
const connection = new Connection();
connection.data = 'encrypted-data';
expect(connection.eligibleForDecryption()).toBe(true);
});
it("should return false when data property doesn't exist", async () => {
const connection = new Connection();
connection.data = undefined;
expect(connection.eligibleForDecryption()).toBe(false);
});
});
describe('getApp', () => {
it('should return connection app when valid key exists', async () => {
const connection = new Connection();
connection.key = 'gitlab';
const connectionApp = await connection.getApp();
const app = await App.findOneByKey('gitlab');
expect(connectionApp).toStrictEqual(app);
});
it('should throw an error when invalid key exists', async () => {
const connection = new Connection();
connection.key = 'invalid-key';
await expect(() => connection.getApp()).rejects.toThrowError(
`An application with the "invalid-key" key couldn't be found.`
);
});
it('should return null when no key exists', async () => {
const connection = new Connection();
await expect(connection.getApp()).resolves.toBe(null);
});
});
it('getAppConfig should return connection app config', async () => {
const connection = new Connection();
connection.key = 'gitlab';
const appConfig = await createAppConfig({ key: 'gitlab' });
const connectionAppConfig = await connection.getAppConfig();
expect(connectionAppConfig).toStrictEqual(appConfig);
});
describe('checkEligibilityForCreation', () => {
it('should return connection if no app config exists', async () => {
vi.spyOn(Connection.prototype, 'getApp').mockResolvedValue({
name: 'gitlab',
});
vi.spyOn(Connection.prototype, 'getAppConfig').mockResolvedValue();
const connection = new Connection();
expect(await connection.checkEligibilityForCreation()).toBe(connection);
});
it('should throw an error when app does not exist', async () => {
vi.spyOn(Connection.prototype, 'getApp').mockRejectedValue(
new Error(
`An application with the "unexisting-app" key couldn't be found.`
)
);
vi.spyOn(Connection.prototype, 'getAppConfig').mockResolvedValue();
const connection = new Connection();
await expect(() =>
connection.checkEligibilityForCreation()
).rejects.toThrow(
`An application with the "unexisting-app" key couldn't be found.`
);
});
it('should throw an error when app config is disabled', async () => {
vi.spyOn(Connection.prototype, 'getApp').mockResolvedValue({
name: 'gitlab',
});
vi.spyOn(Connection.prototype, 'getAppConfig').mockResolvedValue({
disabled: true,
});
const connection = new Connection();
await expect(() =>
connection.checkEligibilityForCreation()
).rejects.toThrow(
'The application has been disabled for new connections!'
);
});
it('should throw an error when app config does not allow custom connection with formatted data', async () => {
vi.spyOn(Connection.prototype, 'getApp').mockResolvedValue({
name: 'gitlab',
});
vi.spyOn(Connection.prototype, 'getAppConfig').mockResolvedValue({
disabled: false,
customConnectionAllowed: false,
});
const connection = new Connection();
connection.formattedData = {};
await expect(() =>
connection.checkEligibilityForCreation()
).rejects.toThrow(
'New custom connections have been disabled for gitlab!'
);
});
it('should throw an error when app config is not shared with app auth client', async () => {
vi.spyOn(Connection.prototype, 'getApp').mockResolvedValue({
name: 'gitlab',
});
vi.spyOn(Connection.prototype, 'getAppConfig').mockResolvedValue({
disabled: false,
shared: false,
});
const connection = new Connection();
connection.appAuthClientId = 'sample-id';
await expect(() =>
connection.checkEligibilityForCreation()
).rejects.toThrow(
'The connection with the given app auth client is not allowed!'
);
});
it('should apply app auth client auth defaults when creating with shared app auth client', async () => {
await createAppConfig({
key: 'gitlab',
disabled: false,
customConnectionAllowed: true,
shared: true,
});
const appAuthClient = await createAppAuthClient({
appKey: 'gitlab',
active: true,
formattedAuthDefaults: {
clientId: 'sample-id',
},
});
const connection = await createConnection({
key: 'gitlab',
appAuthClientId: appAuthClient.id,
formattedData: null,
});
await connection.checkEligibilityForCreation();
expect(connection.formattedData).toStrictEqual({
clientId: 'sample-id',
});
});
});
describe('testAndUpdateConnection', () => {
it('should verify connection and persist it', async () => {
const connection = await createConnection({ verified: false });
const isStillVerifiedSpy = vi.fn().mockReturnValue(true);
const originalApp = await connection.getApp();
const getAppSpy = vi
.spyOn(connection, 'getApp')
.mockImplementation(() => {
return {
...originalApp,
auth: {
...originalApp.auth,
isStillVerified: isStillVerifiedSpy,
},
};
});
const updatedConnection = await connection.testAndUpdateConnection();
expect(getAppSpy).toHaveBeenCalledOnce();
expect(isStillVerifiedSpy).toHaveBeenCalledOnce();
expect(updatedConnection.verified).toBe(true);
});
it('should unverify connection and persist it', async () => {
const connection = await createConnection({ verified: true });
const isStillVerifiedSpy = vi
.fn()
.mockRejectedValue(new Error('Wrong credentials!'));
const originalApp = await connection.getApp();
const getAppSpy = vi
.spyOn(connection, 'getApp')
.mockImplementation(() => {
return {
...originalApp,
auth: {
...originalApp.auth,
isStillVerified: isStillVerifiedSpy,
},
};
});
const updatedConnection = await connection.testAndUpdateConnection();
expect(getAppSpy).toHaveBeenCalledOnce();
expect(isStillVerifiedSpy).toHaveBeenCalledOnce();
expect(updatedConnection.verified).toBe(false);
});
});
describe('verifyAndUpdateConnection', () => {
it('should verify connection with valid token', async () => {
const connection = await createConnection({
verified: false,
draft: true,
});
const verifyCredentialsSpy = vi.fn().mockResolvedValue(true);
const originalApp = await connection.getApp();
vi.spyOn(connection, 'getApp').mockImplementation(() => {
return {
...originalApp,
auth: {
...originalApp.auth,
verifyCredentials: verifyCredentialsSpy,
},
};
});
const updatedConnection = await connection.verifyAndUpdateConnection();
expect(verifyCredentialsSpy).toHaveBeenCalledOnce();
expect(updatedConnection.verified).toBe(true);
expect(updatedConnection.draft).toBe(false);
});
it('should throw an error with invalid token', async () => {
const connection = await createConnection({
verified: false,
draft: true,
});
const verifyCredentialsSpy = vi
.fn()
.mockRejectedValue(new Error('Invalid token!'));
const originalApp = await connection.getApp();
vi.spyOn(connection, 'getApp').mockImplementation(() => {
return {
...originalApp,
auth: {
...originalApp.auth,
verifyCredentials: verifyCredentialsSpy,
},
};
});
await expect(() =>
connection.verifyAndUpdateConnection()
).rejects.toThrowError('Invalid token!');
expect(verifyCredentialsSpy).toHaveBeenCalledOnce();
});
});
describe('verifyWebhook', () => {
it('should verify webhook on remote', async () => {
const connection = await createConnection({ key: 'typeform' });
const verifyWebhookSpy = vi.fn().mockResolvedValue('verified-webhook');
const originalApp = await connection.getApp();
vi.spyOn(connection, 'getApp').mockImplementation(() => {
return {
...originalApp,
auth: {
...originalApp.auth,
verifyWebhook: verifyWebhookSpy,
},
};
});
expect(await connection.verifyWebhook()).toBe('verified-webhook');
});
it('should return true if connection does not have value in key property', async () => {
const connection = await createConnection({ key: null });
expect(await connection.verifyWebhook()).toBe(true);
});
it('should throw an error at failed webhook verification', async () => {
const connection = await createConnection({ key: 'typeform' });
const verifyWebhookSpy = vi.fn().mockRejectedValue('unverified-webhook');
const originalApp = await connection.getApp();
vi.spyOn(connection, 'getApp').mockImplementation(() => {
return {
...originalApp,
auth: {
...originalApp.auth,
verifyWebhook: verifyWebhookSpy,
},
};
});
await expect(() => connection.verifyWebhook()).rejects.toThrowError(
'unverified-webhook'
);
});
});
it('generateAuthUrl should return authentication url', async () => {
const connection = await createConnection({
key: 'typeform',
formattedData: {
url: 'https://automatisch.io/authentication-url',
},
});
const generateAuthUrlSpy = vi.fn();
const originalApp = await connection.getApp();
vi.spyOn(connection, 'getApp').mockImplementation(() => {
return {
...originalApp,
auth: {
...originalApp.auth,
generateAuthUrl: generateAuthUrlSpy,
},
};
});
expect(await connection.generateAuthUrl()).toStrictEqual({
url: 'https://automatisch.io/authentication-url',
});
});
describe('reset', () => {
it('should keep screen name when exists and reset the rest of the formatted data', async () => {
const connection = await createConnection({
formattedData: {
screenName: 'Sample connection',
token: 'sample-token',
},
});
await connection.reset();
const refetchedConnection = await connection.$query();
expect(refetchedConnection.formattedData).toStrictEqual({
screenName: 'Sample connection',
});
});
it('should empty formatted data object when screen name does not exist', async () => {
const connection = await createConnection({
formattedData: {
token: 'sample-token',
},
});
await connection.reset();
const refetchedConnection = await connection.$query();
expect(refetchedConnection.formattedData).toStrictEqual({});
});
});
describe('updateFormattedData', () => {
it('should extend connection data with app auth client auth defaults', async () => {
const appAuthClient = await createAppAuthClient({
formattedAuthDefaults: {
clientId: 'sample-id',
},
});
const connection = await createConnection({
appAuthClientId: appAuthClient.id,
formattedData: {
token: 'sample-token',
},
});
const updatedConnection = await connection.updateFormattedData({
appAuthClientId: appAuthClient.id,
});
expect(updatedConnection.formattedData).toStrictEqual({
clientId: 'sample-id',
token: 'sample-token',
});
});
});
describe('$beforeInsert', () => {
it('should call super.$beforeInsert', async () => {
const superBeforeInsertSpy = vi
.spyOn(Base.prototype, '$beforeInsert')
.mockResolvedValue();
await createConnection();
expect(superBeforeInsertSpy).toHaveBeenCalledOnce();
});
it('should call checkEligibilityForCreation', async () => {
const checkEligibilityForCreationSpy = vi
.spyOn(Connection.prototype, 'checkEligibilityForCreation')
.mockResolvedValue();
await createConnection();
expect(checkEligibilityForCreationSpy).toHaveBeenCalledOnce();
});
it('should call encryptData', async () => {
const encryptDataSpy = vi
.spyOn(Connection.prototype, 'encryptData')
.mockResolvedValue();
await createConnection();
expect(encryptDataSpy).toHaveBeenCalledOnce();
});
});
describe('$beforeUpdate', () => {
it('should call super.$beforeUpdate', async () => {
const superBeforeUpdateSpy = vi
.spyOn(Base.prototype, '$beforeUpdate')
.mockResolvedValue();
const connection = await createConnection();
await connection.$query().patch({ verified: false });
expect(superBeforeUpdateSpy).toHaveBeenCalledOnce();
});
it('should call encryptData', async () => {
const connection = await createConnection();
const encryptDataSpy = vi
.spyOn(Connection.prototype, 'encryptData')
.mockResolvedValue();
await connection.$query().patch({ verified: false });
expect(encryptDataSpy).toHaveBeenCalledOnce();
});
});
describe('$afterFind', () => {
it('should call decryptData', async () => {
const connection = await createConnection();
const decryptDataSpy = vi
.spyOn(Connection.prototype, 'decryptData')
.mockResolvedValue();
await connection.$query();
expect(decryptDataSpy).toHaveBeenCalledOnce();
});
});
describe('$afterInsert', () => {
it('should call super.$afterInsert', async () => {
const superAfterInsertSpy = vi.spyOn(Base.prototype, '$afterInsert');
await createConnection();
expect(superAfterInsertSpy).toHaveBeenCalledOnce();
});
it('should call Telemetry.connectionCreated', async () => {
const telemetryConnectionCreatedSpy = vi
.spyOn(Telemetry, 'connectionCreated')
.mockImplementation(() => {});
const connection = await createConnection();
expect(telemetryConnectionCreatedSpy).toHaveBeenCalledWith(connection);
});
});
describe('$afterUpdate', () => {
it('should call super.$afterUpdate', async () => {
const superAfterInsertSpy = vi.spyOn(Base.prototype, '$afterUpdate');
const connection = await createConnection();
await connection.$query().patch({ verified: false });
expect(superAfterInsertSpy).toHaveBeenCalledOnce();
});
it('should call Telemetry.connectionUpdated', async () => {
const telemetryconnectionUpdatedSpy = vi
.spyOn(Telemetry, 'connectionCreated')
.mockImplementation(() => {});
const connection = await createConnection();
await connection.$query().patch({ verified: false });
expect(telemetryconnectionUpdatedSpy).toHaveBeenCalledWith(connection);
});
});
});

View File

@@ -88,15 +88,13 @@ class Flow extends Base {
},
});
static async afterFind(args) {
const { result } = args;
const referenceFlow = result[0];
static async populateStatusProperty(flows) {
const referenceFlow = flows[0];
if (referenceFlow) {
const shouldBePaused = await referenceFlow.isPaused();
for (const flow of result) {
for (const flow of flows) {
if (!flow.active) {
flow.status = 'draft';
} else if (flow.active && shouldBePaused) {
@@ -108,6 +106,10 @@ class Flow extends Base {
}
}
static async afterFind(args) {
await this.populateStatusProperty(args.result);
}
async lastInternalId() {
const lastExecution = await this.$relatedQuery('lastExecution');
@@ -123,13 +125,14 @@ class Flow extends Base {
return lastExecutions.map((execution) => execution.internalId);
}
get IncompleteStepsError() {
static get IncompleteStepsError() {
return new ValidationError({
data: {
flow: [
{
message: 'All steps should be completed before updating flow status!'
}
message:
'All steps should be completed before updating flow status!',
},
],
},
type: 'incompleteStepsError',
@@ -148,36 +151,48 @@ class Flow extends Base {
type: 'action',
position: 2,
});
return this.$query().withGraphFetched('steps');
}
async createActionStep(previousStepId) {
const previousStep = await this.$relatedQuery('steps')
.findById(previousStepId)
.throwIfNotFound();
async getStepById(stepId) {
return await this.$relatedQuery('steps').findById(stepId).throwIfNotFound();
}
const createdStep = await this.$relatedQuery('steps').insertAndFetch({
async insertActionStepAtPosition(position) {
return await this.$relatedQuery('steps').insertAndFetch({
type: 'action',
position: previousStep.position + 1,
position,
});
}
const nextSteps = await this.$relatedQuery('steps')
.where('position', '>=', createdStep.position)
.whereNot('id', createdStep.id);
async getStepsAfterPosition(position) {
return await this.$relatedQuery('steps').where('position', '>', position);
}
const nextStepQueries = nextSteps.map(async (nextStep, index) => {
return await nextStep.$query().patchAndFetch({
position: createdStep.position + index + 1,
async updateStepPositionsFrom(startPosition, steps) {
const stepPositionUpdates = steps.map(async (step, index) => {
return await step.$query().patch({
position: startPosition + index,
});
});
await Promise.all(nextStepQueries);
return await Promise.all(stepPositionUpdates);
}
async createStepAfter(previousStepId) {
const previousStep = await this.getStepById(previousStepId);
const nextSteps = await this.getStepsAfterPosition(previousStep.position);
const createdStep = await this.insertActionStepAtPosition(
previousStep.position + 1
);
await this.updateStepPositionsFrom(createdStep.position + 1, nextSteps);
return createdStep;
}
async delete() {
async unregisterWebhook() {
const triggerStep = await this.getTriggerStep();
const trigger = await triggerStep?.getTriggerCommand();
@@ -198,15 +213,33 @@ class Flow extends Base {
);
}
}
}
async deleteExecutionSteps() {
const executionIds = (
await this.$relatedQuery('executions').select('executions.id')
).map((execution) => execution.id);
await ExecutionStep.query().delete().whereIn('execution_id', executionIds);
return await ExecutionStep.query()
.delete()
.whereIn('execution_id', executionIds);
}
async deleteExecutions() {
return await this.$relatedQuery('executions').delete();
}
async deleteSteps() {
return await this.$relatedQuery('steps').delete();
}
async delete() {
await this.unregisterWebhook();
await this.deleteExecutionSteps();
await this.deleteExecutions();
await this.deleteSteps();
await this.$relatedQuery('executions').delete();
await this.$relatedQuery('steps').delete();
await this.$query().delete();
}
@@ -291,6 +324,18 @@ class Flow extends Base {
return duplicatedFlowWithSteps;
}
async getTriggerStep() {
return await this.$relatedQuery('steps').findOne({
type: 'trigger',
});
}
async isPaused() {
const user = await this.$relatedQuery('user').withSoftDeleted();
const allowedToRunFlows = await user.isAllowedToRunFlows();
return allowedToRunFlows ? false : true;
}
async updateStatus(newActiveValue) {
if (this.active === newActiveValue) {
return this;
@@ -299,7 +344,7 @@ class Flow extends Base {
const triggerStep = await this.getTriggerStep();
if (triggerStep.status === 'incomplete') {
throw this.IncompleteStepsError;
throw Flow.IncompleteStepsError;
}
const trigger = await triggerStep.getTriggerCommand();
@@ -353,60 +398,55 @@ class Flow extends Base {
});
}
async $beforeUpdate(opt, queryContext) {
await super.$beforeUpdate(opt, queryContext);
if (!this.active) return;
const oldFlow = opt.old;
const incompleteStep = await oldFlow.$relatedQuery('steps').findOne({
async throwIfHavingIncompleteSteps() {
const incompleteStep = await this.$relatedQuery('steps').findOne({
status: 'incomplete',
});
if (incompleteStep) {
throw this.IncompleteStepsError;
throw Flow.IncompleteStepsError;
}
}
const allSteps = await oldFlow.$relatedQuery('steps');
async throwIfHavingLessThanTwoSteps() {
const allSteps = await this.$relatedQuery('steps');
if (allSteps.length < 2) {
throw new ValidationError({
data: {
flow: [
{
message: 'There should be at least one trigger and one action steps in the flow!'
}
message:
'There should be at least one trigger and one action steps in the flow!',
},
],
},
type: 'insufficientStepsError',
});
}
}
return;
async $beforeUpdate(opt, queryContext) {
await super.$beforeUpdate(opt, queryContext);
if (this.active) {
await opt.old.throwIfHavingIncompleteSteps();
await opt.old.throwIfHavingLessThanTwoSteps();
}
}
async $afterInsert(queryContext) {
await super.$afterInsert(queryContext);
Telemetry.flowCreated(this);
}
async $afterUpdate(opt, queryContext) {
await super.$afterUpdate(opt, queryContext);
Telemetry.flowUpdated(this);
}
async getTriggerStep() {
return await this.$relatedQuery('steps').findOne({
type: 'trigger',
});
}
async isPaused() {
const user = await this.$relatedQuery('user').withSoftDeleted();
const allowedToRunFlows = await user.isAllowedToRunFlows();
return allowedToRunFlows ? false : true;
}
}
export default Flow;

View File

@@ -0,0 +1,616 @@
import { describe, it, expect, vi } from 'vitest';
import Flow from './flow.js';
import User from './user.js';
import Base from './base.js';
import Step from './step.js';
import Execution from './execution.js';
import Telemetry from '../helpers/telemetry/index.js';
import * as globalVariableModule from '../helpers/global-variable.js';
import { createFlow } from '../../test/factories/flow.js';
import { createStep } from '../../test/factories/step.js';
import { createExecution } from '../../test/factories/execution.js';
import { createExecutionStep } from '../../test/factories/execution-step.js';
describe('Flow model', () => {
it('tableName should return correct name', () => {
expect(Flow.tableName).toBe('flows');
});
it('jsonSchema should have correct validations', () => {
expect(Flow.jsonSchema).toMatchSnapshot();
});
describe('relationMappings', () => {
it('should return correct associations', () => {
const relationMappings = Flow.relationMappings();
const expectedRelations = {
steps: {
relation: Base.HasManyRelation,
modelClass: Step,
join: {
from: 'flows.id',
to: 'steps.flow_id',
},
filter: expect.any(Function),
},
triggerStep: {
relation: Base.HasOneRelation,
modelClass: Step,
join: {
from: 'flows.id',
to: 'steps.flow_id',
},
filter: expect.any(Function),
},
executions: {
relation: Base.HasManyRelation,
modelClass: Execution,
join: {
from: 'flows.id',
to: 'executions.flow_id',
},
},
lastExecution: {
relation: Base.HasOneRelation,
modelClass: Execution,
join: {
from: 'flows.id',
to: 'executions.flow_id',
},
filter: expect.any(Function),
},
user: {
relation: Base.HasOneRelation,
modelClass: User,
join: {
from: 'flows.user_id',
to: 'users.id',
},
},
};
expect(relationMappings).toStrictEqual(expectedRelations);
});
it('steps should return the steps', () => {
const relations = Flow.relationMappings();
const orderBySpy = vi.fn();
relations.steps.filter({ orderBy: orderBySpy });
expect(orderBySpy).toHaveBeenCalledWith('position', 'asc');
});
it('triggerStep should return the trigger step', () => {
const relations = Flow.relationMappings();
const firstSpy = vi.fn();
const limitSpy = vi.fn().mockImplementation(() => ({
first: firstSpy,
}));
const whereSpy = vi.fn().mockImplementation(() => ({
limit: limitSpy,
}));
relations.triggerStep.filter({ where: whereSpy });
expect(whereSpy).toHaveBeenCalledWith('type', 'trigger');
expect(limitSpy).toHaveBeenCalledWith(1);
expect(firstSpy).toHaveBeenCalledOnce();
});
it('lastExecution should return the last execution', () => {
const relations = Flow.relationMappings();
const firstSpy = vi.fn();
const limitSpy = vi.fn().mockImplementation(() => ({
first: firstSpy,
}));
const orderBySpy = vi.fn().mockImplementation(() => ({
limit: limitSpy,
}));
relations.lastExecution.filter({ orderBy: orderBySpy });
expect(orderBySpy).toHaveBeenCalledWith('created_at', 'desc');
expect(limitSpy).toHaveBeenCalledWith(1);
expect(firstSpy).toHaveBeenCalledOnce();
});
});
describe('populateStatusProperty', () => {
it('should assign "draft" to status property when a flow is not active', async () => {
const referenceFlow = await createFlow({ active: false });
const flows = [referenceFlow];
vi.spyOn(referenceFlow, 'isPaused').mockResolvedValue();
await Flow.populateStatusProperty(flows);
expect(referenceFlow.status).toBe('draft');
});
it('should assign "paused" to status property when a flow is active, but should be paused', async () => {
const referenceFlow = await createFlow({ active: true });
const flows = [referenceFlow];
vi.spyOn(referenceFlow, 'isPaused').mockResolvedValue(true);
await Flow.populateStatusProperty(flows);
expect(referenceFlow.status).toBe('paused');
});
it('should assign "published" to status property when a flow is active', async () => {
const referenceFlow = await createFlow({ active: true });
const flows = [referenceFlow];
vi.spyOn(referenceFlow, 'isPaused').mockResolvedValue(false);
await Flow.populateStatusProperty(flows);
expect(referenceFlow.status).toBe('published');
});
});
it('afterFind should call Flow.populateStatusProperty', async () => {
const populateStatusPropertySpy = vi
.spyOn(Flow, 'populateStatusProperty')
.mockImplementation(() => {});
await createFlow();
expect(populateStatusPropertySpy).toHaveBeenCalledOnce();
});
describe('lastInternalId', () => {
it('should return internal ID of last execution when exists', async () => {
const flow = await createFlow();
await createExecution({ flowId: flow.id });
await createExecution({ flowId: flow.id });
const lastExecution = await createExecution({ flowId: flow.id });
expect(await flow.lastInternalId()).toBe(lastExecution.internalId);
});
it('should return null when no flow execution exists', async () => {
const flow = await createFlow();
expect(await flow.lastInternalId()).toBe(null);
});
});
describe('lastInternalIds', () => {
it('should return last internal IDs', async () => {
const flow = await createFlow();
const internalIds = [
await createExecution({ flowId: flow.id }),
await createExecution({ flowId: flow.id }),
await createExecution({ flowId: flow.id }),
].map((execution) => execution.internalId);
expect(await flow.lastInternalIds()).toStrictEqual(internalIds);
});
it('should return last 50 internal IDs by default', async () => {
const flow = new Flow();
const limitSpy = vi.fn().mockResolvedValue([]);
vi.spyOn(flow, '$relatedQuery').mockReturnValue({
select: vi.fn().mockReturnThis(),
orderBy: vi.fn().mockReturnThis(),
limit: limitSpy,
});
await flow.lastInternalIds();
expect(limitSpy).toHaveBeenCalledWith(50);
});
});
it('IncompleteStepsError should return validation error for incomplete steps', () => {
expect(() => {
throw Flow.IncompleteStepsError;
}).toThrowError(
'flow: All steps should be completed before updating flow status!'
);
});
it('createInitialSteps should create one trigger and one action step', async () => {
const flow = await createFlow();
await flow.createInitialSteps();
const steps = await flow.$relatedQuery('steps');
expect(steps.length).toBe(2);
expect(steps[0]).toMatchObject({
flowId: flow.id,
type: 'trigger',
position: 1,
});
expect(steps[1]).toMatchObject({
flowId: flow.id,
type: 'action',
position: 2,
});
});
it('getStepById should return the step with the given ID from the flow', async () => {
const flow = await createFlow();
const step = await createStep({ flowId: flow.id });
expect(await flow.getStepById(step.id)).toStrictEqual(step);
});
it('insertActionStepAtPosition should insert action step at given position', async () => {
const flow = await createFlow();
await flow.createInitialSteps();
const createdStep = await flow.insertActionStepAtPosition(2);
expect(createdStep).toMatchObject({
type: 'action',
position: 2,
});
});
it('getStepsAfterPosition should return steps after the given position', async () => {
const flow = await createFlow();
await flow.createInitialSteps();
await createStep({ flowId: flow.id });
expect(await flow.getStepsAfterPosition(1)).toMatchObject([
{ position: 2 },
{ position: 3 },
]);
});
it('updateStepPositionsFrom', async () => {
const flow = await createFlow();
await createStep({ type: 'trigger', flowId: flow.id, position: 6 });
await createStep({ type: 'action', flowId: flow.id, position: 8 });
await createStep({ type: 'action', flowId: flow.id, position: 10 });
await flow.updateStepPositionsFrom(2, await flow.$relatedQuery('steps'));
expect(await flow.$relatedQuery('steps')).toMatchObject([
{ position: 2, type: 'trigger' },
{ position: 3, type: 'action' },
{ position: 4, type: 'action' },
]);
});
it('createStepAfter should create an action step after given step ID', async () => {
const flow = await createFlow();
const triggerStep = await createStep({ type: 'trigger', flowId: flow.id });
const actionStep = await createStep({ type: 'action', flowId: flow.id });
const createdStep = await flow.createStepAfter(triggerStep.id);
const refetchedActionStep = await actionStep.$query();
expect(createdStep).toMatchObject({ type: 'action', position: 2 });
expect(refetchedActionStep.position).toBe(3);
});
describe('unregisterWebhook', () => {
it('should unregister webhook on remote when supported', async () => {
const flow = await createFlow();
const triggerStep = await createStep({
flowId: flow.id,
appKey: 'typeform',
key: 'new-entry',
type: 'trigger',
});
const unregisterHookSpy = vi.fn().mockResolvedValue();
vi.spyOn(Step.prototype, 'getTriggerCommand').mockResolvedValue({
type: 'webhook',
unregisterHook: unregisterHookSpy,
});
const globalVariableSpy = vi
.spyOn(globalVariableModule, 'default')
.mockResolvedValue('global-variable');
await flow.unregisterWebhook();
expect(unregisterHookSpy).toHaveBeenCalledWith('global-variable');
expect(globalVariableSpy).toHaveBeenCalledWith({
flow,
step: triggerStep,
connection: undefined,
app: await triggerStep.getApp(),
});
});
it('should silently fail when unregistration fails', async () => {
const flow = await createFlow();
await createStep({
flowId: flow.id,
appKey: 'typeform',
key: 'new-entry',
type: 'trigger',
});
const unregisterHookSpy = vi.fn().mockRejectedValue(new Error());
vi.spyOn(Step.prototype, 'getTriggerCommand').mockResolvedValue({
type: 'webhook',
unregisterHook: unregisterHookSpy,
});
expect(await flow.unregisterWebhook()).toBe(undefined);
expect(unregisterHookSpy).toHaveBeenCalledOnce();
});
it('should do nothing when trigger step is not webhook', async () => {
const flow = await createFlow();
await createStep({
flowId: flow.id,
type: 'trigger',
});
const unregisterHookSpy = vi.fn().mockRejectedValue(new Error());
expect(await flow.unregisterWebhook()).toBe(undefined);
expect(unregisterHookSpy).not.toHaveBeenCalled();
});
});
it('deleteExecutionSteps should delete related execution steps', async () => {
const flow = await createFlow();
const execution = await createExecution({ flowId: flow.id });
const firstExecutionStep = await createExecutionStep({
executionId: execution.id,
});
const secondExecutionStep = await createExecutionStep({
executionId: execution.id,
});
await flow.deleteExecutionSteps();
expect(await firstExecutionStep.$query()).toBe(undefined);
expect(await secondExecutionStep.$query()).toBe(undefined);
});
it('deleteExecutions should delete related executions', async () => {
const flow = await createFlow();
const firstExecution = await createExecution({ flowId: flow.id });
const secondExecution = await createExecution({ flowId: flow.id });
await flow.deleteExecutions();
expect(await firstExecution.$query()).toBe(undefined);
expect(await secondExecution.$query()).toBe(undefined);
});
it('deleteSteps should delete related steps', async () => {
const flow = await createFlow();
await flow.createInitialSteps();
await flow.deleteSteps();
expect(await flow.$relatedQuery('steps')).toStrictEqual([]);
});
it('delete should delete the flow with its relations', async () => {
const flow = await createFlow();
const unregisterWebhookSpy = vi
.spyOn(flow, 'unregisterWebhook')
.mockResolvedValue();
const deleteExecutionStepsSpy = vi
.spyOn(flow, 'deleteExecutionSteps')
.mockResolvedValue();
const deleteExecutionsSpy = vi
.spyOn(flow, 'deleteExecutions')
.mockResolvedValue();
const deleteStepsSpy = vi.spyOn(flow, 'deleteSteps').mockResolvedValue();
await flow.delete();
expect(unregisterWebhookSpy).toHaveBeenCalledOnce();
expect(deleteExecutionStepsSpy).toHaveBeenCalledOnce();
expect(deleteExecutionsSpy).toHaveBeenCalledOnce();
expect(deleteStepsSpy).toHaveBeenCalledOnce();
expect(await flow.$query()).toBe(undefined);
});
it.todo('duplicateFor');
it('getTriggerStep', async () => {
const flow = await createFlow();
const triggerStep = await createStep({ flowId: flow.id, type: 'trigger' });
await createStep({ flowId: flow.id, type: 'action' });
expect(await flow.getTriggerStep()).toStrictEqual(triggerStep);
});
describe('isPaused', () => {
it('should return true when user.isAllowedToRunFlows returns false', async () => {
const flow = await createFlow();
const isAllowedToRunFlowsSpy = vi.fn().mockResolvedValue(false);
vi.spyOn(flow, '$relatedQuery').mockReturnValue({
withSoftDeleted: vi.fn().mockReturnThis(),
isAllowedToRunFlows: isAllowedToRunFlowsSpy,
});
expect(await flow.isPaused()).toBe(true);
expect(isAllowedToRunFlowsSpy).toHaveBeenCalledOnce();
});
it('should return false when user.isAllowedToRunFlows returns true', async () => {
const flow = await createFlow();
const isAllowedToRunFlowsSpy = vi.fn().mockResolvedValue(true);
vi.spyOn(flow, '$relatedQuery').mockReturnValue({
withSoftDeleted: vi.fn().mockReturnThis(),
isAllowedToRunFlows: isAllowedToRunFlowsSpy,
});
expect(await flow.isPaused()).toBe(false);
expect(isAllowedToRunFlowsSpy).toHaveBeenCalledOnce();
});
});
describe('throwIfHavingIncompleteSteps', () => {
it('should throw validation error with incomplete steps', async () => {
const flow = await createFlow();
await flow.createInitialSteps();
await expect(() =>
flow.throwIfHavingIncompleteSteps()
).rejects.toThrowError(
'flow: All steps should be completed before updating flow status!'
);
});
it('should return undefined when all steps are completed', async () => {
const flow = await createFlow();
await createStep({
flowId: flow.id,
status: 'completed',
type: 'trigger',
});
await createStep({
flowId: flow.id,
status: 'completed',
type: 'action',
});
expect(await flow.throwIfHavingIncompleteSteps()).toBe(undefined);
});
});
describe('throwIfHavingLessThanTwoSteps', () => {
it('should throw validation error with less than two steps', async () => {
const flow = await createFlow();
await expect(() =>
flow.throwIfHavingLessThanTwoSteps()
).rejects.toThrowError(
'flow: There should be at least one trigger and one action steps in the flow!'
);
});
it('should return undefined when there are at least two steps', async () => {
const flow = await createFlow();
await createStep({
flowId: flow.id,
type: 'trigger',
});
await createStep({
flowId: flow.id,
type: 'action',
});
expect(await flow.throwIfHavingLessThanTwoSteps()).toBe(undefined);
});
});
describe('$beforeUpdate', () => {
it('should invoke throwIfHavingIncompleteSteps when flow is becoming active', async () => {
const flow = await createFlow({ active: false });
const throwIfHavingIncompleteStepsSpy = vi
.spyOn(Flow.prototype, 'throwIfHavingIncompleteSteps')
.mockImplementation(() => {});
const throwIfHavingLessThanTwoStepsSpy = vi
.spyOn(Flow.prototype, 'throwIfHavingLessThanTwoSteps')
.mockImplementation(() => {});
await flow.$query().patch({ active: true });
expect(throwIfHavingIncompleteStepsSpy).toHaveBeenCalledOnce();
expect(throwIfHavingLessThanTwoStepsSpy).toHaveBeenCalledOnce();
});
it('should invoke throwIfHavingIncompleteSteps when flow is not becoming active', async () => {
const flow = await createFlow({ active: true });
const throwIfHavingIncompleteStepsSpy = vi
.spyOn(Flow.prototype, 'throwIfHavingIncompleteSteps')
.mockImplementation(() => {});
const throwIfHavingLessThanTwoStepsSpy = vi
.spyOn(Flow.prototype, 'throwIfHavingLessThanTwoSteps')
.mockImplementation(() => {});
await flow.$query().patch({});
expect(throwIfHavingIncompleteStepsSpy).not.toHaveBeenCalledOnce();
expect(throwIfHavingLessThanTwoStepsSpy).not.toHaveBeenCalledOnce();
});
});
describe('$afterInsert', () => {
it('should call super.$afterInsert', async () => {
const superAfterInsertSpy = vi.spyOn(Base.prototype, '$afterInsert');
await createFlow();
expect(superAfterInsertSpy).toHaveBeenCalled();
});
it('should call Telemetry.flowCreated', async () => {
const telemetryFlowCreatedSpy = vi
.spyOn(Telemetry, 'flowCreated')
.mockImplementation(() => {});
const flow = await createFlow();
expect(telemetryFlowCreatedSpy).toHaveBeenCalledWith(flow);
});
});
describe('$afterUpdate', () => {
it('should call super.$afterUpdate', async () => {
const superAfterUpdateSpy = vi.spyOn(Base.prototype, '$afterUpdate');
const flow = await createFlow();
await flow.$query().patch({ active: false });
expect(superAfterUpdateSpy).toHaveBeenCalledOnce();
});
it('$afterUpdate should call Telemetry.flowUpdated', async () => {
const telemetryFlowUpdatedSpy = vi
.spyOn(Telemetry, 'flowUpdated')
.mockImplementation(() => {});
const flow = await createFlow();
await flow.$query().patch({ active: false });
expect(telemetryFlowUpdatedSpy).toHaveBeenCalled({});
});
});
});

View File

@@ -19,25 +19,39 @@ class Permission extends Base {
},
};
static sanitize(permissions) {
static filter(permissions) {
const sanitizedPermissions = permissions.filter((permission) => {
const { action, subject, conditions } = permission;
const relevantAction = permissionCatalog.actions.find(
(actionCatalogItem) => actionCatalogItem.key === action
);
const validSubject = relevantAction.subjects.includes(subject);
const validConditions = conditions.every((condition) => {
return !!permissionCatalog.conditions.find(
(conditionCatalogItem) => conditionCatalogItem.key === condition
);
});
const relevantAction = this.findAction(action);
const validSubject = this.isSubjectValid(subject, relevantAction);
const validConditions = this.areConditionsValid(conditions);
return validSubject && validConditions;
return relevantAction && validSubject && validConditions;
});
return sanitizedPermissions;
}
static findAction(action) {
return permissionCatalog.actions.find(
(actionCatalogItem) => actionCatalogItem.key === action
);
}
static isSubjectValid(subject, action) {
return action && action.subjects.includes(subject);
}
static areConditionsValid(conditions) {
return conditions.every((condition) => this.isConditionValid(condition));
}
static isConditionValid(condition) {
return !!permissionCatalog.conditions.find(
(conditionCatalogItem) => conditionCatalogItem.key === condition
);
}
}
export default Permission;

View File

@@ -0,0 +1,95 @@
import { describe, it, expect } from 'vitest';
import Permission from './permission';
import permissionCatalog from '../helpers/permission-catalog.ee.js';
describe('Permission model', () => {
it('tableName should return correct name', () => {
expect(Permission.tableName).toBe('permissions');
});
it('jsonSchema should have correct validations', () => {
expect(Permission.jsonSchema).toMatchSnapshot();
});
it('filter should return only valid permissions based on permission catalog', () => {
const permissions = [
{ action: 'read', subject: 'Flow', conditions: ['isCreator'] },
{ action: 'delete', subject: 'Connection', conditions: [] },
{ action: 'publish', subject: 'Flow', conditions: ['isCreator'] },
{ action: 'update', subject: 'Execution', conditions: [] }, // Invalid subject
{ action: 'read', subject: 'Execution', conditions: ['invalid'] }, // Invalid condition
{ action: 'invalid', subject: 'Execution', conditions: [] }, // Invalid action
];
const result = Permission.filter(permissions);
expect(result).toStrictEqual([
{ action: 'read', subject: 'Flow', conditions: ['isCreator'] },
{ action: 'delete', subject: 'Connection', conditions: [] },
{ action: 'publish', subject: 'Flow', conditions: ['isCreator'] },
]);
});
describe('findAction', () => {
it('should return action from permission catalog', () => {
const action = Permission.findAction('create');
expect(action.key).toStrictEqual('create');
});
it('should return undefined for invalid actions', () => {
const invalidAction = Permission.findAction('invalidAction');
expect(invalidAction).toBeUndefined();
});
});
describe('isSubjectValid', () => {
it('should return true for valid subjects', () => {
const validAction = permissionCatalog.actions.find(
(action) => action.key === 'create'
);
const validSubject = Permission.isSubjectValid('Connection', validAction);
expect(validSubject).toBe(true);
});
it('should return false for invalid subjects', () => {
const validAction = permissionCatalog.actions.find(
(action) => action.key === 'create'
);
const invalidSubject = Permission.isSubjectValid(
'Execution',
validAction
);
expect(invalidSubject).toBe(false);
});
});
describe('areConditionsValid', () => {
it('should return true for valid conditions', () => {
const validConditions = Permission.areConditionsValid(['isCreator']);
expect(validConditions).toBe(true);
});
it('should return false for invalid conditions', () => {
const invalidConditions = Permission.areConditionsValid([
'invalidCondition',
]);
expect(invalidConditions).toBe(false);
});
});
describe('isConditionValid', () => {
it('should return true for valid conditions', () => {
const validCondition = Permission.isConditionValid('isCreator');
expect(validCondition).toBe(true);
});
it('should return false for invalid conditions', () => {
const invalidCondition = Permission.isConditionValid('invalidCondition');
expect(invalidCondition).toBe(false);
});
});
});

View File

@@ -52,57 +52,64 @@ class Role extends Base {
return await this.query().findOne({ name: 'Admin' });
}
async updateWithPermissions(data) {
if (this.isAdmin) {
async preventAlteringAdmin() {
const currentRole = await Role.query().findById(this.id);
if (currentRole.isAdmin) {
throw new NotAuthorizedError('The admin role cannot be altered!');
}
}
async deletePermissions() {
return await this.$relatedQuery('permissions').delete();
}
async createPermissions(permissions) {
if (permissions?.length) {
const validPermissions = Permission.filter(permissions).map(
(permission) => ({
...permission,
roleId: this.id,
})
);
await Permission.query().insert(validPermissions);
}
}
async updatePermissions(permissions) {
await this.deletePermissions();
await this.createPermissions(permissions);
}
async updateWithPermissions(data) {
const { name, description, permissions } = data;
return await Role.transaction(async (trx) => {
await this.$relatedQuery('permissions', trx).delete();
await this.updatePermissions(permissions);
if (permissions?.length) {
const sanitizedPermissions = Permission.sanitize(permissions).map(
(permission) => ({
...permission,
roleId: this.id,
})
);
await Permission.query().insert(sanitizedPermissions);
}
await this.$query(trx).patch({
name,
description,
});
return await this.$query(trx)
.leftJoinRelated({
permissions: true,
})
.withGraphFetched({
permissions: true,
});
await this.$query().patchAndFetch({
id: this.id,
name,
description,
});
return await this.$query()
.leftJoinRelated({
permissions: true,
})
.withGraphFetched({
permissions: true,
});
}
async deleteWithPermissions() {
return await Role.transaction(async (trx) => {
await this.$relatedQuery('permissions', trx).delete();
await this.deletePermissions();
return await this.$query(trx).delete();
});
return await this.$query().delete();
}
async $beforeDelete(queryContext) {
await super.$beforeDelete(queryContext);
if (this.isAdmin) {
throw new NotAuthorizedError('The admin role cannot be deleted!');
}
async assertNoRoleUserExists() {
const userCount = await this.$relatedQuery('users').limit(1).resultSize();
const hasUsers = userCount > 0;
@@ -118,7 +125,9 @@ class Role extends Base {
type: 'ValidationError',
});
}
}
async assertNoConfigurationUsage() {
const samlAuthProviderUsingDefaultRole = await SamlAuthProvider.query()
.where({
default_role_id: this.id,
@@ -140,6 +149,26 @@ class Role extends Base {
});
}
}
async assertRoleIsNotUsed() {
await this.assertNoRoleUserExists();
await this.assertNoConfigurationUsage();
}
async $beforeUpdate(opt, queryContext) {
await super.$beforeUpdate(opt, queryContext);
await this.preventAlteringAdmin();
}
async $beforeDelete(queryContext) {
await super.$beforeDelete(queryContext);
await this.preventAlteringAdmin();
await this.assertRoleIsNotUsed();
}
}
export default Role;

View File

@@ -0,0 +1,287 @@
import { describe, it, expect, vi } from 'vitest';
import Role from './role';
import Base from './base.js';
import Permission from './permission.js';
import User from './user.js';
import { createRole } from '../../test/factories/role.js';
import { createPermission } from '../../test/factories/permission.js';
import { createUser } from '../../test/factories/user.js';
import { createSamlAuthProvider } from '../../test/factories/saml-auth-provider.ee.js';
describe('Role model', () => {
it('tableName should return correct name', () => {
expect(Role.tableName).toBe('roles');
});
it('jsonSchema should have correct validations', () => {
expect(Role.jsonSchema).toMatchSnapshot();
});
it('relationMappingsshould return correct associations', () => {
const relationMappings = Role.relationMappings();
const expectedRelations = {
users: {
relation: Base.HasManyRelation,
modelClass: User,
join: {
from: 'roles.id',
to: 'users.role_id',
},
},
permissions: {
relation: Base.HasManyRelation,
modelClass: Permission,
join: {
from: 'roles.id',
to: 'permissions.role_id',
},
},
};
expect(relationMappings).toStrictEqual(expectedRelations);
});
it('virtualAttributes should return correct attributes', () => {
expect(Role.virtualAttributes).toStrictEqual(['isAdmin']);
});
describe('isAdmin', () => {
it('should return true for admin named role', () => {
const role = new Role();
role.name = 'Admin';
expect(role.isAdmin).toBe(true);
});
it('should return false for not admin named roles', () => {
const role = new Role();
role.name = 'User';
expect(role.isAdmin).toBe(false);
});
});
it('findAdmin should return admin role', async () => {
const createdAdminRole = await createRole({ name: 'Admin' });
const adminRole = await Role.findAdmin();
expect(createdAdminRole).toStrictEqual(adminRole);
});
describe('preventAlteringAdmin', () => {
it('preventAlteringAdmin should throw an error when altering admin role', async () => {
const role = await createRole({ name: 'Admin' });
await expect(() => role.preventAlteringAdmin()).rejects.toThrowError(
'The admin role cannot be altered!'
);
});
it('preventAlteringAdmin should not throw an error when altering non-admin roles', async () => {
const role = await createRole({ name: 'User' });
expect(await role.preventAlteringAdmin()).toBe(undefined);
});
});
it("deletePermissions should delete role's permissions", async () => {
const role = await createRole({ name: 'User' });
await createPermission({ roleId: role.id });
await role.deletePermissions();
expect(await role.$relatedQuery('permissions')).toStrictEqual([]);
});
describe('createPermissions', () => {
it('should create permissions', async () => {
const role = await createRole({ name: 'User' });
await role.createPermissions([
{ action: 'read', subject: 'Flow', conditions: [] },
]);
expect(await role.$relatedQuery('permissions')).toMatchObject([
{
action: 'read',
subject: 'Flow',
conditions: [],
},
]);
});
it('should call Permission.filter', async () => {
const role = await createRole({ name: 'User' });
const permissions = [{ action: 'read', subject: 'Flow', conditions: [] }];
const permissionFilterSpy = vi
.spyOn(Permission, 'filter')
.mockReturnValue(permissions);
await role.createPermissions(permissions);
expect(permissionFilterSpy).toHaveBeenCalledWith(permissions);
});
});
it('updatePermissions should delete existing permissions and create new permissions', async () => {
const permissionsData = [
{ action: 'read', subject: 'Flow', conditions: [] },
];
const deletePermissionsSpy = vi
.spyOn(Role.prototype, 'deletePermissions')
.mockResolvedValueOnce();
const createPermissionsSpy = vi
.spyOn(Role.prototype, 'createPermissions')
.mockResolvedValueOnce();
const role = await createRole({ name: 'User' });
await role.updatePermissions(permissionsData);
expect(deletePermissionsSpy.mock.invocationCallOrder[0]).toBeLessThan(
createPermissionsSpy.mock.invocationCallOrder[0]
);
expect(deletePermissionsSpy).toHaveBeenNthCalledWith(1);
expect(createPermissionsSpy).toHaveBeenNthCalledWith(1, permissionsData);
});
describe('updateWithPermissions', () => {
it('should update role along with given permissions', async () => {
const role = await createRole({ name: 'User' });
await createPermission({
roleId: role.id,
subject: 'Flow',
action: 'read',
conditions: [],
});
const newRoleData = {
name: 'Updated user',
description: 'Updated description',
permissions: [
{
action: 'update',
subject: 'Flow',
conditions: [],
},
],
};
await role.updateWithPermissions(newRoleData);
const roleWithPermissions = await role
.$query()
.leftJoinRelated({ permissions: true })
.withGraphFetched({ permissions: true });
expect(roleWithPermissions).toMatchObject(newRoleData);
});
});
describe('deleteWithPermissions', () => {
it('should delete role along with given permissions', async () => {
const role = await createRole({ name: 'User' });
await createPermission({
roleId: role.id,
subject: 'Flow',
action: 'read',
conditions: [],
});
await role.deleteWithPermissions();
const refetchedRole = await role.$query();
const rolePermissions = await Permission.query().where({
roleId: role.id,
});
expect(refetchedRole).toBe(undefined);
expect(rolePermissions).toStrictEqual([]);
});
});
describe('assertNoRoleUserExists', () => {
it('should reject with an error when the role has users', async () => {
const role = await createRole({ name: 'User' });
await createUser({ roleId: role.id });
await expect(() => role.assertNoRoleUserExists()).rejects.toThrowError(
`All users must be migrated away from the "User" role.`
);
});
it('should resolve when the role does not have any users', async () => {
const role = await createRole();
expect(await role.assertNoRoleUserExists()).toBe(undefined);
});
});
describe('assertNoConfigurationUsage', () => {
it('should reject with an error when the role is used in configuration', async () => {
const role = await createRole();
await createSamlAuthProvider({ defaultRoleId: role.id });
await expect(() =>
role.assertNoConfigurationUsage()
).rejects.toThrowError(
'samlAuthProvider: You need to change the default role in the SAML configuration before deleting this role.'
);
});
it('should resolve when the role does not have any users', async () => {
const role = await createRole();
expect(await role.assertNoConfigurationUsage()).toBe(undefined);
});
});
it('assertRoleIsNotUsed should call assertNoRoleUserExists and assertNoConfigurationUsage', async () => {
const role = new Role();
const assertNoRoleUserExistsSpy = vi
.spyOn(role, 'assertNoRoleUserExists')
.mockResolvedValue();
const assertNoConfigurationUsageSpy = vi
.spyOn(role, 'assertNoConfigurationUsage')
.mockResolvedValue();
await role.assertRoleIsNotUsed();
expect(assertNoRoleUserExistsSpy).toHaveBeenCalledOnce();
expect(assertNoConfigurationUsageSpy).toHaveBeenCalledOnce();
});
describe('$beforeDelete', () => {
it('should call preventAlteringAdmin', async () => {
const role = await createRole({ name: 'User' });
const preventAlteringAdminSpy = vi
.spyOn(role, 'preventAlteringAdmin')
.mockResolvedValue();
await role.$query().delete();
expect(preventAlteringAdminSpy).toHaveBeenCalledOnce();
});
it('should call assertRoleIsNotUsed', async () => {
const role = await createRole({ name: 'User' });
const assertRoleIsNotUsedSpy = vi
.spyOn(role, 'assertRoleIsNotUsed')
.mockResolvedValue();
await role.$query().delete();
expect(assertRoleIsNotUsedSpy).toHaveBeenCalledOnce();
});
});
});

View File

@@ -0,0 +1,84 @@
import { vi, describe, it, expect } from 'vitest';
import SamlAuthProvider from '../models/saml-auth-provider.ee';
import SamlAuthProvidersRoleMapping from '../models/saml-auth-providers-role-mapping.ee';
import Identity from './identity.ee';
import Base from './base';
import appConfig from '../config/app';
describe('SamlAuthProvider model', () => {
it('tableName should return correct name', () => {
expect(SamlAuthProvider.tableName).toBe('saml_auth_providers');
});
it('jsonSchema should have the correct schema', () => {
expect(SamlAuthProvider.jsonSchema).toMatchSnapshot();
});
it('relationMappings should return correct associations', () => {
const relationMappings = SamlAuthProvider.relationMappings();
const expectedRelations = {
identities: {
relation: Base.HasOneRelation,
modelClass: Identity,
join: {
from: 'identities.provider_id',
to: 'saml_auth_providers.id',
},
},
samlAuthProvidersRoleMappings: {
relation: Base.HasManyRelation,
modelClass: SamlAuthProvidersRoleMapping,
join: {
from: 'saml_auth_providers.id',
to: 'saml_auth_providers_role_mappings.saml_auth_provider_id',
},
},
};
expect(relationMappings).toStrictEqual(expectedRelations);
});
it('virtualAttributes should return correct attributes', () => {
const virtualAttributes = SamlAuthProvider.virtualAttributes;
const expectedAttributes = ['loginUrl', 'remoteLogoutUrl'];
expect(virtualAttributes).toStrictEqual(expectedAttributes);
});
it('loginUrl should return the URL of login', () => {
const samlAuthProvider = new SamlAuthProvider();
samlAuthProvider.issuer = 'sample-issuer';
vi.spyOn(appConfig, 'baseUrl', 'get').mockReturnValue(
'https://automatisch.io'
);
expect(samlAuthProvider.loginUrl).toStrictEqual(
'https://automatisch.io/login/saml/sample-issuer'
);
});
it('loginCallbackUrl should return the URL of login callback', () => {
const samlAuthProvider = new SamlAuthProvider();
samlAuthProvider.issuer = 'sample-issuer';
vi.spyOn(appConfig, 'baseUrl', 'get').mockReturnValue(
'https://automatisch.io'
);
expect(samlAuthProvider.loginCallBackUrl).toStrictEqual(
'https://automatisch.io/login/saml/sample-issuer/callback'
);
});
it('remoteLogoutUrl should return the URL from entrypoint', () => {
const samlAuthProvider = new SamlAuthProvider();
samlAuthProvider.entryPoint = 'https://example.com/saml/logout';
expect(samlAuthProvider.remoteLogoutUrl).toStrictEqual(
'https://example.com/saml/logout'
);
});
});

View File

@@ -93,6 +93,14 @@ class Step extends Base {
return `${appConfig.baseUrl}/apps/${this.appKey}/assets/favicon.svg`;
}
get isTrigger() {
return this.type === 'trigger';
}
get isAction() {
return this.type === 'action';
}
async computeWebhookPath() {
if (this.type === 'action') return null;
@@ -135,24 +143,6 @@ class Step extends Base {
return webhookUrl;
}
async $afterInsert(queryContext) {
await super.$afterInsert(queryContext);
Telemetry.stepCreated(this);
}
async $afterUpdate(opt, queryContext) {
await super.$afterUpdate(opt, queryContext);
Telemetry.stepUpdated(this);
}
get isTrigger() {
return this.type === 'trigger';
}
get isAction() {
return this.type === 'action';
}
async getApp() {
if (!this.appKey) return null;
@@ -170,12 +160,7 @@ class Step extends Base {
}
async getLastExecutionStep() {
const lastExecutionStep = await this.$relatedQuery('executionSteps')
.orderBy('created_at', 'desc')
.limit(1)
.first();
return lastExecutionStep;
return await this.$relatedQuery('lastExecutionStep');
}
async getNextStep() {
@@ -207,19 +192,18 @@ class Step extends Base {
}
async getSetupFields() {
let setupSupsteps;
let substeps;
if (this.isTrigger) {
setupSupsteps = (await this.getTriggerCommand()).substeps;
substeps = (await this.getTriggerCommand()).substeps;
} else {
setupSupsteps = (await this.getActionCommand()).substeps;
substeps = (await this.getActionCommand()).substeps;
}
const existingArguments = setupSupsteps.find(
const setupSubstep = substeps.find(
(substep) => substep.key === 'chooseTrigger'
).arguments;
return existingArguments;
);
return setupSubstep.arguments;
}
async getSetupAndDynamicFields() {
@@ -326,23 +310,17 @@ class Step extends Base {
.$relatedQuery('steps')
.where('position', '>', this.position);
const nextStepQueries = nextSteps.map(async (nextStep) => {
await nextStep.$query().patch({
position: nextStep.position - 1,
});
});
await Promise.all(nextStepQueries);
await flow.updateStepPositionsFrom(this.position, nextSteps);
}
async updateFor(user, newStepData) {
const { connectionId, appKey, key, parameters } = newStepData;
const { appKey = this.appKey, connectionId, key, parameters } = newStepData;
if (connectionId && (appKey || this.appKey)) {
if (connectionId && appKey) {
await user.authorizedConnections
.findOne({
id: connectionId,
key: appKey || this.appKey,
key: appKey,
})
.throwIfNotFound();
}
@@ -356,8 +334,8 @@ class Step extends Base {
}
const updatedStep = await this.$query().patchAndFetch({
key: key,
appKey: appKey,
key,
appKey,
connectionId: connectionId,
parameters: parameters,
status: 'incomplete',
@@ -367,6 +345,16 @@ class Step extends Base {
return updatedStep;
}
async $afterInsert(queryContext) {
await super.$afterInsert(queryContext);
Telemetry.stepCreated(this);
}
async $afterUpdate(opt, queryContext) {
await super.$afterUpdate(opt, queryContext);
Telemetry.stepUpdated(this);
}
}
export default Step;

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