@aifabrix/builder 2.52.1 → 2.53.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (225) hide show
  1. package/lib/app/deploy-config.js +3 -2
  2. package/lib/channels/add-channel.js +11 -41
  3. package/lib/channels/approval-guides/index.js +3 -0
  4. package/lib/channels/approval-guides/microsoft-copilot.js +26 -17
  5. package/lib/channels/channel-add-existing.js +72 -0
  6. package/lib/channels/channel-runtime-resolver.js +5 -1
  7. package/lib/channels/channel-status-display.js +30 -3
  8. package/lib/channels/microsoft-copilot-entra-sso-form.js +73 -2
  9. package/lib/channels/microsoft-copilot-entra-sso.js +26 -5
  10. package/lib/channels/microsoft-copilot-limits.js +22 -3
  11. package/lib/channels/microsoft-copilot-package.js +59 -14
  12. package/lib/channels/microsoft-copilot-validate.js +40 -10
  13. package/lib/cli/attach-default-help-examples.js +102 -0
  14. package/lib/cli/index.js +2 -0
  15. package/lib/cli/setup-credential-deployment.js +7 -5
  16. package/lib/cli/setup-utility-validate.js +1 -0
  17. package/lib/commands/credential-push.js +37 -5
  18. package/lib/commands/package-score.js +72 -0
  19. package/lib/commands/role-assistant.js +46 -8
  20. package/lib/deployment/deployer-poll-run.js +2 -2
  21. package/lib/deployment/deployer.js +1 -1
  22. package/lib/deployment/environment.js +2 -2
  23. package/lib/external-system/deploy.js +11 -21
  24. package/lib/integration-definition/apply.js +12 -14
  25. package/lib/integration-definition/enrich-openapi-operations.js +9 -10
  26. package/lib/lab/package-parity-score.js +417 -0
  27. package/lib/programmatic/capability-registry.js +1 -0
  28. package/lib/programmatic/help-content/channel-onboarding.md +35 -0
  29. package/lib/programmatic/help-content/cip-overview.md +27 -0
  30. package/lib/programmatic/help-content/connected-system-ui.md +70 -0
  31. package/lib/programmatic/help-content/dimensions-guide.md +23 -0
  32. package/lib/programmatic/help-content/enterprise-sync-fabrix.md +123 -0
  33. package/lib/programmatic/help-content/overview.md +25 -0
  34. package/lib/programmatic/help-content/subscription-guide.md +27 -0
  35. package/lib/programmatic/help-content/workflow.md +50 -0
  36. package/lib/programmatic/openapi-component-schemas.js +1 -0
  37. package/lib/programmatic/openapi-descriptions.js +6 -0
  38. package/lib/programmatic/openapi-envelope-examples.js +11 -0
  39. package/lib/programmatic/openapi-operation-bindings.js +5 -0
  40. package/lib/programmatic/openapi-request-schemas.js +39 -13
  41. package/lib/programmatic/openapi-result-schemas.js +30 -0
  42. package/lib/programmatic/openapi-schema-descriptions-core.js +18 -0
  43. package/lib/programmatic/openapi-schema-descriptions-operations.js +19 -0
  44. package/lib/programmatic/persist-supplied-openapi.js +180 -0
  45. package/lib/programmatic/route-handler-map.js +6 -0
  46. package/lib/programmatic/run-integration-definition.js +13 -5
  47. package/lib/role-assistant/evidence-sync.js +55 -7
  48. package/lib/role-assistant/test-runner-attachments.js +118 -0
  49. package/lib/role-assistant/test-runner-bid-apply.js +100 -21
  50. package/lib/role-assistant/test-runner-bid-document.js +1 -1
  51. package/lib/role-assistant/test-runner-start-payload.js +96 -0
  52. package/lib/role-assistant/test-runner-workhub-answers.js +11 -7
  53. package/lib/role-assistant/test-runner-workhub-helpers.js +100 -0
  54. package/lib/role-assistant/test-runner-workhub-soft.js +86 -0
  55. package/lib/role-assistant/test-runner-workhub.js +68 -105
  56. package/lib/role-assistant/test-runner.js +5 -63
  57. package/lib/role-assistant/test-suite.js +33 -40
  58. package/lib/schema/external-datasource.schema.json +13 -4
  59. package/lib/utils/compose-generator.js +10 -1
  60. package/lib/utils/compose-traefik-extra-routes.js +107 -0
  61. package/lib/utils/docker-build.js +2 -2
  62. package/lib/utils/help-builder.js +2 -1
  63. package/lib/utils/promote-dev-docker-image.js +15 -10
  64. package/package.json +14 -16
  65. package/templates/README.md +12 -1
  66. package/templates/applications/dataplane/application.yaml +8 -1
  67. package/templates/applications/dataplane/env.template +1 -1
  68. package/templates/applications/miso-controller/application.yaml +2 -1
  69. package/templates/applications/miso-controller/rbac.yaml +1 -0
  70. package/templates/channels/microsoft-copilot/ai-plugin.template.json.hbs +6 -2
  71. package/templates/channels/microsoft-copilot/entra-sso-registration.template.md.hbs +8 -5
  72. package/templates/channels/microsoft-copilot/fi-fi.template.json.hbs +7 -0
  73. package/templates/channels/microsoft-copilot/install.template.md.hbs +74 -22
  74. package/templates/channels/microsoft-copilot/manifest.template.json.hbs +29 -14
  75. package/templates/channels/microsoft-copilot/oauth-plugin-vault-required.template.md.hbs +6 -2
  76. package/templates/python/docker-compose.hbs +36 -0
  77. package/templates/typescript/docker-compose.hbs +36 -0
  78. package/.cursor/plans/Archive/157.1-security-quality-violations-by-type.csv +0 -374
  79. package/.cursor/process/README.md +0 -9
  80. package/.cursor/rules/anchor-docs.mdc +0 -15
  81. package/.cursor/rules/assistant-driven-integration-build.mdc +0 -40
  82. package/.cursor/rules/cli-layout.mdc +0 -81
  83. package/.cursor/rules/cli-product-neutral.mdc +0 -43
  84. package/.cursor/rules/deploy-rules/docker-image-ip-policy.mdc +0 -49
  85. package/.cursor/rules/docs-rules.mdc +0 -32
  86. package/.cursor/rules/plan-rules/application-lifecycle.mdc +0 -30
  87. package/.cursor/rules/plan-rules/plan-frontmatter.mdc +0 -35
  88. package/.cursor/rules/plan-rules/plan-lifecycle.mdc +0 -31
  89. package/.cursor/rules/plan-rules/plan-validation-gate.mdc +0 -34
  90. package/.cursor/rules/project-rules.mdc +0 -913
  91. package/.cursor/skills/README.md +0 -15
  92. package/.dockerignore +0 -52
  93. package/.eslintignore +0 -22
  94. package/.markdownlint.json +0 -14
  95. package/anchor-docs/README.md +0 -10
  96. package/anchor-docs/_TEMPLATE +0 -24
  97. package/babel.config.js +0 -6
  98. package/bin/builder-api-cleanup.js +0 -26
  99. package/bin/builder-api-nest.js +0 -10
  100. package/bin/builder-api-spike.js +0 -19
  101. package/enterprise-knowledge/README.md +0 -50
  102. package/enterprise-knowledge/enterprise-integration-lifecycle/README.md +0 -22
  103. package/enterprise-knowledge/enterprise-integration-lifecycle/application.json +0 -15
  104. package/enterprise-knowledge/enterprise-integration-lifecycle/deploy.js +0 -69
  105. package/enterprise-knowledge/enterprise-integration-lifecycle/enterprise-integration-lifecycle-deploy.json +0 -52
  106. package/enterprise-knowledge/enterprise-integration-lifecycle/enterprise-integration-lifecycle-system.json +0 -29
  107. package/enterprise-knowledge/enterprise-integration-lifecycle/env.template +0 -2
  108. package/integration/README.md +0 -81
  109. package/integration/github/README.md +0 -53
  110. package/integration/github/application.json +0 -21
  111. package/integration/github/deploy.js +0 -72
  112. package/integration/github/env.template +0 -11
  113. package/integration/github/github-datasource-issue-comments.json +0 -239
  114. package/integration/github/github-datasource-issues.json +0 -399
  115. package/integration/github/github-datasource-repository.json +0 -354
  116. package/integration/github/github-deploy.json +0 -1202
  117. package/integration/github/github-system.json +0 -82
  118. package/integration/hubspot-test/README.md +0 -159
  119. package/integration/hubspot-test/application.json +0 -54
  120. package/integration/hubspot-test/companies.json +0 -2048
  121. package/integration/hubspot-test/create-hubspot.js +0 -523
  122. package/integration/hubspot-test/env.template +0 -4
  123. package/integration/hubspot-test/hubspot-test-datasource-company.json +0 -138
  124. package/integration/hubspot-test/hubspot-test-datasource-contact.json +0 -146
  125. package/integration/hubspot-test/hubspot-test-datasource-deal.json +0 -146
  126. package/integration/hubspot-test/hubspot-test-datasource-users.json +0 -76
  127. package/integration/hubspot-test/hubspot-test-deploy.json +0 -2160
  128. package/integration/hubspot-test/hubspot-test-system.json +0 -74
  129. package/integration/hubspot-test/rbac.json +0 -166
  130. package/integration/hubspot-test/test-artifacts/wizard-hubspot-credential-real.yaml +0 -20
  131. package/integration/hubspot-test/test-artifacts/wizard-hubspot-env-vars.yaml +0 -9
  132. package/integration/hubspot-test/test-artifacts/wizard-invalid-add-datasource.yaml +0 -5
  133. package/integration/hubspot-test/test-artifacts/wizard-invalid-app-name.yaml +0 -5
  134. package/integration/hubspot-test/test-artifacts/wizard-invalid-credential-create.yaml +0 -7
  135. package/integration/hubspot-test/test-artifacts/wizard-invalid-credential-select.yaml +0 -7
  136. package/integration/hubspot-test/test-artifacts/wizard-invalid-known-platform.yaml +0 -4
  137. package/integration/hubspot-test/test-artifacts/wizard-invalid-missing-app.yaml +0 -4
  138. package/integration/hubspot-test/test-artifacts/wizard-invalid-missing-source.yaml +0 -2
  139. package/integration/hubspot-test/test-artifacts/wizard-invalid-mode.yaml +0 -5
  140. package/integration/hubspot-test/test-artifacts/wizard-invalid-openapi-file.yaml +0 -5
  141. package/integration/hubspot-test/test-artifacts/wizard-invalid-openapi-url.yaml +0 -4
  142. package/integration/hubspot-test/test-artifacts/wizard-invalid-source.yaml +0 -4
  143. package/integration/hubspot-test/test-artifacts/wizard-valid-for-dimension-array-test.yaml +0 -5
  144. package/integration/hubspot-test/test-artifacts/wizard-valid-for-dimension-key-test.yaml +0 -5
  145. package/integration/hubspot-test/test-artifacts/wizard-valid-for-dimension-path-test.yaml +0 -5
  146. package/integration/hubspot-test/test-artifacts/wizard-valid-for-dimension-test.yaml +0 -5
  147. package/integration/hubspot-test/test-artifacts/wizard-valid-for-rbac-test.yaml +0 -5
  148. package/integration/hubspot-test/test-artifacts/wizard-valid-for-rbac-yaml-test.yaml +0 -5
  149. package/integration/hubspot-test/test-dataplane-down-helpers.js +0 -249
  150. package/integration/hubspot-test/test-dataplane-down-tests.js +0 -386
  151. package/integration/hubspot-test/test-dataplane-down.js +0 -209
  152. package/integration/hubspot-test/test.js +0 -1588
  153. package/integration/hubspot-test/wizard-hubspot-e2e.yaml +0 -16
  154. package/integration/hubspot-test/wizard-hubspot-platform.yaml +0 -8
  155. package/integration/hubspot-test/wizard-hubspot-test-headless.yaml +0 -23
  156. package/integration/roundtrip-test-local/README.md +0 -143
  157. package/integration/roundtrip-test-local/application.yaml +0 -13
  158. package/integration/roundtrip-test-local/env.template +0 -15
  159. package/integration/roundtrip-test-local/roundtrip-test-local-datasource-roundtrip-test-company.yaml +0 -14
  160. package/integration/roundtrip-test-local/roundtrip-test-local-deploy.json +0 -61
  161. package/integration/roundtrip-test-local/roundtrip-test-local-system.yaml +0 -25
  162. package/integration/roundtrip-test-local2/README.md +0 -143
  163. package/integration/roundtrip-test-local2/application.yaml +0 -13
  164. package/integration/roundtrip-test-local2/env.template +0 -15
  165. package/integration/roundtrip-test-local2/roundtrip-test-local2-datasource-company.yaml +0 -31
  166. package/integration/roundtrip-test-local2/roundtrip-test-local2-deploy.json +0 -86
  167. package/integration/roundtrip-test-local2/roundtrip-test-local2-system.yaml +0 -25
  168. package/integration/test/wizard.yaml +0 -8
  169. package/jest.config.coverage.js +0 -37
  170. package/jest.config.default.js +0 -11
  171. package/jest.config.integration.fixtures.js +0 -22
  172. package/jest.config.integration.js +0 -33
  173. package/jest.config.isolated.js +0 -11
  174. package/jest.config.manual.js +0 -30
  175. package/jest.global-live-fabrix-hooks.js +0 -36
  176. package/jest.isolated-projects.js +0 -283
  177. package/jest.projects.js +0 -79
  178. package/lib/integration-definition/openapi-enrichment/hubspot-crm-companies.json +0 -205
  179. package/packages/builder-api/README.md +0 -120
  180. package/packages/builder-api/package.json +0 -30
  181. package/packages/builder-api/src/app.module.ts +0 -7
  182. package/packages/builder-api/src/auth/auth.module.ts +0 -5
  183. package/packages/builder-api/src/external-system/external-system.module.ts +0 -5
  184. package/packages/builder-api/src/main.ts +0 -16
  185. package/packages/builder-api/tsconfig.json +0 -15
  186. package/scripts/check-builder-api-architecture-grep.js +0 -79
  187. package/scripts/check-datasource-test-run-schema-sync.js +0 -34
  188. package/scripts/ci-fix.sh +0 -19
  189. package/scripts/ci-simulate.sh +0 -19
  190. package/scripts/diagnose-cli.js +0 -150
  191. package/scripts/docker_image_content_gate.js +0 -47
  192. package/scripts/docker_image_content_gate.sh +0 -5
  193. package/scripts/export-builder-api-evidence.js +0 -262
  194. package/scripts/install-local.js +0 -490
  195. package/scripts/lib/builder-api-docker-staging-copy.js +0 -60
  196. package/scripts/lib/builder-api-docker-staging-core.js +0 -183
  197. package/scripts/lib/docker-image-content-gate-core.js +0 -166
  198. package/scripts/lib/plan-actor.js +0 -109
  199. package/scripts/lib/plan-config.js +0 -20
  200. package/scripts/lib/plan-gates.js +0 -154
  201. package/scripts/lib/plan-meta.js +0 -104
  202. package/scripts/lib/plan-parse.js +0 -162
  203. package/scripts/lib/plan-prevalidate-gates.js +0 -337
  204. package/scripts/lib/plan-run-core.js +0 -125
  205. package/scripts/lib/plan-run-report.js +0 -119
  206. package/scripts/plan-approve.js +0 -24
  207. package/scripts/plan-archive.js +0 -49
  208. package/scripts/plan-run.js +0 -13
  209. package/scripts/plan-set-actor.js +0 -21
  210. package/scripts/plan-validate.js +0 -80
  211. package/scripts/pnpm/af.mjs +0 -125
  212. package/scripts/pnpm/check-quiet.mjs +0 -32
  213. package/scripts/pnpm/dev-install.mjs +0 -29
  214. package/scripts/pnpm/dev-reload.mjs +0 -18
  215. package/scripts/pnpm/help.mjs +0 -55
  216. package/scripts/pnpm/lib/dev-reload-core.mjs +0 -189
  217. package/scripts/pnpm/lib/dev-reload-log-capture.mjs +0 -67
  218. package/scripts/pnpm/lib/dev-reload-log-utils.mjs +0 -32
  219. package/scripts/pnpm/lib/resolve-aifabrix-bin.mjs +0 -124
  220. package/scripts/pnpm/lib/resolve-app-url.mjs +0 -183
  221. package/scripts/pnpm/lib/spinner.mjs +0 -42
  222. package/scripts/pnpm-global-remove.js +0 -48
  223. package/scripts/stage_builder_api_for_docker.js +0 -65
  224. package/scripts/sync-builder-api-template.js +0 -44
  225. package/scripts/test-dataplane-bootstrap.js +0 -331
