2020-08-08 07:16:06 +05:30
|
|
|
# frozen_string_literal: true
|
|
|
|
|
|
|
|
require "livecheck/strategy"
|
|
|
|
|
2024-02-18 15:11:11 -08:00
|
|
|
RSpec.describe Homebrew::Livecheck::Strategy do
|
2020-08-08 07:16:06 +05:30
|
|
|
subject(:strategy) { described_class }
|
|
|
|
|
2025-02-04 10:30:16 -05:00
|
|
|
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(: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
|
|
|
|
|
2020-08-08 07:16:06 +05:30
|
|
|
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
|
2025-02-04 15:48:59 -05:00
|
|
|
|
|
|
|
it "returns `nil` if the argument is `nil`" do
|
|
|
|
expect(strategy.from_symbol(nil)).to be_nil
|
|
|
|
end
|
2020-08-08 07:16:06 +05:30
|
|
|
end
|
|
|
|
|
|
|
|
describe "::from_url" do
|
2025-02-04 15:48:59 -05:00
|
|
|
let(:sourceforge_url) { "https://sourceforge.net/projects/test" }
|
2020-08-08 07:16:06 +05:30
|
|
|
|
2025-02-04 15:48:59 -05:00
|
|
|
context "when a regex or `strategy` block is not provided" do
|
2020-08-08 07:16:06 +05:30
|
|
|
it "returns an array of usable strategies which doesn't include PageMatch" do
|
2025-02-04 15:48:59 -05:00
|
|
|
expect(strategy.from_url(sourceforge_url)).to eq([Homebrew::Livecheck::Strategy::Sourceforge])
|
2020-08-08 07:16:06 +05:30
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2025-02-04 15:48:59 -05:00
|
|
|
context "when a regex or `strategy` block is provided" do
|
2020-08-08 07:16:06 +05:30
|
|
|
it "returns an array of usable strategies including PageMatch, sorted in descending order by priority" do
|
2025-02-04 15:48:59 -05:00
|
|
|
expect(strategy.from_url(sourceforge_url, regex_provided: true))
|
2020-08-08 07:16:06 +05:30
|
|
|
.to eq(
|
|
|
|
[Homebrew::Livecheck::Strategy::Sourceforge, Homebrew::Livecheck::Strategy::PageMatch],
|
|
|
|
)
|
|
|
|
end
|
|
|
|
end
|
2025-02-04 15:48:59 -05:00
|
|
|
|
|
|
|
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
|
2020-08-08 07:16:06 +05:30
|
|
|
end
|
2021-08-10 11:09:55 -04:00
|
|
|
|
2025-02-04 10:30:16 -05:00
|
|
|
describe "::post_args" do
|
2025-03-07 20:31:00 -05:00
|
|
|
let(:form_string_content_length) { "Content-Length: #{form_string.length}" }
|
|
|
|
let(:json_string_content_length) { "Content-Length: #{json_string.length}" }
|
|
|
|
|
2025-02-04 10:30:16 -05:00
|
|
|
it "returns an array including `--data` and an encoded form data string" do
|
2025-03-07 20:31:00 -05:00
|
|
|
expect(strategy.post_args(post_form: post_hash))
|
|
|
|
.to eq(["--data", form_string, "--header", form_string_content_length])
|
2025-02-04 10:30:16 -05:00
|
|
|
|
|
|
|
# If both `post_form` and `post_json` are present, only `post_form` will
|
|
|
|
# be used.
|
2025-03-07 20:31:00 -05:00
|
|
|
expect(strategy.post_args(post_form: post_hash, post_json: post_hash))
|
|
|
|
.to eq(["--data", form_string, "--header", form_string_content_length])
|
2025-02-04 10:30:16 -05:00
|
|
|
end
|
|
|
|
|
|
|
|
it "returns an array including `--json` and a JSON string" do
|
2025-03-07 20:31:00 -05:00
|
|
|
expect(strategy.post_args(post_json: post_hash))
|
|
|
|
.to eq(["--json", json_string, "--header", json_string_content_length])
|
2025-02-04 10:30:16 -05:00
|
|
|
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: })
|
|
|
|
|
livecheck: Add Options class
This adds a `Livecheck::Options` class, which is intended to house
various configuration options that are set in `livecheck` blocks,
conditionally set by livecheck at runtime, etc. The general idea is
that when we add features involving configurations options (e.g., for
livecheck, strategies, curl, etc.), we can make changes to `Options`
without needing to modify parameters for strategy `find_versions`
methods, `Strategy` methods like `page_headers` and `page_content`,
etc. This is something that I've been trying to improve over the years
and `Options` should help to reduce maintenance overhead in this area
while also strengthening type signatures.
`Options` replaces the existing `homebrew_curl` option (which related
strategies pass to `Strategy` methods and on to `curl_args`) and the
new `url_options` (which contains `post_form` or `post_json` values
that are used to make `POST` requests). I recently added `url_options`
as a temporary way of enabling `POST` support without `Options` but
this restores the original `Options`-based implementation.
Along the way, I added a `homebrew_curl` parameter to the `url` DSL
method, allowing us to set an explicit value in `livecheck` blocks.
This is something that we've needed in some cases but I also intend
to replace implicit/inferred `homebrew_curl` usage with explicit
values in `livecheck` blocks once this is available for use. My
intention is to eventually remove the implicit behavior and only rely
on explicit values. That will align with how `homebrew_curl` options
work for other URLs and makes the behavior clear just from looking at
the `livecheck` block.
Lastly, this removes the `unused` rest parameter from `find_versions`
methods. I originally added `unused` as a way of handling parameters
that some `find_versions` methods have but others don't (e.g., `cask`
in `ExtractPlist`), as this allowed us to pass various arguments to
`find_versions` methods without worrying about whether a particular
parameter is available. This isn't an ideal solution and I originally
wanted to handle this situation by only passing expected arguments to
`find_versions` methods but there was a technical issue standing in
the way. I recently found an answer to the issue, so this also
replaces the existing `ExtractPlist` special case with generic logic
that checks the parameters for a strategy's `find_versions` method
and only passes expected arguments.
Replacing the aforementioned `find_versions` parameters with `Options`
ensures that the remaining parameters are fairly consistent across
strategies and any differences are handled by the aforementioned
logic. Outside of `ExtractPlist`, the only other difference is that
some `find_versions` methods have a `provided_content` parameter but
that's currently only used by tests (though it's intended for caching
support in the future). I will be renaming that parameter to `content`
in an upcoming PR and expanding it to the other strategies, which
should make them all consistent outside of `ExtractPlist`.
2025-02-11 18:04:38 -05:00
|
|
|
expect(
|
|
|
|
strategy.page_headers(
|
|
|
|
url,
|
|
|
|
options: Homebrew::Livecheck::Options.new(post_form: post_hash),
|
|
|
|
),
|
|
|
|
).to eq([responses.first[:headers]])
|
2025-02-04 10:30:16 -05:00
|
|
|
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])
|
|
|
|
|
livecheck: Add Options class
This adds a `Livecheck::Options` class, which is intended to house
various configuration options that are set in `livecheck` blocks,
conditionally set by livecheck at runtime, etc. The general idea is
that when we add features involving configurations options (e.g., for
livecheck, strategies, curl, etc.), we can make changes to `Options`
without needing to modify parameters for strategy `find_versions`
methods, `Strategy` methods like `page_headers` and `page_content`,
etc. This is something that I've been trying to improve over the years
and `Options` should help to reduce maintenance overhead in this area
while also strengthening type signatures.
`Options` replaces the existing `homebrew_curl` option (which related
strategies pass to `Strategy` methods and on to `curl_args`) and the
new `url_options` (which contains `post_form` or `post_json` values
that are used to make `POST` requests). I recently added `url_options`
as a temporary way of enabling `POST` support without `Options` but
this restores the original `Options`-based implementation.
Along the way, I added a `homebrew_curl` parameter to the `url` DSL
method, allowing us to set an explicit value in `livecheck` blocks.
This is something that we've needed in some cases but I also intend
to replace implicit/inferred `homebrew_curl` usage with explicit
values in `livecheck` blocks once this is available for use. My
intention is to eventually remove the implicit behavior and only rely
on explicit values. That will align with how `homebrew_curl` options
work for other URLs and makes the behavior clear just from looking at
the `livecheck` block.
Lastly, this removes the `unused` rest parameter from `find_versions`
methods. I originally added `unused` as a way of handling parameters
that some `find_versions` methods have but others don't (e.g., `cask`
in `ExtractPlist`), as this allowed us to pass various arguments to
`find_versions` methods without worrying about whether a particular
parameter is available. This isn't an ideal solution and I originally
wanted to handle this situation by only passing expected arguments to
`find_versions` methods but there was a technical issue standing in
the way. I recently found an answer to the issue, so this also
replaces the existing `ExtractPlist` special case with generic logic
that checks the parameters for a strategy's `find_versions` method
and only passes expected arguments.
Replacing the aforementioned `find_versions` parameters with `Options`
ensures that the remaining parameters are fairly consistent across
strategies and any differences are handled by the aforementioned
logic. Outside of `ExtractPlist`, the only other difference is that
some `find_versions` methods have a `provided_content` parameter but
that's currently only used by tests (though it's intended for caching
support in the future). I will be renaming that parameter to `content`
in an upcoming PR and expanding it to the other strategies, which
should make them all consistent outside of `ExtractPlist`.
2025-02-11 18:04:38 -05:00
|
|
|
expect(
|
|
|
|
strategy.page_content(
|
|
|
|
url,
|
|
|
|
options: Homebrew::Livecheck::Options.new(post_form: post_hash),
|
|
|
|
),
|
|
|
|
).to eq({ content: body })
|
2025-02-04 10:30:16 -05:00
|
|
|
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])
|
|
|
|
|
livecheck: Add Options class
This adds a `Livecheck::Options` class, which is intended to house
various configuration options that are set in `livecheck` blocks,
conditionally set by livecheck at runtime, etc. The general idea is
that when we add features involving configurations options (e.g., for
livecheck, strategies, curl, etc.), we can make changes to `Options`
without needing to modify parameters for strategy `find_versions`
methods, `Strategy` methods like `page_headers` and `page_content`,
etc. This is something that I've been trying to improve over the years
and `Options` should help to reduce maintenance overhead in this area
while also strengthening type signatures.
`Options` replaces the existing `homebrew_curl` option (which related
strategies pass to `Strategy` methods and on to `curl_args`) and the
new `url_options` (which contains `post_form` or `post_json` values
that are used to make `POST` requests). I recently added `url_options`
as a temporary way of enabling `POST` support without `Options` but
this restores the original `Options`-based implementation.
Along the way, I added a `homebrew_curl` parameter to the `url` DSL
method, allowing us to set an explicit value in `livecheck` blocks.
This is something that we've needed in some cases but I also intend
to replace implicit/inferred `homebrew_curl` usage with explicit
values in `livecheck` blocks once this is available for use. My
intention is to eventually remove the implicit behavior and only rely
on explicit values. That will align with how `homebrew_curl` options
work for other URLs and makes the behavior clear just from looking at
the `livecheck` block.
Lastly, this removes the `unused` rest parameter from `find_versions`
methods. I originally added `unused` as a way of handling parameters
that some `find_versions` methods have but others don't (e.g., `cask`
in `ExtractPlist`), as this allowed us to pass various arguments to
`find_versions` methods without worrying about whether a particular
parameter is available. This isn't an ideal solution and I originally
wanted to handle this situation by only passing expected arguments to
`find_versions` methods but there was a technical issue standing in
the way. I recently found an answer to the issue, so this also
replaces the existing `ExtractPlist` special case with generic logic
that checks the parameters for a strategy's `find_versions` method
and only passes expected arguments.
Replacing the aforementioned `find_versions` parameters with `Options`
ensures that the remaining parameters are fairly consistent across
strategies and any differences are handled by the aforementioned
logic. Outside of `ExtractPlist`, the only other difference is that
some `find_versions` methods have a `provided_content` parameter but
that's currently only used by tests (though it's intended for caching
support in the future). I will be renaming that parameter to `content`
in an upcoming PR and expanding it to the other strategies, which
should make them all consistent outside of `ExtractPlist`.
2025-02-11 18:04:38 -05:00
|
|
|
expect(
|
|
|
|
strategy.page_content(
|
|
|
|
url,
|
|
|
|
options: Homebrew::Livecheck::Options.new(post_json: post_hash),
|
|
|
|
),
|
|
|
|
).to eq({ content: body })
|
2025-02-04 10:30:16 -05:00
|
|
|
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
|
|
|
|
|
2021-08-10 11:09:55 -04:00
|
|
|
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
|
2020-08-08 07:16:06 +05:30
|
|
|
end
|