pages_domain_spec.rb 9.19 KB
Newer Older
Kamil Trzcinski's avatar
Kamil Trzcinski committed
1 2
require 'spec_helper'

3
describe PagesDomain do
4 5 6 7
  using RSpec::Parameterized::TableSyntax

  subject(:pages_domain) { described_class.new }

Kamil Trzcinski's avatar
Kamil Trzcinski committed
8 9 10
  describe 'associations' do
    it { is_expected.to belong_to(:project) }
  end
11

12
  describe 'validate domain' do
Drew Blessing's avatar
Drew Blessing committed
13
    subject(:pages_domain) { build(:pages_domain, domain: domain) }
Kamil Trzcinski's avatar
Kamil Trzcinski committed
14 15 16 17

    context 'is unique' do
      let(:domain) { 'my.domain.com' }

18
      it { is_expected.to validate_uniqueness_of(:domain).case_insensitive }
Kamil Trzcinski's avatar
Kamil Trzcinski committed
19 20
    end

Rob Watson's avatar
Rob Watson committed
21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
    describe "hostname" do
      {
        'my.domain.com'    => true,
        '123.456.789'      => true,
        '0x12345.com'      => true,
        '0123123'          => true,
        '_foo.com'         => false,
        'reserved.com'     => false,
        'a.reserved.com'   => false,
        nil                => false
      }.each do |value, validity|
        context "domain #{value.inspect} validity" do
          before do
            allow(Settings.pages).to receive(:host).and_return('reserved.com')
          end

          let(:domain) { value }

          it { expect(pages_domain.valid?).to eq(validity) }
        end
      end
    end

    describe "HTTPS-only" do
      using RSpec::Parameterized::TableSyntax

      let(:domain) { 'my.domain.com' }

      let(:project) do
        instance_double(Project, pages_https_only?: pages_https_only)
      end

      let(:pages_domain) do
        build(:pages_domain, certificate: certificate, key: key).tap do |pd|
          allow(pd).to receive(:project).and_return(project)
          pd.valid?
Drew Blessing's avatar
Drew Blessing committed
57
        end
Rob Watson's avatar
Rob Watson committed
58
      end
Drew Blessing's avatar
Drew Blessing committed
59

Rob Watson's avatar
Rob Watson committed
60 61 62 63 64 65 66 67 68 69 70 71 72
      where(:pages_https_only, :certificate, :key, :errors_on) do
        attributes = attributes_for(:pages_domain)
        cert, key = attributes.fetch_values(:certificate, :key)

        true  | nil  | nil | %i(certificate key)
        true  | cert | nil | %i(key)
        true  | nil  | key | %i(certificate key)
        true  | cert | key | []
        false | nil  | nil | []
        false | cert | nil | %i(key)
        false | nil  | key | %i(key)
        false | cert | key | []
      end
Drew Blessing's avatar
Drew Blessing committed
73

Rob Watson's avatar
Rob Watson committed
74 75 76 77
      with_them do
        it "is adds the expected errors" do
          expect(pages_domain.errors.keys).to eq errors_on
        end
Drew Blessing's avatar
Drew Blessing committed
78
      end
Kamil Trzcinski's avatar
Kamil Trzcinski committed
79 80 81 82 83 84
    end
  end

  describe 'validate certificate' do
    subject { domain }

Rob Watson's avatar
Rob Watson committed
85 86
    context 'with matching key' do
      let(:domain) { build(:pages_domain) }
Kamil Trzcinski's avatar
Kamil Trzcinski committed
87

Rob Watson's avatar
Rob Watson committed
88
      it { is_expected.to be_valid }
Kamil Trzcinski's avatar
Kamil Trzcinski committed
89 90
    end

Rob Watson's avatar
Rob Watson committed
91 92
    context 'when no certificate is specified' do
      let(:domain) { build(:pages_domain, :without_certificate) }
Kamil Trzcinski's avatar
Kamil Trzcinski committed
93

Grzegorz Bizon's avatar
Grzegorz Bizon committed
94
      it { is_expected.not_to be_valid }
Kamil Trzcinski's avatar
Kamil Trzcinski committed
95 96
    end

Rob Watson's avatar
Rob Watson committed
97 98
    context 'when no key is specified' do
      let(:domain) { build(:pages_domain, :without_key) }
Kamil Trzcinski's avatar
Kamil Trzcinski committed
99

Rob Watson's avatar
Rob Watson committed
100
      it { is_expected.not_to be_valid }
Kamil Trzcinski's avatar
Kamil Trzcinski committed
101 102 103
    end

    context 'for not matching key' do
Rob Watson's avatar
Rob Watson committed
104
      let(:domain) { build(:pages_domain, :with_missing_chain) }
Kamil Trzcinski's avatar
Kamil Trzcinski committed
105

