puppet 6.15.0 → 6.16.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of puppet might be problematic. Click here for more details.

Files changed (132) hide show
  1. checksums.yaml +4 -4
  2. data/CODEOWNERS +2 -7
  3. data/Gemfile.lock +17 -14
  4. data/lib/puppet.rb +32 -8
  5. data/lib/puppet/agent.rb +18 -4
  6. data/lib/puppet/application/agent.rb +1 -2
  7. data/lib/puppet/application/device.rb +1 -1
  8. data/lib/puppet/application/plugin.rb +1 -0
  9. data/lib/puppet/application/ssl.rb +1 -1
  10. data/lib/puppet/configurer.rb +2 -2
  11. data/lib/puppet/context/trusted_information.rb +14 -8
  12. data/lib/puppet/daemon.rb +13 -27
  13. data/lib/puppet/defaults.rb +19 -0
  14. data/lib/puppet/face/facts.rb +1 -1
  15. data/lib/puppet/face/help.rb +29 -3
  16. data/lib/puppet/face/module/search.rb +5 -0
  17. data/lib/puppet/face/plugin.rb +1 -1
  18. data/lib/puppet/file_serving/http_metadata.rb +1 -1
  19. data/lib/puppet/file_system/uniquefile.rb +4 -0
  20. data/lib/puppet/forge/repository.rb +7 -6
  21. data/lib/puppet/functions/filter.rb +1 -0
  22. data/lib/puppet/http/client.rb +22 -11
  23. data/lib/puppet/http/external_client.rb +0 -6
  24. data/lib/puppet/indirector/file_content/http.rb +5 -0
  25. data/lib/puppet/indirector/file_metadata/http.rb +4 -4
  26. data/lib/puppet/indirector/rest.rb +7 -1
  27. data/lib/puppet/network/http/compression.rb +7 -0
  28. data/lib/puppet/network/http/connection.rb +2 -0
  29. data/lib/puppet/network/http/connection_adapter.rb +182 -0
  30. data/lib/puppet/network/http/nocache_pool.rb +1 -0
  31. data/lib/puppet/network/http_pool.rb +2 -2
  32. data/lib/puppet/pal/catalog_compiler.rb +5 -0
  33. data/lib/puppet/pal/pal_impl.rb +4 -1
  34. data/lib/puppet/parser/compiler.rb +28 -25
  35. data/lib/puppet/parser/functions/filter.rb +1 -0
  36. data/lib/puppet/provider/package/aix.rb +17 -2
  37. data/lib/puppet/provider/package/apt.rb +4 -1
  38. data/lib/puppet/provider/package/dnfmodule.rb +24 -4
  39. data/lib/puppet/provider/package/pip.rb +60 -37
  40. data/lib/puppet/provider/package/portage.rb +2 -2
  41. data/lib/puppet/provider/package/yum.rb +7 -0
  42. data/lib/puppet/provider/package/zypper.rb +59 -1
  43. data/lib/puppet/provider/service/systemd.rb +21 -4
  44. data/lib/puppet/provider/user/useradd.rb +5 -1
  45. data/lib/puppet/reports/http.rb +5 -3
  46. data/lib/puppet/runtime.rb +25 -2
  47. data/lib/puppet/ssl/state_machine.rb +33 -8
  48. data/lib/puppet/ssl/verifier_adapter.rb +9 -1
  49. data/lib/puppet/test/test_helper.rb +1 -1
  50. data/lib/puppet/type/file/source.rb +1 -1
  51. data/lib/puppet/type/package.rb +16 -1
  52. data/lib/puppet/type/service.rb +6 -8
  53. data/lib/puppet/type/user.rb +1 -7
  54. data/lib/puppet/util/autoload.rb +1 -18
  55. data/lib/puppet/util/log/destinations.rb +1 -10
  56. data/lib/puppet/util/package/version/range.rb +4 -1
  57. data/lib/puppet/util/package/version/range/eq.rb +14 -0
  58. data/lib/puppet/version.rb +1 -1
  59. data/locales/puppet.pot +191 -111
  60. data/man/man5/puppet.conf.5 +21 -2
  61. data/man/man8/puppet-agent.8 +1 -1
  62. data/man/man8/puppet-apply.8 +1 -1
  63. data/man/man8/puppet-catalog.8 +1 -1
  64. data/man/man8/puppet-config.8 +1 -1
  65. data/man/man8/puppet-describe.8 +1 -1
  66. data/man/man8/puppet-device.8 +1 -1
  67. data/man/man8/puppet-doc.8 +1 -1
  68. data/man/man8/puppet-epp.8 +1 -1
  69. data/man/man8/puppet-facts.8 +1 -1
  70. data/man/man8/puppet-filebucket.8 +1 -1
  71. data/man/man8/puppet-generate.8 +1 -1
  72. data/man/man8/puppet-help.8 +6 -3
  73. data/man/man8/puppet-key.8 +1 -1
  74. data/man/man8/puppet-lookup.8 +1 -1
  75. data/man/man8/puppet-man.8 +1 -1
  76. data/man/man8/puppet-module.8 +4 -1
  77. data/man/man8/puppet-node.8 +1 -1
  78. data/man/man8/puppet-parser.8 +1 -1
  79. data/man/man8/puppet-plugin.8 +1 -1
  80. data/man/man8/puppet-report.8 +1 -1
  81. data/man/man8/puppet-resource.8 +1 -1
  82. data/man/man8/puppet-script.8 +1 -1
  83. data/man/man8/puppet-ssl.8 +1 -1
  84. data/man/man8/puppet-status.8 +1 -1
  85. data/man/man8/puppet.8 +2 -2
  86. data/spec/fixtures/unit/provider/package/dnfmodule/{dnf-module-list-enabled.txt → dnf-module-list.txt} +6 -0
  87. data/spec/fixtures/unit/provider/package/zypper/zypper-search-uninstalled.out +13 -0
  88. data/spec/integration/application/agent_spec.rb +66 -1
  89. data/spec/integration/application/plugin_spec.rb +23 -0
  90. data/spec/integration/http/client_spec.rb +6 -1
  91. data/spec/integration/network/http_pool_spec.rb +56 -0
  92. data/spec/integration/util/windows/adsi_spec.rb +5 -0
  93. data/spec/lib/puppet_spec/https.rb +6 -0
  94. data/spec/unit/agent_spec.rb +47 -1
  95. data/spec/unit/application/agent_spec.rb +4 -4
  96. data/spec/unit/context/trusted_information_spec.rb +17 -0
  97. data/spec/unit/daemon_spec.rb +5 -64
  98. data/spec/unit/face/module/search_spec.rb +17 -0
  99. data/spec/unit/file_system/uniquefile_spec.rb +11 -0
  100. data/spec/unit/http/client_spec.rb +10 -10
  101. data/spec/unit/http/external_client_spec.rb +9 -9
  102. data/spec/unit/indirector/catalog/compiler_spec.rb +1 -0
  103. data/spec/unit/indirector/file_metadata/http_spec.rb +167 -0
  104. data/spec/unit/indirector/file_metadata/rest_spec.rb +15 -14
  105. data/spec/unit/indirector/rest_spec.rb +13 -0
  106. data/spec/unit/network/http/connection_spec.rb +542 -190
  107. data/spec/unit/network/http/nocache_pool_spec.rb +22 -0
  108. data/spec/unit/network/http_pool_spec.rb +63 -57
  109. data/spec/unit/network/http_spec.rb +1 -1
  110. data/spec/unit/provider/package/aix_spec.rb +29 -0
  111. data/spec/unit/provider/package/dnfmodule_spec.rb +25 -5
  112. data/spec/unit/provider/package/pip_spec.rb +42 -16
  113. data/spec/unit/provider/package/portage_spec.rb +5 -0
  114. data/spec/unit/provider/package/yum_spec.rb +16 -8
  115. data/spec/unit/provider/package/zypper_spec.rb +84 -0
  116. data/spec/unit/provider/service/init_spec.rb +1 -0
  117. data/spec/unit/provider/service/openbsd_spec.rb +9 -0
  118. data/spec/unit/provider/service/openwrt_spec.rb +1 -0
  119. data/spec/unit/provider/service/redhat_spec.rb +9 -0
  120. data/spec/unit/provider/service/systemd_spec.rb +84 -13
  121. data/spec/unit/provider/user/useradd_spec.rb +8 -0
  122. data/spec/unit/puppet_pal_catalog_spec.rb +43 -0
  123. data/spec/unit/puppet_spec.rb +33 -0
  124. data/spec/unit/reports/http_spec.rb +1 -1
  125. data/spec/unit/ssl/state_machine_spec.rb +52 -8
  126. data/spec/unit/type/service_spec.rb +9 -8
  127. data/spec/unit/type/user_spec.rb +1 -1
  128. data/spec/unit/util/autoload_spec.rb +2 -1
  129. data/spec/unit/util/log/destinations_spec.rb +1 -29
  130. data/spec/unit/util/package/version/range_spec.rb +22 -1
  131. data/tasks/manpages.rake +5 -35
  132. metadata +10 -4
