chef-zero 15.0.17 → 15.1.11

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 (123) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +36 -31
  3. data/LICENSE +201 -201
  4. data/Rakefile +73 -68
  5. data/bin/chef-zero +111 -111
  6. data/chef-zero.gemspec +29 -33
  7. data/lib/chef_zero/chef_data/acl_path.rb +140 -140
  8. data/lib/chef_zero/chef_data/cookbook_data.rb +237 -237
  9. data/lib/chef_zero/chef_data/data_normalizer.rb +276 -276
  10. data/lib/chef_zero/chef_data/default_creator.rb +476 -476
  11. data/lib/chef_zero/data_store/data_already_exists_error.rb +29 -29
  12. data/lib/chef_zero/data_store/data_error.rb +32 -32
  13. data/lib/chef_zero/data_store/data_not_found_error.rb +29 -29
  14. data/lib/chef_zero/data_store/default_facade.rb +143 -147
  15. data/lib/chef_zero/data_store/interface_v1.rb +67 -67
  16. data/lib/chef_zero/data_store/interface_v2.rb +18 -18
  17. data/lib/chef_zero/data_store/memory_store.rb +33 -33
  18. data/lib/chef_zero/data_store/memory_store_v2.rb +159 -159
  19. data/lib/chef_zero/data_store/raw_file_store.rb +143 -143
  20. data/lib/chef_zero/data_store/v1_to_v2_adapter.rb +150 -150
  21. data/lib/chef_zero/data_store/v2_to_v1_adapter.rb +105 -105
  22. data/lib/chef_zero/dist.rb +9 -9
  23. data/lib/chef_zero/endpoints/acl_endpoint.rb +39 -39
  24. data/lib/chef_zero/endpoints/acls_endpoint.rb +41 -41
  25. data/lib/chef_zero/endpoints/actor_default_key_endpoint.rb +78 -78
  26. data/lib/chef_zero/endpoints/actor_endpoint.rb +184 -184
  27. data/lib/chef_zero/endpoints/actor_key_endpoint.rb +62 -62
  28. data/lib/chef_zero/endpoints/actor_keys_endpoint.rb +129 -129
  29. data/lib/chef_zero/endpoints/actors_endpoint.rb +104 -104
  30. data/lib/chef_zero/endpoints/authenticate_user_endpoint.rb +32 -32
  31. data/lib/chef_zero/endpoints/container_endpoint.rb +22 -22
  32. data/lib/chef_zero/endpoints/containers_endpoint.rb +25 -25
  33. data/lib/chef_zero/endpoints/controls_endpoint.rb +16 -16
  34. data/lib/chef_zero/endpoints/cookbook_artifact_endpoint.rb +24 -24
  35. data/lib/chef_zero/endpoints/cookbook_artifact_identifier_endpoint.rb +68 -68
  36. data/lib/chef_zero/endpoints/cookbook_artifacts_endpoint.rb +34 -34
  37. data/lib/chef_zero/endpoints/cookbook_endpoint.rb +39 -39
  38. data/lib/chef_zero/endpoints/cookbook_version_endpoint.rb +136 -136
  39. data/lib/chef_zero/endpoints/cookbooks_base.rb +80 -80
  40. data/lib/chef_zero/endpoints/cookbooks_endpoint.rb +19 -19
  41. data/lib/chef_zero/endpoints/data_bag_endpoint.rb +45 -45
  42. data/lib/chef_zero/endpoints/data_bag_item_endpoint.rb +25 -25
  43. data/lib/chef_zero/endpoints/data_bags_endpoint.rb +23 -23
  44. data/lib/chef_zero/endpoints/dummy_endpoint.rb +29 -29
  45. data/lib/chef_zero/endpoints/environment_cookbook_endpoint.rb +24 -24
  46. data/lib/chef_zero/endpoints/environment_cookbook_versions_endpoint.rb +139 -126
  47. data/lib/chef_zero/endpoints/environment_cookbooks_endpoint.rb +22 -22
  48. data/lib/chef_zero/endpoints/environment_endpoint.rb +33 -33
  49. data/lib/chef_zero/endpoints/environment_nodes_endpoint.rb +23 -23
  50. data/lib/chef_zero/endpoints/environment_recipes_endpoint.rb +22 -22
  51. data/lib/chef_zero/endpoints/environment_role_endpoint.rb +36 -36
  52. data/lib/chef_zero/endpoints/file_store_file_endpoint.rb +22 -22
  53. data/lib/chef_zero/endpoints/group_endpoint.rb +20 -20
  54. data/lib/chef_zero/endpoints/groups_endpoint.rb +13 -13
  55. data/lib/chef_zero/endpoints/license_endpoint.rb +25 -25
  56. data/lib/chef_zero/endpoints/node_endpoint.rb +34 -34
  57. data/lib/chef_zero/endpoints/node_identifiers_endpoint.rb +22 -22
  58. data/lib/chef_zero/endpoints/nodes_endpoint.rb +34 -34
  59. data/lib/chef_zero/endpoints/not_found_endpoint.rb +11 -11
  60. data/lib/chef_zero/endpoints/organization_association_request_endpoint.rb +22 -22
  61. data/lib/chef_zero/endpoints/organization_association_requests_endpoint.rb +30 -30
  62. data/lib/chef_zero/endpoints/organization_authenticate_user_endpoint.rb +26 -26
  63. data/lib/chef_zero/endpoints/organization_endpoint.rb +47 -47
  64. data/lib/chef_zero/endpoints/organization_user_base.rb +15 -15
  65. data/lib/chef_zero/endpoints/organization_user_default_key_endpoint.rb +16 -16
  66. data/lib/chef_zero/endpoints/organization_user_endpoint.rb +26 -26
  67. data/lib/chef_zero/endpoints/organization_user_key_endpoint.rb +17 -17
  68. data/lib/chef_zero/endpoints/organization_user_keys_endpoint.rb +17 -17
  69. data/lib/chef_zero/endpoints/organization_users_endpoint.rb +43 -43
  70. data/lib/chef_zero/endpoints/organization_validator_key_endpoint.rb +20 -20
  71. data/lib/chef_zero/endpoints/organizations_endpoint.rb +61 -61
  72. data/lib/chef_zero/endpoints/policies_endpoint.rb +26 -26
  73. data/lib/chef_zero/endpoints/policy_endpoint.rb +24 -24
  74. data/lib/chef_zero/endpoints/policy_group_endpoint.rb +46 -46
  75. data/lib/chef_zero/endpoints/policy_group_policy_endpoint.rb +83 -83
  76. data/lib/chef_zero/endpoints/policy_groups_endpoint.rb +38 -38
  77. data/lib/chef_zero/endpoints/policy_revision_endpoint.rb +66 -66
  78. data/lib/chef_zero/endpoints/policy_revisions_endpoint.rb +15 -15
  79. data/lib/chef_zero/endpoints/principal_endpoint.rb +55 -55
  80. data/lib/chef_zero/endpoints/rest_list_endpoint.rb +42 -42
  81. data/lib/chef_zero/endpoints/rest_object_endpoint.rb +78 -78
  82. data/lib/chef_zero/endpoints/role_endpoint.rb +16 -16
  83. data/lib/chef_zero/endpoints/role_environments_endpoint.rb +14 -14
  84. data/lib/chef_zero/endpoints/sandbox_endpoint.rb +27 -27
  85. data/lib/chef_zero/endpoints/sandboxes_endpoint.rb +51 -51
  86. data/lib/chef_zero/endpoints/search_endpoint.rb +208 -208
  87. data/lib/chef_zero/endpoints/searches_endpoint.rb +18 -18
  88. data/lib/chef_zero/endpoints/server_api_version_endpoint.rb +14 -14
  89. data/lib/chef_zero/endpoints/system_recovery_endpoint.rb +30 -30
  90. data/lib/chef_zero/endpoints/universe_endpoint.rb +15 -15
  91. data/lib/chef_zero/endpoints/user_association_request_endpoint.rb +41 -41
  92. data/lib/chef_zero/endpoints/user_association_requests_count_endpoint.rb +19 -19
  93. data/lib/chef_zero/endpoints/user_association_requests_endpoint.rb +19 -19
  94. data/lib/chef_zero/endpoints/user_organizations_endpoint.rb +22 -22
  95. data/lib/chef_zero/endpoints/version_endpoint.rb +13 -13
  96. data/lib/chef_zero/log.rb +7 -7
  97. data/lib/chef_zero/rest_base.rb +332 -332
  98. data/lib/chef_zero/rest_error_response.rb +11 -11
  99. data/lib/chef_zero/rest_request.rb +84 -88
  100. data/lib/chef_zero/rest_router.rb +72 -72
  101. data/lib/chef_zero/rspec.rb +355 -355
  102. data/lib/chef_zero/server.rb +730 -730
  103. data/lib/chef_zero/socketless_server_map.rb +92 -93
  104. data/lib/chef_zero/solr/query/binary_operator.rb +52 -52
  105. data/lib/chef_zero/solr/query/phrase.rb +23 -23
  106. data/lib/chef_zero/solr/query/range_query.rb +46 -46
  107. data/lib/chef_zero/solr/query/regexpable_query.rb +30 -30
  108. data/lib/chef_zero/solr/query/subquery.rb +37 -37
  109. data/lib/chef_zero/solr/query/term.rb +45 -45
  110. data/lib/chef_zero/solr/query/unary_operator.rb +41 -41
  111. data/lib/chef_zero/solr/solr_doc.rb +53 -53
  112. data/lib/chef_zero/solr/solr_parser.rb +208 -208
  113. data/lib/chef_zero/version.rb +3 -3
  114. data/lib/chef_zero.rb +10 -10
  115. data/spec/endpoints/environment_cookbook_versions_endpoint_spec.rb +436 -0
  116. data/spec/run_oc_pedant.rb +210 -226
  117. data/spec/search_spec.rb +36 -36
  118. data/spec/server_spec.rb +96 -96
  119. data/spec/socketless_server_map_spec.rb +74 -74
  120. data/spec/support/oc_pedant.rb +149 -149
  121. data/spec/support/secrets.json +6 -6
  122. data/spec/support/stickywicket.pem +27 -27
  123. metadata +30 -40
