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,1389 @@
1
+ import { describe, it, expect, vi, afterEach, beforeAll } from 'vitest'
2
+ import { mkdir, writeFile, rm } from 'fs/promises'
3
+ import { join } from 'path'
4
+ import { tmpdir } from 'os'
5
+ import { randomUUID } from 'crypto'
6
+
7
+ vi.mock('../utils/logger.js', () => ({
8
+ logger: {
9
+ info: vi.fn(),
10
+ warn: vi.fn(),
11
+ error: vi.fn(),
12
+ },
13
+ }))
14
+
15
+ const mockEnv = vi.hoisted(() => ({
16
+ ENFORCE_MINIMUM_CATALOG_VERSION: 'false',
17
+ }))
18
+
19
+ vi.mock('../config/env.js', () => ({
20
+ env: mockEnv,
21
+ }))
22
+
23
+ import { parseCatalogInfo } from './catalog.js'
24
+
25
+ async function createFixture(catalogYaml: string): Promise<string> {
26
+ const dir = join(tmpdir(), `iec-catalog-test-${randomUUID()}`)
27
+ await mkdir(dir, { recursive: true })
28
+ await writeFile(join(dir, 'catalog-info.yaml'), catalogYaml, 'utf-8')
29
+ return dir
30
+ }
31
+
32
+ describe('catalog defaults', () => {
33
+ const tempDirs: string[] = []
34
+
35
+ afterEach(async () => {
36
+ await Promise.all(tempDirs.map(d => rm(d, { recursive: true, force: true })))
37
+ tempDirs.splice(0, tempDirs.length)
38
+ })
39
+
40
+ it('should apply correct static framework defaults', async () => {
41
+ const dir = await createFixture(`
42
+ apiVersion: backstage.io/v1alpha1
43
+ kind: Component
44
+ metadata:
45
+ name: my-static-site
46
+ description: A simple static site
47
+ annotations:
48
+ insureco.io/catalog-version: "1"
49
+ insureco.io/framework: static
50
+ spec:
51
+ type: service
52
+ lifecycle: production
53
+ owner: test-org
54
+ `)
55
+ tempDirs.push(dir)
56
+
57
+ const result = await parseCatalogInfo(dir)
58
+ expect(result).not.toBeNull()
59
+ expect(result!.framework).toBe('static')
60
+ expect(result!.buildCommand).toBe('true')
61
+ expect(result!.outputDir).toBe('.')
62
+ expect(result!.port).toBe(80)
63
+ expect(result!.healthEndpoint).toBe('/health')
64
+ // startCommand falls back to 'npm start' because '' is falsy in the || chain
65
+ expect(result!.startCommand).toBe('npm start')
66
+ })
67
+
68
+ it('should allow annotation overrides for static framework', async () => {
69
+ const dir = await createFixture(`
70
+ apiVersion: backstage.io/v1alpha1
71
+ kind: Component
72
+ metadata:
73
+ name: my-static-site
74
+ annotations:
75
+ insureco.io/catalog-version: "1"
76
+ insureco.io/framework: static
77
+ insureco.io/build-command: "npm run build"
78
+ insureco.io/output-dir: "dist"
79
+ insureco.io/port: "8080"
80
+ spec:
81
+ type: service
82
+ lifecycle: production
83
+ owner: test-org
84
+ `)
85
+ tempDirs.push(dir)
86
+
87
+ const result = await parseCatalogInfo(dir)
88
+ expect(result).not.toBeNull()
89
+ expect(result!.framework).toBe('static')
90
+ expect(result!.buildCommand).toBe('npm run build')
91
+ expect(result!.outputDir).toBe('dist')
92
+ expect(result!.port).toBe(8080)
93
+ })
94
+
95
+ it('should return null when no catalog-info.yaml exists', async () => {
96
+ const dir = join(tmpdir(), `iec-catalog-test-${randomUUID()}`)
97
+ await mkdir(dir, { recursive: true })
98
+ tempDirs.push(dir)
99
+
100
+ const result = await parseCatalogInfo(dir)
101
+ expect(result).toBeNull()
102
+ })
103
+
104
+ it('should apply express framework defaults', async () => {
105
+ const dir = await createFixture(`
106
+ apiVersion: backstage.io/v1alpha1
107
+ kind: Component
108
+ metadata:
109
+ name: my-api
110
+ annotations:
111
+ insureco.io/catalog-version: "1"
112
+ insureco.io/framework: express
113
+ spec:
114
+ type: service
115
+ lifecycle: production
116
+ owner: test-org
117
+ `)
118
+ tempDirs.push(dir)
119
+
120
+ const result = await parseCatalogInfo(dir)
121
+ expect(result).not.toBeNull()
122
+ expect(result!.framework).toBe('express')
123
+ expect(result!.buildCommand).toBe('npm run build')
124
+ expect(result!.outputDir).toBe('dist')
125
+ expect(result!.port).toBe(3000)
126
+ expect(result!.healthEndpoint).toBe('/health')
127
+ })
128
+ })
129
+
130
+ describe('catalog version validation', () => {
131
+ const tempDirs: string[] = []
132
+
133
+ afterEach(async () => {
134
+ await Promise.all(tempDirs.map(d => rm(d, { recursive: true, force: true })))
135
+ tempDirs.splice(0, tempDirs.length)
136
+ })
137
+
138
+ it('should reject catalog with missing version annotation', async () => {
139
+ const dir = await createFixture(`
140
+ apiVersion: backstage.io/v1alpha1
141
+ kind: Component
142
+ metadata:
143
+ name: my-api
144
+ annotations:
145
+ insureco.io/framework: express
146
+ spec:
147
+ type: service
148
+ lifecycle: production
149
+ owner: test-org
150
+ `)
151
+ tempDirs.push(dir)
152
+
153
+ await expect(parseCatalogInfo(dir)).rejects.toThrow(
154
+ 'missing the required "insureco.io/catalog-version" annotation'
155
+ )
156
+ })
157
+
158
+ it('should reject catalog with invalid semver format', async () => {
159
+ const dir = await createFixture(`
160
+ apiVersion: backstage.io/v1alpha1
161
+ kind: Component
162
+ metadata:
163
+ name: my-api
164
+ annotations:
165
+ insureco.io/catalog-version: "0"
166
+ insureco.io/framework: express
167
+ spec:
168
+ type: service
169
+ lifecycle: production
170
+ owner: test-org
171
+ `)
172
+ tempDirs.push(dir)
173
+
174
+ await expect(parseCatalogInfo(dir)).rejects.toThrow(
175
+ 'Invalid catalog version format'
176
+ )
177
+ })
178
+
179
+ it('should include docs URL in missing version error', async () => {
180
+ const dir = await createFixture(`
181
+ apiVersion: backstage.io/v1alpha1
182
+ kind: Component
183
+ metadata:
184
+ name: my-api
185
+ annotations:
186
+ insureco.io/framework: express
187
+ spec:
188
+ type: service
189
+ lifecycle: production
190
+ owner: test-org
191
+ `)
192
+ tempDirs.push(dir)
193
+
194
+ await expect(parseCatalogInfo(dir)).rejects.toThrow(
195
+ 'https://tawa.insureco.io/docs/catalog-reference'
196
+ )
197
+ })
198
+
199
+ it('should reject catalog with incompatible major version', async () => {
200
+ const dir = await createFixture(`
201
+ apiVersion: backstage.io/v1alpha1
202
+ kind: Component
203
+ metadata:
204
+ name: my-api
205
+ annotations:
206
+ insureco.io/catalog-version: "1.0.0"
207
+ insureco.io/framework: express
208
+ spec:
209
+ type: service
210
+ lifecycle: production
211
+ owner: test-org
212
+ `)
213
+ tempDirs.push(dir)
214
+
215
+ await expect(parseCatalogInfo(dir)).rejects.toThrow(
216
+ 'not compatible with this builder'
217
+ )
218
+ })
219
+
220
+ it('should reject catalog version ahead of builder', async () => {
221
+ const dir = await createFixture(`
222
+ apiVersion: backstage.io/v1alpha1
223
+ kind: Component
224
+ metadata:
225
+ name: my-api
226
+ annotations:
227
+ insureco.io/catalog-version: "0.9.0"
228
+ insureco.io/framework: express
229
+ insureco.io/health-endpoint: /health
230
+ spec:
231
+ type: service
232
+ lifecycle: production
233
+ owner: test-org
234
+ `)
235
+ tempDirs.push(dir)
236
+
237
+ await expect(parseCatalogInfo(dir)).rejects.toThrow(
238
+ 'not supported by this builder'
239
+ )
240
+ })
241
+
242
+ it('should warn but accept catalog with lower minor version', async () => {
243
+ const dir = await createFixture(`
244
+ apiVersion: backstage.io/v1alpha1
245
+ kind: Component
246
+ metadata:
247
+ name: my-api
248
+ annotations:
249
+ insureco.io/catalog-version: "0.1.0"
250
+ insureco.io/framework: express
251
+ spec:
252
+ type: service
253
+ lifecycle: production
254
+ owner: test-org
255
+ `)
256
+ tempDirs.push(dir)
257
+
258
+ const result = await parseCatalogInfo(dir)
259
+ expect(result).not.toBeNull()
260
+ expect(result!.catalogVersion).toBe('0.1.0')
261
+ })
262
+
263
+ it('should accept legacy version "1" as "0.1.0"', async () => {
264
+ const dir = await createFixture(`
265
+ apiVersion: backstage.io/v1alpha1
266
+ kind: Component
267
+ metadata:
268
+ name: my-api
269
+ annotations:
270
+ insureco.io/catalog-version: "1"
271
+ insureco.io/framework: express
272
+ spec:
273
+ type: service
274
+ lifecycle: production
275
+ owner: test-org
276
+ `)
277
+ tempDirs.push(dir)
278
+
279
+ const result = await parseCatalogInfo(dir)
280
+ expect(result).not.toBeNull()
281
+ expect(result!.catalogVersion).toBe('0.1.0')
282
+ })
283
+
284
+ it('should accept catalog with current version 0.2.0', async () => {
285
+ const dir = await createFixture(`
286
+ apiVersion: backstage.io/v1alpha1
287
+ kind: Component
288
+ metadata:
289
+ name: my-api
290
+ annotations:
291
+ insureco.io/catalog-version: "0.2.0"
292
+ insureco.io/framework: express
293
+ insureco.io/health-endpoint: /health
294
+ spec:
295
+ type: service
296
+ lifecycle: production
297
+ owner: test-org
298
+ `)
299
+ tempDirs.push(dir)
300
+
301
+ const result = await parseCatalogInfo(dir)
302
+ expect(result).not.toBeNull()
303
+ expect(result!.catalogVersion).toBe('0.2.0')
304
+ })
305
+
306
+ it('should reject 0.2.0 catalog missing required framework annotation', async () => {
307
+ const dir = await createFixture(`
308
+ apiVersion: backstage.io/v1alpha1
309
+ kind: Component
310
+ metadata:
311
+ name: my-api
312
+ annotations:
313
+ insureco.io/catalog-version: "0.2.0"
314
+ insureco.io/health-endpoint: /health
315
+ spec:
316
+ type: service
317
+ lifecycle: production
318
+ owner: test-org
319
+ `)
320
+ tempDirs.push(dir)
321
+
322
+ await expect(parseCatalogInfo(dir)).rejects.toThrow(
323
+ 'requires the "insureco.io/framework" annotation'
324
+ )
325
+ })
326
+
327
+ it('should reject 0.2.0 catalog missing required health-endpoint annotation', async () => {
328
+ const dir = await createFixture(`
329
+ apiVersion: backstage.io/v1alpha1
330
+ kind: Component
331
+ metadata:
332
+ name: my-api
333
+ annotations:
334
+ insureco.io/catalog-version: "0.2.0"
335
+ insureco.io/framework: express
336
+ spec:
337
+ type: service
338
+ lifecycle: production
339
+ owner: test-org
340
+ `)
341
+ tempDirs.push(dir)
342
+
343
+ await expect(parseCatalogInfo(dir)).rejects.toThrow(
344
+ 'requires the "insureco.io/health-endpoint" annotation'
345
+ )
346
+ })
347
+
348
+ it('should accept catalog with current version', async () => {
349
+ const dir = await createFixture(`
350
+ apiVersion: backstage.io/v1alpha1
351
+ kind: Component
352
+ metadata:
353
+ name: my-api
354
+ annotations:
355
+ insureco.io/catalog-version: "1"
356
+ insureco.io/framework: express
357
+ spec:
358
+ type: service
359
+ lifecycle: production
360
+ owner: test-org
361
+ `)
362
+ tempDirs.push(dir)
363
+
364
+ const result = await parseCatalogInfo(dir)
365
+ expect(result).not.toBeNull()
366
+ expect(result!.name).toBe('my-api')
367
+ expect(result!.framework).toBe('express')
368
+ })
369
+
370
+ it('should reject catalog with no annotations at all', async () => {
371
+ const dir = await createFixture(`
372
+ apiVersion: backstage.io/v1alpha1
373
+ kind: Component
374
+ metadata:
375
+ name: my-api
376
+ spec:
377
+ type: service
378
+ lifecycle: production
379
+ owner: test-org
380
+ `)
381
+ tempDirs.push(dir)
382
+
383
+ await expect(parseCatalogInfo(dir)).rejects.toThrow(
384
+ 'missing the required "insureco.io/catalog-version" annotation'
385
+ )
386
+ })
387
+ })
388
+
389
+ describe('catalog config declarations', () => {
390
+ const tempDirs: string[] = []
391
+
392
+ afterEach(async () => {
393
+ await Promise.all(tempDirs.map(d => rm(d, { recursive: true, force: true })))
394
+ tempDirs.splice(0, tempDirs.length)
395
+ })
396
+
397
+ it('should parse spec.config declarations with all fields', async () => {
398
+ const dir = await createFixture(`
399
+ apiVersion: backstage.io/v1alpha1
400
+ kind: Component
401
+ metadata:
402
+ name: my-api
403
+ annotations:
404
+ insureco.io/catalog-version: "1"
405
+ insureco.io/framework: express
406
+ spec:
407
+ type: service
408
+ lifecycle: production
409
+ owner: test-org
410
+ config:
411
+ - key: NEXTAUTH_SECRET
412
+ secret: true
413
+ required: true
414
+ - key: LOG_LEVEL
415
+ default: "info"
416
+ - key: STRIPE_API_KEY
417
+ secret: true
418
+ required: true
419
+ `)
420
+ tempDirs.push(dir)
421
+
422
+ const result = await parseCatalogInfo(dir)
423
+ expect(result).not.toBeNull()
424
+ expect(result!.configDeclarations).toEqual([
425
+ { key: 'NEXTAUTH_SECRET', secret: true, required: true },
426
+ { key: 'LOG_LEVEL', default: 'info' },
427
+ { key: 'STRIPE_API_KEY', secret: true, required: true },
428
+ ])
429
+ })
430
+
431
+ it('should default to empty array when spec.config is missing', async () => {
432
+ const dir = await createFixture(`
433
+ apiVersion: backstage.io/v1alpha1
434
+ kind: Component
435
+ metadata:
436
+ name: my-api
437
+ annotations:
438
+ insureco.io/catalog-version: "1"
439
+ insureco.io/framework: express
440
+ spec:
441
+ type: service
442
+ lifecycle: production
443
+ owner: test-org
444
+ `)
445
+ tempDirs.push(dir)
446
+
447
+ const result = await parseCatalogInfo(dir)
448
+ expect(result).not.toBeNull()
449
+ expect(result!.configDeclarations).toEqual([])
450
+ })
451
+
452
+ it('should reject declarations with invalid key names', async () => {
453
+ const dir = await createFixture(`
454
+ apiVersion: backstage.io/v1alpha1
455
+ kind: Component
456
+ metadata:
457
+ name: my-api
458
+ annotations:
459
+ insureco.io/catalog-version: "1"
460
+ insureco.io/framework: express
461
+ spec:
462
+ type: service
463
+ lifecycle: production
464
+ owner: test-org
465
+ config:
466
+ - key: "INVALID KEY WITH SPACES"
467
+ `)
468
+ tempDirs.push(dir)
469
+
470
+ await expect(parseCatalogInfo(dir)).rejects.toThrow('Invalid config declaration key')
471
+ })
472
+
473
+ it('should reject duplicate declaration keys', async () => {
474
+ const dir = await createFixture(`
475
+ apiVersion: backstage.io/v1alpha1
476
+ kind: Component
477
+ metadata:
478
+ name: my-api
479
+ annotations:
480
+ insureco.io/catalog-version: "1"
481
+ insureco.io/framework: express
482
+ spec:
483
+ type: service
484
+ lifecycle: production
485
+ owner: test-org
486
+ config:
487
+ - key: STRIPE_KEY
488
+ secret: true
489
+ - key: STRIPE_KEY
490
+ `)
491
+ tempDirs.push(dir)
492
+
493
+ await expect(parseCatalogInfo(dir)).rejects.toThrow('Duplicate config declaration key: "STRIPE_KEY"')
494
+ })
495
+
496
+ it('should skip non-object entries in config array', async () => {
497
+ const dir = await createFixture(`
498
+ apiVersion: backstage.io/v1alpha1
499
+ kind: Component
500
+ metadata:
501
+ name: my-api
502
+ annotations:
503
+ insureco.io/catalog-version: "1"
504
+ insureco.io/framework: express
505
+ spec:
506
+ type: service
507
+ lifecycle: production
508
+ owner: test-org
509
+ config:
510
+ - key: VALID_KEY
511
+ - "just a string"
512
+ - 42
513
+ `)
514
+ tempDirs.push(dir)
515
+
516
+ const result = await parseCatalogInfo(dir)
517
+ expect(result).not.toBeNull()
518
+ expect(result!.configDeclarations).toEqual([{ key: 'VALID_KEY' }])
519
+ })
520
+
521
+ it('should parse minimal declarations with only key', async () => {
522
+ const dir = await createFixture(`
523
+ apiVersion: backstage.io/v1alpha1
524
+ kind: Component
525
+ metadata:
526
+ name: my-api
527
+ annotations:
528
+ insureco.io/catalog-version: "1"
529
+ insureco.io/framework: express
530
+ spec:
531
+ type: service
532
+ lifecycle: production
533
+ owner: test-org
534
+ config:
535
+ - key: APP_NAME
536
+ `)
537
+ tempDirs.push(dir)
538
+
539
+ const result = await parseCatalogInfo(dir)
540
+ expect(result).not.toBeNull()
541
+ expect(result!.configDeclarations).toEqual([
542
+ { key: 'APP_NAME' },
543
+ ])
544
+ })
545
+ })
546
+
547
+ describe('catalog auth parsing', () => {
548
+ const tempDirs: string[] = []
549
+
550
+ afterEach(async () => {
551
+ await Promise.all(tempDirs.map(d => rm(d, { recursive: true, force: true })))
552
+ tempDirs.splice(0, tempDirs.length)
553
+ })
554
+
555
+ it('should parse auth mode sso', async () => {
556
+ const dir = await createFixture(`
557
+ apiVersion: backstage.io/v1alpha1
558
+ kind: Component
559
+ metadata:
560
+ name: my-api
561
+ annotations:
562
+ insureco.io/catalog-version: "1"
563
+ insureco.io/framework: express
564
+ spec:
565
+ type: service
566
+ lifecycle: production
567
+ owner: test-org
568
+ auth:
569
+ mode: sso
570
+ `)
571
+ tempDirs.push(dir)
572
+
573
+ const result = await parseCatalogInfo(dir)
574
+ expect(result).not.toBeNull()
575
+ expect(result!.authMode).toBe('sso')
576
+ })
577
+
578
+ it('should parse auth mode service-only', async () => {
579
+ const dir = await createFixture(`
580
+ apiVersion: backstage.io/v1alpha1
581
+ kind: Component
582
+ metadata:
583
+ name: worker-svc
584
+ annotations:
585
+ insureco.io/catalog-version: "1"
586
+ insureco.io/framework: express
587
+ spec:
588
+ type: service
589
+ lifecycle: production
590
+ owner: test-org
591
+ auth:
592
+ mode: service-only
593
+ `)
594
+ tempDirs.push(dir)
595
+
596
+ const result = await parseCatalogInfo(dir)
597
+ expect(result).not.toBeNull()
598
+ expect(result!.authMode).toBe('service-only')
599
+ })
600
+
601
+ it('should parse auth mode none', async () => {
602
+ const dir = await createFixture(`
603
+ apiVersion: backstage.io/v1alpha1
604
+ kind: Component
605
+ metadata:
606
+ name: public-api
607
+ annotations:
608
+ insureco.io/catalog-version: "1"
609
+ insureco.io/framework: express
610
+ spec:
611
+ type: service
612
+ lifecycle: production
613
+ owner: test-org
614
+ auth:
615
+ mode: none
616
+ `)
617
+ tempDirs.push(dir)
618
+
619
+ const result = await parseCatalogInfo(dir)
620
+ expect(result).not.toBeNull()
621
+ expect(result!.authMode).toBe('none')
622
+ })
623
+
624
+ it('should return undefined authMode when spec.auth is not specified', async () => {
625
+ const dir = await createFixture(`
626
+ apiVersion: backstage.io/v1alpha1
627
+ kind: Component
628
+ metadata:
629
+ name: my-api
630
+ annotations:
631
+ insureco.io/catalog-version: "1"
632
+ insureco.io/framework: express
633
+ spec:
634
+ type: service
635
+ lifecycle: production
636
+ owner: test-org
637
+ `)
638
+ tempDirs.push(dir)
639
+
640
+ const result = await parseCatalogInfo(dir)
641
+ expect(result).not.toBeNull()
642
+ expect(result!.authMode).toBeUndefined()
643
+ })
644
+
645
+ it('should throw for invalid auth mode value', async () => {
646
+ const dir = await createFixture(`
647
+ apiVersion: backstage.io/v1alpha1
648
+ kind: Component
649
+ metadata:
650
+ name: my-api
651
+ annotations:
652
+ insureco.io/catalog-version: "1"
653
+ insureco.io/framework: express
654
+ spec:
655
+ type: service
656
+ lifecycle: production
657
+ owner: test-org
658
+ auth:
659
+ mode: invalid-mode
660
+ `)
661
+ tempDirs.push(dir)
662
+
663
+ await expect(parseCatalogInfo(dir)).rejects.toThrow('Invalid spec.auth.mode: "invalid-mode"')
664
+ })
665
+ })
666
+
667
+ describe('catalog consumesDatabase parsing', () => {
668
+ const tempDirs: string[] = []
669
+
670
+ afterEach(async () => {
671
+ await Promise.all(tempDirs.map(d => rm(d, { recursive: true, force: true })))
672
+ tempDirs.splice(0, tempDirs.length)
673
+ })
674
+
675
+ it('should parse consumesDatabase with envVar override in 0.2.0 catalog', async () => {
676
+ const dir = await createFixture(`
677
+ apiVersion: backstage.io/v1alpha1
678
+ kind: Component
679
+ metadata:
680
+ name: portal
681
+ annotations:
682
+ insureco.io/catalog-version: "0.2.0"
683
+ insureco.io/framework: nextjs
684
+ insureco.io/health-endpoint: /api/health
685
+ spec:
686
+ type: service
687
+ lifecycle: production
688
+ owner: test-org
689
+ consumesDatabase:
690
+ - service: bio
691
+ type: mongodb
692
+ envVar: MONGODB_URI
693
+ `)
694
+ tempDirs.push(dir)
695
+
696
+ const result = await parseCatalogInfo(dir)
697
+ expect(result).not.toBeNull()
698
+ expect(result!.consumesDatabase).toEqual([
699
+ { service: 'bio', type: 'mongodb', envVar: 'MONGODB_URI' },
700
+ ])
701
+ })
702
+
703
+ it('should ignore consumesDatabase in 0.1.0 catalog', async () => {
704
+ const dir = await createFixture(`
705
+ apiVersion: backstage.io/v1alpha1
706
+ kind: Component
707
+ metadata:
708
+ name: portal
709
+ annotations:
710
+ insureco.io/catalog-version: "0.1.0"
711
+ insureco.io/framework: nextjs
712
+ spec:
713
+ type: service
714
+ lifecycle: production
715
+ owner: test-org
716
+ consumesDatabase:
717
+ - service: bio
718
+ type: mongodb
719
+ `)
720
+ tempDirs.push(dir)
721
+
722
+ const result = await parseCatalogInfo(dir)
723
+ expect(result).not.toBeNull()
724
+ expect(result!.consumesDatabase).toEqual([])
725
+ })
726
+
727
+ it('should parse databases with sharedWith in 0.2.0 catalog', async () => {
728
+ const dir = await createFixture(`
729
+ apiVersion: backstage.io/v1alpha1
730
+ kind: Component
731
+ metadata:
732
+ name: bio
733
+ annotations:
734
+ insureco.io/catalog-version: "0.2.0"
735
+ insureco.io/framework: express
736
+ insureco.io/health-endpoint: /health
737
+ spec:
738
+ type: service
739
+ lifecycle: production
740
+ owner: test-org
741
+ databases:
742
+ - type: mongodb
743
+ name: bio
744
+ sharedWith:
745
+ - service: portal
746
+ access: readWrite
747
+ `)
748
+ tempDirs.push(dir)
749
+
750
+ const result = await parseCatalogInfo(dir)
751
+ expect(result).not.toBeNull()
752
+ expect(result!.databases).toHaveLength(1)
753
+ expect(result!.databases[0].sharedWith).toEqual([
754
+ { service: 'portal', access: 'readWrite' },
755
+ ])
756
+ })
757
+ })
758
+
759
+ describe('checkCatalogVersion', () => {
760
+ // Import the exported function directly
761
+ let checkCatalogVersion: typeof import('./catalog.js').checkCatalogVersion
762
+
763
+ beforeAll(async () => {
764
+ const mod = await import('./catalog.js')
765
+ checkCatalogVersion = mod.checkCatalogVersion
766
+ })
767
+
768
+ it('should return ok for matching version', () => {
769
+ expect(checkCatalogVersion('0.4.0').result).toBe('ok')
770
+ })
771
+
772
+ it('should return warn for lower minor version', () => {
773
+ const result = checkCatalogVersion('0.1.0')
774
+ expect(result.result).toBe('warn')
775
+ expect(result.message).toContain('behind')
776
+ })
777
+
778
+ it('should return reject for higher minor version', () => {
779
+ const result = checkCatalogVersion('0.9.0')
780
+ expect(result.result).toBe('reject')
781
+ expect(result.message).toContain('not supported')
782
+ })
783
+
784
+ it('should return reject for different major version', () => {
785
+ const result = checkCatalogVersion('1.0.0')
786
+ expect(result.result).toBe('reject')
787
+ expect(result.message).toContain('not compatible')
788
+ })
789
+
790
+ it('should return reject for invalid format', () => {
791
+ const result = checkCatalogVersion('abc')
792
+ expect(result.result).toBe('reject')
793
+ expect(result.message).toContain('Invalid catalog version format')
794
+ })
795
+ })
796
+
797
+ describe('catalog minimum version enforcement', () => {
798
+ const tempDirs: string[] = []
799
+
800
+ afterEach(async () => {
801
+ await Promise.all(tempDirs.map(d => rm(d, { recursive: true, force: true })))
802
+ tempDirs.splice(0, tempDirs.length)
803
+ mockEnv.ENFORCE_MINIMUM_CATALOG_VERSION = 'false'
804
+ })
805
+
806
+ it('should reject legacy version "1" when enforcement is enabled', async () => {
807
+ mockEnv.ENFORCE_MINIMUM_CATALOG_VERSION = 'true'
808
+
809
+ const dir = await createFixture(`
810
+ apiVersion: backstage.io/v1alpha1
811
+ kind: Component
812
+ metadata:
813
+ name: my-api
814
+ annotations:
815
+ insureco.io/catalog-version: "1"
816
+ insureco.io/framework: express
817
+ spec:
818
+ type: service
819
+ lifecycle: production
820
+ owner: test-org
821
+ `)
822
+ tempDirs.push(dir)
823
+
824
+ await expect(parseCatalogInfo(dir)).rejects.toThrow(
825
+ 'below the minimum supported version'
826
+ )
827
+ })
828
+
829
+ it('should warn but accept legacy version "1" when enforcement is off', async () => {
830
+ const dir = await createFixture(`
831
+ apiVersion: backstage.io/v1alpha1
832
+ kind: Component
833
+ metadata:
834
+ name: my-api
835
+ annotations:
836
+ insureco.io/catalog-version: "1"
837
+ insureco.io/framework: express
838
+ spec:
839
+ type: service
840
+ lifecycle: production
841
+ owner: test-org
842
+ `)
843
+ tempDirs.push(dir)
844
+
845
+ const result = await parseCatalogInfo(dir)
846
+ expect(result).not.toBeNull()
847
+ expect(result!.catalogVersion).toBe('0.1.0')
848
+ })
849
+
850
+ it('should reject version 0.1.0 when enforcement is enabled', async () => {
851
+ mockEnv.ENFORCE_MINIMUM_CATALOG_VERSION = 'true'
852
+
853
+ const dir = await createFixture(`
854
+ apiVersion: backstage.io/v1alpha1
855
+ kind: Component
856
+ metadata:
857
+ name: my-api
858
+ annotations:
859
+ insureco.io/catalog-version: "0.1.0"
860
+ insureco.io/framework: express
861
+ spec:
862
+ type: service
863
+ lifecycle: production
864
+ owner: test-org
865
+ `)
866
+ tempDirs.push(dir)
867
+
868
+ await expect(parseCatalogInfo(dir)).rejects.toThrow(
869
+ 'below the minimum supported version'
870
+ )
871
+ })
872
+
873
+ it('should include upgrade instructions in minimum version error', async () => {
874
+ mockEnv.ENFORCE_MINIMUM_CATALOG_VERSION = 'true'
875
+
876
+ const dir = await createFixture(`
877
+ apiVersion: backstage.io/v1alpha1
878
+ kind: Component
879
+ metadata:
880
+ name: my-api
881
+ annotations:
882
+ insureco.io/catalog-version: "1"
883
+ insureco.io/framework: express
884
+ spec:
885
+ type: service
886
+ lifecycle: production
887
+ owner: test-org
888
+ `)
889
+ tempDirs.push(dir)
890
+
891
+ await expect(parseCatalogInfo(dir)).rejects.toThrow(
892
+ 'tawa.insureco.io/docs/catalog-reference'
893
+ )
894
+ })
895
+ })
896
+
897
+ describe('catalog spec field validation', () => {
898
+ const tempDirs: string[] = []
899
+
900
+ afterEach(async () => {
901
+ await Promise.all(tempDirs.map(d => rm(d, { recursive: true, force: true })))
902
+ tempDirs.splice(0, tempDirs.length)
903
+ })
904
+
905
+ it('should reject 0.2.0 catalog with missing owner', async () => {
906
+ const dir = await createFixture(`
907
+ apiVersion: backstage.io/v1alpha1
908
+ kind: Component
909
+ metadata:
910
+ name: my-api
911
+ annotations:
912
+ insureco.io/catalog-version: "0.2.0"
913
+ insureco.io/framework: express
914
+ insureco.io/health-endpoint: /health
915
+ spec:
916
+ type: service
917
+ lifecycle: production
918
+ `)
919
+ tempDirs.push(dir)
920
+
921
+ await expect(parseCatalogInfo(dir)).rejects.toThrow(
922
+ 'spec.owner is required'
923
+ )
924
+ })
925
+
926
+ it('should reject 0.2.0 catalog with owner set to unknown', async () => {
927
+ const dir = await createFixture(`
928
+ apiVersion: backstage.io/v1alpha1
929
+ kind: Component
930
+ metadata:
931
+ name: my-api
932
+ annotations:
933
+ insureco.io/catalog-version: "0.2.0"
934
+ insureco.io/framework: express
935
+ insureco.io/health-endpoint: /health
936
+ spec:
937
+ type: service
938
+ lifecycle: production
939
+ owner: unknown
940
+ `)
941
+ tempDirs.push(dir)
942
+
943
+ await expect(parseCatalogInfo(dir)).rejects.toThrow(
944
+ 'spec.owner is required'
945
+ )
946
+ })
947
+
948
+ it('should reject 0.2.0 catalog with missing lifecycle', async () => {
949
+ const dir = await createFixture(`
950
+ apiVersion: backstage.io/v1alpha1
951
+ kind: Component
952
+ metadata:
953
+ name: my-api
954
+ annotations:
955
+ insureco.io/catalog-version: "0.2.0"
956
+ insureco.io/framework: express
957
+ insureco.io/health-endpoint: /health
958
+ spec:
959
+ type: service
960
+ owner: test-org
961
+ `)
962
+ tempDirs.push(dir)
963
+
964
+ await expect(parseCatalogInfo(dir)).rejects.toThrow(
965
+ 'spec.lifecycle is required'
966
+ )
967
+ })
968
+
969
+ it('should reject 0.2.0 catalog with missing type', async () => {
970
+ const dir = await createFixture(`
971
+ apiVersion: backstage.io/v1alpha1
972
+ kind: Component
973
+ metadata:
974
+ name: my-api
975
+ annotations:
976
+ insureco.io/catalog-version: "0.2.0"
977
+ insureco.io/framework: express
978
+ insureco.io/health-endpoint: /health
979
+ spec:
980
+ lifecycle: production
981
+ owner: test-org
982
+ `)
983
+ tempDirs.push(dir)
984
+
985
+ await expect(parseCatalogInfo(dir)).rejects.toThrow(
986
+ 'spec.type is required'
987
+ )
988
+ })
989
+
990
+ it('should accept 0.2.0 catalog with all required fields', async () => {
991
+ const dir = await createFixture(`
992
+ apiVersion: backstage.io/v1alpha1
993
+ kind: Component
994
+ metadata:
995
+ name: my-api
996
+ annotations:
997
+ insureco.io/catalog-version: "0.2.0"
998
+ insureco.io/framework: express
999
+ insureco.io/health-endpoint: /health
1000
+ spec:
1001
+ type: service
1002
+ lifecycle: production
1003
+ owner: test-org
1004
+ `)
1005
+ tempDirs.push(dir)
1006
+
1007
+ const result = await parseCatalogInfo(dir)
1008
+ expect(result).not.toBeNull()
1009
+ expect(result!.catalogVersion).toBe('0.2.0')
1010
+ expect(result!.owner).toBe('test-org')
1011
+ expect(result!.framework).toBe('express')
1012
+ })
1013
+
1014
+ it('should NOT validate spec fields for legacy 0.1.0 catalogs', async () => {
1015
+ const dir = await createFixture(`
1016
+ apiVersion: backstage.io/v1alpha1
1017
+ kind: Component
1018
+ metadata:
1019
+ name: my-api
1020
+ annotations:
1021
+ insureco.io/catalog-version: "0.1.0"
1022
+ insureco.io/framework: express
1023
+ spec:
1024
+ type: service
1025
+ lifecycle: production
1026
+ `)
1027
+ tempDirs.push(dir)
1028
+
1029
+ const result = await parseCatalogInfo(dir)
1030
+ expect(result).not.toBeNull()
1031
+ expect(result!.owner).toBe('unknown')
1032
+ })
1033
+
1034
+ it('should include field name and example in spec validation error', async () => {
1035
+ const dir = await createFixture(`
1036
+ apiVersion: backstage.io/v1alpha1
1037
+ kind: Component
1038
+ metadata:
1039
+ name: my-api
1040
+ annotations:
1041
+ insureco.io/catalog-version: "0.2.0"
1042
+ insureco.io/framework: express
1043
+ insureco.io/health-endpoint: /health
1044
+ spec:
1045
+ type: service
1046
+ lifecycle: production
1047
+ `)
1048
+ tempDirs.push(dir)
1049
+
1050
+ try {
1051
+ await parseCatalogInfo(dir)
1052
+ expect.fail('Should have thrown')
1053
+ } catch (error) {
1054
+ const message = (error as Error).message
1055
+ expect(message).toContain('spec.owner')
1056
+ expect(message).toContain('owner: my-org')
1057
+ expect(message).toContain('tawa.insureco.io/docs/catalog-reference')
1058
+ }
1059
+ })
1060
+ })
1061
+
1062
+ describe('catalog unified dependencies parsing', () => {
1063
+ const tempDirs: string[] = []
1064
+
1065
+ afterEach(async () => {
1066
+ await Promise.all(tempDirs.map(d => rm(d, { recursive: true, force: true })))
1067
+ tempDirs.splice(0, tempDirs.length)
1068
+ })
1069
+
1070
+ it('should parse spec.dependencies with transport direct in 0.4.0 catalog', async () => {
1071
+ const dir = await createFixture(`
1072
+ apiVersion: backstage.io/v1alpha1
1073
+ kind: Component
1074
+ metadata:
1075
+ name: my-api
1076
+ annotations:
1077
+ insureco.io/catalog-version: "0.4.0"
1078
+ insureco.io/framework: express
1079
+ insureco.io/health-endpoint: /health
1080
+ spec:
1081
+ type: service
1082
+ lifecycle: production
1083
+ owner: test-org
1084
+ dependencies:
1085
+ - service: relay
1086
+ scopes: [relay:send]
1087
+ transport: direct
1088
+ `)
1089
+ tempDirs.push(dir)
1090
+
1091
+ const result = await parseCatalogInfo(dir)
1092
+ expect(result).not.toBeNull()
1093
+ expect(result!.dependencies).toEqual([
1094
+ { service: 'relay', scopes: ['relay:send'], transport: 'direct' },
1095
+ ])
1096
+ })
1097
+
1098
+ it('should parse spec.dependencies with transport gateway', async () => {
1099
+ const dir = await createFixture(`
1100
+ apiVersion: backstage.io/v1alpha1
1101
+ kind: Component
1102
+ metadata:
1103
+ name: my-api
1104
+ annotations:
1105
+ insureco.io/catalog-version: "0.4.0"
1106
+ insureco.io/framework: express
1107
+ insureco.io/health-endpoint: /health
1108
+ spec:
1109
+ type: service
1110
+ lifecycle: production
1111
+ owner: test-org
1112
+ dependencies:
1113
+ - service: raterspot
1114
+ scopes: [raterspot:rate, raterspot:quote]
1115
+ transport: gateway
1116
+ `)
1117
+ tempDirs.push(dir)
1118
+
1119
+ const result = await parseCatalogInfo(dir)
1120
+ expect(result).not.toBeNull()
1121
+ expect(result!.dependencies).toEqual([
1122
+ { service: 'raterspot', scopes: ['raterspot:rate', 'raterspot:quote'], transport: 'gateway' },
1123
+ ])
1124
+ })
1125
+
1126
+ it('should reject invalid transport value', async () => {
1127
+ const dir = await createFixture(`
1128
+ apiVersion: backstage.io/v1alpha1
1129
+ kind: Component
1130
+ metadata:
1131
+ name: my-api
1132
+ annotations:
1133
+ insureco.io/catalog-version: "0.4.0"
1134
+ insureco.io/framework: express
1135
+ insureco.io/health-endpoint: /health
1136
+ spec:
1137
+ type: service
1138
+ lifecycle: production
1139
+ owner: test-org
1140
+ dependencies:
1141
+ - service: relay
1142
+ scopes: [relay:send]
1143
+ transport: invalid
1144
+ `)
1145
+ tempDirs.push(dir)
1146
+
1147
+ await expect(parseCatalogInfo(dir)).rejects.toThrow(
1148
+ 'Invalid transport "invalid"'
1149
+ )
1150
+ })
1151
+
1152
+ it('should reject dependency with missing service name', async () => {
1153
+ const dir = await createFixture(`
1154
+ apiVersion: backstage.io/v1alpha1
1155
+ kind: Component
1156
+ metadata:
1157
+ name: my-api
1158
+ annotations:
1159
+ insureco.io/catalog-version: "0.4.0"
1160
+ insureco.io/framework: express
1161
+ insureco.io/health-endpoint: /health
1162
+ spec:
1163
+ type: service
1164
+ lifecycle: production
1165
+ owner: test-org
1166
+ dependencies:
1167
+ - scopes: [relay:send]
1168
+ transport: direct
1169
+ `)
1170
+ tempDirs.push(dir)
1171
+
1172
+ await expect(parseCatalogInfo(dir)).rejects.toThrow(
1173
+ 'service is required'
1174
+ )
1175
+ })
1176
+
1177
+ it('should reject dependency with empty scopes', async () => {
1178
+ const dir = await createFixture(`
1179
+ apiVersion: backstage.io/v1alpha1
1180
+ kind: Component
1181
+ metadata:
1182
+ name: my-api
1183
+ annotations:
1184
+ insureco.io/catalog-version: "0.4.0"
1185
+ insureco.io/framework: express
1186
+ insureco.io/health-endpoint: /health
1187
+ spec:
1188
+ type: service
1189
+ lifecycle: production
1190
+ owner: test-org
1191
+ dependencies:
1192
+ - service: relay
1193
+ scopes: []
1194
+ transport: direct
1195
+ `)
1196
+ tempDirs.push(dir)
1197
+
1198
+ await expect(parseCatalogInfo(dir)).rejects.toThrow(
1199
+ 'at least one scope is required'
1200
+ )
1201
+ })
1202
+
1203
+ it('should reject duplicate service dependencies', async () => {
1204
+ const dir = await createFixture(`
1205
+ apiVersion: backstage.io/v1alpha1
1206
+ kind: Component
1207
+ metadata:
1208
+ name: my-api
1209
+ annotations:
1210
+ insureco.io/catalog-version: "0.4.0"
1211
+ insureco.io/framework: express
1212
+ insureco.io/health-endpoint: /health
1213
+ spec:
1214
+ type: service
1215
+ lifecycle: production
1216
+ owner: test-org
1217
+ dependencies:
1218
+ - service: relay
1219
+ scopes: [relay:send]
1220
+ transport: direct
1221
+ - service: relay
1222
+ scopes: [relay:receive]
1223
+ transport: direct
1224
+ `)
1225
+ tempDirs.push(dir)
1226
+
1227
+ await expect(parseCatalogInfo(dir)).rejects.toThrow(
1228
+ 'Duplicate dependency on service "relay"'
1229
+ )
1230
+ })
1231
+
1232
+ it('should default transport to direct when omitted', async () => {
1233
+ const dir = await createFixture(`
1234
+ apiVersion: backstage.io/v1alpha1
1235
+ kind: Component
1236
+ metadata:
1237
+ name: my-api
1238
+ annotations:
1239
+ insureco.io/catalog-version: "0.4.0"
1240
+ insureco.io/framework: express
1241
+ insureco.io/health-endpoint: /health
1242
+ spec:
1243
+ type: service
1244
+ lifecycle: production
1245
+ owner: test-org
1246
+ dependencies:
1247
+ - service: relay
1248
+ scopes: [relay:send]
1249
+ `)
1250
+ tempDirs.push(dir)
1251
+
1252
+ const result = await parseCatalogInfo(dir)
1253
+ expect(result).not.toBeNull()
1254
+ expect(result!.dependencies[0].transport).toBe('direct')
1255
+ })
1256
+
1257
+ it('should ignore spec.dependencies in pre-0.4.0 catalogs', async () => {
1258
+ const dir = await createFixture(`
1259
+ apiVersion: backstage.io/v1alpha1
1260
+ kind: Component
1261
+ metadata:
1262
+ name: my-api
1263
+ annotations:
1264
+ insureco.io/catalog-version: "0.3.0"
1265
+ insureco.io/framework: express
1266
+ insureco.io/health-endpoint: /health
1267
+ spec:
1268
+ type: service
1269
+ lifecycle: production
1270
+ owner: test-org
1271
+ dependencies:
1272
+ - service: relay
1273
+ scopes: [relay:send]
1274
+ transport: direct
1275
+ `)
1276
+ tempDirs.push(dir)
1277
+
1278
+ const result = await parseCatalogInfo(dir)
1279
+ expect(result).not.toBeNull()
1280
+ expect(result!.dependencies).toEqual([])
1281
+ })
1282
+
1283
+ it('should map legacy internalDependencies to transport:direct with empty scopes', async () => {
1284
+ const dir = await createFixture(`
1285
+ apiVersion: backstage.io/v1alpha1
1286
+ kind: Component
1287
+ metadata:
1288
+ name: my-api
1289
+ annotations:
1290
+ insureco.io/catalog-version: "0.3.0"
1291
+ insureco.io/framework: express
1292
+ insureco.io/health-endpoint: /health
1293
+ spec:
1294
+ type: service
1295
+ lifecycle: production
1296
+ owner: test-org
1297
+ internalDependencies:
1298
+ - service: iec-wallet
1299
+ port: 3000
1300
+ `)
1301
+ tempDirs.push(dir)
1302
+
1303
+ const result = await parseCatalogInfo(dir)
1304
+ expect(result).not.toBeNull()
1305
+ expect(result!.dependencies).toEqual([
1306
+ { service: 'iec-wallet', scopes: [], transport: 'direct', port: 3000 },
1307
+ ])
1308
+ })
1309
+
1310
+ it('should map legacy externalDependencies to transport:gateway', async () => {
1311
+ const dir = await createFixture(`
1312
+ apiVersion: backstage.io/v1alpha1
1313
+ kind: Component
1314
+ metadata:
1315
+ name: my-api
1316
+ annotations:
1317
+ insureco.io/catalog-version: "0.3.0"
1318
+ insureco.io/framework: express
1319
+ insureco.io/health-endpoint: /health
1320
+ spec:
1321
+ type: service
1322
+ lifecycle: production
1323
+ owner: test-org
1324
+ externalDependencies:
1325
+ - service: raterspot
1326
+ scopes: [raterspot:rate]
1327
+ reason: Rating engine
1328
+ `)
1329
+ tempDirs.push(dir)
1330
+
1331
+ const result = await parseCatalogInfo(dir)
1332
+ expect(result).not.toBeNull()
1333
+ expect(result!.dependencies).toEqual([
1334
+ { service: 'raterspot', scopes: ['raterspot:rate'], transport: 'gateway' },
1335
+ ])
1336
+ })
1337
+
1338
+ it('should merge both legacy fields into dependencies', async () => {
1339
+ const dir = await createFixture(`
1340
+ apiVersion: backstage.io/v1alpha1
1341
+ kind: Component
1342
+ metadata:
1343
+ name: my-api
1344
+ annotations:
1345
+ insureco.io/catalog-version: "0.3.0"
1346
+ insureco.io/framework: express
1347
+ insureco.io/health-endpoint: /health
1348
+ spec:
1349
+ type: service
1350
+ lifecycle: production
1351
+ owner: test-org
1352
+ internalDependencies:
1353
+ - service: iec-wallet
1354
+ port: 3000
1355
+ externalDependencies:
1356
+ - service: raterspot
1357
+ scopes: [raterspot:rate]
1358
+ `)
1359
+ tempDirs.push(dir)
1360
+
1361
+ const result = await parseCatalogInfo(dir)
1362
+ expect(result).not.toBeNull()
1363
+ expect(result!.dependencies).toHaveLength(2)
1364
+ expect(result!.dependencies[0]).toEqual({ service: 'iec-wallet', scopes: [], transport: 'direct', port: 3000 })
1365
+ expect(result!.dependencies[1]).toEqual({ service: 'raterspot', scopes: ['raterspot:rate'], transport: 'gateway' })
1366
+ })
1367
+
1368
+ it('should return empty dependencies when no deps declared', async () => {
1369
+ const dir = await createFixture(`
1370
+ apiVersion: backstage.io/v1alpha1
1371
+ kind: Component
1372
+ metadata:
1373
+ name: my-api
1374
+ annotations:
1375
+ insureco.io/catalog-version: "0.4.0"
1376
+ insureco.io/framework: express
1377
+ insureco.io/health-endpoint: /health
1378
+ spec:
1379
+ type: service
1380
+ lifecycle: production
1381
+ owner: test-org
1382
+ `)
1383
+ tempDirs.push(dir)
1384
+
1385
+ const result = await parseCatalogInfo(dir)
1386
+ expect(result).not.toBeNull()
1387
+ expect(result!.dependencies).toEqual([])
1388
+ })
1389
+ })