loading
Generated 2023-04-02T15:48:28-04:00

All Files ( 97.64% covered at 22.02 hits/line )

29 files in total.
677 relevant lines, 661 lines covered and 16 lines missed. ( 97.64% )
File % covered Lines Relevant Lines Lines covered Lines missed Avg. Hits / Line
lib/ruby-link-checker.rb 100.00 % 13 11 11 0 1.00
lib/ruby-link-checker/callbacks.rb 88.46 % 50 26 23 3 197.54
lib/ruby-link-checker/checker.rb 100.00 % 42 22 22 0 49.77
lib/ruby-link-checker/config.rb 100.00 % 41 19 19 0 29.68
lib/ruby-link-checker/errors.rb 100.00 % 2 2 2 0 1.00
lib/ruby-link-checker/errors/base_error.rb 100.00 % 8 3 3 0 1.00
lib/ruby-link-checker/errors/redirect_loop_error.rb 88.89 % 18 9 8 1 1.56
lib/ruby-link-checker/logger.rb 100.00 % 14 8 8 0 15.63
lib/ruby-link-checker/net/http.rb 100.00 % 3 3 3 0 1.00
lib/ruby-link-checker/net/http/checker.rb 100.00 % 31 20 20 0 21.65
lib/ruby-link-checker/net/http/config.rb 93.75 % 35 16 15 1 4.31
lib/ruby-link-checker/net/http/result.rb 96.43 % 53 28 27 1 35.29
lib/ruby-link-checker/result.rb 94.87 % 74 39 37 2 22.41
lib/ruby-link-checker/task.rb 92.31 % 20 13 12 1 37.38
lib/ruby-link-checker/tasks.rb 98.46 % 99 65 64 1 35.25
lib/ruby-link-checker/typhoeus/hydra.rb 100.00 % 3 3 3 0 1.00
lib/ruby-link-checker/typhoeus/hydra/checker.rb 96.00 % 52 25 24 1 17.04
lib/ruby-link-checker/typhoeus/hydra/config.rb 93.75 % 35 16 15 1 0.94
lib/ruby-link-checker/typhoeus/hydra/result.rb 96.43 % 53 28 27 1 36.11
spec/ruby-link-checker/checker_spec.rb 100.00 % 10 5 5 0 1.20
spec/ruby-link-checker/config_spec.rb 100.00 % 39 21 21 0 1.05
spec/ruby-link-checker/net/http/checker_spec.rb 100.00 % 61 33 33 0 1.94
spec/ruby-link-checker/typhoeus/hydra/checker_spec.rb 100.00 % 67 35 35 0 2.49
spec/ruby-link-checker/version_spec.rb 100.00 % 9 4 4 0 1.00
spec/support/config.rb 100.00 % 8 5 5 0 26.20
spec/support/link_checker.rb 98.97 % 322 194 192 2 2.69
spec/support/vcr.rb 85.71 % 13 7 6 1 0.86
spec/support/with_result.rb 100.00 % 15 9 9 0 52.78
spec/support/with_url.rb 100.00 % 14 8 8 0 5.38

lib/ruby-link-checker.rb

100.0% lines covered

11 relevant lines. 11 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require_relative 'ruby-link-checker/version'
  3. 1 require_relative 'ruby-link-checker/errors'
  4. 1 require_relative 'ruby-link-checker/config'
  5. 1 require_relative 'ruby-link-checker/callbacks'
  6. 1 require_relative 'ruby-link-checker/logger'
  7. 1 require_relative 'ruby-link-checker/task'
  8. 1 require_relative 'ruby-link-checker/tasks'
  9. 1 require_relative 'ruby-link-checker/checker'
  10. 1 require_relative 'ruby-link-checker/result'
  11. 1 require_relative 'ruby-link-checker/net/http'
  12. 1 require_relative 'ruby-link-checker/typhoeus/hydra'

lib/ruby-link-checker/callbacks.rb

88.46% lines covered

26 relevant lines. 23 lines covered and 3 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 module LinkChecker
  3. 1 module Callbacks
  4. 1 def callbacks
  5. 888 @callbacks ||= Hash.new { |h, k| h[k] = [] }
  6. end
  7. 1 def delegates
  8. 525 @delegates ||= []
  9. end
  10. 1 def on(*events, &block)
  11. 238 if events && Array(events).any?
  12. 134 Array(events).each do |event|
  13. 135 callbacks[event.to_s] << block
  14. end
  15. else
  16. 104 delegates << block
  17. end
  18. end
  19. 1 def method_missing(m, *args, &block)
  20. 246 if m.to_s[-1] == '!'
  21. 246 callback(m.to_s[...-1].to_sym, *args)
  22. else
  23. super
  24. end
  25. end
  26. 1 private
  27. 1 def callback(event, *data)
  28. 421 delegates.each do |c|
  29. 336 c.call(event, *data)
  30. end
  31. 421 callbacks = self.callbacks[event.to_s]
  32. 421 return false unless callbacks
  33. 421 callbacks.each do |c|
  34. 171 c.call(*data)
  35. end
  36. 421 true
  37. rescue StandardError => e
  38. logger.error("#{self}##{__method__}") { e }
  39. false
  40. end
  41. end
  42. end

lib/ruby-link-checker/checker.rb

100.0% lines covered

