brew/Library/Homebrew/test/livecheck/strategy_spec.rb
Sam Ford 2187316262
Strategy: Expand test coverage
Between this commit and the previous one, this brings test coverage
for `Livecheck::Strategy` up to 98.18% line coverage and 97.22%
branch coverage. The only uncovered areas are some Sorbet `params`
calls (which I'm not sure how to cover) and a conditional `break` in
`page_headers` that will be refactored away in the future.

The increased coverage is primarily in areas that weren't covered
before because they call methods that make network requests. I worked
around this with stubs and doubles, so we can test this code to some
degree. I plan to expand this approach to other areas in livecheck
that aren't covered for the same reason and that should significantly
increase test coverage (along with some other test improvements that
I have lined up).
2025-02-07 08:57:32 -05:00

277 lines
11 KiB
Ruby

# frozen_string_literal: true
require "livecheck/strategy"
RSpec.describe Homebrew::Livecheck::Strategy do
subject(:strategy) { described_class }
let(:url) { "https://brew.sh/" }
let(:redirection_url) { "https://brew.sh/redirection" }
let(:post_hash) do
{
"empty" => "",
"boolean" => "true",
"number" => "1",
"string" => "a + b = c",
}
end
let(:post_hash_symbol_keys) do
{
empty: "",
boolean: "true",
number: "1",
string: "a + b = c",
}
end
let(:form_string) { "empty=&boolean=true&number=1&string=a+%2B+b+%3D+c" }
let(:json_string) { '{"empty":"","boolean":"true","number":"1","string":"a + b = c"}' }
let(:response_hash) do
response_hash = {}
response_hash[:ok] = {
status_code: "200",
status_text: "OK",
headers: {
"cache-control" => "max-age=604800",
"content-type" => "text/html; charset=UTF-8",
"date" => "Wed, 1 Jan 2020 01:23:45 GMT",
"expires" => "Wed, 31 Jan 2020 01:23:45 GMT",
"last-modified" => "Thu, 1 Jan 2019 01:23:45 GMT",
"content-length" => "123",
},
}
response_hash[:redirection] = {
status_code: "301",
status_text: "Moved Permanently",
headers: {
"cache-control" => "max-age=604800",
"content-type" => "text/html; charset=UTF-8",
"date" => "Wed, 1 Jan 2020 01:23:45 GMT",
"expires" => "Wed, 31 Jan 2020 01:23:45 GMT",
"last-modified" => "Thu, 1 Jan 2019 01:23:45 GMT",
"content-length" => "123",
"location" => redirection_url,
},
}
response_hash
end
let(:body) do
<<~HTML
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Thank you!</title>
</head>
<body>
<h1>Download</h1>
<p>This download link could have been made publicly available in a reasonable fashion but we appreciate that you jumped through the hoops that we carefully set up!: <a href="https://brew.sh/example-1.2.3.tar.gz">Example v1.2.3</a></p>
<p>The current legacy version is: <a href="https://brew.sh/example-0.1.2.tar.gz">Example v0.1.2</a></p>
</body>
</html>
HTML
end
let(:response_text) do
response_text = {}
response_text[:ok] = <<~EOS
HTTP/1.1 #{response_hash[:ok][:status_code]} #{response_hash[:ok][:status_text]}\r
Cache-Control: #{response_hash[:ok][:headers]["cache-control"]}\r
Content-Type: #{response_hash[:ok][:headers]["content-type"]}\r
Date: #{response_hash[:ok][:headers]["date"]}\r
Expires: #{response_hash[:ok][:headers]["expires"]}\r
Last-Modified: #{response_hash[:ok][:headers]["last-modified"]}\r
Content-Length: #{response_hash[:ok][:headers]["content-length"]}\r
\r
#{body.rstrip}
EOS
response_text[:redirection_to_ok] = response_text[:ok].sub(
"HTTP/1.1 #{response_hash[:ok][:status_code]} #{response_hash[:ok][:status_text]}\r",
"HTTP/1.1 #{response_hash[:redirection][:status_code]} #{response_hash[:redirection][:status_text]}\r\n" \
"Location: #{response_hash[:redirection][:headers]["location"]}\r",
)
response_text
end
describe "::from_symbol" do
it "returns the Strategy module represented by the Symbol argument" do
expect(strategy.from_symbol(:page_match)).to eq(Homebrew::Livecheck::Strategy::PageMatch)
end
it "returns `nil` if the argument is `nil`" do
expect(strategy.from_symbol(nil)).to be_nil
end
end
describe "::from_url" do
let(:sourceforge_url) { "https://sourceforge.net/projects/test" }
context "when a regex or `strategy` block is not provided" do
it "returns an array of usable strategies which doesn't include PageMatch" do
expect(strategy.from_url(sourceforge_url)).to eq([Homebrew::Livecheck::Strategy::Sourceforge])
end
end
context "when a regex or `strategy` block is provided" do
it "returns an array of usable strategies including PageMatch, sorted in descending order by priority" do
expect(strategy.from_url(sourceforge_url, regex_provided: true))
.to eq(
[Homebrew::Livecheck::Strategy::Sourceforge, Homebrew::Livecheck::Strategy::PageMatch],
)
end
end
context "when a `strategy` block is required and one is provided" do
it "returns an array of usable strategies including the specified strategy" do
# The strategies array is naturally in alphabetic order when all
# applicable strategies have the same priority
expect(strategy.from_url(url, livecheck_strategy: :json, block_provided: true))
.to eq([Homebrew::Livecheck::Strategy::Json, Homebrew::Livecheck::Strategy::PageMatch])
expect(strategy.from_url(url, livecheck_strategy: :xml, block_provided: true))
.to eq([Homebrew::Livecheck::Strategy::PageMatch, Homebrew::Livecheck::Strategy::Xml])
expect(strategy.from_url(url, livecheck_strategy: :yaml, block_provided: true))
.to eq([Homebrew::Livecheck::Strategy::PageMatch, Homebrew::Livecheck::Strategy::Yaml])
end
end
context "when a `strategy` block is required and one is not provided" do
it "returns an array of usable strategies not including the specified strategy" do
expect(strategy.from_url(url, livecheck_strategy: :json, block_provided: false)).to eq([])
expect(strategy.from_url(url, livecheck_strategy: :xml, block_provided: false)).to eq([])
expect(strategy.from_url(url, livecheck_strategy: :yaml, block_provided: false)).to eq([])
end
end
end
describe "::post_args" do
it "returns an array including `--data` and an encoded form data string" do
expect(strategy.post_args(post_form: post_hash)).to eq(["--data", form_string])
expect(strategy.post_args(post_form: post_hash_symbol_keys)).to eq(["--data", form_string])
# If both `post_form` and `post_json` are present, only `post_form` will
# be used.
expect(strategy.post_args(post_form: post_hash, post_json: post_hash)).to eq(["--data", form_string])
end
it "returns an array including `--json` and a JSON string" do
expect(strategy.post_args(post_json: post_hash)).to eq(["--json", json_string])
expect(strategy.post_args(post_json: post_hash_symbol_keys)).to eq(["--json", json_string])
end
it "returns an empty array if `post_form` value is blank" do
expect(strategy.post_args(post_form: {})).to eq([])
end
it "returns an empty array if `post_json` value is blank" do
expect(strategy.post_args(post_json: {})).to eq([])
end
it "returns an empty array if hash argument doesn't have a `post_form` or `post_json` value" do
expect(strategy.post_args).to eq([])
end
end
describe "::page_headers" do
let(:responses) { [response_hash[:ok]] }
it "returns headers from fetched content" do
allow(strategy).to receive(:curl_headers).and_return({ responses:, body: })
expect(strategy.page_headers(url)).to eq([responses.first[:headers]])
end
it "handles `post_form` `url` options" do
allow(strategy).to receive(:curl_headers).and_return({ responses:, body: })
expect(strategy.page_headers(url, url_options: { post_form: post_hash }))
.to eq([responses.first[:headers]])
end
it "returns an empty array if `curl_headers` only raises an `ErrorDuringExecution` error" do
allow(strategy).to receive(:curl_headers).and_raise(ErrorDuringExecution.new([], status: 1))
expect(strategy.page_headers(url)).to eq([])
end
end
describe "::page_content" do
let(:curl_version) { Version.new("8.7.1") }
let(:success_status) { instance_double(Process::Status, success?: true, exitstatus: 0) }
it "returns hash including fetched content" do
allow_any_instance_of(Utils::Curl).to receive(:curl_version).and_return(curl_version)
allow(strategy).to receive(:curl_output).and_return([response_text[:ok], nil, success_status])
expect(strategy.page_content(url)).to eq({ content: body })
end
it "handles `post_form` `url` option" do
allow_any_instance_of(Utils::Curl).to receive(:curl_version).and_return(curl_version)
allow(strategy).to receive(:curl_output).and_return([response_text[:ok], nil, success_status])
expect(strategy.page_content(url, url_options: { post_form: post_hash })).to eq({ content: body })
end
it "handles `post_json` `url` option" do
allow_any_instance_of(Utils::Curl).to receive(:curl_version).and_return(curl_version)
allow(strategy).to receive(:curl_output).and_return([response_text[:ok], nil, success_status])
expect(strategy.page_content(url, url_options: { post_json: post_hash })).to eq({ content: body })
end
it "returns error `messages` from `stderr` in the return hash on failure when `stderr` is not `nil`" do
error_message = "curl: (6) Could not resolve host: brew.sh"
allow_any_instance_of(Utils::Curl).to receive(:curl_version).and_return(curl_version)
allow(strategy).to receive(:curl_output).and_return([
nil,
error_message,
instance_double(Process::Status, success?: false, exitstatus: 6),
])
expect(strategy.page_content(url)).to eq({ messages: [error_message] })
end
it "returns default error `messages` in the return hash on failure when `stderr` is `nil`" do
allow_any_instance_of(Utils::Curl).to receive(:curl_version).and_return(curl_version)
allow(strategy).to receive(:curl_output).and_return([
nil,
nil,
instance_double(Process::Status, success?: false, exitstatus: 1),
])
expect(strategy.page_content(url)).to eq({ messages: ["cURL failed without a detectable error"] })
end
it "returns hash including `final_url` if it differs from initial `url`" do
allow_any_instance_of(Utils::Curl).to receive(:curl_version).and_return(curl_version)
allow(strategy).to receive(:curl_output).and_return([response_text[:redirection_to_ok], nil, success_status])
expect(strategy.page_content(url)).to eq({ content: body, final_url: redirection_url })
end
end
describe "::handle_block_return" do
it "returns an array of version strings when given a valid value" do
expect(strategy.handle_block_return("1.2.3")).to eq(["1.2.3"])
expect(strategy.handle_block_return(["1.2.3", "1.2.4"])).to eq(["1.2.3", "1.2.4"])
end
it "returns an empty array when given a nil value" do
expect(strategy.handle_block_return(nil)).to eq([])
end
it "errors when given an invalid value" do
expect { strategy.handle_block_return(123) }
.to raise_error(TypeError, strategy::INVALID_BLOCK_RETURN_VALUE_MSG)
end
end
end