iec-builder 0.1.0

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 (337) hide show
  1. package/.claude/settings.local.json +111 -0
  2. package/.iec.yaml +5 -0
  3. package/CLAUDE.md +174 -0
  4. package/Dockerfile +34 -0
  5. package/README.md +84 -0
  6. package/catalog-info.yaml +11 -0
  7. package/dist/config/env.d.ts +219 -0
  8. package/dist/config/env.d.ts.map +1 -0
  9. package/dist/config/env.js +89 -0
  10. package/dist/config/env.js.map +1 -0
  11. package/dist/index.d.ts +2 -0
  12. package/dist/index.d.ts.map +1 -0
  13. package/dist/index.js +148 -0
  14. package/dist/index.js.map +1 -0
  15. package/dist/middleware/auth.d.ts +43 -0
  16. package/dist/middleware/auth.d.ts.map +1 -0
  17. package/dist/middleware/auth.js +217 -0
  18. package/dist/middleware/auth.js.map +1 -0
  19. package/dist/middleware/org-access.d.ts +28 -0
  20. package/dist/middleware/org-access.d.ts.map +1 -0
  21. package/dist/middleware/org-access.js +102 -0
  22. package/dist/middleware/org-access.js.map +1 -0
  23. package/dist/models/types.d.ts +254 -0
  24. package/dist/models/types.d.ts.map +1 -0
  25. package/dist/models/types.js +2 -0
  26. package/dist/models/types.js.map +1 -0
  27. package/dist/routes/ai.d.ts +2 -0
  28. package/dist/routes/ai.d.ts.map +1 -0
  29. package/dist/routes/ai.js +77 -0
  30. package/dist/routes/ai.js.map +1 -0
  31. package/dist/routes/audit.d.ts +2 -0
  32. package/dist/routes/audit.d.ts.map +1 -0
  33. package/dist/routes/audit.js +102 -0
  34. package/dist/routes/audit.js.map +1 -0
  35. package/dist/routes/builds.d.ts +2 -0
  36. package/dist/routes/builds.d.ts.map +1 -0
  37. package/dist/routes/builds.js +262 -0
  38. package/dist/routes/builds.js.map +1 -0
  39. package/dist/routes/cluster.d.ts +2 -0
  40. package/dist/routes/cluster.d.ts.map +1 -0
  41. package/dist/routes/cluster.js +181 -0
  42. package/dist/routes/cluster.js.map +1 -0
  43. package/dist/routes/config.d.ts +2 -0
  44. package/dist/routes/config.d.ts.map +1 -0
  45. package/dist/routes/config.js +291 -0
  46. package/dist/routes/config.js.map +1 -0
  47. package/dist/routes/databases.d.ts +2 -0
  48. package/dist/routes/databases.d.ts.map +1 -0
  49. package/dist/routes/databases.js +161 -0
  50. package/dist/routes/databases.js.map +1 -0
  51. package/dist/routes/db-whitelist.d.ts +2 -0
  52. package/dist/routes/db-whitelist.d.ts.map +1 -0
  53. package/dist/routes/db-whitelist.js +148 -0
  54. package/dist/routes/db-whitelist.js.map +1 -0
  55. package/dist/routes/domains.d.ts +2 -0
  56. package/dist/routes/domains.d.ts.map +1 -0
  57. package/dist/routes/domains.js +449 -0
  58. package/dist/routes/domains.js.map +1 -0
  59. package/dist/routes/oauth.d.ts +2 -0
  60. package/dist/routes/oauth.d.ts.map +1 -0
  61. package/dist/routes/oauth.js +180 -0
  62. package/dist/routes/oauth.js.map +1 -0
  63. package/dist/routes/observability.d.ts +2 -0
  64. package/dist/routes/observability.d.ts.map +1 -0
  65. package/dist/routes/observability.js +167 -0
  66. package/dist/routes/observability.js.map +1 -0
  67. package/dist/routes/orgs.d.ts +2 -0
  68. package/dist/routes/orgs.d.ts.map +1 -0
  69. package/dist/routes/orgs.js +270 -0
  70. package/dist/routes/orgs.js.map +1 -0
  71. package/dist/routes/platform.d.ts +2 -0
  72. package/dist/routes/platform.d.ts.map +1 -0
  73. package/dist/routes/platform.js +107 -0
  74. package/dist/routes/platform.js.map +1 -0
  75. package/dist/routes/push.d.ts +2 -0
  76. package/dist/routes/push.d.ts.map +1 -0
  77. package/dist/routes/push.js +233 -0
  78. package/dist/routes/push.js.map +1 -0
  79. package/dist/routes/rotation.d.ts +3 -0
  80. package/dist/routes/rotation.d.ts.map +1 -0
  81. package/dist/routes/rotation.js +154 -0
  82. package/dist/routes/rotation.js.map +1 -0
  83. package/dist/routes/services.d.ts +2 -0
  84. package/dist/routes/services.d.ts.map +1 -0
  85. package/dist/routes/services.js +246 -0
  86. package/dist/routes/services.js.map +1 -0
  87. package/dist/routes/storage.d.ts +2 -0
  88. package/dist/routes/storage.d.ts.map +1 -0
  89. package/dist/routes/storage.js +118 -0
  90. package/dist/routes/storage.js.map +1 -0
  91. package/dist/routes/users.d.ts +2 -0
  92. package/dist/routes/users.d.ts.map +1 -0
  93. package/dist/routes/users.js +183 -0
  94. package/dist/routes/users.js.map +1 -0
  95. package/dist/routes/versions.d.ts +2 -0
  96. package/dist/routes/versions.d.ts.map +1 -0
  97. package/dist/routes/versions.js +195 -0
  98. package/dist/routes/versions.js.map +1 -0
  99. package/dist/routes/webhooks.d.ts +2 -0
  100. package/dist/routes/webhooks.d.ts.map +1 -0
  101. package/dist/routes/webhooks.js +334 -0
  102. package/dist/routes/webhooks.js.map +1 -0
  103. package/dist/services/__tests__/deploy-pipeline.integration.test.d.ts +2 -0
  104. package/dist/services/__tests__/deploy-pipeline.integration.test.d.ts.map +1 -0
  105. package/dist/services/__tests__/deploy-pipeline.integration.test.js +482 -0
  106. package/dist/services/__tests__/deploy-pipeline.integration.test.js.map +1 -0
  107. package/dist/services/bio-client.d.ts +68 -0
  108. package/dist/services/bio-client.d.ts.map +1 -0
  109. package/dist/services/bio-client.js +110 -0
  110. package/dist/services/bio-client.js.map +1 -0
  111. package/dist/services/build-queue.d.ts +7 -0
  112. package/dist/services/build-queue.d.ts.map +1 -0
  113. package/dist/services/build-queue.js +114 -0
  114. package/dist/services/build-queue.js.map +1 -0
  115. package/dist/services/builder.d.ts +7 -0
  116. package/dist/services/builder.d.ts.map +1 -0
  117. package/dist/services/builder.js +1384 -0
  118. package/dist/services/builder.js.map +1 -0
  119. package/dist/services/catalog.d.ts +177 -0
  120. package/dist/services/catalog.d.ts.map +1 -0
  121. package/dist/services/catalog.js +805 -0
  122. package/dist/services/catalog.js.map +1 -0
  123. package/dist/services/catalog.test.d.ts +2 -0
  124. package/dist/services/catalog.test.d.ts.map +1 -0
  125. package/dist/services/catalog.test.js +467 -0
  126. package/dist/services/catalog.test.js.map +1 -0
  127. package/dist/services/cloudflare.d.ts +43 -0
  128. package/dist/services/cloudflare.d.ts.map +1 -0
  129. package/dist/services/cloudflare.js +182 -0
  130. package/dist/services/cloudflare.js.map +1 -0
  131. package/dist/services/config-validator.d.ts +28 -0
  132. package/dist/services/config-validator.d.ts.map +1 -0
  133. package/dist/services/config-validator.js +68 -0
  134. package/dist/services/config-validator.js.map +1 -0
  135. package/dist/services/config-validator.test.d.ts +2 -0
  136. package/dist/services/config-validator.test.d.ts.map +1 -0
  137. package/dist/services/config-validator.test.js +151 -0
  138. package/dist/services/config-validator.test.js.map +1 -0
  139. package/dist/services/crypto.d.ts +19 -0
  140. package/dist/services/crypto.d.ts.map +1 -0
  141. package/dist/services/crypto.js +63 -0
  142. package/dist/services/crypto.js.map +1 -0
  143. package/dist/services/database.d.ts +26 -0
  144. package/dist/services/database.d.ts.map +1 -0
  145. package/dist/services/database.js +100 -0
  146. package/dist/services/database.js.map +1 -0
  147. package/dist/services/db-credential-manager.d.ts +73 -0
  148. package/dist/services/db-credential-manager.d.ts.map +1 -0
  149. package/dist/services/db-credential-manager.js +342 -0
  150. package/dist/services/db-credential-manager.js.map +1 -0
  151. package/dist/services/db-provisioner.d.ts +57 -0
  152. package/dist/services/db-provisioner.d.ts.map +1 -0
  153. package/dist/services/db-provisioner.js +400 -0
  154. package/dist/services/db-provisioner.js.map +1 -0
  155. package/dist/services/db-provisioner.test.d.ts +2 -0
  156. package/dist/services/db-provisioner.test.d.ts.map +1 -0
  157. package/dist/services/db-provisioner.test.js +141 -0
  158. package/dist/services/db-provisioner.test.js.map +1 -0
  159. package/dist/services/db-whitelist.d.ts +58 -0
  160. package/dist/services/db-whitelist.d.ts.map +1 -0
  161. package/dist/services/db-whitelist.js +379 -0
  162. package/dist/services/db-whitelist.js.map +1 -0
  163. package/dist/services/dependency-resolver.d.ts +58 -0
  164. package/dist/services/dependency-resolver.d.ts.map +1 -0
  165. package/dist/services/dependency-resolver.js +180 -0
  166. package/dist/services/dependency-resolver.js.map +1 -0
  167. package/dist/services/dependency-resolver.test.d.ts +2 -0
  168. package/dist/services/dependency-resolver.test.d.ts.map +1 -0
  169. package/dist/services/dependency-resolver.test.js +195 -0
  170. package/dist/services/dependency-resolver.test.js.map +1 -0
  171. package/dist/services/deploy-gate.d.ts +19 -0
  172. package/dist/services/deploy-gate.d.ts.map +1 -0
  173. package/dist/services/deploy-gate.js +56 -0
  174. package/dist/services/deploy-gate.js.map +1 -0
  175. package/dist/services/deploy-gate.test.d.ts +2 -0
  176. package/dist/services/deploy-gate.test.d.ts.map +1 -0
  177. package/dist/services/deploy-gate.test.js +199 -0
  178. package/dist/services/deploy-gate.test.js.map +1 -0
  179. package/dist/services/dockerfile-generator.d.ts +31 -0
  180. package/dist/services/dockerfile-generator.d.ts.map +1 -0
  181. package/dist/services/dockerfile-generator.js +544 -0
  182. package/dist/services/dockerfile-generator.js.map +1 -0
  183. package/dist/services/dockerfile-generator.test.d.ts +2 -0
  184. package/dist/services/dockerfile-generator.test.d.ts.map +1 -0
  185. package/dist/services/dockerfile-generator.test.js +144 -0
  186. package/dist/services/dockerfile-generator.test.js.map +1 -0
  187. package/dist/services/forgejo.d.ts +58 -0
  188. package/dist/services/forgejo.d.ts.map +1 -0
  189. package/dist/services/forgejo.js +131 -0
  190. package/dist/services/forgejo.js.map +1 -0
  191. package/dist/services/koko.d.ts +153 -0
  192. package/dist/services/koko.d.ts.map +1 -0
  193. package/dist/services/koko.js +260 -0
  194. package/dist/services/koko.js.map +1 -0
  195. package/dist/services/kubernetes.d.ts +16 -0
  196. package/dist/services/kubernetes.d.ts.map +1 -0
  197. package/dist/services/kubernetes.js +102 -0
  198. package/dist/services/kubernetes.js.map +1 -0
  199. package/dist/services/oauth-provisioner.d.ts +30 -0
  200. package/dist/services/oauth-provisioner.d.ts.map +1 -0
  201. package/dist/services/oauth-provisioner.js +182 -0
  202. package/dist/services/oauth-provisioner.js.map +1 -0
  203. package/dist/services/oauth-provisioner.test.d.ts +2 -0
  204. package/dist/services/oauth-provisioner.test.d.ts.map +1 -0
  205. package/dist/services/oauth-provisioner.test.js +349 -0
  206. package/dist/services/oauth-provisioner.test.js.map +1 -0
  207. package/dist/services/pod-diagnostics.d.ts +11 -0
  208. package/dist/services/pod-diagnostics.d.ts.map +1 -0
  209. package/dist/services/pod-diagnostics.js +201 -0
  210. package/dist/services/pod-diagnostics.js.map +1 -0
  211. package/dist/services/rotation-scheduler.d.ts +2 -0
  212. package/dist/services/rotation-scheduler.d.ts.map +1 -0
  213. package/dist/services/rotation-scheduler.js +215 -0
  214. package/dist/services/rotation-scheduler.js.map +1 -0
  215. package/dist/services/storage-credential-manager.d.ts +43 -0
  216. package/dist/services/storage-credential-manager.d.ts.map +1 -0
  217. package/dist/services/storage-credential-manager.js +159 -0
  218. package/dist/services/storage-credential-manager.js.map +1 -0
  219. package/dist/services/storage-provisioner.d.ts +32 -0
  220. package/dist/services/storage-provisioner.d.ts.map +1 -0
  221. package/dist/services/storage-provisioner.js +136 -0
  222. package/dist/services/storage-provisioner.js.map +1 -0
  223. package/dist/services/storage.d.ts +65 -0
  224. package/dist/services/storage.d.ts.map +1 -0
  225. package/dist/services/storage.js +204 -0
  226. package/dist/services/storage.js.map +1 -0
  227. package/dist/services/troubleshooter.d.ts +22 -0
  228. package/dist/services/troubleshooter.d.ts.map +1 -0
  229. package/dist/services/troubleshooter.js +168 -0
  230. package/dist/services/troubleshooter.js.map +1 -0
  231. package/dist/services/vault-client.d.ts +114 -0
  232. package/dist/services/vault-client.d.ts.map +1 -0
  233. package/dist/services/vault-client.js +411 -0
  234. package/dist/services/vault-client.js.map +1 -0
  235. package/dist/utils/logger.d.ts +2 -0
  236. package/dist/utils/logger.d.ts.map +1 -0
  237. package/dist/utils/logger.js +6 -0
  238. package/dist/utils/logger.js.map +1 -0
  239. package/dist/utils/response.d.ts +13 -0
  240. package/dist/utils/response.d.ts.map +1 -0
  241. package/dist/utils/response.js +12 -0
  242. package/dist/utils/response.js.map +1 -0
  243. package/docs/registry-migration.md +301 -0
  244. package/docs/registry-quickstart.md +169 -0
  245. package/ecosystem.config.cjs +14 -0
  246. package/findings.md +168 -0
  247. package/helm/default-service/Chart.yaml +6 -0
  248. package/helm/default-service/templates/deployment.yaml +97 -0
  249. package/helm/default-service/templates/ingress.yaml +43 -0
  250. package/helm/default-service/templates/service.yaml +17 -0
  251. package/helm/default-service/values.yaml +82 -0
  252. package/helm/services/iec-builder/Chart.yaml +6 -0
  253. package/helm/services/iec-builder/templates/_helpers.tpl +61 -0
  254. package/helm/services/iec-builder/templates/deployment.yaml +73 -0
  255. package/helm/services/iec-builder/templates/service.yaml +15 -0
  256. package/helm/services/iec-builder/templates/serviceaccount.yaml +12 -0
  257. package/helm/services/iec-builder/values.yaml +56 -0
  258. package/helm/vault-values.yaml +127 -0
  259. package/package.json +45 -0
  260. package/progress.md +156 -0
  261. package/scripts/.vault-init-keys.json +23 -0
  262. package/scripts/backfill-ownership.ts +113 -0
  263. package/scripts/finalize-mongo-auth.sh +212 -0
  264. package/scripts/setup-ipset.sh +107 -0
  265. package/scripts/setup-mongo-auth.sh +163 -0
  266. package/scripts/setup-neo4j-auth.sh +62 -0
  267. package/scripts/setup-redis-auth.sh +55 -0
  268. package/scripts/setup-registry-secret.sh +71 -0
  269. package/scripts/setup-vault.sh +308 -0
  270. package/src/config/env.ts +117 -0
  271. package/src/index.ts +153 -0
  272. package/src/middleware/auth.ts +294 -0
  273. package/src/middleware/org-access.ts +126 -0
  274. package/src/models/types.ts +288 -0
  275. package/src/routes/ai.ts +115 -0
  276. package/src/routes/audit.ts +121 -0
  277. package/src/routes/builds.ts +320 -0
  278. package/src/routes/cluster.ts +235 -0
  279. package/src/routes/config.ts +369 -0
  280. package/src/routes/databases.ts +201 -0
  281. package/src/routes/db-whitelist.ts +204 -0
  282. package/src/routes/domains.ts +547 -0
  283. package/src/routes/oauth.ts +195 -0
  284. package/src/routes/observability.ts +205 -0
  285. package/src/routes/orgs.ts +330 -0
  286. package/src/routes/platform.ts +134 -0
  287. package/src/routes/rotation.ts +191 -0
  288. package/src/routes/services.ts +290 -0
  289. package/src/routes/storage.ts +153 -0
  290. package/src/routes/users.ts +235 -0
  291. package/src/routes/webhooks.ts +384 -0
  292. package/src/services/__tests__/catalog-storage.test.ts +186 -0
  293. package/src/services/__tests__/deploy-pipeline.integration.test.ts +624 -0
  294. package/src/services/__tests__/pod-diagnostics.test.ts +332 -0
  295. package/src/services/__tests__/storage-credential-manager.test.ts +129 -0
  296. package/src/services/__tests__/storage-provisioner.test.ts +166 -0
  297. package/src/services/__tests__/troubleshooter.test.ts +329 -0
  298. package/src/services/bio-client.ts +189 -0
  299. package/src/services/build-queue.ts +137 -0
  300. package/src/services/builder.ts +1800 -0
  301. package/src/services/catalog.test.ts +1389 -0
  302. package/src/services/catalog.ts +1187 -0
  303. package/src/services/cloudflare.ts +259 -0
  304. package/src/services/config-validator.test.ts +190 -0
  305. package/src/services/config-validator.ts +108 -0
  306. package/src/services/crypto.ts +78 -0
  307. package/src/services/database.ts +122 -0
  308. package/src/services/db-credential-manager.test.ts +101 -0
  309. package/src/services/db-credential-manager.ts +447 -0
  310. package/src/services/db-provisioner.test.ts +602 -0
  311. package/src/services/db-provisioner.ts +589 -0
  312. package/src/services/db-whitelist.test.ts +671 -0
  313. package/src/services/db-whitelist.ts +496 -0
  314. package/src/services/dependency-resolver.test.ts +677 -0
  315. package/src/services/dependency-resolver.ts +319 -0
  316. package/src/services/deploy-gate.test.ts +247 -0
  317. package/src/services/deploy-gate.ts +75 -0
  318. package/src/services/dockerfile-generator.test.ts +401 -0
  319. package/src/services/dockerfile-generator.ts +606 -0
  320. package/src/services/forgejo.ts +212 -0
  321. package/src/services/koko.ts +492 -0
  322. package/src/services/kubernetes.ts +141 -0
  323. package/src/services/oauth-provisioner.test.ts +477 -0
  324. package/src/services/oauth-provisioner.ts +286 -0
  325. package/src/services/pod-diagnostics.ts +261 -0
  326. package/src/services/rotation-scheduler.ts +293 -0
  327. package/src/services/storage-credential-manager.ts +223 -0
  328. package/src/services/storage-provisioner.ts +216 -0
  329. package/src/services/storage.ts +274 -0
  330. package/src/services/troubleshooter.ts +208 -0
  331. package/src/services/vault-client.test.ts +272 -0
  332. package/src/services/vault-client.ts +587 -0
  333. package/src/utils/logger.ts +6 -0
  334. package/src/utils/response.ts +23 -0
  335. package/task_plan.md +171 -0
  336. package/tsconfig.json +20 -0
  337. package/vitest.config.ts +19 -0
