Merge pull request #19233 from Homebrew/livecheck/add-post-support

livecheck: Add support for POST requests
This commit is contained in:
Mike McQuaid 2025-02-07 19:31:49 +00:00 committed by GitHub
commit 743971bcc4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 461 additions and 54 deletions

View File

@ -20,6 +20,14 @@ class Livecheck
sig { returns(T.nilable(String)) }
attr_reader :skip_msg
# A block used by strategies to identify version information.
sig { returns(T.nilable(Proc)) }
attr_reader :strategy_block
# Options used by `Strategy` methods to modify `curl` behavior.
sig { returns(T.nilable(T::Hash[Symbol, T.untyped])) }
attr_reader :url_options
sig { params(package_or_resource: T.any(Cask::Cask, T.class_of(Formula), Resource)).void }
def initialize(package_or_resource)
@package_or_resource = package_or_resource
@ -32,6 +40,7 @@ class Livecheck
@strategy_block = T.let(nil, T.nilable(Proc))
@throttle = T.let(nil, T.nilable(Integer))
@url = T.let(nil, T.any(NilClass, String, Symbol))
@url_options = T.let(nil, T.nilable(T::Hash[Symbol, T.untyped]))
end
# Sets the `@referenced_cask_name` instance variable to the provided `String`
@ -134,9 +143,6 @@ class Livecheck
end
end
sig { returns(T.nilable(Proc)) }
attr_reader :strategy_block
# Sets the `@throttle` instance variable to the provided `Integer` or returns
# the `@throttle` instance variable when no argument is provided.
sig {
@ -158,13 +164,22 @@ class Livecheck
# `@url` instance variable when no argument is provided. The argument can be
# a `String` (a URL) or a supported `Symbol` corresponding to a URL in the
# formula/cask/resource (e.g. `:stable`, `:homepage`, `:head`, `:url`).
# Any options provided to the method are passed through to `Strategy` methods
# (`page_headers`, `page_content`).
sig {
params(
# URL to check for version information.
url: T.any(String, Symbol),
url: T.any(String, Symbol),
post_form: T.nilable(T::Hash[T.any(String, Symbol), String]),
post_json: T.nilable(T::Hash[T.any(String, Symbol), String]),
).returns(T.nilable(T.any(String, Symbol)))
}
def url(url = T.unsafe(nil))
def url(url = T.unsafe(nil), post_form: nil, post_json: nil)
raise ArgumentError, "Only use `post_form` or `post_json`, not both" if post_form && post_json
options = { post_form:, post_json: }.compact
@url_options = options if options.present?
case url
when nil
@url
@ -183,14 +198,15 @@ class Livecheck
sig { returns(T::Hash[String, T.untyped]) }
def to_hash
{
"cask" => @referenced_cask_name,
"formula" => @referenced_formula_name,
"regex" => @regex,
"skip" => @skip,
"skip_msg" => @skip_msg,
"strategy" => @strategy,
"throttle" => @throttle,
"url" => @url,
"cask" => @referenced_cask_name,
"formula" => @referenced_formula_name,
"regex" => @regex,
"skip" => @skip,
"skip_msg" => @skip_msg,
"strategy" => @strategy,
"throttle" => @throttle,
"url" => @url,
"url_options" => @url_options,
}
end
end

View File

@ -614,6 +614,7 @@ module Homebrew
referenced_livecheck = referenced_formula_or_cask&.livecheck
livecheck_url = livecheck.url || referenced_livecheck&.url
livecheck_url_options = livecheck.url_options || referenced_livecheck&.url_options
livecheck_regex = livecheck.regex || referenced_livecheck&.regex
livecheck_strategy = livecheck.strategy || referenced_livecheck&.strategy
livecheck_strategy_block = livecheck.strategy_block || referenced_livecheck&.strategy_block
@ -673,6 +674,7 @@ module Homebrew
elsif original_url.present? && original_url != "None"
puts "URL: #{original_url}"
end
puts "URL Options: #{livecheck_url_options}" if livecheck_url_options.present?
puts "URL (processed): #{url}" if url != original_url
if strategies.present? && verbose
puts "Strategies: #{strategies.map { |s| livecheck_strategy_names[s] }.join(", ")}"
@ -701,6 +703,7 @@ module Homebrew
strategy_args = {
regex: livecheck_regex,
url_options: livecheck_url_options,
homebrew_curl:,
}
# TODO: Set `cask`/`url` args based on the presence of the keyword arg
@ -807,6 +810,7 @@ module Homebrew
version_info[:meta][:url][:strategy] = strategy_data[:url]
end
version_info[:meta][:url][:final] = strategy_data[:final_url] if strategy_data[:final_url]
version_info[:meta][:url][:options] = livecheck_url_options if livecheck_url_options.present?
version_info[:meta][:url][:homebrew_curl] = homebrew_curl if homebrew_curl.present?
end
version_info[:meta][:strategy] = strategy_name if strategy.present?
@ -856,6 +860,7 @@ module Homebrew
livecheck = resource.livecheck
livecheck_reference = livecheck.formula
livecheck_url = livecheck.url
livecheck_url_options = livecheck.url_options
livecheck_regex = livecheck.regex
livecheck_strategy = livecheck.strategy
livecheck_strategy_block = livecheck.strategy_block
@ -893,6 +898,7 @@ module Homebrew
elsif original_url.present? && original_url != "None"
puts "URL: #{original_url}"
end
puts "URL Options: #{livecheck_url_options}" if livecheck_url_options.present?
puts "URL (processed): #{url}" if url != original_url
if strategies.present? && verbose
puts "Strategies: #{strategies.map { |s| livecheck_strategy_names[s] }.join(", ")}"
@ -923,6 +929,7 @@ module Homebrew
strategy_args = {
url:,
regex: livecheck_regex,
url_options: livecheck_url_options,
homebrew_curl: false,
}.compact
@ -1012,6 +1019,7 @@ module Homebrew
resource_version_info[:meta][:url][:strategy] = strategy_data[:url]
end
resource_version_info[:meta][:url][:final] = strategy_data[:final_url] if strategy_data&.dig(:final_url)
resource_version_info[:meta][:url][:options] = livecheck_url_options if livecheck_url_options.present?
end
resource_version_info[:meta][:strategy] = strategy_name if strategy.present?
if strategies.present?

View File

@ -166,20 +166,59 @@ module Homebrew
end
end
# Creates `curl` `--data` or `--json` arguments (for `POST` requests`)
# from related `livecheck` block `url` options.
#
# @param post_form [Hash, nil] data to encode using `URI::encode_www_form`
# @param post_json [Hash, nil] data to encode using `JSON::generate`
# @return [Array]
sig {
params(
post_form: T.nilable(T::Hash[T.any(String, Symbol), String]),
post_json: T.nilable(T::Hash[T.any(String, Symbol), String]),
).returns(T::Array[String])
}
def post_args(post_form: nil, post_json: nil)
if post_form.present?
require "uri"
["--data", URI.encode_www_form(post_form)]
elsif post_json.present?
require "json"
["--json", JSON.generate(post_json)]
else
[]
end
end
# Collects HTTP response headers, starting with the provided URL.
# Redirections will be followed and all the response headers are
# collected into an array of hashes.
#
# @param url [String] the URL to fetch
# @param url_options [Hash] options to modify curl behavior
# @param homebrew_curl [Boolean] whether to use brewed curl with the URL
# @return [Array]
sig { params(url: String, homebrew_curl: T::Boolean).returns(T::Array[T::Hash[String, String]]) }
def self.page_headers(url, homebrew_curl: false)
sig {
params(
url: String,
url_options: T::Hash[Symbol, T.untyped],
homebrew_curl: T::Boolean,
).returns(T::Array[T::Hash[String, String]])
}
def self.page_headers(url, url_options: {}, homebrew_curl: false)
headers = []
if url_options[:post_form].present? || url_options[:post_json].present?
curl_post_args = ["--request", "POST", *post_args(
post_form: url_options[:post_form],
post_json: url_options[:post_json],
)]
end
[:default, :browser].each do |user_agent|
begin
parsed_output = curl_headers(
*curl_post_args,
"--max-redirs",
MAX_REDIRECTIONS.to_s,
url,
@ -205,13 +244,28 @@ module Homebrew
# array with the error message instead.
#
# @param url [String] the URL of the content to check
# @param url_options [Hash] options to modify curl behavior
# @param homebrew_curl [Boolean] whether to use brewed curl with the URL
# @return [Hash]
sig { params(url: String, homebrew_curl: T::Boolean).returns(T::Hash[Symbol, T.untyped]) }
def self.page_content(url, homebrew_curl: false)
sig {
params(
url: String,
url_options: T::Hash[Symbol, T.untyped],
homebrew_curl: T::Boolean,
).returns(T::Hash[Symbol, T.untyped])
}
def self.page_content(url, url_options: {}, homebrew_curl: false)
if url_options[:post_form].present? || url_options[:post_json].present?
curl_post_args = ["--request", "POST", *post_args(
post_form: url_options[:post_form],
post_json: url_options[:post_json],
)]
end
stderr = T.let(nil, T.nilable(String))
[:default, :browser].each do |user_agent|
stdout, stderr, status = curl_output(
*curl_post_args,
*PAGE_CONTENT_CURL_ARGS, url,
**DEFAULT_CURL_OPTIONS,
use_homebrew_curl: homebrew_curl || !curl_supports_fail_with_body?,

View File

@ -0,0 +1,9 @@
# typed: strict
module Homebrew
module Livecheck
module Strategy
include Kernel
end
end
end

View File

@ -81,11 +81,11 @@ module Homebrew
regex: T.nilable(Regexp),
provided_content: T.nilable(String),
homebrew_curl: T::Boolean,
_unused: T.untyped,
unused: T.untyped,
block: T.nilable(Proc),
).returns(T::Hash[Symbol, T.untyped])
}
def self.find_versions(url:, regex: nil, provided_content: nil, homebrew_curl: false, **_unused, &block)
def self.find_versions(url:, regex: nil, provided_content: nil, homebrew_curl: false, **unused, &block)
match_data = { matches: {}, regex:, url: }
match_data[:cached] = true if provided_content.is_a?(String)
@ -97,7 +97,13 @@ module Homebrew
content = if provided_content
provided_content
else
match_data.merge!(Strategy.page_content(match_data[:url], homebrew_curl:))
match_data.merge!(
Strategy.page_content(
match_data[:url],
url_options: unused.fetch(:url_options, {}),
homebrew_curl:,
),
)
match_data[:content]
end
return match_data unless content

View File

@ -74,14 +74,18 @@ module Homebrew
url: String,
regex: T.nilable(Regexp),
homebrew_curl: T::Boolean,
_unused: T.untyped,
unused: T.untyped,
block: T.nilable(Proc),
).returns(T::Hash[Symbol, T.untyped])
}
def self.find_versions(url:, regex: nil, homebrew_curl: false, **_unused, &block)
def self.find_versions(url:, regex: nil, homebrew_curl: false, **unused, &block)
match_data = { matches: {}, regex:, url: }
headers = Strategy.page_headers(url, homebrew_curl:)
headers = Strategy.page_headers(
url,
url_options: unused.fetch(:url_options, {}),
homebrew_curl:,
)
# Merge the headers from all responses into one hash
merged_headers = headers.reduce(&:merge)