22 relevant lines. 22 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 module LinkChecker
  3. 1 class Checker
  4. 1 include LinkChecker::Config
  5. 1 include LinkChecker::Callbacks
  6. 1 attr_reader :results
  7. 1 attr_accessor(*Config::ATTRIBUTES)
  8. 1 def initialize(options = {})
  9. 55 LinkChecker::Config::ATTRIBUTES.each do |key|
  10. 220 send("#{key}=", options[key] || LinkChecker.config.send(key))
  11. end
  12. 55 raise ArgumentError, "Missing methods." if methods&.none?
  13. 54 @logger ||= options[:logger] || LinkChecker::Config.logger || LinkChecker::Logger.default
  14. 54 @results = { error: [], failure: [], success: [] } unless options.key?(:results) && !options[:results]
  15. end
  16. 1 def task_klass
  17. 52 @task_klass ||= begin
  18. 52 module_name = self.class.name.split("::")[...-1].join('::')
  19. 52 Object.const_get("#{module_name}::Task")
  20. end
  21. end
  22. 1 def check(uri, options = {})
  23. 52 tasks = Tasks.new(
  24. self,
  25. task_klass,
  26. uri,
  27. methods,
  28. options
  29. )
  30. 52 tasks.on do |event, *args|
  31. 168 results[event] << args.first if @results && %i[error failure success].include?(event)
  32. 168 callback event, *args
  33. end
  34. 52 tasks.execute!
  35. end
  36. end
  37. end

lib/ruby-link-checker/config.rb

100.0% lines covered

19 relevant lines. 19 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 module LinkChecker
  3. 1 module Config
  4. 1 extend self
  5. 1 ATTRIBUTES = %i[
  6. methods
  7. user_agent
  8. logger
  9. retries
  10. ].freeze
  11. 1 attr_accessor(*Config::ATTRIBUTES)
  12. 1 def reset
  13. 65 self.methods = %w[HEAD GET]
  14. 65 self.user_agent = "Ruby Link Checker/#{LinkChecker::VERSION}"
  15. 65 self.logger = nil
  16. 65 self.retries = 0
  17. end
  18. 1 def retries=(value)
  19. 66 raise ArgumentError, "Invalid number of retries: #{value}" unless value.is_a?(Integer) && value >= 0
  20. 65 @retries = value
  21. end
  22. end
  23. 1 class << self
  24. 1 def configure
  25. 1 block_given? ? yield(Config) : Config
  26. end
  27. 1 def config
  28. 161 Config
  29. end
  30. end
  31. end
  32. 1 LinkChecker::Config.reset

lib/ruby-link-checker/errors.rb

100.0% lines covered

2 relevant lines. 2 lines covered and 0 lines missed.
    
  1. 1 require_relative 'errors/base_error'
  2. 1 require_relative 'errors/redirect_loop_error'

lib/ruby-link-checker/errors/base_error.rb

100.0% lines covered

3 relevant lines. 3 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 module LinkChecker
  3. 1 module Errors
  4. 1 class BaseError < StandardError
  5. end
  6. end
  7. end

lib/ruby-link-checker/errors/redirect_loop_error.rb

88.89% lines covered

9 relevant lines. 8 lines covered and 1 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 module LinkChecker
  3. 1 module Errors
  4. 1 class RedirectLoopError < BaseError
  5. 1 attr_accessor :urls
  6. 1 def initialize(urls)
  7. 4 @urls = urls
  8. 4 super "Redirect loop: #{urls.join(' -> ')}."
  9. end
  10. 1 def url
  11. @urls.last
  12. end
  13. end
  14. end
  15. end

lib/ruby-link-checker/logger.rb

100.0% lines covered

8 relevant lines. 8 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require 'logger'
  3. 1 module LinkChecker
  4. 1 class Logger < ::Logger
  5. 1 def self.default
  6. 118 return @default if @default
  7. 1 logger = Logger.new(STDOUT)
  8. 1 logger.level = Logger::WARN
  9. 1 @default = logger
  10. end
  11. end
  12. end

lib/ruby-link-checker/net/http.rb

100.0% lines covered

3 relevant lines. 3 lines covered and 0 lines missed.
    
  1. 1 require_relative 'http/config'
  2. 1 require_relative 'http/result'
  3. 1 require_relative 'http/checker'

lib/ruby-link-checker/net/http/checker.rb

100.0% lines covered

20 relevant lines. 20 lines covered and 0 lines missed.
    
  1. 1 module LinkChecker
  2. 1 module Net
  3. 1 module HTTP
  4. 1 class Task < ::LinkChecker::Task
  5. 1 def run!
  6. 40 ::Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
  7. 40 http.read_timeout = checker.read_timeout if checker.read_timeout
  8. 40 http.open_timeout = checker.open_timeout if checker.open_timeout
  9. 40 request = ::Net::HTTPGenericRequest.new(method, false, true, uri)
  10. 40 request['User-Agent'] = checker.user_agent
  11. 40 response = http.request(request)
  12. 38 logger.debug "#{method} #{uri}: #{response.code}"
  13. 38 result! Result.new(uri, method, original_uri, request, response, options)
  14. end
  15. end
  16. end
  17. 1 class Checker < LinkChecker::Checker
  18. 1 extend ::LinkChecker::Net::HTTP::Config
  19. 1 attr_accessor(*LinkChecker::Net::HTTP::Config::ATTRIBUTES)
  20. 1 def initialize(options = {})
  21. 27 LinkChecker::Net::HTTP::Config::ATTRIBUTES.each do |key|
  22. 54 send("#{key}=", options[key] || LinkChecker::Net::HTTP::Config.send(key))
  23. end
  24. 27 super options
  25. end
  26. end
  27. end
  28. end
  29. end

lib/ruby-link-checker/net/http/config.rb

93.75% lines covered

