@solidxai/core 0.1.1 → 0.1.4

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 (295) hide show
  1. package/dist/commands/run-tests.command.d.ts +37 -0
  2. package/dist/commands/run-tests.command.d.ts.map +1 -0
  3. package/dist/commands/run-tests.command.js +345 -0
  4. package/dist/commands/run-tests.command.js.map +1 -0
  5. package/dist/commands/test-data.command.d.ts +6 -6
  6. package/dist/commands/test-data.command.d.ts.map +1 -1
  7. package/dist/commands/test-data.command.js +25 -25
  8. package/dist/commands/test-data.command.js.map +1 -1
  9. package/dist/commands/test.command.d.ts +5 -0
  10. package/dist/commands/test.command.d.ts.map +1 -0
  11. package/dist/commands/test.command.js +26 -0
  12. package/dist/commands/test.command.js.map +1 -0
  13. package/dist/controllers/service.controller.d.ts +0 -9
  14. package/dist/controllers/service.controller.d.ts.map +1 -1
  15. package/dist/controllers/service.controller.js +0 -45
  16. package/dist/controllers/service.controller.js.map +1 -1
  17. package/dist/dtos/basic-filters.dto.d.ts.map +1 -1
  18. package/dist/dtos/basic-filters.dto.js.map +1 -1
  19. package/dist/dtos/create-user.dto.d.ts +1 -0
  20. package/dist/dtos/create-user.dto.d.ts.map +1 -1
  21. package/dist/dtos/create-user.dto.js +2 -1
  22. package/dist/dtos/create-user.dto.js.map +1 -1
  23. package/dist/helpers/schematic.service.js +1 -1
  24. package/dist/helpers/schematic.service.js.map +1 -1
  25. package/dist/index.d.ts +3 -0
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +3 -0
  28. package/dist/index.js.map +1 -1
  29. package/dist/seeders/module-metadata-seeder.service.d.ts.map +1 -1
  30. package/dist/seeders/module-metadata-seeder.service.js +3 -21
  31. package/dist/seeders/module-metadata-seeder.service.js.map +1 -1
  32. package/dist/seeders/module-test-data.service.d.ts.map +1 -1
  33. package/dist/seeders/module-test-data.service.js +3 -3
  34. package/dist/seeders/module-test-data.service.js.map +1 -1
  35. package/dist/seeders/seed-data/solid-core-metadata.json +34 -9
  36. package/dist/services/chatter-message.service.d.ts +2 -0
  37. package/dist/services/chatter-message.service.d.ts.map +1 -1
  38. package/dist/services/chatter-message.service.js +18 -2
  39. package/dist/services/chatter-message.service.js.map +1 -1
  40. package/dist/services/crud.service.d.ts.map +1 -1
  41. package/dist/services/crud.service.js.map +1 -1
  42. package/dist/services/model-metadata.service.d.ts.map +1 -1
  43. package/dist/services/model-metadata.service.js +2 -1
  44. package/dist/services/model-metadata.service.js.map +1 -1
  45. package/dist/services/module-metadata.service.d.ts.map +1 -1
  46. package/dist/services/module-metadata.service.js +2 -1
  47. package/dist/services/module-metadata.service.js.map +1 -1
  48. package/dist/services/queues/common.d.ts +3 -0
  49. package/dist/services/queues/common.d.ts.map +1 -0
  50. package/dist/services/queues/common.js +39 -0
  51. package/dist/services/queues/common.js.map +1 -0
  52. package/dist/services/queues/database-publisher.service.d.ts.map +1 -1
  53. package/dist/services/queues/database-publisher.service.js +3 -1
  54. package/dist/services/queues/database-publisher.service.js.map +1 -1
  55. package/dist/services/queues/database-subscriber.service.d.ts.map +1 -1
  56. package/dist/services/queues/database-subscriber.service.js +5 -2
  57. package/dist/services/queues/database-subscriber.service.js.map +1 -1
  58. package/dist/services/queues/rabbitmq-publisher.service.d.ts.map +1 -1
  59. package/dist/services/queues/rabbitmq-publisher.service.js +13 -6
  60. package/dist/services/queues/rabbitmq-publisher.service.js.map +1 -1
  61. package/dist/services/queues/rabbitmq-subscriber.service.d.ts +14 -1
  62. package/dist/services/queues/rabbitmq-subscriber.service.d.ts.map +1 -1
  63. package/dist/services/queues/rabbitmq-subscriber.service.js +197 -65
  64. package/dist/services/queues/rabbitmq-subscriber.service.js.map +1 -1
  65. package/dist/solid-core.module.d.ts.map +1 -1
  66. package/dist/solid-core.module.js +4 -0
  67. package/dist/solid-core.module.js.map +1 -1
  68. package/dist/testing/__examples__/register-example-specs.d.ts +3 -0
  69. package/dist/testing/__examples__/register-example-specs.d.ts.map +1 -0
  70. package/dist/testing/__examples__/register-example-specs.js +8 -0
  71. package/dist/testing/__examples__/register-example-specs.js.map +1 -0
  72. package/dist/testing/__examples__/specs/custom-health.spec.d.ts +17 -0
  73. package/dist/testing/__examples__/specs/custom-health.spec.d.ts.map +1 -0
  74. package/dist/testing/__examples__/specs/custom-health.spec.js +30 -0
  75. package/dist/testing/__examples__/specs/custom-health.spec.js.map +1 -0
  76. package/dist/testing/adapters/api/api-adapter.d.ts +9 -0
  77. package/dist/testing/adapters/api/api-adapter.d.ts.map +1 -0
  78. package/dist/testing/adapters/api/api-adapter.js +76 -0
  79. package/dist/testing/adapters/api/api-adapter.js.map +1 -0
  80. package/dist/testing/adapters/api/api.types.d.ts +14 -0
  81. package/dist/testing/adapters/api/api.types.d.ts.map +1 -0
  82. package/dist/testing/adapters/api/api.types.js +3 -0
  83. package/dist/testing/adapters/api/api.types.js.map +1 -0
  84. package/dist/testing/adapters/ui/playwright-adapter.d.ts +14 -0
  85. package/dist/testing/adapters/ui/playwright-adapter.d.ts.map +1 -0
  86. package/dist/testing/adapters/ui/playwright-adapter.js +47 -0
  87. package/dist/testing/adapters/ui/playwright-adapter.js.map +1 -0
  88. package/dist/testing/adapters/ui/ui.types.d.ts +5 -0
  89. package/dist/testing/adapters/ui/ui.types.d.ts.map +1 -0
  90. package/dist/testing/adapters/ui/ui.types.js +3 -0
  91. package/dist/testing/adapters/ui/ui.types.js.map +1 -0
  92. package/dist/testing/contracts/runtime-context.types.d.ts +35 -0
  93. package/dist/testing/contracts/runtime-context.types.d.ts.map +1 -0
  94. package/dist/testing/contracts/runtime-context.types.js +3 -0
  95. package/dist/testing/contracts/runtime-context.types.js.map +1 -0
  96. package/dist/testing/contracts/test-spec.types.d.ts +21 -0
  97. package/dist/testing/contracts/test-spec.types.d.ts.map +1 -0
  98. package/dist/testing/contracts/test-spec.types.js +3 -0
  99. package/dist/testing/contracts/test-spec.types.js.map +1 -0
  100. package/dist/testing/contracts/testing-metadata.types.d.ts +41 -0
  101. package/dist/testing/contracts/testing-metadata.types.d.ts.map +1 -0
  102. package/dist/testing/contracts/testing-metadata.types.js +3 -0
  103. package/dist/testing/contracts/testing-metadata.types.js.map +1 -0
  104. package/dist/testing/core/interpolation.d.ts +4 -0
  105. package/dist/testing/core/interpolation.d.ts.map +1 -0
  106. package/dist/testing/core/interpolation.js +180 -0
  107. package/dist/testing/core/interpolation.js.map +1 -0
  108. package/dist/testing/core/normalize-steps.d.ts +7 -0
  109. package/dist/testing/core/normalize-steps.d.ts.map +1 -0
  110. package/dist/testing/core/normalize-steps.js +20 -0
  111. package/dist/testing/core/normalize-steps.js.map +1 -0
  112. package/dist/testing/core/resource-store.d.ts +8 -0
  113. package/dist/testing/core/resource-store.d.ts.map +1 -0
  114. package/dist/testing/core/resource-store.js +41 -0
  115. package/dist/testing/core/resource-store.js.map +1 -0
  116. package/dist/testing/core/spec-registry.d.ts +10 -0
  117. package/dist/testing/core/spec-registry.d.ts.map +1 -0
  118. package/dist/testing/core/spec-registry.js +32 -0
  119. package/dist/testing/core/spec-registry.js.map +1 -0
  120. package/dist/testing/core/step-registry.d.ts +10 -0
  121. package/dist/testing/core/step-registry.d.ts.map +1 -0
  122. package/dist/testing/core/step-registry.js +26 -0
  123. package/dist/testing/core/step-registry.js.map +1 -0
  124. package/dist/testing/core/testing-engine.d.ts +14 -0
  125. package/dist/testing/core/testing-engine.d.ts.map +1 -0
  126. package/dist/testing/core/testing-engine.js +97 -0
  127. package/dist/testing/core/testing-engine.js.map +1 -0
  128. package/dist/testing/core/timeout.d.ts +2 -0
  129. package/dist/testing/core/timeout.d.ts.map +1 -0
  130. package/dist/testing/core/timeout.js +18 -0
  131. package/dist/testing/core/timeout.js.map +1 -0
  132. package/dist/testing/reporter/attachments.d.ts +4 -0
  133. package/dist/testing/reporter/attachments.d.ts.map +1 -0
  134. package/dist/testing/reporter/attachments.js +25 -0
  135. package/dist/testing/reporter/attachments.js.map +1 -0
  136. package/dist/testing/reporter/console-reporter.d.ts +45 -0
  137. package/dist/testing/reporter/console-reporter.d.ts.map +1 -0
  138. package/dist/testing/reporter/console-reporter.js +189 -0
  139. package/dist/testing/reporter/console-reporter.js.map +1 -0
  140. package/dist/testing/reporter/reporter.types.d.ts +37 -0
  141. package/dist/testing/reporter/reporter.types.d.ts.map +1 -0
  142. package/dist/testing/reporter/reporter.types.js +3 -0
  143. package/dist/testing/reporter/reporter.types.js.map +1 -0
  144. package/dist/testing/runner/lifecycle.d.ts +9 -0
  145. package/dist/testing/runner/lifecycle.d.ts.map +1 -0
  146. package/dist/testing/runner/lifecycle.js +33 -0
  147. package/dist/testing/runner/lifecycle.js.map +1 -0
  148. package/dist/testing/runner/run-from-metadata.d.ts +24 -0
  149. package/dist/testing/runner/run-from-metadata.d.ts.map +1 -0
  150. package/dist/testing/runner/run-from-metadata.js +70 -0
  151. package/dist/testing/runner/run-from-metadata.js.map +1 -0
  152. package/dist/testing/runner/scenario-filter.d.ts +9 -0
  153. package/dist/testing/runner/scenario-filter.d.ts.map +1 -0
  154. package/dist/testing/runner/scenario-filter.js +22 -0
  155. package/dist/testing/runner/scenario-filter.js.map +1 -0
  156. package/dist/testing/steps/api/auth.step.d.ts +3 -0
  157. package/dist/testing/steps/api/auth.step.d.ts.map +1 -0
  158. package/dist/testing/steps/api/auth.step.js +38 -0
  159. package/dist/testing/steps/api/auth.step.js.map +1 -0
  160. package/dist/testing/steps/api/index.d.ts +3 -0
  161. package/dist/testing/steps/api/index.d.ts.map +1 -0
  162. package/dist/testing/steps/api/index.js +10 -0
  163. package/dist/testing/steps/api/index.js.map +1 -0
  164. package/dist/testing/steps/api/request.step.d.ts +3 -0
  165. package/dist/testing/steps/api/request.step.d.ts.map +1 -0
  166. package/dist/testing/steps/api/request.step.js +281 -0
  167. package/dist/testing/steps/api/request.step.js.map +1 -0
  168. package/dist/testing/steps/assert/http.step.d.ts +3 -0
  169. package/dist/testing/steps/assert/http.step.d.ts.map +1 -0
  170. package/dist/testing/steps/assert/http.step.js +27 -0
  171. package/dist/testing/steps/assert/http.step.js.map +1 -0
  172. package/dist/testing/steps/assert/index.d.ts +3 -0
  173. package/dist/testing/steps/assert/index.d.ts.map +1 -0
  174. package/dist/testing/steps/assert/index.js +12 -0
  175. package/dist/testing/steps/assert/index.js.map +1 -0
  176. package/dist/testing/steps/assert/jsonpath.step.d.ts +3 -0
  177. package/dist/testing/steps/assert/jsonpath.step.d.ts.map +1 -0
  178. package/dist/testing/steps/assert/jsonpath.step.js +40 -0
  179. package/dist/testing/steps/assert/jsonpath.step.js.map +1 -0
  180. package/dist/testing/steps/assert/primitives.step.d.ts +3 -0
  181. package/dist/testing/steps/assert/primitives.step.d.ts.map +1 -0
  182. package/dist/testing/steps/assert/primitives.step.js +43 -0
  183. package/dist/testing/steps/assert/primitives.step.js.map +1 -0
  184. package/dist/testing/steps/test/index.d.ts +3 -0
  185. package/dist/testing/steps/test/index.d.ts.map +1 -0
  186. package/dist/testing/steps/test/index.js +8 -0
  187. package/dist/testing/steps/test/index.js.map +1 -0
  188. package/dist/testing/steps/test/test-spec.step.d.ts +3 -0
  189. package/dist/testing/steps/test/test-spec.step.d.ts.map +1 -0
  190. package/dist/testing/steps/test/test-spec.step.js +41 -0
  191. package/dist/testing/steps/test/test-spec.step.js.map +1 -0
  192. package/dist/testing/steps/ui/actions.step.d.ts +3 -0
  193. package/dist/testing/steps/ui/actions.step.d.ts.map +1 -0
  194. package/dist/testing/steps/ui/actions.step.js +31 -0
  195. package/dist/testing/steps/ui/actions.step.js.map +1 -0
  196. package/dist/testing/steps/ui/assertions.step.d.ts +3 -0
  197. package/dist/testing/steps/ui/assertions.step.d.ts.map +1 -0
  198. package/dist/testing/steps/ui/assertions.step.js +41 -0
  199. package/dist/testing/steps/ui/assertions.step.js.map +1 -0
  200. package/dist/testing/steps/ui/form.step.d.ts +3 -0
  201. package/dist/testing/steps/ui/form.step.d.ts.map +1 -0
  202. package/dist/testing/steps/ui/form.step.js +34 -0
  203. package/dist/testing/steps/ui/form.step.js.map +1 -0
  204. package/dist/testing/steps/ui/index.d.ts +3 -0
  205. package/dist/testing/steps/ui/index.d.ts.map +1 -0
  206. package/dist/testing/steps/ui/index.js +14 -0
  207. package/dist/testing/steps/ui/index.js.map +1 -0
  208. package/dist/testing/steps/ui/navigation.step.d.ts +3 -0
  209. package/dist/testing/steps/ui/navigation.step.d.ts.map +1 -0
  210. package/dist/testing/steps/ui/navigation.step.js +39 -0
  211. package/dist/testing/steps/ui/navigation.step.js.map +1 -0
  212. package/dist/testing/steps/util/index.d.ts +3 -0
  213. package/dist/testing/steps/util/index.d.ts.map +1 -0
  214. package/dist/testing/steps/util/index.js +12 -0
  215. package/dist/testing/steps/util/index.js.map +1 -0
  216. package/dist/testing/steps/util/log.step.d.ts +3 -0
  217. package/dist/testing/steps/util/log.step.d.ts.map +1 -0
  218. package/dist/testing/steps/util/log.step.js +18 -0
  219. package/dist/testing/steps/util/log.step.js.map +1 -0
  220. package/dist/testing/steps/util/require.step.d.ts +3 -0
  221. package/dist/testing/steps/util/require.step.d.ts.map +1 -0
  222. package/dist/testing/steps/util/require.step.js +16 -0
  223. package/dist/testing/steps/util/require.step.js.map +1 -0
  224. package/dist/testing/steps/util/sleep.step.d.ts +3 -0
  225. package/dist/testing/steps/util/sleep.step.d.ts.map +1 -0
  226. package/dist/testing/steps/util/sleep.step.js +13 -0
  227. package/dist/testing/steps/util/sleep.step.js.map +1 -0
  228. package/docs/test-data-workflow.md +51 -11
  229. package/package.json +4 -2
  230. package/src/commands/run-tests.command.ts +278 -0
  231. package/src/commands/test-data.command.ts +26 -26
  232. package/src/commands/test.command.ts +14 -0
  233. package/src/controllers/service.controller.ts +58 -59
  234. package/src/dtos/basic-filters.dto.ts +0 -2
  235. package/src/dtos/create-user.dto.ts +1 -0
  236. package/src/helpers/schematic.service.ts +1 -1
  237. package/src/index.ts +3 -0
  238. package/src/seeders/module-metadata-seeder.service.ts +5 -25
  239. package/src/seeders/module-test-data.service.ts +5 -3
  240. package/src/seeders/seed-data/solid-core-metadata.json +34 -9
  241. package/src/services/chatter-message.service.ts +18 -1
  242. package/src/services/crud.service.ts +1 -0
  243. package/src/services/model-metadata.service.ts +2 -1
  244. package/src/services/module-metadata.service.ts +2 -1
  245. package/src/services/queues/common.ts +75 -0
  246. package/src/services/queues/database-publisher.service.ts +4 -1
  247. package/src/services/queues/database-subscriber.service.ts +5 -3
  248. package/src/services/queues/rabbitmq-publisher.service.ts +17 -7
  249. package/src/services/queues/rabbitmq-subscriber.service.ts +223 -95
  250. package/src/solid-core.module.ts +4 -0
  251. package/src/testing/README.md +364 -0
  252. package/src/testing/__examples__/register-example-specs.ts +6 -0
  253. package/src/testing/__examples__/specs/custom-health.spec.ts +29 -0
  254. package/src/testing/__examples__/testing.sample.json +82 -0
  255. package/src/testing/adapters/api/api-adapter.ts +85 -0
  256. package/src/testing/adapters/api/api.types.ts +15 -0
  257. package/src/testing/adapters/ui/playwright-adapter.ts +54 -0
  258. package/src/testing/adapters/ui/ui.types.ts +4 -0
  259. package/src/testing/contracts/runtime-context.types.ts +36 -0
  260. package/src/testing/contracts/test-spec.types.ts +24 -0
  261. package/src/testing/contracts/testing-metadata.types.ts +46 -0
  262. package/src/testing/core/interpolation.ts +189 -0
  263. package/src/testing/core/normalize-steps.ts +21 -0
  264. package/src/testing/core/resource-store.ts +38 -0
  265. package/src/testing/core/spec-registry.ts +33 -0
  266. package/src/testing/core/step-registry.ts +27 -0
  267. package/src/testing/core/testing-engine.ts +127 -0
  268. package/src/testing/core/timeout.ts +19 -0
  269. package/src/testing/reporter/attachments.ts +25 -0
  270. package/src/testing/reporter/console-reporter.ts +229 -0
  271. package/src/testing/reporter/reporter.types.ts +36 -0
  272. package/src/testing/runner/lifecycle.ts +31 -0
  273. package/src/testing/runner/run-from-metadata.ts +87 -0
  274. package/src/testing/runner/scenario-filter.ts +33 -0
  275. package/src/testing/steps/api/auth.step.ts +66 -0
  276. package/src/testing/steps/api/index.ts +10 -0
  277. package/src/testing/steps/api/request.step.ts +358 -0
  278. package/src/testing/steps/assert/http.step.ts +33 -0
  279. package/src/testing/steps/assert/index.ts +12 -0
  280. package/src/testing/steps/assert/jsonpath.step.ts +50 -0
  281. package/src/testing/steps/assert/primitives.step.ts +69 -0
  282. package/src/testing/steps/test/index.ts +8 -0
  283. package/src/testing/steps/test/test-spec.step.ts +52 -0
  284. package/src/testing/steps/ui/actions.step.ts +36 -0
  285. package/src/testing/steps/ui/assertions.step.ts +54 -0
  286. package/src/testing/steps/ui/form.step.ts +39 -0
  287. package/src/testing/steps/ui/index.ts +12 -0
  288. package/src/testing/steps/ui/navigation.step.ts +53 -0
  289. package/src/testing/steps/util/index.ts +10 -0
  290. package/src/testing/steps/util/log.step.ts +19 -0
  291. package/src/testing/steps/util/require.step.ts +16 -0
  292. package/src/testing/steps/util/sleep.step.ts +15 -0
  293. package/tsconfig.json +35 -25
  294. package/tsconfig.tests.json +14 -0
  295. package/dist/tsconfig.tsbuildinfo +0 -1