View File

@ -102,11 +102,11 @@ module Homebrew
regex: T.nilable(Regexp),
provided_content: T.nilable(String),
homebrew_curl: T::Boolean,
_unused: T.untyped,
unused: T.untyped,
block: T.nilable(Proc),
).returns(T::Hash[Symbol, T.untyped])
}
def self.find_versions(url:, regex: nil, provided_content: nil, homebrew_curl: false, **_unused, &block)
def self.find_versions(url:, regex: nil, provided_content: nil, homebrew_curl: false, **unused, &block)
raise ArgumentError, "#{Utils.demodulize(T.must(name))} requires a `strategy` block" if block.blank?
match_data = { matches: {}, regex:, url: }
@ -116,7 +116,13 @@ module Homebrew
match_data[:cached] = true
provided_content
else
match_data.merge!(Strategy.page_content(url, homebrew_curl:))
match_data.merge!(
Strategy.page_content(
url,
url_options: unused.fetch(:url_options, {}),
homebrew_curl:,
),
)
match_data[:content]
end
return match_data if content.blank?

View File

@ -85,11 +85,11 @@ module Homebrew
regex: T.nilable(Regexp),
provided_content: T.nilable(String),
homebrew_curl: T::Boolean,
_unused: T.untyped,
unused: T.untyped,
block: T.nilable(Proc),
).returns(T::Hash[Symbol, T.untyped])
}
def self.find_versions(url:, regex: nil, provided_content: nil, homebrew_curl: false, **_unused, &block)
def self.find_versions(url:, regex: nil, provided_content: nil, homebrew_curl: false, **unused, &block)
if regex.blank? && block.blank?
raise ArgumentError, "#{Utils.demodulize(T.must(name))} requires a regex or `strategy` block"
end
@ -101,7 +101,13 @@ module Homebrew
match_data[:cached] = true
provided_content
else
match_data.merge!(Strategy.page_content(url, homebrew_curl:))
match_data.merge!(
Strategy.page_content(
url,
url_options: unused.fetch(:url_options, {}),
homebrew_curl:,
),
)
match_data[:content]
end
return match_data if content.blank?

