From 2f99d296937030103ff6b7d0ed242de6e6170a54 Mon Sep 17 00:00:00 2001 From: phinze Date: Sun, 18 Nov 2012 23:12:21 -0600 Subject: [PATCH] brew cask audit for a given cask: - checks required fields - checks URL responds successfully - checks content_length specified --- lib/cask/audit.rb | 128 ++++++++++++++++++++++++++++++++++++++++ lib/cask/auditor.rb | 7 +++ lib/cask/cli/audit.rb | 12 ++++ lib/cask/dsl.rb | 6 ++ test/cask/audit_test.rb | 78 ++++++++++++++++++++++++ 5 files changed, 231 insertions(+) create mode 100644 lib/cask/audit.rb create mode 100644 lib/cask/auditor.rb create mode 100644 lib/cask/cli/audit.rb create mode 100644 test/cask/audit_test.rb diff --git a/lib/cask/audit.rb b/lib/cask/audit.rb new file mode 100644 index 000000000..185f8be13 --- /dev/null +++ b/lib/cask/audit.rb @@ -0,0 +1,128 @@ +class Cask::Audit + attr_reader :cask, :errors, :warnings, :headers, :response_status + + def initialize(cask) + @cask = cask + @errors = [] + @warnings = [] + @headers = {} + end + + def run! + _check_required_fields + return if errors? + _get_data_from_request + return if errors? + _check_response_status + return if errors? + _check_content_length + end + + def add_error(message) + @errors << message + end + + def add_warning(message) + @warnings << message + end + + def errors? + !@errors.empty? + end + + def warnings? + !@warnings.empty? + end + + def result + if errors? + "#{Tty.red}failed#{Tty.reset}" + elsif warnings? + "#{Tty.yellow}warning#{Tty.reset}" + else + "#{Tty.green}passed#{Tty.reset}" + end + end + + def summary + summary = ["audit for #{cask}: #{result}"] + + @errors.each do |error| + summary << " #{Tty.red}-#{Tty.reset} #{error}" + end + + @warnings.each do |warning| + summary << " #{Tty.yellow}-#{Tty.reset} #{warning}" + end + + summary.join("\n") + end + + def _check_required_fields + add_error "url is required" unless cask.url + add_error "version is required" unless cask.version + add_error "homepage is required" unless cask.homepage + end + + http_responses = [ + 'HTTP/1.0 200 OK', + 'HTTP/1.1 200 OK' + ] + + OK_RESPONSES = { + 'http' => http_responses, + 'https' => http_responses, + 'ftp' => [ 'OK' ] + } + + def _check_response_status + ok = OK_RESPONSES[cask.url.scheme] + unless ok.include?(@response_status) + add_error "unexpected http response, expecting #{ok.map(&:inspect).join(' or ')}, got #{@response_status.inspect}" + end + end + + def _check_content_length + remote_content_length = @headers['Content-Length'] + if cask.content_length.nil? + add_warning "specify content_length so we can check against URL, currently: content_length '#{remote_content_length}'" + else + unless cask.content_length == remote_content_length + add_warning "unexpected content_length for #{cask}; specified #{cask.content_length.inspect}, but got #{remote_content_length.inspect}" + end + end + end + + def _get_data_from_request + response = _curl(cask.url) + + if response.empty? + add_error "timeout while requesting #{cask.url}" + return + end + + response_lines = response.split("\n").map(&:chomp) + + case cask.url.scheme + when 'http', 'https' then + @response_status = response_lines.grep(/^HTTP/).last + http_headers = response_lines[(response_lines.index(@response_status)+1)..-1] + http_headers.each { |line| + header_name, header_value = line.split(': ') + @headers[header_name] = header_value + } + when 'ftp' then + @response_status = 'OK' + response_lines.each { |line| + header_name, header_value = line.split(': ') + @headers[header_name] = header_value + } + else + add_error "unknown scheme for #{cask.url}" + end + end + + def _curl(url) + `curl --max-time 5 --silent --location --head '#{url}'` + end +end diff --git a/lib/cask/auditor.rb b/lib/cask/auditor.rb new file mode 100644 index 000000000..1fe2dae6c --- /dev/null +++ b/lib/cask/auditor.rb @@ -0,0 +1,7 @@ +class Cask::Auditor + def self.audit(cask) + audit = Cask::Audit.new(cask) + audit.run! + puts audit.summary + end +end diff --git a/lib/cask/cli/audit.rb b/lib/cask/cli/audit.rb new file mode 100644 index 000000000..4839e54b7 --- /dev/null +++ b/lib/cask/cli/audit.rb @@ -0,0 +1,12 @@ +class Cask::CLI::Audit + def self.run(*args) + casks_to_audit = args.empty? ? Cask.all : args.map { |arg| Cask.load(arg) } + casks_to_audit.each do |cask| + Cask::Auditor.audit(cask) + end + end + + def self.help + "verifies installability of casks" + end +end diff --git a/lib/cask/dsl.rb b/lib/cask/dsl.rb index e48b91b2a..cfa4c3c3a 100644 --- a/lib/cask/dsl.rb +++ b/lib/cask/dsl.rb @@ -3,6 +3,8 @@ module Cask::DSL base.extend(ClassMethods) end + def content_length; self.class.content_length; end + def homepage; self.class.homepage; end def url; self.class.url; end @@ -10,6 +12,10 @@ module Cask::DSL def version; self.class.version; end module ClassMethods + def content_length(content_length=nil) + @content_length ||= content_length + end + def homepage(homepage=nil) @homepage ||= homepage end diff --git a/test/cask/audit_test.rb b/test/cask/audit_test.rb new file mode 100644 index 000000000..36d132cd7 --- /dev/null +++ b/test/cask/audit_test.rb @@ -0,0 +1,78 @@ +require 'test_helper' + +describe Cask::Audit do + describe "result" do + it "is 'failed' if there are have been any errors added" do + audit = Cask::Audit.new(mock()) + audit.add_error 'bad' + audit.add_warning 'eh' + audit.result.must_match /failed/ + end + + it "is 'warning' if there are no errors, but there are warnings" do + audit = Cask::Audit.new(mock()) + audit.add_warning 'eh' + audit.result.must_match /warning/ + end + + it "is 'passed' if there are no errors or warning" do + audit = Cask::Audit.new(mock()) + audit.result.must_match /passed/ + end + end + + describe "run!" do + describe "required fields" do + it "adds an error if url is missing" do + audit = Cask::Audit.new(stub(:url => nil, :version => 'something', :homepage => 'something')) + audit.run! + audit.errors.must_include 'url is required' + end + + it "adds an error if version is missing" do + audit = Cask::Audit.new(stub(:url => 'something', :version => nil, :homepage => 'something')) + audit.run! + audit.errors.must_include 'version is required' + end + + it "adds an error if homepage is missing" do + audit = Cask::Audit.new(stub(:url => 'something', :version => 'something', :homepage => nil)) + audit.run! + audit.errors.must_include 'homepage is required' + end + end + + describe "request processing" do + it "adds an error if response is empty" do + audit = Cask::Audit.new(stub(:url => 'something', :version => 'something', :homepage => 'something')) + audit.stubs(:_curl).returns('') + audit.run! + audit.errors.must_include 'timeout while requesting something' + end + + it "properly populates the response code and headers from an http response" do + audit = Cask::Audit.new(stub( + :url => URI('http://something/file.zip'), + :version => 'something', + :homepage => 'something', + :content_length => '123' + )) + audit.stubs(:_curl).returns(<<-RESPONSE.gsub(/^ /, '')) + HTTP/1.1 200 OK + Content-Type: application/x-apple-diskimage + ETag: "b4208f3e84967be4b078ecaa03fba941" + Content-Length: 23726161 + Last-Modified: Sun, 12 Aug 2012 21:17:21 GMT + RESPONSE + audit.run! + audit.response_status.must_equal 'HTTP/1.1 200 OK' + audit.headers.must_equal({ + 'Content-Type' => 'application/x-apple-diskimage', + 'ETag' => '"b4208f3e84967be4b078ecaa03fba941"', + 'Content-Length' => '23726161', + 'Last-Modified' => 'Sun, 12 Aug 2012 21:17:21 GMT' + }) + end + end + end +end