loading
Generated 2023-04-02T21:03:25-04:00

All Files ( 97.84% covered at 25.66 hits/line )

29 files in total.
693 relevant lines, 678 lines covered and 15 lines missed. ( 97.84% )
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 232.15
lib/ruby-link-checker/checker.rb 100.00 % 42 22 22 0 55.23
lib/ruby-link-checker/config.rb 100.00 % 41 19 19 0 31.47
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 16.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 % 30 19 19 0 24.16
lib/ruby-link-checker/net/http/config.rb 93.75 % 35 16 15 1 4.56
lib/ruby-link-checker/net/http/result.rb 96.43 % 53 28 27 1 45.54
lib/ruby-link-checker/result.rb 94.87 % 74 39 37 2 25.85
lib/ruby-link-checker/task.rb 92.31 % 20 13 12 1 44.77
lib/ruby-link-checker/tasks.rb 98.59 % 110 71 70 1 44.03
lib/ruby-link-checker/typhoeus/hydra.rb 100.00 % 3 3 3 0 1.00
lib/ruby-link-checker/typhoeus/hydra/checker.rb 95.65 % 50 23 22 1 19.30
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 46.75
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 2.00
spec/ruby-link-checker/typhoeus/hydra/checker_spec.rb 100.00 % 67 35 35 0 2.60
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 27.80
spec/support/link_checker.rb 99.03 % 343 206 204 2 2.69
spec/support/vcr.rb 100.00 % 13 8 8 0 1.38
spec/support/with_result.rb 100.00 % 15 9 9 0 59.89
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. 1032 @callbacks ||= Hash.new { |h, k| h[k] = [] }
  6. end
  7. 1 def delegates
  8. 613 @delegates ||= []
  9. end
  10. 1 def on(*events, &block)
  11. 266 if events && Array(events).any?
  12. 154 Array(events).each do |event|
  13. 155 callbacks[event.to_s] << block
  14. end
  15. else
  16. 112 delegates << block
  17. end
  18. end
  19. 1 def method_missing(m, *args, &block)
  20. 294 if m.to_s[-1] == '!'
  21. 294 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. 501 delegates.each do |c|
  29. 400 c.call(event, *data)
  30. end
  31. 501 callbacks = self.callbacks[event.to_s]
  32. 501 return false unless callbacks
  33. 501 callbacks.each do |c|
  34. 203 c.call(*data)
  35. end
  36. 501 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. 59 LinkChecker::Config::ATTRIBUTES.each do |key|
  10. 236 send("#{key}=", options[key] || LinkChecker.config.send(key))
  11. end
  12. 59 raise ArgumentError, "Missing methods." if methods&.none?
  13. 58 @logger ||= options[:logger] || LinkChecker::Config.logger || LinkChecker::Logger.default
  14. 58 @results = { error: [], failure: [], success: [] } unless options.key?(:results) && !options[:results]
  15. end
  16. 1 def task_klass
  17. 56 @task_klass ||= begin
  18. 56 module_name = self.class.name.split("::")[...-1].join('::')
  19. 56 Object.const_get("#{module_name}::Task")
  20. end
  21. end
  22. 1 def check(uri, options = {})
  23. 56 tasks = Tasks.new(
  24. self,
  25. task_klass,
  26. uri,
  27. methods,
  28. options
  29. )
  30. 56 tasks.on do |event, *args|
  31. 200 results[event] << args.first if @results && %i[error failure success].include?(event)
  32. 200 callback event, *args
  33. end
  34. 56 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. 69 self.methods = %w[HEAD GET]
  14. 69 self.user_agent = "Ruby Link Checker/#{LinkChecker::VERSION}"
  15. 69 self.logger = nil
  16. 69 self.retries = 0
  17. end
  18. 1 def retries=(value)
  19. 70 raise ArgumentError, "Invalid number of retries: #{value}" unless value.is_a?(Integer) && value >= 0
  20. 69 @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. 171 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. 126 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