@@ -0,0 +1,602 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest'
2
+
3
+ // Use vi.hoisted so the mock fn is available when vi.mock factory runs
4
+ const {
5
+ mockExecSync,
6
+ mockEnsureMongoUser,
7
+ mockEnsureRedisUser,
8
+ mockEnsureNeo4jUser,
9
+ mockBuildMongoUsername,
10
+ mockBuildSharedMongoUsername,
11
+ mockBuildRedisUsername,
12
+ mockBuildNeo4jUsername,
13
+ mockGeneratePassword,
14
+ mockEncrypt,
15
+ mockDecrypt,
16
+ } = vi.hoisted(() => ({
17
+ mockExecSync: vi.fn(),
18
+ mockEnsureMongoUser: vi.fn(),
19
+ mockEnsureRedisUser: vi.fn(),
20
+ mockEnsureNeo4jUser: vi.fn(),
21
+ mockBuildMongoUsername: vi.fn(),
22
+ mockBuildSharedMongoUsername: vi.fn(),
23
+ mockBuildRedisUsername: vi.fn(),
24
+ mockBuildNeo4jUsername: vi.fn(),
25
+ mockGeneratePassword: vi.fn(),
26
+ mockEncrypt: vi.fn(),
27
+ mockDecrypt: vi.fn(),
28
+ }))
29
+
30
+ vi.mock('child_process', () => ({
31
+ execSync: mockExecSync,
32
+ }))
33
+
34
+ vi.mock('../utils/logger.js', () => ({
35
+ logger: {
36
+ info: vi.fn(),
37
+ warn: vi.fn(),
38
+ error: vi.fn(),
39
+ },
40
+ }))
41
+
42
+ vi.mock('../config/env.js', () => ({
43
+ env: {
44
+ MONGODB_URI: 'mongodb://64.23.181.20:27017/builder',
45
+ REDIS_URL: 'redis://64.23.181.20:6379',
46
+ DB_MONGODB_HOST: '64.23.181.20',
47
+ DB_MONGODB_PORT: 27017,
48
+ DB_MONGODB_ADMIN_URI: '',
49
+ DB_REDIS_HOST: '64.23.181.20',
50
+ DB_REDIS_PORT: 6379,
51
+ DB_REDIS_ADMIN_URL: '',
52
+ DB_NEO4J_HOST: '64.23.181.20',
53
+ DB_NEO4J_PORT: 7687,
54
+ DB_NEO4J_ADMIN_URI: '',
55
+ DB_NEO4J_ADMIN_USER: '',
56
+ DB_NEO4J_ADMIN_PASSWORD: '',
57
+ },
58
+ }))
59
+
60
+ vi.mock('./crypto.js', () => ({
61
+ encrypt: mockEncrypt,
62
+ decrypt: mockDecrypt,
63
+ }))
64
+
65
+ vi.mock('./db-credential-manager.js', () => ({
66
+ ensureMongoUser: mockEnsureMongoUser,
67
+ ensureRedisUser: mockEnsureRedisUser,
68
+ ensureNeo4jUser: mockEnsureNeo4jUser,
69
+ buildMongoUsername: mockBuildMongoUsername,
70
+ buildSharedMongoUsername: mockBuildSharedMongoUsername,
71
+ buildRedisUsername: mockBuildRedisUsername,
72
+ buildNeo4jUsername: mockBuildNeo4jUsername,
73
+ generatePassword: mockGeneratePassword,
74
+ }))
75
+
76
+ import { provisionDatabases, provisionConsumedDatabases, buildConnectionString, buildAuthenticatedConnectionString, buildAuthenticatedRedisUrl, type DatabaseSpec, type ConsumesDatabaseSpec } from './db-provisioner.js'
77
+ import { env } from '../config/env.js'
78
+
79
+ describe('db-provisioner', () => {
80
+ beforeEach(() => {
81
+ vi.clearAllMocks()
82
+ mockExecSync.mockReturnValue('')
83
+ mockBuildMongoUsername.mockReturnValue('svc_myapp_prod')
84
+ mockBuildSharedMongoUsername.mockReturnValue('svc_portal_on_bio_prod')
85
+ mockBuildRedisUsername.mockReturnValue('svc_myapp_prod')
86
+ mockBuildNeo4jUsername.mockReturnValue('svc_myapp_prod')
87
+ mockGeneratePassword.mockReturnValue('test-password-123')
88
+ mockEncrypt.mockReturnValue('encrypted-password')
89
+ mockDecrypt.mockReturnValue('decrypted-password')
90
+ mockEnsureMongoUser.mockResolvedValue(true)
91
+ mockEnsureRedisUser.mockResolvedValue(true)
92
+ mockEnsureNeo4jUser.mockResolvedValue(true)
93
+ // Reset admin URIs to empty (default)
94
+ ;(env as Record<string, unknown>).DB_MONGODB_ADMIN_URI = ''
95
+ ;(env as Record<string, unknown>).DB_REDIS_ADMIN_URL = ''
96
+ ;(env as Record<string, unknown>).DB_NEO4J_ADMIN_URI = ''
97
+ })
98
+
99
+ describe('buildConnectionString', () => {
100
+ it('should build mongodb connection string with service name and environment', () => {
101
+ const result = buildConnectionString('mongodb', 'myapp', 'prod')
102
+ expect(result).toBe('mongodb://64.23.181.20:27017/myapp-prod')
103
+ })
104
+
105
+ it('should build mongodb connection string with custom db name', () => {
106
+ const result = buildConnectionString('mongodb', 'myapp', 'sandbox', 'custom-db')
107
+ expect(result).toBe('mongodb://64.23.181.20:27017/custom-db')
108
+ })
109
+
110
+ it('should build redis connection string', () => {
111
+ const result = buildConnectionString('redis', 'myapp', 'prod')
112
+ expect(result).toBe('redis://64.23.181.20:6379/0')
113
+ })
114
+
115
+ it('should return empty string for unknown database type', () => {
116
+ const result = buildConnectionString('postgres' as 'mongodb', 'myapp', 'prod')
117
+ expect(result).toBe('')
118
+ })
119
+
120
+ it('should build neo4j connection string', () => {
121
+ const result = buildConnectionString('neo4j', 'myapp', 'prod')
122
+ expect(result).toBe('bolt://64.23.181.20:7687')
123
+ })
124
+ })
125
+
126
+ describe('buildAuthenticatedConnectionString', () => {
127
+ it('should include user and password in the URI', () => {
128
+ const result = buildAuthenticatedConnectionString('svc_myapp_prod', 'mypass', 'myapp-prod')
129
+ expect(result).toBe('mongodb://svc_myapp_prod:mypass@64.23.181.20:27017/myapp-prod?authSource=myapp-prod')
130
+ })
131
+
132
+ it('should encode special characters in username and password', () => {
133
+ const result = buildAuthenticatedConnectionString('user@host', 'p@ss:w/rd', 'db-name')
134
+ expect(result).toContain(encodeURIComponent('user@host'))
135
+ expect(result).toContain(encodeURIComponent('p@ss:w/rd'))
136
+ })
137
+
138
+ it('should include authSource parameter matching the database name', () => {
139
+ const result = buildAuthenticatedConnectionString('user', 'pass', 'mydb')
140
+ expect(result).toContain('?authSource=mydb')
141
+ })
142
+ })
143
+
144
+ describe('provisionDatabases', () => {
145
+ it('should return empty result when no databases specified', async () => {
146
+ const result = await provisionDatabases('myapp', 'prod', 'myapp-prod', [])
147
+ expect(result).toEqual({ envVars: {}, credentials: [] })
148
+ expect(mockExecSync).not.toHaveBeenCalled()
149
+ })
150
+
151
+ it('should provision mongodb without credentials when admin URI is empty', async () => {
152
+ const databases: DatabaseSpec[] = [{ type: 'mongodb' }]
153
+
154
+ const result = await provisionDatabases('myapp', 'prod', 'myapp-prod', databases)
155
+
156
+ expect(result.envVars).toEqual({
157
+ MONGODB_URI: 'mongodb://64.23.181.20:27017/myapp-prod',
158
+ })
159
+ expect(result.credentials).toEqual([])
160
+ expect(mockEnsureMongoUser).not.toHaveBeenCalled()
161
+ })
162
+
163
+ it('should provision mongodb with credentials when admin URI is configured', async () => {
164
+ ;(env as Record<string, unknown>).DB_MONGODB_ADMIN_URI = 'mongodb://admin:pass@64.23.181.20:27017/admin'
165
+ const databases: DatabaseSpec[] = [{ type: 'mongodb' }]
166
+
167
+ const result = await provisionDatabases('myapp', 'prod', 'myapp-prod', databases)
168
+
169
+ expect(result.envVars.MONGODB_URI).toContain('svc_myapp_prod')
170
+ expect(result.envVars.MONGODB_URI).toContain('test-password-123')
171
+ expect(result.envVars.MONGODB_URI).toContain('authSource=myapp-prod')
172
+ expect(result.credentials).toHaveLength(1)
173
+ expect(result.credentials[0]).toMatchObject({
174
+ type: 'mongodb',
175
+ username: 'svc_myapp_prod',
176
+ passwordEncrypted: 'encrypted-password',
177
+ databaseName: 'myapp-prod',
178
+ environment: 'prod',
179
+ })
180
+ expect(mockEnsureMongoUser).toHaveBeenCalledWith('myapp-prod', 'svc_myapp_prod', 'test-password-123', 'readWrite')
181
+ })
182
+
183
+ it('should reuse existing credentials on redeploy', async () => {
184
+ ;(env as Record<string, unknown>).DB_MONGODB_ADMIN_URI = 'mongodb://admin:pass@64.23.181.20:27017/admin'
185
+ const databases: DatabaseSpec[] = [{ type: 'mongodb' }]
186
+ const existingCredentials = [{
187
+ type: 'mongodb' as const,
188
+ username: 'svc_myapp_prod',
189
+ passwordEncrypted: 'existing-encrypted',
190
+ databaseName: 'myapp-prod',
191
+ environment: 'prod' as const,
192
+ provisionedAt: '2026-01-01T00:00:00.000Z',
193
+ }]
194
+
195
+ const result = await provisionDatabases('myapp', 'prod', 'myapp-prod', databases, existingCredentials)
196
+
197
+ expect(mockDecrypt).toHaveBeenCalledWith('existing-encrypted')
198
+ expect(mockGeneratePassword).not.toHaveBeenCalled()
199
+ expect(result.credentials[0].username).toBe('svc_myapp_prod')
200
+ expect(result.credentials[0].provisionedAt).toBe('2026-01-01T00:00:00.000Z')
201
+ })
202
+
203
+ it('should provision a redis database and return REDIS_URL env var', async () => {
204
+ const databases: DatabaseSpec[] = [{ type: 'redis' }]
205
+
206
+ const result = await provisionDatabases('myapp', 'prod', 'myapp-prod', databases)
207
+
208
+ expect(result.envVars).toEqual({
209
+ REDIS_URL: 'redis://64.23.181.20:6379/0',
210
+ })
211
+ expect(result.credentials).toEqual([])
212
+ })
213
+
214
+ it('should provision a neo4j database and return NEO4J_URI env var', async () => {
215
+ const databases: DatabaseSpec[] = [{ type: 'neo4j' }]
216
+
217
+ const result = await provisionDatabases('myapp', 'prod', 'myapp-prod', databases)
218
+
219
+ expect(result.envVars).toEqual({
220
+ NEO4J_URI: 'bolt://64.23.181.20:7687',
221
+ })
222
+ })
223
+
224
+ it('should provision multiple databases and return all env vars', async () => {
225
+ const databases: DatabaseSpec[] = [
226
+ { type: 'mongodb' },
227
+ { type: 'redis', name: 'myapp-cache' },
228
+ ]
229
+
230
+ const result = await provisionDatabases('myapp', 'sandbox', 'myapp-sandbox', databases)
231
+
232
+ expect(result.envVars.MONGODB_URI).toBe('mongodb://64.23.181.20:27017/myapp-sandbox')
233
+ expect(result.envVars.REDIS_URL).toBe('redis://64.23.181.20:6379/0')
234
+ expect(mockExecSync).toHaveBeenCalledTimes(2)
235
+ })
236
+
237
+ it('should use custom name for mongodb when provided', async () => {
238
+ const databases: DatabaseSpec[] = [{ type: 'mongodb', name: 'custom-db' }]
239
+
240
+ const result = await provisionDatabases('myapp', 'prod', 'myapp-prod', databases)
241
+
242
+ expect(result.envVars).toEqual({
243
+ MONGODB_URI: 'mongodb://64.23.181.20:27017/custom-db',
244
+ })
245
+ })
246
+
247
+ it('should skip unknown database types with warning', async () => {
248
+ const databases: DatabaseSpec[] = [
249
+ { type: 'postgres' as 'mongodb' },
250
+ { type: 'mongodb' },
251
+ ]
252
+
253
+ const result = await provisionDatabases('myapp', 'prod', 'myapp-prod', databases)
254
+
255
+ expect(result.envVars.MONGODB_URI).toBe('mongodb://64.23.181.20:27017/myapp-prod')
256
+ expect(mockExecSync).toHaveBeenCalledTimes(1)
257
+ })
258
+
259
+ it('should be idempotent using kubectl apply with dry-run pipe', async () => {
260
+ const databases: DatabaseSpec[] = [{ type: 'mongodb' }]
261
+
262
+ await provisionDatabases('myapp', 'prod', 'myapp-prod', databases)
263
+
264
+ const kubectlCommand = mockExecSync.mock.calls[0][0] as string
265
+ expect(kubectlCommand).toContain('--dry-run=client -o yaml | kubectl apply -f -')
266
+ })
267
+
268
+ it('should create secret in the correct namespace', async () => {
269
+ const databases: DatabaseSpec[] = [{ type: 'mongodb' }]
270
+
271
+ await provisionDatabases('myapp', 'prod', 'myapp-prod', databases)
272
+
273
+ const kubectlCommand = mockExecSync.mock.calls[0][0] as string
274
+ expect(kubectlCommand).toContain('--namespace myapp-prod')
275
+ })
276
+
277
+ it('should name the secret with service name and db type', async () => {
278
+ const databases: DatabaseSpec[] = [{ type: 'mongodb' }]
279
+
280
+ await provisionDatabases('myapp', 'prod', 'myapp-prod', databases)
281
+
282
+ const kubectlCommand = mockExecSync.mock.calls[0][0] as string
283
+ expect(kubectlCommand).toContain('myapp-db-mongodb')
284
+ })
285
+
286
+ it('should handle kubectl failure gracefully', async () => {
287
+ mockExecSync.mockImplementation(() => {
288
+ throw new Error('kubectl: command not found')
289
+ })
290
+
291
+ const databases: DatabaseSpec[] = [{ type: 'mongodb' }]
292
+
293
+ await expect(
294
+ provisionDatabases('myapp', 'prod', 'myapp-prod', databases)
295
+ ).rejects.toThrow('Failed to provision mongodb database')
296
+ })
297
+
298
+ it('should fall back to unauthenticated when ensureMongoUser returns false', async () => {
299
+ ;(env as Record<string, unknown>).DB_MONGODB_ADMIN_URI = 'mongodb://admin:pass@64.23.181.20:27017/admin'
300
+ mockEnsureMongoUser.mockResolvedValue(false)
301
+ const databases: DatabaseSpec[] = [{ type: 'mongodb' }]
302
+
303
+ const result = await provisionDatabases('myapp', 'prod', 'myapp-prod', databases)
304
+
305
+ expect(result.envVars.MONGODB_URI).toBe('mongodb://64.23.181.20:27017/myapp-prod')
306
+ expect(result.credentials).toEqual([])
307
+ })
308
+ })
309
+
310
+ describe('provisionConsumedDatabases', () => {
311
+ const mockLookupOwner = vi.fn()
312
+
313
+ beforeEach(() => {
314
+ mockLookupOwner.mockReset()
315
+ ;(env as Record<string, unknown>).DB_MONGODB_ADMIN_URI = 'mongodb://admin:pass@64.23.181.20:27017/admin'
316
+ })
317
+
318
+ it('should return empty result when no consumed databases', async () => {
319
+ const result = await provisionConsumedDatabases('portal', 'prod', 'portal-prod', [], [], mockLookupOwner)
320
+ expect(result).toEqual({ envVars: {}, credentials: [] })
321
+ expect(mockLookupOwner).not.toHaveBeenCalled()
322
+ })
323
+
324
+ it('should throw when owner service is not found', async () => {
325
+ mockLookupOwner.mockResolvedValue(null)
326
+ const consumed: ConsumesDatabaseSpec[] = [{ service: 'bio', type: 'mongodb' }]
327
+
328
+ await expect(
329
+ provisionConsumedDatabases('portal', 'prod', 'portal-prod', consumed, [], mockLookupOwner)
330
+ ).rejects.toThrow('service not found')
331
+ })
332
+
333
+ it('should throw when owner has no shared databases of requested type', async () => {
334
+ mockLookupOwner.mockResolvedValue({ name: 'bio', databaseSharing: [] })
335
+ const consumed: ConsumesDatabaseSpec[] = [{ service: 'bio', type: 'mongodb' }]
336
+
337
+ await expect(
338
+ provisionConsumedDatabases('portal', 'prod', 'portal-prod', consumed, [], mockLookupOwner)
339
+ ).rejects.toThrow('no shared databases of type "mongodb"')
340
+ })
341
+
342
+ it('should throw when consumer is not authorized in sharedWith', async () => {
343
+ mockLookupOwner.mockResolvedValue({
344
+ name: 'bio',
345
+ databaseSharing: [{
346
+ type: 'mongodb',
347
+ databaseName: 'bio',
348
+ sharedWith: [{ service: 'other-svc', access: 'readWrite' }],
349
+ }],
350
+ })
351
+ const consumed: ConsumesDatabaseSpec[] = [{ service: 'bio', type: 'mongodb' }]
352
+
353
+ await expect(
354
+ provisionConsumedDatabases('portal', 'prod', 'portal-prod', consumed, [], mockLookupOwner)
355
+ ).rejects.toThrow('not authorized')
356
+ })
357
+
358
+ it('should provision consumed database with readWrite access', async () => {
359
+ mockLookupOwner.mockResolvedValue({
360
+ name: 'bio',
361
+ databaseSharing: [{
362
+ type: 'mongodb',
363
+ databaseName: 'bio',
364
+ sharedWith: [{ service: 'portal', access: 'readWrite' }],
365
+ }],
366
+ })
367
+ const consumed: ConsumesDatabaseSpec[] = [{ service: 'bio', type: 'mongodb' }]
368
+
369
+ const result = await provisionConsumedDatabases('portal', 'prod', 'portal-prod', consumed, [], mockLookupOwner)
370
+
371
+ expect(mockEnsureMongoUser).toHaveBeenCalledWith('bio', 'svc_portal_on_bio_prod', 'test-password-123', 'readWrite')
372
+ expect(result.envVars).toHaveProperty('BIO_MONGODB_URI')
373
+ expect(result.envVars.BIO_MONGODB_URI).toContain('svc_portal_on_bio_prod')
374
+ expect(result.credentials).toHaveLength(1)
375
+ })
376
+
377
+ it('should provision consumed database with readOnly access', async () => {
378
+ mockLookupOwner.mockResolvedValue({
379
+ name: 'bio',
380
+ databaseSharing: [{
381
+ type: 'mongodb',
382
+ databaseName: 'bio',
383
+ sharedWith: [{ service: 'portal', access: 'readOnly' }],
384
+ }],
385
+ })
386
+ const consumed: ConsumesDatabaseSpec[] = [{ service: 'bio', type: 'mongodb' }]
387
+
388
+ await provisionConsumedDatabases('portal', 'prod', 'portal-prod', consumed, [], mockLookupOwner)
389
+
390
+ expect(mockEnsureMongoUser).toHaveBeenCalledWith('bio', 'svc_portal_on_bio_prod', 'test-password-123', 'read')
391
+ })
392
+
393
+ it('should use custom envVar when specified', async () => {
394
+ mockLookupOwner.mockResolvedValue({
395
+ name: 'bio',
396
+ databaseSharing: [{
397
+ type: 'mongodb',
398
+ databaseName: 'bio',
399
+ sharedWith: [{ service: 'portal', access: 'readWrite' }],
400
+ }],
401
+ })
402
+ const consumed: ConsumesDatabaseSpec[] = [{ service: 'bio', type: 'mongodb', envVar: 'MONGODB_URI' }]
403
+
404
+ const result = await provisionConsumedDatabases('portal', 'prod', 'portal-prod', consumed, [], mockLookupOwner)
405
+
406
+ expect(result.envVars).toHaveProperty('MONGODB_URI')
407
+ expect(result.envVars).not.toHaveProperty('BIO_MONGODB_URI')
408
+ })
409
+
410
+ it('should create K8s secret with correct naming convention', async () => {
411
+ mockLookupOwner.mockResolvedValue({
412
+ name: 'bio',
413
+ databaseSharing: [{
414
+ type: 'mongodb',
415
+ databaseName: 'bio',
416
+ sharedWith: [{ service: 'portal', access: 'readWrite' }],
417
+ }],
418
+ })
419
+ const consumed: ConsumesDatabaseSpec[] = [{ service: 'bio', type: 'mongodb' }]
420
+
421
+ await provisionConsumedDatabases('portal', 'prod', 'portal-prod', consumed, [], mockLookupOwner)
422
+
423
+ const kubectlCommand = mockExecSync.mock.calls[0][0] as string
424
+ expect(kubectlCommand).toContain('portal-shared-bio-db-mongodb')
425
+ expect(kubectlCommand).toContain('--namespace portal-prod')
426
+ })
427
+ })
428
+
429
+ describe('buildAuthenticatedRedisUrl', () => {
430
+ it('should include user and password in the URL', () => {
431
+ const result = buildAuthenticatedRedisUrl('svc_myapp_prod', 'mypass')
432
+ expect(result).toBe('redis://svc_myapp_prod:mypass@64.23.181.20:6379/0')
433
+ })
434
+
435
+ it('should encode special characters in username and password', () => {
436
+ const result = buildAuthenticatedRedisUrl('user@host', 'p@ss:w/rd')
437
+ expect(result).toContain(encodeURIComponent('user@host'))
438
+ expect(result).toContain(encodeURIComponent('p@ss:w/rd'))
439
+ })
440
+ })
441
+
442
+ describe('Redis provisioning', () => {
443
+ it('should provision redis without credentials when admin URL is empty', async () => {
444
+ const databases: DatabaseSpec[] = [{ type: 'redis' }]
445
+
446
+ const result = await provisionDatabases('myapp', 'prod', 'myapp-prod', databases)
447
+
448
+ expect(result.envVars).toEqual({ REDIS_URL: 'redis://64.23.181.20:6379/0' })
449
+ expect(result.credentials).toEqual([])
450
+ expect(mockEnsureRedisUser).not.toHaveBeenCalled()
451
+ })
452
+
453
+ it('should provision redis with credentials when admin URL is configured', async () => {
454
+ ;(env as Record<string, unknown>).DB_REDIS_ADMIN_URL = 'redis://redis-admin:pass@64.23.181.20:6379/0'
455
+ const databases: DatabaseSpec[] = [{ type: 'redis' }]
456
+
457
+ const result = await provisionDatabases('myapp', 'prod', 'myapp-prod', databases)
458
+
459
+ expect(result.envVars.REDIS_URL).toContain('svc_myapp_prod')
460
+ expect(result.envVars.REDIS_URL).toContain('test-password-123')
461
+ expect(result.envVars.REDIS_URL).toContain('64.23.181.20:6379/0')
462
+ expect(result.credentials).toHaveLength(1)
463
+ expect(result.credentials[0]).toMatchObject({
464
+ type: 'redis',
465
+ username: 'svc_myapp_prod',
466
+ passwordEncrypted: 'encrypted-password',
467
+ databaseName: 'redis-myapp-prod',
468
+ environment: 'prod',
469
+ })
470
+ expect(mockEnsureRedisUser).toHaveBeenCalledWith('svc_myapp_prod', 'test-password-123')
471
+ })
472
+
473
+ it('should reuse existing redis credentials on redeploy', async () => {
474
+ ;(env as Record<string, unknown>).DB_REDIS_ADMIN_URL = 'redis://redis-admin:pass@64.23.181.20:6379/0'
475
+ const databases: DatabaseSpec[] = [{ type: 'redis' }]
476
+ const existingCredentials = [{
477
+ type: 'redis' as const,
478
+ username: 'svc_myapp_prod',
479
+ passwordEncrypted: 'existing-encrypted',
480
+ databaseName: 'redis-myapp-prod',
481
+ environment: 'prod' as const,
482
+ provisionedAt: '2026-01-01T00:00:00.000Z',
483
+ }]
484
+
485
+ const result = await provisionDatabases('myapp', 'prod', 'myapp-prod', databases, existingCredentials)
486
+
487
+ expect(mockDecrypt).toHaveBeenCalledWith('existing-encrypted')
488
+ expect(mockGeneratePassword).not.toHaveBeenCalled()
489
+ expect(result.credentials[0].username).toBe('svc_myapp_prod')
490
+ expect(result.credentials[0].provisionedAt).toBe('2026-01-01T00:00:00.000Z')
491
+ })
492
+
493
+ it('should fall back to unauthenticated when ensureRedisUser returns false', async () => {
494
+ ;(env as Record<string, unknown>).DB_REDIS_ADMIN_URL = 'redis://redis-admin:pass@64.23.181.20:6379/0'
495
+ mockEnsureRedisUser.mockResolvedValue(false)
496
+ const databases: DatabaseSpec[] = [{ type: 'redis' }]
497
+
498
+ const result = await provisionDatabases('myapp', 'prod', 'myapp-prod', databases)
499
+
500
+ expect(result.envVars.REDIS_URL).toBe('redis://64.23.181.20:6379/0')
501
+ expect(result.credentials).toEqual([])
502
+ })
503
+ })
504
+
505
+ describe('Neo4j provisioning', () => {
506
+ it('should provision neo4j without credentials when admin URI is empty', async () => {
507
+ const databases: DatabaseSpec[] = [{ type: 'neo4j' }]
508
+
509
+ const result = await provisionDatabases('myapp', 'prod', 'myapp-prod', databases)
510
+
511
+ expect(result.envVars).toEqual({ NEO4J_URI: 'bolt://64.23.181.20:7687' })
512
+ expect(result.credentials).toEqual([])
513
+ expect(mockEnsureNeo4jUser).not.toHaveBeenCalled()
514
+ })
515
+
516
+ it('should provision neo4j with credentials when admin URI is configured', async () => {
517
+ ;(env as Record<string, unknown>).DB_NEO4J_ADMIN_URI = 'bolt://64.23.181.20:7687'
518
+ const databases: DatabaseSpec[] = [{ type: 'neo4j' }]
519
+
520
+ const result = await provisionDatabases('myapp', 'prod', 'myapp-prod', databases)
521
+
522
+ expect(result.envVars.NEO4J_URI).toBe('bolt://64.23.181.20:7687')
523
+ expect(result.envVars.NEO4J_USERNAME).toBe('svc_myapp_prod')
524
+ expect(result.envVars.NEO4J_PASSWORD).toBe('test-password-123')
525
+ expect(result.credentials).toHaveLength(1)
526
+ expect(result.credentials[0]).toMatchObject({
527
+ type: 'neo4j',
528
+ username: 'svc_myapp_prod',
529
+ passwordEncrypted: 'encrypted-password',
530
+ databaseName: 'myapp-prod',
531
+ environment: 'prod',
532
+ })
533
+ expect(mockEnsureNeo4jUser).toHaveBeenCalledWith('myapp-prod', 'svc_myapp_prod', 'test-password-123')
534
+ })
535
+
536
+ it('should create K8s secret with 3 keys for neo4j', async () => {
537
+ ;(env as Record<string, unknown>).DB_NEO4J_ADMIN_URI = 'bolt://64.23.181.20:7687'
538
+ const databases: DatabaseSpec[] = [{ type: 'neo4j' }]
539
+
540
+ await provisionDatabases('myapp', 'prod', 'myapp-prod', databases)
541
+
542
+ const kubectlCommand = mockExecSync.mock.calls[0][0] as string
543
+ expect(kubectlCommand).toContain('--from-literal=NEO4J_URI=')
544
+ expect(kubectlCommand).toContain('--from-literal=NEO4J_USERNAME=')
545
+ expect(kubectlCommand).toContain('--from-literal=NEO4J_PASSWORD=')
546
+ })
547
+
548
+ it('should reuse existing neo4j credentials on redeploy', async () => {
549
+ ;(env as Record<string, unknown>).DB_NEO4J_ADMIN_URI = 'bolt://64.23.181.20:7687'
550
+ const databases: DatabaseSpec[] = [{ type: 'neo4j' }]
551
+ const existingCredentials = [{
552
+ type: 'neo4j' as const,
553
+ username: 'svc_myapp_prod',
554
+ passwordEncrypted: 'existing-encrypted',
555
+ databaseName: 'myapp-prod',
556
+ environment: 'prod' as const,
557
+ provisionedAt: '2026-01-01T00:00:00.000Z',
558
+ }]
559
+
560
+ const result = await provisionDatabases('myapp', 'prod', 'myapp-prod', databases, existingCredentials)
561
+
562
+ expect(mockDecrypt).toHaveBeenCalledWith('existing-encrypted')
563
+ expect(mockGeneratePassword).not.toHaveBeenCalled()
564
+ expect(result.credentials[0].username).toBe('svc_myapp_prod')
565
+ expect(result.credentials[0].provisionedAt).toBe('2026-01-01T00:00:00.000Z')
566
+ })
567
+
568
+ it('should fall back to unauthenticated when ensureNeo4jUser returns false', async () => {
569
+ ;(env as Record<string, unknown>).DB_NEO4J_ADMIN_URI = 'bolt://64.23.181.20:7687'
570
+ mockEnsureNeo4jUser.mockResolvedValue(false)
571
+ const databases: DatabaseSpec[] = [{ type: 'neo4j' }]
572
+
573
+ const result = await provisionDatabases('myapp', 'prod', 'myapp-prod', databases)
574
+
575
+ expect(result.envVars.NEO4J_URI).toBe('bolt://64.23.181.20:7687')
576
+ expect(result.credentials).toEqual([])
577
+ })
578
+ })
579
+
580
+ describe('Multi-type provisioning', () => {
581
+ it('should provision all three database types together', async () => {
582
+ ;(env as Record<string, unknown>).DB_MONGODB_ADMIN_URI = 'mongodb://admin:pass@64.23.181.20:27017/admin'
583
+ ;(env as Record<string, unknown>).DB_REDIS_ADMIN_URL = 'redis://redis-admin:pass@64.23.181.20:6379/0'
584
+ ;(env as Record<string, unknown>).DB_NEO4J_ADMIN_URI = 'bolt://64.23.181.20:7687'
585
+ const databases: DatabaseSpec[] = [
586
+ { type: 'mongodb' },
587
+ { type: 'redis' },
588
+ { type: 'neo4j' },
589
+ ]
590
+
591
+ const result = await provisionDatabases('myapp', 'prod', 'myapp-prod', databases)
592
+
593
+ expect(result.envVars.MONGODB_URI).toContain('authSource=myapp-prod')
594
+ expect(result.envVars.REDIS_URL).toContain('svc_myapp_prod')
595
+ expect(result.envVars.NEO4J_URI).toBe('bolt://64.23.181.20:7687')
596
+ expect(result.envVars.NEO4J_USERNAME).toBe('svc_myapp_prod')
597
+ expect(result.envVars.NEO4J_PASSWORD).toBe('test-password-123')
598
+ expect(result.credentials).toHaveLength(3)
599
+ expect(mockExecSync).toHaveBeenCalledTimes(3)
600
+ })
601
+ })
602
+ })