16 relevant lines. 15 lines covered and 1 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 module LinkChecker
  3. 1 module Net
  4. 1 module HTTP
  5. 1 module Config
  6. 1 extend self
  7. 1 ATTRIBUTES = %i[
  8. read_timeout
  9. open_timeout
  10. ].freeze
  11. 1 attr_accessor(*Config::ATTRIBUTES)
  12. 1 def reset
  13. 28 self.read_timeout = nil
  14. 28 self.open_timeout = nil
  15. end
  16. end
  17. 1 class << self
  18. 1 def configure
  19. 1 block_given? ? yield(Config) : Config
  20. end
  21. 1 def config
  22. Config
  23. end
  24. end
  25. end
  26. end
  27. end
  28. 1 LinkChecker::Net::HTTP::Config.reset

lib/ruby-link-checker/net/http/result.rb

96.43% lines covered

28 relevant lines. 27 lines covered and 1 lines missed.
    
  1. 1 module LinkChecker
  2. 1 module Net
  3. 1 module HTTP
  4. 1 class Result < ::LinkChecker::Result
  5. 1 attr_accessor :request, :response
  6. 1 def initialize(uri, method, original_uri, request, response, options)
  7. 38 @request = request
  8. 38 @response = response
  9. 38 super uri, method, original_uri, options
  10. end
  11. 1 def error?
  12. 15 false
  13. end
  14. 1 def failure?
  15. 37 !success? && !redirect?
  16. end
  17. 1 def code
  18. 354 @code ||= begin
  19. 38 response.code.to_i
  20. rescue StandardError
  21. -1
  22. end
  23. end
  24. 1 def request_headers
  25. 1 request
  26. end
  27. 1 def redirect_to
  28. 9 return nil unless response
  29. 9 response['Location']
  30. end
  31. 1 def redirect?
  32. 82 return false unless response
  33. 82 [301, 302, 303, 307, 308].include?(code)
  34. end
  35. 1 def success?
  36. 117 return false unless response
  37. 117 code >= 200 && code <= 299
  38. end
  39. end
  40. end
  41. end
  42. end

lib/ruby-link-checker/result.rb

94.87% lines covered

39 relevant lines. 37 lines covered and 2 lines missed.
    
  1. 1 module LinkChecker
  2. 1 class Result
  3. 1 attr_accessor :uri, :result_uri, :method, :options, :checker
  4. 1 def initialize(current_uri, method, original_uri, options = {})
  5. 88 @uri = original_uri
  6. 88 @result_uri = current_uri
  7. 88 @method = method
  8. 88 @options = options
  9. end
  10. 1 def success?
  11. 32 false
  12. end
  13. 1 def failure?
  14. 18 false
  15. end
  16. 1 def error?
  17. false
  18. end
  19. 1 def redirect?
  20. 28 false
  21. end
  22. 1 def redirect_to
  23. nil
  24. end
  25. 1 def request_headers
  26. {}
  27. end
  28. 1 def code
  29. nil
  30. end
  31. 1 def error
  32. nil
  33. end
  34. 1 def to_s
  35. 90 status_s = if success?
  36. 24 'OK'
  37. 66 elsif failure?
  38. 34 'FAIL'
  39. 32 elsif redirect?
  40. 18 'REDIRECT'
  41. else
  42. 14 'ERROR'
  43. end
  44. 90 "#{method} #{uri}#{result_uri == uri ? nil : ' (' + result_uri.to_s + ')'}: #{status_s} (#{code})"
  45. end
  46. end
  47. 1 class ResultError < Result
  48. 1 attr_accessor :error
  49. 1 def initialize(uri, method, original_uri, error, options = {})
  50. 12 @error = error
  51. 12 super uri, method, original_uri, options
  52. end
  53. 1 def error?
  54. 20 true
  55. end
  56. 1 def code
  57. 14 error.class.name
  58. end
  59. end
  60. end

lib/ruby-link-checker/task.rb

92.31% lines covered

13 relevant lines. 12 lines covered and 1 lines missed.
    
  1. 1 module LinkChecker
  2. 1 class Task
  3. 1 include LinkChecker::Callbacks
  4. 1 attr_reader :uri, :original_uri, :method, :logger, :options, :checker
  5. 1 def initialize(checker, uri, method, original_uri, options = {})
  6. 80 @checker = checker
  7. 80 @logger = checker.logger
  8. 80 @uri = uri
  9. 80 @original_uri = original_uri || @uri
  10. 80 @method = method
  11. 80 @options = options
  12. end
  13. 1 def run!
  14. raise NotImplementedError
  15. end
  16. end
  17. end

lib/ruby-link-checker/tasks.rb

98.46% lines covered