Grzegorz Bizon's avatar
Grzegorz Bizon committed
106
      it { is_expected.not_to be_valid }
Kamil Trzcinski's avatar
Kamil Trzcinski committed
107 108 109
    end
  end

110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141
  describe 'validations' do
    it { is_expected.to validate_presence_of(:verification_code) }
  end

  describe '#verification_code' do
    subject { pages_domain.verification_code }

    it 'is set automatically with 128 bits of SecureRandom data' do
      expect(SecureRandom).to receive(:hex).with(16) { 'verification code' }

      is_expected.to eq('verification code')
    end
  end

  describe '#keyed_verification_code' do
    subject { pages_domain.keyed_verification_code }

    it { is_expected.to eq("gitlab-pages-verification-code=#{pages_domain.verification_code}") }
  end

  describe '#verification_domain' do
    subject { pages_domain.verification_domain }

    it { is_expected.to be_nil }

    it 'is a well-known subdomain if the domain is present' do
      pages_domain.domain = 'example.com'

      is_expected.to eq('_gitlab-pages-verification-code.example.com')
    end
  end

142
  describe '#url' do
Kamil Trzcinski's avatar
Kamil Trzcinski committed
143 144
    subject { domain.url }

Rob Watson's avatar
Rob Watson committed
145
    let(:domain) { build(:pages_domain) }
Kamil Trzcinski's avatar
Kamil Trzcinski committed
146

Rob Watson's avatar
Rob Watson committed
147
    it { is_expected.to eq("https://#{domain.domain}") }
Kamil Trzcinski's avatar
Kamil Trzcinski committed
148

Rob Watson's avatar
Rob Watson committed
149 150
    context 'without the certificate' do
      let(:domain) { build(:pages_domain, :without_certificate) }
Kamil Trzcinski's avatar
Kamil Trzcinski committed
151

Rob Watson's avatar
Rob Watson committed
152
      it { is_expected.to eq("http://#{domain.domain}") }
Kamil Trzcinski's avatar
Kamil Trzcinski committed
153 154 155
    end
  end

156
  describe '#has_matching_key?' do
Kamil Trzcinski's avatar
Kamil Trzcinski committed
157 158
    subject { domain.has_matching_key? }

Rob Watson's avatar
Rob Watson committed
159
    let(:domain) { build(:pages_domain) }
Kamil Trzcinski's avatar
Kamil Trzcinski committed
160

Rob Watson's avatar
Rob Watson committed
161
    it { is_expected.to be_truthy }
Kamil Trzcinski's avatar
Kamil Trzcinski committed
162 163

    context 'for invalid key' do
Rob Watson's avatar
Rob Watson committed
164
      let(:domain) { build(:pages_domain, :with_missing_chain) }
Kamil Trzcinski's avatar
Kamil Trzcinski committed
165 166 167 168 169

      it { is_expected.to be_falsey }
    end
  end

170
  describe '#has_intermediates?' do
Kamil Trzcinski's avatar
Kamil Trzcinski committed
171 172 173
    subject { domain.has_intermediates? }

    context 'for self signed' do
Rob Watson's avatar
Rob Watson committed
174
      let(:domain) { build(:pages_domain) }
Kamil Trzcinski's avatar
Kamil Trzcinski committed
175 176 177 178

      it { is_expected.to be_truthy }
    end

179 180
    context 'for missing certificate chain' do
      let(:domain) { build(:pages_domain, :with_missing_chain) }
Kamil Trzcinski's avatar
Kamil Trzcinski committed
181 182 183

      it { is_expected.to be_falsey }
    end
184 185

    context 'for trusted certificate chain' do
Kamil Trzcinski's avatar
Kamil Trzcinski committed
186 187 188 189
      # We only validate that we can to rebuild the trust chain, for certificates
      # We assume that 'AddTrustExternalCARoot' needed to validate the chain is in trusted store.
      # It will be if ca-certificates is installed on Debian/Ubuntu/Alpine

190 191 192 193
      let(:domain) { build(:pages_domain, :with_trusted_chain) }

      it { is_expected.to be_truthy }
    end
Kamil Trzcinski's avatar
Kamil Trzcinski committed
194 195
  end

196
  describe '#expired?' do
Kamil Trzcinski's avatar
Kamil Trzcinski committed
197 198 199
    subject { domain.expired? }

    context 'for valid' do
Rob Watson's avatar
Rob Watson committed
200
      let(:domain) { build(:pages_domain) }
Kamil Trzcinski's avatar
Kamil Trzcinski committed
201 202 203 204 205 206 207 208 209 210 211

      it { is_expected.to be_falsey }
    end

    context 'for expired' do
      let(:domain) { build(:pages_domain, :with_expired_certificate) }

      it { is_expected.to be_truthy }
    end
  end

212
  describe '#subject' do