View File

@ -214,16 +214,18 @@ module Homebrew
#
# @param url [String] the URL of the content to check
# @param regex [Regexp, nil] a regex for use in a strategy block
# @param homebrew_curl [Boolean] whether to use brewed curl with the URL
# @return [Hash]
sig {
params(
url: String,
regex: T.nilable(Regexp),
_unused: T.untyped,
block: T.nilable(Proc),
url: String,
regex: T.nilable(Regexp),
homebrew_curl: T::Boolean,
unused: T.untyped,
block: T.nilable(Proc),
).returns(T::Hash[Symbol, T.untyped])
}
def self.find_versions(url:, regex: nil, **_unused, &block)
def self.find_versions(url:, regex: nil, homebrew_curl: false, **unused, &block)
if regex.present? && block.blank?
raise ArgumentError,
"#{Utils.demodulize(T.must(name))} only supports a regex when using a `strategy` block"
@ -231,7 +233,13 @@ module Homebrew
match_data = { matches: {}, regex:, url: }
match_data.merge!(Strategy.page_content(url))
match_data.merge!(
Strategy.page_content(
url,
url_options: unused.fetch(:url_options, {}),
homebrew_curl:,
),
)
content = match_data.delete(:content)
return match_data if content.blank?

View File

@ -142,11 +142,11 @@ module Homebrew
regex: T.nilable(Regexp),
provided_content: T.nilable(String),
homebrew_curl: T::Boolean,
_unused: T.untyped,
unused: T.untyped,
block: T.nilable(Proc),
).returns(T::Hash[Symbol, T.untyped])
}
def self.find_versions(url:, regex: nil, provided_content: nil, homebrew_curl: false, **_unused, &block)
def self.find_versions(url:, regex: nil, provided_content: nil, homebrew_curl: false, **unused, &block)
raise ArgumentError, "#{Utils.demodulize(T.must(name))} requires a `strategy` block" if block.blank?
match_data = { matches: {}, regex:, url: }
@ -156,7 +156,13 @@ module Homebrew
match_data[:cached] = true
provided_content
else
match_data.merge!(Strategy.page_content(url, homebrew_curl:))
match_data.merge!(
Strategy.page_content(
url,
url_options: unused.fetch(:url_options, {}),
homebrew_curl:,
),
)
match_data[:content]
end
return match_data if content.blank?