65 relevant lines. 64 lines covered and 1 lines missed.
    
  1. 1 module LinkChecker
  2. 1 class Tasks
  3. 1 include LinkChecker::Callbacks
  4. 1 attr_reader :result, :uri, :original_uri
  5. 1 def initialize(checker, task_klass, uri, methods, options = {})
  6. 52 @uri = uri
  7. 52 @retries_left = checker.retries
  8. 52 @methods_left = methods.dup
  9. 52 @methods = methods.dup
  10. 52 @task_klass = task_klass
  11. 52 @checker = checker
  12. 52 @logger = checker.logger
  13. 52 @redirects = [uri]
  14. 52 @options = options
  15. 52 raise ArgumentError, :tasks_klass unless @task_klass && @task_klass < ::LinkChecker::Task
  16. end
  17. 1 def new_task(uri, method, original_uri, options)
  18. 80 task_klass.new(checker, uri, method, original_uri, options)
  19. end
  20. 1 def execute!
  21. 98 if retry?
  22. 10 @retries_left -= 1
  23. 10 retry! @result
  24. 10 _queue_task(uri, method, original_uri || uri, options)
  25. 88 elsif methods_left.any?
  26. 60 @method = methods_left.shift
  27. 60 @redirects = [uri]
  28. 60 @uri = URI(@uri) unless @uri.is_a?(URI)
  29. 56 _queue_task(uri, method, original_uri || uri, options)
  30. 28 elsif @result && result.error?
  31. 12 error! @result
  32. else
  33. 16 failure! @result
  34. end
  35. rescue StandardError => e
  36. 8 logger.error("#{self}##{__method__}") { e }
  37. 4 _handle_result ResultError.new(uri, method, original_uri || uri, e, options)
  38. end
  39. 1 private
  40. 1 attr_reader :logger, :methods_left, :options, :task_klass, :redirects, :checker, :method
  41. 1 def retries
  42. checker.retries
  43. end
  44. 1 def first_time?
  45. 98 !!method.nil?
  46. end
  47. 1 def retries_left
  48. 46 @retries_left ||= retries
  49. end
  50. 1 def retry?
  51. 98 !first_time? && retries_left > 0
  52. end
  53. 1 def _queue_task(uri, method, original_uri, options = {})
  54. 80 task = new_task(uri, method, original_uri, options)
  55. 80 task.on :result do |result|
  56. 78 _handle_result result
  57. end
  58. 80 task.run!
  59. rescue StandardError => e
  60. 4 logger.error("#{self}##{__method__}") { e }
  61. 2 _handle_result ResultError.new(uri, method, original_uri, e, options)
  62. end
  63. 1 def _handle_result(result)
  64. 88 @result = result
  65. 88 logger.info "#{' ' * (redirects.count - 1)}#{result}"
  66. 88 result! result
  67. 88 if result.redirect?
  68. 18 redirect! result
  69. 18 redirected_to_uri = URI.join(uri, result.redirect_to)
  70. 18 if redirects.include?(redirected_to_uri)
  71. 4 raise LinkChecker::Errors::RedirectLoopError,
  72. redirects.push(redirected_to_uri)
  73. end
  74. 14 redirects << redirected_to_uri
  75. 14 _queue_task(redirected_to_uri, result.method, uri, options)
  76. 70 elsif result.success?
  77. 24 success! result
  78. else
  79. 46 execute!
  80. end
  81. rescue StandardError => e
  82. 8 logger.error("#{self}##{__method__}") { e }
  83. 4 _handle_result ResultError.new(result.uri, result.method, result.result_uri, e, options)
  84. end
  85. end
  86. end

lib/ruby-link-checker/typhoeus/hydra.rb

100.0% lines covered

3 relevant lines. 3 lines covered and 0 lines missed.
    
  1. 1 require_relative 'hydra/config'
  2. 1 require_relative 'hydra/checker'
  3. 1 require_relative 'hydra/result'

lib/ruby-link-checker/typhoeus/hydra/checker.rb

96.0% lines covered

25 relevant lines. 24 lines covered and 1 lines missed.
    
  1. 1 module LinkChecker
  2. 1 module Typhoeus
  3. 1 module Hydra
  4. 1 class Task < ::LinkChecker::Task
  5. 1 def run!
  6. 40 request = ::Typhoeus::Request.new(
  7. uri, {
  8. method: method,
  9. followlocation: false,
  10. timeout: checker.timeout,
  11. connecttimeout: checker.connecttimeout,
  12. headers: {
  13. 'User-Agent' => checker.user_agent
  14. }
  15. }
  16. )
  17. 40 request.on_complete do |response|
  18. 40 if response.timed_out?
  19. 2 logger.debug "#{method} #{uri}: #{response.return_code}"
  20. 2 result! ResultError.new(uri, method, original_uri, Timeout::Error.new, options)
  21. else
  22. 38 logger.debug "#{method} #{uri}: #{response.code}"
  23. 38 result! Result.new(uri, method, original_uri, request, response, options)
  24. end
  25. end
  26. 40 checker._queue(request)
  27. end
  28. end
  29. 1 class Checker < LinkChecker::Checker
  30. 1 extend ::LinkChecker::Typhoeus::Hydra::Config
  31. 1 attr_accessor(*LinkChecker::Typhoeus::Hydra::Config::ATTRIBUTES)
  32. 1 def initialize(options = {})
  33. 27 LinkChecker::Typhoeus::Hydra::Config::ATTRIBUTES.each do |key|
  34. 54 send("#{key}=", options[key] || LinkChecker::Typhoeus::Hydra::Config.send(key))
  35. end
  36. 27 @hydra = ::Typhoeus::Hydra.new(options[:hydra] || { max_concurrency: 10 })
  37. 27 super options
  38. end
  39. 1 def run
  40. @hydra.run
  41. end
  42. 1 def _queue(request)
  43. 40 @hydra.queue(request)
  44. end
  45. end
  46. end
  47. end
  48. end

lib/ruby-link-checker/typhoeus/hydra/config.rb

93.75% lines covered

16 relevant lines. 15 lines covered and 1 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 module LinkChecker
  3. 1 module Typhoeus
  4. 1 module Hydra
  5. 1 module Config
  6. 1 extend self
  7. 1 ATTRIBUTES = %i[
  8. timeout
  9. connecttimeout
  10. ].freeze
  11. 1 attr_accessor(*Config::ATTRIBUTES)
  12. 1 def reset
  13. 1 self.timeout = 60
  14. 1 self.connecttimeout = 10
  15. end
  16. end
  17. 1 class << self
  18. 1 def configure
  19. 1 block_given? ? yield(Config) : Config
  20. end
  21. 1 def config
  22. Config
  23. end
  24. end
  25. end
  26. end
  27. end
  28. 1 LinkChecker::Typhoeus::Hydra::Config.reset

