Skip to content

Commit 4c40d4e

Browse files
committed
Curl: Expand test coverage
This adds more tests to `curl_spec.rb` to increase test coverage. This brings almost all of the methods that don't make network requests up to 100% line and branch coverage (the exception being some guards in `parse_curl_output` that shouldn't happen under normal circumstances). In the process of writing more tests for `parse_curl_response`, I made some tweaks to remove checks for conditions that shouldn't ever be true (e.g., `match["code"]` isn't optional, so it will be present if `HTTP_STATUS_LINE_REGEX` matches) and to refactor some others. I contributed this method a while back (9171eb2), so this is me coming back to clarify some behavior.
1 parent c82996d commit 4c40d4e

File tree

2 files changed

+187
-14
lines changed

2 files changed

+187
-14
lines changed

Library/Homebrew/test/utils/curl_spec.rb

Lines changed: 181 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,12 @@
138138
},
139139
}
140140

141+
response_hash[:ok_no_status_text] = response_hash[:ok].deep_dup
142+
response_hash[:ok_no_status_text].delete(:status_text)
143+
144+
response_hash[:ok_blank_header_value] = response_hash[:ok].deep_dup
145+
response_hash[:ok_blank_header_value][:headers]["range"] = ""
146+
141147
response_hash[:redirection] = {
142148
status_code: "301",
143149
status_text: "Moved Permanently",
@@ -257,6 +263,16 @@
257263
\r
258264
EOS
259265

266+
response_text[:ok_no_status_text] = response_text[:ok].sub(" #{response_hash[:ok][:status_text]}", "")
267+
response_text[:ok_blank_header_name] = response_text[:ok].sub(
268+
"#{response_hash[:ok][:headers]["date"]}\r\n",
269+
"#{response_hash[:ok][:headers]["date"]}\r\n: Test\r\n",
270+
)
271+
response_text[:ok_blank_header_value] = response_text[:ok].sub(
272+
"#{response_hash[:ok][:headers]["date"]}\r\n",
273+
"#{response_hash[:ok][:headers]["date"]}\r\nRange:\r\n",
274+
)
275+
260276
response_text[:redirection] = response_text[:ok].sub(
261277
"HTTP/1.1 #{response_hash[:ok][:status_code]} #{response_hash[:ok][:status_text]}\r",
262278
"HTTP/1.1 #{response_hash[:redirection][:status_code]} #{response_hash[:redirection][:status_text]}\r\n" \
@@ -306,7 +322,27 @@
306322
body
307323
end
308324

309-
describe "curl_args" do
325+
describe "::curl_executable" do
326+
it "returns `HOMEBREW_BREWED_CURL_PATH` when `use_homebrew_curl` is `true`" do
327+
expect(curl_executable(use_homebrew_curl: true)).to eq(HOMEBREW_BREWED_CURL_PATH)
328+
end
329+
330+
it "returns curl shim path when `use_homebrew_curl` is `false` or omitted" do
331+
curl_shim_path = HOMEBREW_SHIMS_PATH/"shared/curl"
332+
expect(curl_executable(use_homebrew_curl: false)).to eq(curl_shim_path)
333+
expect(curl_executable).to eq(curl_shim_path)
334+
end
335+
end
336+
337+
describe "::curl_path" do
338+
it "returns a curl path string" do
339+
expect(curl_path).to match(%r{[^/]+(?:/[^/]+)*})
340+
end
341+
end
342+
343+
describe "::curl_args" do
344+
include Context
345+
310346
let(:args) { ["foo"] }
311347
let(:user_agent_string) { "Lorem ipsum dolor sit amet" }
312348

@@ -388,6 +424,11 @@
388424
expect { curl_args(*args, retry_max_time: "test") }.to raise_error(TypeError)
389425
end
390426

427+
it "uses `--show-error` when :show_error is `true`" do
428+
expect(curl_args(*args, show_error: true)).to include("--show-error")
429+
expect(curl_args(*args, show_error: false)).not_to include("--show-error")
430+
end
431+
391432
it "uses `--referer` when :referer is present" do
392433
expect(curl_args(*args, referer: "https://brew.sh").join(" ")).to include("--referer https://brew.sh")
393434
end
@@ -426,9 +467,62 @@
426467
expect(curl_args(*args).join(" ")).to include("--fail")
427468
expect(curl_args(*args, show_output: true).join(" ")).not_to include("--fail")
428469
end
470+
471+
it "uses `--progress-bar` outside of a `--verbose` context" do
472+
expect(curl_args(*args).join(" ")).to include("--progress-bar")
473+
with_context verbose: true do
474+
expect(curl_args(*args).join(" ")).not_to include("--progress-bar")
475+
end
476+
end
477+
478+
context "when `EnvConfig.curl_verbose?` is `true`" do
479+
before do
480+
allow(Homebrew::EnvConfig).to receive(:curl_verbose?).and_return(true)
481+
end
482+
483+
it "uses `--verbose`" do
484+
expect(curl_args(*args).join(" ")).to include("--verbose")
485+
end
486+
end
487+
488+
context "when `EnvConfig.curl_verbose?` is `false`" do
489+
before do
490+
allow(Homebrew::EnvConfig).to receive(:curl_verbose?).and_return(false)
491+
end
492+
493+
it "doesn't use `--verbose`" do
494+
expect(curl_args(*args).join(" ")).not_to include("--verbose")
495+
end
496+
end
497+
498+
context "when `$stdout.tty?` is `false`" do
499+
before do
500+
allow($stdout).to receive(:tty?).and_return(false)
501+
end
502+
503+
it "uses `--silent`" do
504+
expect(curl_args(*args).join(" ")).to include("--silent")
505+
end
506+
end
507+
508+
context "when `$stdout.tty?` is `true`" do
509+
before do
510+
allow($stdout).to receive(:tty?).and_return(true)
511+
end
512+
513+
it "doesn't use `--silent` outside of a `--quiet` context" do
514+
with_context quiet: false do
515+
expect(curl_args(*args).join(" ")).not_to include("--silent")
516+
end
517+
518+
with_context quiet: true do
519+
expect(curl_args(*args).join(" ")).to include("--silent")
520+
end
521+
end
522+
end
429523
end
430524

431-
describe "url_protected_by_cloudflare?" do
525+
describe "::url_protected_by_cloudflare?" do
432526
it "returns `true` when a URL is protected by Cloudflare" do
433527
expect(url_protected_by_cloudflare?(details[:cloudflare][:single_cookie])).to be(true)
434528
expect(url_protected_by_cloudflare?(details[:cloudflare][:multiple_cookies])).to be(true)
@@ -448,7 +542,7 @@
448542
end
449543
end
450544

451-
describe "url_protected_by_incapsula?" do
545+
describe "::url_protected_by_incapsula?" do
452546
it "returns `true` when a URL is protected by Cloudflare" do
453547
expect(url_protected_by_incapsula?(details[:incapsula][:single_cookie_visid_incap])).to be(true)
454548
expect(url_protected_by_incapsula?(details[:incapsula][:single_cookie_incap_ses])).to be(true)
@@ -468,7 +562,54 @@
468562
end
469563
end
470564

471-
describe "#parse_curl_output" do
565+
describe "::curl_version" do
566+
it "returns a curl version string" do
567+
expect(curl_version).to match(/^v?(\d+(?:\.\d+)+)$/)
568+
end
569+
end
570+
571+
describe "::curl_supports_fail_with_body?" do
572+
it "returns `true` if curl version is 7.76.0 or higher" do
573+
allow_any_instance_of(Utils::Curl).to receive(:curl_version).and_return(Version.new("7.76.0"))
574+
expect(curl_supports_fail_with_body?).to be(true)
575+
576+
allow_any_instance_of(Utils::Curl).to receive(:curl_version).and_return(Version.new("7.76.1"))
577+
expect(curl_supports_fail_with_body?).to be(true)
578+
end
579+
580+
it "returns `false` if curl version is lower than 7.76.0" do
581+
allow_any_instance_of(Utils::Curl).to receive(:curl_version).and_return(Version.new("7.75.0"))
582+
expect(curl_supports_fail_with_body?).to be(false)
583+
end
584+
end
585+
586+
describe "::curl_supports_tls13?" do
587+
it "returns `true` if curl command is successful" do
588+
allow_any_instance_of(Kernel).to receive(:quiet_system).and_return(true)
589+
expect(curl_supports_tls13?).to be(true)
590+
end
591+
592+
it "returns `false` if curl command is not successful" do
593+
allow_any_instance_of(Kernel).to receive(:quiet_system).and_return(false)
594+
expect(curl_supports_tls13?).to be(false)
595+
end
596+
end
597+
598+
describe "::http_status_ok?" do
599+
it "returns `true` when `status` is 1xx or 2xx" do
600+
expect(http_status_ok?("200")).to be(true)
601+
end
602+
603+
it "returns `false` when `status` is not 1xx or 2xx" do
604+
expect(http_status_ok?("301")).to be(false)
605+
end
606+
607+
it "returns `false` when `status` is `nil`" do
608+
expect(http_status_ok?(nil)).to be(false)
609+
end
610+
end
611+
612+
describe "::parse_curl_output" do
472613
it "returns a correct hash when curl output contains response(s) and body" do
473614
expect(parse_curl_output("#{response_text[:ok]}#{body[:default]}"))
474615
.to eq({ responses: [response_hash[:ok]], body: body[:default] })
@@ -505,21 +646,33 @@
505646
it "returns correct hash when curl output is blank" do
506647
expect(parse_curl_output("")).to eq({ responses: [], body: "" })
507648
end
649+
650+
it "errors if response count exceeds `max_iterations`" do
651+
expect do
652+
parse_curl_output(response_text[:redirections_to_ok], max_iterations: 1)
653+
end.to raise_error("Too many redirects (max = 1)")
654+
end
508655
end
509656

510-
describe "#parse_curl_response" do
657+
describe "::parse_curl_response" do
511658
it "returns a correct hash when given HTTP response text" do
512659
expect(parse_curl_response(response_text[:ok])).to eq(response_hash[:ok])
660+
expect(parse_curl_response(response_text[:ok_no_status_text])).to eq(response_hash[:ok_no_status_text])
661+
expect(parse_curl_response(response_text[:ok_blank_header_value])).to eq(response_hash[:ok_blank_header_value])
513662
expect(parse_curl_response(response_text[:redirection])).to eq(response_hash[:redirection])
514663
expect(parse_curl_response(response_text[:duplicate_header])).to eq(response_hash[:duplicate_header])
515664
end
516665

666+
it "skips over response header lines with blank header name" do
667+
expect(parse_curl_response(response_text[:ok_blank_header_name])).to eq(response_hash[:ok])
668+
end
669+
517670
it "returns an empty hash when given an empty string" do
518671
expect(parse_curl_response("")).to eq({})
519672
end
520673
end
521674

522-
describe "#curl_response_last_location" do
675+
describe "::curl_response_last_location" do
523676
it "returns the last location header when given an array of HTTP response hashes" do
524677
expect(curl_response_last_location([
525678
response_hash[:redirection],
@@ -577,12 +730,20 @@
577730
).to eq(response_hash[:redirection_parent_relative][:headers]["location"].sub(/^\./, "https://brew.sh/test1"))
578731
end
579732

733+
it "skips response hashes without a `:headers` value" do
734+
expect(curl_response_last_location([
735+
response_hash[:redirection],
736+
{ status_code: "404", status_text: "Not Found" },
737+
response_hash[:ok],
738+
])).to eq(response_hash[:redirection][:headers]["location"])
739+
end
740+
580741
it "returns nil when the response hash doesn't contain a location header" do
581742
expect(curl_response_last_location([response_hash[:ok]])).to be_nil
582743
end
583744
end
584745

585-
describe "#curl_response_follow_redirections" do
746+
describe "::curl_response_follow_redirections" do
586747
it "returns the original URL when there are no location headers" do
587748
expect(
588749
curl_response_follow_redirections(
@@ -634,5 +795,18 @@
634795
),
635796
).to eq("#{location_urls[0]}example/")
636797
end
798+
799+
it "skips response hashes without a `:headers` value" do
800+
expect(
801+
curl_response_follow_redirections(
802+
[
803+
response_hash[:redirection_root_relative],
804+
{ status_code: "404", status_text: "Not Found" },
805+
response_hash[:ok],
806+
],
807+
"https://brew.sh/test1/test2",
808+
),
809+
).to eq("https://brew.sh/example/")
810+
end
637811
end
638812
end

Library/Homebrew/utils/curl.rb

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -702,30 +702,29 @@ def curl_response_follow_redirections(responses, base_url)
702702
sig { params(response_text: String).returns(T::Hash[Symbol, T.untyped]) }
703703
def parse_curl_response(response_text)
704704
response = {}
705-
return response unless response_text.match?(HTTP_STATUS_LINE_REGEX)
705+
return response unless (match = response_text.match(HTTP_STATUS_LINE_REGEX))
706706

707707
# Parse the status line and remove it
708-
match = T.must(response_text.match(HTTP_STATUS_LINE_REGEX))
709-
response[:status_code] = match["code"] if match["code"].present?
708+
response[:status_code] = match["code"]
710709
response[:status_text] = match["text"] if match["text"].present?
711710
response_text = response_text.sub(%r{^HTTP/.* (\d+).*$\s*}, "")
712711

713712
# Create a hash from the header lines
714713
response[:headers] = {}
715714
response_text.split("\r\n").each do |line|
716715
header_name, header_value = line.split(/:\s*/, 2)
717-
next if header_name.blank?
716+
next if header_name.blank? || header_value.nil?
718717

719718
header_name = header_name.strip.downcase
720-
header_value&.strip!
719+
header_value.strip!
721720

722721
case response[:headers][header_name]
723-
when nil
724-
response[:headers][header_name] = header_value
725722
when String
726723
response[:headers][header_name] = [response[:headers][header_name], header_value]
727724
when Array
728725
response[:headers][header_name].push(header_value)
726+
else
727+
response[:headers][header_name] = header_value
729728
end
730729

731730
response[:headers][header_name]

0 commit comments

Comments
 (0)