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%
)
# frozen_string_literal: true
- 1
require_relative 'ruby-link-checker/version'
- 1
require_relative 'ruby-link-checker/errors'
- 1
require_relative 'ruby-link-checker/config'
- 1
require_relative 'ruby-link-checker/callbacks'
- 1
require_relative 'ruby-link-checker/logger'
- 1
require_relative 'ruby-link-checker/task'
- 1
require_relative 'ruby-link-checker/tasks'
- 1
require_relative 'ruby-link-checker/checker'
- 1
require_relative 'ruby-link-checker/result'
- 1
require_relative 'ruby-link-checker/net/http'
- 1
require_relative 'ruby-link-checker/typhoeus/hydra'
# frozen_string_literal: true
- 1
module LinkChecker
- 1
module Callbacks
- 1
def callbacks
- 888
@callbacks ||= Hash.new { |h, k| h[k] = [] }
end
- 1
def delegates
- 525
@delegates ||= []
end
- 1
def on(*events, &block)
- 238
if events && Array(events).any?
- 134
Array(events).each do |event|
- 135
callbacks[event.to_s] << block
end
else
- 104
delegates << block
end
end
- 1
def method_missing(m, *args, &block)
- 246
if m.to_s[-1] == '!'
- 246
callback(m.to_s[...-1].to_sym, *args)
else
super
end
end
- 1
private
- 1
def callback(event, *data)
- 421
delegates.each do |c|
- 336
c.call(event, *data)
end
- 421
callbacks = self.callbacks[event.to_s]
- 421
return false unless callbacks
- 421
callbacks.each do |c|
- 171
c.call(*data)
end
- 421
true
rescue StandardError => e
logger.error("#{self}##{__method__}") { e }
false
end
end
end
# frozen_string_literal: true
- 1
module LinkChecker
- 1
class Checker
- 1
include LinkChecker::Config
- 1
include LinkChecker::Callbacks
- 1
attr_reader :results
- 1
attr_accessor(*Config::ATTRIBUTES)
- 1
def initialize(options = {})
- 55
LinkChecker::Config::ATTRIBUTES.each do |key|
- 220
send("#{key}=", options[key] || LinkChecker.config.send(key))
end
- 55
raise ArgumentError, "Missing methods." if methods&.none?
- 54
@logger ||= options[:logger] || LinkChecker::Config.logger || LinkChecker::Logger.default
- 54
@results = { error: [], failure: [], success: [] } unless options.key?(:results) && !options[:results]
end
- 1
def task_klass
- 52
@task_klass ||= begin
- 52
module_name = self.class.name.split("::")[...-1].join('::')
- 52
Object.const_get("#{module_name}::Task")
end
end
- 1
def check(uri, options = {})
- 52
tasks = Tasks.new(
self,
task_klass,
uri,
methods,
options
)
- 52
tasks.on do |event, *args|
- 168
results[event] << args.first if @results && %i[error failure success].include?(event)
- 168
callback event, *args
end
- 52
tasks.execute!
end
end
end
# frozen_string_literal: true
- 1
module LinkChecker
- 1
module Config
- 1
extend self
- 1
ATTRIBUTES = %i[
methods
user_agent
logger
retries
].freeze
- 1
attr_accessor(*Config::ATTRIBUTES)
- 1
def reset
- 65
self.methods = %w[HEAD GET]
- 65
self.user_agent = "Ruby Link Checker/#{LinkChecker::VERSION}"
- 65
self.logger = nil
- 65
self.retries = 0
end
- 1
def retries=(value)
- 66
raise ArgumentError, "Invalid number of retries: #{value}" unless value.is_a?(Integer) && value >= 0
- 65
@retries = value
end
end
- 1
class << self
- 1
def configure
- 1
block_given? ? yield(Config) : Config
end
- 1
def config
- 161
Config
end
end
end
- 1
LinkChecker::Config.reset
- 1
require_relative 'errors/base_error'
- 1
require_relative 'errors/redirect_loop_error'
# frozen_string_literal: true
- 1
module LinkChecker
- 1
module Errors
- 1
class BaseError < StandardError
end
end
end
# frozen_string_literal: true
- 1
module LinkChecker
- 1
module Errors
- 1
class RedirectLoopError < BaseError
- 1
attr_accessor :urls
- 1
def initialize(urls)
- 4
@urls = urls
- 4
super "Redirect loop: #{urls.join(' -> ')}."
end
- 1
def url
@urls.last
end
end
end
end
# frozen_string_literal: true
- 1
require 'logger'
- 1
module LinkChecker
- 1
class Logger < ::Logger
- 1
def self.default
- 118
return @default if @default
- 1
logger = Logger.new(STDOUT)
- 1
logger.level = Logger::WARN
- 1
@default = logger
end
end
end
- 1
require_relative 'http/config'
- 1
require_relative 'http/result'
- 1
require_relative 'http/checker'
- 1
module LinkChecker
- 1
module Net
- 1
module HTTP
- 1
class Task < ::LinkChecker::Task
- 1
def run!
- 40
::Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
- 40
http.read_timeout = checker.read_timeout if checker.read_timeout
- 40
http.open_timeout = checker.open_timeout if checker.open_timeout
- 40
request = ::Net::HTTPGenericRequest.new(method, false, true, uri)
- 40
request['User-Agent'] = checker.user_agent
- 40
response = http.request(request)
- 38
logger.debug "#{method} #{uri}: #{response.code}"
- 38
result! Result.new(uri, method, original_uri, request, response, options)
end
end
end
- 1
class Checker < LinkChecker::Checker
- 1
extend ::LinkChecker::Net::HTTP::Config
- 1
attr_accessor(*LinkChecker::Net::HTTP::Config::ATTRIBUTES)
- 1
def initialize(options = {})
- 27
LinkChecker::Net::HTTP::Config::ATTRIBUTES.each do |key|
- 54
send("#{key}=", options[key] || LinkChecker::Net::HTTP::Config.send(key))
end
- 27
super options
end
end
end
end
end
# frozen_string_literal: true
- 1
module LinkChecker
- 1
module Net
- 1
module HTTP
- 1
module Config
- 1
extend self
- 1
ATTRIBUTES = %i[
read_timeout
open_timeout
].freeze
- 1
attr_accessor(*Config::ATTRIBUTES)
- 1
def reset
- 28
self.read_timeout = nil
- 28
self.open_timeout = nil
end
end
- 1
class << self
- 1
def configure
- 1
block_given? ? yield(Config) : Config
end
- 1
def config
Config
end
end
end
end
end
- 1
LinkChecker::Net::HTTP::Config.reset
- 1
module LinkChecker
- 1
module Net
- 1
module HTTP
- 1
class Result < ::LinkChecker::Result
- 1
attr_accessor :request, :response
- 1
def initialize(uri, method, original_uri, request, response, options)
- 38
@request = request
- 38
@response = response
- 38
super uri, method, original_uri, options
end
- 1
def error?
- 15
false
end
- 1
def failure?
- 37
!success? && !redirect?
end
- 1
def code
- 354
@code ||= begin
- 38
response.code.to_i
rescue StandardError
-1
end
end
- 1
def request_headers
- 1
request
end
- 1
def redirect_to
- 9
return nil unless response
- 9
response['Location']
end
- 1
def redirect?
- 82
return false unless response
- 82
[301, 302, 303, 307, 308].include?(code)
end
- 1
def success?
- 117
return false unless response
- 117
code >= 200 && code <= 299
end
end
end
end
end
- 1
module LinkChecker
- 1
class Result
- 1
attr_accessor :uri, :result_uri, :method, :options, :checker
- 1
def initialize(current_uri, method, original_uri, options = {})
- 88
@uri = original_uri
- 88
@result_uri = current_uri
- 88
@method = method
- 88
@options = options
end
- 1
def success?
- 32
false
end
- 1
def failure?
- 18
false
end
- 1
def error?
false
end
- 1
def redirect?
- 28
false
end
- 1
def redirect_to
nil
end
- 1
def request_headers
{}
end
- 1
def code
nil
end
- 1
def error
nil
end
- 1
def to_s
- 90
status_s = if success?
- 24
'OK'
- 66
elsif failure?
- 34
'FAIL'
- 32
elsif redirect?
- 18
'REDIRECT'
else
- 14
'ERROR'
end
- 90
"#{method} #{uri}#{result_uri == uri ? nil : ' (' + result_uri.to_s + ')'}: #{status_s} (#{code})"
end
end
- 1
class ResultError < Result
- 1
attr_accessor :error
- 1
def initialize(uri, method, original_uri, error, options = {})
- 12
@error = error
- 12
super uri, method, original_uri, options
end
- 1
def error?
- 20
true
end
- 1
def code
- 14
error.class.name
end
end
end
- 1
module LinkChecker
- 1
class Task
- 1
include LinkChecker::Callbacks
- 1
attr_reader :uri, :original_uri, :method, :logger, :options, :checker
- 1
def initialize(checker, uri, method, original_uri, options = {})
- 80
@checker = checker
- 80
@logger = checker.logger
- 80
@uri = uri
- 80
@original_uri = original_uri || @uri
- 80
@method = method
- 80
@options = options
end
- 1
def run!
raise NotImplementedError
end
end
end
- 1
module LinkChecker
- 1
class Tasks
- 1
include LinkChecker::Callbacks
- 1
attr_reader :result, :uri, :original_uri
- 1
def initialize(checker, task_klass, uri, methods, options = {})
- 52
@uri = uri
- 52
@retries_left = checker.retries
- 52
@methods_left = methods.dup
- 52
@methods = methods.dup
- 52
@task_klass = task_klass
- 52
@checker = checker
- 52
@logger = checker.logger
- 52
@redirects = [uri]
- 52
@options = options
- 52
raise ArgumentError, :tasks_klass unless @task_klass && @task_klass < ::LinkChecker::Task
end
- 1
def new_task(uri, method, original_uri, options)
- 80
task_klass.new(checker, uri, method, original_uri, options)
end
- 1
def execute!
- 98
if retry?
- 10
@retries_left -= 1
- 10
retry! @result
- 10
_queue_task(uri, method, original_uri || uri, options)
- 88
elsif methods_left.any?
- 60
@method = methods_left.shift
- 60
@redirects = [uri]
- 60
@uri = URI(@uri) unless @uri.is_a?(URI)
- 56
_queue_task(uri, method, original_uri || uri, options)
- 28
elsif @result && result.error?
- 12
error! @result
else
- 16
failure! @result
end
rescue StandardError => e
- 8
logger.error("#{self}##{__method__}") { e }
- 4
_handle_result ResultError.new(uri, method, original_uri || uri, e, options)
end
- 1
private
- 1
attr_reader :logger, :methods_left, :options, :task_klass, :redirects, :checker, :method
- 1
def retries
checker.retries
end
- 1
def first_time?
- 98
!!method.nil?
end
- 1
def retries_left
- 46
@retries_left ||= retries
end
- 1
def retry?
- 98
!first_time? && retries_left > 0
end
- 1
def _queue_task(uri, method, original_uri, options = {})
- 80
task = new_task(uri, method, original_uri, options)
- 80
task.on :result do |result|
- 78
_handle_result result
end
- 80
task.run!
rescue StandardError => e
- 4
logger.error("#{self}##{__method__}") { e }
- 2
_handle_result ResultError.new(uri, method, original_uri, e, options)
end
- 1
def _handle_result(result)
- 88
@result = result
- 88
logger.info "#{' ' * (redirects.count - 1)}#{result}"
- 88
result! result
- 88
if result.redirect?
- 18
redirect! result
- 18
redirected_to_uri = URI.join(uri, result.redirect_to)
- 18
if redirects.include?(redirected_to_uri)
- 4
raise LinkChecker::Errors::RedirectLoopError,
redirects.push(redirected_to_uri)
end
- 14
redirects << redirected_to_uri
- 14
_queue_task(redirected_to_uri, result.method, uri, options)
- 70
elsif result.success?
- 24
success! result
else
- 46
execute!
end
rescue StandardError => e
- 8
logger.error("#{self}##{__method__}") { e }
- 4
_handle_result ResultError.new(result.uri, result.method, result.result_uri, e, options)
end
end
end
- 1
require_relative 'hydra/config'
- 1
require_relative 'hydra/checker'
- 1
require_relative 'hydra/result'
- 1
module LinkChecker
- 1
module Typhoeus
- 1
module Hydra
- 1
class Task < ::LinkChecker::Task
- 1
def run!
- 40
request = ::Typhoeus::Request.new(
uri, {
method: method,
followlocation: false,
timeout: checker.timeout,
connecttimeout: checker.connecttimeout,
headers: {
'User-Agent' => checker.user_agent
}
}
)
- 40
request.on_complete do |response|
- 40
if response.timed_out?
- 2
logger.debug "#{method} #{uri}: #{response.return_code}"
- 2
result! ResultError.new(uri, method, original_uri, Timeout::Error.new, options)
else
- 38
logger.debug "#{method} #{uri}: #{response.code}"
- 38
result! Result.new(uri, method, original_uri, request, response, options)
end
end
- 40
checker._queue(request)
end
end
- 1
class Checker < LinkChecker::Checker
- 1
extend ::LinkChecker::Typhoeus::Hydra::Config
- 1
attr_accessor(*LinkChecker::Typhoeus::Hydra::Config::ATTRIBUTES)
- 1
def initialize(options = {})
- 27
LinkChecker::Typhoeus::Hydra::Config::ATTRIBUTES.each do |key|
- 54
send("#{key}=", options[key] || LinkChecker::Typhoeus::Hydra::Config.send(key))
end
- 27
@hydra = ::Typhoeus::Hydra.new(options[:hydra] || { max_concurrency: 10 })
- 27
super options
end
- 1
def run
@hydra.run
end
- 1
def _queue(request)
- 40
@hydra.queue(request)
end
end
end
end
end
# frozen_string_literal: true
- 1
module LinkChecker
- 1
module Typhoeus
- 1
module Hydra
- 1
module Config
- 1
extend self
- 1
ATTRIBUTES = %i[
timeout
connecttimeout
].freeze
- 1
attr_accessor(*Config::ATTRIBUTES)
- 1
def reset
- 1
self.timeout = 60
- 1
self.connecttimeout = 10
end
end
- 1
class << self
- 1
def configure
- 1
block_given? ? yield(Config) : Config
end
- 1
def config
Config
end
end
end
end
end
- 1
LinkChecker::Typhoeus::Hydra::Config.reset
- 1
module LinkChecker
- 1
module Typhoeus
- 1
module Hydra
- 1
class Result < ::LinkChecker::Result
- 1
attr_accessor :request, :response
- 1
def initialize(uri, method, original_uri, request, response, options)
- 38
@request = request
- 38
@response = response
- 38
super uri, method, original_uri, options
end
- 1
def error?
- 38
false
end
- 1
def failure?
- 37
!success? && !redirect? && !error?
end
- 1
def code
- 354
@code ||= begin
- 38
response.code.to_i
rescue StandardError
-1
end
end
- 1
def request_headers
- 1
request.options[:headers]
end
- 1
def redirect_to
- 9
return nil unless response
- 9
response.headers['Location']
end
- 1
def redirect?
- 82
return false unless response
- 82
[301, 302, 303, 307, 308].include?(code)
end
- 1
def success?
- 117
return false unless response
- 117
code >= 200 && code <= 299
end
end
end
end
end
# frozen_string_literal: true
- 1
require 'spec_helper'
- 1
describe LinkChecker::Checker do
- 1
context 'config' do
- 1
it 'requires at least one method' do
- 2
expect { LinkChecker::Checker.new(methods: []) }.to raise_error ArgumentError, 'Missing methods.'
end
end
end
# frozen_string_literal: true
- 1
require 'spec_helper'
- 1
describe LinkChecker::Config do
- 1
describe '#configure' do
- 1
context 'methods' do
- 1
before do
- 1
LinkChecker.configure do |config|
- 1
config.methods = %w[GET]
end
end
- 1
it 'sets methods' do
- 1
expect(LinkChecker.config.methods).to eq %w[GET]
end
end
- 1
context 'retries' do
- 1
it 'requires a positive integer' do
- 2
expect { LinkChecker.config.retries = -1 }.to raise_error ArgumentError, 'Invalid number of retries: -1'
end
end
end
- 1
describe 'defaults' do
- 1
it 'sets methods' do
- 1
expect(LinkChecker.config.methods).to eq %w[HEAD GET]
end
- 1
it 'sets user agent' do
- 1
expect(LinkChecker.config.user_agent).to eq "Ruby Link Checker/#{LinkChecker::VERSION}"
end
- 1
it 'does not set logger' do
- 1
expect(LinkChecker.config.logger).to be nil
end
- 1
it 'sets retries' do
- 1
expect(LinkChecker.config.retries).to eq 0
end
end
end
# frozen_string_literal: true
- 1
require 'spec_helper'
- 1
describe LinkChecker::Net::HTTP::Checker do
- 1
before :all do
- 1
VCR.configure do |config|
- 1
config.hook_into :webmock
end
end
- 1
after do
- 27
LinkChecker::Net::HTTP::Config.reset
end
- 1
it_behaves_like 'a link checker'
- 1
context 'with timeout options', vcr: { cassette_name: '200' } do
- 1
before do
- 1
LinkChecker::Net::HTTP.configure do |config|
- 1
config.read_timeout = 5
- 1
config.open_timeout = 10
end
- 1
expect_any_instance_of(Net::HTTP).to receive(:read_timeout=).with(5)
- 1
expect_any_instance_of(Net::HTTP).to receive(:open_timeout=).with(10)
end
- 1
include_context 'with url'
- 1
it 'creates requests with a default timeout' do
- 1
expect(result.success?).to be true
end
end
- 1
context 'timeout' do
- 1
before do
- 2
stub_request(:get, 'https://www.example.org/').to_timeout
end
- 1
include_context 'with url'
- 1
around do |example|
- 4
VCR.turned_off { example.run }
end
- 1
it 'times out' do
- 1
expect(result.success?).to be false
- 1
expect(result.error?).to be true
- 1
expect(result.to_s).to eq 'GET https://www.example.org: ERROR (Net::OpenTimeout)'
end
- 1
context 'with metadata' do
- 2
let(:options) { { foo: :bar } }
- 1
it 'times out' do
- 1
expect(result.error?).to be true
- 1
expect(result.options).to eq(foo: :bar)
end
end
end
end
# frozen_string_literal: true
- 1
require 'spec_helper'
- 1
describe LinkChecker::Typhoeus::Hydra::Checker do
- 1
module TestLinkChecker
- 1
class Task < LinkChecker::Typhoeus::Hydra::Task; end
- 1
class LinkChecker < LinkChecker::Typhoeus::Hydra::Checker
- 1
def check(url, options = {})
- 26
super url, options
- 26
@hydra.run
end
end
end
- 1
before :all do
- 1
VCR.configure do |config|
- 1
config.hook_into :typhoeus
end
end
- 1
describe TestLinkChecker::LinkChecker do
- 1
it_behaves_like 'a link checker'
- 1
context 'with timeout options', vcr: { cassette_name: '200' } do
- 1
before do
- 1
LinkChecker::Typhoeus::Hydra.configure do |config|
- 1
config.timeout = 5
- 1
config.connecttimeout = 10
end
- 1
expect(Typhoeus::Request).to receive(:new).with(
URI(url),
hash_including(timeout: 5, connecttimeout: 10)
).and_call_original
end
- 1
include_context 'with url'
- 1
it 'creates requests with a default timeout' do
- 1
expect(result.success?).to be true
end
end
- 1
context 'timeout', vcr: { cassette_name: '200' } do
- 1
before do
- 2
allow_any_instance_of(Typhoeus::Response).to receive(:timed_out?).and_return(true)
end
- 1
include_context 'with url'
- 1
it 'times out' do
- 1
expect(result.success?).to be false
- 1
expect(result.error?).to be true
- 1
expect(result.to_s).to eq 'GET https://www.example.org: ERROR (Timeout::Error)'
end
- 1
context 'with metadata' do
- 2
let(:options) { { foo: :bar } }
- 1
it 'times out' do
- 1
expect(result.error?).to be true
- 1
expect(result.options).to eq(foo: :bar)
end
end
end
end
end
# frozen_string_literal: true
- 1
require 'spec_helper'
- 1
describe LinkChecker do
- 1
it 'has a version' do
- 1
expect(LinkChecker::VERSION).not_to be_nil
end
end
- 1
RSpec.configure do |config|
- 1
config.before do
- 64
LinkChecker::Logger.default.level = Logger::DEBUG
end
- 1
config.after do
- 64
LinkChecker::Config.reset
end
end
- 1
shared_context 'a link checker' do
- 2
context 'user-agent' do
- 2
subject do
- 2
described_class.new(user_agent: 'user/agent')
end
- 2
it 'updates user-agent' do
- 2
expect(subject.user_agent).to eq 'user/agent'
end
end
- 2
context 'check' do
- 44
let(:url) { 'https://www.example.org' }
- 2
include_context 'with result'
- 2
context 'with metadata' do
- 2
before do
- 8
subject.check(url, foo: 'bar')
end
- 2
context 'GET' do
- 2
subject do
- 8
described_class.new(methods: ['GET'])
end
- 2
context 'check' do
- 2
context '200', vcr: { cassette_name: '200' } do
- 2
it 'passes metadata' do
- 2
expect(result.options).to eq(foo: 'bar')
end
end
- 2
context '404', vcr: { cassette_name: '404' } do
- 2
it 'passes metadata' do
- 2
expect(result.options).to eq(foo: 'bar')
end
end
- 2
context 'error', vcr: { cassette_name: '404' } do
- 4
let(:url) { '\/invalid-url' }
- 2
it 'passes metadata' do
- 2
expect(result.options).to eq(foo: 'bar')
end
end
- 2
context 'a redirect loop', vcr: { cassette_name: '301+301' } do
- 2
it 'passes metadata' do
- 2
expect(result.options).to eq(foo: 'bar')
end
end
end
end
end
- 2
context 'without results' do
- 2
before do
- 2
subject.check(url, foo: 'bar')
end
- 2
context 'GET' do
- 2
subject do
- 2
described_class.new(results: false, methods: ['GET'])
end
- 2
context 'check' do
- 2
context 'a valid URI that returns a 200', vcr: { cassette_name: '200' } do
- 2
it 'passes metadata' do
- 2
expect(subject.results).to be_nil
end
end
end
end
end
- 2
context 'without metadata' do
- 2
before do
- 36
subject.check(url)
end
- 2
context 'GET' do
- 2
subject do
- 20
described_class.new(methods: ['GET'])
end
- 2
context 'check' do
- 2
context 'a valid URI that returns a 200', vcr: { cassette_name: '200' } do
- 2
it 'sets user agent' do
- 2
expect(result.request_headers['User-Agent']).to eq "Ruby Link Checker/#{LinkChecker::VERSION}"
end
- 2
it 'returns all metadata' do
- 2
expect(result.options).to eq({})
end
- 2
it 'returns results' do
- 2
expect(subject.results).to eq(
error: [],
failure: [],
success: [
result
]
)
end
- 2
it 'succeeds' do
- 2
expect(result.success?).to be true
- 2
expect(result.error?).to be false
- 2
expect(result.failure?).to be false
- 2
expect(result.uri).to eq URI(url)
- 2
expect(subject).to have_received(:called!).with(:result, result)
- 2
expect(subject).to have_received(:called!).with(:success, result)
end
end
- 2
context 'a 404', vcr: { cassette_name: '404', allow_playback_repeats: true } do
- 2
it 'fails' do
- 2
expect(result.success?).to be false
- 2
expect(result.error?).to be false
- 2
expect(result.failure?).to be true
- 2
expect(result.uri).to eq URI(url)
- 2
expect(result.response.code.to_i).to eq 404
- 2
expect(subject).to have_received(:called!).with(:failure, result)
end
- 2
context 'with 0 retries' do
- 2
subject do
- 2
described_class.new(methods: ['GET'], retries: 0)
end
- 2
it 'fails' do
- 2
expect(result.success?).to be false
- 2
expect(result.error?).to be false
- 2
expect(result.failure?).to be true
- 2
expect(result.uri).to eq URI(url)
- 2
expect(result.response.code.to_i).to eq 404
- 2
expect(subject).to have_received(:called!).with(:failure, result).once
- 2
expect(subject).not_to have_received(:called!).with(:retry, anything)
end
end
- 2
context 'with 1 retry' do
- 2
subject do
- 2
described_class.new(methods: ['GET'], retries: 1)
end
- 2
it 'fails' do
- 2
expect(result.success?).to be false
- 2
expect(result.error?).to be false
- 2
expect(result.failure?).to be true
- 2
expect(result.uri).to eq URI(url)
- 2
expect(result.response.code.to_i).to eq 404
- 2
expect(subject).to have_received(:called!).with(:failure, result).once
- 2
expect(subject).to have_received(:called!).with(:retry, anything).once
end
end
- 2
context 'with 2 retries' do
- 2
subject do
- 2
described_class.new(methods: ['GET'], retries: 2)
end
- 2
it 'fails' do
- 2
expect(result.success?).to be false
- 2
expect(result.error?).to be false
- 2
expect(result.failure?).to be true
- 2
expect(result.uri).to eq URI(url)
- 2
expect(result.response.code.to_i).to eq 404
- 2
expect(subject).to have_received(:called!).with(:failure, result).once
- 2
expect(subject).to have_received(:called!).with(:retry, anything).twice
end
end
end
- 2
context 'a redirect on HEAD followed by a 403', vcr: { cassette_name: '301+403' } do
- 2
it 'calls redirect callback' do
- 2
expect(result.success?).to be false
- 2
expect(result.failure?).to be true
- 2
expect(subject).to have_received(:called!).with(:redirect, anything)
- 2
expect(subject).to have_received(:called!).with(:failure, result).once
end
- 2
it 'reports its original and result urls' do
- 2
expect(result.uri.to_s).to eq url
- 2
expect(result.result_uri.to_s).not_to eq url
- 2
expect(result.result_uri.to_s).to eq 'https://www.dblock.org/'
end
end
- 2
context 'a redirect on HEAD followed by a 200', vcr: { cassette_name: '301+200' } do
- 2
it 'calls redirect callback' do
- 2
expect(result.success?).to be true
- 2
expect(result.failure?).to be false
- 2
expect(result.redirect?).to be false
- 2
expect(subject).to have_received(:called!).with(:redirect, anything)
- 2
expect(subject).to have_received(:called!).with(:success, result)
- 2
expect(subject).not_to have_received(:called!).with(:failure, anything)
end
end
- 2
context 'a redirect loop', vcr: { cassette_name: '301+301' } do
- 2
it 'calls redirect callback' do
- 2
expect(result.success?).to be false
- 2
expect(result.failure?).to be false
- 2
expect(result.error?).to be true
- 2
expect(result.error).to be_a LinkChecker::Errors::RedirectLoopError
- 2
expect(result.redirect?).to be false
- 2
expect(subject).to have_received(:called!).with(:redirect, anything).twice
- 2
expect(subject).to have_received(:called!).with(:error, result)
- 2
expect(subject).not_to have_received(:called!).with(:failure, result)
- 2
expect(subject).not_to have_received(:called!).with(:success, result)
end
end
- 2
context 'a retry on 429', vcr: {
cassette_name: '429+200',
match_requests_on: [lambda { |_request, recorded_request|
- 4
@matched ||= []
- 4
if @matched.size + 1 === recorded_request.headers['Index'].first
- 4
@matched << recorded_request
- 4
true
else
false
end
}]
} do
- 2
subject do
- 2
described_class.new(methods: ['GET'], retries: 1)
end
- 2
it 'calls a retry callback' do
- 2
expect(result.success?).to be true
- 2
expect(result.failure?).to be false
- 2
expect(result.redirect?).to be false
- 2
expect(subject).to have_received(:called!).with(:retry, anything)
- 2
expect(subject).to have_received(:called!).with(:success, result)
- 2
expect(subject).not_to have_received(:called!).with(:failure, anything)
- 2
expect(subject).not_to have_received(:called!).with(:error, anything)
end
end
- 2
context 'an invalid URI' do
- 4
let(:url) { '\/invalid-url' }
- 2
it 'fails' do
- 2
expect(result.success?).to be false
- 2
expect(result.failure?).to be false
- 2
expect(result.error?).to be true
- 2
expect(result.uri).to eq url
- 2
expect(subject).to have_received(:called!).with(:result, result)
- 2
expect(subject).to have_received(:called!).with(:error, result)
- 2
expect(subject).not_to have_received(:called!).with(:failure)
- 2
expect(subject).not_to have_received(:called!).with(:success)
end
end
end
- 2
context 'HEAD,GET' do
- 2
subject do
- 6
described_class.new(methods: %w[HEAD GET])
end
- 2
context 'a valid URI that fails on HEAD and succeeds on GET', vcr: { cassette_name: '404+200' } do
- 2
it 'succeeds' do
- 2
expect(result.success?).to be true
- 2
expect(result.error?).to be false
- 2
expect(result.failure?).to be false
- 2
expect(result.uri).to eq URI(url)
- 2
expect(subject).to have_received(:called!).with(:success, result)
- 2
expect(subject).not_to have_received(:called!).with(:failure, result)
end
end
- 2
context 'a valid URI that fails both on HEAD and GET', vcr: { cassette_name: '404+404' } do
- 2
it 'fails' do
- 2
expect(result.success?).to be false
- 2
expect(result.error?).to be false
- 2
expect(result.failure?).to be true
- 2
expect(result.uri).to eq URI(url)
- 2
expect(result.response.code.to_i).to eq 404
- 2
expect(subject).to have_received(:called!).with(:failure, result).once
end
end
- 2
context 'a retry on 429', vcr: {
cassette_name: '429+429+200',
match_requests_on: [lambda { |request, recorded_request|
- 6
@matched ||= []
- 6
if recorded_request.method == request.method && @matched.size + 1 === recorded_request.headers['Index'].first
- 6
@matched << recorded_request
- 6
true
else
false
end
}]
} do
- 2
subject do
- 2
described_class.new(methods: %w[HEAD GET], retries: 1)
end
- 2
it 'executes HEAD twice, then falls back to GET' do
- 2
expect(result.success?).to be true
end
end
- 2
context 'a redirect on HEAD followed by a 400 error succeeds on GET',
vcr: { cassette_name: '301+400+301+200' } do
- 2
it 'calls redirect callback' do
- 2
expect(result.success?).to be true
- 2
expect(result.failure?).to be false
- 2
expect(result.redirect?).to be false
- 2
expect(subject).to have_received(:called!).with(:redirect, anything).twice
- 2
expect(subject).to have_received(:called!).with(:success, result).once
- 2
expect(subject).not_to have_received(:called!).with(:failure, anything)
end
end
end
end
end
end
end
# frozen_string_literal: true
- 1
require 'vcr'
- 1
require 'webmock/rspec'
- 1
VCR.configure do |config|
- 1
config.cassette_library_dir = 'spec/fixtures'
# config.default_cassette_options = { record: :new_episodes }
- 1
config.configure_rspec_metadata!
- 1
config.before_record do |i|
i.response.body.force_encoding('UTF-8')
end
end
- 1
shared_context 'with result' do
- 6
before do
- 52
allow(subject).to receive(:called!)
- 52
subject.on do |event, *data|
- 168
subject.called! event, *data
end
- 52
subject.on :result do |result|
- 88
@result = result
end
end
- 6
let(:result) do
- 50
@result
end
end
- 1
shared_context 'with url' do
- 4
subject do
- 6
described_class.new(methods: ['GET'])
end
- 8
let(:options) { {} }
- 10
let(:url) { 'https://www.example.org' }
- 4
include_context 'with result'
- 4
before do
- 6
subject.check(url, options)
end
end