lib/ruby-link-checker/typhoeus/hydra/result.rb

96.43% lines covered

28 relevant lines. 27 lines covered and 1 lines missed.
    
  1. 1 module LinkChecker
  2. 1 module Typhoeus
  3. 1 module Hydra
  4. 1 class Result < ::LinkChecker::Result
  5. 1 attr_accessor :request, :response
  6. 1 def initialize(uri, method, original_uri, request, response, options)
  7. 38 @request = request
  8. 38 @response = response
  9. 38 super uri, method, original_uri, options
  10. end
  11. 1 def error?
  12. 38 false
  13. end
  14. 1 def failure?
  15. 37 !success? && !redirect? && !error?
  16. end
  17. 1 def code
  18. 354 @code ||= begin
  19. 38 response.code.to_i
  20. rescue StandardError
  21. -1
  22. end
  23. end
  24. 1 def request_headers
  25. 1 request.options[:headers]
  26. end
  27. 1 def redirect_to
  28. 9 return nil unless response
  29. 9 response.headers['Location']
  30. end
  31. 1 def redirect?
  32. 82 return false unless response
  33. 82 [301, 302, 303, 307, 308].include?(code)
  34. end
  35. 1 def success?
  36. 117 return false unless response
  37. 117 code >= 200 && code <= 299
  38. end
  39. end
  40. end
  41. end
  42. end

spec/ruby-link-checker/checker_spec.rb

100.0% lines covered

5 relevant lines. 5 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require 'spec_helper'
  3. 1 describe LinkChecker::Checker do
  4. 1 context 'config' do
  5. 1 it 'requires at least one method' do
  6. 2 expect { LinkChecker::Checker.new(methods: []) }.to raise_error ArgumentError, 'Missing methods.'
  7. end
  8. end
  9. end

spec/ruby-link-checker/config_spec.rb

100.0% lines covered

21 relevant lines. 21 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require 'spec_helper'
  3. 1 describe LinkChecker::Config do
  4. 1 describe '#configure' do
  5. 1 context 'methods' do
  6. 1 before do
  7. 1 LinkChecker.configure do |config|
  8. 1 config.methods = %w[GET]
  9. end
  10. end
  11. 1 it 'sets methods' do
  12. 1 expect(LinkChecker.config.methods).to eq %w[GET]
  13. end
  14. end
  15. 1 context 'retries' do
  16. 1 it 'requires a positive integer' do
  17. 2 expect { LinkChecker.config.retries = -1 }.to raise_error ArgumentError, 'Invalid number of retries: -1'
  18. end
  19. end
  20. end
  21. 1 describe 'defaults' do
  22. 1 it 'sets methods' do
  23. 1 expect(LinkChecker.config.methods).to eq %w[HEAD GET]
  24. end
  25. 1 it 'sets user agent' do
  26. 1 expect(LinkChecker.config.user_agent).to eq "Ruby Link Checker/#{LinkChecker::VERSION}"
  27. end
  28. 1 it 'does not set logger' do
  29. 1 expect(LinkChecker.config.logger).to be nil
  30. end
  31. 1 it 'sets retries' do
  32. 1 expect(LinkChecker.config.retries).to eq 0
  33. end
  34. end
  35. end

spec/ruby-link-checker/net/http/checker_spec.rb

100.0% lines covered

33 relevant lines. 33 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require 'spec_helper'
  3. 1 describe LinkChecker::Net::HTTP::Checker do
  4. 1 before :all do
  5. 1 VCR.configure do |config|
  6. 1 config.hook_into :webmock
  7. end
  8. end
  9. 1 after do
  10. 27 LinkChecker::Net::HTTP::Config.reset
  11. end
  12. 1 it_behaves_like 'a link checker'
  13. 1 context 'with timeout options', vcr: { cassette_name: '200' } do
  14. 1 before do
  15. 1 LinkChecker::Net::HTTP.configure do |config|
  16. 1 config.read_timeout = 5
  17. 1 config.open_timeout = 10
  18. end
  19. 1 expect_any_instance_of(Net::HTTP).to receive(:read_timeout=).with(5)
  20. 1 expect_any_instance_of(Net::HTTP).to receive(:open_timeout=).with(10)
  21. end
  22. 1 include_context 'with url'
  23. 1 it 'creates requests with a default timeout' do
  24. 1 expect(result.success?).to be true
  25. end
  26. end
  27. 1 context 'timeout' do
  28. 1 before do
  29. 2 stub_request(:get, 'https://www.example.org/').to_timeout
  30. end
  31. 1 include_context 'with url'
  32. 1 around do |example|
  33. 4 VCR.turned_off { example.run }
  34. end
  35. 1 it 'times out' do
  36. 1 expect(result.success?).to be false
  37. 1 expect(result.error?).to be true
  38. 1 expect(result.to_s).to eq 'GET https://www.example.org: ERROR (Net::OpenTimeout)'
  39. end
  40. 1 context 'with metadata' do
  41. 2 let(:options) { { foo: :bar } }
  42. 1 it 'times out' do
  43. 1 expect(result.error?).to be true
  44. 1 expect(result.options).to eq(foo: :bar)
  45. end
  46. end
  47. end
  48. end

spec/ruby-link-checker/typhoeus/hydra/checker_spec.rb