View File

@ -102,11 +102,11 @@ module Homebrew
regex: T.nilable(Regexp),
provided_content: T.nilable(String),
homebrew_curl: T::Boolean,
_unused: T.untyped,
unused: T.untyped,
block: T.nilable(Proc),
).returns(T::Hash[Symbol, T.untyped])
}
def self.find_versions(url:, regex: nil, provided_content: nil, homebrew_curl: false, **_unused, &block)
def self.find_versions(url:, regex: nil, provided_content: nil, homebrew_curl: false, **unused, &block)
raise ArgumentError, "#{Utils.demodulize(T.must(name))} requires a `strategy` block" if block.blank?
match_data = { matches: {}, regex:, url: }
@ -116,7 +116,13 @@ module Homebrew
match_data[:cached] = true
provided_content
else
match_data.merge!(Strategy.page_content(url, homebrew_curl:))
match_data.merge!(
Strategy.page_content(
url,
url_options: unused.fetch(:url_options, {}),
homebrew_curl:,
),
)
match_data[:content]
end
return match_data if content.blank?

View File

@ -5,29 +5,257 @@ 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(:url) { "https://sourceforge.net/projects/test" }
let(:sourceforge_url) { "https://sourceforge.net/projects/test" }
context "when no regex is provided" do
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(url)).to eq([Homebrew::Livecheck::Strategy::Sourceforge])
expect(strategy.from_url(sourceforge_url)).to eq([Homebrew::Livecheck::Strategy::Sourceforge])
end
end
context "when a regex is provided" do
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(url, regex_provided: true))
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

