| 
									
										
										
										
											2021-01-31 14:50:29 -05:00
										 |  |  | # frozen_string_literal: true | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | require "download_strategy" | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-02-18 15:11:11 -08:00
										 |  |  | RSpec.describe CurlDownloadStrategy do | 
					
						
							| 
									
										
										
										
											2021-01-31 14:50:29 -05:00
										 |  |  |   subject(:strategy) { described_class.new(url, name, version, **specs) } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   let(:name) { "foo" } | 
					
						
							|  |  |  |   let(:url) { "https://example.com/foo.tar.gz" } | 
					
						
							|  |  |  |   let(:version) { "1.2.3" } | 
					
						
							|  |  |  |   let(:specs) { { user: "download:123456" } } | 
					
						
							| 
									
										
										
										
											2022-05-02 18:17:15 +02:00
										 |  |  |   let(:artifact_domain) { nil } | 
					
						
							| 
									
										
										
										
											2023-05-19 13:58:58 +02:00
										 |  |  |   let(:headers) do | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |       "accept-ranges"  => "bytes", | 
					
						
							|  |  |  |       "content-length" => "37182", | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   end | 
					
						
							| 
									
										
										
										
											2021-01-31 14:50:29 -05:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-03-31 21:35:58 +02:00
										 |  |  |   before do | 
					
						
							| 
									
										
										
										
											2023-05-19 13:58:58 +02:00
										 |  |  |     allow(strategy).to receive(:curl_headers).with(any_args) | 
					
						
							| 
									
										
										
										
											2024-03-07 16:20:20 +00:00
										 |  |  |                                              .and_return({ responses: [{ headers: }] }) | 
					
						
							| 
									
										
										
										
											2023-03-31 21:35:58 +02:00
										 |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-01-31 14:50:29 -05:00
										 |  |  |   it "parses the opts and sets the corresponding args" do | 
					
						
							|  |  |  |     expect(strategy.send(:_curl_args)).to eq(["--user", "download:123456"]) | 
					
						
							|  |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   describe "#fetch" do | 
					
						
							|  |  |  |     before do | 
					
						
							| 
									
										
										
										
											2022-05-02 18:17:15 +02:00
										 |  |  |       allow(Homebrew::EnvConfig).to receive(:artifact_domain).and_return(artifact_domain) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-01-31 14:50:29 -05:00
										 |  |  |       strategy.temporary_path.dirname.mkpath | 
					
						
							|  |  |  |       FileUtils.touch strategy.temporary_path | 
					
						
							|  |  |  |     end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     it "calls curl with default arguments" do | 
					
						
							|  |  |  |       expect(strategy).to receive(:curl).with( | 
					
						
							| 
									
										
										
										
											2023-06-22 14:25:18 +01:00
										 |  |  |         "--remote-time", | 
					
						
							| 
									
										
											  
											
												Curl: use `typed: strict`
This upgrades `utils/curl.rb` to `typed: strict`, which requires
a number of changes to pass `brew typecheck`. The most
straightforward are adding type signatures to methods, adding type
annotations (e.g., `T.let`) to variables that need them, and ensuring
that methods always use the expected return type.
I had to refactor areas where we call a `Utils::Curl` method and use
array destructuring on a `SystemCommand::Result` return value
(e.g., `output, errors, status = curl_output(...)`), as Sorbet
doesn't understand implicit array conversion. As suggested by Markus,
I've switched these areas to use `#stdout`, `#stderr`, and `#status`.
This requires the use of an intermediate variable (`result`) in some
cases but this was a fairly straightforward substitution.
I also had to refactor how `Cask::URL::BlockDSL::PageWithURL` works.
It currently uses `page.extend PageWithURL` to add a `url` attribute
but this reworks it to subclass `SimpleDelegator` and use an
`initialize` method instead. This achieves the same goal but in a way
that Sorbet can understand.
											
										 
											2025-01-10 21:37:20 -05:00
										 |  |  |         "--output", an_instance_of(String), | 
					
						
							| 
									
										
										
										
											2021-05-13 12:54:17 -04:00
										 |  |  |         # example.com supports partial requests. | 
					
						
							|  |  |  |         "--continue-at", "-", | 
					
						
							| 
									
										
										
										
											2021-01-31 14:50:29 -05:00
										 |  |  |         "--location", | 
					
						
							|  |  |  |         url, | 
					
						
							|  |  |  |         an_instance_of(Hash) | 
					
						
							|  |  |  |       ) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       strategy.fetch | 
					
						
							|  |  |  |     end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     context "with an explicit user agent" do | 
					
						
							|  |  |  |       let(:specs) { { user_agent: "Mozilla/25.0.1" } } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       it "adds the appropriate curl args" do | 
					
						
							| 
									
										
										
										
											2023-03-03 22:13:41 +00:00
										 |  |  |         expect(strategy).to receive(:system_command) | 
					
						
							|  |  |  |           .with( | 
					
						
							|  |  |  |             /curl/, | 
					
						
							|  |  |  |             hash_including(args: array_including_cons("--user-agent", "Mozilla/25.0.1")), | 
					
						
							|  |  |  |           ) | 
					
						
							|  |  |  |           .at_least(:once) | 
					
						
							|  |  |  |           .and_return(instance_double(SystemCommand::Result, success?: true, stdout: "", assert_success!: nil)) | 
					
						
							| 
									
										
										
										
											2021-01-31 14:50:29 -05:00
										 |  |  | 
 | 
					
						
							|  |  |  |         strategy.fetch | 
					
						
							|  |  |  |       end | 
					
						
							|  |  |  |     end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     context "with a generalized fake user agent" do | 
					
						
							|  |  |  |       alias_matcher :a_string_matching, :match | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-09-06 22:04:22 -07:00
										 |  |  |       let(:specs) { { user_agent: "fake" } } | 
					
						
							| 
									
										
										
										
											2021-01-31 14:50:29 -05:00
										 |  |  | 
 | 
					
						
							|  |  |  |       it "adds the appropriate curl args" do | 
					
						
							| 
									
										
										
										
											2023-03-03 22:13:41 +00:00
										 |  |  |         expect(strategy).to receive(:system_command) | 
					
						
							|  |  |  |           .with( | 
					
						
							|  |  |  |             /curl/, | 
					
						
							| 
									
										
										
										
											2025-09-07 17:56:31 -07:00
										 |  |  |             hash_including(args: array_including_cons("--user-agent", "fake")), | 
					
						
							| 
									
										
										
										
											2023-03-03 22:13:41 +00:00
										 |  |  |           ) | 
					
						
							|  |  |  |           .at_least(:once) | 
					
						
							|  |  |  |           .and_return(instance_double(SystemCommand::Result, success?: true, stdout: "", assert_success!: nil)) | 
					
						
							| 
									
										
										
										
											2021-01-31 14:50:29 -05:00
										 |  |  | 
 | 
					
						
							|  |  |  |         strategy.fetch | 
					
						
							|  |  |  |       end | 
					
						
							|  |  |  |     end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     context "with cookies set" do | 
					
						
							| 
									
										
										
										
											2023-03-08 23:14:46 +00:00
										 |  |  |       let(:specs) do | 
					
						
							| 
									
										
										
										
											2021-01-31 14:50:29 -05:00
										 |  |  |         { | 
					
						
							|  |  |  |           cookies: { | 
					
						
							|  |  |  |             coo: "k/e", | 
					
						
							|  |  |  |             mon: "ster", | 
					
						
							|  |  |  |           }, | 
					
						
							|  |  |  |         } | 
					
						
							| 
									
										
										
										
											2023-03-08 23:14:46 +00:00
										 |  |  |       end | 
					
						
							| 
									
										
										
										
											2021-01-31 14:50:29 -05:00
										 |  |  | 
 | 
					
						
							|  |  |  |       it "adds the appropriate curl args and does not URL-encode the cookies" do | 
					
						
							| 
									
										
										
										
											2023-03-03 22:13:41 +00:00
										 |  |  |         expect(strategy).to receive(:system_command) | 
					
						
							|  |  |  |           .with( | 
					
						
							|  |  |  |             /curl/, | 
					
						
							|  |  |  |             hash_including(args: array_including_cons("-b", "coo=k/e;mon=ster")), | 
					
						
							|  |  |  |           ) | 
					
						
							|  |  |  |           .at_least(:once) | 
					
						
							|  |  |  |           .and_return(instance_double(SystemCommand::Result, success?: true, stdout: "", assert_success!: nil)) | 
					
						
							| 
									
										
										
										
											2021-01-31 14:50:29 -05:00
										 |  |  | 
 | 
					
						
							|  |  |  |         strategy.fetch | 
					
						
							|  |  |  |       end | 
					
						
							|  |  |  |     end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     context "with referer set" do | 
					
						
							|  |  |  |       let(:specs) { { referer: "https://somehost/also" } } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       it "adds the appropriate curl args" do | 
					
						
							| 
									
										
										
										
											2023-03-03 22:13:41 +00:00
										 |  |  |         expect(strategy).to receive(:system_command) | 
					
						
							|  |  |  |           .with( | 
					
						
							|  |  |  |             /curl/, | 
					
						
							|  |  |  |             hash_including(args: array_including_cons("-e", "https://somehost/also")), | 
					
						
							|  |  |  |           ) | 
					
						
							|  |  |  |           .at_least(:once) | 
					
						
							|  |  |  |           .and_return(instance_double(SystemCommand::Result, success?: true, stdout: "", assert_success!: nil)) | 
					
						
							| 
									
										
										
										
											2021-01-31 14:50:29 -05:00
										 |  |  | 
 | 
					
						
							|  |  |  |         strategy.fetch | 
					
						
							|  |  |  |       end | 
					
						
							|  |  |  |     end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     context "with headers set" do | 
					
						
							|  |  |  |       alias_matcher :a_string_matching, :match | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       let(:specs) { { headers: ["foo", "bar"] } } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       it "adds the appropriate curl args" do | 
					
						
							| 
									
										
										
										
											2023-03-03 22:13:41 +00:00
										 |  |  |         expect(strategy).to receive(:system_command) | 
					
						
							|  |  |  |           .with( | 
					
						
							|  |  |  |             /curl/, | 
					
						
							|  |  |  |             hash_including( | 
					
						
							|  |  |  |               args: array_including_cons("--header", "foo").and(array_including_cons("--header", "bar")), | 
					
						
							|  |  |  |             ), | 
					
						
							|  |  |  |           ) | 
					
						
							|  |  |  |           .at_least(:once) | 
					
						
							|  |  |  |           .and_return(instance_double(SystemCommand::Result, success?: true, stdout: "", assert_success!: nil)) | 
					
						
							| 
									
										
										
										
											2021-01-31 14:50:29 -05:00
										 |  |  | 
 | 
					
						
							|  |  |  |         strategy.fetch | 
					
						
							|  |  |  |       end | 
					
						
							|  |  |  |     end | 
					
						
							| 
									
										
										
										
											2022-05-02 18:17:15 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |     context "with artifact_domain set" do | 
					
						
							|  |  |  |       let(:artifact_domain) { "https://mirror.example.com/oci" } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       context "with an asset hosted under example.com" do | 
					
						
							| 
									
										
										
										
											2022-05-07 16:43:06 +02:00
										 |  |  |         it "leaves the URL unchanged" do | 
					
						
							| 
									
										
										
										
											2023-03-03 22:13:41 +00:00
										 |  |  |           expect(strategy).to receive(:system_command) | 
					
						
							|  |  |  |             .with( | 
					
						
							|  |  |  |               /curl/, | 
					
						
							|  |  |  |               hash_including(args: array_including_cons(url)), | 
					
						
							|  |  |  |             ) | 
					
						
							|  |  |  |             .at_least(:once) | 
					
						
							|  |  |  |             .and_return(instance_double(SystemCommand::Result, success?: true, stdout: "", assert_success!: nil)) | 
					
						
							| 
									
										
										
										
											2022-05-02 18:17:15 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |           strategy.fetch | 
					
						
							|  |  |  |         end | 
					
						
							|  |  |  |       end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       context "with an asset hosted under #{GitHubPackages::URL_DOMAIN} (HTTP)" do | 
					
						
							|  |  |  |         let(:resource_path) { "v2/homebrew/core/spec/manifests/0.0" } | 
					
						
							|  |  |  |         let(:url) { "http://#{GitHubPackages::URL_DOMAIN}/#{resource_path}" } | 
					
						
							|  |  |  |         let(:status) { instance_double(Process::Status, success?: true, exitstatus: 0) } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         it "rewrites the URL correctly" do | 
					
						
							| 
									
										
										
										
											2023-03-03 22:13:41 +00:00
										 |  |  |           expect(strategy).to receive(:system_command) | 
					
						
							|  |  |  |             .with( | 
					
						
							|  |  |  |               /curl/, | 
					
						
							|  |  |  |               hash_including(args: array_including_cons("#{artifact_domain}/#{resource_path}")), | 
					
						
							|  |  |  |             ) | 
					
						
							|  |  |  |             .at_least(:once) | 
					
						
							| 
									
										
										
										
											2025-09-07 17:56:31 -07:00
										 |  |  |             .and_return(SystemCommand::Result.new(["curl"], [[:stdout, ""]], status, secrets: [])) | 
					
						
							| 
									
										
										
										
											2022-05-02 18:17:15 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |           strategy.fetch | 
					
						
							|  |  |  |         end | 
					
						
							|  |  |  |       end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       context "with an asset hosted under #{GitHubPackages::URL_DOMAIN} (HTTPS)" do | 
					
						
							|  |  |  |         let(:resource_path) { "v2/homebrew/core/spec/manifests/0.0" } | 
					
						
							|  |  |  |         let(:url) { "https://#{GitHubPackages::URL_DOMAIN}/#{resource_path}" } | 
					
						
							|  |  |  |         let(:status) { instance_double(Process::Status, success?: true, exitstatus: 0) } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         it "rewrites the URL correctly" do | 
					
						
							| 
									
										
										
										
											2023-03-03 22:13:41 +00:00
										 |  |  |           expect(strategy).to receive(:system_command) | 
					
						
							|  |  |  |             .with( | 
					
						
							|  |  |  |               /curl/, | 
					
						
							|  |  |  |               hash_including(args: array_including_cons("#{artifact_domain}/#{resource_path}")), | 
					
						
							|  |  |  |             ) | 
					
						
							|  |  |  |             .at_least(:once) | 
					
						
							| 
									
										
										
										
											2025-09-07 15:29:11 -07:00
										 |  |  |             .and_return(SystemCommand::Result.new(["curl"], [[:stdout, ""]], status, secrets: [])) | 
					
						
							| 
									
										
										
										
											2022-05-02 18:17:15 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |           strategy.fetch | 
					
						
							|  |  |  |         end | 
					
						
							|  |  |  |       end | 
					
						
							|  |  |  |     end | 
					
						
							| 
									
										
										
										
											2021-01-31 14:50:29 -05:00
										 |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   describe "#cached_location" do | 
					
						
							| 
									
										
										
										
											2023-03-31 21:35:58 +02:00
										 |  |  |     subject(:cached_location) { strategy.cached_location } | 
					
						
							| 
									
										
										
										
											2021-02-21 13:42:46 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  |     context "when URL ends with file" do | 
					
						
							| 
									
										
										
										
											2023-03-31 21:35:58 +02:00
										 |  |  |       it "falls back to the file name in the URL" do | 
					
						
							| 
									
										
										
										
											2021-02-21 13:42:46 +00:00
										 |  |  |         expect(cached_location).to eq( | 
					
						
							|  |  |  |           HOMEBREW_CACHE/"downloads/3d1c0ae7da22be9d83fb1eb774df96b7c4da71d3cf07e1cb28555cf9a5e5af70--foo.tar.gz", | 
					
						
							|  |  |  |         ) | 
					
						
							| 
									
										
										
										
											2023-03-31 21:35:58 +02:00
										 |  |  |       end | 
					
						
							| 
									
										
										
										
											2021-02-21 13:42:46 +00:00
										 |  |  |     end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     context "when URL file is in middle" do | 
					
						
							|  |  |  |       let(:url) { "https://example.com/foo.tar.gz/from/this/mirror" } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-03-31 21:35:58 +02:00
										 |  |  |       it "falls back to the file name in the URL" do | 
					
						
							| 
									
										
										
										
											2021-02-21 13:42:46 +00:00
										 |  |  |         expect(cached_location).to eq( | 
					
						
							|  |  |  |           HOMEBREW_CACHE/"downloads/1ab61269ba52c83994510b1e28dd04167a2f2e8393a35a9c50c1f7d33fd8f619--foo.tar.gz", | 
					
						
							|  |  |  |         ) | 
					
						
							| 
									
										
										
										
											2023-03-31 21:35:58 +02:00
										 |  |  |       end | 
					
						
							| 
									
										
										
										
											2021-02-21 13:42:46 +00:00
										 |  |  |     end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-01-31 14:50:29 -05:00
										 |  |  |     context "with a file name trailing the URL path" do | 
					
						
							|  |  |  |       let(:url) { "https://example.com/cask.dmg" } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-03-31 21:35:58 +02:00
										 |  |  |       it "falls back to the file extension in the URL" do | 
					
						
							| 
									
										
										
										
											2021-02-21 13:42:46 +00:00
										 |  |  |         expect(cached_location.extname).to eq(".dmg") | 
					
						
							| 
									
										
										
										
											2023-03-31 21:35:58 +02:00
										 |  |  |       end | 
					
						
							| 
									
										
										
										
											2021-01-31 14:50:29 -05:00
										 |  |  |     end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     context "with a file name trailing the first query parameter" do | 
					
						
							|  |  |  |       let(:url) { "https://example.com/download?file=cask.zip&a=1" } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-03-31 21:35:58 +02:00
										 |  |  |       it "falls back to the file extension in the URL" do | 
					
						
							| 
									
										
										
										
											2021-02-21 13:42:46 +00:00
										 |  |  |         expect(cached_location.extname).to eq(".zip") | 
					
						
							| 
									
										
										
										
											2023-03-31 21:35:58 +02:00
										 |  |  |       end | 
					
						
							| 
									
										
										
										
											2021-01-31 14:50:29 -05:00
										 |  |  |     end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     context "with a file name trailing the second query parameter" do | 
					
						
							|  |  |  |       let(:url) { "https://example.com/dl?a=1&file=cask.zip&b=2" } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-03-31 21:35:58 +02:00
										 |  |  |       it "falls back to the file extension in the URL" do | 
					
						
							| 
									
										
										
										
											2021-02-21 13:42:46 +00:00
										 |  |  |         expect(cached_location.extname).to eq(".zip") | 
					
						
							| 
									
										
										
										
											2023-03-31 21:35:58 +02:00
										 |  |  |       end | 
					
						
							| 
									
										
										
										
											2021-01-31 14:50:29 -05:00
										 |  |  |     end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     context "with an unusually long query string" do | 
					
						
							|  |  |  |       let(:url) do | 
					
						
							|  |  |  |         [ | 
					
						
							|  |  |  |           "https://node49152.ssl.fancycdn.example.com", | 
					
						
							|  |  |  |           "/fancycdn/node/49152/file/upload/download", | 
					
						
							|  |  |  |           "?cask_class=zf920df", | 
					
						
							|  |  |  |           "&cask_group=2348779087242312", | 
					
						
							|  |  |  |           "&cask_archive_file_name=cask.zip", | 
					
						
							|  |  |  |           "&signature=CGmDulxL8pmutKTlCleNTUY%2FyO9Xyl5u9yVZUE0", | 
					
						
							|  |  |  |           "uWrjadjuz67Jp7zx3H7NEOhSyOhu8nzicEHRBjr3uSoOJzwkLC8L", | 
					
						
							|  |  |  |           "BLKnz%2B2X%2Biq5m6IdwSVFcLp2Q1Hr2kR7ETn3rF1DIq5o0lHC", | 
					
						
							|  |  |  |           "yzMmyNe5giEKJNW8WF0KXriULhzLTWLSA3ZTLCIofAdRiiGje1kN", | 
					
						
							|  |  |  |           "YY3C0SBqymQB8CG3ONn5kj7CIGbxrDOq5xI2ZSJdIyPysSX7SLvE", | 
					
						
							|  |  |  |           "DBw2KdR24q9t1wfjS9LUzelf5TWk6ojj8p9%2FHjl%2Fi%2FVCXN", | 
					
						
							|  |  |  |           "N4o1mW%2FMayy2tTY1qcC%2FTmqI1ulZS8SNuaSgr9Iys9oDF1%2", | 
					
						
							|  |  |  |           "BPK%2B4Sg==", | 
					
						
							|  |  |  |         ].join | 
					
						
							|  |  |  |       end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-03-31 21:35:58 +02:00
										 |  |  |       it "falls back to the file extension in the URL" do | 
					
						
							| 
									
										
										
										
											2021-02-21 13:42:46 +00:00
										 |  |  |         expect(cached_location.extname).to eq(".zip") | 
					
						
							|  |  |  |         expect(cached_location.to_path.length).to be_between(0, 255) | 
					
						
							| 
									
										
										
										
											2023-03-31 21:35:58 +02:00
										 |  |  |       end | 
					
						
							| 
									
										
										
										
											2021-01-31 14:50:29 -05:00
										 |  |  |     end | 
					
						
							|  |  |  |   end | 
					
						
							|  |  |  | end |