# 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(: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
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!: Example v1.2.3
The current legacy version is: Example v0.1.2
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 let(:form_string_content_length) { "Content-Length: #{form_string.length}" } let(:json_string_content_length) { "Content-Length: #{json_string.length}" } 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, "--header", form_string_content_length]) # 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, "--header", form_string_content_length]) 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, "--header", json_string_content_length]) 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, options: Homebrew::Livecheck::Options.new(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, options: Homebrew::Livecheck::Options.new(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, options: Homebrew::Livecheck::Options.new(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