View File

@ -27,6 +27,15 @@ RSpec.describe Livecheck do
end
let(:livecheck_c) { described_class.new(c) }
let(:post_hash) do
{
"empty" => "",
"boolean" => "true",
"number" => "1",
"string" => "a + b = c",
}
end
describe "#formula" do
it "returns nil if not set" do
expect(livecheck_f.formula).to be_nil
@ -90,13 +99,23 @@ RSpec.describe Livecheck do
end
describe "#strategy" do
block = proc { |page, regex| page.scan(regex).map { |match| match[0].tr("_", ".") } }
it "returns nil if not set" do
expect(livecheck_f.strategy).to be_nil
expect(livecheck_f.strategy_block).to be_nil
end
it "returns the Symbol if set" do
livecheck_f.strategy(:page_match)
expect(livecheck_f.strategy).to eq(:page_match)
expect(livecheck_f.strategy_block).to be_nil
end
it "sets `strategy_block` when provided" do
livecheck_f.strategy(:page_match, &block)
expect(livecheck_f.strategy).to eq(:page_match)
expect(livecheck_f.strategy_block).to eq(block)
end
end
@ -137,25 +156,38 @@ RSpec.describe Livecheck do
expect(livecheck_c.url).to eq(:url)
end
it "sets `url_options` when provided" do
post_args = { post_form: post_hash }
livecheck_f.url(url_string, **post_args)
expect(livecheck_f.url_options).to eq(post_args)
end
it "raises an ArgumentError if the argument isn't a valid Symbol" do
expect do
livecheck_f.url(:not_a_valid_symbol)
end.to raise_error ArgumentError
end
it "raises an ArgumentError if both `post_form` and `post_json` arguments are provided" do
expect do
livecheck_f.url(:stable, post_form: post_hash, post_json: post_hash)
end.to raise_error ArgumentError
end
end
describe "#to_hash" do
it "returns a Hash of all instance variables" do
expect(livecheck_f.to_hash).to eq(
{
"cask" => nil,
"formula" => nil,
"regex" => nil,
"skip" => false,
"skip_msg" => nil,
"strategy" => nil,
"throttle" => nil,
"url" => nil,
"cask" => nil,
"formula" => nil,
"regex" => nil,
"skip" => false,
"skip_msg" => nil,
"strategy" => nil,
"throttle" => nil,
"url" => nil,
"url_options" => nil,
},
)
end

View File

@ -112,6 +112,24 @@ end
The referenced formula/cask should be in the same tap, as a reference to a formula/cask from another tap will generate an error if the user doesn't already have it tapped.
### `POST` requests
Some checks require making a `POST` request and that can be accomplished by adding a `post_form` or `post_json` option to a `livecheck` block `url`.
```ruby
livecheck do
url "https://example.com/download.php", post_form: {
"Name" => "",
"E-mail" => "",
}
regex(/href=.*?example[._-]v?(\d+(?:\.\d+)+)\.t/i)
end
```
`post_form` is used for form data and `post_json` is used for JSON data. livecheck will encode the provided hash value to the appropriate format before making the request.
`POST` support only applies to strategies that use `Strategy::page_headers` or `::page_content` (directly or indirectly), so it does not apply to `ExtractPlist`, `Git`, `GithubLatest`, `GithubReleases`, etc.
### `strategy` blocks
If the upstream version format needs to be manipulated to match the formula/cask format, a `strategy` block can be used instead of a `regex`.