New Upstream Snapshot - ruby-github-pages-health-check

Ready changes

Summary

Merged new upstream version: 1.18.1 (was: 1.16.1).

Resulting package

Built on 2022-12-17T12:29 (took 5m59s)

The resulting binary packages can be installed (if you have the apt repository enabled) by running one of:

apt install -t fresh-snapshots ruby-github-pages-health-check

Lintian Result

Diff

diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..b844b14
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1 @@
+Gemfile.lock
diff --git a/.gitignore b/.gitignore
deleted file mode 100644
index b2f83a8..0000000
--- a/.gitignore
+++ /dev/null
@@ -1,7 +0,0 @@
-/*.gem
-*.lock
-.bundle
-vendor/gems
-/bin
-.env
-spec/examples.txt
diff --git a/.rubocop.yml b/.rubocop.yml
index 0f3d8a7..bad031d 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -17,7 +17,7 @@
 #
 
 AllCops:
-  TargetRubyVersion: 2.2
+  TargetRubyVersion: 2.6
   Exclude:
     - 'bin/**/*'
     - 'script/**/*'
@@ -30,10 +30,6 @@ Layout/EndAlignment:
 Lint/UnreachableCode:
   Severity: error
 
-Style/StringLiterals:
-  EnforcedStyle: double_quotes
-  Severity: error
-
 Style/StringLiteralsInInterpolation:
   EnforcedStyle: double_quotes
 
@@ -41,10 +37,10 @@ Style/HashSyntax:
   EnforcedStyle: hash_rockets
   Severity: error
 
-Layout/AlignHash:
+Layout/HashAlignment:
   SupportedLastArgumentHashStyles: always_ignore
 
-Layout/AlignParameters:
+Layout/ParameterAlignment:
   Enabled: false # This is usually true, but we often want to roll back to
                  # the start of a line.
 
@@ -55,13 +51,8 @@ Style/Attr:
 Style/ClassAndModuleChildren:
   Enabled: false # module X<\n>module Y is just as good as module X::Y.
 
-Style/PercentLiteralDelimiters:
-  PreferredDelimiters:
-    '%w': '{}'
-    '%r': '{}'
-
-Metrics/LineLength:
-  Max: 90
+Layout/LineLength:
+  Max: 120
   Severity: warning
   Exclude:
     - github-pages-health-check.gemspec
@@ -99,13 +90,12 @@ Naming/FileName: #Rubocop doesn't like the Git*H*ub namespace
   Enabled: false
 
 Metrics/ParameterLists: { Max: 4 }
-Metrics/AbcSize: { Max: 20 }
 
-Layout/IndentHash: { EnforcedStyle: consistent }
+Layout/FirstHashElementIndentation: { EnforcedStyle: consistent }
 Layout/MultilineMethodCallIndentation: { EnforcedStyle: indented }
 Layout/MultilineOperationIndentation: { EnforcedStyle: indented }
 Layout/FirstParameterIndentation: { EnforcedStyle: consistent }
-Layout/IndentArray: { EnforcedStyle: consistent }
+Layout/FirstArrayElementIndentation: { EnforcedStyle: consistent }
 Layout/ExtraSpacing: { AllowForAlignment: true }
 Style/SignalException: { EnforcedStyle: only_raise }
 Style/StringLiterals: { EnforcedStyle: double_quotes }
@@ -155,3 +145,10 @@ Style/FrozenStringLiteralComment:
 
 Gemspec/RequiredRubyVersion:
   Enabled: false
+
+Style/HashEachMethods:
+  Enabled: false
+Style/HashTransformKeys:
+  Enabled: false
+Style/HashTransformValues:
+  Enabled: false
diff --git a/.ruby-version b/.ruby-version
new file mode 100644
index 0000000..860487c
--- /dev/null
+++ b/.ruby-version
@@ -0,0 +1 @@
+2.7.1
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 66aa871..0000000
--- a/.travis.yml
+++ /dev/null
@@ -1,16 +0,0 @@
-language: ruby
-rvm:
-  - 2.4
-  - 2.5
-  - 2.6
-
-before_install:
-  - gem install bundler
-
-script: "script/cibuild"
-
-notifications:
-  email: false
-
-cache: bundler
-sudo: false
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..f548631
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,18 @@
+ARG RUBY_VERSION
+FROM ruby:$RUBY_VERSION-slim
+RUN set -ex \
+  && gem update --system --silent --quiet \
+  && apt-get update -y \
+  && apt-get upgrade -y \
+  && apt-get install -y \
+    build-essential \
+    git \
+    libcurl4-openssl-dev \
+  && apt-get clean
+WORKDIR /app/github-pages-health-check
+COPY Gemfile .
+COPY github-pages-health-check.gemspec .
+COPY lib/github-pages-health-check/version.rb lib/github-pages-health-check/version.rb
+RUN bundle install
+COPY . .
+ENTRYPOINT [ "/bin/bash" ]
diff --git a/Gemfile b/Gemfile
index be173b2..7ddd7c1 100644
--- a/Gemfile
+++ b/Gemfile
@@ -2,4 +2,14 @@
 
 source "https://rubygems.org"
 
+group :development do
+  gem "dotenv", "~> 2.7"
+  gem "gem-release", "~> 2.1"
+  gem "pry", "~> 0.10"
+  gem "pry-byebug"
+  gem "rspec", "~> 3.0"
+  gem "rubocop", "~> 0.52"
+  gem "webmock", "~> 3.8"
+end
+
 gemspec