19 relevant lines. 19 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. 48 ::Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
  7. 48 http.read_timeout = checker.read_timeout if checker.read_timeout
  8. 48 http.open_timeout = checker.open_timeout if checker.open_timeout
  9. 48 request = ::Net::HTTPGenericRequest.new(method, false, true, uri)
  10. 48 request['User-Agent'] = checker.user_agent
  11. 48 response = http.request(request)
  12. 46 result! Result.new(uri, method, original_uri, request, response, options)
  13. end
  14. end
  15. end
  16. 1 class Checker < LinkChecker::Checker
  17. 1 extend ::LinkChecker::Net::HTTP::Config
  18. 1 attr_accessor(*LinkChecker::Net::HTTP::Config::ATTRIBUTES)
  19. 1 def initialize(options = {})
  20. 29 LinkChecker::Net::HTTP::Config::ATTRIBUTES.each do |key|
  21. 58 send("#{key}=", options[key] || LinkChecker::Net::HTTP::Config.send(key))
  22. end
  23. 29 super options
  24. end
  25. end
  26. end
  27. end
  28. 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. 30 self.read_timeout = nil
  14. 30 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. 46 @request = request
  8. 46 @response = response
  9. 46 super uri, method, original_uri, options
  10. end
  11. 1 def error?
  12. 26 false
  13. end
  14. 1 def failure?
  15. 54 !success? && !redirect?
  16. end
  17. 1 def code
  18. 453 @code ||= begin
  19. 46 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. 13 return nil unless response
  29. 13 response['Location']
  30. end
  31. 1 def redirect?
  32. 111 return false unless response
  33. 111 [301, 302, 303, 307, 308].include?(code)
  34. end
  35. 1 def success?
  36. 148 return false unless response
  37. 148 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. 104 @uri = original_uri
  6. 104 @result_uri = current_uri
  7. 104 @method = method
  8. 104 @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. 106 status_s = if success?
  36. 26 'OK'
  37. 80 elsif failure?
  38. 40 'FAIL'
  39. 40 elsif redirect?
  40. 26 'REDIRECT'
  41. else
  42. 14 'ERROR'
  43. end
  44. 106 "#{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. 96 @checker = checker
  7. 96 @logger = checker.logger
  8. 96 @uri = uri
  9. 96 @original_uri = original_uri || @uri
  10. 96 @method = method
  11. 96 @options = options
  12. end
  13. 1 def run!
  14. raise NotImplementedError
  15. end
  16. end
  17. end

lib/ruby-link-checker/tasks.rb

98.59% lines covered

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

95.65% lines covered

23 relevant lines. 22 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. 48 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. 48 request.on_complete do |response|
  18. 48 if response.timed_out?
  19. 2 result! ResultError.new(uri, method, original_uri, Timeout::Error.new, options)
  20. else
  21. 46 result! Result.new(uri, method, original_uri, request, response, options)
  22. end
  23. end
  24. 48 checker._queue(request)
  25. end
  26. end
  27. 1 class Checker < LinkChecker::Checker
  28. 1 extend ::LinkChecker::Typhoeus::Hydra::Config
  29. 1 attr_accessor(*LinkChecker::Typhoeus::Hydra::Config::ATTRIBUTES)
  30. 1 def initialize(options = {})
  31. 29 LinkChecker::Typhoeus::Hydra::Config::ATTRIBUTES.each do |key|
  32. 58 send("#{key}=", options[key] || LinkChecker::Typhoeus::Hydra::Config.send(key))
  33. end
  34. 29 @hydra = ::Typhoeus::Hydra.new(options[:hydra] || { max_concurrency: 10 })
  35. 29 super options
  36. end
  37. 1 def run
  38. @hydra.run
  39. end
  40. 1 def _queue(request)
  41. 48 @hydra.queue(request)
  42. end
  43. end
  44. end
  45. end
  46. 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. 46 @request = request
  8. 46 @response = response
  9. 46 super uri, method, original_uri, options
  10. end
  11. 1 def error?
  12. 60 false
  13. end
  14. 1 def failure?
  15. 54 !success? && !redirect? && !error?
  16. end
  17. 1 def code
  18. 453 @code ||= begin
  19. 46 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. 13 return nil unless response
  29. 13 response.headers['Location']
  30. end
  31. 1 def redirect?
  32. 111 return false unless response
  33. 111 [301, 302, 303, 307, 308].include?(code)
  34. end
  35. 1 def success?
  36. 148 return false unless response
  37. 148 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. 29 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. 28 super url, options
  9. 28 @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. 68 LinkChecker::Logger.default.level = Logger::DEBUG
  4. end
  5. 1 config.after do
  6. 68 LinkChecker::Config.reset
  7. end
  8. end

spec/support/link_checker.rb

99.03% lines covered