@@ -0,0 +1,436 @@
1
+ require "uri" unless defined?(URI)
2
+ require "chef_zero/endpoints/environment_cookbook_versions_endpoint"
3
+
4
+ describe ChefZero::Endpoints::EnvironmentCookbookVersionsEndpoint do
5
+ let(:server) { double("server") }
6
+ let(:endpoint) { described_class.new(server) }
7
+
8
+ describe "#sort_versions" do
9
+ it "returns versions sorted descending by semver" do
10
+ versions = ["1.0.0", "2.0.0", "1.5.0", "0.9.0"]
11
+ expect(endpoint.sort_versions(versions)).to eq(["2.0.0", "1.5.0", "1.0.0", "0.9.0"])
12
+ end
13
+
14
+ it "handles single-element lists" do
15
+ expect(endpoint.sort_versions(["1.0.0"])).to eq(["1.0.0"])
16
+ end
17
+
18
+ it "handles an empty list" do
19
+ expect(endpoint.sort_versions([])).to eq([])
20
+ end
21
+
22
+ it "sorts multi-digit version components numerically" do
23
+ versions = ["1.2.3", "1.10.0", "1.9.0"]
24
+ expect(endpoint.sort_versions(versions)).to eq(["1.10.0", "1.9.0", "1.2.3"])
25
+ end
26
+ end
27
+
28
+ describe "#filter_by_constraint" do
29
+ let(:versions) { { "apache" => ["1.0.0", "1.5.0", "2.0.0", "2.1.0"] } }
30
+
31
+ it "returns versions unchanged when constraint is nil" do
32
+ result = endpoint.filter_by_constraint(versions, "apache", nil)
33
+ expect(result["apache"]).to eq(["1.0.0", "1.5.0", "2.0.0", "2.1.0"])
34
+ end
35
+
36
+ it "filters with an exact version constraint" do
37
+ result = endpoint.filter_by_constraint(versions, "apache", "= 1.5.0")
38
+ expect(result["apache"]).to eq(["1.5.0"])
39
+ end
40
+
41
+ it "filters with a >= constraint" do
42
+ result = endpoint.filter_by_constraint(versions, "apache", ">= 2.0.0")
43
+ expect(result["apache"]).to eq(["2.0.0", "2.1.0"])
44
+ end
45
+
46
+ it "filters with a ~> constraint" do
47
+ result = endpoint.filter_by_constraint(versions, "apache", "~> 1.0")
48
+ expect(result["apache"]).to eq(["1.0.0", "1.5.0"])
49
+ end
50
+
51
+ it "returns empty array when no versions match" do
52
+ result = endpoint.filter_by_constraint(versions, "apache", "= 9.9.9")
53
+ expect(result["apache"]).to eq([])
54
+ end
55
+
56
+ it "does not mutate the original versions hash" do
57
+ original = versions.dup
58
+ endpoint.filter_by_constraint(versions, "apache", "= 1.0.0")
59
+ expect(versions).to eq(original)
60
+ end
61
+ end
62
+
63
+ describe "#depsolve" do
64
+ let(:org_prefix) { %w{organizations testorg} }
65
+ let(:request) { double("request", rest_path: org_prefix + %w{environments _default cookbook_versions}) }
66
+ let(:data_store) { double("data_store") }
67
+
68
+ before do
69
+ allow(server).to receive(:data_store).and_return(data_store)
70
+ end
71
+
72
+ def cookbook_json(name, version, dependencies = {})
73
+ FFI_Yajl::Encoder.encode({
74
+ "metadata" => {
75
+ "name" => name,
76
+ "version" => version,
77
+ "dependencies" => dependencies,
78
+ },
79
+ })
80
+ end
81
+
82
+ context "base cases" do
83
+ it "returns [nil, nil] when a cookbook has empty versions" do
84
+ desired = { "apache" => [] }
85
+ result, _cache = endpoint.depsolve(request, ["apache"], desired, {})
86
+ expect(result).to be_nil
87
+ end
88
+
89
+ it "sets @last_constraint_failure when a cookbook has empty versions" do
90
+ desired = { "apache" => [] }
91
+ endpoint.depsolve(request, ["apache"], desired, {})
92
+ expect(endpoint.instance_variable_get(:@last_constraint_failure)).to eq("apache")
93
+ end
94
+
95
+ it "returns desired_versions when unsolved list is empty" do
96
+ desired = { "apache" => ["1.0.0"] }
97
+ result, _cache = endpoint.depsolve(request, [], desired, {})
98
+ expect(result).to eq(desired)
99
+ end
100
+
101
+ it "resolves a cookbook with no metadata key" do
102
+ desired = { "minimal" => ["1.0.0"] }
103
+ allow(data_store).to receive(:get)
104
+ .with(org_prefix + ["cookbooks", "minimal", "1.0.0"], request)
105
+ .and_return(FFI_Yajl::Encoder.encode({}))
106
+
107
+ result, cache = endpoint.depsolve(request, ["minimal"], desired, {})
108
+ expect(result["minimal"]).to eq(["1.0.0"])
109
+ expect(cache["minimal"]).to have_key("1.0.0")
110
+ end
111
+ end
112
+
113
+ context "simple resolution" do
114
+ it "resolves a single cookbook with no dependencies" do
115
+ desired = { "apache" => ["1.0.0", "2.0.0"] }
116
+ allow(data_store).to receive(:get)
117
+ .with(org_prefix + ["cookbooks", "apache", "2.0.0"], request)
118
+ .and_return(cookbook_json("apache", "2.0.0"))
119
+ allow(data_store).to receive(:get)
120
+ .with(org_prefix + ["cookbooks", "apache", "1.0.0"], request)
121
+ .and_return(cookbook_json("apache", "1.0.0"))
122
+
123
+ result, cache = endpoint.depsolve(request, ["apache"], desired, {})
124
+ expect(result["apache"]).to eq(["2.0.0"])
125
+ expect(cache["apache"]["2.0.0"]).to be_a(Hash)
126
+ end
127
+
128
+ it "resolves a cookbook with a satisfiable dependency" do
129
+ desired = { "apache" => ["1.0.0"] }
130
+ allow(data_store).to receive(:get)
131
+ .with(org_prefix + ["cookbooks", "apache", "1.0.0"], request)
132
+ .and_return(cookbook_json("apache", "1.0.0", { "mysql" => ">= 1.0.0" }))
133
+ allow(data_store).to receive(:exists_dir?)
134
+ .with(org_prefix + %w{cookbooks mysql})
135
+ .and_return(true)
136
+ allow(data_store).to receive(:list)
137
+ .with(org_prefix + %w{cookbooks mysql})
138
+ .and_return(["1.0.0", "2.0.0"])
139
+ allow(data_store).to receive(:get)
140
+ .with(org_prefix + ["cookbooks", "mysql", "2.0.0"], request)
141
+ .and_return(cookbook_json("mysql", "2.0.0"))
142
+ allow(data_store).to receive(:get)
143
+ .with(org_prefix + ["cookbooks", "mysql", "1.0.0"], request)
144
+ .and_return(cookbook_json("mysql", "1.0.0"))
145
+
146
+ result, _cache = endpoint.depsolve(request, ["apache"], desired, {})
147
+ expect(result["apache"]).to eq(["1.0.0"])
148
+ expect(result["mysql"]).to eq(["2.0.0"])
149
+ end
150
+
151
+ it "returns [nil, nil] when a dependency cookbook does not exist" do
152
+ desired = { "apache" => ["1.0.0"] }
153
+ allow(data_store).to receive(:get)
154
+ .with(org_prefix + ["cookbooks", "apache", "1.0.0"], request)
155
+ .and_return(cookbook_json("apache", "1.0.0", { "missing" => ">= 0.0.0" }))
156
+ allow(data_store).to receive(:exists_dir?)
157
+ .with(org_prefix + %w{cookbooks missing})
158
+ .and_return(false)
159
+
160
+ result, _cache = endpoint.depsolve(request, ["apache"], desired, {})
161
+ expect(result).to be_nil
162
+ expect(endpoint.instance_variable_get(:@last_missing_dep)).to eq("missing")
163
+ end
164
+ end
165
+
166
+ context "constraint filtering" do
167
+ it "applies environment constraints to dependency versions" do
168
+ desired = { "apache" => ["1.0.0"] }
169
+ env_constraints = { "mysql" => "= 1.0.0" }
170
+
171
+ allow(data_store).to receive(:get)
172
+ .with(org_prefix + ["cookbooks", "apache", "1.0.0"], request)
173
+ .and_return(cookbook_json("apache", "1.0.0", { "mysql" => ">= 0.0.0" }))
174
+ allow(data_store).to receive(:exists_dir?)
175
+ .with(org_prefix + %w{cookbooks mysql})
176
+ .and_return(true)
177
+ allow(data_store).to receive(:list)
178
+ .with(org_prefix + %w{cookbooks mysql})
179
+ .and_return(["1.0.0", "2.0.0"])
180
+ allow(data_store).to receive(:get)
181
+ .with(org_prefix + ["cookbooks", "mysql", "1.0.0"], request)
182
+ .and_return(cookbook_json("mysql", "1.0.0"))
183
+
184
+ result, _cache = endpoint.depsolve(request, ["apache"], desired, env_constraints)
185
+ expect(result["mysql"]).to eq(["1.0.0"])
186
+ end
187
+
188
+ it "filters dependency versions by the dependency's own constraint" do
189
+ desired = { "apache" => ["1.0.0"] }
190
+ allow(data_store).to receive(:get)
191
+ .with(org_prefix + ["cookbooks", "apache", "1.0.0"], request)
192
+ .and_return(cookbook_json("apache", "1.0.0", { "mysql" => ">= 2.0.0" }))
193
+ allow(data_store).to receive(:exists_dir?)
194
+ .with(org_prefix + %w{cookbooks mysql})
195
+ .and_return(true)
196
+ allow(data_store).to receive(:list)
197
+ .with(org_prefix + %w{cookbooks mysql})
198
+ .and_return(["1.0.0", "2.0.0"])
199
+ allow(data_store).to receive(:get)
200
+ .with(org_prefix + ["cookbooks", "mysql", "2.0.0"], request)
201
+ .and_return(cookbook_json("mysql", "2.0.0"))
202
+
203
+ result, _cache = endpoint.depsolve(request, ["apache"], desired, {})
204
+ expect(result["mysql"]).to eq(["2.0.0"])
205
+ end
206
+
207
+ it "returns nil when dep constraint filters out all available versions" do
208
+ desired = { "apache" => ["1.0.0"] }
209
+ allow(data_store).to receive(:get)
210
+ .with(org_prefix + ["cookbooks", "apache", "1.0.0"], request)
211
+ .and_return(cookbook_json("apache", "1.0.0", { "mysql" => ">= 5.0.0" }))
212
+ allow(data_store).to receive(:exists_dir?)
213
+ .with(org_prefix + %w{cookbooks mysql})
214
+ .and_return(true)
215
+ allow(data_store).to receive(:list)
216
+ .with(org_prefix + %w{cookbooks mysql})
217
+ .and_return(["1.0.0", "2.0.0"])
218
+
219
+ result, _cache = endpoint.depsolve(request, ["apache"], desired, {})
220
+ expect(result).to be_nil
221
+ end
222
+ end
223
+
224
+ context "backtracking" do
225
+ it "falls back to an older version when newest has unsatisfiable dep" do
226
+ desired = { "web" => ["1.0.0", "2.0.0"] }
227
+ # v2.0.0 depends on "ghost" which doesn't exist
228
+ allow(data_store).to receive(:get)
229
+ .with(org_prefix + ["cookbooks", "web", "2.0.0"], request)
230
+ .and_return(cookbook_json("web", "2.0.0", { "ghost" => ">= 0.0.0" }))
231
+ allow(data_store).to receive(:exists_dir?)
232
+ .with(org_prefix + %w{cookbooks ghost})
233
+ .and_return(false)
234
+ # v1.0.0 has no deps
235
+ allow(data_store).to receive(:get)
236
+ .with(org_prefix + ["cookbooks", "web", "1.0.0"], request)
237
+ .and_return(cookbook_json("web", "1.0.0"))
238
+
239
+ result, _cache = endpoint.depsolve(request, ["web"], desired, {})
240
+ expect(result["web"]).to eq(["1.0.0"])
241
+ end
242
+
243
+ it "backtracks through multiple failing versions" do
244
+ desired = { "web" => ["1.0.0", "2.0.0", "3.0.0"] }
245
+ allow(data_store).to receive(:get)
246
+ .with(org_prefix + ["cookbooks", "web", "3.0.0"], request)
247
+ .and_return(cookbook_json("web", "3.0.0", { "missing_a" => ">= 0.0.0" }))
248
+ allow(data_store).to receive(:exists_dir?)
249
+ .with(org_prefix + %w{cookbooks missing_a})
250
+ .and_return(false)
251
+ allow(data_store).to receive(:get)
252
+ .with(org_prefix + ["cookbooks", "web", "2.0.0"], request)
253
+ .and_return(cookbook_json("web", "2.0.0", { "missing_b" => ">= 0.0.0" }))
254
+ allow(data_store).to receive(:exists_dir?)
255
+ .with(org_prefix + %w{cookbooks missing_b})
256
+ .and_return(false)
257
+ allow(data_store).to receive(:get)
258
+ .with(org_prefix + ["cookbooks", "web", "1.0.0"], request)
259
+ .and_return(cookbook_json("web", "1.0.0"))
260
+
261
+ result, _cache = endpoint.depsolve(request, ["web"], desired, {})
262
+ expect(result["web"]).to eq(["1.0.0"])
263
+ end
264
+ end
265
+
266
+ context "multiple cookbooks and dependencies" do
267
+ it "populates cache entries for all resolved cookbooks" do
268
+ desired = { "apache" => ["1.0.0"] }
269
+ allow(data_store).to receive(:get)
270
+ .with(org_prefix + ["cookbooks", "apache", "1.0.0"], request)
271
+ .and_return(cookbook_json("apache", "1.0.0", { "mysql" => ">= 1.0.0" }))
272
+ allow(data_store).to receive(:exists_dir?)
273
+ .with(org_prefix + %w{cookbooks mysql})
274
+ .and_return(true)
275
+ allow(data_store).to receive(:list)
276
+ .with(org_prefix + %w{cookbooks mysql})
277
+ .and_return(["1.0.0"])
278
+ allow(data_store).to receive(:get)
279
+ .with(org_prefix + ["cookbooks", "mysql", "1.0.0"], request)
280
+ .and_return(cookbook_json("mysql", "1.0.0"))
281
+
282
+ _result, cache = endpoint.depsolve(request, ["apache"], desired, {})
283
+ expect(cache["apache"]).to have_key("1.0.0")
284
+ expect(cache["apache"]["1.0.0"]["metadata"]["name"]).to eq("apache")
285
+ expect(cache["mysql"]).to have_key("1.0.0")
286
+ expect(cache["mysql"]["1.0.0"]["metadata"]["name"]).to eq("mysql")
287
+ end
288
+
289
+ it "resolves multiple independent unsolved cookbooks" do
290
+ desired = {
291
+ "apache" => ["1.0.0", "2.0.0"],
292
+ "nginx" => ["3.0.0", "4.0.0"],
293
+ }
294
+ allow(data_store).to receive(:get)
295
+ .with(org_prefix + ["cookbooks", "apache", "2.0.0"], request)
296
+ .and_return(cookbook_json("apache", "2.0.0"))
297
+ allow(data_store).to receive(:get)
298
+ .with(org_prefix + ["cookbooks", "apache", "1.0.0"], request)
299
+ .and_return(cookbook_json("apache", "1.0.0"))
300
+ allow(data_store).to receive(:get)
301
+ .with(org_prefix + ["cookbooks", "nginx", "4.0.0"], request)
302
+ .and_return(cookbook_json("nginx", "4.0.0"))
303
+ allow(data_store).to receive(:get)
304
+ .with(org_prefix + ["cookbooks", "nginx", "3.0.0"], request)
305
+ .and_return(cookbook_json("nginx", "3.0.0"))
306
+
307
+ result, cache = endpoint.depsolve(request, %w{apache nginx}, desired, {})
308
+ expect(result["apache"]).to eq(["2.0.0"])
309
+ expect(result["nginx"]).to eq(["4.0.0"])
310
+ expect(cache.keys).to contain_exactly("apache", "nginx")
311
+ end
312
+
313
+ it "resolves a diamond dependency (two cookbooks sharing a dep)" do
314
+ desired = {
315
+ "apache" => ["1.0.0"],
316
+ "nginx" => ["1.0.0"],
317
+ }
318
+ allow(data_store).to receive(:get)
319
+ .with(org_prefix + ["cookbooks", "apache", "1.0.0"], request)
320
+ .and_return(cookbook_json("apache", "1.0.0", { "mysql" => ">= 1.0.0" }))
321
+ allow(data_store).to receive(:get)
322
+ .with(org_prefix + ["cookbooks", "nginx", "1.0.0"], request)
323
+ .and_return(cookbook_json("nginx", "1.0.0", { "mysql" => ">= 2.0.0" }))
324
+ allow(data_store).to receive(:exists_dir?)
325
+ .with(org_prefix + %w{cookbooks mysql})
326
+ .and_return(true)
327
+ allow(data_store).to receive(:list)
328
+ .with(org_prefix + %w{cookbooks mysql})
329
+ .and_return(["1.0.0", "2.0.0", "3.0.0"])
330
+ allow(data_store).to receive(:get)
331
+ .with(org_prefix + ["cookbooks", "mysql", "3.0.0"], request)
332
+ .and_return(cookbook_json("mysql", "3.0.0"))
333
+ allow(data_store).to receive(:get)
334
+ .with(org_prefix + ["cookbooks", "mysql", "2.0.0"], request)
335
+ .and_return(cookbook_json("mysql", "2.0.0"))
336
+
337
+ result, _cache = endpoint.depsolve(request, %w{apache nginx}, desired, {})
338
+ expect(result["apache"]).to eq(["1.0.0"])
339
+ expect(result["nginx"]).to eq(["1.0.0"])
340
+ expect(result["mysql"]).to eq(["3.0.0"])
341
+ end
342
+
343
+ it "returns nil when dependency constraints from two cookbooks conflict" do
344
+ desired = {
345
+ "apache" => ["1.0.0"],
346
+ "nginx" => ["1.0.0"],
347
+ }
348
+ allow(data_store).to receive(:get)
349
+ .with(org_prefix + ["cookbooks", "apache", "1.0.0"], request)
350
+ .and_return(cookbook_json("apache", "1.0.0", { "mysql" => ">= 2.0.0" }))
351
+ allow(data_store).to receive(:get)
352
+ .with(org_prefix + ["cookbooks", "nginx", "1.0.0"], request)
353
+ .and_return(cookbook_json("nginx", "1.0.0", { "mysql" => "< 2.0.0" }))
354
+ allow(data_store).to receive(:exists_dir?)
355
+ .with(org_prefix + %w{cookbooks mysql})
356
+ .and_return(true)
357
+ allow(data_store).to receive(:list)
358
+ .with(org_prefix + %w{cookbooks mysql})
359
+ .and_return(["1.0.0", "2.0.0"])
360
+
361
+ result, _cache = endpoint.depsolve(request, %w{apache nginx}, desired, {})
362
+ expect(result).to be_nil
363
+ end
364
+
365
+ it "uses cached cookbook data instead of re-fetching during backtracking" do
366
+ desired = {
367
+ "web" => ["1.0.0", "2.0.0"],
368
+ "db" => ["1.0.0"],
369
+ }
370
+ # web 2.0.0 depends on lib, which depends on missing (doesn't exist) → branch fails
371
+ # web 1.0.0 has no deps → backtracks here, db must be re-solved
372
+ # db 1.0.0 should be fetched only once (cached from the first branch)
373
+ allow(data_store).to receive(:get)
374
+ .with(org_prefix + ["cookbooks", "web", "2.0.0"], request)
375
+ .and_return(cookbook_json("web", "2.0.0", { "lib" => ">= 1.0.0" }))
376
+ allow(data_store).to receive(:get)
377
+ .with(org_prefix + ["cookbooks", "web", "1.0.0"], request)
378
+ .and_return(cookbook_json("web", "1.0.0"))
379
+ expect(data_store).to receive(:get)
380
+ .with(org_prefix + ["cookbooks", "db", "1.0.0"], request)
381
+ .once
382
+ .and_return(cookbook_json("db", "1.0.0"))
383
+ allow(data_store).to receive(:exists_dir?)
384
+ .with(org_prefix + %w{cookbooks lib})
385
+ .and_return(true)
386
+ allow(data_store).to receive(:list)
387
+ .with(org_prefix + %w{cookbooks lib})
388
+ .and_return(["1.0.0"])
389
+ allow(data_store).to receive(:get)
390
+ .with(org_prefix + ["cookbooks", "lib", "1.0.0"], request)
391
+ .and_return(cookbook_json("lib", "1.0.0", { "missing" => ">= 1.0.0" }))
392
+ allow(data_store).to receive(:exists_dir?)
393
+ .with(org_prefix + %w{cookbooks missing})
394
+ .and_return(false)
395
+
396
+ result, _cache = endpoint.depsolve(request, %w{web db}, desired, {})
397
+ expect(result["web"]).to eq(["1.0.0"])
398
+ expect(result["db"]).to eq(["1.0.0"])
399
+ end
400
+
401
+ it "resolves a deep dependency chain" do
402
+ desired = { "app" => ["1.0.0"] }
403
+ allow(data_store).to receive(:get)
404
+ .with(org_prefix + ["cookbooks", "app", "1.0.0"], request)
405
+ .and_return(cookbook_json("app", "1.0.0", { "framework" => ">= 1.0.0" }))
406
+ allow(data_store).to receive(:exists_dir?)
407
+ .with(org_prefix + %w{cookbooks framework})
408
+ .and_return(true)
409
+ allow(data_store).to receive(:list)
410
+ .with(org_prefix + %w{cookbooks framework})
411
+ .and_return(["1.0.0"])
412
+ allow(data_store).to receive(:get)
413
+ .with(org_prefix + ["cookbooks", "framework", "1.0.0"], request)
414
+ .and_return(cookbook_json("framework", "1.0.0", { "lib" => ">= 1.0.0" }))
415
+ allow(data_store).to receive(:exists_dir?)
416
+ .with(org_prefix + %w{cookbooks lib})
417
+ .and_return(true)
418
+ allow(data_store).to receive(:list)
419
+ .with(org_prefix + %w{cookbooks lib})
420
+ .and_return(["1.0.0", "2.0.0"])
421
+ allow(data_store).to receive(:get)
422
+ .with(org_prefix + ["cookbooks", "lib", "2.0.0"], request)
423
+ .and_return(cookbook_json("lib", "2.0.0"))
424
+ allow(data_store).to receive(:get)
425
+ .with(org_prefix + ["cookbooks", "lib", "1.0.0"], request)
426
+ .and_return(cookbook_json("lib", "1.0.0"))
427
+
428
+ result, cache = endpoint.depsolve(request, ["app"], desired, {})
429
+ expect(result["app"]).to eq(["1.0.0"])
430
+ expect(result["framework"]).to eq(["1.0.0"])
431
+ expect(result["lib"]).to eq(["2.0.0"])
432
+ expect(cache.keys).to contain_exactly("app", "framework", "lib")
433
+ end
434
+ end
435
+ end
436
+ end