@@ -164,38 +164,38 @@ describe Puppet::HTTP::ExternalClient do
164
164
  it "submits credentials for GET requests" do
165
165
  stub_request(:get, uri).with(basic_auth: credentials)
166
166
 
167
- client.get(uri, options: {user: 'user', password: 'pass'})
167
+ client.get(uri, options: {basic_auth: {user: 'user', password: 'pass'}})
168
168
  end
169
169
 
170
170
  it "submits credentials for POST requests" do
171
171
  stub_request(:post, uri).with(basic_auth: credentials)
172
172
 
173
- client.post(uri, "", options: {content_type: 'text/plain', user: 'user', password: 'pass'})
173
+ client.post(uri, "", options: {content_type: 'text/plain', basic_auth: {user: 'user', password: 'pass'}})
174
174
  end
175
175
 
176
176
  it "returns response containing access denied" do
177
177
  stub_request(:get, uri).with(basic_auth: credentials).to_return(status: [403, "Ye Shall Not Pass"])
178
178
 
179
- response = client.get(uri, options: {user: 'user', password: 'pass'})
179
+ response = client.get(uri, options: { basic_auth: {user: 'user', password: 'pass'}})
180
180
  expect(response.code).to eq(403)
181
181
  expect(response.reason).to eq("Ye Shall Not Pass")
182
182
  expect(response).to_not be_success
183
183
  end
184
184
 
185
- it 'omits basic auth if user is nil' do
185
+ it 'includes basic auth if user is nil' do
186
186
  stub_request(:get, uri).with do |req|
187
- expect(req.headers).to_not include('Authorization')
187
+ expect(req.headers).to include('Authorization')
188
188
  end
189
189
 
190
- client.get(uri, options: {user: nil, password: 'pass'})
190
+ client.get(uri, options: {basic_auth: {user: nil, password: 'pass'}})
191
191
  end
192
192
 
193
- it 'omits basic auth if password is nil' do
193
+ it 'includes basic auth if password is nil' do
194
194
  stub_request(:get, uri).with do |req|
195
- expect(req.headers).to_not include('Authorization')
195
+ expect(req.headers).to include('Authorization')
196
196
  end
197
197
 
198
- client.get(uri, options: {user: 'user', password: nil})
198
+ client.get(uri, options: {basic_auth: {user: 'user', password: nil}})
199
199
  end
200
200
  end
201
201
  end
@@ -477,6 +477,7 @@ describe Puppet::Resource::Catalog::Compiler do
477
477
  end
478
478
 
479
479
  it "should add 'pe_serverversion' when PE" do
480
+ allow(File).to receive(:readable?).and_call_original
480
481
  allow(File).to receive(:readable?).with(pe_version_file).and_return(true)
481
482
  allow(File).to receive(:zero?).with(pe_version_file).and_return(false)
482
483
  allow(File).to receive(:read).and_call_original
@@ -0,0 +1,167 @@
1
+ require 'spec_helper'
2
+
3
+ require 'puppet/indirector/file_metadata'
4
+ require 'puppet/indirector/file_metadata/http'
5
+
6
+ describe Puppet::Indirector::FileMetadata::Http do
7
+ DEFAULT_HEADERS = {
8
+ "Cache-Control" => "private, max-age=0",
9
+ "Connection" => "close",
10
+ "Content-Encoding" => "gzip",
11
+ "Content-Type" => "text/html; charset=ISO-8859-1",
12
+ "Date" => "Fri, 01 May 2020 17:16:00 GMT",
13
+ "Expires" => "-1",
14
+ "Server" => "gws"
15
+ }.freeze
16
+
17
+ let(:certname) { 'ziggy' }
18
+ # The model is Puppet:FileServing::Metadata
19
+ let(:model) { described_class.model }
20
+ # The http terminus creates instances of HttpMetadata which subclass Metadata
21
+ let(:metadata) { Puppet::FileServing::HttpMetadata.new(key) }
22
+ let(:key) { "https://example.com/path/to/file" }
23
+ # Digest::MD5.base64digest("") => "1B2M2Y8AsgTpgAmY7PhCfg=="
24
+ let(:content_md5) { {"Content-MD5" => "1B2M2Y8AsgTpgAmY7PhCfg=="} }
25
+ let(:last_modified) { {"Last-Modified" => "Wed, 01 Jan 2020 08:00:00 GMT"} }
26
+
27
+ before :each do
28
+ described_class.indirection.terminus_class = :http
29
+ end
30
+
31
+ context "when finding" do
32
+ it "returns http file metadata" do
33
+ stub_request(:head, key)
34
+ .to_return(status: 200, headers: DEFAULT_HEADERS)
35
+
36
+ result = model.indirection.find(key)
37
+ expect(result.ftype).to eq('file')
38
+ expect(result.path).to eq('/dev/null')
39
+ expect(result.relative_path).to be_nil
40
+ expect(result.destination).to be_nil
41
+ expect(result.checksum).to match(%r{mtime})
42
+ expect(result.owner).to be_nil
43
+ expect(result.group).to be_nil
44
+ expect(result.mode).to be_nil
45
+ end
46
+
47
+ it "reports an md5 checksum if present in the response" do
48
+ stub_request(:head, key)
49
+ .to_return(status: 200, headers: DEFAULT_HEADERS.merge(content_md5))
50
+
51
+ result = model.indirection.find(key)
52
+ expect(result.checksum_type).to eq(:md5)
53
+ expect(result.checksum).to eq("{md5}d41d8cd98f00b204e9800998ecf8427e")
54
+ end
55
+
56
+ it "reports an mtime checksum if present in the response" do
57
+ stub_request(:head, key)
58
+ .to_return(status: 200, headers: DEFAULT_HEADERS.merge(last_modified))
59
+
60
+ result = model.indirection.find(key)
61
+ expect(result.checksum_type).to eq(:mtime)
62
+ expect(result.checksum).to eq("{mtime}2020-01-01 08:00:00 UTC")
63
+ end
64
+
65
+ it "prefers md5" do
66
+ stub_request(:head, key)
67
+ .to_return(status: 200, headers: DEFAULT_HEADERS.merge(content_md5).merge(last_modified))
68
+
69
+ result = model.indirection.find(key)
70
+ expect(result.checksum_type).to eq(:md5)
71
+ expect(result.checksum).to eq("{md5}d41d8cd98f00b204e9800998ecf8427e")
72
+ end
73
+
74
+ it "prefers mtime when explicitly requested" do
75
+ stub_request(:head, key)
76
+ .to_return(status: 200, headers: DEFAULT_HEADERS.merge(content_md5).merge(last_modified))
77
+
78
+ result = model.indirection.find(key, checksum_type: :mtime)
79
+ expect(result.checksum_type).to eq(:mtime)
80
+ expect(result.checksum).to eq("{mtime}2020-01-01 08:00:00 UTC")
81
+ end
82
+
83
+ it "leniently parses base64" do
84
+ # Content-MD5 header is missing '==' padding
85
+ stub_request(:head, key)
86
+ .to_return(status: 200, headers: DEFAULT_HEADERS.merge("Content-MD5" => "1B2M2Y8AsgTpgAmY7PhCfg"))
87
+
88
+ result = model.indirection.find(key)
89
+ expect(result.checksum_type).to eq(:md5)
90
+ expect(result.checksum).to eq("{md5}d41d8cd98f00b204e9800998ecf8427e")
91
+ end
92
+
93
+ it "URL encodes special characters" do
94
+ pending("HTTP terminus doesn't encode the URI before parsing")
95
+
96
+ stub_request(:head, %r{/path%20to%20file})
97
+
98
+ model.indirection.find('https://example.com/path to file')
99
+ end
100
+
101
+ it "sends query parameters" do
102
+ stub_request(:head, key).with(query: {'a' => 'b'})
103
+
104
+ model.indirection.find("#{key}?a=b")
105
+ end
106
+
107
+ it "returns nil if the content doesn't exist" do
108
+ stub_request(:head, key).to_return(status: 404)
109
+
110
+ expect(model.indirection.find(key)).to be_nil
111
+ end
112
+
113
+ it "returns nil if fail_on_404" do
114
+ stub_request(:head, key).to_return(status: 404)
115
+
116
+ expect(model.indirection.find(key, fail_on_404: true)).to be_nil
117
+ end
118
+
119
+ it "returns nil on HTTP 500" do
120
+ stub_request(:head, key).to_return(status: 500)
121
+
122
+ # this is kind of strange, but it does allow puppet to try
123
+ # multiple `source => ["URL1", "URL2"]` and use the first
124
+ # one based on sourceselect
125
+ expect(model.indirection.find(key)).to be_nil
126
+ end
127
+
128
+ it "accepts all content types" do
129
+ stub_request(:head, key).with(headers: {'Accept' => '*/*'})
130
+
131
+ model.indirection.find(key)
132
+ end
133
+
134
+ it "sets puppet user-agent" do
135
+ stub_request(:head, key).with(headers: {'User-Agent' => Puppet[:http_user_agent]})
136
+
137
+ model.indirection.find(key)
138
+ end
139
+
140
+ it "tries to persist the connection" do
141
+ # HTTP/1.1 defaults to persistent connections, so check for
142
+ # the header's absence
143
+ stub_request(:head, key).with do |request|
144
+ expect(request.headers).to_not include('Connection')
145
+ end
146
+
147
+ model.indirection.find(key)
148
+ end
149
+
150
+ it "follows redirects" do
151
+ new_url = "https://example.com/different/path"
152
+ redirect = { status: 200, headers: { 'Location' => new_url }, body: ""}
153
+ stub_request(:head, key).to_return(redirect)
154
+ stub_request(:head, new_url)
155
+
156
+ model.indirection.find(key)
157
+ end
158
+ end
159
+
160
+ context "when searching" do
161
+ it "raises an error" do
162
+ expect {
163
+ model.indirection.search(key)
164
+ }.to raise_error(Puppet::Error, 'cannot lookup multiple files')
165
+ end
166
+ end
167
+ end
@@ -6,6 +6,7 @@ require 'puppet/indirector/file_metadata/rest'
6
6
  describe Puppet::Indirector::FileMetadata::Rest do