100.0% lines covered

35 relevant lines. 35 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require 'spec_helper'
  3. 1 describe LinkChecker::Typhoeus::Hydra::Checker do
  4. 1 module TestLinkChecker
  5. 1 class Task < LinkChecker::Typhoeus::Hydra::Task; end
  6. 1 class LinkChecker < LinkChecker::Typhoeus::Hydra::Checker
  7. 1 def check(url, options = {})
  8. 26 super url, options
  9. 26 @hydra.run
  10. end
  11. end
  12. end
  13. 1 before :all do
  14. 1 VCR.configure do |config|
  15. 1 config.hook_into :typhoeus
  16. end
  17. end
  18. 1 describe TestLinkChecker::LinkChecker do
  19. 1 it_behaves_like 'a link checker'
  20. 1 context 'with timeout options', vcr: { cassette_name: '200' } do
  21. 1 before do
  22. 1 LinkChecker::Typhoeus::Hydra.configure do |config|
  23. 1 config.timeout = 5
  24. 1 config.connecttimeout = 10
  25. end
  26. 1 expect(Typhoeus::Request).to receive(:new).with(
  27. URI(url),
  28. hash_including(timeout: 5, connecttimeout: 10)
  29. ).and_call_original
  30. end
  31. 1 include_context 'with url'
  32. 1 it 'creates requests with a default timeout' do
  33. 1 expect(result.success?).to be true
  34. end
  35. end
  36. 1 context 'timeout', vcr: { cassette_name: '200' } do
  37. 1 before do
  38. 2 allow_any_instance_of(Typhoeus::Response).to receive(:timed_out?).and_return(true)
  39. end
  40. 1 include_context 'with url'
  41. 1 it 'times out' do
  42. 1 expect(result.success?).to be false
  43. 1 expect(result.error?).to be true
  44. 1 expect(result.to_s).to eq 'GET https://www.example.org: ERROR (Timeout::Error)'
  45. end
  46. 1 context 'with metadata' do
  47. 2 let(:options) { { foo: :bar } }
  48. 1 it 'times out' do
  49. 1 expect(result.error?).to be true
  50. 1 expect(result.options).to eq(foo: :bar)
  51. end
  52. end
  53. end
  54. end
  55. end

spec/ruby-link-checker/version_spec.rb

100.0% lines covered

4 relevant lines. 4 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require 'spec_helper'
  3. 1 describe LinkChecker do
  4. 1 it 'has a version' do
  5. 1 expect(LinkChecker::VERSION).not_to be_nil
  6. end
  7. end

spec/support/config.rb

100.0% lines covered

5 relevant lines. 5 lines covered and 0 lines missed.
    
  1. 1 RSpec.configure do |config|
  2. 1 config.before do
  3. 64 LinkChecker::Logger.default.level = Logger::DEBUG
  4. end
  5. 1 config.after do
  6. 64 LinkChecker::Config.reset
  7. end
  8. end

spec/support/link_checker.rb

98.97% lines covered