@@ -0,0 +1,358 @@
1
+ import type { ApiRequestOptions } from "../../adapters/api/api.types";
2
+ import type { TestContext } from "../../contracts/runtime-context.types";
3
+ import type { OpStep } from "../../contracts/testing-metadata.types";
4
+ import { StepRegistry } from "../../core/step-registry";
5
+ import { attachJson } from "../../reporter/attachments";
6
+ import FormData from "form-data";
7
+ import crypto from "crypto";
8
+ import fs from "fs";
9
+ import https from "https";
10
+ import path from "path";
11
+ import qs from "qs";
12
+
13
+ const TMP_DIR = "/tmp/.solidx-testing-files";
14
+ const MAX_REDIRECTS = 5;
15
+
16
+ type ApiFormItem = {
17
+ name: string;
18
+ value: any;
19
+ type?: "text" | "file";
20
+ filename?: string;
21
+ contentType?: string;
22
+ };
23
+
24
+ type ApiRequestInput = {
25
+ method: string;
26
+ url: string;
27
+ headers?: Record<string, string>;
28
+ json?: unknown;
29
+ bodyText?: string;
30
+ // Extra query params (object or querystring).
31
+ query?: Record<string, any> | string;
32
+ // Form data payload (array of items).
33
+ formData?: ApiFormItem[] | Record<string, any>;
34
+ // Alias for formData (array or object).
35
+ body?: ApiFormItem[] | Record<string, any>;
36
+ };
37
+
38
+ function isPlainObject(value: unknown): value is Record<string, any> {
39
+ // Only merge plain objects.
40
+ return !!value && typeof value === "object" && !Array.isArray(value);
41
+ }
42
+
43
+ function deepMerge(target: any, source: any): any {
44
+ if (isPlainObject(target) && isPlainObject(source)) {
45
+ // Clone target to avoid mutation.
46
+ const out: Record<string, any> = { ...target };
47
+ for (const [key, value] of Object.entries(source)) {
48
+ if (key in out) {
49
+ // Merge nested keys.
50
+ out[key] = deepMerge(out[key], value);
51
+ } else {
52
+ // Add new keys.
53
+ out[key] = value;
54
+ }
55
+ }
56
+ // Return merged object.
57
+ return out;
58
+ }
59
+ // For non-objects, source wins.
60
+ return source;
61
+ }
62
+
63
+ function stripUndefined(value: any): any {
64
+ if (Array.isArray(value)) {
65
+ return value.map(stripUndefined);
66
+ }
67
+ if (isPlainObject(value)) {
68
+ const out: Record<string, any> = {};
69
+ for (const [key, val] of Object.entries(value)) {
70
+ if (val === undefined) continue;
71
+ out[key] = stripUndefined(val);
72
+ }
73
+ return out;
74
+ }
75
+ return value;
76
+ }
77
+
78
+ function ensureTmpDir(): void {
79
+ fs.mkdirSync(TMP_DIR, { recursive: true });
80
+ }
81
+
82
+ function buildTempFilePath(urlObj: URL): string {
83
+ const ext = path.extname(urlObj.pathname);
84
+ const base = path.basename(urlObj.pathname, ext) || "file";
85
+ const safeBase = base.replace(/[^a-zA-Z0-9-_]/g, "_");
86
+ const suffix = `${Date.now()}-${crypto.randomBytes(6).toString("hex")}`;
87
+ return path.join(TMP_DIR, `${safeBase}-${suffix}${ext}`);
88
+ }
89
+
90
+ function downloadUrlToFile(urlValue: string, redirectCount: number = 0): Promise<string> {
91
+ if (!urlValue.startsWith("https://")) {
92
+ throw new Error(`Only https URLs are allowed for file downloads: ${urlValue}`);
93
+ }
94
+ if (redirectCount > MAX_REDIRECTS) {
95
+ throw new Error(`Too many redirects while downloading file: ${urlValue}`);
96
+ }
97
+
98
+ const urlObj = new URL(urlValue);
99
+ ensureTmpDir();
100
+ const filePath = buildTempFilePath(urlObj);
101
+
102
+ return new Promise((resolve, reject) => {
103
+ const fileStream = fs.createWriteStream(filePath);
104
+
105
+ const request = https.get(urlObj, (response) => {
106
+ const status = response.statusCode ?? 0;
107
+ const location = response.headers.location;
108
+
109
+ if (status >= 300 && status < 400 && location) {
110
+ response.resume();
111
+ fileStream.close(() => fs.unlink(filePath, () => {}));
112
+ const nextUrl = new URL(location, urlObj).toString();
113
+ downloadUrlToFile(nextUrl, redirectCount + 1).then(resolve).catch(reject);
114
+ return;
115
+ }
116
+
117
+ if (status >= 400) {
118
+ response.resume();
119
+ fileStream.close(() => fs.unlink(filePath, () => {}));
120
+ reject(new Error(`Failed to download file. HTTP ${status}`));
121
+ return;
122
+ }
123
+
124
+ response.pipe(fileStream);
125
+ });
126
+
127
+ request.on("error", (err) => {
128
+ fileStream.close(() => fs.unlink(filePath, () => {}));
129
+ reject(err);
130
+ });
131
+
132
+ fileStream.on("error", (err) => {
133
+ request.destroy();
134
+ fs.unlink(filePath, () => {});
135
+ reject(err);
136
+ });
137
+
138
+ fileStream.on("finish", () => {
139
+ fileStream.close(() => resolve(filePath));
140
+ });
141
+ });
142
+ }
143
+
144
+ function resolveFilePath(rawValue: string): string {
145
+ const value = rawValue.startsWith("file:") ? rawValue.slice(5) : rawValue;
146
+ if (!value) {
147
+ throw new Error('Invalid file reference. Use "file:/absolute/path".');
148
+ }
149
+ if (!path.isAbsolute(value)) {
150
+ throw new Error(`File path must be absolute: ${value}`);
151
+ }
152
+ if (!fs.existsSync(value)) {
153
+ throw new Error(`File does not exist: ${value}`);
154
+ }
155
+ return value;
156
+ }
157
+
158
+ async function resolveFileReference(rawValue: string): Promise<{ filePath: string; source: "file" | "url"; url?: string }>{
159
+ if (rawValue.startsWith("url:")) {
160
+ const urlValue = rawValue.slice(4);
161
+ if (!urlValue) {
162
+ throw new Error('Invalid url reference. Use "url:https://...".');
163
+ }
164
+ const filePath = await downloadUrlToFile(urlValue);
165
+ return { filePath, source: "url", url: urlValue };
166
+ }
167
+
168
+ return { filePath: resolveFilePath(rawValue), source: "file" };
169
+ }
170
+
171
+ function formItemsFromRecord(record: Record<string, any>): ApiFormItem[] {
172
+ return Object.entries(record).map(([name, value]) => {
173
+ if (typeof value === "string" && (value.startsWith("file:") || value.startsWith("url:"))) {
174
+ return { name, value, type: "file" };
175
+ }
176
+ return { name, value };
177
+ });
178
+ }
179
+
180
+ async function buildFormData(items: ApiFormItem[]): Promise<{ form: FormData; logItems: Record<string, any>[] }>{
181
+ const form = new FormData();
182
+ const logItems: Record<string, any>[] = [];
183
+
184
+ for (const item of items) {
185
+ if (!item?.name) {
186
+ throw new Error('Form item is missing required "name".');
187
+ }
188
+ const rawValue = item.value;
189
+ const isFile = item.type === "file" || (typeof rawValue === "string" && (rawValue.startsWith("file:") || rawValue.startsWith("url:")));
190
+
191
+ if (isFile) {
192
+ if (typeof rawValue !== "string") {
193
+ throw new Error(`Form file value must be a string for field: ${item.name}`);
194
+ }
195
+ const resolved = await resolveFileReference(rawValue);
196
+ const filename = item.filename ?? path.basename(resolved.filePath);
197
+ form.append(item.name, fs.createReadStream(resolved.filePath), {
198
+ filename,
199
+ contentType: item.contentType,
200
+ });
201
+ logItems.push({
202
+ name: item.name,
203
+ type: "file",
204
+ source: resolved.source,
205
+ url: resolved.url,
206
+ path: resolved.filePath,
207
+ filename,
208
+ contentType: item.contentType,
209
+ });
210
+ } else {
211
+ // TODO: Need to test the JSON.stringify(rawValue) functionality here...
212
+ // This scenario will happen only when we try to create embedded one-to-many or many-to-one objects in create API calls...
213
+ const textValue = rawValue === undefined || rawValue === null ? "" : typeof rawValue === "string" ? rawValue : JSON.stringify(rawValue);
214
+ form.append(item.name, textValue);
215
+ logItems.push({
216
+ name: item.name,
217
+ type: "text",
218
+ value: textValue,
219
+ });
220
+ }
221
+ }
222
+
223
+ return { form, logItems };
224
+ }
225
+
226
+ export function registerApiRequestStep(registry: StepRegistry): void {
227
+ registry.register("api.request", async (ctx: TestContext, step: OpStep) => {
228
+ if (!ctx.api) {
229
+ throw new Error('Missing API adapter on context for op "api.request"');
230
+ }
231
+
232
+ const input = (step.with ?? {}) as ApiRequestInput;
233
+ if (!input.method) {
234
+ throw new Error('Missing "method" in step.with for op "api.request"');
235
+ }
236
+ if (!input.url) {
237
+ throw new Error('Missing "url" in step.with for op "api.request"');
238
+ }
239
+
240
+ const rawFormData = input.formData ?? input.body;
241
+ const formItems = Array.isArray(rawFormData) ? rawFormData : isPlainObject(rawFormData) ? formItemsFromRecord(rawFormData) : undefined;
242
+
243
+ if (rawFormData !== undefined && !formItems) {
244
+ throw new Error('formData/body must be an array of items or an object, for op "api.request".');
245
+ }
246
+
247
+ if (formItems && (input.json !== undefined || input.bodyText !== undefined)) {
248
+ throw new Error('Use either formData/body or json/bodyText, not both, for op "api.request".');
249
+ }
250
+
251
+ // Original URL (may include query).
252
+ const rawUrl = input.url;
253
+
254
+ // Detect absolute URLs by scheme.
255
+ const absolute = /^[a-z][a-z0-9+.-]*:/i.test(rawUrl);
256
+
257
+ // Parse with base for relative URLs.
258
+ const urlObj = new URL(rawUrl, "http://solid.local");
259
+
260
+ // Parse URL query into object.
261
+ const urlQuery = urlObj.search
262
+ ? (qs.parse(urlObj.search, { ignoreQueryPrefix: true, allowDots: true, depth: 10 }) as Record<string, any>)
263
+ : {}; // No query in URL.
264
+ let stepQuery: Record<string, any> = {};
265
+ if (input.query !== undefined) {
266
+ stepQuery =
267
+ typeof input.query === "string"
268
+ // Parse query string.
269
+ ? (qs.parse(input.query, { ignoreQueryPrefix: true, allowDots: true, depth: 10 }) as Record<string, any>)
270
+ // Use object as-is.
271
+ : (input.query as Record<string, any>);
272
+ }
273
+
274
+ // Step query overrides URL query.
275
+ const mergedQuery = deepMerge(urlQuery, stepQuery);
276
+
277
+ // Rebuild query string.
278
+ const queryString = qs.stringify(mergedQuery, { addQueryPrefix: true, allowDots: true });
279
+
280
+ // Apply merged query to URL.
281
+ urlObj.search = queryString;
282
+ const finalUrl = absolute
283
+ // Preserve absolute URL.
284
+ ? urlObj.toString()
285
+ // Keep relative URL shape.
286
+ : `${urlObj.pathname}${urlObj.search}${urlObj.hash}`;
287
+
288
+ let formData: FormData | undefined;
289
+ let formLogItems: Record<string, any>[] | undefined;
290
+ if (formItems) {
291
+ const built = await buildFormData(formItems);
292
+ formData = built.form;
293
+ formLogItems = built.logItems;
294
+ }
295
+
296
+ const req: ApiRequestOptions = {
297
+ method: input.method,
298
+ url: finalUrl,
299
+ headers: input.headers,
300
+ json: input.json,
301
+ bodyText: input.bodyText,
302
+ formData,
303
+ };
304
+
305
+ const startedAt = Date.now();
306
+ const printApiLogs = ctx.options?.printApiLogs ?? false;
307
+ const logName = `api.request ${req.method} ${finalUrl}`;
308
+ const requestLog = stripUndefined({
309
+ method: req.method,
310
+ url: finalUrl,
311
+ headers: req.headers,
312
+ query: mergedQuery,
313
+ queryString: urlObj.search ? urlObj.search.slice(1) : "",
314
+ json: input.json,
315
+ bodyText: input.bodyText,
316
+ formData: formLogItems,
317
+ });
318
+
319
+ let response;
320
+ try {
321
+ response = await ctx.api.http(req);
322
+ } catch (err: any) {
323
+ if (printApiLogs) {
324
+ attachJson(ctx, logName, {
325
+ request: requestLog,
326
+ durationMs: Date.now() - startedAt,
327
+ error: err instanceof Error ? err.message : String(err),
328
+ });
329
+ }
330
+ throw err;
331
+ }
332
+
333
+ const durationMs = Date.now() - startedAt;
334
+ ctx.last = { ...(ctx.last ?? {}), apiResponse: response };
335
+
336
+ if (printApiLogs) {
337
+ const responseLog = stripUndefined({
338
+ status: response.status,
339
+ headers: response.headers,
340
+ bodyJson: response.bodyJson,
341
+ bodyText: response.bodyJson === undefined ? response.bodyText : undefined,
342
+ });
343
+ attachJson(ctx, logName, {
344
+ request: requestLog,
345
+ response: responseLog,
346
+ durationMs,
347
+ });
348
+ }
349
+
350
+ return {
351
+ status: response.status,
352
+ headers: response.headers,
353
+ bodyText: response.bodyText,
354
+ bodyJson: response.bodyJson,
355
+ body: response.bodyJson ?? response.bodyText,
356
+ };
357
+ });
358
+ }
@@ -0,0 +1,33 @@
1
+ import type { ApiResponse, TestContext } from "../../contracts/runtime-context.types";
2
+ import type { OpStep } from "../../contracts/testing-metadata.types";
3
+ import { StepRegistry } from "../../core/step-registry";
4
+ import { attachJson } from "../../reporter/attachments";
5
+
6
+ type HttpStatusInput = { from?: ApiResponse; is: number };
7
+
8
+ export function registerHttpAssertSteps(registry: StepRegistry): void {
9
+ registry.register("assert.httpStatus", async (ctx: TestContext, step: OpStep) => {
10
+ const input = (step.with ?? {}) as HttpStatusInput;
11
+ const response = input.from ?? ctx.last?.apiResponse;
12
+ if (!response) {
13
+ throw new Error('Missing ApiResponse for op "assert.httpStatus"');
14
+ }
15
+ if (input.is === undefined) {
16
+ throw new Error('Missing "is" in step.with for op "assert.httpStatus"');
17
+ }
18
+
19
+ if (ctx.options?.printApiLogs && ctx.reporter.attach) {
20
+ attachJson(ctx, "apiResponse", response);
21
+ }
22
+
23
+ if (response.status !== input.is) {
24
+ const err = new Error(
25
+ `Expected HTTP status ${input.is} but got ${response.status}`,
26
+ );
27
+ (err as any).httpResponseBody = response.bodyText;
28
+ (err as any).httpStatus = response.status;
29
+ (err as any).httpExpectedStatus = input.is;
30
+ throw err;
31
+ }
32
+ });
33
+ }
@@ -0,0 +1,12 @@
1
+ // Purpose: Assert step registrations.
2
+
3
+ import { StepRegistry } from "../../core/step-registry";
4
+ import { registerHttpAssertSteps } from "./http.step";
5
+ import { registerJsonPathAssertSteps } from "./jsonpath.step";
6
+ import { registerPrimitiveAssertSteps } from "./primitives.step";
7
+
8
+ export function registerAssertSteps(registry: StepRegistry): void {
9
+ registerPrimitiveAssertSteps(registry);
10
+ registerHttpAssertSteps(registry);
11
+ registerJsonPathAssertSteps(registry);
12
+ }
@@ -0,0 +1,50 @@
1
+ // Purpose: JSONPath assertion step registrations.
2
+
3
+ import type { TestContext } from "../../contracts/runtime-context.types";
4
+ import type { OpStep } from "../../contracts/testing-metadata.types";
5
+ import { StepRegistry } from "../../core/step-registry";
6
+
7
+ type JsonPathInput = { from: any; path: string; equals: any };
8
+
9
+ function resolveJsonPath(from: any, path: string): unknown {
10
+ let normalized = path.trim();
11
+ if (normalized.startsWith("$.")) {
12
+ normalized = normalized.slice(2);
13
+ } else if (normalized === "$") {
14
+ return from;
15
+ }
16
+
17
+ normalized = normalized.replace(/\[(\d+)\]/g, ".$1");
18
+ const parts = normalized.split(".").filter(Boolean);
19
+
20
+ let current: any = from;
21
+ for (const part of parts) {
22
+ if (current == null) return undefined;
23
+ current = current[part];
24
+ }
25
+ return current;
26
+ }
27
+
28
+ export function registerJsonPathAssertSteps(registry: StepRegistry): void {
29
+ registry.register("assert.jsonPath", async (_ctx: TestContext, step: OpStep) => {
30
+ const input = (step.with ?? {}) as JsonPathInput;
31
+ if (!("from" in input)) {
32
+ throw new Error('Missing "from" in step.with for op "assert.jsonPath"');
33
+ }
34
+ if (!input.path) {
35
+ throw new Error('Missing "path" in step.with for op "assert.jsonPath"');
36
+ }
37
+ if (!("equals" in input)) {
38
+ throw new Error('Missing "equals" in step.with for op "assert.jsonPath"');
39
+ }
40
+
41
+ const actual = resolveJsonPath(input.from, input.path);
42
+ if (actual !== input.equals) {
43
+ throw new Error(
44
+ `Expected JSONPath "${input.path}" to equal ${String(
45
+ input.equals,
46
+ )} but got ${String(actual)}`,
47
+ );
48
+ }
49
+ });
50
+ }
@@ -0,0 +1,69 @@
1
+ // Purpose: Primitive assertion step registrations.
2
+
3
+ import type { TestContext } from "../../contracts/runtime-context.types";
4
+ import type { OpStep } from "../../contracts/testing-metadata.types";
5
+ import { StepRegistry } from "../../core/step-registry";
6
+
7
+ type EqualsInput = { actual: unknown; expected: unknown };
8
+ type ContainsInput = { actual: string; expected: string };
9
+ type MatchesInput = { actual: string; pattern: string };
10
+
11
+ export function registerPrimitiveAssertSteps(registry: StepRegistry): void {
12
+ registry.register("assert.equals", async (_ctx: TestContext, step: OpStep) => {
13
+ const input = (step.with ?? {}) as EqualsInput;
14
+ if (!("actual" in input)) {
15
+ throw new Error('Missing "actual" in step.with for op "assert.equals"');
16
+ }
17
+ if (!("expected" in input)) {
18
+ throw new Error('Missing "expected" in step.with for op "assert.equals"');
19
+ }
20
+ if (input.actual !== input.expected) {
21
+ throw new Error(
22
+ `Expected values to be equal. Actual: ${String(
23
+ input.actual,
24
+ )}, Expected: ${String(input.expected)}`,
25
+ );
26
+ }
27
+ });
28
+
29
+ registry.register(
30
+ "assert.contains",
31
+ async (_ctx: TestContext, step: OpStep) => {
32
+ const input = (step.with ?? {}) as ContainsInput;
33
+ if (!input.actual) {
34
+ throw new Error('Missing "actual" in step.with for op "assert.contains"');
35
+ }
36
+ if (input.expected === undefined) {
37
+ throw new Error(
38
+ 'Missing "expected" in step.with for op "assert.contains"',
39
+ );
40
+ }
41
+ if (!input.actual.includes(input.expected)) {
42
+ throw new Error(
43
+ `Expected "${input.actual}" to contain "${input.expected}"`,
44
+ );
45
+ }
46
+ },
47
+ );
48
+
49
+ registry.register(
50
+ "assert.matches",
51
+ async (_ctx: TestContext, step: OpStep) => {
52
+ const input = (step.with ?? {}) as MatchesInput;
53
+ if (!input.actual) {
54
+ throw new Error('Missing "actual" in step.with for op "assert.matches"');
55
+ }
56
+ if (!input.pattern) {
57
+ throw new Error(
58
+ 'Missing "pattern" in step.with for op "assert.matches"',
59
+ );
60
+ }
61
+ const regex = new RegExp(input.pattern);
62
+ if (!regex.test(input.actual)) {
63
+ throw new Error(
64
+ `Expected "${input.actual}" to match /${input.pattern}/`,
65
+ );
66
+ }
67
+ },
68
+ );
69
+ }
@@ -0,0 +1,8 @@
1
+ // Purpose: Test step registrations.
2
+
3
+ import { StepRegistry } from "../../core/step-registry";
4
+ import { registerTestSpecStep } from "./test-spec.step";
5
+
6
+ export function registerTestSteps(registry: StepRegistry): void {
7
+ registerTestSpecStep(registry);
8
+ }
@@ -0,0 +1,52 @@
1
+ // Purpose: test.spec step registration.
2
+
3
+ import type { TestContext } from "../../contracts/runtime-context.types";
4
+ import type { OpStep } from "../../contracts/testing-metadata.types";
5
+ import { StepRegistry } from "../../core/step-registry";
6
+
7
+ export function registerTestSpecStep(registry: StepRegistry): void {
8
+ registry.register("test.spec", async (ctx: TestContext, step: OpStep) => {
9
+ const specId =
10
+ step.spec ?? (step.with?.specId as string | undefined);
11
+ if (!specId) {
12
+ throw new Error(
13
+ 'Missing "spec" on step (or "specId" in step.with) for op "test.spec"',
14
+ );
15
+ }
16
+ if (!ctx.specRegistry) {
17
+ throw new Error('Missing specRegistry on context for op "test.spec"');
18
+ }
19
+
20
+ const input = (step.with?.input ?? {}) as Record<string, any>;
21
+ const spec = ctx.specRegistry.create(specId);
22
+ const result = await spec.run({ ctx, input });
23
+
24
+ ctx.reporter.onSpecResult?.({
25
+ scenarioId: ctx.scenarioId,
26
+ specId,
27
+ stepName: step.name,
28
+ result,
29
+ });
30
+
31
+ if (result.attachments && ctx.reporter.attach) {
32
+ for (const attachment of result.attachments) {
33
+ const data =
34
+ attachment.encoding === "base64"
35
+ ? Buffer.from(attachment.data, "base64")
36
+ : attachment.data;
37
+ ctx.reporter.attach({
38
+ scenarioId: ctx.scenarioId,
39
+ name: attachment.name,
40
+ contentType: attachment.contentType,
41
+ data,
42
+ });
43
+ }
44
+ }
45
+
46
+ if (!result.ok) {
47
+ throw new Error(`test.spec failed: ${specId}`);
48
+ }
49
+
50
+ return result;
51
+ });
52
+ }
@@ -0,0 +1,36 @@
1
+ import type { TestContext } from "../../contracts/runtime-context.types";
2
+ import type { OpStep } from "../../contracts/testing-metadata.types";
3
+ import { StepRegistry } from "../../core/step-registry";
4
+
5
+ type ClickInput = { selector: string };
6
+ type PressInput = { selector: string; key: string };
7
+
8
+ function requirePage(ctx: TestContext, op: string) {
9
+ if (!ctx.ui || !ctx.ui.page) {
10
+ throw new Error(`Missing UI page on context for op "${op}"`);
11
+ }
12
+ return ctx.ui.page;
13
+ }
14
+
15
+ export function registerActionSteps(registry: StepRegistry): void {
16
+ registry.register("ui.click", async (ctx: TestContext, step: OpStep) => {
17
+ const page = requirePage(ctx, "ui.click");
18
+ const input = (step.with ?? {}) as ClickInput;
19
+ if (!input.selector) {
20
+ throw new Error('Missing "selector" in step.with for op "ui.click"');
21
+ }
22
+ await page.click(input.selector);
23
+ });
24
+
25
+ registry.register("ui.press", async (ctx: TestContext, step: OpStep) => {
26
+ const page = requirePage(ctx, "ui.press");
27
+ const input = (step.with ?? {}) as PressInput;
28
+ if (!input.selector) {
29
+ throw new Error('Missing "selector" in step.with for op "ui.press"');
30
+ }
31
+ if (!input.key) {
32
+ throw new Error('Missing "key" in step.with for op "ui.press"');
33
+ }
34
+ await page.press(input.selector, input.key);
35
+ });
36
+ }
@@ -0,0 +1,54 @@
1
+ import type { TestContext } from "../../contracts/runtime-context.types";
2
+ import type { OpStep } from "../../contracts/testing-metadata.types";
3
+ import { StepRegistry } from "../../core/step-registry";
4
+
5
+ type VisibleInput = { selector: string };
6
+ type ExpectTextInput = { selector: string; equals?: string; contains?: string };
7
+
8
+ function requirePage(ctx: TestContext, op: string) {
9
+ if (!ctx.ui || !ctx.ui.page) {
10
+ throw new Error(`Missing UI page on context for op "${op}"`);
11
+ }
12
+ return ctx.ui.page;
13
+ }
14
+
15
+ export function registerAssertionSteps(registry: StepRegistry): void {
16
+ registry.register("ui.expectVisible", async (ctx: TestContext, step: OpStep) => {
17
+ const page = requirePage(ctx, "ui.expectVisible");
18
+ const input = (step.with ?? {}) as VisibleInput;
19
+ if (!input.selector) {
20
+ throw new Error('Missing "selector" in step.with for op "ui.expectVisible"');
21
+ }
22
+ await page.waitForSelector(input.selector, { state: "visible" });
23
+ });
24
+
25
+ registry.register("ui.expectText", async (ctx: TestContext, step: OpStep) => {
26
+ const page = requirePage(ctx, "ui.expectText");
27
+ const input = (step.with ?? {}) as ExpectTextInput;
28
+ if (!input.selector) {
29
+ throw new Error('Missing "selector" in step.with for op "ui.expectText"');
30
+ }
31
+
32
+ const text = await page.locator(input.selector).innerText();
33
+ if (input.equals !== undefined) {
34
+ if (text !== input.equals) {
35
+ throw new Error(
36
+ `Expected text to equal "${input.equals}" but got "${text}"`,
37
+ );
38
+ }
39
+ return;
40
+ }
41
+ if (input.contains !== undefined) {
42
+ if (!text.includes(input.contains)) {
43
+ throw new Error(
44
+ `Expected text to contain "${input.contains}" but got "${text}"`,
45
+ );
46
+ }
47
+ return;
48
+ }
49
+
50
+ throw new Error(
51
+ 'Missing "equals" or "contains" in step.with for op "ui.expectText"',
52
+ );
53
+ });
54
+ }