diff --git a/README.md b/README.md
index dc0b658..1eb0b8d 100644
--- a/README.md
+++ b/README.md
@@ -81,3 +81,40 @@ check = GitHubPages::HealthCheck::Site.new "github/pages-health-check", access_t
 ```
 
 You can also set `OCTOKIT_ACCESS_TOKEN` as an environmental variable, or via a `.env` file in your working directory.
+
+### Command Line
+
+```
+./script/check pages.github.com
+
+host: pages.github.com
+uri: https://pages.github.com/
+nameservers: :default
+dns_resolves?: true
+proxied?: false
+cloudflare_ip?: false
+fastly_ip?: false
+old_ip_address?: false
+a_record?: false
+cname_record?: true
+mx_records_present?: false
+valid_domain?: true
+apex_domain?: false
+should_be_a_record?: false
+cname_to_github_user_domain?: true
+cname_to_pages_dot_github_dot_com?: false
+cname_to_fastly?: false
+pointed_to_github_pages_ip?: false
+non_github_pages_ip_present?: false
+pages_domain?: true
+served_by_pages?: true
+valid?: true
+reason:
+https?: true
+enforces_https?: true
+https_error:
+https_eligible?: true
+caa_error:
+dns_zone_soa?: false
+dns_zone_ns?: false
+```
diff --git a/config/cloudflare-ips.txt b/config/cloudflare-ips.txt
index 2800771..fd160bd 100644
--- a/config/cloudflare-ips.txt
+++ b/config/cloudflare-ips.txt
@@ -9,6 +9,14 @@
 197.234.240.0/22
 198.41.128.0/17
 162.158.0.0/15
-104.16.0.0/12
+104.16.0.0/13
+104.24.0.0/14
 172.64.0.0/13
 131.0.72.0/22
+2400:cb00::/32
+2606:4700::/32
+2803:f800::/32
+2405:b500::/32
+2405:8100::/32
+2a06:98c0::/29
+2c0f:f248::/32
\ No newline at end of file
diff --git a/config/fastly-ips.txt b/config/fastly-ips.txt
index daac7eb..29d9649 100644
--- a/config/fastly-ips.txt
+++ b/config/fastly-ips.txt
@@ -4,9 +4,18 @@
 103.245.222.0/23
 103.245.224.0/24
 104.156.80.0/20
+140.248.64.0/18
+140.248.128.0/17
+146.75.0.0/17
 151.101.0.0/16
 157.52.64.0/18
+167.82.0.0/17
+167.82.128.0/20
+167.82.160.0/20
+167.82.224.0/20
 172.111.64.0/18
 185.31.16.0/22
 199.27.72.0/21
-199.232.0.0/16
\ No newline at end of file
+199.232.0.0/16
+2a04:4e40::/32
+2a04:4e42::/32
\ No newline at end of file
diff --git a/debian/changelog b/debian/changelog
index c9b6756..fb549c8 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,11 +1,14 @@
-ruby-github-pages-health-check (1.16.1-4) UNRELEASED; urgency=medium
+ruby-github-pages-health-check (1.18.1-1) UNRELEASED; urgency=medium
 
   * Remove constraints unnecessary since buster (oldstable):
     + ruby-github-pages-health-check: Drop versioned constraint on ruby-dnsruby
       in Depends.
   * Update standards version to 4.6.1, no changes needed.
+  * New upstream release.
+  * Drop patch 0002-Relax-version-dependency-on-publicsuffix-gem.patch, present
+    upstream.
 
- -- Debian Janitor <janitor@jelmer.uk>  Wed, 21 Sep 2022 16:09:37 -0000
+ -- Debian Janitor <janitor@jelmer.uk>  Sat, 17 Dec 2022 12:25:57 -0000
 
 ruby-github-pages-health-check (1.16.1-3) unstable; urgency=medium
 
diff --git a/debian/patches/0001-Replace-git-execution-from-gemspec.patch b/debian/patches/0001-Replace-git-execution-from-gemspec.patch
index 0dd0a0f..8ec8e45 100644
--- a/debian/patches/0001-Replace-git-execution-from-gemspec.patch
+++ b/debian/patches/0001-Replace-git-execution-from-gemspec.patch
@@ -6,10 +6,10 @@ Subject: Replace git execution from gemspec
  github-pages-health-check.gemspec | 2 +-
  1 file changed, 1 insertion(+), 1 deletion(-)
 
-diff --git a/github-pages-health-check.gemspec b/github-pages-health-check.gemspec
-index b6ee220..bab1c19 100644
---- a/github-pages-health-check.gemspec
-+++ b/github-pages-health-check.gemspec
+Index: ruby-github-pages-health-check.git/github-pages-health-check.gemspec
+===================================================================
+--- ruby-github-pages-health-check.git.orig/github-pages-health-check.gemspec
++++ ruby-github-pages-health-check.git/github-pages-health-check.gemspec
 @@ -13,7 +13,7 @@ Gem::Specification.new do |s|
    s.email                 = "support@github.com"
    s.homepage              = "https://github.com/github/github-pages-health-check"
diff --git a/debian/patches/0002-Relax-version-dependency-on-publicsuffix-gem.patch b/debian/patches/0002-Relax-version-dependency-on-publicsuffix-gem.patch
deleted file mode 100644
index f6bbaf1..0000000
--- a/debian/patches/0002-Relax-version-dependency-on-publicsuffix-gem.patch
+++ /dev/null
@@ -1,21 +0,0 @@
-From: =?utf-8?q?C=C3=A9dric_Boutillier?= <boutil@debian.org>
-Date: Tue, 26 Oct 2021 14:46:02 +0200
-Subject: Relax version dependency on publicsuffix gem
-
----
- github-pages-health-check.gemspec | 2 +-
- 1 file changed, 1 insertion(+), 1 deletion(-)
-
-diff --git a/github-pages-health-check.gemspec b/github-pages-health-check.gemspec
-index bab1c19..4363400 100644
---- a/github-pages-health-check.gemspec
-+++ b/github-pages-health-check.gemspec
-@@ -19,7 +19,7 @@ Gem::Specification.new do |s|
-   s.add_dependency("addressable", "~> 2.3")
-   s.add_dependency("dnsruby", "~> 1.60")
-   s.add_dependency("octokit", "~> 4.0")
--  s.add_dependency("public_suffix", "~> 3.0")
-+  s.add_dependency("public_suffix", ">= 3.0", "< 5.0")
-   s.add_dependency("typhoeus", "~> 1.3")
- 
-   s.add_development_dependency("dotenv", "~> 1.0")
diff --git a/debian/patches/series b/debian/patches/series
index 029c171..2b06a4b 100644
--- a/debian/patches/series
+++ b/debian/patches/series
@@ -1,2 +1 @@
 0001-Replace-git-execution-from-gemspec.patch
-0002-Relax-version-dependency-on-publicsuffix-gem.patch
diff --git a/github-pages-health-check.gemspec b/github-pages-health-check.gemspec
index b6ee220..87ad564 100644
--- a/github-pages-health-check.gemspec
+++ b/github-pages-health-check.gemspec
@@ -19,13 +19,6 @@ Gem::Specification.new do |s|
   s.add_dependency("addressable", "~> 2.3")
   s.add_dependency("dnsruby", "~> 1.60")
   s.add_dependency("octokit", "~> 4.0")
-  s.add_dependency("public_suffix", "~> 3.0")
+  s.add_dependency("public_suffix", ">= 3.0", "< 5.0")
   s.add_dependency("typhoeus", "~> 1.3")
-
-  s.add_development_dependency("dotenv", "~> 1.0")
-  s.add_development_dependency("gem-release", "~> 0.7")
-  s.add_development_dependency("pry", "~> 0.10")
-  s.add_development_dependency("rspec", "~> 3.0")
-  s.add_development_dependency("rubocop", "~> 0.52")
-  s.add_development_dependency("webmock", "~> 1.21")
 end
diff --git a/lib/github-pages-health-check.rb b/lib/github-pages-health-check.rb
index 2b75724..c14e019 100644
--- a/lib/github-pages-health-check.rb
+++ b/lib/github-pages-health-check.rb
@@ -37,19 +37,9 @@ module GitHubPages
     # DNS and HTTP timeout, in seconds
     TIMEOUT = 7
 
-    HUMAN_NAME = "GitHub Pages Health Check".freeze
-    URL = "https://github.com/github/pages-health-check".freeze
-    USER_AGENT = "Mozilla/5.0 (compatible; #{HUMAN_NAME}/#{VERSION}; +#{URL})".freeze
-
-    TYPHOEUS_OPTIONS = {
-      :followlocation => true,
-      :timeout => TIMEOUT,
-      :accept_encoding => "gzip",
-      :method => :head,
-      :headers => {
-        "User-Agent" => USER_AGENT
-      }
-    }.freeze
+    HUMAN_NAME = "GitHub Pages Health Check"
+    URL = "https://github.com/github/pages-health-check"
+    USER_AGENT = "Mozilla/5.0 (compatible; #{HUMAN_NAME}/#{VERSION}; +#{URL})"
 
     # surpress warn-level feedback due to unsupported record types
     def self.without_warnings(&block)
@@ -63,5 +53,27 @@ module GitHubPages
     def self.check(repository_or_domain, access_token: nil)
       Site.new repository_or_domain, :access_token => access_token
     end
+
+    # rubocop:disable Naming/AccessorMethodName (this is not an accessor method)
+    def self.set_proxy(proxy_url)
+      @typhoeus_options = typhoeus_options.merge(:proxy => proxy_url).freeze
+      nil
+    end
+    # rubocop:enable Naming/AccessorMethodName
+
+    def self.typhoeus_options
+      return @typhoeus_options if defined?(@typhoeus_options)
+
+      @typhoeus_options = {
+        :followlocation => true,
+        :timeout => TIMEOUT,
+        :accept_encoding => "gzip",
+        :method => :head,
+        :headers => {
+          "User-Agent" => USER_AGENT
+        },
+        :proxy => nil
+      }.freeze
+    end
   end
 end
diff --git a/lib/github-pages-health-check/caa.rb b/lib/github-pages-health-check/caa.rb
index 3e49774..e9e8a7f 100644
--- a/lib/github-pages-health-check/caa.rb
+++ b/lib/github-pages-health-check/caa.rb
@@ -9,7 +9,7 @@ module GitHubPages
     class CAA
       attr_reader :host, :error, :nameservers
 
-      def initialize(host, nameservers: :default)
+      def initialize(host:, nameservers: :default)
         raise ArgumentError, "host cannot be nil" if host.nil?
 
         @host = host
diff --git a/lib/github-pages-health-check/domain.rb b/lib/github-pages-health-check/domain.rb
index cfc35f6..ff39189 100644
--- a/lib/github-pages-health-check/domain.rb
+++ b/lib/github-pages-health-check/domain.rb
@@ -77,15 +77,25 @@ module GitHubPages
         185.199.111.153
       ).freeze
 
+      CURRENT_IPV6_ADDRESSES = %w(
+        2606:50c0:8000::153
+        2606:50c0:8001::153
+        2606:50c0:8002::153
+        2606:50c0:8003::153
+      ).freeze
+
+      CURRENT_IP_ADDRESSES_ALL =
+        (CURRENT_IP_ADDRESSES + CURRENT_IPV6_ADDRESSES).freeze
+
       HASH_METHODS = %i[
         host uri nameservers dns_resolves? proxied? cloudflare_ip?
-        fastly_ip? old_ip_address? a_record? cname_record?
-        mx_records_present? valid_domain? apex_domain? should_be_a_record?
-        cname_to_github_user_domain? cname_to_pages_dot_github_dot_com?
-        cname_to_fastly? pointed_to_github_pages_ip?
-        non_github_pages_ip_present? pages_domain?
+        fastly_ip? old_ip_address? a_record? aaaa_record? a_record_present? aaaa_record_present?
+        cname_record? mx_records_present? valid_domain? apex_domain?
+        should_be_a_record? cname_to_github_user_domain?
+        cname_to_pages_dot_github_dot_com? cname_to_fastly?
+        pointed_to_github_pages_ip? non_github_pages_ip_present? pages_domain?
         served_by_pages? valid? reason valid_domain? https?
-        enforces_https? https_error https_eligible? caa_error
+        enforces_https? https_error https_eligible? caa_error dns_zone_soa? dns_zone_ns?
       ].freeze
 
       def self.redundant(host)
@@ -100,22 +110,24 @@ module GitHubPages
         @host = normalize_host(host)
         @nameservers = nameservers
         @resolver = GitHubPages::HealthCheck::Resolver.new(self.host,
-          :nameservers => nameservers)
+                                                           :nameservers => nameservers)
       end
 
       # Runs all checks, raises an error if invalid
+      # rubocop:disable Metrics/AbcSize
       def check!
-        raise Errors::InvalidDomainError, :domain => self unless valid_domain?
-        raise Errors::InvalidDNSError, :domain => self    unless dns_resolves?
-        raise Errors::DeprecatedIPError, :domain => self if deprecated_ip?
+        raise Errors::InvalidDomainError.new :domain => self unless valid_domain?
+        raise Errors::InvalidDNSError.new :domain => self    unless dns_resolves?
+        raise Errors::DeprecatedIPError.new :domain => self  if deprecated_ip?
         return true if proxied?
-        raise Errors::InvalidARecordError, :domain => self    if invalid_a_record?
-        raise Errors::InvalidCNAMEError, :domain => self      if invalid_cname?
-        raise Errors::InvalidAAAARecordError, :domain => self if invalid_aaaa_record?
-        raise Errors::NotServedByPagesError, :domain => self  unless served_by_pages?
+        raise Errors::InvalidARecordError.new :domain => self    if invalid_a_record?
+        raise Errors::InvalidCNAMEError.new :domain => self      if invalid_cname?
+        raise Errors::InvalidAAAARecordError.new :domain => self if invalid_aaaa_record?
+        raise Errors::NotServedByPagesError.new :domain => self  unless served_by_pages?
 
         true
       end
+      # rubocop:enable Metrics/AbcSize
 
       def deprecated_ip?
         return @deprecated_ip if defined? @deprecated_ip
@@ -126,14 +138,13 @@ module GitHubPages
       def invalid_aaaa_record?
         return @invalid_aaaa_record if defined? @invalid_aaaa_record
 
-        @invalid_aaaa_record = (valid_domain? && should_be_a_record? &&
-                                aaaa_record_present?)
+        @invalid_aaaa_record = (valid_domain? && aaaa_record_present? && !should_be_a_record?)
       end
 
       def invalid_a_record?
         return @invalid_a_record if defined? @invalid_a_record
 
-        @invalid_a_record = (valid_domain? && a_record? && !should_be_a_record?)
+        @invalid_a_record = (valid_domain? && a_record_present? && !should_be_a_record?)
       end
 
       def invalid_cname?
@@ -162,7 +173,10 @@ module GitHubPages
       # Is this domain an apex domain, meaning a CNAME would be innapropriate
       def apex_domain?
         return @apex_domain if defined?(@apex_domain)
-        return unless valid_domain?
+
+        return false unless valid_domain?
+
+        return true if dns_zone_soa? && dns_zone_ns?
 
         # PublicSuffix.domain pulls out the apex-level domain name.
         # E.g. PublicSuffix.domain("techblog.netflix.com") # => "netflix.com"
@@ -175,6 +189,30 @@ module GitHubPages
                             :ignore_private => true) == unicode_host
       end
 
+      #
+      # Does the domain have an associated SOA record?
+      #
+      def dns_zone_soa?
+        return @soa_records if defined?(@soa_records)
+        return false unless dns?
+
+        @soa_records = dns.any? do |answer|
+          answer.type == Dnsruby::Types::SOA && answer.name.to_s == host
+        end
+      end
+
+      #
+      # Does the domain have assoicated NS records?
+      #
+      def dns_zone_ns?
+        return @ns_records if defined?(@ns_records)
+        return false unless dns?
+
+        @ns_records = dns.any? do |answer|
+          answer.type == Dnsruby::Types::NS && answer.name.to_s == host
+        end
+      end
+
       # Should the domain use an A record?
       def should_be_a_record?
         !pages_io_domain? && (apex_domain? || mx_records_present?)
@@ -184,20 +222,20 @@ module GitHubPages
         !should_be_a_record?
       end
 
-      # Is the domain's first response an A record to a valid GitHub Pages IP?
+      # Is the domain's first response an A or AAAA record to a valid GitHub Pages IP?
       def pointed_to_github_pages_ip?
-        a_record? && CURRENT_IP_ADDRESSES.include?(dns.first.address.to_s)
+        return false unless address_record?
+
+        CURRENT_IP_ADDRESSES_ALL.include?(dns.first.address.to_s.downcase)
       end
 
-      # Are any of the domain's A records pointing elsewhere?
+      # Are any of the domain's A or AAAA records pointing elsewhere?
       def non_github_pages_ip_present?
         return unless dns?
 
-        a_records = dns.select { |answer| answer.type == Dnsruby::Types::A }
-
-        a_records.any? { |answer| !github_pages_ip?(answer.address.to_s) }
-
-        false
+        dns
+          .select { |a| Dnsruby::Types::A == a.type || Dnsruby::Types::AAAA == a.type }
+          .any? { |a| !github_pages_ip?(a.address.to_s) }
       end
 
       # Is the domain's first response a CNAME to a pages domain?
@@ -276,7 +314,9 @@ module GitHubPages
         Dnsruby::Types::A,
         Dnsruby::Types::AAAA,
         Dnsruby::Types::CNAME,
-        Dnsruby::Types::MX
+        Dnsruby::Types::MX,
+        Dnsruby::Types::NS,
+        Dnsruby::Types::SOA
       ].freeze
 
       # Returns an array of DNS answers
@@ -314,15 +354,32 @@ module GitHubPages
 
       # Is this domain's first response an A record?
       def a_record?
+        return @is_a_record if defined?(@is_a_record)
+        return unless dns?
+
+        @is_a_record = Dnsruby::Types::A == dns.first.type
+      end
+
+      # Is this domain's first response an AAAA record?
+      def aaaa_record?
+        return @is_aaaa_record if defined?(@is_aaaa_record)
+        return unless dns?
+
+        @is_aaaa_record = Dnsruby::Types::AAAA == dns.first.type
+      end
+
+      # Does this domain has an A record setup (not necessarily as the first record)?
+      def a_record_present?
         return unless dns?
 
-        dns.first.type == Dnsruby::Types::A
+        dns.any? { |answer| answer.type == Dnsruby::Types::A && answer.name.to_s == host }
       end
 
+      # Does this domain has an AAAA record setup (not necessarily as the first record)?
       def aaaa_record_present?
         return unless dns?
 
-        dns.any? { |answer| answer.type == Dnsruby::Types::AAAA }
+        dns.any? { |answer| answer.type == Dnsruby::Types::AAAA && answer.name.to_s == host }
       end
 
       # Is this domain's first response a CNAME record?
@@ -337,6 +394,8 @@ module GitHubPages
       # The domain to which this domain's CNAME resolves
       # Returns nil if the domain is not a CNAME
       def cname
+        return unless dns?
+
         cnames = dns.take_while { |answer| answer.type == Dnsruby::Types::CNAME }
         return if cnames.empty?
 
@@ -354,11 +413,10 @@ module GitHubPages
         return unless dns_resolves?
 
         @served_by_pages = begin
-          return false unless response.mock? || response.return_code == :ok
           return true if response.headers["Server"] == "GitHub.com"
 
           # Typhoeus mangles the case of the header, compare insensitively
-          response.headers.any? { |k, _v| k =~ /X-GitHub-Request-Id/i }
+          response.headers.any? { |k, _v| k.downcase == "x-github-request-id" }
         end
       end
 
@@ -391,10 +449,11 @@ module GitHubPages
       def https_eligible?
         # Can't have any IP's which aren't GitHub's present.
         return false if non_github_pages_ip_present?
-        # Can't have any AAAA records present
-        return false if aaaa_record_present?
-        # Must be a CNAME or point to our IPs.
 
+        # Can't have underscores in the domain name (Let's Encrypt does not allow it)
+        return false if host.include?("_")
+
+        # Must be a CNAME or point to our IPs.
         # Only check the one domain if a CNAME. Don't check the parent domain.
         return true if cname_to_github_user_domain?
 
@@ -404,27 +463,34 @@ module GitHubPages
 
       # Any errors querying CAA records
       def caa_error
-        return nil unless caa.errored?
+        return nil unless caa&.errored?
 
         caa.error.class.name
       end
 
       private
 
+      def address_record?
+        a_record? || aaaa_record?
+      end
+
       def caa
-        @caa ||= GitHubPages::HealthCheck::CAA.new(host, :nameservers => nameservers)
+        @caa ||= GitHubPages::HealthCheck::CAA.new(
+          :host => cname&.host || host,
+          :nameservers => nameservers
+        )
       end
 
       # The domain's response to HTTP(S) requests, following redirects
       def response
         return @response if defined? @response
 
-        @response = Typhoeus.head(uri, TYPHOEUS_OPTIONS)
+        @response = Typhoeus.head(uri, GitHubPages::HealthCheck.typhoeus_options)
 
         # Workaround for webmock not playing nicely with Typhoeus redirects
         # See https://github.com/bblimke/webmock/issues/237
         if @response.mock? && @response.headers["Location"]
-          @response = Typhoeus.head(response.headers["Location"], TYPHOEUS_OPTIONS)
+          @response = Typhoeus.head(response.headers["Location"], GitHubPages::HealthCheck.typhoeus_options)
         end
 
         @response
@@ -432,13 +498,13 @@ module GitHubPages
 
       # The domain's response to HTTP requests, without following redirects
       def http_response
-        options = TYPHOEUS_OPTIONS.merge(:followlocation => false)
+        options = GitHubPages::HealthCheck.typhoeus_options.merge(:followlocation => false)
         @http_response ||= Typhoeus.head(uri(:scheme => "http"), options)
       end
 
       # The domain's response to HTTPS requests, without following redirects
       def https_response
-        options = TYPHOEUS_OPTIONS.merge(:followlocation => false)
+        options = GitHubPages::HealthCheck.typhoeus_options.merge(:followlocation => false)
         @https_response ||= Typhoeus.head(uri(:scheme => "https"), options)
       end
 
@@ -482,10 +548,12 @@ module GitHubPages
       def cdn_ip?(cdn)
         return unless dns?
 
-        a_records = dns.select { |answer| answer.type == Dnsruby::Types::A }
-        return false if !a_records || a_records.empty?
+        address_records = dns.select do |answer|
+          Dnsruby::Types::A == answer.type || Dnsruby::Types::AAAA == answer.type
+        end
+        return false if !address_records || address_records.empty?
 
-        a_records.all? do |answer|
+        address_records.all? do |answer|
           cdn.controls_ip?(answer.address)
         end
       end
@@ -495,7 +563,7 @@ module GitHubPages
       end
 
       def github_pages_ip?(ip_addr)
-        CURRENT_IP_ADDRESSES.include?(ip_addr)
+        CURRENT_IP_ADDRESSES_ALL.include?(ip_addr&.to_s&.downcase)
       end
     end
   end
diff --git a/lib/github-pages-health-check/error.rb b/lib/github-pages-health-check/error.rb
index f478182..61b4018 100644
--- a/lib/github-pages-health-check/error.rb
+++ b/lib/github-pages-health-check/error.rb
@@ -3,8 +3,8 @@
 module GitHubPages
   module HealthCheck
     class Error < StandardError
-      DOCUMENTATION_BASE = "https://help.github.com".freeze
-      DOCUMENTATION_PATH = "/categories/github-pages-basics/".freeze
+      DOCUMENTATION_BASE = "https://help.github.com"
+      DOCUMENTATION_PATH = "/categories/github-pages-basics/"
       LOCAL_ONLY = false # Error is only used when running locally
 
       attr_reader :repository, :domain
diff --git a/lib/github-pages-health-check/errors.rb b/lib/github-pages-health-check/errors.rb
index 7ef1c04..6c25e80 100644
--- a/lib/github-pages-health-check/errors.rb
+++ b/lib/github-pages-health-check/errors.rb
@@ -1,6 +1,6 @@
 # frozen_string_literal: true
 
-Dir[File.expand_path("errors/*_error.rb", __dir__)].each do |f|
+Dir[File.expand_path("errors/*_error.rb", __dir__)].sort.each do |f|
   require f
 end
 
diff --git a/lib/github-pages-health-check/errors/build_error.rb b/lib/github-pages-health-check/errors/build_error.rb
index 438c4d9..ca83563 100644
--- a/lib/github-pages-health-check/errors/build_error.rb
+++ b/lib/github-pages-health-check/errors/build_error.rb
@@ -4,7 +4,7 @@ module GitHubPages
   module HealthCheck
     module Errors
       class BuildError < GitHubPages::HealthCheck::Error
-        DOCUMENTATION_PATH = "/articles/troubleshooting-jekyll-builds/".freeze
+        DOCUMENTATION_PATH = "/articles/troubleshooting-jekyll-builds/"
         LOCAL_ONLY = true
       end
     end
diff --git a/lib/github-pages-health-check/errors/deprecated_ip_error.rb b/lib/github-pages-health-check/errors/deprecated_ip_error.rb
index d4e5d44..fff3f52 100644
--- a/lib/github-pages-health-check/errors/deprecated_ip_error.rb
+++ b/lib/github-pages-health-check/errors/deprecated_ip_error.rb
@@ -4,7 +4,7 @@ module GitHubPages
   module HealthCheck
     module Errors
       class DeprecatedIPError < GitHubPages::HealthCheck::Error
-        DOCUMENTATION_PATH = "/articles/setting-up-a-custom-domain-with-github-pages/".freeze
+        DOCUMENTATION_PATH = "/articles/setting-up-a-custom-domain-with-github-pages/"
 
         def message
           <<-MSG
diff --git a/lib/github-pages-health-check/errors/invalid_a_record_error.rb b/lib/github-pages-health-check/errors/invalid_a_record_error.rb
index 8563a9e..3becf88 100644
--- a/lib/github-pages-health-check/errors/invalid_a_record_error.rb
+++ b/lib/github-pages-health-check/errors/invalid_a_record_error.rb
@@ -4,7 +4,7 @@ module GitHubPages
   module HealthCheck
     module Errors
       class InvalidARecordError < GitHubPages::HealthCheck::Error
-        DOCUMENTATION_PATH = "/articles/setting-up-a-custom-domain-with-github-pages/".freeze
+        DOCUMENTATION_PATH = "/articles/setting-up-a-custom-domain-with-github-pages/"
 
         def message
           <<-MSG
diff --git a/lib/github-pages-health-check/errors/invalid_aaaa_record_error.rb b/lib/github-pages-health-check/errors/invalid_aaaa_record_error.rb
index 8afa42c..2d8ed7c 100644
--- a/lib/github-pages-health-check/errors/invalid_aaaa_record_error.rb
+++ b/lib/github-pages-health-check/errors/invalid_aaaa_record_error.rb
@@ -4,13 +4,13 @@ module GitHubPages
   module HealthCheck
     module Errors
       class InvalidAAAARecordError < GitHubPages::HealthCheck::Error
-        DOCUMENTATION_PATH = "/articles/setting-up-a-custom-domain-with-github-pages/".freeze
+        DOCUMENTATION_PATH = "/articles/setting-up-a-custom-domain-with-github-pages/"
 
         def message
           <<-MSG
-             Your site's DNS settings are using a custom subdomain, #{domain.host},
-             that's set up with an AAAA record. GitHub Pages currently does not support
-             IPv6.
+          Your site's DNS settings are using a custom subdomain, #{domain.host},
+          that's set up as an AAAA record. We recommend you change this to a CNAME
+          record pointing at #{username}.github.io.
           MSG
         end
       end
diff --git a/lib/github-pages-health-check/errors/invalid_cname_error.rb b/lib/github-pages-health-check/errors/invalid_cname_error.rb
index 5bd3669..e62c04c 100644
--- a/lib/github-pages-health-check/errors/invalid_cname_error.rb
+++ b/lib/github-pages-health-check/errors/invalid_cname_error.rb
@@ -4,7 +4,7 @@ module GitHubPages
   module HealthCheck
     module Errors
       class InvalidCNAMEError < GitHubPages::HealthCheck::Error
-        DOCUMENTATION_PATH = "/articles/setting-up-a-custom-domain-with-github-pages/".freeze
+        DOCUMENTATION_PATH = "/articles/setting-up-a-custom-domain-with-github-pages/"
 
         def message
           <<-MSG
diff --git a/lib/github-pages-health-check/errors/invalid_dns_error.rb b/lib/github-pages-health-check/errors/invalid_dns_error.rb
index 85c197d..9fa9bc8 100644
--- a/lib/github-pages-health-check/errors/invalid_dns_error.rb
+++ b/lib/github-pages-health-check/errors/invalid_dns_error.rb
@@ -4,7 +4,7 @@ module GitHubPages
   module HealthCheck
     module Errors
       class InvalidDNSError < GitHubPages::HealthCheck::Error
-        DOCUMENTATION_PATH = "/articles/setting-up-a-custom-domain-with-github-pages/".freeze
+        DOCUMENTATION_PATH = "/articles/setting-up-a-custom-domain-with-github-pages/"
 
         def message
           "Domain's DNS record could not be retrieved"
diff --git a/lib/github-pages-health-check/errors/invalid_domain_error.rb b/lib/github-pages-health-check/errors/invalid_domain_error.rb
index adbb56c..c33cd25 100644
--- a/lib/github-pages-health-check/errors/invalid_domain_error.rb
+++ b/lib/github-pages-health-check/errors/invalid_domain_error.rb
@@ -4,7 +4,7 @@ module GitHubPages
   module HealthCheck
     module Errors
       class InvalidDomainError < GitHubPages::HealthCheck::Error
-        DOCUMENTATION_PATH = "/articles/setting-up-a-custom-domain-with-github-pages/".freeze
+        DOCUMENTATION_PATH = "/articles/setting-up-a-custom-domain-with-github-pages/"
 
         def message
           "Domain is not a valid domain"
diff --git a/lib/github-pages-health-check/errors/not_served_by_pages_error.rb b/lib/github-pages-health-check/errors/not_served_by_pages_error.rb
index d1b7349..7b3c57c 100644
--- a/lib/github-pages-health-check/errors/not_served_by_pages_error.rb
+++ b/lib/github-pages-health-check/errors/not_served_by_pages_error.rb
@@ -4,7 +4,7 @@ module GitHubPages
   module HealthCheck
     module Errors
       class NotServedByPagesError < GitHubPages::HealthCheck::Error
-        DOCUMENTATION_PATH = "/articles/setting-up-a-custom-domain-with-github-pages/".freeze
+        DOCUMENTATION_PATH = "/articles/setting-up-a-custom-domain-with-github-pages/"
 
         def message
           "Domain does not resolve to the GitHub Pages server"
diff --git a/lib/github-pages-health-check/printer.rb b/lib/github-pages-health-check/printer.rb
index 5cb3ff1..0004a8d 100644
--- a/lib/github-pages-health-check/printer.rb
+++ b/lib/github-pages-health-check/printer.rb
@@ -4,7 +4,7 @@ module GitHubPages
   module HealthCheck
     class Printer
       PRETTY_LEFT_WIDTH = 11
-      PRETTY_JOINER = " | ".freeze
+      PRETTY_JOINER = " | "
 
       attr_reader :health_check
 
diff --git a/lib/github-pages-health-check/repository.rb b/lib/github-pages-health-check/repository.rb
index 4bf2a1f..800a5c4 100644
--- a/lib/github-pages-health-check/repository.rb
+++ b/lib/github-pages-health-check/repository.rb
@@ -47,11 +47,11 @@ module GitHubPages
       alias reason build_error
 
       def build_duration
-        last_build && last_build.duration
+        last_build&.duration
       end
 
       def last_built
-        last_build && last_build.updated_at
+        last_build&.updated_at
       end
 
       def domain
diff --git a/lib/github-pages-health-check/resolver.rb b/lib/github-pages-health-check/resolver.rb
index 009fa84..9d161d7 100644
--- a/lib/github-pages-health-check/resolver.rb
+++ b/lib/github-pages-health-check/resolver.rb
@@ -43,16 +43,16 @@ module GitHubPages
                         self.class.default_resolver
                       when :authoritative
                         Dnsruby::Resolver.new(DEFAULT_RESOLVER_OPTIONS.merge(
-                          :nameservers => authoritative_nameservers
-                        ))
+                                                :nameservers => authoritative_nameservers
+                                              ))
                       when :public
                         Dnsruby::Resolver.new(DEFAULT_RESOLVER_OPTIONS.merge(
-                          :nameservers => PUBLIC_NAMESERVERS
-                        ))
+                                                :nameservers => PUBLIC_NAMESERVERS
+                                              ))
                       when Array
                         Dnsruby::Resolver.new(DEFAULT_RESOLVER_OPTIONS.merge(
-                          :nameservers => nameservers
-                        ))
+                                                :nameservers => nameservers
+                                              ))
                       else
                         raise "Invalid nameserver type: #{nameservers.inspect}"
                       end
diff --git a/lib/github-pages-health-check/version.rb b/lib/github-pages-health-check/version.rb
index 9b8d9dd..86b9bdf 100644
--- a/lib/github-pages-health-check/version.rb
+++ b/lib/github-pages-health-check/version.rb
@@ -2,6 +2,6 @@
 
 module GitHubPages
   module HealthCheck
-    VERSION = "1.16.1".freeze
+    VERSION = "1.18.1"
   end
 end
diff --git a/script/check b/script/check
index 2328b0a..742daff 100755
--- a/script/check
+++ b/script/check
@@ -3,6 +3,8 @@
 #
 # Usage: script/check [DOMAIN]
 
+require "rubygems"
+require "bundler/setup"
 require_relative "../lib/github-pages-health-check"
 
 if ARGV.count != 1
diff --git a/script/cibuild b/script/cibuild
index e311b09..7831760 100755
--- a/script/cibuild
+++ b/script/cibuild
@@ -4,7 +4,7 @@ set -ex
 
 script/bootstrap
 
-script/test
+script/test $@
 script/check-cdn-ips
 bundle exec script/check www.parkermoore.de | grep 'valid?: true'
 bundle exec script/check ben.balter.com | grep 'valid?: true'
diff --git a/script/cibuild-docker b/script/cibuild-docker
new file mode 100755
index 0000000..605b3dd
--- /dev/null
+++ b/script/cibuild-docker
@@ -0,0 +1,11 @@
+#!/bin/bash
+
+: ${RUBY_VERSION:="2.7"}
+docker build -t github-pages-health-check --build-arg RUBY_VERSION=$RUBY_VERSION .
+if [ -n "$DEBUG" ]; then
+  # Run a shell.
+  docker run -it --rm -v $(pwd):/app/github-pages-health-check github-pages-health-check
+else
+  # Run CI
+  docker run --rm github-pages-health-check script/cibuild --profile --fail-fast
+fi
diff --git a/script/test b/script/test
index ca04f20..6eead4a 100755
--- a/script/test
+++ b/script/test
@@ -3,4 +3,4 @@
 set -e
 
 bundle exec rspec $@
-script/fmt $@
+script/fmt
diff --git a/script/update-cdn-ips b/script/update-cdn-ips
index 1cbfa00..a496f4f 100755
--- a/script/update-cdn-ips
+++ b/script/update-cdn-ips
@@ -8,15 +8,43 @@ require "open-uri"
 require "json"
 
 SOURCES = {
-  :cloudflare => "https://www.cloudflare.com/ips-v4",
-  :fastly => "https://api.fastly.com/public-ip-list"
+  :cloudflare => ["https://www.cloudflare.com/ips-v4", "https://www.cloudflare.com/ips-v6"],
+  :fastly => ["https://api.fastly.com/public-ip-list"]
 }.freeze
 
-SOURCES.each do |source, url|
+def parse_fastly(data)
+  json_data = JSON.parse(data)
+  (json_data["addresses"] + json_data["ipv6_addresses"]).join("\n")
+end
+
+def parse_cloudflare(data)
+  data
+end
+
+def fetch_ips_from_cdn(urls)
+  urls.map do |url|
+    puts "Fetching #{url}..."
+    URI.parse(url).open.read
+  end.join("\n")
+end
+
+def update_cdn_file(source, data)
   file = "config/#{source}-ips.txt"
-  puts "Fetching #{url}..."
-  data = open(url).read
-  data = JSON.parse(data)["addresses"].join("\n") if source == :fastly
   File.write(file, data)
+  puts "Writing contents to #{file} and staging changes."
   `git add --verbose #{file}`
 end