7
7
  let(:certname) { 'ziggy' }
8
8
  let(:formatter) { Puppet::Network::FormatHandler.format(:json) }
9
+ let(:model) { described_class.model }
9
10
 
10
11
  before :each do
11
12
  described_class.indirection.terminus_class = :rest
@@ -18,13 +19,13 @@ describe Puppet::Indirector::FileMetadata::Rest do
18
19
  context "when finding" do
19
20
  let(:uri) { %r{/puppet/v3/file_metadata/:mount/path/to/file} }
20
21
  let(:key) { "puppet:///:mount/path/to/file" }
21
- let(:metadata) { Puppet::FileServing::Metadata.new('/path/to/file') }
22
+ let(:metadata) { model.new('/path/to/file') }
22
23
 
23
24
  it "returns file metadata" do
24
25
  stub_request(:get, uri)
25
26
  .to_return(status: 200, **metadata_response(metadata))
26
27
 
27
- result = described_class.indirection.find(key)
28
+ result = model.indirection.find(key)
28
29
  expect(result.path).to eq('/path/to/file')
29
30
  end
30
31
 
@@ -32,20 +33,20 @@ describe Puppet::Indirector::FileMetadata::Rest do
32
33
  stub_request(:get, %r{/puppet/v3/file_metadata/:mount/path%20to%20file})
33
34
  .to_return(status: 200, **metadata_response(metadata))
34
35
 
35
- described_class.indirection.find('puppet:///:mount/path to file')
36
+ model.indirection.find('puppet:///:mount/path to file')
36
37
  end
37
38
 
38
39
  it "returns nil if the content doesn't exist" do
39
40
  stub_request(:get, uri).to_return(status: 404)
40
41
 
41
- expect(described_class.indirection.find(key)).to be_nil
42
+ expect(model.indirection.find(key)).to be_nil
42
43
  end
43
44
 
44
45
  it "raises if fail_on_404 is true" do
45
46
  stub_request(:get, uri).to_return(status: 404, headers: { 'Content-Type' => 'application/json'}, body: "{}")
46
47
 
47
48
  expect {
48
- described_class.indirection.find(key, fail_on_404: true)
49
+ model.indirection.find(key, fail_on_404: true)
49
50
  }.to raise_error(Puppet::Error, %r{Find /puppet/v3/file_metadata/:mount/path/to/file resulted in 404 with the message: {}})
50
51
  end
51
52
 
@@ -53,7 +54,7 @@ describe Puppet::Indirector::FileMetadata::Rest do
53
54
  stub_request(:get, uri).to_return(status: 500, headers: { 'Content-Type' => 'application/json'}, body: "{}")
54
55
 
55
56
  expect {
56
- described_class.indirection.find(key)
57
+ model.indirection.find(key)
57
58
  }.to raise_error(Net::HTTPError, %r{Error 500 on SERVER: })
58
59
  end
59
60
 