194 relevant lines. 192 lines covered and 2 lines missed.
    
  1. 1 shared_context 'a link checker' do
  2. 2 context 'user-agent' do
  3. 2 subject do
  4. 2 described_class.new(user_agent: 'user/agent')
  5. end
  6. 2 it 'updates user-agent' do
  7. 2 expect(subject.user_agent).to eq 'user/agent'
  8. end
  9. end
  10. 2 context 'check' do
  11. 44 let(:url) { 'https://www.example.org' }
  12. 2 include_context 'with result'
  13. 2 context 'with metadata' do
  14. 2 before do
  15. 8 subject.check(url, foo: 'bar')
  16. end
  17. 2 context 'GET' do
  18. 2 subject do
  19. 8 described_class.new(methods: ['GET'])
  20. end
  21. 2 context 'check' do
  22. 2 context '200', vcr: { cassette_name: '200' } do
  23. 2 it 'passes metadata' do
  24. 2 expect(result.options).to eq(foo: 'bar')
  25. end
  26. end
  27. 2 context '404', vcr: { cassette_name: '404' } do
  28. 2 it 'passes metadata' do
  29. 2 expect(result.options).to eq(foo: 'bar')
  30. end
  31. end
  32. 2 context 'error', vcr: { cassette_name: '404' } do
  33. 4 let(:url) { '\/invalid-url' }
  34. 2 it 'passes metadata' do
  35. 2 expect(result.options).to eq(foo: 'bar')
  36. end
  37. end
  38. 2 context 'a redirect loop', vcr: { cassette_name: '301+301' } do
  39. 2 it 'passes metadata' do
  40. 2 expect(result.options).to eq(foo: 'bar')
  41. end
  42. end
  43. end
  44. end
  45. end
  46. 2 context 'without results' do
  47. 2 before do
  48. 2 subject.check(url, foo: 'bar')
  49. end
  50. 2 context 'GET' do
  51. 2 subject do
  52. 2 described_class.new(results: false, methods: ['GET'])
  53. end
  54. 2 context 'check' do
  55. 2 context 'a valid URI that returns a 200', vcr: { cassette_name: '200' } do
  56. 2 it 'passes metadata' do
  57. 2 expect(subject.results).to be_nil
  58. end
  59. end
  60. end
  61. end
  62. end
  63. 2 context 'without metadata' do
  64. 2 before do
  65. 36 subject.check(url)
  66. end
  67. 2 context 'GET' do
  68. 2 subject do
  69. 20 described_class.new(methods: ['GET'])
  70. end
  71. 2 context 'check' do
  72. 2 context 'a valid URI that returns a 200', vcr: { cassette_name: '200' } do
  73. 2 it 'sets user agent' do
  74. 2 expect(result.request_headers['User-Agent']).to eq "Ruby Link Checker/#{LinkChecker::VERSION}"
  75. end
  76. 2 it 'returns all metadata' do
  77. 2 expect(result.options).to eq({})
  78. end
  79. 2 it 'returns results' do
  80. 2 expect(subject.results).to eq(
  81. error: [],
  82. failure: [],
  83. success: [
  84. result
  85. ]
  86. )
  87. end
  88. 2 it 'succeeds' do
  89. 2 expect(result.success?).to be true
  90. 2 expect(result.error?).to be false
  91. 2 expect(result.failure?).to be false
  92. 2 expect(result.uri).to eq URI(url)
  93. 2 expect(subject).to have_received(:called!).with(:result, result)
  94. 2 expect(subject).to have_received(:called!).with(:success, result)
  95. end
  96. end
  97. 2 context 'a 404', vcr: { cassette_name: '404', allow_playback_repeats: true } do
  98. 2 it 'fails' do
  99. 2 expect(result.success?).to be false
  100. 2 expect(result.error?).to be false
  101. 2 expect(result.failure?).to be true
  102. 2 expect(result.uri).to eq URI(url)
  103. 2 expect(result.response.code.to_i).to eq 404
  104. 2 expect(subject).to have_received(:called!).with(:failure, result)
  105. end
  106. 2 context 'with 0 retries' do
  107. 2 subject do
  108. 2 described_class.new(methods: ['GET'], retries: 0)
  109. end
  110. 2 it 'fails' do
  111. 2 expect(result.success?).to be false
  112. 2 expect(result.error?).to be false
  113. 2 expect(result.failure?).to be true
  114. 2 expect(result.uri).to eq URI(url)
  115. 2 expect(result.response.code.to_i).to eq 404
  116. 2 expect(subject).to have_received(:called!).with(:failure, result).once
  117. 2 expect(subject).not_to have_received(:called!).with(:retry, anything)
  118. end
  119. end
  120. 2 context 'with 1 retry' do
  121. 2 subject do
  122. 2 described_class.new(methods: ['GET'], retries: 1)
  123. end
  124. 2 it 'fails' do
  125. 2 expect(result.success?).to be false
  126. 2 expect(result.error?).to be false
  127. 2 expect(result.failure?).to be true
  128. 2 expect(result.uri).to eq URI(url)
  129. 2 expect(result.response.code.to_i).to eq 404
  130. 2 expect(subject).to have_received(:called!).with(:failure, result).once
  131. 2 expect(subject).to have_received(:called!).with(:retry, anything).once
  132. end
  133. end
  134. 2 context 'with 2 retries' do
  135. 2 subject do
  136. 2 described_class.new(methods: ['GET'], retries: 2)
  137. end
  138. 2 it 'fails' do
  139. 2 expect(result.success?).to be false
  140. 2 expect(result.error?).to be false
  141. 2 expect(result.failure?).to be true
  142. 2 expect(result.uri).to eq URI(url)
  143. 2 expect(result.response.code.to_i).to eq 404
  144. 2 expect(subject).to have_received(:called!).with(:failure, result).once
  145. 2 expect(subject).to have_received(:called!).with(:retry, anything).twice
  146. end
  147. end
  148. end
  149. 2 context 'a redirect on HEAD followed by a 403', vcr: { cassette_name: '301+403' } do
  150. 2 it 'calls redirect callback' do
  151. 2 expect(result.success?).to be false
  152. 2 expect(result.failure?).to be true
  153. 2 expect(subject).to have_received(:called!).with(:redirect, anything)
  154. 2 expect(subject).to have_received(:called!).with(:failure, result).once
  155. end
  156. 2 it 'reports its original and result urls' do
  157. 2 expect(result.uri.to_s).to eq url
  158. 2 expect(result.result_uri.to_s).not_to eq url
  159. 2 expect(result.result_uri.to_s).to eq 'https://www.dblock.org/'
  160. end
  161. end
  162. 2 context 'a redirect on HEAD followed by a 200', vcr: { cassette_name: '301+200' } do
  163. 2 it 'calls redirect callback' do
  164. 2 expect(result.success?).to be true
  165. 2 expect(result.failure?).to be false
  166. 2 expect(result.redirect?).to be false
  167. 2 expect(subject).to have_received(:called!).with(:redirect, anything)
  168. 2 expect(subject).to have_received(:called!).with(:success, result)
  169. 2 expect(subject).not_to have_received(:called!).with(:failure, anything)
  170. end
  171. end
  172. 2 context 'a redirect loop', vcr: { cassette_name: '301+301' } do
  173. 2 it 'calls redirect callback' do
  174. 2 expect(result.success?).to be false
  175. 2 expect(result.failure?).to be false
  176. 2 expect(result.error?).to be true
  177. 2 expect(result.error).to be_a LinkChecker::Errors::RedirectLoopError
  178. 2 expect(result.redirect?).to be false
  179. 2 expect(subject).to have_received(:called!).with(:redirect, anything).twice
  180. 2 expect(subject).to have_received(:called!).with(:error, result)
  181. 2 expect(subject).not_to have_received(:called!).with(:failure, result)
  182. 2 expect(subject).not_to have_received(:called!).with(:success, result)
  183. end
  184. end
  185. 2 context 'a retry on 429', vcr: {
  186. cassette_name: '429+200',
  187. match_requests_on: [lambda { |_request, recorded_request|
  188. 4 @matched ||= []
  189. 4 if @matched.size + 1 === recorded_request.headers['Index'].first
  190. 4 @matched << recorded_request
  191. 4 true
  192. else
  193. false
  194. end
  195. }]
  196. } do
  197. 2 subject do
  198. 2 described_class.new(methods: ['GET'], retries: 1)
  199. end
  200. 2 it 'calls a retry callback' do
  201. 2 expect(result.success?).to be true
  202. 2 expect(result.failure?).to be false
  203. 2 expect(result.redirect?).to be false
  204. 2 expect(subject).to have_received(:called!).with(:retry, anything)
  205. 2 expect(subject).to have_received(:called!).with(:success, result)
  206. 2 expect(subject).not_to have_received(:called!).with(:failure, anything)
  207. 2 expect(subject).not_to have_received(:called!).with(:error, anything)
  208. end
  209. end
  210. 2 context 'an invalid URI' do
  211. 4 let(:url) { '\/invalid-url' }
  212. 2 it 'fails' do
  213. 2 expect(result.success?).to be false
  214. 2 expect(result.failure?).to be false
  215. 2 expect(result.error?).to be true
  216. 2 expect(result.uri).to eq url
  217. 2 expect(subject).to have_received(:called!).with(:result, result)
  218. 2 expect(subject).to have_received(:called!).with(:error, result)
  219. 2 expect(subject).not_to have_received(:called!).with(:failure)
  220. 2 expect(subject).not_to have_received(:called!).with(:success)
  221. end
  222. end
  223. end
  224. 2 context 'HEAD,GET' do
  225. 2 subject do
  226. 6 described_class.new(methods: %w[HEAD GET])
  227. end
  228. 2 context 'a valid URI that fails on HEAD and succeeds on GET', vcr: { cassette_name: '404+200' } do
  229. 2 it 'succeeds' do
  230. 2 expect(result.success?).to be true
  231. 2 expect(result.error?).to be false
  232. 2 expect(result.failure?).to be false
  233. 2 expect(result.uri).to eq URI(url)
  234. 2 expect(subject).to have_received(:called!).with(:success, result)
  235. 2 expect(subject).not_to have_received(:called!).with(:failure, result)
  236. end
  237. end
  238. 2 context 'a valid URI that fails both on HEAD and GET', vcr: { cassette_name: '404+404' } do
  239. 2 it 'fails' do
  240. 2 expect(result.success?).to be false
  241. 2 expect(result.error?).to be false
  242. 2 expect(result.failure?).to be true
  243. 2 expect(result.uri).to eq URI(url)
  244. 2 expect(result.response.code.to_i).to eq 404
  245. 2 expect(subject).to have_received(:called!).with(:failure, result).once
  246. end
  247. end
  248. 2 context 'a retry on 429', vcr: {
  249. cassette_name: '429+429+200',
  250. match_requests_on: [lambda { |request, recorded_request|
  251. 6 @matched ||= []
  252. 6 if recorded_request.method == request.method && @matched.size + 1 === recorded_request.headers['Index'].first
  253. 6 @matched << recorded_request
  254. 6 true
  255. else
  256. false
  257. end
  258. }]
  259. } do
  260. 2 subject do
  261. 2 described_class.new(methods: %w[HEAD GET], retries: 1)
  262. end
  263. 2 it 'executes HEAD twice, then falls back to GET' do
  264. 2 expect(result.success?).to be true
  265. end
  266. end
  267. 2 context 'a redirect on HEAD followed by a 400 error succeeds on GET',
  268. vcr: { cassette_name: '301+400+301+200' } do
  269. 2 it 'calls redirect callback' do
  270. 2 expect(result.success?).to be true
  271. 2 expect(result.failure?).to be false
  272. 2 expect(result.redirect?).to be false
  273. 2 expect(subject).to have_received(:called!).with(:redirect, anything).twice
  274. 2 expect(subject).to have_received(:called!).with(:success, result).once
  275. 2 expect(subject).not_to have_received(:called!).with(:failure, anything)
  276. end
  277. end
  278. end
  279. end
  280. end
  281. end
  282. end