+
+def parse_cdn_response(source, ips)
+  send("parse_#{source}", ips)
+end
+
+def update_cdn_ips(source, urls)
+  ips = fetch_ips_from_cdn(urls)
+  data = parse_cdn_response(source, ips)
+  update_cdn_file(source, data)
+end
+
+SOURCES.each do |source, urls|
+  update_cdn_ips(source, urls)
+end
diff --git a/spec/fixtures/build_error.json b/spec/fixtures/build_error.json
new file mode 100644
index 0000000..20e8be4
--- /dev/null
+++ b/spec/fixtures/build_error.json
@@ -0,0 +1,30 @@
+{
+  "url": "https://api.github.com/repos/github/developer.github.com/pages/builds/5472601",
+  "status": "errored",
+  "error": {
+    "message": "Some message"
+  },
+  "pusher": {
+    "login": "octocat",
+    "id": 1,
+    "avatar_url": "https://github.com/images/error/octocat_happy.gif",
+    "gravatar_id": "",
+    "url": "https://api.github.com/users/octocat",
+    "html_url": "https://github.com/octocat",
+    "followers_url": "https://api.github.com/users/octocat/followers",
+    "following_url": "https://api.github.com/users/octocat/following{/other_user}",
+    "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}",
+    "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}",
+    "subscriptions_url": "https://api.github.com/users/octocat/subscriptions",
+    "organizations_url": "https://api.github.com/users/octocat/orgs",
+    "repos_url": "https://api.github.com/users/octocat/repos",
+    "events_url": "https://api.github.com/users/octocat/events{/privacy}",
+    "received_events_url": "https://api.github.com/users/octocat/received_events",
+    "type": "User",
+    "site_admin": false
+  },
+  "commit": "351391cdcb88ffae71ec3028c91f375a8036a26b",
+  "duration": 2104,
+  "created_at": "2014-02-10T19:00:49Z",
+  "updated_at": "2014-02-10T19:00:51Z"
+}
diff --git a/spec/fixtures/build_success.json b/spec/fixtures/build_success.json
new file mode 100644
index 0000000..e62528d
--- /dev/null
+++ b/spec/fixtures/build_success.json
@@ -0,0 +1,30 @@
+{
+  "url": "https://api.github.com/repos/github/developer.github.com/pages/builds/5472601",
+  "status": "built",
+  "error": {
+    "message": null
+  },
+  "pusher": {
+    "login": "octocat",
+    "id": 1,
+    "avatar_url": "https://github.com/images/error/octocat_happy.gif",
+    "gravatar_id": "",
+    "url": "https://api.github.com/users/octocat",
+    "html_url": "https://github.com/octocat",
+    "followers_url": "https://api.github.com/users/octocat/followers",
+    "following_url": "https://api.github.com/users/octocat/following{/other_user}",
+    "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}",
+    "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}",
+    "subscriptions_url": "https://api.github.com/users/octocat/subscriptions",
+    "organizations_url": "https://api.github.com/users/octocat/orgs",
+    "repos_url": "https://api.github.com/users/octocat/repos",
+    "events_url": "https://api.github.com/users/octocat/events{/privacy}",
+    "received_events_url": "https://api.github.com/users/octocat/received_events",
+    "type": "User",
+    "site_admin": false
+  },
+  "commit": "351391cdcb88ffae71ec3028c91f375a8036a26b",
+  "duration": 2104,
+  "created_at": "2014-02-10T19:00:49Z",
+  "updated_at": "2014-02-10T19:00:51Z"
+}
diff --git a/spec/fixtures/pages_info.json b/spec/fixtures/pages_info.json
new file mode 100644
index 0000000..ca8b6f0
--- /dev/null
+++ b/spec/fixtures/pages_info.json
@@ -0,0 +1,6 @@
+{
+  "url": "https://api.github.com/repos/github/pages.github.com/pages",
+  "status": "built",
+  "cname": "pages.github.com",
+  "custom_404": false
+}
diff --git a/spec/fixtures/pages_info_no_cname.json b/spec/fixtures/pages_info_no_cname.json
new file mode 100644
index 0000000..dfc7414
--- /dev/null
+++ b/spec/fixtures/pages_info_no_cname.json
@@ -0,0 +1,6 @@
+{
+  "url": "https://api.github.com/repos/github/pages.github.com/pages",
+  "status": "built",
+  "cname": null,
+  "custom_404": false
+}
diff --git a/spec/github_pages_health_check/caa_spec.rb b/spec/github_pages_health_check/caa_spec.rb
new file mode 100644
index 0000000..f16d525
--- /dev/null
+++ b/spec/github_pages_health_check/caa_spec.rb
@@ -0,0 +1,94 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+RSpec.describe(GitHubPages::HealthCheck::CAA) do
+  let(:domain) { "foo.sub.githubtest.com" }
+  let(:parent_domain) { "sub.githubtest.com" }
+  subject { described_class.new(:host => domain) }
+  let(:caa_packet_le) do
+    Dnsruby::RR.create("sub.githubtest.com. IN CAA 0 issue \"letsencrypt.org\"")
+  end
+  let(:caa_packet_le_apex) do
+    Dnsruby::RR.create("githubtest.com. IN CAA 0 issue \"digicert.com\"")
+  end
+  let(:caa_packet_other) do
+    Dnsruby::RR.create("#{domain}. IN CAA 0 issue \"digicert.com\"")
+  end
+
+  context "a domain without CAA records" do
+    before(:each) do
+      expect(subject).to receive(:query).with(domain).and_return([])
+      expect(subject).to receive(:query).with(parent_domain).and_return([])
+    end
+
+    it "knows no records exist" do
+      expect(subject).not_to be_records_present
+    end
+
+    it "allows let's encrypt" do
+      expect(subject).to be_lets_encrypt_allowed
+    end
+
+    it "does not encounter an error" do
+      expect(subject).not_to be_errored
+    end
+  end
+
+  context "a domain with LE CAA record" do
+    before(:each) do
+      expect(subject).to receive(:query).with(domain).and_return([caa_packet_le])
+    end
+
+    it "knows records exist" do
+      expect(subject).to be_records_present
+    end
+
+    it "allows let's encrypt" do
+      expect(subject).to be_lets_encrypt_allowed
+    end
+
+    it "does not encounter an error" do
+      expect(subject).not_to be_errored
+    end
+  end
+
+  context "a domain without LE CAA record" do
+    before(:each) do
+      expect(subject).to receive(:query).with(domain).and_return([caa_packet_other])
+    end
+
+    it "knows records exist" do
+      expect(subject).to be_records_present
+    end
+
+    it "doesn't let's encrypt" do
+      expect(subject).not_to be_lets_encrypt_allowed
+    end
+
+    it "does not encounter an error" do
+      expect(subject).not_to be_errored
+    end
+  end
+
+  context "a domain which errors" do
+    before(:each) do
+      expect(subject).to receive(:query).with(domain).and_return([])
+      expect(subject).to receive(:query).with(parent_domain).and_return([])
+      subject.instance_variable_set(:@error, Dnsruby::ServFail.new)
+    end
+
+    it "knows no records exist" do
+      expect(subject).not_to be_records_present
+    end
+
+    it "doesn't allows let's encrypt" do
+      expect(subject).not_to be_lets_encrypt_allowed
+    end
+
+    it "surfaces the error" do
+      expect(subject).to be_errored
+      expect(subject.error.class.name).to eql("Dnsruby::ServFail")
+    end
+  end
+end
diff --git a/spec/github_pages_health_check/cdn_spec.rb b/spec/github_pages_health_check/cdn_spec.rb
new file mode 100644
index 0000000..fb0aa5a
--- /dev/null
+++ b/spec/github_pages_health_check/cdn_spec.rb
@@ -0,0 +1,95 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+require "json"
+require "tempfile"
+require "ipaddr"
+
+RSpec.describe(GitHubPages::HealthCheck::CDN) do
+  subject { described_class.instance }
+
+  it "loads the default config" do
+    path = File.expand_path(subject.path)
+    relative_path = "../../config/cdn-ips.txt"
+    expected = File.expand_path(relative_path, File.dirname(__FILE__))
+    expect(path).to eql(expected)
+  end
+
+  context "with the IP file stubbed" do
+    let(:tempfile) { Tempfile.new("pages-cdn-ips").tap { |f| f.sync = true } }
+    let(:ipaddr_path) { tempfile.path }
+    subject { described_class.send(:new, :path => ipaddr_path) }
+
+    context "no config file" do
+      before { tempfile.unlink }
+
+      it "raises an error" do
+        error = "no implicit conversion of nil into String"
+        expect { subject.send(:ranges) }.to raise_error error
+      end
+    end
+
+    context "parses config" do
+      before { tempfile.write("199.27.128.0/21\n173.245.48.0/20\n2400:cb00::/32") }
+
+      it "has three IPs" do
+        expect(subject.send(:ranges).size).to eql(3)
+      end
+
+      it "loads the IP addresses" do
+        expect(subject.send(:ranges)).to include(IPAddr.new("199.27.128.0/21"))
+        expect(subject.send(:ranges)).to include(IPAddr.new("173.245.48.0/20"))
+        expect(subject.send(:ranges)).to include(IPAddr.new("2400:cb00::/32"))
+      end
+
+      it("controls? 199.27.128.55") do
+        expect(subject.controls_ip?(IPAddr.new("199.27.128.55"))).to be_truthy
+      end
+
+      it("controls? 173.245.48.55") do
+        expect(subject.controls_ip?(IPAddr.new("173.245.48.55"))).to be_truthy
+      end
+
+      it("controls? 2400:cb00:1000:2000:3000:4000:5000:6000") do
+        expect(
+          subject.controls_ip?(
+            IPAddr.new("2400:cb00:1000:2000:3000:4000:5000:6000")
+          )
+        ).to be_truthy
+      end
+
+      it("controls? 200.27.128.55") do
+        expect(subject.controls_ip?(IPAddr.new("200.27.128.55"))).to be_falsey
+      end
+    end
+
+    {
+      "Fastly" => {
+        :valid_ips => ["151.101.32.133", "2a04:4e40:1000:2000:3000:4000:5000:6000"],
+        :invalid_ips => ["108.162.196.20", "2400:cb00:7000:8000:9000:A000:B000:C000"]
+      },
+      "CloudFlare" => {
+        :valid_ips => ["108.162.196.20", "2400:cb00:7000:8000:9000:A000:B000:C000"],
+        :invalid_ips => ["151.101.32.133", "2a04:4e40:1000:2000:3000:4000:5000:6000"]
+      }
+    }.each do |service, ips|
+      context service do
+        it "works as a singleton" do
+          const = "GitHubPages::HealthCheck::#{service}"
+          klass = Kernel.const_get(const).send(:new)
+
+          ips[:valid_ips].each do |ip|
+            expect(klass.controls_ip?(ip)).to eq(true), ip
+          end
+
+          ips[:invalid_ips].each do |ip|
+            expect(klass.controls_ip?(ip)).to eq(false), ip
+          end
+
+          github_ips = GitHubPages::HealthCheck::Domain::CURRENT_IP_ADDRESSES
+          expect(klass.controls_ip?(github_ips.first)).to be(false)
+        end
+      end
+    end
+  end
+end
diff --git a/spec/github_pages_health_check/checkable_spec.rb b/spec/github_pages_health_check/checkable_spec.rb
new file mode 100644
index 0000000..cea7748
--- /dev/null
+++ b/spec/github_pages_health_check/checkable_spec.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+class CheckableHelper < GitHubPages::HealthCheck::Checkable
+  def check!
+    if ENV["OCTOKIT_ACCESS_TOKEN"].to_s.empty?
+      raise GitHubPages::HealthCheck::Errors::MissingAccessTokenError
+    end
+
+    true
+  end
+end
+
+RSpec.describe(CheckableHelper) do
+  context "valid" do
+    it "knows the check is valid" do
+      with_env "OCTOKIT_ACCESS_TOKEN", "1234" do
+        expect(subject.valid?).to eql(true)
+      end
+    end
+  end
+
+  context "invalid" do
+    it "knows the check is invalid" do
+      with_env "OCTOKIT_ACCESS_TOKEN", "" do
+        expect(subject.valid?).to eql(false)
+      end
+    end
+
+    it "knows the reason" do
+      with_env "OCTOKIT_ACCESS_TOKEN", "" do
+        expected = GitHubPages::HealthCheck::Errors::MissingAccessTokenError
+        expect(subject.reason.class).to eql(expected)
+      end
+    end
+  end
+end
diff --git a/spec/github_pages_health_check/domain_spec.rb b/spec/github_pages_health_check/domain_spec.rb
new file mode 100644
index 0000000..878eda1
--- /dev/null
+++ b/spec/github_pages_health_check/domain_spec.rb
@@ -0,0 +1,1268 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+RSpec.describe(GitHubPages::HealthCheck::Domain) do
+  let(:domain) { "foo.github.io" }
+  let(:cname) { domain }
+  subject { described_class.new(domain) }
+  let(:cname_packet) do
+    Dnsruby::RR.create("#{domain}. 1000 IN CNAME #{cname}.")
+  end
+  let(:mx_packet) do
+    Dnsruby::RR.create("#{domain}. 1000 IN MX 10 mail.example.com.")
+  end
+  let(:ip) { "127.0.0.1" }
+  let(:a_packet) do
+    Dnsruby::RR.create("#{domain}. 1000 IN A #{ip}")
+  end
+  let(:ip6) { "::1" }
+  let(:aaaa_packet) do
+    Dnsruby::RR.create("#{domain}. 1000 IN AAAA #{ip6}")
+  end
+  let(:caa_domain) { "" }
+  let(:caa_packet) do
+    Dnsruby::RR.create("#{domain}. 1000 IN CAA 0 issue #{caa_domain.inspect}")
+  end
+  let(:soa_packet) do
+    Dnsruby::RR.create("#{domain}. 1000 IN SOA ns.example.com. #{domain.inspect}")
+  end
+  let(:ns_packet) do
+    Dnsruby::RR.create("#{domain}. 1000 IN NS ns.example.com.")
+  end
+
+  context "constructor" do
+    it "can handle bare domains" do
+      expect(subject.host).to eql(domain)
+    end
+
+    context "schemes" do
+      %w(http https ftp).each do |scheme|
+        context scheme do
+          subject do
+            described_class.new("#{scheme}://#{domain}")
+          end
+
+          it "parses the domain" do
+            expect(subject.host).to eql(domain)
+          end
+        end
+      end
+    end
+
+    context "paths" do
+      ["/im-a-path", "/im-a-path/", "/index.html"].each do |path|
+        context path do
+          subject do
+            described_class.new("http://#{domain}/#{path}")
+          end
+
+          it "parses the domain" do
+            expect(subject.host).to eql(domain)
+          end
+        end
+      end
+    end
+
+    context "stripping whitespace" do
+      subject do
+        described_class.new(" #{domain} ")
+      end
+
+      it "parses the domain" do
+        expect(subject.host).to eql(domain)
+      end
+    end
+
+    context "FQDNs" do
+      subject do
+        described_class.new("#{domain}.")
+      end
+
+      it "parses the domain" do
+        expect(subject.host).to eql(domain)
+      end
+    end
+
+    context "invalid domains" do
+      let(:error) { GitHubPages::HealthCheck::Errors::InvalidDomainError }
+
+      context "when given http://@" do
+        let(:domain) { "http://@" }
+
+        it "doesn't blow up" do
+          expect(subject.host).to be_nil
+          expect(subject.reason).to be_a(error)
+        end
+      end
+
+      context "when given //" do
+        let(:domain) { "//" }
+
+        it "doesn't blow up" do
+          expect(subject.host).to be_nil
+          expect(subject.reason).to be_a(error)
+        end
+      end
+    end
+  end
+
+  context "response" do
+    let(:proxy) { "http://proxy.org:5000" }
+    before do
+      GitHubPages::HealthCheck.set_proxy(proxy)
+    end
+    after do
+      GitHubPages::HealthCheck.set_proxy(nil)
+    end
+
+    it "uses a network proxy for outgoing requests" do
+      stub_request(:head, domain).to_return(:status => 200, :headers => {})
+      response = GitHubPages::HealthCheck::Domain.new(domain).send(:response)
+      expect(response.request.options).to include(:proxy => proxy)
+    end
+  end
+
+  context "A records" do
+    before(:each) { allow(subject).to receive(:dns) { [a_packet] } }
+
+    context "old IP addresses" do
+      %w(204.232.175.78 207.97.227.245 192.30.252.153 192.30.252.154).each do |ip_address|
+        context ip_address do
+          let(:ip) { ip_address }
+
+          it "knows it's a deprecated IP" do
+            expect(subject).to be_a_old_ip_address
+            expect(subject).to be_a_deprecated_ip
+          end
+        end
+      end
+
+      %w(185.199.108.153 185.199.109.153 185.199.110.153
+         185.199.111.153).each do |ip_address|
+        context ip_address do
+          let(:ip) { ip_address }
+
+          it "knows it's not an old IP" do
+            expect(subject).not_to be_a_old_ip_address
+            expect(subject).not_to be_a_deprecated_ip
+            expect(subject).to be_pointed_to_github_pages_ip
+          end
+        end
+      end
+
+      context "a random IP" do
+        let(:ip) { "1.2.3.4" }
+
+        it "knows it's not an old IP" do
+          expect(subject).to_not be_a_old_ip_address
+        end
+      end
+
+      it "doesn't list current IPs as deprecated" do
+        deprecated = GitHubPages::HealthCheck::Domain::LEGACY_IP_ADDRESSES
+        GitHubPages::HealthCheck::Domain::CURRENT_IP_ADDRESSES.each do |ip|
+          expect(deprecated).to_not include(ip)
+        end
+      end
+    end
+
+    it "knows when a domain is an A record" do
+      expect(subject).to be_an_a_record
+      expect(subject).to_not be_a_cname_record
+    end
+
+    it "knows when a domain has an invalid A record" do
+      expect(subject).to be_an_a_record
+      expect(subject).to be_a_valid_domain
+      expect(subject.should_be_a_record?).to be_falsy
+      expect(subject).to be_a_invalid_a_record
+    end
+  end
+
+  context "AAAA records" do
+    before(:each) { allow(subject).to receive(:dns) { [aaaa_packet] } }
+
+    it "knows when a domain is an AAAA record" do
+      expect(subject).to be_an_aaaa_record_present
+      expect(subject).to_not be_a_cname_record
+    end
+
+    it "knows when a domain has an invalid AAAA record" do
+      expect(subject).to be_an_aaaa_record_present
+      expect(subject).to be_a_valid_domain
+      expect(subject.should_be_a_record?).to eq(false)
+      expect(subject).to be_a_invalid_aaaa_record
+    end
+  end
+
+  context "A & AAAA recursive resolutions" do
+    let(:domain) { "domain.com" }
+    let(:cname) { "domain.github.io" }
+    before(:each) do
+      allow(subject).to receive(:dns) {
+        [
+          cname_packet,
+          Dnsruby::RR.create("#{cname}. 1000 IN A #{ip}"),
+          Dnsruby::RR.create("#{cname}. 1000 IN AAAA #{ip6}")
+        ]
+      }
+    end
+
+    it "does not get tricked by recursive resolution" do
+      expect(subject).to_not be_an_aaaa_record_present
+      expect(subject).to_not be_an_a_record_present
+    end
+  end
+
+  context "CNAMEs" do
+    before(:each) { allow(subject).to receive(:dns) { [cname_packet] } }
+
+    it "known when a domain is a CNAME record" do
+      expect(subject).to be_a_cname_record
+      expect(subject).to_not be_an_a_record
+    end
+
+    context "multiple CNAMEs" do
+      let(:cname) { "github.example.com" }
+      before do
+        allow(subject).to receive(:dns) do
+          [
+            cname_packet,
+            Dnsruby::RR.create("#{cname}. 1000 IN CNAME example.github.io."),
+            Dnsruby::RR.create("example.github.io 1000 IN A 192.168.0.1")
+          ]
+        end
+      end
+
+      it "follows the CNAMEs all the way down" do
+        expect(subject.cname.host).to eq("example.github.io")
+      end
+    end
+
+    context "broken CNAMEs" do
+      before do
+        allow(subject).to receive(:dns) do
+          [Dnsruby::RR.create("#{domain}. 300 IN CNAME @.")]
+        end
+      end
+
+      it "handles a broken CNAME gracefully" do
+        expect(subject).to_not be_a_cname
+        expect(subject.cname).to_not be_a_valid_domain
+      end
+    end
+
+    it "returns the cname" do
+      expect(subject.cname.host).to eql(domain)
+    end
+
+    context "with a subdomain" do
+      let(:domain) { "blog.parkermoore.de" }
+
+      it "knows a subdomain is not an apex domain" do
+        expect(subject).to_not be_an_apex_domain
+      end
+    end
+
+    context "with a co.uk subdomain" do
+      let(:domain) { "www.bbc.co.uk" }
+
+      it "knows a subdomain is not an apex domain" do
+        expect(subject).to_not be_an_apex_domain
+      end
+    end
+
+    context "apex records" do
+      ["parkermoore.de", "bbc.co.uk", "techblog.netflix.com"].each do |apex_domain|
+        context "given domain: #{apex_domain} with SOA" do
+          before(:each) { allow(subject).to receive(:dns) { [soa_packet, ns_packet] } }
+
+          let(:domain) { apex_domain }
+
+          it "knows it should be an a record" do
+            expect(subject.should_be_a_record?).to be_truthy
+          end
+        end
+      end
+
+      ["blog.parkermoore.de", "www.bbc.co.uk",
+       "foo.github.io", "pages.github.com"].each do |apex_domain|
+        context "given #{apex_domain}" do
+          let(:domain) { apex_domain }
+
+          it "knows it shouldn't be an a record" do
+            expect(subject.should_be_a_record?).to be_falsy
+          end
+        end
+      end
+
+      ["private.dns.zone"].each do |soa_domain|
+        context "given #{soa_domain}" do
+          before(:each) { allow(subject).to receive(:dns) { [soa_packet, ns_packet] } }
+          let(:domain) { soa_domain }
+
+          it "allows child zones with an SOA to be an Apex" do
+            expect(subject.should_be_a_record?).to eq(true)
+          end
+
+          it "reports whether child zones publish an SOA record" do
+            expect(subject.dns_zone_soa?).to be_truthy
+          end
+        end
+      end
+
+      context "a domain with an MX record" do
+        let(:domain) { "blog.parkermoore.de" }
+        before(:each) do
+          allow(subject).to receive(:dns) { [a_packet, mx_packet] }
+          stub_request(:head, "http://#{domain}")
+            .to_return(:status => 200, :headers => { "Server" => "GitHub.com" })
+        end
+
+        it { is_expected.to be_should_be_a_record }
+        it { is_expected.to be_valid }
+
+        context "pointed to Fastly" do
+          let(:ip) { "151.101.33.147" }
+
+          it { is_expected.to be_a_fastly_ip }
+        end
+      end
+    end
+
+    context "only MX records" do
+      before(:each) { allow(subject).to receive(:dns) { [mx_packet] } }
+      let(:domain) { "pages.invalid" }
+
+      it "must not be valid" do
+        expect(subject).not_to be_valid
+      end
+
+      it "does not think it forwards to a Fastly IP" do
+        expect(subject).not_to be_a_fastly_ip
+      end
+    end
+
+    context "CNAMEs to Pages" do
+      before(:each) { allow(subject).to receive(:dns) { [cname_packet] } }
+
+      ["parkr.github.io", "mattr-.github.com"].each do |cname|
+        context cname do
+          let(:domain) { cname }
+
+          it "can determine a valid GitHub Pages CNAME value" do
+            expect(subject).to be_a_cname_to_github_user_domain
+          end
+        end
+      end
+
+      ["github.com", "ben.balter.com"].each do |cname|
+        context cname do
+          let(:domain) { cname }
+
+          it "can determine a valid GitHub Pages CNAME value" do
+            expect(subject).to_not be_a_cname_to_github_user_domain
+          end
+        end
+      end
+    end
+
+    context "CNAMEs" do
+      let(:domain) { "foo.github.biz" }
+      before(:each) { allow(subject).to receive(:dns) { [cname_packet] } }
+
+      it "detects invalid CNAMEs" do
+        expect(subject).to be_a_valid_domain
+        expect(subject).to_not be_a_github_domain
+        expect(subject).to_not be_an_apex_domain
+        expect(subject).to_not be_a_cname_to_github_user_domain
+        expect(subject).to be_an_invalid_cname
+      end
+
+      context "to pages.github.com" do
+        let(:cname) { "pages.github.com" }
+
+        it "flags CNAMEs to pages.github.com as invalid" do
+          expect(subject).to be_an_invalid_cname
+        end
+
+        it "knows when the domain is CNAME'd to pages.github.com" do
+          expect(subject).to be_a_cname_to_pages_dot_github_dot_com
+        end
+      end
+
+      context "to fastly" do
+        context "github map" do
+          let(:cname) { "github.map.fastly.net" }
+
+          it "flags CNAMEs directly to fastly as invalid" do
+            expect(subject).to be_an_invalid_cname
+          end
+
+          it "knows when the domain is CNAME'd to fastly" do
+            expect(subject).to be_a_cname_to_fastly
+          end
+        end
+
+        context "sni.github map" do
+          let(:cname) { "sni.github.map.fastly.net" }
+
+          it "flags CNAMEs directly to fastly as invalid" do
+            expect(subject).to be_an_invalid_cname
+          end
+
+          it "knows when the domain is CNAME'd to fastly" do
+            expect(subject).to be_a_cname_to_fastly
+          end
+        end
+      end
+
+      context "to other subdomains" do
+        let(:cname) { "foo.github.io" }
+
+        it "knows CNAMEs to user subdomains are valid" do
+          expect(subject.invalid_cname?).to be_falsy
+        end
+
+        it "knows when the domain is CNAME'd to a user domain" do
+          expect(subject).to be_a_cname_to_github_user_domain
+        end
+      end
+    end
+  end
+
+  context "domains" do
+    context "github domains" do
+      let(:domain) { "government.github.com" }
+
+      it "knows if the domain is a github domain" do
+        expect(subject).to be_a_github_domain
+      end
+    end
+
+    context "fastly domain" do
+      let(:domain) { "github.map.fastly.net" }
+
+      it "knows if the domain is a fastly domain" do
+        expect(subject).to be_fastly
+      end
+    end
+
+    context "apex domains" do
+      let(:domain) { "parkermoore.de" }
+
+      it "knows what an apex domain is" do
+        expect(subject).to be_an_apex_domain
+      end
+    end
+  end
+
+  context "cloudflare" do
+    let(:ip) { "108.162.196.20" }
+    before(:each) { allow(subject).to receive(:dns) { [a_packet] } }
+
+    it "knows when the domain is on cloudflare" do
+      expect(subject).to be_a_cloudflare_ip
+    end
+
+    context "a random IP" do
+      let(:ip) { "1.1.1.1" }
+
+      it "know's it's not cloudflare" do
+        expect(subject).to_not be_a_cloudflare_ip
+      end
+    end
+  end
+
+  context "cloudflare IPv6" do
+    let(:ip6) { "2405:b500:7000:8000:9000:A000:B000:C000" }
+    before(:each) { allow(subject).to receive(:dns) { [aaaa_packet] } }
+
+    it "knows when the domain is on cloudflare" do
+      expect(subject).to be_a_cloudflare_ip
+    end
+
+    context "a random IP" do
+      let(:ip6) { "2a04:4e40:1000:2000:3000:4000:5000:6000" }
+
+      it "know's it's not cloudflare" do
+        expect(subject).to_not be_a_cloudflare_ip
+      end
+    end
+  end
+
+  context "GitHub Pages IPs" do
+    context "apex domains" do
+      context "pointed to Pages IP" do
+        let(:domain) { "fontawesome.io" }
+        let(:ip) { "185.199.108.153" }
+        before(:each) { allow(subject).to receive(:dns) { [a_packet] } }
+
+        it "Knows it's a Pages IP" do
+          expect(subject).to be_pointed_to_github_pages_ip
+        end
+      end
+
+      context "not pointed to a pages IP" do
+        let(:domain) { "example.com" }
+
+        it "knows it's not a Pages IP" do
+          expect(subject).to_not be_pointed_to_github_pages_ip
+        end
+      end
+    end
+
+    context "subdomains" do
+      let(:domain) { "pages.github.com" }
+
+      it "Knows it's not a Pages IP" do
+        expect(subject).to_not be_pointed_to_github_pages_ip
+      end
+    end
+  end
+
+  context "GitHub Pages IPv6s" do
+    context "apex domains" do
+      context "pointed to Pages IPv6" do
+        let(:domain) { "myipv6.io" }
+        let(:ip6) { "2606:50C0:8000::153" }
+        before(:each) { allow(subject).to receive(:dns) { [aaaa_packet] } }
+
+        it "Knows it's a Pages IP" do
+          expect(subject).to be_pointed_to_github_pages_ip
+        end
+      end
+
+      context "not pointed to a pages IP" do
+        let(:domain) { "example.com" }
+        let(:ip6) { "::1" }
+
+        it "knows it's not a Pages IP" do
+          expect(subject).to_not be_pointed_to_github_pages_ip
+        end
+      end
+    end
+  end
+
+  context "Pages domains" do
+    ["pages.github.com",
+     "pages.github.io",
+     "pages.github.io."].each do |pages_domain|
+      context pages_domain do
+        let(:domain) { pages_domain }
+
+        it "can detect pages domains" do
+          expect(subject).to be_a_pages_domain
+        end
+      end
+    end
+
+    ["github.com", "google.co.uk"].each do |random_domain|
+      context random_domain do
+        let(:domain) { random_domain }
+
+        it "doesn't detect non-pages domains as a pages domain" do
+          expect(subject).to_not be_a_pages_domain
+        end
+      end
+    end
+  end
+
+  context "served by pages" do
+    let(:domain) { "http://choosealicense.com" }
+    let(:status) { 200 }
+    let(:headers) { {} }
+
+    before do
+      allow(subject).to receive(:dns) { [a_packet] }
+      stub_request(:head, domain)
+        .to_return(:status => status, :headers => headers)
+
+      stub_request(:head, "https://githubuniverse.com/")
+        .to_return(:status => 200, :headers => { :server => "GitHub.com" })
+
+      stub_request(:head, "https://github.com/login")
+        .to_return(:status => 200, :headers => { :server => "GitHub.com" })
+    end
+
+    context "with the Pages server header" do
+      let(:headers) { { :server => "GitHub.com" } }
+
+      it "knows when a domain is served by pages" do
+        expect(subject).to be_served_by_pages
+      end
+
+      context "with a 404" do
+        let(:status) { 404 }
+
+        it "knows when a domain is served by pages even if it returns a 404" do
+          expect(subject).to be_served_by_pages
+        end
+      end
+
+      context "a GitHub domain" do
+        let(:domain) { "https://mac.github.com" }
+
+        it "knows when a GitHub domain is served by pages" do
+          expect(subject).to be_served_by_pages
+        end
+      end
+
+      context "an alternate domain" do
+        let(:domain) { "http://www.githubuniverse.com" }
+        let(:status) { 301 }
+        let(:headers) { { :location => "https://githubuniverse.com" } }
+        before(:each) { allow(subject).to receive(:dns) { [cname_packet] } }
+
+        it "knows about domains that redirect to the primary domain on pages" do
+          expect(subject).to be_served_by_pages
+        end
+      end
+
+      context "a private page" do
+        let(:domain) { "http://private-page.githubapp.com" }
+        let(:status) { 302 }
+        let(:headers) { { :location => "https://github.com/login" } }
+
+        it "considers it valid if it redirects to github.com/login" do
+          expect(subject).to be_served_by_pages
+        end
+      end
+    end
+
+    context "with a request ID" do
+      let(:headers) { { "X-GitHub-Request-Id" => "1234" } }
+
+      it "falls back to the request ID" do
+        expect(subject).to be_served_by_pages
+      end
+    end
+
+    context "a redirect to /" do
+      let(:domain) { "http://getbootstrap.com" }
+
+      before do
+        stub_request(:head, domain)
+          .to_return(:status => 302, :headers => { :location => "/" })
+
+        stub_request(:head, "#{domain}/")
+          .to_return(:status => status, :headers => { :server => "GitHub.com" })
+      end
+
+      it "knows it's served by pages" do
+        expect(subject).to be_served_by_pages
+      end
+    end
+
+    context "an https redirect" do
+      let(:domain) { "management.cio.gov" }
+
+      before do
+        stub_request(:head, "http://#{domain}")
+          .to_return(:status => 302,
+                     :headers => { :location => "https://#{domain}" })
+
+        stub_request(:head, "https://#{domain}")
+          .to_return(:status => status, :headers => { :server => "GitHub.com" })
+      end
+
+      it "knows when a domain with a redirect is served by pages" do
+        expect(subject).to be_served_by_pages
+      end
+    end
+
+    context "domains with underscores" do
+      let(:domain) { "this_domain_is_valid.example.com" }
+      let(:cname)  { "something.example.com" }
+      let(:headers) { { :server => "GitHub.com" } }
+      before(:each) { allow(subject).to receive(:dns) { [cname_packet] } }
+
+      it "doesn't error out on domains with underscores" do
+        expect(subject).to be_served_by_pages
+        expect(subject).to be_valid
+      end
+    end
+
+    context "private tlds in the public suffix list" do
+      let(:domain) { "githubusercontent.com" }
+      let(:cname)  { "something.example.com" }
+      let(:headers) { { :server => "GitHub.com" } }
+      before(:each) { allow(subject).to receive(:dns) { [cname_packet] } }
+
+      it "doesn't error out on private tlds in the public suffix list" do
+        expect(subject).to be_served_by_pages
+        expect(subject).to be_valid
+      end
+    end
+
+    context "domains with unicode encoding" do
+      let(:domain) { "dómain.example.com" }
+      let(:cname)  { "sómething.example.com" }
+      let(:headers) { { :server => "GitHub.com" } }
+      before(:each) { allow(subject).to receive(:dns) { [cname_packet] } }
+
+      it "doesn't error out on domains with unicode encoding" do
+        expect(subject).to be_served_by_pages
+        expect(subject).to be_valid
+      end
+    end
+
+    context "domains with punycode encoding" do
+      let(:domain) { "xn--dmain-0ta.example.com" }
+      let(:cname)  { "xn--smething-v3a.example.com" }
+      let(:headers) { { :server => "GitHub.com" } }
+      before(:each) { allow(subject).to receive(:dns) { [cname_packet] } }
+
+      it "doesn't error out on domains with punycode encoding" do
+        expect(subject).to be_served_by_pages
+        expect(subject).to be_valid
+      end
+    end
+  end
+
+  context "not served by pages" do
+    let(:domain) { "http://choosealicense.com" }
+    let(:status) { 200 }
+    let(:headers) { {} }
+    let(:not_served_error) do
+      GitHubPages::HealthCheck::Errors::NotServedByPagesError
+    end
+
+    before do
+      stub_request(:head, domain)
+        .to_return(:status => status, :headers => headers)
+    end
+
+    context "a random domain" do
+      let(:domain) { "geogle.com" }
+      let(:ip) { "127.0.0.1" }
+      before(:each) { allow(subject).to receive(:dns) { [a_packet] } }
+
+      it "knows when a domain isn't served by pages" do
+        expect(subject).to_not be_served_by_pages
+        expect(subject.reason).to be_a(not_served_error)
+        msg = "Domain does not resolve to the GitHub Pages server"
+        expect(subject.reason.message).to eql(msg)
+      end
+    end
+
+    context "a non-CNAME" do
+      let(:domain) { "techblog.netflix.com" }
+      let(:cname_error) do
+        GitHubPages::HealthCheck::Errors::InvalidCNAMEError
+      end
+      let(:cname) { "netflix.com" }
+      before(:each) { allow(subject).to receive(:dns) { [cname_packet] } }
+
+      it "returns the error" do
+        expect(subject.valid?).to be_falsy
+        expect(subject.mx_records_present?).to be_falsy
+        expect(subject.should_be_cname_record?).to be_truthy
+        expect(subject.reason).to be_a(cname_error)
+        regex = /not set up with a correct CNAME record/i
+        expect(subject.reason.message).to match(regex)
+      end
+    end
+  end
+
+  context "proxies" do
+    context "by IP" do
+      before(:each) { allow(subject).to receive(:dns) { [a_packet] } }
+
+      context "cloudflare" do
+        let(:ip) { "108.162.196.20" }
+
+        it "knows cloudflare sites are proxied" do
+          expect(subject).to be_proxied
+        end
+      end
+
+      context "a pages IP" do
+        let(:ip) { "185.199.108.153" }
+
+        it "knows a site pointed to a Pages IP isn't proxied" do
+          expect(subject).to_not be_proxied
+        end
+      end
+    end
+
+    context "by cname" do
+      before(:each) { allow(subject).to receive(:dns) { [cname_packet] } }
+
+      context "pointed to pages" do
+        let(:cname) { "foo.github.io" }
+
+        it "knows a site pointed to a Pages domain isn't proxied" do
+          expect(subject).to_not be_proxied
+        end
+      end
+
+      context "pointed to pages.github.com" do
+        let(:cname) { "pages.github.com" }
+
+        it "knows a site CNAMEd to pages.github.com isn't proxied" do
+          expect(subject).to_not be_proxied
+        end
+      end
+
+      context "pointed to Fastly" do
+        let(:cname) { "github.map.fastly.net" }
+        let(:domain) { "foo.github.biz" }
+
+        before do
+          stub_request(:head, "http://#{domain}")
+            .to_return(:status => 200, :headers => { :server => "GitHub.com" })
+        end
+
+        it "knows a site CNAME'd directly to Fastly isn't proxied" do
+          expect(subject).to_not be_proxied
+        end
+      end
+    end
+
+    context "proxying" do
+      let(:headers) { { :server => "GitHub.com" } }
+      let(:status) { 200 }
+      before(:each) do
+        stub_request(:head, domain)
+          .to_return(:status => status, :headers => headers)
+        allow(subject).to receive(:dns) { [a_packet] }
+      end
+
+      context "a site that returns GitHub.com headers" do
+        let(:domain) { "http://management.cio.gov" }
+
+        it "detects proxied sites" do
+          expect(subject).to be_proxied
+        end
+      end
+
+      context "a random site" do
+        let(:domain) { "http://google.com" }
+        let(:headers) { {} }
+
+        it "knows a site not served by pages isn't proxied" do
+          expect(subject).to_not be_proxied
+        end
+      end
+    end
+  end
+
+  context "github domains" do
+    context "pages.github.com" do
+      let(:domain) { "pages.github.com" }
+
+      it "knows when the domain is a github domain" do
+        expect(subject).to be_a_github_domain
+      end
+    end
+
+    context "choosealicense.com" do
+      let(:domain) { "choosealicense.com" }
+
+      it "knows when the domain is not a github domain" do
+        expect(subject).to_not be_a_github_domain
+      end
+    end
+
+    context "benbalter.github.io" do
+      let(:domain) { "benbalter.github.io" }
+
+      it "knows when the domain is not a github domain" do
+        expect(subject).to_not be_a_github_domain
+      end
+    end
+  end
+
+  context "invalid domains" do
+    let(:domain) { "this-domain-does-not-exist-and-should-not-ever-exist.io" }
+
+    it "does not resolve domains that do not exist" do
+      expect(subject.dns).to be_nil
+    end
+
+    context "a valid domain" do
+      let(:domain) { "github.com" }
+
+      it "is valid" do
+        expect(subject).to be_a_valid_domain
+      end
+    end
+
+    context "an invalid domain" do
+      let(:domain) { "github.invalid" }
+
+      it "is invalid" do
+        expect(subject).to_not be_a_valid_domain
+        error = GitHubPages::HealthCheck::Errors::InvalidDomainError
+        expect(subject.reason).to be_a(error)
+        expect(subject.reason.message).to eql("Domain is not a valid domain")
+      end
+    end
+  end
+
+  it "returns the Typhoeus options" do
+    expected = Regexp.escape GitHubPages::HealthCheck::VERSION
+    header = GitHubPages::HealthCheck.typhoeus_options[:headers]["User-Agent"]
+    expect(header).to match(expected)
+  end
+
+  context "dns" do
+    let(:domain) { "pages.github.com" }
+
+    it "retrieves a site's dns record" do
+      expect(subject).to be_dns_resolves
+      expect(subject.dns.first).to be_a(Dnsruby::RR::CNAME)
+    end
+
+    context "with DNS stubbed" do
+      let(:ip) { "1.2.3.4" }
+      before(:each) { allow(subject).to receive(:dns) { [a_packet] } }
+
+      it "knows when the DNS resolves" do
+        expect(subject.dns?).to be_truthy
+      end
+    end
+
+    context "when DNS doesn't resolve" do
+      before(:each) { allow(subject).to receive(:dns) { nil } }
+
+      it "knows when the DNS doesn't resolve" do
+        expect(subject.dns?).to be_falsy
+      end
+
+      it "treats cname as nil if DNS doesn't resolve" do
+        expect(subject.cname).to be_nil
+      end
+    end
+
+    context "an invalid domain" do
+      let(:domain) { "example.invalid" }
+
+      it "knows when a domain has no record" do
+        expect(subject.dns?).to be_falsy
+      end
+    end
+  end
+
+  context "https" do
+    let(:domain) { "pages.github.com" }
+    let(:return_code) { nil }
+
+    before do
+      stub_request(:head, "https://#{domain}/")
+        .to_return(:status => 200, :headers => { :server => "GitHub.com" })
+      allow(subject.send(:https_response)).to receive(:return_code) { return_code }
+    end
+
+    context "a site that supports HTTPS" do
+      let(:return_code) { :ok }
+
+      it "knows it supports https" do
+        expect(subject.https?).to be_truthy
+      end
+
+      it "knows there's no error" do
+        expect(subject.https_error).to be_nil
+      end
+    end
+
+    context "a site that doesn't support HTTPS" do
+      let(:return_code) { :ssl_cacert }
+
+      it "knows it doesn't support https" do
+        expect(subject.https?).to be_falsy
+      end
+
+      it "knows the error reason" do
+        expect(subject.https_error).to eql(:ssl_cacert)
+      end
+
+      it "knows it doesn't enforce https" do
+        expect(subject.enforces_https?).to be_falsy
+      end
+    end
+
+    context "a site that enforces HTTPS" do
+      let(:return_code) { :ok }
+      before do
+        stub_request(:head, "http://#{domain}/")
+          .to_return(:status => 301,
+                     :headers => { :Location => "https://#{domain}" })
+      end
+
+      it "knows it supports https" do
+        expect(subject.https?).to be_truthy
+      end
+
+      it "knows it enforces https" do
+        expect(subject.enforces_https?).to be_truthy
+      end
+    end
+
+    context "a site with a relative redirect" do
+      let(:return_code) { :ok }
+      before do
+        stub_request(:head, "http://#{domain}/")
+          .to_return(:status => 301, :headers => { :Location => "/versions" })
+      end
+
+      it "knows it doesn't enforce https" do
+        expect(subject.enforces_https?).to be_falsy
+      end
+    end
+  end
+
+  context "https eligibility" do
+    context "A records pointed to old IPs" do
+      let(:ip) { "192.30.252.153" }
+      before(:each) { allow(subject).to receive(:dns) { [a_packet] } }
+      before(:each) { allow(subject.send(:caa)).to receive(:query) { [a_packet] } }
+
+      it { is_expected.not_to be_https_eligible }
+
+      context "with unicode encoded domain" do
+        let(:domain) { "dómain.example.com" }
+
+        it { is_expected.not_to be_https_eligible }
+      end
+
+      context "with punycode encoded domain" do
+        let(:domain) { "xn--dmain-0ta.example.com" }
+
+        it { is_expected.not_to be_https_eligible }
+      end
+    end
+
+    context "A records pointed to new IPs" do
+      let(:ip) { "185.199.108.153" }
+      before(:each) { allow(subject).to receive(:dns) { [a_packet] } }
+      before(:each) { allow(subject.send(:caa)).to receive(:query) { [a_packet] } }
+
+      it { is_expected.to be_https_eligible }
+
+      context "with underscore domain" do
+        let(:domain) { "foo_bar.com" }
+
+        it { is_expected.not_to be_https_eligible }
+      end
+
+      context "with bad CAA records" do
+        let(:caa_domain) { "digicert.com" }
+        before(:each) { allow(subject.send(:caa)).to receive(:query) { [caa_packet] } }
+
+        it { is_expected.not_to be_https_eligible }
+      end
+
+      context "with good CAA records" do
+        let(:caa_domain) { "letsencrypt.org" }
+        before(:each) { allow(subject.send(:caa)).to receive(:query) { [caa_packet] } }
+
+        it { is_expected.to be_https_eligible }
+      end
+
+      context "with good additional A record" do
+        let(:ip) { "185.199.109.153" }
+
+        it { is_expected.to be_https_eligible }
+      end
+
+      context "with bad additional A record" do
+        let(:ip) { "192.30.252.153" }
+
+        it { is_expected.not_to be_https_eligible }
+      end
+
+      context "with unicode encoded domain" do
+        let(:domain) { "dómain.example.com" }
+
+        it { is_expected.to be_https_eligible }
+
+        context "with bad CAA records" do
+          let(:caa_domain) { "digicert.com" }
+          before(:each) { allow(subject.send(:caa)).to receive(:query) { [caa_packet] } }
+
+          it { is_expected.not_to be_https_eligible }
+        end
+
+        context "with good CAA records" do
+          let(:caa_domain) { "letsencrypt.org" }
+          before(:each) { allow(subject.send(:caa)).to receive(:query) { [caa_packet] } }
+
+          it { is_expected.to be_https_eligible }
+        end
+      end
+
+      context "with punycode encoded domain" do
+        let(:domain) { "xn--dmain-0ta.example.com" }
+
+        it { is_expected.to be_https_eligible }
+
+        context "with bad CAA records" do
+          let(:caa_domain) { "digicert.com" }
+          before(:each) { allow(subject.send(:caa)).to receive(:query) { [caa_packet] } }
+
+          it { is_expected.not_to be_https_eligible }
+        end
+
+        context "with good CAA records" do
+          let(:caa_domain) { "letsencrypt.org" }
+          before(:each) { allow(subject.send(:caa)).to receive(:query) { [caa_packet] } }
+
+          it { is_expected.to be_https_eligible }
+        end
+      end
+    end
+
+    context "AAAA records pointed to current IPs" do
+      let(:ip6) { "2606:50C0:8002::153" }
+      before(:each) { allow(subject).to receive(:dns) { [aaaa_packet] } }
+      before(:each) { allow(subject.send(:caa)).to receive(:query) { [aaaa_packet] } }
+
+      it { is_expected.to be_https_eligible }
+
+      context "with bad CAA records" do
+        let(:caa_domain) { "digicert.com" }
+        before(:each) { allow(subject.send(:caa)).to receive(:query) { [caa_packet] } }
+
+        it { is_expected.not_to be_https_eligible }
+      end
+
+      context "with good CAA records" do
+        let(:caa_domain) { "letsencrypt.org" }
+        before(:each) { allow(subject.send(:caa)).to receive(:query) { [caa_packet] } }
+
+        it { is_expected.to be_https_eligible }
+      end
+
+      context "with good additional A record" do
+        let(:ip6) { "2606:50c0:8003::153" }
+
+        it { is_expected.to be_https_eligible }
+      end
+
+      context "with bad additional A record" do
+        let(:ip6) { "2606:50c0:8003::1111" }
+
+        it { is_expected.not_to be_https_eligible }
+      end
+
+      context "with unicode encoded domain" do
+        let(:domain) { "dómain.example.com" }
+
+        it { is_expected.to be_https_eligible }
+
+        context "with bad CAA records" do
+          let(:caa_domain) { "digicert.com" }
+          before(:each) { allow(subject.send(:caa)).to receive(:query) { [caa_packet] } }
+
+          it { is_expected.not_to be_https_eligible }
+        end
+
+        context "with good CAA records" do
+          let(:caa_domain) { "letsencrypt.org" }
+          before(:each) { allow(subject.send(:caa)).to receive(:query) { [caa_packet] } }
+
+          it { is_expected.to be_https_eligible }
+        end
+      end
+    end
+
+    context "CNAME record pointed to username" do
+      let(:cname) { "foobar.github.io" }
+      before(:each) { allow(subject).to receive(:dns) { [cname_packet] } }
+      before(:each) { allow(subject.send(:caa)).to receive(:query) { [cname_packet] } }
+
+      it { is_expected.to be_https_eligible }
+
+      context "with bad CAA records" do
+        let(:caa_domain) { "digicert.com" }
+        before(:each) { allow(subject.send(:caa)).to receive(:query) { [caa_packet] } }
+
+        it { is_expected.to be_https_eligible }
+      end
+
+      context "with good CAA records" do
+        let(:caa_domain) { "letsencrypt.org" }
+        before(:each) { allow(subject.send(:caa)).to receive(:query) { [caa_packet] } }
+
+        it { is_expected.to be_https_eligible }
+      end
+
+      context "with unicode encoded domain" do
+        let(:domain) { "dómain.example.com" }
+
+        it { is_expected.to be_https_eligible }
+
+        context "with bad CAA records" do
+          let(:caa_domain) { "digicert.com" }
+          before(:each) { allow(subject.send(:caa)).to receive(:query) { [caa_packet] } }
+
+          it { is_expected.to be_https_eligible }
+        end
+
+        context "with good CAA records" do
+          let(:caa_domain) { "letsencrypt.org" }
+          before(:each) { allow(subject.send(:caa)).to receive(:query) { [caa_packet] } }
+
+          it { is_expected.to be_https_eligible }
+        end
+      end
+
+      context "with punycode encoded domain" do
+        let(:domain) { "xn--dmain-0ta.example.com" }
+
+        it { is_expected.to be_https_eligible }
+
+        context "with bad CAA records" do
+          let(:caa_domain) { "digicert.com" }
+          before(:each) { allow(subject.send(:caa)).to receive(:query) { [caa_packet] } }
+
+          it { is_expected.to be_https_eligible }
+        end
+
+        context "with good CAA records" do
+          let(:caa_domain) { "letsencrypt.org" }
+          before(:each) { allow(subject.send(:caa)).to receive(:query) { [caa_packet] } }
+
+          it { is_expected.to be_https_eligible }
+        end
+      end
+    end
+
+    context "CNAME record pointed elsewhere" do
+      let(:cname) { "jinglebells.com" }
+      before(:each) { allow(subject).to receive(:dns) { [cname_packet] } }
+      before(:each) { allow(subject.send(:caa)).to receive(:query) { [cname_packet] } }
+
+      it { is_expected.not_to be_https_eligible }
+
+      context "with bad CAA records" do
+        let(:caa_domain) { "digicert.com" }
+        before(:each) { allow(subject.send(:caa)).to receive(:query) { [caa_packet] } }
+
+        it { is_expected.not_to be_https_eligible }
+      end
+
+      context "with good CAA records" do
+        let(:caa_domain) { "letsencrypt.org" }
+        before(:each) { allow(subject.send(:caa)).to receive(:query) { [caa_packet] } }
+
+        it { is_expected.not_to be_https_eligible }
+      end
+
+      context "with unicode encoded domain" do
+        let(:domain) { "dómain.example.com" }
+
+        it { is_expected.not_to be_https_eligible }
+      end
+
+      context "with punycode encoded domain" do
+        let(:domain) { "xn--dmain-0ta.example.com" }
+
+        it { is_expected.not_to be_https_eligible }
+      end
+    end
+  end
+end
diff --git a/spec/github_pages_health_check/error_spec.rb b/spec/github_pages_health_check/error_spec.rb
new file mode 100644
index 0000000..bc8da15
--- /dev/null
+++ b/spec/github_pages_health_check/error_spec.rb
@@ -0,0 +1,64 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+RSpec.describe(GitHubPages::HealthCheck::Error) do
+  GitHubPages::HealthCheck::Errors.all.each do |klass|
+    next if klass::LOCAL_ONLY
+
+    context "The #{klass.name.split("::").last} error" do
+      let(:domain) { GitHubPages::HealthCheck::Domain.new("example.com") }
+      subject { klass.new(:domain => domain) }
+
+      it "has a message" do
+        expect(subject.message).to_not be_empty
+      end
+
+      it "has a documentation url" do
+        expect(klass::DOCUMENTATION_PATH).to_not be_nil
+        expect(klass::DOCUMENTATION_PATH).to_not be_empty
+        default = "/categories/github-pages-basics/"
+        expect(klass::DOCUMENTATION_PATH).to_not eql(default)
+        expect(subject.send(:documentation_url)).to_not be_nil
+      end
+
+      it "the documentation path has a trailing slash" do
+        expect(klass::DOCUMENTATION_PATH).to match(%r{/$})
+      end
+    end
+  end
+
+  context "with a repository" do
+    let(:nwo) { "github/pages.github.com" }
+    let(:repo) { GitHubPages::HealthCheck::Repository.new(nwo) }
+    subject { described_class.new(:repository => repo) }
+
+    it "knows the username" do
+      expect(subject.send(:username)).to eql("github")
+    end
+  end
+
+  context "without a repository" do
+    it "has a placeholder username" do
+      expect(subject.send(:username)).to eql("[YOUR USERNAME]")
+    end
+  end
+
+  it "builds the documentation URL" do
+    url = "https://help.github.com/categories/github-pages-basics/"
+    expect(subject.send(:documentation_url)).to eql(url)
+  end
+
+  it "builds the more info string" do
+    msg = "For more information, " \
+      "see https://help.github.com/categories/github-pages-basics/."
+    expect(subject.send(:more_info)).to eql(msg)
+  end
+
+  it "returns the message with URL" do
+    msg = "Something's wrong with your GitHub Pages site. " \
+      "For more information, " \
+      "see https://help.github.com/categories/github-pages-basics/."
+    expect(subject.message_with_url).to eql(msg)
+  end
+end
diff --git a/spec/github_pages_health_check/errors_spec.rb b/spec/github_pages_health_check/errors_spec.rb
new file mode 100644
index 0000000..bd13037
--- /dev/null
+++ b/spec/github_pages_health_check/errors_spec.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+RSpec.describe(GitHubPages::HealthCheck::Errors) do
+  it "returns the errors" do
+    expect(GitHubPages::HealthCheck::Errors.all.count).to eql(10)
+  end
+end
diff --git a/spec/github_pages_health_check/redundant_check_spec.rb b/spec/github_pages_health_check/redundant_check_spec.rb
new file mode 100644
index 0000000..e5526d2
--- /dev/null
+++ b/spec/github_pages_health_check/redundant_check_spec.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+RSpec.describe(GitHubPages::HealthCheck::RedundantCheck) do
+  let(:domain) { "www.parkermoore.de" }
+  subject { described_class.new(domain) }
+  before(:each) do
+    stub_request(:head, "http://#{domain}/")
+      .to_return(:status => 200, :body => "", :headers => { "Server" => "GitHub.com" })
+  end
+
+  it { is_expected.to be_valid }
+  it { is_expected.to be_https_eligible }
+
+  it "has a link to the check which was most valid" do
+    expect(subject.check).not_to be_nil
+    expect(subject.check).to be_valid
+  end
+end
diff --git a/spec/github_pages_health_check/repository_spec.rb b/spec/github_pages_health_check/repository_spec.rb
new file mode 100644
index 0000000..a0b98a0
--- /dev/null
+++ b/spec/github_pages_health_check/repository_spec.rb
@@ -0,0 +1,171 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+RSpec.describe(GitHubPages::HealthCheck::Repository) do
+  let(:owner) { "github" }
+  let(:repo_name) { "pages.github.com" }
+  let(:repo) { "#{owner}/#{repo_name}" }
+  let(:access_token) { nil }
+  subject { described_class.new(repo, :access_token => access_token) }
+  before(:each) do
+    stub_request(:head, "https://#{repo_name}/")
+      .to_return(:status => 200, :body => "", :headers => { "Server" => "GitHub.com" })
+  end
+
+  context "constructor" do
+    context "an invalid repository" do
+      it "should raise an error" do
+        expected = GitHubPages::HealthCheck::Errors::InvalidRepositoryError
+        expect { described_class.new("example.com") }.to raise_error(expected)
+      end
+    end
+
+    it "should extract the owner" do
+      expect(subject.owner).to eql(owner)
+    end
+
+    it "should extract the repo name" do
+      expect(subject.name).to eql(repo_name)
+    end
+
+    it "should build the name with owner" do
+      expect(subject.name_with_owner).to eql(repo)
+    end
+
+    context "with an access token" do
+      let(:access_token) { "1234" }
+
+      it "should parse the access token, when explicitly passed" do
+        actual_token = subject.instance_variable_get("@access_token")
+        expect(actual_token).to eql(access_token)
+      end
+    end
+
+    it "should parse the access token when passed as an env var" do
+      with_env "OCTOKIT_ACCESS_TOKEN", "1234" do
+        actual_token = subject.instance_variable_get("@access_token")
+        expect(actual_token).to eql("1234")
+      end
+    end
+  end
+
+  %w(error success).each do |type|
+    context "a build that was a(n) #{type}" do
+      let(:access_token) { "1234" }
+      let(:fixture) { File.read(fixture_path("build_#{type}.json")) }
+      let(:url) { "https://api.github.com/repos/#{repo}/pages/builds/latest" }
+
+      before do
+        stub_request(:get, url)
+          .to_return(:status => 200,
+                     :body => fixture,
+                     :headers => { "Content-Type" => "application/json" })
+      end
+
+      if type == "error"
+        it "fails the check" do
+          build_error = GitHubPages::HealthCheck::Errors::BuildError
+          expect { subject.check! }.to raise_error(build_error)
+        end
+
+        it "returns the build error" do
+          expect(subject.build_error).to eql("Some message")
+        end
+
+        it "knows the site wasn't built" do
+          expect(subject.built?).to be_falsy
+        end
+      else
+        it "passes the check" do
+          expect(subject.check!).to be_truthy
+        end
+
+        it "returns no build error" do
+          expect(subject.build_error).to be_nil
+        end
+
+        it "knows the site was built" do
+          expect(subject.built?).to be_truthy
+        end
+      end
+
+      it "returns the build info" do
+        expected = "351391cdcb88ffae71ec3028c91f375a8036a26b"
+        expect(subject.last_build["commit"]).to eql(expected)
+      end
+
+      it "knows the build duration" do
+        expect(subject.build_duration).to eql(2104)
+      end
+
+      it "knows when it was last built" do
+        expect(subject.last_built.to_s).to match(/2014-02-10/)
+      end
+    end
+  end
+
+  context "the client" do
+    context "with an access token" do
+      let(:access_token) { "1234" }
+
+      it "inits the client" do
+        expect(subject.send(:client)).to be_a(Octokit::Client)
+      end
+
+      it "passes the token" do
+        expect(subject.send(:client).access_token).to eql("1234")
+      end
+    end
+
+    context "without an access token" do
+      it "raises an error" do
+        expected = GitHubPages::HealthCheck::Errors::MissingAccessTokenError
+        expect { subject.send(:client) }.to raise_error(expected)
+      end
+    end
+  end
+
+  context "pages info" do
+    let(:access_token) { "1234" }
+    let(:fixture) { File.read(fixture_path("pages_info.json")) }
+    let(:url) { "https://api.github.com/repos/#{repo}/pages" }
+
+    before do
+      stub_request(:get, url)
+        .to_return(:status => 200,
+                   :body => fixture,
+                   :headers => { "Content-Type" => "application/json" })
+    end
+
+    it "returns the pages info" do
+      expect(subject.send(:pages_info).status).to eql("built")
+    end
+
+    it "knows the CNAME" do
+      expect(subject.send(:cname)).to eql("pages.github.com")
+    end
+
+    it "returns the domain" do
+      expect(subject.domain.class).to eql(GitHubPages::HealthCheck::Domain)
+      expect(subject.domain.host).to eql("pages.github.com")
+    end
+
+    context "without a CNAME" do
+      let(:access_token) { "1234" }
+      let(:fixture) { File.read(fixture_path("pages_info_no_cname.json")) }
+      let(:url) { "https://api.github.com/repos/#{repo}/pages" }
+
+      before do
+        stub_request(:get, url)
+          .to_return(:status => 200,
+                     :body => fixture,
+                     :headers => { "Content-Type" => "application/json" })
+      end
+
+      it "doesn't try to build the domain" do
+        expect(subject.domain).to be_nil
+      end
+    end
+  end
+end
diff --git a/spec/github_pages_health_check/resolver_spec.rb b/spec/github_pages_health_check/resolver_spec.rb
new file mode 100644
index 0000000..966c673
--- /dev/null
+++ b/spec/github_pages_health_check/resolver_spec.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+RSpec.describe(GitHubPages::HealthCheck::Resolver) do
+  let(:domain) { "example.com" }
+  let(:nameservers) { :default }
+  subject { described_class.new(domain, :nameservers => nameservers) }
+
+  context "default" do
+    it "uses the default resolver" do
+      expect(described_class.default_resolver).to \
+        receive(:query).with(domain, Dnsruby::Types::A).and_call_original
+      subject.query(Dnsruby::Types::A)
+    end
+  end
+
+  context "authoritative" do
+    let(:nameservers) { :authoritative }
+
+    it "uses an authoritative resolver" do
+      expect(described_class.default_resolver).to \
+        receive(:query).with(domain, Dnsruby::Types::NS).and_call_original
+      expect(subject.send(:resolver)).to \
+        receive(:query).with(domain, Dnsruby::Types::A).and_call_original
+      subject.query(Dnsruby::Types::A)
+    end
+  end
+
+  context "custom" do
+    let(:nameservers) { ["8.8.8.8", "8.8.4.4"] }
+
+    it "uses the custom resolver" do
+      expect(subject.send(:resolver).config.nameserver).to eq(nameservers)
+      expect(subject.send(:resolver)).to \
+        receive(:query).with(domain, Dnsruby::Types::A).and_call_original
+      subject.query(Dnsruby::Types::A)
+    end
+  end
+end
diff --git a/spec/github_pages_health_check/site_spec.rb b/spec/github_pages_health_check/site_spec.rb
new file mode 100644
index 0000000..bce7476
--- /dev/null
+++ b/spec/github_pages_health_check/site_spec.rb
@@ -0,0 +1,128 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+RSpec.describe(GitHubPages::HealthCheck::Site) do
+  let(:api_base) { "https://api.github.com" }
+  let(:repo) { "github/page.github.com" }
+  let(:domain) { "pages.github.com" }
+
+  before do
+    stub_request(:head, "https://#{domain}/")
+      .to_return(:status => 200, :headers => { :server => "GitHub.com" })
+
+    stub_request(:head, "http://#{domain}/")
+      .to_return(:status => 200, :headers => { :server => "GitHub.com" })
+  end
+
+  %w(domain repo).each do |init_type|
+    context "initialized with a #{init_type}" do
+      subject do
+        return described_class.new(domain) if init_type == "domain"
+
+        subject = nil
+        with_env "OCTOKIT_ACCESS_TOKEN", "1234" do
+          subject = described_class.new repo
+        end
+        subject
+      end
+
+      context "with a cname" do
+        before do
+          if init_type == "repo"
+            fixture = File.read(fixture_path("pages_info.json"))
+            stub_request(:get, "#{api_base}/repos/#{repo}/pages")
+              .to_return(:status => 200,
+                         :body => fixture,
+                         :headers => { "Content-Type" => "application/json" })
+
+            fixture = File.read(fixture_path("build_success.json"))
+            stub_request(:get, "#{api_base}/repos/#{repo}/pages/builds/latest")
+              .to_return(:status => 200,
+                         :body => fixture,
+                         :headers => { "Content-Type" => "application/json" })
+          end
+        end
+
+        it "knows the domain" do
+          expect(subject.domain).to be_a(GitHubPages::HealthCheck::Domain)
+          expect(subject.domain.host).to eql("pages.github.com")
+        end
+
+        it "builds the hash" do
+          expect(subject.to_hash[:host]).to eql("pages.github.com")
+        end
+
+        if init_type == "repo"
+          it "knows the repository" do
+            klass = GitHubPages::HealthCheck::Repository
+            expect(subject.repository).to be_a(klass)
+            expect(subject.repository.name_with_owner).to eql(repo)
+          end
+        else
+          it "knows it doesn't know the repository" do
+            expect(subject.repository).to be_nil
+          end
+        end
+
+        context "json" do
+          let(:json) { JSON.parse subject.to_json }
+
+          it "returns valid json" do
+            expect(json.delete("uri")).to eql("https://pages.github.com/")
+          end
+        end
+
+        context "hash" do
+          let(:valid_values) { [true, false, nil, :default] }
+
+          it "returns a valid values" do
+            hash = subject.to_hash
+            expect(hash.delete(:host)).to eql(domain)
+            expect(hash.delete(:uri)).to eql("https://#{domain}/")
+
+            if init_type == "repo"
+              expect(hash.delete(:name_with_owner)).to eql(repo)
+              expect(hash.delete(:last_built).to_s).to match(/2014-02-10/)
+              expect(hash.delete(:build_duration)).to eql(2104)
+            end
+
+            hash.each do |key, value|
+              msg = "Expected #{key} to be one of #{valid_values}"
+              expect(valid_values).to include(value), msg
+            end
+          end
+        end
+      end
+
+      context "with no cname" do
+        before do
+          if init_type == "repo"
+            fixture = File.read(fixture_path("pages_info_no_cname.json"))
+            stub_request(:get, "#{api_base}/repos/#{repo}/pages")
+              .to_return(:status => 200,
+                         :body => fixture,
+                         :headers => { "Content-Type" => "application/json" })
+
+            fixture = File.read(fixture_path("build_success.json"))
+            url = "#{api_base}/repos/#{repo}/pages/builds/latest"
+            stub_request(:get, url)
+              .to_return(:status => 200,
+                         :body => fixture,
+                         :headers => { "Content-Type" => "application/json" })
+          end
+        end
+
+        if init_type == "repo"
+          it "knows it doesn't know the domain" do
+            expect(subject.domain).to be_nil
+          end
+
+          it "doesnt err out when it checks" do
+            expect(subject.check!).to be_truthy
+          end
+        end
+      end
+    end
+  end
+end
diff --git a/spec/github_pages_health_check_spec.rb b/spec/github_pages_health_check_spec.rb
new file mode 100644
index 0000000..dc1e90f
--- /dev/null
+++ b/spec/github_pages_health_check_spec.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+RSpec.describe(GitHubPages::HealthCheck) do
+  let(:domain) { "pages.github.com" }
+  before(:each) do
+    stub_request(:head, "https://#{domain}/")
+      .to_return(:status => 200, :body => "", :headers => { "Server" => "GitHub.com" })
+  end
+
+  it "checks" do
+    check = GitHubPages::HealthCheck.check(domain)
+    expect(check.class).to eql(GitHubPages::HealthCheck::Site)
+    expect(check.domain.host).to eql(domain)
+  end
+
+  it "sets a network proxy url" do
+    expect(GitHubPages::HealthCheck.typhoeus_options).to include(:proxy => nil)
+    GitHubPages::HealthCheck.set_proxy("http://proxy.org")
+    expect(GitHubPages::HealthCheck.typhoeus_options).to include(:proxy => "http://proxy.org")
+  end
+end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
new file mode 100644
index 0000000..3609286
--- /dev/null
+++ b/spec/spec_helper.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+require "bundler/setup"
+require "webmock/rspec"
+require "pry-byebug"
+require_relative "../lib/github-pages-health-check"
+
+WebMock.disable_net_connect!
+
+RSpec.configure do |config|
+  config.raise_errors_for_deprecations!
+  config.disable_monkey_patching!
+  config.example_status_persistence_file_path = "spec/examples.txt"
+  config.default_formatter = "doc" if config.files_to_run.one?
+  config.order = :random
+  Kernel.srand config.seed
+end
+
+def with_env(key, value)
+  old_env = ENV[key]
+  ENV[key] = value
+  yield
+  ENV[key] = old_env
+end
+
+def fixture_path(fixture = "")
+  File.expand_path "./fixtures/#{fixture}", File.dirname(__FILE__)
+end

Debdiff

[The following lists of changes regard files as different if they have different names, permissions or owners.]

Files in second set of .debs but not in first

-rw-r--r--  root/root   /usr/share/rubygems-integration/all/specifications/github-pages-health-check-1.18.1.gemspec

Files in first set of .debs but not in second

-rw-r--r--  root/root   /usr/share/rubygems-integration/all/specifications/github-pages-health-check-1.16.1.gemspec

Control files: lines which differ (wdiff format)

  • Ruby-Versions: all

More details

Full run details