@@ -61,20 +62,20 @@ describe Puppet::Indirector::FileMetadata::Rest do
61
62
  stub_request(:get, %r{https://example.com:8140/puppet/v3/file_metadata/:mount/path/to/file})
62
63
  .to_return(status: 200, **metadata_response(metadata))
63
64
 
64
- described_class.indirection.find("puppet://example.com:8140/:mount/path/to/file")
65
+ model.indirection.find("puppet://example.com:8140/:mount/path/to/file")
65
66
  end
66
67
  end
67
68
 
68
69
  context "when searching" do
69
70
  let(:uri) { %r{/puppet/v3/file_metadatas/:mount/path/to/dir} }
70
71
  let(:key) { "puppet:///:mount/path/to/dir" }
71
- let(:metadatas) { [Puppet::FileServing::Metadata.new('/path/to/dir')] }
72
+ let(:metadatas) { [model.new('/path/to/dir')] }
72
73
 
73
74
  it "returns an array of file metadata" do
74
75
  stub_request(:get, uri)
75
76
  .to_return(status: 200, **metadata_response(metadatas))
76
77
 
77
- result = described_class.indirection.search(key)
78
+ result = model.indirection.search(key)
78
79
  expect(result.first.path).to eq('/path/to/dir')
79
80
  end
80
81
 
@@ -82,26 +83,26 @@ describe Puppet::Indirector::FileMetadata::Rest do
82
83
  stub_request(:get, %r{/puppet/v3/file_metadatas/:mount/path%20to%20dir})
83
84
  .to_return(status: 200, **metadata_response(metadatas))
84
85
 
85
- described_class.indirection.search('puppet:///:mount/path to dir')
86
+ model.indirection.search('puppet:///:mount/path to dir')
86
87
  end
87
88
 
88
89
  it "returns an empty array if the metadata doesn't exist" do
89
90
  stub_request(:get, uri).to_return(status: 404)
90
91
 
91
- expect(described_class.indirection.search(key)).to eq([])
92
+ expect(model.indirection.search(key)).to eq([])
92
93
  end
93
94
 
94
95
  it "returns an empty array if the metadata doesn't exist and fail_on_404 is true" do
95
96
  stub_request(:get, uri).to_return(status: 404, headers: { 'Content-Type' => 'application/json'}, body: "{}")
96
97
 
97
- expect(described_class.indirection.search(key, fail_on_404: true)).to eq([])
98
+ expect(model.indirection.search(key, fail_on_404: true)).to eq([])
98
99
  end
99
100
 
100
101
  it "raises an error on HTTP 500" do
101
102
  stub_request(:get, uri).to_return(status: 500, headers: { 'Content-Type' => 'application/json'}, body: "{}")
102
103
 
103
104
  expect {
104
- described_class.indirection.search(key)
105
+ model.indirection.search(key)
105
106
  }.to raise_error(Net::HTTPError, %r{Error 500 on SERVER: })
106
107
  end
107
108
 
@@ -109,7 +110,7 @@ describe Puppet::Indirector::FileMetadata::Rest do
109
110
  stub_request(:get, %r{https://example.com:8140/puppet/v3/file_metadatas/:mount/path/to/dir})
110
111
  .to_return(status: 200, **metadata_response(metadatas))
111
112
 
112
- described_class.indirection.search("puppet://example.com:8140/:mount/path/to/dir")
113
+ model.indirection.search("puppet://example.com:8140/:mount/path/to/dir")
113
114
  end
114
115
  end
115
116
 
@@ -45,6 +45,19 @@ shared_examples_for "a REST terminus method" do |terminus_method|
45
45
 
46
46
  expect(Puppet[:preferred_serialization_format]).to eq("pson")
47
47
  end
48
+
49
+ it "logs a deprecation warning" do
50
+ terminus.send(terminus_method, request)
51
+
52
+ expect(@logs).to include(an_object_having_attributes(level: :warning, message: /Puppet::Indirector::Rest##{terminus_method} is deprecated. Use Puppet::HTTP::Client instead./))
53
+ end
54
+
55
+ it "omits the warning when deprecations are disabled" do
56
+ Puppet[:disable_warnings] = 'deprecations'
57
+ terminus.send(terminus_method, request)
58
+
59
+ expect(@logs).to eq([])
60
+ end
48
61
  end
49
62
 
50
63
  HTTP_ERROR_CODES.each do |code|
@@ -1,296 +1,648 @@
1
1
  require 'spec_helper'
2
2
  require 'puppet/network/http/connection'
3
+ require 'puppet/network/http/connection_adapter'
3
4
  require 'puppet/test_ca'
4
5
 
5
6
  describe Puppet::Network::HTTP::Connection do
6
- let (:host) { "me.example.com" }
7
- let (:port) { 8140 }
8
- let (:url) { "https://#{host}:#{port}/foo" }
7
+ let(:host) { "me.example.com" }
8
+ let(:port) { 8140 }
9
+ let(:path) { '/foo' }
10
+ let(:url) { "https://#{host}:#{port}#{path}" }
9
11
 
10
- subject { Puppet::Network::HTTP::Connection.new(host, port, :verify => Puppet::SSL::Validator.no_validator) }
12
+ shared_examples_for "an HTTP connection" do |klass, legacy_api|
13
+ subject { klass.new(host, port, :verify => Puppet::SSL::Validator.no_validator) }
11
14
 
12
- context "when providing HTTP connections" do
13
- context "when initializing http instances" do
14
- it "should return an http instance created with the passed host and port" do
15
- conn = Puppet::Network::HTTP::Connection.new(host, port, :verify => Puppet::SSL::Validator.no_validator)
15
+ context "when providing HTTP connections" do
16
+ context "when initializing http instances" do
17
+ it "should return an http instance created with the passed host and port" do
18
+ conn = klass.new(host, port, :verify => Puppet::SSL::Validator.no_validator)
16
19
 
17
- expect(conn.address).to eq(host)
18
- expect(conn.port).to eq(port)
20
+ expect(conn.address).to eq(host)
21
+ expect(conn.port).to eq(port)
22
+ end
23
+
24
+ it "should enable ssl on the http instance by default" do
25
+ conn = klass.new(host, port, :verify => Puppet::SSL::Validator.no_validator)
26
+
27
+ expect(conn).to be_use_ssl
28
+ end
29
+
30
+ it "can disable ssl using an option and ignore the verify" do
31
+ conn = klass.new(host, port, :use_ssl => false)
32
+
33
+ expect(conn).to_not be_use_ssl
34
+ end
35
+
36
+ it "can enable ssl using an option" do
37
+ conn = klass.new(host, port, :use_ssl => true, :verify => Puppet::SSL::Validator.no_validator)
38
+
39
+ expect(conn).to be_use_ssl
40
+ end
41
+
42
+ it "ignores the ':verify' option when ssl is disabled" do
43
+ conn = klass.new(host, port, :use_ssl => false, :verify => Puppet::SSL::Validator.no_validator)
44
+
45
+ expect(conn.verifier).to be_nil
46
+ end
47
+
48
+ it "wraps the validator in an adapter" do
49
+ conn = klass.new(host, port, :verify => Puppet::SSL::Validator.no_validator)
50
+
51
+ expect(conn.verifier).to be_a_kind_of(Puppet::SSL::VerifierAdapter)
52
+ end
53
+
54
+ it "should raise Puppet::Error when invalid options are specified" do
55
+ expect { klass.new(host, port, :invalid_option => nil) }.to raise_error(Puppet::Error, 'Unrecognized option(s): :invalid_option')
56
+ end
57
+
58
+ it "accepts a verifier" do
59
+ verifier = Puppet::SSL::Verifier.new(host, double('ssl_context'))
60
+ conn = klass.new(host, port, :use_ssl => true, :verifier => verifier)
61
+
62
+ expect(conn.verifier).to eq(verifier)
63
+ end
64
+
65
+ it "raises if the wrong verifier class is specified" do
66
+ expect {
67
+ klass.new(host, port, :verifier => Puppet::SSL::Validator.default_validator)
68
+ }.to raise_error(ArgumentError,
69
+ "Expected an instance of Puppet::SSL::Verifier but was passed a Puppet::SSL::Validator::DefaultValidator")
70
+ end
19
71
  end
72
+ end
20
73
 
21
- it "should enable ssl on the http instance by default" do
22
- conn = Puppet::Network::HTTP::Connection.new(host, port, :verify => Puppet::SSL::Validator.no_validator)
74
+ context "for streaming GET requests" do
75
+ it 'yields the response' do
76
+ stub_request(:get, url)
23
77
 
24
- expect(conn).to be_use_ssl
78
+ expect { |b|
79
+ subject.request_get('/foo', {}, &b)
80
+ }.to yield_with_args(Net::HTTPResponse)
25
81
  end
26
82
 
27
- it "can disable ssl using an option and ignore the verify" do
28
- conn = Puppet::Network::HTTP::Connection.new(host, port, :use_ssl => false)
83
+ it "stringifies keys and encodes values in the query" do
84
+ stub_request(:get, url).with(query: "foo=bar%3Dbaz")
29
85
 
30
- expect(conn).to_not be_use_ssl
86
+ subject.request_get("#{path}?foo=bar=baz") { |_| }
31
87
  end
32
88
 
33
- it "can enable ssl using an option" do
34
- conn = Puppet::Network::HTTP::Connection.new(host, port, :use_ssl => true, :verify => Puppet::SSL::Validator.no_validator)
89
+ it "merges custom headers with default ones" do
90
+ stub_request(:get, url).with(headers: { 'X-Foo' => 'Bar', 'User-Agent' => /./ })
35
91
 
36
- expect(conn).to be_use_ssl
92
+ subject.request_get(path, {'X-Foo' => 'Bar'}) { |_| }
37
93
  end
38
94
 
39
- it "ignores the ':verify' option when ssl is disabled" do
40
- conn = Puppet::Network::HTTP::Connection.new(host, port, :use_ssl => false, :verify => Puppet::SSL::Validator.no_validator)
95
+ it "returns the response" do
96
+ stub_request(:get, url)
41
97
 
42
- expect(conn.verifier).to be_nil
98
+ response = subject.request_get(path) { |_| }
99
+ expect(response).to be_an_instance_of(Net::HTTPOK)
100
+ expect(response.code).to eq("200")
43
101
  end
44
102
 
45
- it "wraps the validator in an adapter" do
46
- conn = Puppet::Network::HTTP::Connection.new(host, port, :verify => Puppet::SSL::Validator.no_validator)
103
+ it "accepts a URL string as the path" do
104
+ stub_request(:get, url)
47
105
 
48
- expect(conn.verifier).to be_a_kind_of(Puppet::SSL::VerifierAdapter)
106
+ response = subject.request_get(url) { |_| }
107
+ expect(response).to be_an_instance_of(Net::HTTPOK)
49
108
  end
109
+ end
110
+
111
+ context "for streaming head requests" do
112
+ it 'yields the response when request_head is called' do
113
+ stub_request(:head, url)
50
114
 
51
- it "should raise Puppet::Error when invalid options are specified" do
52
- expect { Puppet::Network::HTTP::Connection.new(host, port, :invalid_option => nil) }.to raise_error(Puppet::Error, 'Unrecognized option(s): :invalid_option')
115
+ expect { |b|
116
+ subject.request_head('/foo', {}, &b)
117
+ }.to yield_with_args(Net::HTTPResponse)
53
118
  end
54
119
 
55
- it "accepts a verifier" do
56
- verifier = Puppet::SSL::Verifier.new('fqdn', double('ssl_context'))
57
- conn = Puppet::Network::HTTP::Connection.new(host, port, :use_ssl => true, :verifier => verifier)
120
+ it "stringifies keys and encodes values in the query" do
121
+ stub_request(:head, url).with(query: "foo=bar%3Dbaz")
58
122
 
59
- expect(conn.verifier).to eq(verifier)
123
+ subject.request_head("#{path}?foo=bar=baz") { |_| }
60
124
  end
61
125
 
62
- it "raises if the wrong verifier class is specified" do
63
- expect {
64
- Puppet::Network::HTTP::Connection.new(host, port, :verifier => Puppet::SSL::Validator.default_validator)
65
- }.to raise_error(ArgumentError,
66
- "Expected an instance of Puppet::SSL::Verifier but was passed a Puppet::SSL::Validator::DefaultValidator")
126
+ it "merges custom headers with default ones" do
127
+ stub_request(:head, url).with(headers: { 'X-Foo' => 'Bar', 'User-Agent' => /./ })
128
+
129
+ subject.request_head(path, {'X-Foo' => 'Bar'}) { |_| }
67
130
  end
68
- end
69
- end
70
131
 
71
- context "when handling requests" do
72
- it 'yields the response when request_get is called' do
73
- stub_request(:get, url)
132
+ it "returns the response" do
133
+ stub_request(:head, url)
74
134
 
75
- expect { |b|
76
- subject.request_get('/foo', {}, &b)
77
- }.to yield_with_args(Net::HTTPResponse)
78
- end
135
+ response = subject.request_head(path) { |_| }
136
+ expect(response).to be_an_instance_of(Net::HTTPOK)
137
+ expect(response.code).to eq("200")
138
+ end
79
139
 
80
- it 'yields the response when request_head is called' do
81
- stub_request(:head, url)
140
+ it "accepts a URL string as the path" do
141
+ stub_request(:head, url)
82
142
 
83
- expect { |b|
84
- subject.request_head('/foo', {}, &b)
85
- }.to yield_with_args(Net::HTTPResponse)
143
+ response = subject.request_head(url) { |_| }
144
+ expect(response).to be_an_instance_of(Net::HTTPOK)
145
+ end
86
146
  end
87
147
 
88
- it 'yields the response when request_post is called' do
89
- stub_request(:post, url)
148
+ context "for streaming post requests" do
149
+ it 'yields the response when request_post is called' do
150
+ stub_request(:post, url)
90
151
 
91
- expect { |b|
92
- subject.request_post('/foo', "param: value", &b)
93
- }.to yield_with_args(Net::HTTPResponse)
94
- end
95
- end
152
+ expect { |b|
153
+ subject.request_post('/foo', "param: value", &b)
154
+ }.to yield_with_args(Net::HTTPResponse)
155
+ end
96
156
 
97
- context "when response is a redirect" do
98
- def create_connection(options = {})
99
- options[:use_ssl] = false
100
- options[:verify] = Puppet::SSL::Validator.no_validator
101
- Puppet::Network::HTTP::Connection.new(host, port, options)
102
- end
157
+ it "stringifies keys and encodes values in the query" do
158
+ stub_request(:post, url).with(query: "foo=bar%3Dbaz")
103
159
 
104
- def redirect_to(url)
105
- { status: 302, headers: { 'Location' => url } }
106
- end
160
+ subject.request_post("#{path}?foo=bar=baz", "") { |_| }
161
+ end
107
162
 
108
- it "should follow the redirect to the final resource location" do
109
- stub_request(:get, "http://me.example.com:8140/foo").to_return(redirect_to("http://me.example.com:8140/bar"))
110
- stub_request(:get, "http://me.example.com:8140/bar").to_return(status: 200)
163
+ it "merges custom headers with default ones" do
164
+ stub_request(:post, url).with(headers: { 'X-Foo' => 'Bar', 'User-Agent' => /./ })
111
165
 
112
- create_connection.get('/foo')
113
- end
166
+ subject.request_post(path, "", {'X-Foo' => 'Bar'}) { |_| }
167
+ end
114
168
 
115
- def expects_limit_exceeded(conn)
116
- expect {
117
- conn.get('/')
118
- }.to raise_error(Puppet::Network::HTTP::RedirectionLimitExceededException)
119
- end
169
+ it "returns the response" do
170
+ stub_request(:post, url)
120
171
 
121
- it "should not follow any redirects when the limit is 0" do
122
- stub_request(:get, "http://me.example.com:8140/").to_return(redirect_to("http://me.example.com:8140/foo"))
172
+ response = subject.request_post(path, "") { |_| }
173
+ expect(response).to be_an_instance_of(Net::HTTPOK)
174
+ expect(response.code).to eq("200")
175
+ end
176
+
177
+ it "accepts a URL string as the path" do
178
+ stub_request(:post, url)
123
179
 
124
- conn = create_connection(:redirect_limit => 0)
125
- expects_limit_exceeded(conn)
180
+ response = subject.request_post(url, "") { |_| }
181
+ expect(response).to be_an_instance_of(Net::HTTPOK)
182
+ end
126
183
  end
127
184
 
128
- it "should follow the redirect once" do
129
- stub_request(:get, "http://me.example.com:8140/").to_return(redirect_to("http://me.example.com:8140/foo"))
130
- stub_request(:get, "http://me.example.com:8140/foo").to_return(redirect_to("http://me.example.com:8140/bar"))
185
+ context "for GET requests" do
186
+ it "includes default HTTP headers" do
187
+ stub_request(:get, url).with(headers: {'User-Agent' => /./})
131
188
 
132
- conn = create_connection(:redirect_limit => 1)
133
- expects_limit_exceeded(conn)
134
- end
189
+ subject.get(path)
190
+ end
135
191
 
136
- it "should raise an exception when the redirect limit is exceeded" do
137
- stub_request(:get, "http://me.example.com:8140/").to_return(redirect_to("http://me.example.com:8140/foo"))
138
- stub_request(:get, "http://me.example.com:8140/foo").to_return(redirect_to("http://me.example.com:8140/bar"))
139
- stub_request(:get, "http://me.example.com:8140/bar").to_return(redirect_to("http://me.example.com:8140/baz"))
140
- stub_request(:get, "http://me.example.com:8140/baz").to_return(redirect_to("http://me.example.com:8140/qux"))
192
+ it "stringifies keys and encodes values in the query" do
193
+ stub_request(:get, url).with(query: "foo=bar%3Dbaz")
141
194
 
142
- conn = create_connection(:redirect_limit => 3)
143
- expects_limit_exceeded(conn)
144
- end
145
- end
195
+ subject.get("#{path}?foo=bar=baz")
196
+ end
146
197
 
147
- context "when response indicates an overloaded server" do
148
- def retry_after(datetime)
149
- stub_request(:get, url)
150
- .to_return(status: [503, 'Service Unavailable'], headers: {'Retry-After' => datetime}).then
151
- .to_return(status: 200)
152
- end
198
+ it "merges custom headers with default ones" do
199
+ stub_request(:get, url).with(headers: { 'X-Foo' => 'Bar', 'User-Agent' => /./ })
153
200
 
154
- it "should return a 503 response if Retry-After is not set" do
155
- stub_request(:get, url).to_return(status: [503, 'Service Unavailable'])
201
+ subject.get(path, {'X-Foo' => 'Bar'})
202
+ end
156
203
 
157
- result = subject.get('/foo')
158
- expect(result.code).to eq("503")
159
- end
204
+ it "returns the response" do
205
+ stub_request(:get, url)
206
+
207
+ response = subject.get(path)
208
+ expect(response).to be_an_instance_of(Net::HTTPOK)
209
+ expect(response.code).to eq("200")
210
+ end
211
+
212
+ it "returns the entire response body" do
213
+ stub_request(:get, url).to_return(body: "abc")
214
+
215
+ response = subject.get(path)
216
+ expect(response.body).to eq("abc")
217
+ end
160
218
 
161
- it "should return a 503 response if Retry-After is not convertible to an Integer or RFC 2822 Date" do
162
- retry_after('foo')
219
+ it "accepts a URL string as the path" do
220
+ stub_request(:get, url)
163
221
 
164
- result = subject.get('/foo')
165
- expect(result.code).to eq("503")
222
+ response = subject.get(url)
223
+ expect(response).to be_an_instance_of(Net::HTTPOK)
224
+ end
166
225
  end
167
226
 
168
- it "should close the connection before sleeping" do
169
- retry_after('42')
227
+ context "for HEAD requests" do
228
+ it "includes default HTTP headers" do
229
+ stub_request(:head, url).with(headers: {'User-Agent' => /./})
170
230
 
171
- http1 = Net::HTTP.new(host, port)
172
- http1.use_ssl = true
173
- allow(http1).to receive(:started?).and_return(true)
231
+ subject.head(path)
232
+ end
174
233
 
175
- http2 = Net::HTTP.new(host, port)
176
- http2.use_ssl = true
177
- allow(http1).to receive(:started?).and_return(true)
234
+ it "stringifies keys and encodes values in the query" do
235
+ stub_request(:head, url).with(query: "foo=bar%3Dbaz")
178
236
 
179
- # The "with_connection" method is required to yield started connections
180
- pool = Puppet.lookup(:http_pool)
181
- allow(pool).to receive(:with_connection).and_yield(http1).and_yield(http2)
237
+ subject.head("#{path}?foo=bar=baz")
238
+ end
182
239
 
183
- expect(http1).to receive(:finish).ordered
184
- expect(::Kernel).to receive(:sleep).with(42).ordered
240
+ it "merges custom headers with default ones" do
241
+ stub_request(:head, url).with(headers: { 'X-Foo' => 'Bar', 'User-Agent' => /./ })
185
242
 
186
- subject.get('/foo')
187
- end
243
+ subject.head(path, {'X-Foo' => 'Bar'})
244
+ end
245
+
246
+ it "returns the response" do
247
+ stub_request(:head, url)
188
248
 
189
- it "should sleep and retry if Retry-After is an Integer" do
190
- retry_after('42')
249
+ response = subject.head(path)
250
+ expect(response).to be_an_instance_of(Net::HTTPOK)
251
+ expect(response.code).to eq("200")
252
+ end
191
253
 
192
- expect(::Kernel).to receive(:sleep).with(42)
254
+ it "accepts a URL string as the path" do
255
+ stub_request(:head, url)
193
256
 
194
- result = subject.get('/foo')
195
- expect(result.code).to eq("200")
257
+ response = subject.head(url)
258
+ expect(response).to be_an_instance_of(Net::HTTPOK)
259
+ end
196
260
  end
197
261
 
198
- it "should sleep and retry if Retry-After is an RFC 2822 Date" do
199
- retry_after('Wed, 13 Apr 2005 15:18:05 GMT')
262
+ context "for PUT requests" do
263
+ it "includes default HTTP headers" do
264
+ stub_request(:put, url).with(headers: {'User-Agent' => /./})
200
265
 
201
- now = DateTime.new(2005, 4, 13, 8, 17, 5, '-07:00')
202
- allow(DateTime).to receive(:now).and_return(now)
266
+ subject.put(path, "", {'Content-Type' => 'text/plain'})
267
+ end
203
268
 
204
- expect(::Kernel).to receive(:sleep).with(60)
269
+ it "stringifies keys and encodes values in the query" do
270
+ stub_request(:put, url).with(query: "foo=bar%3Dbaz")
205
271
 
206
- result = subject.get('/foo')
207
- expect(result.code).to eq("200")
208
- end
272
+ subject.put("#{path}?foo=bar=baz", "")
273
+ end
209
274
 
210
- it "should sleep for no more than the Puppet runinterval" do
211
- retry_after('60')
275
+ it "includes custom headers" do
276
+ stub_request(:put, url).with(headers: { 'X-Foo' => 'Bar' })
212
277
 
213
- Puppet[:runinterval] = 30
278
+ subject.put(path, "", {'X-Foo' => 'Bar', 'Content-Type' => 'text/plain'})
279
+ end
214
280
 
215
- expect(::Kernel).to receive(:sleep).with(30)
281
+ it "returns the response" do
282
+ stub_request(:put, url)
216
283
 
217
- subject.get('/foo')
218
- end
284
+ response = subject.put(path, "", {'Content-Type' => 'text/plain'})
285
+ expect(response).to be_an_instance_of(Net::HTTPOK)
286
+ expect(response.code).to eq("200")
287
+ end
288
+
289
+ it "sets content-type for the body" do
290
+ stub_request(:put, url).with(headers: {"Content-Type" => "text/plain"})
219
291
 
220
- it "should sleep for 0 seconds if the RFC 2822 date has past" do
221
- retry_after('Wed, 13 Apr 2005 15:18:05 GMT')
292
+ subject.put(path, "hello", {'Content-Type' => 'text/plain'})
293
+ end
222
294
 
223
- expect(::Kernel).to receive(:sleep).with(0)
295
+ it 'sends an empty body' do
296
+ stub_request(:put, url).with(body: '')
224
297
 
225
- subject.get('/foo')
226
- end
227
- end
298
+ subject.put(path, nil)
299
+ end
300
+
301
+ it 'defaults content-type to application/x-www-form-urlencoded' do
302
+ skip("Net::HTTP sends a default content-type header, but it's not visible to webmock") if legacy_api
228
303
 
229
- context "basic auth" do
230
- let(:auth) { { :user => 'user', :password => 'password' } }
231
- let(:creds) { [ 'user', 'password'] }
304
+ stub_request(:put, url).with(headers: {'Content-Type' => 'application/x-www-form-urlencoded'})
232
305
 
233
- it "is allowed in get requests" do
234
- stub_request(:get, url).with(basic_auth: creds)
306
+ subject.put(path, '')
307
+ end
308
+
309
+ it "accepts a URL string as the path" do
310
+ stub_request(:put, url)
235
311
 
236
- subject.get('/foo', nil, :basic_auth => auth)
312
+ response = subject.put(url, '')
313
+ expect(response).to be_an_instance_of(Net::HTTPOK)
314
+ end
237
315
  end
238
316
 
239
- it "is allowed in post requests" do
240
- stub_request(:post, url).with(basic_auth: creds)
317
+ context "for POST requests" do
318
+ it "includes default HTTP headers" do
319
+ stub_request(:post, url).with(headers: {'User-Agent' => /./})
320
+
321
+ subject.post(path, "", {'Content-Type' => 'text/plain'})
322
+ end
323
+
324
+ it "stringifies keys and encodes values in the query" do
325
+ stub_request(:post, url).with(query: "foo=bar%3Dbaz")
326
+
327
+ subject.post("#{path}?foo=bar=baz", "", {'Content-Type' => 'text/plain'})
328
+ end
329
+
330
+ it "includes custom headers" do
331
+ stub_request(:post, url).with(headers: { 'X-Foo' => 'Bar' })
332
+
333
+ subject.post(path, "", {'X-Foo' => 'Bar', 'Content-Type' => 'text/plain'})
334
+ end
335
+
336
+ it "returns the response" do
337
+ stub_request(:post, url)
338
+
339
+ response = subject.post(path, "", {'Content-Type' => 'text/plain'})
340
+ expect(response).to be_an_instance_of(Net::HTTPOK)
341
+ expect(response.code).to eq("200")
342
+ end
343
+
344
+ it "sets content-type for the body" do
345
+ stub_request(:post, url).with(headers: {"Content-Type" => "text/plain"})
346
+
347
+ subject.post(path, "hello", {'Content-Type' => 'text/plain'})
348
+ end
349
+
350
+ it 'sends an empty body' do
351
+ stub_request(:post, url).with(body: '')
352
+
353
+ subject.post(path, nil)
354
+ end
355
+
356
+ it 'defaults content-type to application/x-www-form-urlencoded' do
357
+ skip("Net::HTTP sends a default content-type header, but it's not visible to webmock") if legacy_api
358
+
359
+ stub_request(:post, url).with(headers: {'Content-Type' => 'application/x-www-form-urlencoded'})
360
+
361
+ subject.post(path, "")
362
+ end
241
363
 
242
- subject.post('/foo', 'data', nil, :basic_auth => auth)
364
+ it "accepts a URL string as the path" do
365
+ stub_request(:post, url)
366
+
367
+ response = subject.post(url, '')
368
+ expect(response).to be_an_instance_of(Net::HTTPOK)
369
+ end
243
370
  end
244
371
 
245
- it "is allowed in head requests" do
246
- stub_request(:head, url).with(basic_auth: creds)
372
+ context "for DELETE requests" do
373
+ it "includes default HTTP headers" do
374
+ stub_request(:delete, url).with(headers: {'User-Agent' => /./})
375
+
376
+ subject.delete(path)
377
+ end
378
+
379
+ it "merges custom headers with default ones" do
380
+ stub_request(:delete, url).with(headers: { 'X-Foo' => 'Bar', 'User-Agent' => /./ })
381
+
382
+ subject.delete(path, {'X-Foo' => 'Bar'})
383
+ end
384
+
385
+ it "stringifies keys and encodes values in the query" do
386
+ stub_request(:delete, url).with(query: "foo=bar%3Dbaz")
387
+
388
+ subject.delete("#{path}?foo=bar=baz")
389
+ end
390
+
391
+ it "returns the response" do
392
+ stub_request(:delete, url)
393
+
394
+ response = subject.delete(path)
395
+ expect(response).to be_an_instance_of(Net::HTTPOK)
396
+ expect(response.code).to eq("200")
397
+ end
398
+
399
+ it "returns the entire response body" do
400
+ stub_request(:delete, url).to_return(body: "abc")
401
+
402
+ expect(subject.delete(path).body).to eq("abc")
403
+ end
404
+
405
+ it "accepts a URL string as the path" do
406
+ stub_request(:delete, url)
247
407
 
248
- subject.head('/foo', nil, :basic_auth => auth)
408
+ response = subject.delete(url)
409
+ expect(response).to be_an_instance_of(Net::HTTPOK)
410
+ end
249
411
  end
250
412
 
251
- it "is allowed in delete requests" do
252
- stub_request(:delete, url).with(basic_auth: creds)
413
+ context "when response is a redirect" do
414
+ subject { klass }
415
+
416
+ def create_connection(options = {})
417
+ options[:use_ssl] = false
418
+ options[:verify] = Puppet::SSL::Validator.no_validator
419
+ subject.new(host, port, options)
420
+ end
421
+
422
+ def redirect_to(url)
423
+ { status: 302, headers: { 'Location' => url } }
424
+ end
425
+
426
+ it "should follow the redirect to the final resource location" do
427
+ stub_request(:get, "http://me.example.com:8140/foo").to_return(redirect_to("http://me.example.com:8140/bar"))
428
+ stub_request(:get, "http://me.example.com:8140/bar").to_return(status: 200)
429
+
430
+ create_connection.get('/foo')
431
+ end
432
+
433
+ def expects_limit_exceeded(conn)
434
+ expect {
435
+ conn.get('/')
436
+ }.to raise_error(Puppet::Network::HTTP::RedirectionLimitExceededException)
437
+ end
438
+
439
+ it "should not follow any redirects when the limit is 0" do
440
+ stub_request(:get, "http://me.example.com:8140/").to_return(redirect_to("http://me.example.com:8140/foo"))
441
+
442
+ conn = create_connection(:redirect_limit => 0)
443
+ expects_limit_exceeded(conn)
444
+ end
445
+
446
+ it "should follow the redirect once" do
447
+ stub_request(:get, "http://me.example.com:8140/").to_return(redirect_to("http://me.example.com:8140/foo"))
448
+ stub_request(:get, "http://me.example.com:8140/foo").to_return(redirect_to("http://me.example.com:8140/bar"))
449
+
450
+ conn = create_connection(:redirect_limit => 1)
451
+ expects_limit_exceeded(conn)
452
+ end
453
+
454
+ it "should raise an exception when the redirect limit is exceeded" do
455
+ stub_request(:get, "http://me.example.com:8140/").to_return(redirect_to("http://me.example.com:8140/foo"))
456
+ stub_request(:get, "http://me.example.com:8140/foo").to_return(redirect_to("http://me.example.com:8140/bar"))
457
+ stub_request(:get, "http://me.example.com:8140/bar").to_return(redirect_to("http://me.example.com:8140/baz"))
458
+ stub_request(:get, "http://me.example.com:8140/baz").to_return(redirect_to("http://me.example.com:8140/qux"))
459
+
460
+ conn = create_connection(:redirect_limit => 3)
461
+ expects_limit_exceeded(conn)
462
+ end
253
463
 
254
- subject.delete('/foo', nil, :basic_auth => auth)
464
+ it 'raises an exception when the location header is missing' do
465
+ stub_request(:get, "http://me.example.com:8140/").to_return(status: 302)
466
+
467
+ if legacy_api
468
+ expect {
469
+ create_connection.get('/')
470
+ }.to raise_error(URI::InvalidURIError, /bad URI/)
471
+ else
472
+ expect {
473
+ create_connection.get('/')
474
+ }.to raise_error(Puppet::HTTP::ProtocolError, /Location response header is missing/)
475
+ end
476
+ end
255
477
  end
256
478
 
257
- it "is allowed in put requests" do
258
- stub_request(:put, url).with(basic_auth: creds)
479
+ context "when response indicates an overloaded server" do
480
+ def retry_after(datetime)
481
+ stub_request(:get, url)
482
+ .to_return(status: [503, 'Service Unavailable'], headers: {'Retry-After' => datetime}).then
483
+ .to_return(status: 200)
484
+ end
485
+
486
+ it "should return a 503 response if Retry-After is not set" do
487
+ stub_request(:get, url).to_return(status: [503, 'Service Unavailable'])
488
+
489
+ result = subject.get('/foo')
490
+ expect(result.code).to eq("503")
491
+ end
492
+
493
+ it "should return a 503 response if Retry-After is not convertible to an Integer or RFC 2822 Date" do
494
+ retry_after('foo')
495
+
496
+ if legacy_api
497
+ result = subject.get('/foo')
498
+ expect(result.code).to eq("503")
499
+ else
500
+ expect {
501
+ subject.get('/foo')
502
+ }.to raise_error(Puppet::HTTP::ProtocolError, /Failed to parse Retry-After header 'foo'/)
503
+ end
504
+ end
505
+
506
+ it "should close the connection before sleeping" do
507
+ retry_after('42')
508
+
509
+ http1 = Net::HTTP.new(host, port)
510
+ http1.use_ssl = true
511
+ allow(http1).to receive(:started?).and_return(true)
512
+
513
+ http2 = Net::HTTP.new(host, port)
514
+ http2.use_ssl = true
515
+ allow(http1).to receive(:started?).and_return(true)
516
+
517
+ # The "with_connection" method is required to yield started connections
518
+ pool = if legacy_api
519
+ Puppet.lookup(:http_pool)
520
+ else
521
+ Puppet.runtime[:http].pool
522
+ end
259
523
 
260
- subject.put('/foo', 'data', nil, :basic_auth => auth)
524
+ allow(pool).to receive(:with_connection).and_yield(http1).and_yield(http2)
525
+
526
+ expect(http1).to receive(:finish).ordered
527
+ expect(::Kernel).to receive(:sleep).with(42).ordered
528
+
529
+ subject.get('/foo')
530
+ end
531
+
532
+ it "should sleep and retry if Retry-After is an Integer" do
533
+ retry_after('42')
534
+
535
+ expect(::Kernel).to receive(:sleep).with(42)
536
+
537
+ result = subject.get('/foo')
538
+ expect(result.code).to eq("200")
539
+ end
540
+
541
+ it "should sleep and retry if Retry-After is an RFC 2822 Date" do
542
+ retry_after('Wed, 13 Apr 2005 15:18:05 GMT')
543
+
544
+ now = DateTime.new(2005, 4, 13, 8, 17, 5, '-07:00')
545
+ allow(DateTime).to receive(:now).and_return(now)
546
+
547
+ expect(::Kernel).to receive(:sleep).with(60)
548
+
549
+ result = subject.get('/foo')
550
+ expect(result.code).to eq("200")
551
+ end
552
+
553
+ it "should sleep for no more than the Puppet runinterval" do
554
+ retry_after('60')
555
+
556
+ Puppet[:runinterval] = 30
557
+
558
+ expect(::Kernel).to receive(:sleep).with(30)
559
+
560
+ subject.get('/foo')
561
+ end
562
+
563
+ it "should sleep for 0 seconds if the RFC 2822 date has past" do
564
+ retry_after('Wed, 13 Apr 2005 15:18:05 GMT')
565
+
566
+ expect(::Kernel).to receive(:sleep).with(0)
567
+
568
+ subject.get('/foo')
569
+ end
261
570
  end
262
- end
263
571
 
264
- it "sets HTTP User-Agent header" do
265
- puppet_ua = "Puppet/#{Puppet.version} Ruby/#{RUBY_VERSION}-p#{RUBY_PATCHLEVEL} (#{RUBY_PLATFORM})"
266
- stub_request(:get, url).with(headers: { 'User-Agent' => puppet_ua })
572
+ context "basic auth" do
573
+ let(:auth) { { :user => 'user', :password => 'password' } }
574
+ let(:creds) { [ 'user', 'password'] }
267
575
 
268
- subject.get('/foo')
269
- end
576
+ it "is allowed in get requests" do
577
+ stub_request(:get, url).with(basic_auth: creds)
270
578
 
271
- describe 'connection request errors' do
272
- it "logs and raises generic http errors" do
273
- generic_error = Net::HTTPError.new('generic error', double("response"))
274
- stub_request(:get, url).to_raise(generic_error)
579
+ subject.get('/foo', nil, :basic_auth => auth)
580
+ end
275
581
 
276
- expect(Puppet).to receive(:log_exception).with(anything, /^.*failed: generic error$/)
277
- expect { subject.get('/foo') }.to raise_error(generic_error)
582
+ it "is allowed in post requests" do
583
+ stub_request(:post, url).with(basic_auth: creds)
584
+
585
+ subject.post('/foo', 'data', nil, :basic_auth => auth)
586
+ end
587
+
588
+ it "is allowed in head requests" do
589
+ stub_request(:head, url).with(basic_auth: creds)
590
+
591
+ subject.head('/foo', nil, :basic_auth => auth)
592
+ end
593
+
594
+ it "is allowed in delete requests" do
595
+ stub_request(:delete, url).with(basic_auth: creds)
596
+
597
+ subject.delete('/foo', nil, :basic_auth => auth)
598
+ end
599
+
600
+ it "is allowed in put requests" do
601
+ stub_request(:put, url).with(basic_auth: creds)
602
+
603
+ subject.put('/foo', 'data', nil, :basic_auth => auth)
604
+ end
278
605
  end
279
606
 
280
- it "logs and raises timeout errors" do
281
- timeout_error = Timeout::Error.new
282
- stub_request(:get, url).to_raise(timeout_error)
607
+ it "sets HTTP User-Agent header" do
608
+ puppet_ua = "Puppet/#{Puppet.version} Ruby/#{RUBY_VERSION}-p#{RUBY_PATCHLEVEL} (#{RUBY_PLATFORM})"
609
+ stub_request(:get, url).with(headers: { 'User-Agent' => puppet_ua })
283
610
 
284
- expect(Puppet).to receive(:log_exception).with(anything, /^.*timed out after .* seconds$/)
285
- expect { subject.get('/foo') }.to raise_error(timeout_error)
611
+ subject.get('/foo')
286
612
  end
287
613
 
288
- it "logs and raises eof errors" do
289
- eof_error = EOFError
290
- stub_request(:get, url).to_raise(eof_error)
614
+ describe 'connection request errors' do
615
+ it "logs and raises generic http errors" do
616
+ generic_error = Net::HTTPError.new('generic error', double("response"))
617
+ stub_request(:get, url).to_raise(generic_error)
291
618
 
292
- expect(Puppet).to receive(:log_exception).with(anything, /^.*interrupted after .* seconds$/)
293
- expect { subject.get('/foo') }.to raise_error(eof_error)
619
+ expect(Puppet).to receive(:log_exception).with(anything, /^.*failed.*: generic error$/)
620
+ expect { subject.get('/foo') }.to raise_error(generic_error)
621
+ end
622
+
623
+ it "logs and raises timeout errors" do
624
+ timeout_error = Net::OpenTimeout.new
625
+ stub_request(:get, url).to_raise(timeout_error)
626
+
627
+ expect(Puppet).to receive(:log_exception).with(anything, /^.*timed out .*after .* seconds/)
628
+ expect { subject.get('/foo') }.to raise_error(timeout_error)
629
+ end
630
+
631
+ it "logs and raises eof errors" do
632
+ eof_error = EOFError
633
+ stub_request(:get, url).to_raise(eof_error)
634
+
635
+ expect(Puppet).to receive(:log_exception).with(anything, /^.*interrupted after .* seconds$/)
636
+ expect { subject.get('/foo') }.to raise_error(eof_error)
637
+ end
294
638
  end
295
639
  end
640
+
641
+ describe Puppet::Network::HTTP::Connection do
642
+ it_behaves_like "an HTTP connection", described_class, true
643
+ end
644
+
645
+ describe Puppet::Network::HTTP::ConnectionAdapter do
646
+ it_behaves_like "an HTTP connection", described_class, false
647
+ end
296
648
  end