Rob Watson's avatar
Rob Watson committed
213
    let(:domain) { build(:pages_domain) }
Kamil Trzcinski's avatar
Kamil Trzcinski committed
214 215 216 217 218 219

    subject { domain.subject }

    it { is_expected.to eq('/CN=test-certificate') }
  end

220
  describe '#certificate_text' do
Rob Watson's avatar
Rob Watson committed
221
    let(:domain) { build(:pages_domain) }
Kamil Trzcinski's avatar
Kamil Trzcinski committed
222 223 224 225

    subject { domain.certificate_text }

    # We test only existence of output, since the output is long
Grzegorz Bizon's avatar
Grzegorz Bizon committed
226
    it { is_expected.not_to be_empty }
Kamil Trzcinski's avatar
Kamil Trzcinski committed
227
  end
228

Rob Watson's avatar
Rob Watson committed
229 230 231 232 233 234 235 236 237 238 239 240
  describe "#https?" do
    context "when a certificate is present" do
      subject { build(:pages_domain) }
      it { is_expected.to be_https }
    end

    context "when no certificate is present" do
      subject { build(:pages_domain, :without_certificate) }
      it { is_expected.not_to be_https }
    end
  end

241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316
  describe '#update_daemon' do
    it 'runs when the domain is created' do
      domain = build(:pages_domain)

      expect(domain).to receive(:update_daemon)

      domain.save!
    end

    it 'runs when the domain is destroyed' do
      domain = create(:pages_domain)

      expect(domain).to receive(:update_daemon)

      domain.destroy!
    end

    it 'delegates to Projects::UpdatePagesConfigurationService' do
      service = instance_double('Projects::UpdatePagesConfigurationService')
      expect(Projects::UpdatePagesConfigurationService).to receive(:new) { service }
      expect(service).to receive(:execute)

      create(:pages_domain)
    end

    context 'configuration updates when attributes change' do
      set(:project1) { create(:project) }
      set(:project2) { create(:project) }
      set(:domain) { create(:pages_domain) }

      where(:attribute, :old_value, :new_value, :update_expected) do
        now = Time.now
        future = now + 1.day

        :project | nil       | :project1 | true
        :project | :project1 | :project1 | false
        :project | :project1 | :project2 | true
        :project | :project1 | nil       | true

        # domain can't be set to nil
        :domain | 'a.com' | 'a.com' | false
        :domain | 'a.com' | 'b.com' | true

        # verification_code can't be set to nil
        :verification_code | 'foo' | 'foo'  | false
        :verification_code | 'foo' | 'bar'  | false

        :verified_at | nil | now    | false
        :verified_at | now | now    | false
        :verified_at | now | future | false
        :verified_at | now | nil    | false

        :enabled_until | nil | now    | true
        :enabled_until | now | now    | false
        :enabled_until | now | future | false
        :enabled_until | now | nil    | true
      end

      with_them do
        it 'runs if a relevant attribute has changed' do
          a = old_value.is_a?(Symbol) ? send(old_value) : old_value
          b = new_value.is_a?(Symbol) ? send(new_value) : new_value

          domain.update!(attribute => a)

          if update_expected
            expect(domain).to receive(:update_daemon)
          else
            expect(domain).not_to receive(:update_daemon)
          end

          domain.update!(attribute => b)
        end
      end

      context 'TLS configuration' do
Rob Watson's avatar
Rob Watson committed
317 318
        set(:domain_without_tls) { create(:pages_domain, :without_certificate, :without_key) }
        set(:domain) { create(:pages_domain) }
319

Rob Watson's avatar
Rob Watson committed
320
        let(:cert1) { domain.certificate }
321
        let(:cert2) { cert1 + ' ' }
Rob Watson's avatar
Rob Watson committed
322
        let(:key1) { domain.key }
323 324 325
        let(:key2) { key1 + ' ' }

        it 'updates when added' do
Rob Watson's avatar
Rob Watson committed
326
          expect(domain_without_tls).to receive(:update_daemon)
327

Rob Watson's avatar
Rob Watson committed
328
          domain_without_tls.update!(key: key1, certificate: cert1)
329 330 331
        end

        it 'updates when changed' do
Rob Watson's avatar
Rob Watson committed
332
          expect(domain).to receive(:update_daemon)
333

Rob Watson's avatar
Rob Watson committed
334
          domain.update!(key: key2, certificate: cert2)
335 336 337
        end

        it 'updates when removed' do
Rob Watson's avatar
Rob Watson committed
338
          expect(domain).to receive(:update_daemon)
339

Rob Watson's avatar
Rob Watson committed
340
          domain.update!(key: nil, certificate: nil)
341 342 343 344
        end
      end
    end
  end
Kamil Trzcinski's avatar
Kamil Trzcinski committed
345
end