206 relevant lines. 204 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. 46 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. 40 subject.check(url)
  66. end
  67. 2 context 'GET' do
  68. 2 subject do
  69. 22 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 redirect', vcr: { cassette_name: '301+200' } do
  98. 4 let(:url) { 'https://www.meetup.com/Open-Distro-for-Elasticsearch-Meetup-Group' }
  99. 2 it 'succeeds' do
  100. 2 expect(result.success?).to be true
  101. end
  102. end
  103. 2 context 'a 404', vcr: { cassette_name: '404', allow_playback_repeats: true } do
  104. 2 it 'fails' do
  105. 2 expect(result.success?).to be false
  106. 2 expect(result.error?).to be false
  107. 2 expect(result.failure?).to be true
  108. 2 expect(result.uri).to eq URI(url)
  109. 2 expect(result.response.code.to_i).to eq 404
  110. 2 expect(subject).to have_received(:called!).with(:failure, result)
  111. end
  112. 2 context 'with 0 retries' do
  113. 2 subject do
  114. 2 described_class.new(methods: ['GET'], retries: 0)
  115. end
  116. 2 it 'fails' do
  117. 2 expect(result.success?).to be false
  118. 2 expect(result.error?).to be false
  119. 2 expect(result.failure?).to be true
  120. 2 expect(result.uri).to eq URI(url)
  121. 2 expect(result.response.code.to_i).to eq 404
  122. 2 expect(subject).to have_received(:called!).with(:failure, result).once
  123. 2 expect(subject).not_to have_received(:called!).with(:retry, anything)
  124. end
  125. end
  126. 2 context 'with 1 retry' do
  127. 2 subject do
  128. 2 described_class.new(methods: ['GET'], retries: 1)
  129. end
  130. 2 it 'fails' do
  131. 2 expect(result.success?).to be false
  132. 2 expect(result.error?).to be false
  133. 2 expect(result.failure?).to be true
  134. 2 expect(result.uri).to eq URI(url)
  135. 2 expect(result.response.code.to_i).to eq 404
  136. 2 expect(subject).to have_received(:called!).with(:failure, result).once
  137. 2 expect(subject).to have_received(:called!).with(:retry, anything).once
  138. end
  139. end
  140. 2 context 'with 2 retries' do
  141. 2 subject do
  142. 2 described_class.new(methods: ['GET'], retries: 2)
  143. end
  144. 2 it 'fails' do
  145. 2 expect(result.success?).to be false
  146. 2 expect(result.error?).to be false
  147. 2 expect(result.failure?).to be true
  148. 2 expect(result.uri).to eq URI(url)
  149. 2 expect(result.response.code.to_i).to eq 404
  150. 2 expect(subject).to have_received(:called!).with(:failure, result).once
  151. 2 expect(subject).to have_received(:called!).with(:retry, anything).twice
  152. end
  153. end
  154. end
  155. 2 context 'a redirect on HEAD followed by a 403', vcr: { cassette_name: '301+403' } do
  156. 2 it 'calls redirect callback' do
  157. 2 expect(result.success?).to be false
  158. 2 expect(result.failure?).to be true
  159. 2 expect(subject).to have_received(:called!).with(:redirect, anything)
  160. 2 expect(subject).to have_received(:called!).with(:failure, result).once
  161. end
  162. 2 it 'reports its original and result urls' do
  163. 2 expect(result.uri.to_s).to eq url
  164. 2 expect(result.result_uri.to_s).not_to eq url
  165. 2 expect(result.result_uri.to_s).to eq 'https://www.dblock.org/'
  166. end
  167. end
  168. 2 context 'a redirect on HEAD followed by a 200', vcr: { cassette_name: '301+200' } do
  169. 2 it 'calls redirect callback' do
  170. 2 expect(result.success?).to be true
  171. 2 expect(result.failure?).to be false
  172. 2 expect(result.redirect?).to be false
  173. 2 expect(subject).to have_received(:called!).with(:redirect, anything)
  174. 2 expect(subject).to have_received(:called!).with(:success, result)
  175. 2 expect(subject).not_to have_received(:called!).with(:failure, anything)
  176. end
  177. end
  178. 2 context 'a redirect loop', vcr: { cassette_name: '301+301' } do
  179. 2 it 'calls redirect callback' do
  180. 2 expect(result.success?).to be false
  181. 2 expect(result.failure?).to be false
  182. 2 expect(result.error?).to be true
  183. 2 expect(result.error).to be_a LinkChecker::Errors::RedirectLoopError
  184. 2 expect(result.redirect?).to be false
  185. 2 expect(subject).to have_received(:called!).with(:redirect, anything).twice
  186. 2 expect(subject).to have_received(:called!).with(:error, result)
  187. 2 expect(subject).not_to have_received(:called!).with(:failure, result)
  188. 2 expect(subject).not_to have_received(:called!).with(:success, result)
  189. end
  190. end
  191. 2 context 'a retry on 429', vcr: {
  192. cassette_name: '429+200',
  193. match_requests_on: [lambda { |_request, recorded_request|
  194. 4 @matched ||= []
  195. 4 if @matched.size + 1 === recorded_request.headers['Index'].first
  196. 4 @matched << recorded_request
  197. 4 true
  198. else
  199. false
  200. end
  201. }]
  202. } do
  203. 2 subject do
  204. 2 described_class.new(methods: ['GET'], retries: 1)
  205. end
  206. 2 it 'calls a retry callback' do
  207. 2 expect(result.success?).to be true
  208. 2 expect(result.failure?).to be false
  209. 2 expect(result.redirect?).to be false
  210. 2 expect(subject).to have_received(:called!).with(:retry, anything)
  211. 2 expect(subject).to have_received(:called!).with(:success, result)
  212. 2 expect(subject).not_to have_received(:called!).with(:failure, anything)
  213. 2 expect(subject).not_to have_received(:called!).with(:error, anything)
  214. end
  215. end
  216. 2 context 'a failed retry on a redirect', vcr: { cassette_name: '308+429', allow_playback_repeats: true } do
  217. 2 subject do
  218. 2 described_class.new(methods: %w[HEAD], retries: 2)
  219. end
  220. 2 it 'calls redirect callback' do
  221. 2 expect(result.success?).to be false
  222. 2 expect(result.failure?).to be true
  223. 2 expect(result.error?).to be false
  224. 2 expect(result.error).not_to be_a LinkChecker::Errors::RedirectLoopError
  225. end
  226. end
  227. 2 context 'an invalid URI' do
  228. 4 let(:url) { '\/invalid-url' }
  229. 2 it 'fails' do
  230. 2 expect(result.success?).to be false
  231. 2 expect(result.failure?).to be false
  232. 2 expect(result.error?).to be true
  233. 2 expect(result.uri).to eq url
  234. 2 expect(subject).to have_received(:called!).with(:result, result)
  235. 2 expect(subject).to have_received(:called!).with(:error, result)
  236. 2 expect(subject).not_to have_received(:called!).with(:failure)
  237. 2 expect(subject).not_to have_received(:called!).with(:success)
  238. end
  239. end
  240. end
  241. 2 context 'HEAD,GET' do
  242. 2 subject do
  243. 6 described_class.new(methods: %w[HEAD GET])
  244. end
  245. 2 context 'a valid URI that fails on HEAD and succeeds on GET', vcr: { cassette_name: '404+200' } do
  246. 2 it 'succeeds' do
  247. 2 expect(result.success?).to be true
  248. 2 expect(result.error?).to be false
  249. 2 expect(result.failure?).to be false
  250. 2 expect(result.uri).to eq URI(url)
  251. 2 expect(subject).to have_received(:called!).with(:success, result)
  252. 2 expect(subject).not_to have_received(:called!).with(:failure, result)
  253. end
  254. end
  255. 2 context 'a valid URI that fails both on HEAD and GET', vcr: { cassette_name: '404+404' } do
  256. 2 it 'fails' do
  257. 2 expect(result.success?).to be false
  258. 2 expect(result.error?).to be false
  259. 2 expect(result.failure?).to be true
  260. 2 expect(result.uri).to eq URI(url)
  261. 2 expect(result.response.code.to_i).to eq 404
  262. 2 expect(subject).to have_received(:called!).with(:failure, result).once
  263. end
  264. end
  265. 2 context 'a retry on 429', vcr: {
  266. cassette_name: '429+429+200',
  267. match_requests_on: [lambda { |request, recorded_request|
  268. 6 @matched ||= []
  269. 6 if recorded_request.method == request.method && @matched.size + 1 === recorded_request.headers['Index'].first
  270. 6 @matched << recorded_request
  271. 6 true
  272. else
  273. false
  274. end
  275. }]
  276. } do
  277. 2 subject do
  278. 2 described_class.new(methods: %w[HEAD GET], retries: 1)
  279. end
  280. 2 it 'executes HEAD twice, then falls back to GET' do
  281. 2 expect(result.success?).to be true
  282. end
  283. end
  284. 2 context 'a redirect on HEAD followed by a 400 error succeeds on GET',
  285. vcr: { cassette_name: '301+400+301+200' } do
  286. 2 it 'calls redirect callback' do
  287. 2 expect(result.success?).to be true
  288. 2 expect(result.failure?).to be false
  289. 2 expect(result.redirect?).to be false
  290. 2 expect(subject).to have_received(:called!).with(:redirect, anything).twice
  291. 2 expect(subject).to have_received(:called!).with(:success, result).once
  292. 2 expect(subject).not_to have_received(:called!).with(:failure, anything)
  293. end
  294. end
  295. end
  296. end
  297. end
  298. end
  299. end

spec/support/vcr.rb

100.0% lines covered

8 relevant lines. 8 lines covered and 0 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. 1 config.default_cassette_options = { record: :new_episodes }
  7. 1 config.configure_rspec_metadata!
  8. 1 config.before_record do |i|
  9. 4 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. 56 allow(subject).to receive(:called!)
  4. 56 subject.on do |event, *data|
  5. 200 subject.called! event, *data
  6. end
  7. 56 subject.on :result do |result|
  8. 104 @result = result
  9. end
  10. end
  11. 6 let(:result) do
  12. 54 @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