spec/support/vcr.rb

85.71% lines covered

7 relevant lines. 6 lines covered and 1 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require 'vcr'
  3. 1 require 'webmock/rspec'
  4. 1 VCR.configure do |config|
  5. 1 config.cassette_library_dir = 'spec/fixtures'
  6. # config.default_cassette_options = { record: :new_episodes }
  7. 1 config.configure_rspec_metadata!
  8. 1 config.before_record do |i|
  9. i.response.body.force_encoding('UTF-8')
  10. end
  11. end

spec/support/with_result.rb

100.0% lines covered

9 relevant lines. 9 lines covered and 0 lines missed.
    
  1. 1 shared_context 'with result' do
  2. 6 before do
  3. 52 allow(subject).to receive(:called!)
  4. 52 subject.on do |event, *data|
  5. 168 subject.called! event, *data
  6. end
  7. 52 subject.on :result do |result|
  8. 88 @result = result
  9. end
  10. end
  11. 6 let(:result) do
  12. 50 @result
  13. end
  14. end

spec/support/with_url.rb

100.0% lines covered

8 relevant lines. 8 lines covered and 0 lines missed.
    
  1. 1 shared_context 'with url' do
  2. 4 subject do
  3. 6 described_class.new(methods: ['GET'])
  4. end
  5. 8 let(:options) { {} }
  6. 10 let(:url) { 'https://www.example.org' }
  7. 4 include_context 'with result'
  8. 4 before do
  9. 6 subject.check(url, options)
  10. end
  11. end