
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).
277 lines
11 KiB
Ruby
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
|