Merge pull request #19233 from Homebrew/livecheck/add-post-support
livecheck: Add support for POST requests
This commit is contained in:
commit
743971bcc4
@ -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
|
||||
|
||||
@ -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?
|
||||
|
||||
@ -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?,
|
||||
|
||||
9
Library/Homebrew/livecheck/strategy.rbi
Normal file
9
Library/Homebrew/livecheck/strategy.rbi
Normal file
@ -0,0 +1,9 @@
|
||||
# typed: strict
|
||||
|
||||
module Homebrew
|
||||
module Livecheck
|
||||
module Strategy
|
||||
include Kernel
|
||||
end
|
||||
end
|
||||
end
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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?
|
||||
|
||||
@ -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?
|
||||
|
||||
@ -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?
|
||||
|
||||
|
||||
@ -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?
|
||||
|
||||
@ -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?
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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`.
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user