@intentius/chant-lexicon-terraform 0.124.54 → 0.124.56

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 (351) hide show
  1. package/README.md +7 -4
  2. package/dist/ci-setup.d.ts +11 -0
  3. package/dist/ci-setup.d.ts.map +1 -0
  4. package/dist/ci-setup.js +41 -0
  5. package/dist/components/capability-plugin.d.ts +40 -6
  6. package/dist/components/capability-plugin.d.ts.map +1 -1
  7. package/dist/components/capability-plugin.js +174 -30
  8. package/dist/components/compose-group.d.ts +1 -1
  9. package/dist/components/compose-group.d.ts.map +1 -1
  10. package/dist/components/compose-group.js +1 -1
  11. package/dist/components/release-inputs.d.ts +29 -0
  12. package/dist/components/release-inputs.d.ts.map +1 -0
  13. package/dist/components/release-inputs.js +57 -0
  14. package/dist/components/trace-facts.d.ts +16 -0
  15. package/dist/components/trace-facts.d.ts.map +1 -0
  16. package/dist/components/trace-facts.js +19 -0
  17. package/dist/cone.d.ts +53 -0
  18. package/dist/cone.d.ts.map +1 -0
  19. package/dist/cone.js +119 -0
  20. package/dist/cost-hold.d.ts +27 -0
  21. package/dist/cost-hold.d.ts.map +1 -0
  22. package/dist/cost-hold.js +21 -0
  23. package/dist/drift-rows.d.ts +61 -0
  24. package/dist/drift-rows.d.ts.map +1 -0
  25. package/dist/drift-rows.js +108 -0
  26. package/dist/exit-codes.d.ts +55 -0
  27. package/dist/exit-codes.d.ts.map +1 -0
  28. package/dist/exit-codes.js +48 -0
  29. package/dist/index.d.ts +2 -0
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +3 -0
  32. package/dist/integrity.json +7 -4
  33. package/dist/lint/audit-catalog.d.ts.map +1 -1
  34. package/dist/lint/audit-catalog.js +27 -3
  35. package/dist/lint/audit-lineage.d.ts +14 -2
  36. package/dist/lint/audit-lineage.d.ts.map +1 -1
  37. package/dist/lint/post-synth/index.d.ts.map +1 -1
  38. package/dist/lint/post-synth/index.js +4 -0
  39. package/dist/lint/post-synth/record-store.d.ts +54 -0
  40. package/dist/lint/post-synth/record-store.d.ts.map +1 -0
  41. package/dist/lint/post-synth/record-store.js +104 -0
  42. package/dist/lint/post-synth/tf001.d.ts +2 -2
  43. package/dist/lint/post-synth/tf001.js +2 -2
  44. package/dist/lint/post-synth/tf024.d.ts +35 -11
  45. package/dist/lint/post-synth/tf024.d.ts.map +1 -1
  46. package/dist/lint/post-synth/tf024.js +92 -20
  47. package/dist/lint/post-synth/tf046.d.ts +64 -0
  48. package/dist/lint/post-synth/tf046.d.ts.map +1 -0
  49. package/dist/lint/post-synth/tf046.js +175 -0
  50. package/dist/lint/post-synth/tf047.d.ts +36 -0
  51. package/dist/lint/post-synth/tf047.d.ts.map +1 -0
  52. package/dist/lint/post-synth/tf047.js +94 -0
  53. package/dist/lock-rows.d.ts +36 -0
  54. package/dist/lock-rows.d.ts.map +1 -0
  55. package/dist/lock-rows.js +60 -0
  56. package/dist/lock-timeout.d.ts +22 -0
  57. package/dist/lock-timeout.d.ts.map +1 -0
  58. package/dist/lock-timeout.js +32 -0
  59. package/dist/manifest.json +1 -1
  60. package/dist/module-catalog/index.d.ts +69 -0
  61. package/dist/module-catalog/index.d.ts.map +1 -0
  62. package/dist/module-catalog/index.js +110 -0
  63. package/dist/module-catalog/refresh-cli.d.ts +4 -0
  64. package/dist/module-catalog/refresh-cli.d.ts.map +1 -0
  65. package/dist/module-catalog/refresh-cli.js +64 -0
  66. package/dist/module-catalog/static-tier.d.ts +6 -0
  67. package/dist/module-catalog/static-tier.d.ts.map +1 -0
  68. package/dist/module-catalog/static-tier.js +1650 -0
  69. package/dist/module-catalog/types.d.ts +65 -0
  70. package/dist/module-catalog/types.d.ts.map +1 -0
  71. package/dist/module-catalog/types.js +33 -0
  72. package/dist/okf/index.md +3 -1
  73. package/dist/okf/rules/TF024.md +2 -2
  74. package/dist/okf/rules/TF046.md +11 -0
  75. package/dist/okf/rules/TF047.md +11 -0
  76. package/dist/op/activities/terraform.d.ts +29 -0
  77. package/dist/op/activities/terraform.d.ts.map +1 -1
  78. package/dist/op/activities/terraform.js +19 -4
  79. package/dist/pin/consumers.d.ts +35 -0
  80. package/dist/pin/consumers.d.ts.map +1 -0
  81. package/dist/pin/consumers.js +70 -0
  82. package/dist/plugin.d.ts.map +1 -1
  83. package/dist/plugin.js +5 -0
  84. package/dist/registry/address.d.ts +63 -0
  85. package/dist/registry/address.d.ts.map +1 -0
  86. package/dist/registry/address.js +65 -0
  87. package/dist/registry/catalog-read.d.ts +14 -0
  88. package/dist/registry/catalog-read.d.ts.map +1 -0
  89. package/dist/registry/catalog-read.js +58 -0
  90. package/dist/registry/deprecated.d.ts +37 -0
  91. package/dist/registry/deprecated.d.ts.map +1 -0
  92. package/dist/registry/deprecated.js +43 -0
  93. package/dist/registry/docs.d.ts +96 -0
  94. package/dist/registry/docs.d.ts.map +1 -0
  95. package/dist/registry/docs.js +210 -0
  96. package/dist/registry/edge/edge.d.ts +46 -0
  97. package/dist/registry/edge/edge.d.ts.map +1 -0
  98. package/dist/registry/edge/edge.js +194 -0
  99. package/dist/registry/edge/index.d.ts +11 -0
  100. package/dist/registry/edge/index.d.ts.map +1 -0
  101. package/dist/registry/edge/index.js +10 -0
  102. package/dist/registry/edge/lambda-edge.d.ts +76 -0
  103. package/dist/registry/edge/lambda-edge.d.ts.map +1 -0
  104. package/dist/registry/edge/lambda-edge.js +95 -0
  105. package/dist/registry/edge/sigv4-web.d.ts +33 -0
  106. package/dist/registry/edge/sigv4-web.d.ts.map +1 -0
  107. package/dist/registry/edge/sigv4-web.js +56 -0
  108. package/dist/registry/edge/token-source.d.ts +73 -0
  109. package/dist/registry/edge/token-source.d.ts.map +1 -0
  110. package/dist/registry/edge/token-source.js +115 -0
  111. package/dist/registry/edge/worker.d.ts +45 -0
  112. package/dist/registry/edge/worker.d.ts.map +1 -0
  113. package/dist/registry/edge/worker.js +26 -0
  114. package/dist/registry/handler.d.ts +25 -0
  115. package/dist/registry/handler.d.ts.map +1 -0
  116. package/dist/registry/handler.js +75 -0
  117. package/dist/registry/headers-file.d.ts +21 -0
  118. package/dist/registry/headers-file.d.ts.map +1 -0
  119. package/dist/registry/headers-file.js +57 -0
  120. package/dist/registry/index.d.ts +31 -0
  121. package/dist/registry/index.d.ts.map +1 -0
  122. package/dist/registry/index.js +30 -0
  123. package/dist/registry/mirror-paths.d.ts +9 -0
  124. package/dist/registry/mirror-paths.d.ts.map +1 -0
  125. package/dist/registry/mirror-paths.js +8 -0
  126. package/dist/registry/mirror.d.ts +130 -0
  127. package/dist/registry/mirror.d.ts.map +1 -0
  128. package/dist/registry/mirror.js +319 -0
  129. package/dist/registry/proxies.d.ts +73 -0
  130. package/dist/registry/proxies.d.ts.map +1 -0
  131. package/dist/registry/proxies.js +111 -0
  132. package/dist/registry/reader.d.ts +57 -0
  133. package/dist/registry/reader.d.ts.map +1 -0
  134. package/dist/registry/reader.js +198 -0
  135. package/dist/registry/semver.d.ts +16 -0
  136. package/dist/registry/semver.d.ts.map +1 -0
  137. package/dist/registry/semver.js +24 -0
  138. package/dist/registry/serve.d.ts +24 -0
  139. package/dist/registry/serve.d.ts.map +1 -0
  140. package/dist/registry/serve.js +64 -0
  141. package/dist/registry/source-forms.d.ts +79 -0
  142. package/dist/registry/source-forms.d.ts.map +1 -0
  143. package/dist/registry/source-forms.js +202 -0
  144. package/dist/registry/targets/artifactory.d.ts +28 -0
  145. package/dist/registry/targets/artifactory.d.ts.map +1 -0
  146. package/dist/registry/targets/artifactory.js +206 -0
  147. package/dist/registry/targets/fake-artifactory.d.ts +30 -0
  148. package/dist/registry/targets/fake-artifactory.d.ts.map +1 -0
  149. package/dist/registry/targets/fake-artifactory.js +142 -0
  150. package/dist/registry/targets/fake-gitlab.d.ts +39 -0
  151. package/dist/registry/targets/fake-gitlab.d.ts.map +1 -0
  152. package/dist/registry/targets/fake-gitlab.js +136 -0
  153. package/dist/registry/targets/fake-hcp.d.ts +50 -0
  154. package/dist/registry/targets/fake-hcp.d.ts.map +1 -0
  155. package/dist/registry/targets/fake-hcp.js +202 -0
  156. package/dist/registry/targets/fake-http.d.ts +31 -0
  157. package/dist/registry/targets/fake-http.d.ts.map +1 -0
  158. package/dist/registry/targets/fake-http.js +87 -0
  159. package/dist/registry/targets/fake-nexus.d.ts +27 -0
  160. package/dist/registry/targets/fake-nexus.d.ts.map +1 -0
  161. package/dist/registry/targets/fake-nexus.js +137 -0
  162. package/dist/registry/targets/gitlab.d.ts +53 -0
  163. package/dist/registry/targets/gitlab.d.ts.map +1 -0
  164. package/dist/registry/targets/gitlab.js +187 -0
  165. package/dist/registry/targets/hcp.d.ts +29 -0
  166. package/dist/registry/targets/hcp.d.ts.map +1 -0
  167. package/dist/registry/targets/hcp.js +310 -0
  168. package/dist/registry/targets/hosted.d.ts +62 -0
  169. package/dist/registry/targets/hosted.d.ts.map +1 -0
  170. package/dist/registry/targets/hosted.js +74 -0
  171. package/dist/registry/targets/index.d.ts +18 -0
  172. package/dist/registry/targets/index.d.ts.map +1 -0
  173. package/dist/registry/targets/index.js +17 -0
  174. package/dist/registry/targets/module-target.d.ts +68 -0
  175. package/dist/registry/targets/module-target.d.ts.map +1 -0
  176. package/dist/registry/targets/module-target.js +63 -0
  177. package/dist/registry/targets/nexus.d.ts +43 -0
  178. package/dist/registry/targets/nexus.d.ts.map +1 -0
  179. package/dist/registry/targets/nexus.js +250 -0
  180. package/dist/registry/targets/zip.d.ts +55 -0
  181. package/dist/registry/targets/zip.d.ts.map +1 -0
  182. package/dist/registry/targets/zip.js +278 -0
  183. package/dist/registry/writer.d.ts +101 -0
  184. package/dist/registry/writer.d.ts.map +1 -0
  185. package/dist/registry/writer.js +109 -0
  186. package/dist/rules/record-store.ts +122 -0
  187. package/dist/rules/tf001.ts +2 -2
  188. package/dist/rules/tf024.ts +105 -22
  189. package/dist/rules/tf046.ts +204 -0
  190. package/dist/rules/tf047.ts +114 -0
  191. package/dist/state/azure-blob.d.ts +8 -67
  192. package/dist/state/azure-blob.d.ts.map +1 -1
  193. package/dist/state/azure-blob.js +6 -199
  194. package/dist/state/gcs.d.ts +7 -61
  195. package/dist/state/gcs.d.ts.map +1 -1
  196. package/dist/state/gcs.js +5 -222
  197. package/dist/state/object-store.d.ts +4 -33
  198. package/dist/state/object-store.d.ts.map +1 -1
  199. package/dist/state/object-store.js +4 -17
  200. package/dist/state/s3.d.ts +11 -58
  201. package/dist/state/s3.d.ts.map +1 -1
  202. package/dist/state/s3.js +10 -161
  203. package/dist/state/store.js +2 -2
  204. package/dist/state-ops.d.ts +255 -0
  205. package/dist/state-ops.d.ts.map +1 -0
  206. package/dist/state-ops.js +305 -0
  207. package/dist/state-reads.d.ts +57 -0
  208. package/dist/state-reads.d.ts.map +1 -0
  209. package/dist/state-reads.js +104 -0
  210. package/dist/used-sources.d.ts +54 -0
  211. package/dist/used-sources.d.ts.map +1 -0
  212. package/dist/used-sources.js +157 -0
  213. package/package.json +19 -3
  214. package/src/__fixtures__/drift-record/README.md +5 -0
  215. package/src/__fixtures__/drift-rows/live-root-plan.json +70 -0
  216. package/src/__fixtures__/drift-rows/refresh-plan.json +61 -0
  217. package/src/__fixtures__/used-sources/root/extra.tofu +1 -0
  218. package/src/__fixtures__/used-sources/root/main.tf +31 -0
  219. package/src/__fixtures__/used-sources/root/modules/net/main.tf +14 -0
  220. package/src/__fixtures__/used-sources/root/modules/net/versions.tf.json +7 -0
  221. package/src/__fixtures__/used-sources/unit/terragrunt.hcl +3 -0
  222. package/src/ci-setup.ts +42 -0
  223. package/src/components/capability-plugin.test.ts +26 -14
  224. package/src/components/capability-plugin.ts +185 -31
  225. package/src/components/compose-group.ts +1 -1
  226. package/src/components/pr-loop-compose.tofu.e2e.test.ts +1 -1
  227. package/src/components/pr-loop.tofu.test.ts +1 -1
  228. package/src/components/release-inputs.ts +69 -0
  229. package/src/components/trace-facts.test.ts +21 -0
  230. package/src/components/trace-facts.ts +27 -0
  231. package/src/cone.ts +138 -0
  232. package/src/cost-hold.ts +29 -0
  233. package/src/describe-resources.engine-facts.test.ts +2 -3
  234. package/src/drift-rows.test.ts +135 -0
  235. package/src/drift-rows.ts +137 -0
  236. package/src/exit-codes.test.ts +23 -0
  237. package/src/exit-codes.ts +59 -0
  238. package/src/index.ts +3 -0
  239. package/src/lint/audit-catalog.ts +29 -3
  240. package/src/lint/audit-lineage.ts +14 -2
  241. package/src/lint/post-synth/fixtures/TF024/live-with-record-store/main.tf +18 -0
  242. package/src/lint/post-synth/fixtures/TF024/sidecar-with-backend/estate.chdf.hcl +1 -0
  243. package/src/lint/post-synth/fixtures/TF024/sidecar-with-backend/main.tf +10 -0
  244. package/src/lint/post-synth/fixtures/TF046/azurerm-no-resource-group/main.tf +15 -0
  245. package/src/lint/post-synth/fixtures/TF046/backend-config-only/main.tf +14 -0
  246. package/src/lint/post-synth/fixtures/TF046/derived-s3/backend.tf +7 -0
  247. package/src/lint/post-synth/fixtures/TF046/derived-s3/main.tf +9 -0
  248. package/src/lint/post-synth/fixtures/TF046/explicit-local/main.tf +16 -0
  249. package/src/lint/post-synth/fixtures/TF046/explicit-s3-beside-local-backend/main.tf +15 -0
  250. package/src/lint/post-synth/fixtures/TF046/implied-local/main.tf +9 -0
  251. package/src/lint/post-synth/fixtures/TF046/sidecar-implied/estate.chdf.hcl +1 -0
  252. package/src/lint/post-synth/fixtures/TF046/sidecar-implied/main.tf +5 -0
  253. package/src/lint/post-synth/fixtures/TF047/clear/main.tf +12 -0
  254. package/src/lint/post-synth/fixtures/TF047/refused/main.tf +12 -0
  255. package/src/lint/post-synth/fixtures/TF047/unknown-module/main.tf +12 -0
  256. package/src/lint/post-synth/fixtures/TF047/unknown-version/main.tf +12 -0
  257. package/src/lint/post-synth/index.ts +4 -0
  258. package/src/lint/post-synth/record-store.ts +122 -0
  259. package/src/lint/post-synth/tf001.ts +2 -2
  260. package/src/lint/post-synth/tf024.test.ts +26 -5
  261. package/src/lint/post-synth/tf024.ts +105 -22
  262. package/src/lint/post-synth/tf046.test.ts +137 -0
  263. package/src/lint/post-synth/tf046.ts +204 -0
  264. package/src/lint/post-synth/tf047.test.ts +56 -0
  265. package/src/lint/post-synth/tf047.ts +114 -0
  266. package/src/lock-rows.test.ts +70 -0
  267. package/src/lock-rows.ts +82 -0
  268. package/src/lock-timeout.ts +34 -0
  269. package/src/module-catalog/index.test.ts +93 -0
  270. package/src/module-catalog/index.ts +159 -0
  271. package/src/module-catalog/refresh-cli.ts +68 -0
  272. package/src/module-catalog/static-tier.ts +1659 -0
  273. package/src/module-catalog/types.ts +88 -0
  274. package/src/op/activities/terraform.ts +46 -5
  275. package/src/op-wave-step.test.ts +109 -0
  276. package/src/pin/__fixtures__/consumers/live/dev/main.tf +4 -0
  277. package/src/pin/__fixtures__/consumers/live/prod/main.tf +21 -0
  278. package/src/pin/__fixtures__/consumers/units/vpc/terragrunt.hcl +7 -0
  279. package/src/pin/consumers.test.ts +57 -0
  280. package/src/pin/consumers.ts +95 -0
  281. package/src/plugin.ts +6 -0
  282. package/src/registry/__fixtures__/docs/documented/.terraform-docs.yml +3 -0
  283. package/src/registry/__fixtures__/docs/documented/README.md +1 -0
  284. package/src/registry/__fixtures__/docs/documented/main.tf +8 -0
  285. package/src/registry/__fixtures__/docs/json/main.tf.json +14 -0
  286. package/src/registry/__fixtures__/docs/vpc/README.md +3 -0
  287. package/src/registry/__fixtures__/docs/vpc/examples/complete/main.tf +5 -0
  288. package/src/registry/__fixtures__/docs/vpc/examples/simple/README.md +1 -0
  289. package/src/registry/__fixtures__/docs/vpc/examples/simple/main.tf +4 -0
  290. package/src/registry/__fixtures__/docs/vpc/main.tf +14 -0
  291. package/src/registry/__fixtures__/docs/vpc/outputs.tf +9 -0
  292. package/src/registry/__fixtures__/docs/vpc/variables.tf +16 -0
  293. package/src/registry/__fixtures__/proxied-root/main.tf +12 -0
  294. package/src/registry/address.ts +100 -0
  295. package/src/registry/catalog-read.ts +56 -0
  296. package/src/registry/deprecated.test.ts +139 -0
  297. package/src/registry/deprecated.ts +76 -0
  298. package/src/registry/docs.test.ts +140 -0
  299. package/src/registry/docs.ts +299 -0
  300. package/src/registry/edge/bundle.test.ts +50 -0
  301. package/src/registry/edge/edge.test.ts +176 -0
  302. package/src/registry/edge/edge.ts +211 -0
  303. package/src/registry/edge/index.ts +10 -0
  304. package/src/registry/edge/lambda-edge.ts +127 -0
  305. package/src/registry/edge/sigv4-web.ts +79 -0
  306. package/src/registry/edge/token-source.test.ts +258 -0
  307. package/src/registry/edge/token-source.ts +169 -0
  308. package/src/registry/edge/worker.ts +61 -0
  309. package/src/registry/handler.ts +89 -0
  310. package/src/registry/headers-file.ts +61 -0
  311. package/src/registry/index.ts +31 -0
  312. package/src/registry/mirror-paths.ts +10 -0
  313. package/src/registry/mirror.test.ts +198 -0
  314. package/src/registry/mirror.ts +395 -0
  315. package/src/registry/proxies.test.ts +153 -0
  316. package/src/registry/proxies.ts +132 -0
  317. package/src/registry/reader.ts +225 -0
  318. package/src/registry/registry.test.ts +262 -0
  319. package/src/registry/semver.ts +34 -0
  320. package/src/registry/serve.ts +82 -0
  321. package/src/registry/source-forms.test.ts +87 -0
  322. package/src/registry/source-forms.ts +216 -0
  323. package/src/registry/targets/artifactory.test.ts +248 -0
  324. package/src/registry/targets/artifactory.ts +257 -0
  325. package/src/registry/targets/fake-artifactory.ts +158 -0
  326. package/src/registry/targets/fake-gitlab.ts +161 -0
  327. package/src/registry/targets/fake-hcp.ts +233 -0
  328. package/src/registry/targets/fake-http.ts +97 -0
  329. package/src/registry/targets/fake-nexus.ts +152 -0
  330. package/src/registry/targets/gitlab.test.ts +208 -0
  331. package/src/registry/targets/gitlab.ts +223 -0
  332. package/src/registry/targets/hcp.test.ts +230 -0
  333. package/src/registry/targets/hcp.ts +360 -0
  334. package/src/registry/targets/hosted.ts +117 -0
  335. package/src/registry/targets/index.ts +17 -0
  336. package/src/registry/targets/module-target.ts +131 -0
  337. package/src/registry/targets/nexus.test.ts +220 -0
  338. package/src/registry/targets/nexus.ts +314 -0
  339. package/src/registry/targets/zip.test.ts +65 -0
  340. package/src/registry/targets/zip.ts +287 -0
  341. package/src/registry/writer.ts +155 -0
  342. package/src/state/azure-blob.ts +6 -221
  343. package/src/state/gcs.ts +5 -252
  344. package/src/state/object-store.ts +4 -32
  345. package/src/state/s3.ts +10 -182
  346. package/src/state/store.ts +2 -2
  347. package/src/state-ops.test.ts +182 -0
  348. package/src/state-ops.ts +462 -0
  349. package/src/state-reads.ts +116 -0
  350. package/src/used-sources.test.ts +75 -0
  351. package/src/used-sources.ts +204 -0