@@ -1,1588 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * HubSpot External System Wizard E2E Test Runner
4
- *
5
- * @fileoverview Runs end-to-end wizard tests for HubSpot integration.
6
- * @author AI Fabrix Team
7
- * @version 2.0.0
8
- */
9
- /* eslint-disable max-lines */
10
- 'use strict';
11
-
12
- const fs = require('fs').promises;
13
- const fsSync = require('fs');
14
- const path = require('path');
15
- const { execFile } = require('child_process');
16
- const { promisify } = require('util');
17
- const yaml = require('js-yaml');
18
- const chalk = require('chalk');
19
- const { getDeploymentAuth } = require('../../lib/utils/token-manager');
20
- const { discoverDataplaneUrl } = require('../../lib/commands/wizard-dataplane');
21
- const { resolveControllerUrl } = require('../../lib/utils/controller-url');
22
- const { resolveEnvironment } = require('../../lib/core/config');
23
-
24
- const execFileAsync = promisify(execFile);
25
-
26
- /** Single source for test config: integration/hubspot-test/.env */
27
- const HUBSPOT_DIR = path.join(process.cwd(), 'integration', 'hubspot-test');
28
- const LOCAL_ENV_PATH = path.join(HUBSPOT_DIR, '.env');
29
- const ARTIFACT_DIR = path.join(HUBSPOT_DIR, 'test-artifacts');
30
- const MAX_OUTPUT_BYTES = 10 * 1024 * 1024;
31
- const COMMAND_TIMEOUT_MS = 10 * 60 * 1000;
32
-
33
- /**
34
- * Error class for skipping tests
35
- * @class SkipTestError
36
- * @extends Error
37
- */
38
- class SkipTestError extends Error {
39
- /**
40
- * Creates a SkipTestError
41
- * @param {string} message - Error message
42
- */
43
- constructor(message) {
44
- super(message);
45
- this.name = 'SkipTestError';
46
- }
47
- }
48
-
49
- /**
50
- * Parses command line arguments
51
- * @function parseArgs
52
- * @param {string[]} argv - Command line arguments
53
- * @returns {Object} Parsed arguments object with tests, types, verbose, keepArtifacts, help
54
- */
55
- function parseArgs(argv) {
56
- const args = {
57
- tests: [],
58
- types: [],
59
- verbose: false,
60
- keepArtifacts: false,
61
- help: false
62
- };
63
- for (let i = 2; i < argv.length; i += 1) {
64
- const arg = argv[i];
65
- if (arg === '--test' && argv[i + 1]) {
66
- args.tests = argv[i + 1].split(',').map(item => item.trim()).filter(Boolean);
67
- i += 1;
68
- continue;
69
- }
70
- if (arg === '--type' && argv[i + 1]) {
71
- args.types = argv[i + 1].split(',').map(item => item.trim()).filter(Boolean);
72
- i += 1;
73
- continue;
74
- }
75
- if (arg === '--verbose') {
76
- args.verbose = true;
77
- continue;
78
- }
79
- if (arg === '--keep-artifacts') {
80
- args.keepArtifacts = true;
81
- continue;
82
- }
83
- if (arg === '--help' || arg === '-h') {
84
- args.help = true;
85
- }
86
- }
87
- return args;
88
- }
89
-
90
- /**
91
- * Prints usage information
92
- * @function printUsage
93
- * @returns {void}
94
- */
95
- function printUsage() {
96
- // eslint-disable-next-line no-console
97
- console.log([
98
- 'Usage:',
99
- ' node integration/hubspot-test/test.js',
100
- ' node integration/hubspot-test/test.js --test "1.1"',
101
- ' node integration/hubspot-test/test.js --type positive',
102
- ' node integration/hubspot-test/test.js --type negative --verbose',
103
- '',
104
- 'Options:',
105
- ' --test <id[,id]> Run specific test IDs',
106
- ' --type <type[,type]> Filter by type (positive|negative|real-data)',
107
- ' --verbose Verbose command output',
108
- ' --keep-artifacts Keep generated test artifacts',
109
- ' -h, --help Show help'
110
- ].join('\n'));
111
- }
112
-
113
- /**
114
- * Logs info message
115
- * @function logInfo
116
- * @param {string} message - Message to log
117
- * @returns {void}
118
- */
119
- function logInfo(message) {
120
- // eslint-disable-next-line no-console
121
- console.log(chalk.cyan(message));
122
- }
123
-
124
- /**
125
- * Logs success message
126
- * @function logSuccess
127
- * @param {string} message - Message to log
128
- * @returns {void}
129
- */
130
- function logSuccess(message) {
131
- // eslint-disable-next-line no-console
132
- console.log(chalk.green(message));
133
- }
134
-
135
- /**
136
- * Logs warning message
137
- * @function logWarn
138
- * @param {string} message - Message to log
139
- * @returns {void}
140
- */
141
- function logWarn(message) {
142
- // eslint-disable-next-line no-console
143
- console.warn(chalk.yellow(message));
144
- }
145
-
146
- /**
147
- * Logs error message
148
- * @function logError
149
- * @param {string} message - Message to log
150
- * @returns {void}
151
- */
152
- function logError(message) {
153
- // eslint-disable-next-line no-console
154
- console.error(chalk.red(message));
155
- }
156
-
157
- /**
158
- * Ensures directory exists
159
- * @async
160
- * @function ensureDir
161
- * @param {string} dirPath - Directory path
162
- * @returns {Promise<void>} Resolves when directory is created
163
- */
164
- async function ensureDir(dirPath) {
165
- await fs.mkdir(dirPath, { recursive: true });
166
- }
167
-
168
- /**
169
- * Checks if file exists
170
- * @async
171
- * @function fileExists
172
- * @param {string} filePath - File path to check
173
- * @returns {Promise<boolean>} True if file exists
174
- */
175
- async function fileExists(filePath) {
176
- try {
177
- await fs.access(filePath);
178
- return true;
179
- } catch {
180
- return false;
181
- }
182
- }
183
-
184
- /**
185
- * Parses environment file content
186
- * @function parseEnvFile
187
- * @param {string} content - Environment file content
188
- * @returns {Object<string, string>} Parsed environment variables object
189
- */
190
- function parseEnvFile(content) {
191
- const envVars = {};
192
- const lines = content.split(/\r?\n/);
193
- for (const line of lines) {
194
- const trimmed = line.trim();
195
- if (!trimmed || trimmed.startsWith('#')) {
196
- continue;
197
- }
198
- const eqIndex = trimmed.indexOf('=');
199
- if (eqIndex === -1) {
200
- continue;
201
- }
202
- const key = trimmed.slice(0, eqIndex).trim();
203
- let value = trimmed.slice(eqIndex + 1).trim();
204
- if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith('\'') && value.endsWith('\''))) {
205
- value = value.slice(1, -1);
206
- }
207
- envVars[key] = value;
208
- }
209
- return envVars;
210
- }
211
-
212
- /**
213
- * Stable stringify for comparison
214
- * @function stableStringify
215
- * @param {*} value - Value to stringify
216
- * @returns {string} Stable JSON string
217
- */
218
- function stableStringify(value) {
219
- if (Array.isArray(value)) {
220
- return `[${value.map(item => stableStringify(item)).join(',')}]`;
221
- }
222
- if (value && typeof value === 'object') {
223
- const keys = Object.keys(value).sort();
224
- return `{${keys.map(key => `${JSON.stringify(key)}:${stableStringify(value[key])}`).join(',')}}`;
225
- }
226
- return JSON.stringify(value);
227
- }
228
-
229
- /**
230
- * Normalizes file content for comparisons
231
- * @function normalizeFileContent
232
- * @param {string} filePath - File path
233
- * @param {string} content - File content
234
- * @returns {string} Normalized content
235
- */
236
- function normalizeFileContent(filePath, content) {
237
- const ext = path.extname(filePath).toLowerCase();
238
- if (ext === '.json') {
239
- return stableStringify(JSON.parse(content));
240
- }
241
- if (ext === '.yaml' || ext === '.yml') {
242
- return stableStringify(yaml.load(content));
243
- }
244
- return content.trim();
245
- }
246
-
247
- /**
248
- * Loads environment file
249
- * @async
250
- * @function loadEnvFile
251
- * @param {string} envPath - Path to environment file
252
- * @param {Object} options - Options object
253
- * @param {boolean} options.verbose - Verbose logging flag
254
- * @returns {Promise<void>} Resolves when environment file is loaded
255
- */
256
- async function loadEnvFile(envPath, options) {
257
- const exists = await fileExists(envPath);
258
- if (!exists) {
259
- logWarn(`Env file not found: ${envPath}`);
260
- return;
261
- }
262
- const content = await fs.readFile(envPath, 'utf8');
263
- const parsed = parseEnvFile(content);
264
- for (const [key, value] of Object.entries(parsed)) {
265
- if (process.env[key] === undefined) {
266
- process.env[key] = value;
267
- }
268
- }
269
- // Map common .env keys so tests and wizard use credentials from integration/hubspot-test/.env
270
- if (process.env.CLIENTID && process.env.HUBSPOT_CLIENT_ID === undefined) {
271
- process.env.HUBSPOT_CLIENT_ID = process.env.CLIENTID;
272
- }
273
- if (process.env.CLIENTSECRET && process.env.HUBSPOT_CLIENT_SECRET === undefined) {
274
- process.env.HUBSPOT_CLIENT_SECRET = process.env.CLIENTSECRET;
275
- }
276
- if (options.verbose) {
277
- logInfo(`Loaded env vars from: ${envPath}`);
278
- }
279
- }
280
-
281
- /**
282
- * Load test config (controller, environment, dataplane, openapi file).
283
- * Reads integration/hubspot-test/.env; missing CONTROLLER_URL/ENVIRONMENT fall back to
284
- * the same resolution as the CLI (af auth status) so tests use the same controller.
285
- * @async
286
- * @function loadTestConfigFromEnv
287
- * @returns {Promise<Object>} Context with controllerUrl, environment, dataplaneUrl, openapiFile
288
- */
289
- async function loadTestConfigFromEnv() {
290
- const defaults = {
291
- DATAPLANE_URL: '',
292
- HUBSPOT_OPENAPI_FILE: path.join(HUBSPOT_DIR, 'companies.json')
293
- };
294
- let parsed = {};
295
- try {
296
- if (fsSync.existsSync(LOCAL_ENV_PATH)) {
297
- const content = await fs.readFile(LOCAL_ENV_PATH, 'utf8');
298
- parsed = parseEnvFile(content);
299
- }
300
- } catch (error) {
301
- logWarn(`Could not read ${LOCAL_ENV_PATH}: ${error.message}`);
302
- }
303
- const controllerUrl = parsed.CONTROLLER_URL
304
- ? parsed.CONTROLLER_URL.trim()
305
- : await resolveControllerUrl();
306
- const environment = parsed.ENVIRONMENT
307
- ? parsed.ENVIRONMENT.trim()
308
- : await resolveEnvironment();
309
- return {
310
- controllerUrl,
311
- environment,
312
- dataplaneUrl: parsed.DATAPLANE_URL || defaults.DATAPLANE_URL,
313
- openapiFile: parsed.HUBSPOT_OPENAPI_FILE || defaults.HUBSPOT_OPENAPI_FILE
314
- };
315
- }
316
-
317
- /**
318
- * Ensure dataplane URL is available for tests
319
- * @async
320
- * @function ensureDataplaneUrl
321
- * @param {Object} context - Test context
322
- * @param {string} appName - Application name
323
- * @returns {Promise<string>} Dataplane URL
324
- */
325
- async function ensureDataplaneUrl(context, appName) {
326
- if (context.dataplaneUrl) {
327
- return context.dataplaneUrl;
328
- }
329
- const authConfig = await getDeploymentAuth(context.controllerUrl, context.environment, appName);
330
- const dataplaneUrl = await discoverDataplaneUrl(context.controllerUrl, context.environment, authConfig);
331
- context.dataplaneUrl = dataplaneUrl;
332
- return dataplaneUrl;
333
- }
334
-
335
- /**
336
- * Ensures environment variable is set
337
- * @function ensureEnvVar
338
- * @param {string} name - Variable name
339
- * @param {string} value - Variable value
340
- * @returns {void}
341
- */
342
- function ensureEnvVar(name, value) {
343
- if (process.env[name] === undefined && value !== undefined && value !== null && value !== '') {
344
- process.env[name] = value;
345
- }
346
- }
347
-
348
- /**
349
- * Requires environment variables to be set
350
- * @function requireEnvVars
351
- * @param {string[]} names - Array of variable names
352
- * @returns {void}
353
- * @throws {SkipTestError} If any required variable is missing
354
- */
355
- function requireEnvVars(names) {
356
- const missing = names.filter(name => !process.env[name]);
357
- if (missing.length > 0) {
358
- throw new SkipTestError(`Missing required env vars: ${missing.join(', ')}`);
359
- }
360
- }
361
-
362
- /**
363
- * Runs a command
364
- * @async
365
- * @function runCommand
366
- * @param {string} command - Command to run
367
- * @param {string[]} args - Command arguments
368
- * @param {Object} options - Options object
369
- * @param {boolean} options.verbose - Verbose logging flag
370
- * @returns {Promise<Object>} Command result object with success, stdout, stderr
371
- */
372
- async function runCommand(command, args, options) {
373
- try {
374
- const result = await execFileAsync(command, args, {
375
- cwd: process.cwd(),
376
- env: { ...process.env },
377
- maxBuffer: MAX_OUTPUT_BYTES,
378
- timeout: COMMAND_TIMEOUT_MS
379
- });
380
- if (options.verbose) {
381
- if (result.stdout) {
382
- logInfo(result.stdout.trim());
383
- }
384
- if (result.stderr) {
385
- logWarn(result.stderr.trim());
386
- }
387
- }
388
- return { success: true, stdout: result.stdout || '', stderr: result.stderr || '' };
389
- } catch (error) {
390
- const stdout = error.stdout || '';
391
- const stderr = error.stderr || '';
392
- if (options.verbose) {
393
- if (stdout) {
394
- logInfo(stdout.trim());
395
- }
396
- if (stderr) {
397
- logWarn(stderr.trim());
398
- }
399
- }
400
- return { success: false, stdout, stderr, error };
401
- }
402
- }
403
-
404
- /**
405
- * Validates authentication status
406
- * @async
407
- * @function validateAuth
408
- * @param {Object} context - Test context
409
- * @param {string} context.controllerUrl - Controller URL
410
- * @param {string} context.environment - Environment name
411
- * @param {Object} options - Options object
412
- * @returns {Promise<void>} Resolves when authentication is validated
413
- * @throws {Error} If authentication fails
414
- */
415
- async function validateAuth(context, options) {
416
- logInfo('Validating authentication status...');
417
- const configArgs = [
418
- 'bin/aifabrix.js',
419
- 'auth',
420
- 'config',
421
- '--set-controller',
422
- context.controllerUrl,
423
- '--set-environment',
424
- context.environment
425
- ];
426
- await runCommand('node', configArgs, options);
427
- const args = ['bin/aifabrix.js', 'auth', 'status'];
428
- const result = await runCommand('node', args, options);
429
- if (!result.success) {
430
- throw new Error('Authentication check failed. Run: node bin/aifabrix.js login --controller <url> --method device');
431
- }
432
- const output = `${result.stdout}\n${result.stderr}`;
433
- // Only treat "Not authenticated" as failure; "✖ Not reachable" (dataplane) is not auth failure
434
- if (output.includes('Not authenticated')) {
435
- throw new Error('Not authenticated. Run: node bin/aifabrix.js login --controller <url> --method device');
436
- }
437
- logSuccess('Authentication validated.');
438
- }
439
-
440
- /**
441
- * Writes wizard configuration to file
442
- * @async
443
- * @function writeWizardConfig
444
- * @param {string} name - Configuration name
445
- * @param {Object} config - Configuration object
446
- * @returns {Promise<string>} Path to written configuration file
447
- */
448
- async function writeWizardConfig(name, config) {
449
- await ensureDir(ARTIFACT_DIR);
450
- const configPath = path.join(ARTIFACT_DIR, `${name}.yaml`);
451
- const serialized = yaml.dump(config, { lineWidth: -1, noRefs: true });
452
- await fs.writeFile(configPath, serialized, 'utf8');
453
- return configPath;
454
- }
455
-
456
- /**
457
- * Lists files in a directory recursively
458
- * @async
459
- * @function listFilesRecursive
460
- * @param {string} dirPath - Directory path
461
- * @param {string} [prefix=''] - Prefix for file paths
462
- * @returns {Promise<string[]>} Array of file paths
463
- */
464
- async function listFilesRecursive(dirPath, prefix = '') {
465
- const files = [];
466
- try {
467
- const entries = await fs.readdir(dirPath, { withFileTypes: true });
468
- for (const entry of entries) {
469
- const fullPath = path.join(dirPath, entry.name);
470
- const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name;
471
- if (entry.isDirectory()) {
472
- const subFiles = await listFilesRecursive(fullPath, relativePath);
473
- files.push(...subFiles);
474
- } else {
475
- files.push(relativePath);
476
- }
477
- }
478
- } catch (error) {
479
- // Ignore errors, return empty array
480
- }
481
- return files;
482
- }
483
-
484
- /**
485
- * Checks if application directory exists
486
- * @async
487
- * @function checkAppDirectory
488
- * @param {string} appPath - Application directory path
489
- * @returns {Promise<string[]>} Directory entries
490
- * @throws {Error} If directory doesn't exist or can't be read
491
- */
492
- async function checkAppDirectory(appPath) {
493
- const dirExists = await fileExists(appPath);
494
- if (!dirExists) {
495
- throw new Error(`Application directory does not exist: ${appPath}. Wizard may have failed to create files.`);
496
- }
497
- try {
498
- return await fs.readdir(appPath);
499
- } catch (error) {
500
- throw new Error(`Failed to read application directory ${appPath}: ${error.message}`);
501
- }
502
- }
503
-
504
- /**
505
- * Resolves application config path (application.json or application.yaml)
506
- * @param {string} appPath - Application directory path
507
- * @returns {string|null} Path to config file or null if neither exists
508
- */
509
- async function resolveApplicationConfigPath(appPath) {
510
- const jsonPath = path.join(appPath, 'application.json');
511
- const yamlPath = path.join(appPath, 'application.yaml');
512
- if (await fileExists(jsonPath)) return jsonPath;
513
- if (await fileExists(yamlPath)) return yamlPath;
514
- return null;
515
- }
516
-
517
- /**
518
- * Validates required files exist (application config can be application.json or application.yaml)
519
- * @async
520
- * @function validateRequiredFiles
521
- * @param {string} appPath - Application directory path
522
- * @param {string[]} entries - Directory entries
523
- * @returns {Promise<void>} Resolves when all required files are validated
524
- * @throws {Error} If required files are missing
525
- */
526
- async function validateRequiredFiles(appPath, entries) {
527
- const applicationConfigPath = await resolveApplicationConfigPath(appPath);
528
- const requiredFiles = ['env.template', 'README.md', 'deploy.js'];
529
- const missingFiles = [];
530
- if (!applicationConfigPath) {
531
- missingFiles.push('application.yaml or application.json');
532
- }
533
- for (const fileName of requiredFiles) {
534
- const filePath = path.join(appPath, fileName);
535
- const exists = await fileExists(filePath);
536
- if (!exists) {
537
- missingFiles.push(fileName);
538
- }
539
- }
540
- if (missingFiles.length > 0) {
541
- const allFiles = await listFilesRecursive(appPath);
542
- throw new Error(
543
- `Missing required files in ${appPath}: ${missingFiles.join(', ')}\n` +
544
- 'Directory exists: true\n' +
545
- `Files found in directory: ${entries.length > 0 ? entries.join(', ') : 'none'}\n` +
546
- `All files (recursive): ${allFiles.length > 0 ? allFiles.join(', ') : 'none'}`
547
- );
548
- }
549
- }
550
-
551
- /**
552
- * Validates deploy JSON files exist
553
- * @function validateDeployFiles
554
- * @param {string} appPath - Application directory path
555
- * @param {string[]} entries - Directory entries
556
- * @returns {string[]} Deploy file names
557
- * @throws {Error} If no deploy files found
558
- */
559
- function validateDeployFiles(appPath, entries) {
560
- const deployFiles = entries.filter(file => /-deploy.*\.json$/.test(file));
561
- if (deployFiles.length === 0) {
562
- throw new Error(`No deploy JSON files found in: ${appPath}. Found files: ${entries.join(', ')}`);
563
- }
564
- return deployFiles;
565
- }
566
-
567
- /**
568
- * Validates file contents syntax
569
- * @async
570
- * @function validateFileContents
571
- * @param {string} appPath - Application directory path
572
- * @param {string[]} deployFiles - Deploy file names
573
- * @returns {Promise<void>} Resolves when all file contents are validated
574
- * @throws {Error} If file contents are invalid
575
- */
576
- async function validateFileContents(appPath, deployFiles) {
577
- const configPath = await resolveApplicationConfigPath(appPath);
578
- if (!configPath) {
579
- throw new Error('Application config (application.yaml or application.json) not found');
580
- }
581
- try {
582
- const content = await fs.readFile(configPath, 'utf8');
583
- if (configPath.endsWith('.json')) {
584
- JSON.parse(content);
585
- } else {
586
- yaml.load(content);
587
- }
588
- } catch (error) {
589
- throw new Error(`Invalid syntax in ${path.basename(configPath)}: ${error.message}`);
590
- }
591
- for (const fileName of deployFiles) {
592
- try {
593
- const fileContent = await fs.readFile(path.join(appPath, fileName), 'utf8');
594
- JSON.parse(fileContent);
595
- } catch (error) {
596
- throw new Error(`Invalid JSON syntax in ${fileName}: ${error.message}`);
597
- }
598
- }
599
- }
600
-
601
- /**
602
- * Validates generated files exist and are valid
603
- * @async
604
- * @function validateGeneratedFiles
605
- * @param {string} appName - Application name
606
- * @returns {Promise<void>} Resolves when all files are validated
607
- * @throws {Error} If required files are missing or invalid
608
- */
609
- async function validateGeneratedFiles(appName) {
610
- const appPath = path.join(process.cwd(), 'integration', appName);
611
- const entries = await checkAppDirectory(appPath);
612
- await validateRequiredFiles(appPath, entries);
613
- const deployFiles = validateDeployFiles(appPath, entries);
614
- await validateFileContents(appPath, deployFiles);
615
- }
616
-
617
- /**
618
- * Captures a snapshot of external system files for comparison
619
- * @async
620
- * @function captureExternalSnapshot
621
- * @param {string} appPath - Application directory path
622
- * @returns {Promise<Object>} Snapshot of file contents keyed by path
623
- */
624
- async function captureExternalSnapshot(appPath) {
625
- const configPath = await resolveApplicationConfigPath(appPath);
626
- if (!configPath) {
627
- throw new Error(`Application config not found in ${appPath}`);
628
- }
629
- const content = await fs.readFile(configPath, 'utf8');
630
- const variables = configPath.endsWith('.json') ? JSON.parse(content) : yaml.load(content);
631
-
632
- if (!variables || !variables.externalIntegration) {
633
- throw new Error(`externalIntegration block not found in ${configPath}`);
634
- }
635
-
636
- const fileNames = [
637
- path.basename(configPath),
638
- 'env.template',
639
- 'README.md',
640
- ...(variables.externalIntegration.systems || []),
641
- ...(variables.externalIntegration.dataSources || [])
642
- ];
643
-
644
- const rbacPath = path.join(appPath, 'rbac.yml');
645
- if (await fileExists(rbacPath)) {
646
- fileNames.push('rbac.yml');
647
- }
648
-
649
- const snapshot = {};
650
- for (const fileName of fileNames) {
651
- const filePath = path.join(appPath, fileName);
652
- if (!(await fileExists(filePath))) {
653
- throw new Error(`Expected file not found: ${filePath}`);
654
- }
655
- const content = await fs.readFile(filePath, 'utf8');
656
- snapshot[filePath] = normalizeFileContent(filePath, content);
657
- }
658
-
659
- return snapshot;
660
- }
661
-
662
- /**
663
- * Compares snapshots and throws on differences
664
- * @function compareSnapshots
665
- * @param {Object} before - Snapshot before
666
- * @param {Object} after - Snapshot after
667
- * @returns {void}
668
- */
669
- function compareSnapshots(before, after) {
670
- for (const [filePath, content] of Object.entries(before)) {
671
- if (!Object.prototype.hasOwnProperty.call(after, filePath)) {
672
- throw new Error(`File missing after split: ${filePath}`);
673
- }
674
- if (after[filePath] !== content) {
675
- throw new Error(`File content mismatch after split: ${filePath}`);
676
- }
677
- }
678
- }
679
-
680
- /**
681
- * Checks if app name is a test app name
682
- * @function isTestAppName
683
- * @param {string} appName - Application name
684
- * @returns {boolean} True if test app name
685
- */
686
- function isTestAppName(appName) {
687
- return appName.startsWith('wizard-e2e-');
688
- }
689
-
690
- /**
691
- * Cleans up app artifacts
692
- * @async
693
- * @function cleanupAppArtifacts
694
- * @param {string} appName - Application name
695
- * @param {Object} options - Options object
696
- * @param {boolean} options.keepArtifacts - Keep artifacts flag
697
- * @returns {Promise<void>} Resolves when cleanup is complete
698
- * @throws {Error} If app name is not a test app name
699
- */
700
- async function cleanupAppArtifacts(appName, options) {
701
- if (options.keepArtifacts) {
702
- return;
703
- }
704
- if (!isTestAppName(appName)) {
705
- throw new Error(`Refusing to delete non-test app directory: ${appName}`);
706
- }
707
- const appPath = path.join(process.cwd(), 'integration', appName);
708
- await fs.rm(appPath, { recursive: true, force: true });
709
- }
710
-
711
- /**
712
- * Runs wizard command
713
- * @async
714
- * @function runWizard
715
- * @param {string} configPath - Path to wizard config file
716
- * @param {Object} context - Test context
717
- * @param {Object} options - Options object
718
- * @returns {Promise<Object>} Command result object
719
- */
720
- async function runWizard(configPath, context, options) {
721
- const args = ['bin/aifabrix.js', 'wizard', '--config', configPath];
722
- return await runCommand('node', args, options);
723
- }
724
-
725
- /**
726
- * Builds command arguments for external system commands
727
- * @function buildExternalCommandArgs
728
- * @param {string} command - Command name (download, json, split-json)
729
- * @param {string} appName - Application name
730
- * @returns {string[]} Command arguments array
731
- */
732
- function buildExternalCommandArgs(command, appName) {
733
- const args = ['bin/aifabrix.js', command, appName];
734
- if (command !== 'download') {
735
- args.push('--type', 'external');
736
- }
737
- return args;
738
- }
739
-
740
- /**
741
- * Runs a command and handles errors, with special handling for "not found" cases
742
- * @async
743
- * @function runCommandWithErrorHandling
744
- * @param {string} command - Command description for error messages
745
- * @param {string[]} args - Command arguments
746
- * @param {Object} options - Options object
747
- * @param {string} [appName] - Application name (for skip logic)
748
- * @returns {Promise<Object>} Command result object
749
- * @throws {Error} If command fails (unless it's a "not found" case)
750
- */
751
- async function runCommandWithErrorHandling(command, args, options, appName) {
752
- const result = await runCommand('node', args, options);
753
- if (!result.success) {
754
- const errorOutput = `${result.stdout}\n${result.stderr}`;
755
- if (appName && (errorOutput.includes('not found') ||
756
- (errorOutput.includes('External system') && errorOutput.includes('not found')))) {
757
- logInfo(`System ${appName} not deployed on dataplane; download step omitted (optional).`);
758
- return { skipped: true };
759
- }
760
- throw new Error(`${command} failed: ${result.stderr || result.stdout}`);
761
- }
762
- return result;
763
- }
764
-
765
- /**
766
- * Downloads external system and validates split workflow
767
- * @async
768
- * @function testDownloadAndSplit
769
- * @param {string} appName - Application name
770
- * @param {Object} context - Test context
771
- * @param {Object} options - Options object
772
- * @returns {Promise<void>} Resolves when download and split validation succeeds
773
- */
774
- async function testDownloadAndSplit(appName, context, options) {
775
- logInfo(`Downloading external system ${appName}...`);
776
- const downloadArgs = buildExternalCommandArgs('download', appName);
777
- const downloadResult = await runCommandWithErrorHandling('Download', downloadArgs, options, appName);
778
- if (downloadResult.skipped) {
779
- return;
780
- }
781
-
782
- const appPath = path.join(process.cwd(), 'integration', appName);
783
- const snapshotBefore = await captureExternalSnapshot(appPath);
784
-
785
- logInfo('Generating application-schema.json...');
786
- const jsonArgs = buildExternalCommandArgs('json', appName);
787
- await runCommandWithErrorHandling('JSON generation', jsonArgs, options);
788
-
789
- logInfo('Splitting application-schema.json...');
790
- const splitArgs = buildExternalCommandArgs('split-json', appName);
791
- await runCommandWithErrorHandling('Split', splitArgs, options);
792
-
793
- const snapshotAfter = await captureExternalSnapshot(appPath);
794
- compareSnapshots(snapshotBefore, snapshotAfter);
795
- logSuccess('Download and split workflow validated.');
796
- }
797
-
798
- /**
799
- * Returns a skip message when the wizard cannot run due to environment (dataplane missing,
800
- * or dataplane rejecting the CLI token for wizard session).
801
- * @param {string} errorOutput - Combined stdout + stderr from wizard command
802
- * @returns {string|null} Skip message or null
803
- */
804
- function getWizardEnvironmentSkipMessage(errorOutput) {
805
- if (errorOutput.includes('Failed to discover dataplane URL') ||
806
- errorOutput.includes('Application not found')) {
807
- return 'Dataplane service not found in environment. Deploy dataplane service to the controller.';
808
- }
809
- return null;
810
- }
811
-
812
- /**
813
- * Runs wizard and validates generated files
814
- * @async
815
- * @function runWizardAndValidate
816
- * @param {string} configPath - Path to wizard config file
817
- * @param {string} appName - Application name
818
- * @param {Object} context - Test context
819
- * @param {Object} options - Options object
820
- * @returns {Promise<void>} Resolves when wizard completes and files are validated
821
- * @throws {SkipTestError} If wizard fails due to environment (dataplane/auth)
822
- * @throws {Error} If wizard fails or validation fails
823
- */
824
- async function runWizardAndValidate(configPath, appName, context, options) {
825
- const result = await runWizard(configPath, context, options);
826
- if (!result.success) {
827
- const errorOutput = `${result.stdout}\n${result.stderr}`;
828
- const skipMsg = getWizardEnvironmentSkipMessage(errorOutput);
829
- if (skipMsg) {
830
- throw new SkipTestError(skipMsg);
831
- }
832
- throw new Error(`Wizard failed for ${appName}:\n${errorOutput}`);
833
- }
834
-
835
- // Wait a brief moment to ensure file system operations complete
836
- await new Promise(resolve => setTimeout(resolve, 100));
837
-
838
- // Validate files were created
839
- if (options.verbose) {
840
- logInfo(`Validating files for ${appName}...`);
841
- }
842
- await validateGeneratedFiles(appName);
843
-
844
- if (options.verbose) {
845
- const appPath = path.join(process.cwd(), 'integration', appName);
846
- const entries = await fs.readdir(appPath);
847
- logSuccess(`Files created successfully: ${entries.join(', ')}`);
848
- }
849
-
850
- await testDownloadAndSplit(appName, context, options);
851
-
852
- await cleanupAppArtifacts(appName, options);
853
- }
854
-
855
- /**
856
- * Runs wizard expecting failure
857
- * @async
858
- * @function runWizardExpectFailure
859
- * @param {string} configPath - Path to wizard config file
860
- * @param {Object} context - Test context
861
- * @param {Object} options - Options object
862
- * @param {string} [expectedMessage] - Expected error message
863
- * @returns {Promise<void>} Resolves when wizard fails as expected
864
- * @throws {Error} If wizard succeeds or expected message not found
865
- */
866
- async function runWizardExpectFailure(configPath, context, options, expectedMessage = null) {
867
- const result = await runWizard(configPath, context, options);
868
- if (result.success) {
869
- throw new Error('Expected wizard to fail, but it succeeded.');
870
- }
871
- if (expectedMessage) {
872
- const combined = `${result.stdout}\n${result.stderr}`;
873
- if (!combined.includes(expectedMessage)) {
874
- throw new Error(`Expected error message not found: ${expectedMessage}`);
875
- }
876
- }
877
- }
878
-
879
- /**
880
- * Checks if test case matches selection criteria
881
- * @function matchesSelection
882
- * @param {Object} testCase - Test case object
883
- * @param {string[]} selections - Selection criteria
884
- * @returns {boolean} True if matches
885
- */
886
- function matchesSelection(testCase, selections) {
887
- if (!selections || selections.length === 0) {
888
- return true;
889
- }
890
- const lowerSelections = selections.map(item => item.toLowerCase());
891
- const idMatch = lowerSelections.includes(testCase.id.toLowerCase());
892
- const nameMatch = lowerSelections.some(item => testCase.name.toLowerCase().includes(item));
893
- return idMatch || nameMatch;
894
- }
895
-
896
- /**
897
- * Checks if test case matches type criteria
898
- * @function matchesType
899
- * @param {Object} testCase - Test case object
900
- * @param {string[]} types - Type criteria
901
- * @returns {boolean} True if matches
902
- */
903
- function matchesType(testCase, types) {
904
- if (!types || types.length === 0) {
905
- return true;
906
- }
907
- return types.map(type => type.toLowerCase()).includes(testCase.type.toLowerCase());
908
- }
909
-
910
- /**
911
- * Builds positive test cases
912
- * @function buildPositiveTestCases
913
- * @param {Object} context - Test context
914
- * @returns {Array<Object>} Array of positive test case objects
915
- */
916
- // eslint-disable-next-line max-lines-per-function, require-jsdoc
917
- function buildPositiveTestCases(context) {
918
- return [
919
- {
920
- id: '1.1',
921
- type: 'positive',
922
- name: 'Complete wizard flow with OpenAPI file',
923
- run: async(options) => {
924
- const configPath = path.join(HUBSPOT_DIR, 'wizard-hubspot-e2e.yaml');
925
- if (!(await fileExists(configPath))) {
926
- throw new Error(`Missing config file: ${configPath}`);
927
- }
928
- if (!(await fileExists(context.openapiFile))) {
929
- throw new Error(`OpenAPI file not found: ${context.openapiFile}`);
930
- }
931
- ensureEnvVar('CONTROLLER_URL', context.controllerUrl);
932
- // Try to run wizard - if dataplane/auth fails, skip the test
933
- const result = await runWizard(configPath, context, options);
934
- if (!result.success) {
935
- const errorOutput = `${result.stdout}\n${result.stderr}`;
936
- const skipMsg = getWizardEnvironmentSkipMessage(errorOutput);
937
- if (skipMsg) {
938
- throw new SkipTestError(skipMsg);
939
- }
940
- throw new Error(`Wizard failed: ${errorOutput}`);
941
- }
942
- await validateGeneratedFiles('wizard-e2e-e2e');
943
- await cleanupAppArtifacts('wizard-e2e-e2e', options);
944
- }
945
- },
946
- {
947
- id: '1.2',
948
- type: 'positive',
949
- name: 'Wizard flow with known platform',
950
- run: async(options) => {
951
- const configPath = path.join(HUBSPOT_DIR, 'wizard-hubspot-platform.yaml');
952
- if (!(await fileExists(configPath))) {
953
- throw new Error(`Missing config file: ${configPath}`);
954
- }
955
- try {
956
- await ensureDataplaneUrl(context, 'wizard-e2e-platform');
957
- } catch (error) {
958
- throw new SkipTestError(`Dataplane service not found: ${error.message}`);
959
- }
960
- await runWizardAndValidate(configPath, 'wizard-e2e-platform', context, options);
961
- }
962
- },
963
- {
964
- id: '1.6',
965
- type: 'positive',
966
- name: 'Wizard flow with environment variables',
967
- run: async(options) => {
968
- let dataplaneUrl;
969
- try {
970
- dataplaneUrl = await ensureDataplaneUrl(context, 'wizard-e2e-env-vars');
971
- } catch (error) {
972
- throw new SkipTestError(`Dataplane service not found: ${error.message}`);
973
- }
974
- ensureEnvVar('CONTROLLER_URL', context.controllerUrl);
975
- ensureEnvVar('DATAPLANE_URL', dataplaneUrl);
976
- const configPath = await writeWizardConfig('wizard-hubspot-env-vars', {
977
- appName: 'wizard-e2e-env-vars',
978
- mode: 'create-system',
979
- source: {
980
- type: 'openapi-file',
981
- filePath: context.openapiFile
982
- },
983
- deployment: {
984
- controller: '${CONTROLLER_URL}',
985
- dataplane: '${DATAPLANE_URL}',
986
- environment: context.environment
987
- }
988
- });
989
- await runWizardAndValidate(configPath, 'wizard-e2e-env-vars', context, options);
990
- }
991
- }
992
- ];
993
- }
994
-
995
- /**
996
- * Builds real-data test cases
997
- * @function buildRealDataTestCases
998
- * @param {Object} context - Test context
999
- * @returns {Array<Object>} Array of real-data test cases
1000
- */
1001
- function buildRealDataTestCases(context) {
1002
- return [
1003
- {
1004
- id: '1.3',
1005
- type: 'real-data',
1006
- name: 'Wizard flow with real credential creation',
1007
- run: async(options) => {
1008
- if (!(await fileExists(context.openapiFile))) {
1009
- throw new Error(`OpenAPI file not found: ${context.openapiFile}`);
1010
- }
1011
- requireEnvVars(['HUBSPOT_CLIENT_ID', 'HUBSPOT_CLIENT_SECRET']);
1012
- ensureEnvVar('HUBSPOT_TOKEN_URL', 'https://api.hubapi.com/oauth/v1/token');
1013
- const configPath = await writeWizardConfig('wizard-hubspot-credential-real', {
1014
- appName: 'wizard-e2e-credential-real',
1015
- mode: 'create-system',
1016
- source: {
1017
- type: 'openapi-file',
1018
- filePath: context.openapiFile
1019
- },
1020
- credential: {
1021
- action: 'create',
1022
- config: {
1023
- key: 'wizard-e2e-cred-real',
1024
- displayName: 'HubSpot Test Credential (Real)',
1025
- type: 'OAUTH2',
1026
- config: {
1027
- tokenUrl: '${HUBSPOT_TOKEN_URL}',
1028
- clientId: '${HUBSPOT_CLIENT_ID}',
1029
- clientSecret: '${HUBSPOT_CLIENT_SECRET}',
1030
- scopes: [
1031
- 'crm.objects.companies.read',
1032
- 'crm.objects.companies.write',
1033
- 'crm.objects.contacts.read',
1034
- 'crm.objects.contacts.write'
1035
- ]
1036
- }
1037
- }
1038
- }
1039
- });
1040
- await runWizardAndValidate(configPath, 'wizard-e2e-credential-real', context, options);
1041
- }
1042
- }
1043
- ];
1044
- }
1045
-
1046
- /**
1047
- * Builds negative config validation test cases
1048
- * @function buildNegativeConfigTestCases
1049
- * @param {Object} context - Test context
1050
- * @returns {Array<Object>} Array of negative config test case objects
1051
- */
1052
- // eslint-disable-next-line max-lines-per-function, require-jsdoc
1053
- function buildNegativeConfigTestCases(context) {
1054
- return [
1055
- {
1056
- id: '2.1',
1057
- type: 'negative',
1058
- name: 'Invalid config missing appName',
1059
- run: async(options) => {
1060
- const configPath = await writeWizardConfig('wizard-invalid-missing-app', {
1061
- mode: 'create-system',
1062
- source: { type: 'known-platform', platform: 'hubspot' }
1063
- });
1064
- await runWizardExpectFailure(configPath, context, options, 'Missing required field: appName');
1065
- }
1066
- },
1067
- {
1068
- id: '2.2',
1069
- type: 'negative',
1070
- name: 'Invalid app name with uppercase',
1071
- run: async(options) => {
1072
- const configPath = await writeWizardConfig('wizard-invalid-app-name', {
1073
- appName: 'HubSpot-Test',
1074
- mode: 'create-system',
1075
- source: { type: 'known-platform', platform: 'hubspot' }
1076
- });
1077
- await runWizardExpectFailure(configPath, context, options, 'must match pattern');
1078
- }
1079
- },
1080
- {
1081
- id: '2.3',
1082
- type: 'negative',
1083
- name: 'Missing source block',
1084
- run: async(options) => {
1085
- const configPath = await writeWizardConfig('wizard-invalid-missing-source', {
1086
- appName: 'wizard-e2e-negative-missing-source',
1087
- mode: 'create-system'
1088
- });
1089
- await runWizardExpectFailure(configPath, context, options, 'Missing required field: source');
1090
- }
1091
- },
1092
- {
1093
- id: '2.4',
1094
- type: 'negative',
1095
- name: 'Invalid source type',
1096
- run: async(options) => {
1097
- const configPath = await writeWizardConfig('wizard-invalid-source', {
1098
- appName: 'wizard-e2e-negative-source',
1099
- mode: 'create-system',
1100
- source: { type: 'invalid-type' }
1101
- });
1102
- await runWizardExpectFailure(configPath, context, options, 'Allowed values');
1103
- }
1104
- },
1105
- {
1106
- id: '2.5',
1107
- type: 'negative',
1108
- name: 'Invalid mode',
1109
- run: async(options) => {
1110
- const configPath = await writeWizardConfig('wizard-invalid-mode', {
1111
- appName: 'wizard-e2e-negative-mode',
1112
- mode: 'invalid-mode',
1113
- source: { type: 'known-platform', platform: 'hubspot' }
1114
- });
1115
- await runWizardExpectFailure(configPath, context, options, 'Allowed values');
1116
- }
1117
- },
1118
- {
1119
- id: '2.6',
1120
- type: 'negative',
1121
- name: 'Known platform missing platform',
1122
- run: async(options) => {
1123
- const configPath = await writeWizardConfig('wizard-invalid-known-platform', {
1124
- appName: 'wizard-e2e-negative-platform',
1125
- mode: 'create-system',
1126
- source: { type: 'known-platform' }
1127
- });
1128
- await runWizardExpectFailure(configPath, context, options, 'Missing required field: platform');
1129
- }
1130
- },
1131
- {
1132
- id: '2.7',
1133
- type: 'negative',
1134
- name: 'Missing OpenAPI file path',
1135
- run: async(options) => {
1136
- const configPath = await writeWizardConfig('wizard-invalid-openapi-file', {
1137
- appName: 'wizard-e2e-negative-openapi',
1138
- mode: 'create-system',
1139
- source: { type: 'openapi-file', filePath: '/tmp/does-not-exist.json' }
1140
- });
1141
- await runWizardExpectFailure(configPath, context, options, 'OpenAPI file not found');
1142
- }
1143
- },
1144
- {
1145
- id: '2.8',
1146
- type: 'negative',
1147
- name: 'OpenAPI URL missing url',
1148
- run: async(options) => {
1149
- const configPath = await writeWizardConfig('wizard-invalid-openapi-url', {
1150
- appName: 'wizard-e2e-negative-openapi-url',
1151
- mode: 'create-system',
1152
- source: { type: 'openapi-url' }
1153
- });
1154
- await runWizardExpectFailure(configPath, context, options, 'Missing required field: url');
1155
- }
1156
- }
1157
- ];
1158
- }
1159
-
1160
- /**
1161
- * Builds negative credential and datasource test cases
1162
- * @function buildNegativeCredentialTestCases
1163
- * @param {Object} context - Test context
1164
- * @returns {Array<Object>} Array of negative credential test cases
1165
- */
1166
- function buildNegativeCredentialTestCases(context) {
1167
- return [
1168
- {
1169
- id: '2.9',
1170
- type: 'negative',
1171
- name: 'Add datasource missing systemIdOrKey',
1172
- run: async(options) => {
1173
- const configPath = await writeWizardConfig('wizard-invalid-add-datasource', {
1174
- appName: 'wizard-e2e-negative-add-datasource',
1175
- mode: 'add-datasource',
1176
- source: { type: 'known-platform', platform: 'hubspot' }
1177
- });
1178
- await runWizardExpectFailure(configPath, context, options, 'systemIdOrKey');
1179
- }
1180
- },
1181
- {
1182
- id: '2.10',
1183
- type: 'negative',
1184
- name: 'Credential select missing credentialIdOrKey',
1185
- run: async(options) => {
1186
- const configPath = await writeWizardConfig('wizard-invalid-credential-select', {
1187
- appName: 'wizard-e2e-negative-credential-select',
1188
- mode: 'create-system',
1189
- source: { type: 'known-platform', platform: 'hubspot' },
1190
- credential: { action: 'select' }
1191
- });
1192
- await runWizardExpectFailure(configPath, context, options, 'Missing required field: credentialIdOrKey');
1193
- }
1194
- },
1195
- {
1196
- id: '2.11',
1197
- type: 'negative',
1198
- name: 'Credential create missing config',
1199
- run: async(options) => {
1200
- const configPath = await writeWizardConfig('wizard-invalid-credential-create', {
1201
- appName: 'wizard-e2e-negative-credential-create',
1202
- mode: 'create-system',
1203
- source: { type: 'known-platform', platform: 'hubspot' },
1204
- credential: { action: 'create' }
1205
- });
1206
- await runWizardExpectFailure(configPath, context, options, 'Missing required field: config');
1207
- }
1208
- }
1209
- ];
1210
- }
1211
-
1212
- /**
1213
- * Runs validation command and expects failure
1214
- * @async
1215
- * @function runValidationExpectFailure
1216
- * @param {string} appName - Application name
1217
- * @param {Object} context - Test context
1218
- * @param {Object} options - Options object
1219
- * @param {string|string[]} expectedMessage - Expected error substring(s); any match passes
1220
- * @returns {Promise<void>} Resolves when validation fails as expected
1221
- * @throws {Error} If validation succeeds or expected message not found
1222
- */
1223
- async function runValidationExpectFailure(appName, context, options, expectedMessage = null) {
1224
- const validateArgs = ['bin/aifabrix.js', 'validate', appName];
1225
- const result = await runCommand('node', validateArgs, options);
1226
- if (result.success) {
1227
- throw new Error('Expected validation to fail, but it succeeded.');
1228
- }
1229
- if (expectedMessage) {
1230
- const combined = `${result.stdout}\n${result.stderr}`;
1231
- const needles = Array.isArray(expectedMessage) ? expectedMessage : [expectedMessage];
1232
- const found = needles.some(n => combined.includes(n));
1233
- if (!found) {
1234
- throw new Error(
1235
- `Expected error message not found (any of): ${needles.join(' | ')}\nActual output: ${combined}`
1236
- );
1237
- }
1238
- }
1239
- }
1240
-
1241
- /**
1242
- * Creates a system via wizard for negative testing
1243
- * @async
1244
- * @function createSystemForNegativeTest
1245
- * @param {string} appName - Application name
1246
- * @param {string} configName - Configuration name
1247
- * @param {Object} context - Test context
1248
- * @param {Object} options - Options object
1249
- * @returns {Promise<string>} Application path
1250
- * @throws {SkipTestError} If wizard fails
1251
- */
1252
- async function createSystemForNegativeTest(appName, configName, context, options) {
1253
- const configPath = await writeWizardConfig(configName, {
1254
- appName: appName,
1255
- mode: 'create-system',
1256
- source: { type: 'known-platform', platform: 'hubspot' }
1257
- });
1258
- const wizardResult = await runWizard(configPath, context, options);
1259
- if (!wizardResult.success) {
1260
- const errorOutput = `${wizardResult.stdout}\n${wizardResult.stderr}`;
1261
- const skipMsg = getWizardEnvironmentSkipMessage(errorOutput);
1262
- throw new SkipTestError(skipMsg || `Wizard failed to create system: ${wizardResult.stderr}`);
1263
- }
1264
- await new Promise(resolve => setTimeout(resolve, 200));
1265
- return path.join(process.cwd(), 'integration', appName);
1266
- }
1267
-
1268
- /**
1269
- * Corrupts system file with invalid permission referencing non-existent role
1270
- * @async
1271
- * @function corruptSystemFileWithInvalidRole
1272
- * @param {string} appPath - Application path
1273
- * @returns {Promise<void>} Resolves when file is corrupted
1274
- */
1275
- async function corruptSystemFileWithInvalidRole(appPath) {
1276
- const systemFiles = await fs.readdir(appPath);
1277
- const systemFile = systemFiles.find(f => f.includes('-system.json'));
1278
- if (!systemFile) {
1279
- throw new Error(`System file not found in ${appPath}`);
1280
- }
1281
- const systemFilePath = path.join(appPath, systemFile);
1282
- const systemContent = await fs.readFile(systemFilePath, 'utf8');
1283
- const systemJson = JSON.parse(systemContent);
1284
- if (!systemJson.permissions) {
1285
- systemJson.permissions = [];
1286
- }
1287
- systemJson.permissions.push({
1288
- name: 'test:invalid',
1289
- roles: ['non-existent-role'],
1290
- description: 'Test permission'
1291
- });
1292
- await fs.writeFile(systemFilePath, JSON.stringify(systemJson, null, 2), 'utf8');
1293
- }
1294
-
1295
- /**
1296
- * Corrupts rbac.yaml with invalid YAML
1297
- * @async
1298
- * @function corruptRbacFile
1299
- * @param {string} appPath - Application path
1300
- * @returns {Promise<void>} Resolves when file is corrupted
1301
- */
1302
- async function corruptRbacFile(appPath) {
1303
- const rbacPath = path.join(appPath, 'rbac.yaml');
1304
- await fs.writeFile(rbacPath, 'invalid: yaml: syntax: [', 'utf8');
1305
- }
1306
-
1307
- /**
1308
- * Gets first datasource file path
1309
- * @async
1310
- * @function getFirstDatasourcePath
1311
- * @param {string} appPath - Application path
1312
- * @returns {Promise<string>} Datasource file path
1313
- * @throws {Error} If no datasource files found
1314
- */
1315
- async function getFirstDatasourcePath(appPath) {
1316
- const datasourceFiles = (await fs.readdir(appPath)).filter(f => f.includes('-datasource-'));
1317
- if (datasourceFiles.length === 0) {
1318
- throw new Error(`No datasource files found in ${appPath}`);
1319
- }
1320
- return path.join(appPath, datasourceFiles[0]);
1321
- }
1322
-
1323
- /**
1324
- * Corrupts root v2.4 ``dimensions``: local binding without required ``field`` (schema failure).
1325
- * @async
1326
- * @function corruptDatasourceLocalBindingMissingField
1327
- * @param {string} datasourcePath - Datasource file path
1328
- * @returns {Promise<void>} Resolves when file is corrupted
1329
- */
1330
- async function corruptDatasourceLocalBindingMissingField(datasourcePath) {
1331
- const datasourceContent = await fs.readFile(datasourcePath, 'utf8');
1332
- const datasourceJson = JSON.parse(datasourceContent);
1333
- datasourceJson.dimensions = { orphan_local: { type: 'local' } };
1334
- await fs.writeFile(datasourcePath, JSON.stringify(datasourceJson, null, 2), 'utf8');
1335
- }
1336
-
1337
- /**
1338
- * Corrupts datasource file with invalid dimension key
1339
- * @async
1340
- * @function corruptDatasourceInvalidDimensionKey
1341
- * @param {string} datasourcePath - Datasource file path
1342
- * @returns {Promise<void>} Resolves when file is corrupted
1343
- */
1344
- async function corruptDatasourceInvalidDimensionKey(datasourcePath) {
1345
- const datasourceContent = await fs.readFile(datasourcePath, 'utf8');
1346
- const datasourceJson = JSON.parse(datasourceContent);
1347
- datasourceJson.dimensions = {
1348
- 'invalid-key-with-hyphen': { type: 'local', field: 'id' }
1349
- };
1350
- await fs.writeFile(datasourcePath, JSON.stringify(datasourceJson, null, 2), 'utf8');
1351
- }
1352
-
1353
- /**
1354
- * Corrupts datasource file with invalid attribute path
1355
- * @async
1356
- * @function corruptDatasourceInvalidAttributePath
1357
- * @param {string} datasourcePath - Datasource file path
1358
- * @returns {Promise<void>} Resolves when file is corrupted
1359
- */
1360
- async function corruptDatasourceInvalidAttributePath(datasourcePath) {
1361
- const datasourceContent = await fs.readFile(datasourcePath, 'utf8');
1362
- const datasourceJson = JSON.parse(datasourceContent);
1363
- datasourceJson.dimensions = {
1364
- valid_key: { type: 'local', field: 'id-with-invalid-chars@' }
1365
- };
1366
- await fs.writeFile(datasourcePath, JSON.stringify(datasourceJson, null, 2), 'utf8');
1367
- }
1368
-
1369
- /**
1370
- * Corrupts root ``dimensions`` with a non-object JSON value (schema type error).
1371
- * @async
1372
- * @function corruptDatasourceDimensionsWrongJsonType
1373
- * @param {string} datasourcePath - Datasource file path
1374
- * @returns {Promise<void>} Resolves when file is corrupted
1375
- */
1376
- async function corruptDatasourceDimensionsWrongJsonType(datasourcePath) {
1377
- const datasourceContent = await fs.readFile(datasourcePath, 'utf8');
1378
- const datasourceJson = JSON.parse(datasourceContent);
1379
- datasourceJson.dimensions = ['invalid', 'array', 'format'];
1380
- await fs.writeFile(datasourcePath, JSON.stringify(datasourceJson, null, 2), 'utf8');
1381
- }
1382
-
1383
- /**
1384
- * Builds negative RBAC validation test cases
1385
- * @function buildNegativeRbacTestCases
1386
- * @param {Object} context - Test context
1387
- * @returns {Array<Object>} Array of negative RBAC test cases
1388
- */
1389
- function buildNegativeRbacTestCases(context) {
1390
- return [
1391
- {
1392
- id: '2.12',
1393
- type: 'negative',
1394
- name: 'RBAC missing role referenced in permissions',
1395
- run: async(options) => {
1396
- const appName = 'wizard-e2e-negative-rbac-missing-role';
1397
- const appPath = await createSystemForNegativeTest(appName, 'wizard-valid-for-rbac-test', context, options);
1398
- await corruptSystemFileWithInvalidRole(appPath);
1399
- await runValidationExpectFailure(appName, context, options, 'references role "non-existent-role" which does not exist');
1400
- await cleanupAppArtifacts(appName, options);
1401
- }
1402
- },
1403
- {
1404
- id: '2.13',
1405
- type: 'negative',
1406
- name: 'RBAC invalid YAML syntax',
1407
- run: async(options) => {
1408
- const appName = 'wizard-e2e-negative-rbac-invalid-yaml';
1409
- const appDir = path.join(process.cwd(), 'integration', appName);
1410
- if (fsSync.existsSync(appDir)) {
1411
- await fs.rm(appDir, { recursive: true, force: true });
1412
- }
1413
- const appPath = await createSystemForNegativeTest(appName, 'wizard-valid-for-rbac-yaml-test', context, options);
1414
- await corruptRbacFile(appPath);
1415
- await runValidationExpectFailure(appName, context, options, [
1416
- 'Invalid syntax in rbac.yaml',
1417
- 'Invalid YAML syntax'
1418
- ]);
1419
- await cleanupAppArtifacts(appName, options);
1420
- }
1421
- }
1422
- ];
1423
- }
1424
-
1425
- /**
1426
- * Builds negative dimension validation test cases
1427
- * @function buildNegativeDimensionTestCases
1428
- * @param {Object} context - Test context
1429
- * @returns {Array<Object>} Array of negative dimension test cases
1430
- */
1431
- function buildNegativeDimensionTestCases(context) {
1432
- const createTestCase = (id, name, appName, configName, corruptFn, expectedError) => ({
1433
- id,
1434
- type: 'negative',
1435
- name,
1436
- run: async(options) => {
1437
- const appPath = await createSystemForNegativeTest(appName, configName, context, options);
1438
- const datasourcePath = await getFirstDatasourcePath(appPath);
1439
- await corruptFn(datasourcePath);
1440
- await runValidationExpectFailure(appName, context, options, expectedError);
1441
- await cleanupAppArtifacts(appName, options);
1442
- }
1443
- });
1444
-
1445
- return [
1446
- createTestCase(
1447
- '2.14',
1448
- 'Datasource dimensions: local binding missing required field',
1449
- 'wizard-e2e-negative-dimension-missing',
1450
- 'wizard-valid-for-dimension-test',
1451
- corruptDatasourceLocalBindingMissingField,
1452
- 'Missing required property "field"'
1453
- ),
1454
- createTestCase(
1455
- '2.15',
1456
- 'Datasource invalid dimension key (root dimensions propertyNames)',
1457
- 'wizard-e2e-negative-dimension-invalid-key',
1458
- 'wizard-valid-for-dimension-key-test',
1459
- corruptDatasourceInvalidDimensionKey,
1460
- ['letters, numbers, and underscores only', 'property name must be valid']
1461
- ),
1462
- createTestCase(
1463
- '2.16',
1464
- 'Datasource invalid dimension binding field pattern',
1465
- 'wizard-e2e-negative-dimension-invalid-path',
1466
- 'wizard-valid-for-dimension-path-test',
1467
- corruptDatasourceInvalidAttributePath,
1468
- 'must match pattern'
1469
- ),
1470
- createTestCase(
1471
- '2.17',
1472
- 'Datasource dimensions wrong JSON type (array)',
1473
- 'wizard-e2e-negative-dimension-array',
1474
- 'wizard-valid-for-dimension-array-test',
1475
- corruptDatasourceDimensionsWrongJsonType,
1476
- 'Expected object'
1477
- )
1478
- ];
1479
- }
1480
-
1481
- /**
1482
- * Builds negative test cases
1483
- * @function buildNegativeTestCases
1484
- * @param {Object} context - Test context
1485
- * @returns {Array<Object>} Array of negative test cases
1486
- */
1487
- function buildNegativeTestCases(context) {
1488
- return [
1489
- ...buildNegativeConfigTestCases(context),
1490
- ...buildNegativeCredentialTestCases(context),
1491
- ...buildNegativeRbacTestCases(context),
1492
- ...buildNegativeDimensionTestCases(context)
1493
- ];
1494
- }
1495
-
1496
- /**
1497
- * Builds all test cases
1498
- * @function buildTestCases
1499
- * @param {Object} context - Test context
1500
- * @returns {Array<Object>} Array of all test cases
1501
- */
1502
- function buildTestCases(context) {
1503
- return [
1504
- ...buildPositiveTestCases(context),
1505
- ...buildRealDataTestCases(context),
1506
- ...buildNegativeTestCases(context)
1507
- ];
1508
- }
1509
-
1510
- /**
1511
- * Runs a test case
1512
- * @async
1513
- * @function runTestCase
1514
- * @param {Object} testCase - Test case object
1515
- * @param {Object} context - Test context
1516
- * @param {Object} options - Options object
1517
- * @returns {Promise<Object>} Test result object
1518
- */
1519
- async function runTestCase(testCase, context, options) {
1520
- const start = Date.now();
1521
- try {
1522
- await testCase.run(options);
1523
- const durationMs = Date.now() - start;
1524
- logSuccess(`PASS ${testCase.id} (${durationMs}ms) - ${testCase.name}`);
1525
- return { id: testCase.id, status: 'passed' };
1526
- } catch (error) {
1527
- if (error instanceof SkipTestError) {
1528
- logWarn(`SKIP ${testCase.id} - ${error.message}`);
1529
- return { id: testCase.id, status: 'skipped' };
1530
- }
1531
- logError(`FAIL ${testCase.id} - ${error.message}`);
1532
- return { id: testCase.id, status: 'failed', error };
1533
- }
1534
- }
1535
-
1536
- /**
1537
- * Sets up test context and environment variables
1538
- * @function setupTestContext
1539
- * @param {Object} context - Test context object
1540
- * @returns {void}
1541
- */
1542
- function setupTestContext(context) {
1543
- ensureEnvVar('CONTROLLER_URL', context.controllerUrl);
1544
- ensureEnvVar('ENVIRONMENT', context.environment);
1545
- if (context.dataplaneUrl) {
1546
- ensureEnvVar('DATAPLANE_URL', context.dataplaneUrl);
1547
- }
1548
- ensureEnvVar('HUBSPOT_OPENAPI_FILE', context.openapiFile);
1549
- }
1550
-
1551
- /**
1552
- * Main function
1553
- * @async
1554
- * @function main
1555
- * @returns {Promise<void>} Resolves when all tests complete
1556
- */
1557
- async function main() {
1558
- const args = parseArgs(process.argv);
1559
- if (args.help) {
1560
- printUsage();
1561
- return;
1562
- }
1563
- await ensureDir(ARTIFACT_DIR);
1564
- await loadEnvFile(LOCAL_ENV_PATH, args);
1565
- const context = await loadTestConfigFromEnv();
1566
- setupTestContext(context);
1567
- await validateAuth(context, args);
1568
- const testCases = buildTestCases(context).filter(testCase => (
1569
- matchesSelection(testCase, args.tests) && matchesType(testCase, args.types)
1570
- ));
1571
- if (testCases.length === 0) {
1572
- logWarn('No matching test cases found.');
1573
- return;
1574
- }
1575
- const results = [];
1576
- for (const testCase of testCases) {
1577
- results.push(await runTestCase(testCase, context, args));
1578
- }
1579
- const failed = results.filter(result => result.status === 'failed');
1580
- if (failed.length > 0) {
1581
- process.exitCode = 1;
1582
- }
1583
- }
1584
-
1585
- main().catch(error => {
1586
- logError(error.message || 'Unexpected error');
1587
- process.exitCode = 1;
1588
- });