@@ -0,0 +1,262 @@
1
+ // The module registry protocol moved from terragucci (chant#4565), with the download answer Terraform
2
+ // installs from (terragucci#1287): the JSON body stays, X-Terraform-Get carries the same location.
3
+ import { mkdtempSync, mkdirSync, writeFileSync } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import { dirname, join } from "node:path";
6
+ import { gzipSync } from "node:zlib";
7
+ import { describe, expect, it } from "vitest";
8
+ import {
9
+ DOWNLOAD_HEADER,
10
+ RegistryError,
11
+ RegistryNameError,
12
+ dirFiles,
13
+ downloadAnswer,
14
+ downloadDoc,
15
+ fetchTarball,
16
+ handlerFetch,
17
+ isDownloadPath,
18
+ keyFor,
19
+ newest,
20
+ parseRegistrySource,
21
+ parseSemver,
22
+ readVersions,
23
+ registryAddress,
24
+ registryHandler,
25
+ registryLocation,
26
+ registryVersions,
27
+ serveRegistry,
28
+ storeFiles,
29
+ tarballName,
30
+ versionsDoc,
31
+ writeVersion,
32
+ type RegistryFetch,
33
+ type RegistryWriteStore,
34
+ } from "./index";
35
+
36
+ const REG = { url: "https://modules.test", namespace: "acme" };
37
+
38
+ function memoryStore(): RegistryWriteStore & { objects: Map<string, { body: string | Buffer; etag: string }>; puts: string[] } {
39
+ const objects = new Map<string, { body: string | Buffer; etag: string }>();
40
+ const puts: string[] = [];
41
+ let n = 0;
42
+ return {
43
+ objects,
44
+ puts,
45
+ async put(key, body) {
46
+ puts.push(key);
47
+ objects.set(key, { body, etag: `"${++n}"` });
48
+ return {};
49
+ },
50
+ async read(key) {
51
+ const o = objects.get(key);
52
+ return o ? { body: typeof o.body === "string" ? o.body : o.body.toString("utf-8"), etag: o.etag } : {};
53
+ },
54
+ };
55
+ }
56
+
57
+ /** Serves a store's files as a bucket would: no headers per object. */
58
+ const staticFetch = (objects: Map<string, { body: string | Buffer }>): RegistryFetch => async (url) => {
59
+ const key = new URL(url).pathname.replace(/^\//, "");
60
+ const o = objects.get(key);
61
+ return o ? new Response(typeof o.body === "string" ? o.body : new Uint8Array(o.body), { status: 200 }) : new Response(null, { status: 404 });
62
+ };
63
+
64
+ async function publishOne(store: RegistryWriteStore, version = "0.1.0", prefix?: string) {
65
+ const addr = registryAddress(REG, "modules/net");
66
+ const v = parseSemver(version)!;
67
+ const location = `${REG.url}/${addr.path}/${version}/${tarballName(addr, version)}`;
68
+ await writeVersion(
69
+ store,
70
+ addr,
71
+ v,
72
+ {
73
+ tarball: { key: keyFor(prefix, `${addr.path}/${version}/${tarballName(addr, version)}`), body: gzipSync(Buffer.from("module bytes")) },
74
+ download: { key: keyFor(prefix, `${addr.path}/${version}/download`), body: downloadDoc(location) },
75
+ meta: { key: keyFor(prefix, `${addr.path}/${version}/release.json`), body: JSON.stringify({ module: "modules/net", version, revision: "abc", content: "sha256:x", location }) },
76
+ },
77
+ { prefix },
78
+ );
79
+ return { addr, location };
80
+ }
81
+
82
+ describe("addresses", () => {
83
+ it("places a module by its path, the longest namespace prefix winning", () => {
84
+ const reg = { ...REG, namespaces: { "platform/": "plat", "platform/net": "netw" } };
85
+ expect(registryAddress(reg, "modules/net")).toEqual({ namespace: "acme", name: "net", system: "generic", source: "modules.test/acme/net/generic", path: "v1/modules/acme/net/generic" });
86
+ expect(registryAddress(reg, "platform/db").namespace).toBe("plat");
87
+ expect(registryAddress(reg, "platform/net").namespace).toBe("netw");
88
+ expect(() => registryAddress(REG, "modules/net.v2")).toThrow(RegistryNameError);
89
+ });
90
+
91
+ it("parses registry sources and leaves other sources alone", () => {
92
+ expect(parseRegistrySource("127.0.0.1:8443/acme/network/generic")).toEqual({ host: "127.0.0.1:8443", namespace: "acme", name: "network", system: "generic" });
93
+ expect(parseRegistrySource("Modules.Test/acme/net/aws//sub?ref=x")?.host).toBe("modules.test");
94
+ expect(parseRegistrySource("hashicorp/consul/aws")).toBeUndefined();
95
+ expect(parseRegistrySource("git::https://example.com/x.git")).toBeUndefined();
96
+ });
97
+
98
+ it("keys files under a prefix", () => {
99
+ expect(keyFor("/registry/", "v1/x")).toBe("registry/v1/x");
100
+ expect(keyFor(undefined, "v1/x")).toBe("v1/x");
101
+ });
102
+ });
103
+
104
+ describe("writer", () => {
105
+ it("writes discovery, the tarball, download, release.json, then the version list", async () => {
106
+ const mem = memoryStore();
107
+ await publishOne(mem);
108
+ expect(mem.puts).toEqual([
109
+ ".well-known/terraform.json",
110
+ "v1/modules/acme/net/generic/0.1.0/net-0.1.0.tar.gz",
111
+ "v1/modules/acme/net/generic/0.1.0/download",
112
+ "v1/modules/acme/net/generic/0.1.0/release.json",
113
+ "v1/modules/acme/net/generic/versions",
114
+ ]);
115
+ await publishOne(mem, "0.2.0");
116
+ expect(readVersions((await mem.read("v1/modules/acme/net/generic/versions")).body)).toEqual(["0.1.0", "0.2.0"]);
117
+ expect(mem.puts.filter((k) => k === ".well-known/terraform.json")).toHaveLength(1);
118
+ });
119
+
120
+ it("retries the version list on a conflict from a store with its own conflict class", async () => {
121
+ class StoreConflict extends Error {}
122
+ const mem = memoryStore();
123
+ let fail = 1;
124
+ const store: RegistryWriteStore = {
125
+ read: mem.read,
126
+ async put(key, body, type, when) {
127
+ if (key.endsWith("/versions") && fail-- > 0) throw new StoreConflict("412");
128
+ return mem.put(key, body, type, when);
129
+ },
130
+ };
131
+ await publishOne(store);
132
+ expect(readVersions((await mem.read("v1/modules/acme/net/generic/versions")).body)).toEqual(["0.1.0"]);
133
+ });
134
+
135
+ it("sorts and dedupes versions, and picks the newest", () => {
136
+ expect(readVersions(versionsDoc(["1.0.0", "0.2.0", "v1.0.0", "nope"]))).toEqual(["0.2.0", "1.0.0"]);
137
+ expect(readVersions("not json")).toEqual([]);
138
+ expect(newest(["0.9.0", "0.10.0", "x"])).toEqual({ major: 0, minor: 10, patch: 0 });
139
+ });
140
+
141
+ it("answers a download with the JSON body and X-Terraform-Get, status 200", () => {
142
+ const a = downloadAnswer("https://modules.test/x.tar.gz");
143
+ expect(a.status).toBe(200);
144
+ expect(a.headers[DOWNLOAD_HEADER]).toBe("https://modules.test/x.tar.gz");
145
+ expect(JSON.parse(a.body)).toEqual({ location: "https://modules.test/x.tar.gz" });
146
+ expect(isDownloadPath("v1/modules/acme/net/generic/0.1.0/download")).toBe(true);
147
+ expect(isDownloadPath("registry/v1/modules/acme/net/generic/0.1.0/download")).toBe(true);
148
+ expect(isDownloadPath("v1/modules/acme/net/generic/versions")).toBe(false);
149
+ });
150
+ });
151
+
152
+ describe("reader", () => {
153
+ it("reads static files the way OpenTofu does, and refuses them the way Terraform does", async () => {
154
+ const mem = memoryStore();
155
+ const { location } = await publishOne(mem);
156
+ const fetch = staticFetch(mem.objects);
157
+ expect(await registryVersions("modules.test/acme/net/generic", fetch)).toEqual(["0.1.0"]);
158
+ expect(await registryLocation("modules.test/acme/net/generic", "0.1.0", { fetch, reads: "tofu" })).toBe(location);
159
+ await expect(registryLocation("modules.test/acme/net/generic", "0.1.0", { fetch, reads: "terraform" })).rejects.toThrow("sends no X-Terraform-Get header");
160
+ await expect(registryLocation("modules.test/acme/net/generic", "9.0.0", fetch)).rejects.toThrow("has no version 9.0.0");
161
+ await expect(registryVersions("hashicorp/consul/aws", fetch)).rejects.toThrow(RegistryError);
162
+ });
163
+
164
+ it("reads the handler's answers under both CLIs, and fetches the tarball", async () => {
165
+ const mem = memoryStore();
166
+ const { location } = await publishOne(mem);
167
+ const log: string[] = [];
168
+ const fetch = handlerFetch(registryHandler(storeFiles({ read: async (k) => ({ body: mem.objects.get(k)?.body }) }), { log: (l) => log.push(l) }));
169
+ expect(await registryLocation("modules.test/acme/net/generic", "0.1.0", { fetch, reads: "terraform" })).toBe(location);
170
+ expect(await registryLocation("modules.test/acme/net/generic", "0.1.0", { fetch, reads: "tofu" })).toBe(location);
171
+ expect((await fetchTarball(location, fetch)).toString()).toBe("module bytes");
172
+ expect(log).toContain("GET /v1/modules/acme/net/generic/0.1.0/download -> 200");
173
+ });
174
+
175
+ it("reads a registry written under a prefix through the handler's prefix", async () => {
176
+ const mem = memoryStore();
177
+ await publishOne(mem, "0.1.0", "registry");
178
+ const fetch = handlerFetch(registryHandler(storeFiles({ read: async (k) => ({ body: mem.objects.get(k)?.body }) }), { prefix: "registry" }));
179
+ expect(await registryVersions("modules.test/acme/net/generic", fetch)).toEqual(["0.1.0"]);
180
+ });
181
+ });
182
+
183
+ describe("loopback server", () => {
184
+ it("serves a directory on 127.0.0.1 with the download header", async () => {
185
+ const root = mkdtempSync(join(tmpdir(), "chant-registry-"));
186
+ const mem = memoryStore();
187
+ await publishOne(mem);
188
+ for (const [key, o] of mem.objects) {
189
+ mkdirSync(dirname(join(root, key)), { recursive: true });
190
+ writeFileSync(join(root, key), o.body);
191
+ }
192
+ const served = await serveRegistry(registryHandler(dirFiles(root)));
193
+ try {
194
+ const res = await fetch(`${served.url}/v1/modules/acme/net/generic/0.1.0/download`);
195
+ expect(res.status).toBe(200);
196
+ expect(res.headers.get(DOWNLOAD_HEADER)).toBe("https://modules.test/v1/modules/acme/net/generic/0.1.0/net-0.1.0.tar.gz");
197
+ expect(await res.json()).toEqual({ location: "https://modules.test/v1/modules/acme/net/generic/0.1.0/net-0.1.0.tar.gz" });
198
+ expect((await fetch(`${served.url}/../etc/passwd`)).status).toBe(404);
199
+ expect(await registryVersions(`${served.host}/acme/net/generic`, { scheme: "http" })).toEqual(["0.1.0"]);
200
+ } finally {
201
+ await served.close();
202
+ }
203
+ });
204
+ });
205
+
206
+ describe("credentials (chant#4563)", () => {
207
+ /** A registry that refuses any request without the bearer token, as a private registry or the edge function with auth on does. */
208
+ const guarded = (mem: ReturnType<typeof memoryStore>, token: string, seen: (string | null)[]): RegistryFetch => {
209
+ const inner = handlerFetch(registryHandler(storeFiles({ read: async (k) => ({ body: mem.objects.get(k)?.body }) })));
210
+ return async (url, init) => {
211
+ const auth = new Headers(init?.headers).get("authorization");
212
+ seen.push(auth);
213
+ return auth === `Bearer ${token}` ? inner(url, init) : new Response(null, { status: 401 });
214
+ };
215
+ };
216
+
217
+ it("sends TF_TOKEN_<host> on discovery, versions and download", async () => {
218
+ const mem = memoryStore();
219
+ const { location } = await publishOne(mem);
220
+ const seen: (string | null)[] = [];
221
+ const fetch = guarded(mem, "s3cret", seen);
222
+ const credentials = { env: { TF_TOKEN_modules_test: "s3cret" }, readFile: () => undefined };
223
+ expect(await registryVersions("modules.test/acme/net/generic", { fetch, credentials })).toEqual(["0.1.0"]);
224
+ expect(await registryLocation("modules.test/acme/net/generic", "0.1.0", { fetch, credentials, reads: "terraform" })).toBe(location);
225
+ expect(seen).toHaveLength(4);
226
+ expect(seen.every((a) => a === "Bearer s3cret")).toBe(true);
227
+ });
228
+
229
+ it("reads the token from credentials.tfrc.json when no variable is set", async () => {
230
+ const mem = memoryStore();
231
+ await publishOne(mem);
232
+ const tfrc = join("/home/u", ".terraform.d", "credentials.tfrc.json");
233
+ const credentials = { env: { HOME: "/home/u" }, platform: "linux", readFile: (p: string) => (p === tfrc ? JSON.stringify({ credentials: { "modules.test": { token: "f1" } } }) : undefined) };
234
+ expect(await registryVersions("modules.test/acme/net/generic", { fetch: guarded(mem, "f1", []), credentials })).toEqual(["0.1.0"]);
235
+ });
236
+
237
+ it("names the variable to set when the registry wants a token, and never prints one", async () => {
238
+ const mem = memoryStore();
239
+ await publishOne(mem);
240
+ const none = { env: {}, readFile: () => undefined };
241
+ await expect(registryVersions("modules.test/acme/net/generic", { fetch: guarded(mem, "s3cret", []), credentials: none })).rejects.toThrow("set TF_TOKEN_modules_test");
242
+ const wrong = registryVersions("modules.test/acme/net/generic", { fetch: guarded(mem, "s3cret", []), token: "nope" });
243
+ await expect(wrong).rejects.toThrow("refused the token");
244
+ await expect(wrong).rejects.not.toThrow("nope");
245
+ });
246
+
247
+ it("sends no token when told not to, and none with a signed tarball URL", async () => {
248
+ const mem = memoryStore();
249
+ await publishOne(mem);
250
+ const seen: (string | null)[] = [];
251
+ await expect(registryVersions("modules.test/acme/net/generic", { fetch: guarded(mem, "s3cret", seen), token: false })).rejects.toThrow(RegistryError);
252
+ expect(seen).toEqual([null]);
253
+ const tarballSeen: (string | null)[] = [];
254
+ const probe: RegistryFetch = async (_url, init) => {
255
+ tarballSeen.push(new Headers(init?.headers).get("authorization"));
256
+ return new Response(new Uint8Array(gzipSync(Buffer.from("x"))), { status: 200 });
257
+ };
258
+ await fetchTarball("https://modules.test/x.tar.gz?expires=1&sig=abc", { fetch: probe, token: "s3cret" });
259
+ await fetchTarball("https://modules.test/x.tar.gz", { fetch: probe, token: "s3cret" });
260
+ expect(tarballSeen).toEqual([null, "Bearer s3cret"]);
261
+ });
262
+ });
@@ -0,0 +1,34 @@
1
+ /**
2
+ * The versions a module registry lists: plain `major.minor.patch`, with an
3
+ * optional leading `v` on input. Lifted from terragucci's `publish/semver.ts`
4
+ * (18f6431595), cut to what the registry protocol needs (chant#4565).
5
+ */
6
+
7
+ export interface Semver {
8
+ major: number;
9
+ minor: number;
10
+ patch: number;
11
+ }
12
+
13
+ const RE = /^v?(\d+)\.(\d+)\.(\d+)$/;
14
+
15
+ export function parseSemver(text: string): Semver | undefined {
16
+ const m = RE.exec(text.trim());
17
+ return m ? { major: +m[1]!, minor: +m[2]!, patch: +m[3]! } : undefined;
18
+ }
19
+
20
+ export const formatSemver = (v: Semver): string => `${v.major}.${v.minor}.${v.patch}`;
21
+
22
+ export function compareSemver(a: Semver, b: Semver): number {
23
+ return a.major - b.major || a.minor - b.minor || a.patch - b.patch;
24
+ }
25
+
26
+ /** The newest of a list of versions; entries that are not versions are skipped. */
27
+ export function newest(versions: string[]): Semver | undefined {
28
+ let best: Semver | undefined;
29
+ for (const v of versions) {
30
+ const p = parseSemver(v);
31
+ if (p && (!best || compareSemver(p, best) > 0)) best = p;
32
+ }
33
+ return best;
34
+ }
@@ -0,0 +1,82 @@
1
+ /**
2
+ * A registry's files on a loopback socket, for tests that run real
3
+ * `terraform init` and `tofu init` (terragucci#1287) and for a local preview.
4
+ * The answers come from `registryHandler` in ./handler; this file adds the
5
+ * directory reader and the Node server around it.
6
+ */
7
+ import { existsSync, readFileSync, statSync } from "node:fs";
8
+ import { createServer as createHttpServer, type IncomingMessage, type Server, type ServerResponse } from "node:http";
9
+ import { createServer as createHttpsServer } from "node:https";
10
+ import type { AddressInfo } from "node:net";
11
+ import { join, normalize, sep } from "node:path";
12
+ import type { RegistryFiles, RegistryHandler } from "./handler";
13
+
14
+ /** A registry written into a directory, read back as bytes. Keys that climb out of the directory read as missing. */
15
+ export function dirFiles(root: string): RegistryFiles {
16
+ const base = normalize(root);
17
+ return async (key) => {
18
+ const file = normalize(join(base, key));
19
+ if (file !== base && !file.startsWith(base.endsWith(sep) ? base : `${base}${sep}`)) return {};
20
+ if (!existsSync(file) || !statSync(file).isFile()) return {};
21
+ return { body: readFileSync(file) };
22
+ };
23
+ }
24
+
25
+ export interface ServeOptions {
26
+ /** Default 127.0.0.1. A loopback address keeps the registry off the network. */
27
+ hostname?: string;
28
+ /** Default 0, a free port. */
29
+ port?: number;
30
+ /** PEM key and certificate: the server speaks HTTPS, as both CLIs require for discovery. */
31
+ tls?: { key: string | Buffer; cert: string | Buffer };
32
+ }
33
+
34
+ export interface ServedRegistry {
35
+ /** `<scheme>://<hostname>:<port>`, no trailing slash. */
36
+ url: string;
37
+ /** `<hostname>:<port>`, the host a registry source names. */
38
+ host: string;
39
+ close(): Promise<void>;
40
+ }
41
+
42
+ const readBody = (req: IncomingMessage): Promise<Buffer> =>
43
+ new Promise((resolve, reject) => {
44
+ const chunks: Buffer[] = [];
45
+ req.on("data", (c: Buffer) => chunks.push(c));
46
+ req.on("end", () => resolve(Buffer.concat(chunks)));
47
+ req.on("error", reject);
48
+ });
49
+
50
+ /** Serves a handler on a local socket until `close`. */
51
+ export async function serveRegistry(handler: RegistryHandler, options: ServeOptions = {}): Promise<ServedRegistry> {
52
+ const hostname = options.hostname ?? "127.0.0.1";
53
+ const scheme = options.tls ? "https" : "http";
54
+ const listener = async (req: IncomingMessage, res: ServerResponse): Promise<void> => {
55
+ try {
56
+ const method = req.method ?? "GET";
57
+ const body = method === "GET" || method === "HEAD" ? undefined : await readBody(req);
58
+ const headers = new Headers();
59
+ for (const [k, v] of Object.entries(req.headers)) if (typeof v === "string") headers.set(k, v);
60
+ const answer = await handler(new Request(`${scheme}://${req.headers.host ?? hostname}${req.url ?? "/"}`, { method, headers, ...(body ? { body: new Uint8Array(body) } : {}) }));
61
+ const out: Record<string, string> = {};
62
+ answer.headers.forEach((v, k) => (out[k] = v));
63
+ res.writeHead(answer.status, out);
64
+ res.end(answer.body ? Buffer.from(await answer.arrayBuffer()) : undefined);
65
+ } catch (e) {
66
+ res.writeHead(500, { "content-type": "text/plain" });
67
+ res.end(String((e as Error).message));
68
+ }
69
+ };
70
+ const server: Server = options.tls ? createHttpsServer({ key: options.tls.key, cert: options.tls.cert }, (q, r) => void listener(q, r)) : createHttpServer((q, r) => void listener(q, r));
71
+ await new Promise<void>((resolve, reject) => {
72
+ server.once("error", reject);
73
+ server.listen(options.port ?? 0, hostname, () => resolve());
74
+ });
75
+ const { port } = server.address() as AddressInfo;
76
+ const host = `${hostname.includes(":") ? `[${hostname}]` : hostname}:${port}`;
77
+ return {
78
+ url: `${scheme}://${host}`,
79
+ host,
80
+ close: () => new Promise<void>((resolve, reject) => server.close((e) => (e ? reject(e) : resolve()))),
81
+ };
82
+ }
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Module sources and provider addresses in one normal form (chant#3924):
3
+ * the forms a source matches as, a provider's full address, the implied
4
+ * provider, and the lock file's addresses.
5
+ */
6
+ import { describe, expect, test } from "vitest";
7
+ import { impliedProvider, lockedProviders, moduleSourceForms, normalizeModuleSource, parseProviderAddress, providerAddress, providerAddressForms } from "./source-forms";
8
+
9
+ describe("moduleSourceForms", () => {
10
+ test("drops the query and adds a hostless source at both default registries", () => {
11
+ expect(moduleSourceForms("terraform-aws-modules/vpc/aws?ref=v1")).toEqual([
12
+ "terraform-aws-modules/vpc/aws",
13
+ "registry.terraform.io/terraform-aws-modules/vpc/aws",
14
+ "registry.opentofu.org/terraform-aws-modules/vpc/aws",
15
+ ]);
16
+ });
17
+
18
+ test("adds the hostless and other-registry forms of a source at a default registry", () => {
19
+ expect(moduleSourceForms("registry.opentofu.org/acme/vpc/aws")).toEqual(["registry.opentofu.org/acme/vpc/aws", "acme/vpc/aws", "registry.terraform.io/acme/vpc/aws"]);
20
+ });
21
+
22
+ test("reads Terragrunt's tfr forms", () => {
23
+ expect(moduleSourceForms("tfr:///acme/vpc/aws?version=1.0.0")).toEqual(["tfr:///acme/vpc/aws", "registry.terraform.io/acme/vpc/aws", "registry.opentofu.org/acme/vpc/aws", "acme/vpc/aws"]);
24
+ expect(moduleSourceForms("tfr://Registry.Acme.dev/acme/vpc/aws")).toEqual(["tfr://Registry.Acme.dev/acme/vpc/aws", "registry.acme.dev/acme/vpc/aws"]);
25
+ });
26
+
27
+ test("keeps a private registry, git, OCI and local source to its one form", () => {
28
+ expect(moduleSourceForms("registry.acme.dev/acme/vpc/aws")).toEqual(["registry.acme.dev/acme/vpc/aws"]);
29
+ expect(moduleSourceForms("git::https://git.acme.dev/net.git?ref=v1")).toEqual(["git::https://git.acme.dev/net.git"]);
30
+ expect(moduleSourceForms("oci://registry.acme.dev/modules/vpc?tag=1")).toEqual(["oci://registry.acme.dev/modules/vpc"]);
31
+ expect(moduleSourceForms("./modules/vpc")).toEqual(["./modules/vpc"]);
32
+ expect(moduleSourceForms("github.com/acme/vpc")).toEqual(["github.com/acme/vpc"]);
33
+ });
34
+ });
35
+
36
+ describe("normalizeModuleSource", () => {
37
+ test("gives the binary's registry to a hostless source and lower-cases a host", () => {
38
+ expect(normalizeModuleSource("acme/vpc/aws")).toBe("registry.terraform.io/acme/vpc/aws");
39
+ expect(normalizeModuleSource("acme/vpc/aws", "tofu")).toBe("registry.opentofu.org/acme/vpc/aws");
40
+ expect(normalizeModuleSource("Registry.Acme.dev/acme/vpc/aws//sub")).toBe("registry.acme.dev/acme/vpc/aws//sub");
41
+ expect(normalizeModuleSource("tfr:///acme/vpc/aws")).toBe("registry.terraform.io/acme/vpc/aws");
42
+ });
43
+ });
44
+
45
+ describe("providerAddress", () => {
46
+ test("gives the full lower-case form with the binary's default host", () => {
47
+ expect(providerAddress("hashicorp/AWS")).toBe("registry.terraform.io/hashicorp/aws");
48
+ expect(providerAddress("hashicorp/aws", "tofu")).toBe("registry.opentofu.org/hashicorp/aws");
49
+ expect(providerAddress("aws")).toBe("registry.terraform.io/hashicorp/aws");
50
+ expect(providerAddress("Registry.Acme.dev/acme/thing")).toBe("registry.acme.dev/acme/thing");
51
+ expect(parseProviderAddress("localhost:8080/acme/thing")).toEqual({ host: "localhost:8080", namespace: "acme", type: "thing", address: "localhost:8080/acme/thing" });
52
+ });
53
+
54
+ test("refuses what is not an address", () => {
55
+ expect(providerAddress("a/b/c/d")).toBeUndefined();
56
+ expect(providerAddress("hashicorp/")).toBeUndefined();
57
+ expect(providerAddress("bad host/acme/x")).toBeUndefined();
58
+ });
59
+
60
+ test("implies hashicorp/<name>, never for the binary's own provider", () => {
61
+ expect(impliedProvider("aws")).toBe("hashicorp/aws");
62
+ expect(impliedProvider("terraform")).toBeUndefined();
63
+ });
64
+
65
+ test("matches as written, hostless and at each default registry", () => {
66
+ expect(providerAddressForms("hashicorp/aws")).toEqual(["hashicorp/aws", "registry.terraform.io/hashicorp/aws", "registry.opentofu.org/hashicorp/aws"]);
67
+ expect(providerAddressForms("registry.terraform.io/hashicorp/aws")).toEqual(["registry.terraform.io/hashicorp/aws", "hashicorp/aws", "registry.opentofu.org/hashicorp/aws"]);
68
+ expect(providerAddressForms("registry.acme.dev/acme/x")).toEqual(["registry.acme.dev/acme/x"]);
69
+ });
70
+ });
71
+
72
+ describe("lockedProviders", () => {
73
+ test("lists each provider the lock file names, once", () => {
74
+ const lock = `# This file is maintained automatically by "terraform init".
75
+ provider "registry.terraform.io/hashicorp/aws" {
76
+ version = "5.0.0"
77
+ hashes = ["h1:x"]
78
+ }
79
+
80
+ provider "registry.opentofu.org/hashicorp/random" {
81
+ version = "3.6.0"
82
+ }
83
+ `;
84
+ expect(lockedProviders(lock)).toEqual(["registry.terraform.io/hashicorp/aws", "registry.opentofu.org/hashicorp/random"]);
85
+ expect(lockedProviders("")).toEqual([]);
86
+ });
87
+ });