pages_domain_spec.rb 13.0 KB
Newer Older
1 2
# frozen_string_literal: true

K
Kamil Trzcinski 已提交
3 4
require 'spec_helper'

5
describe PagesDomain do
6 7 8 9
  using RSpec::Parameterized::TableSyntax

  subject(:pages_domain) { described_class.new }

K
Kamil Trzcinski 已提交
10 11 12
  describe 'associations' do
    it { is_expected.to belong_to(:project) }
  end
13

14
  describe 'validate domain' do
D
Drew Blessing 已提交
15
    subject(:pages_domain) { build(:pages_domain, domain: domain) }
K
Kamil Trzcinski 已提交
16 17 18 19

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

20
      it { is_expected.to validate_uniqueness_of(:domain).case_insensitive }
K
Kamil Trzcinski 已提交
21 22
    end

R
Rob Watson 已提交
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 57 58
    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?
D
Drew Blessing 已提交
59
        end
R
Rob Watson 已提交
60
      end
D
Drew Blessing 已提交
61

R
Rob Watson 已提交
62 63 64 65 66 67 68 69 70 71 72 73 74
      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
D
Drew Blessing 已提交
75

R
Rob Watson 已提交
76 77 78 79
      with_them do
        it "is adds the expected errors" do
          expect(pages_domain.errors.keys).to eq errors_on
        end
D
Drew Blessing 已提交
80
      end
K
Kamil Trzcinski 已提交
81 82 83
    end
  end

84 85 86 87 88 89 90 91 92 93 94
  describe 'when certificate is specified' do
    let(:domain) { build(:pages_domain) }

    it 'saves validity time' do
      domain.save

      expect(domain.certificate_valid_not_before).to be_like_time(Time.parse("2016-02-12 14:32:00 UTC"))
      expect(domain.certificate_valid_not_after).to be_like_time(Time.parse("2020-04-12 14:32:00 UTC"))
    end
  end

K
Kamil Trzcinski 已提交
95 96 97
  describe 'validate certificate' do
    subject { domain }

R
Rob Watson 已提交
98 99
    context 'with matching key' do
      let(:domain) { build(:pages_domain) }
K
Kamil Trzcinski 已提交
100

R
Rob Watson 已提交
101
      it { is_expected.to be_valid }
K
Kamil Trzcinski 已提交
102 103
    end

R
Rob Watson 已提交
104 105
    context 'when no certificate is specified' do
      let(:domain) { build(:pages_domain, :without_certificate) }
K
Kamil Trzcinski 已提交
106

G
Grzegorz Bizon 已提交
107
      it { is_expected.not_to be_valid }
K
Kamil Trzcinski 已提交
108 109
    end

R
Rob Watson 已提交
110 111
    context 'when no key is specified' do
      let(:domain) { build(:pages_domain, :without_key) }
K
Kamil Trzcinski 已提交
112

R
Rob Watson 已提交
113
      it { is_expected.not_to be_valid }
K
Kamil Trzcinski 已提交
114 115 116
    end

    context 'for not matching key' do
R
Rob Watson 已提交
117
      let(:domain) { build(:pages_domain, :with_missing_chain) }
K
Kamil Trzcinski 已提交
118

G
Grzegorz Bizon 已提交
119
      it { is_expected.not_to be_valid }
K
Kamil Trzcinski 已提交
120 121 122
    end
  end

123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154
  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

155
  describe '#url' do
K
Kamil Trzcinski 已提交
156 157
    subject { domain.url }

R
Rob Watson 已提交
158
    let(:domain) { build(:pages_domain) }
K
Kamil Trzcinski 已提交
159

R
Rob Watson 已提交
160
    it { is_expected.to eq("https://#{domain.domain}") }
K
Kamil Trzcinski 已提交
161

R
Rob Watson 已提交
162 163
    context 'without the certificate' do
      let(:domain) { build(:pages_domain, :without_certificate) }
K
Kamil Trzcinski 已提交
164

R
Rob Watson 已提交
165
      it { is_expected.to eq("http://#{domain.domain}") }
K
Kamil Trzcinski 已提交
166 167 168
    end
  end

169
  describe '#has_matching_key?' do
K
Kamil Trzcinski 已提交
170 171
    subject { domain.has_matching_key? }

R
Rob Watson 已提交
172
    let(:domain) { build(:pages_domain) }
K
Kamil Trzcinski 已提交
173

R
Rob Watson 已提交
174
    it { is_expected.to be_truthy }
K
Kamil Trzcinski 已提交
175 176

    context 'for invalid key' do
R
Rob Watson 已提交
177
      let(:domain) { build(:pages_domain, :with_missing_chain) }
K
Kamil Trzcinski 已提交
178 179 180 181 182

      it { is_expected.to be_falsey }
    end
  end

183
  describe '#has_intermediates?' do
K
Kamil Trzcinski 已提交
184 185 186
    subject { domain.has_intermediates? }

    context 'for self signed' do
R
Rob Watson 已提交
187
      let(:domain) { build(:pages_domain) }
K
Kamil Trzcinski 已提交
188 189 190 191

      it { is_expected.to be_truthy }
    end

192 193
    context 'for missing certificate chain' do
      let(:domain) { build(:pages_domain, :with_missing_chain) }
K
Kamil Trzcinski 已提交
194 195 196

      it { is_expected.to be_falsey }
    end
197 198

    context 'for trusted certificate chain' do
K
Kamil Trzcinski 已提交
199 200 201 202
      # 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

203 204 205 206
      let(:domain) { build(:pages_domain, :with_trusted_chain) }

      it { is_expected.to be_truthy }
    end
K
Kamil Trzcinski 已提交
207 208
  end

209
  describe '#expired?' do
K
Kamil Trzcinski 已提交
210 211 212
    subject { domain.expired? }

    context 'for valid' do
R
Rob Watson 已提交
213
      let(:domain) { build(:pages_domain) }
K
Kamil Trzcinski 已提交
214 215 216 217 218 219 220 221 222 223 224

      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

225
  describe '#subject' do
R
Rob Watson 已提交
226
    let(:domain) { build(:pages_domain) }
K
Kamil Trzcinski 已提交
227 228 229 230 231 232

    subject { domain.subject }

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

233
  describe '#certificate_text' do
R
Rob Watson 已提交
234
    let(:domain) { build(:pages_domain) }
K
Kamil Trzcinski 已提交
235 236 237 238

    subject { domain.certificate_text }

    # We test only existence of output, since the output is long
G
Grzegorz Bizon 已提交
239
    it { is_expected.not_to be_empty }
K
Kamil Trzcinski 已提交
240
  end
241

R
Rob Watson 已提交
242 243 244 245 246 247 248 249 250 251 252 253
  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

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 317 318 319 320 321 322 323 324 325 326 327 328 329
  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
R
Rob Watson 已提交
330 331
        set(:domain_without_tls) { create(:pages_domain, :without_certificate, :without_key) }
        set(:domain) { create(:pages_domain) }
332

R
Rob Watson 已提交
333
        let(:cert1) { domain.certificate }
334
        let(:cert2) { cert1 + ' ' }
R
Rob Watson 已提交
335
        let(:key1) { domain.key }
336 337 338
        let(:key2) { key1 + ' ' }

        it 'updates when added' do
R
Rob Watson 已提交
339
          expect(domain_without_tls).to receive(:update_daemon)
340

R
Rob Watson 已提交
341
          domain_without_tls.update!(key: key1, certificate: cert1)
342 343 344
        end

        it 'updates when changed' do
R
Rob Watson 已提交
345
          expect(domain).to receive(:update_daemon)
346

R
Rob Watson 已提交
347
          domain.update!(key: key2, certificate: cert2)
348 349 350
        end

        it 'updates when removed' do
R
Rob Watson 已提交
351
          expect(domain).to receive(:update_daemon)
352

R
Rob Watson 已提交
353
          domain.update!(key: nil, certificate: nil)
354 355 356 357
        end
      end
    end
  end
358

359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454
  describe '#user_provided_key' do
    subject { domain.user_provided_key }

    context 'when certificate is provided by user' do
      let(:domain) { create(:pages_domain) }

      it 'returns key' do
        is_expected.to eq(domain.key)
      end
    end

    context 'when certificate is provided by gitlab' do
      let(:domain) { create(:pages_domain, :letsencrypt) }

      it 'returns nil' do
        is_expected.to be_nil
      end
    end
  end

  describe '#user_provided_certificate' do
    subject { domain.user_provided_certificate }

    context 'when certificate is provided by user' do
      let(:domain) { create(:pages_domain) }

      it 'returns key' do
        is_expected.to eq(domain.certificate)
      end
    end

    context 'when certificate is provided by gitlab' do
      let(:domain) { create(:pages_domain, :letsencrypt) }

      it 'returns nil' do
        is_expected.to be_nil
      end
    end
  end

  shared_examples 'certificate setter' do |attribute, setter_name, old_certificate_source, new_certificate_source|
    let(:domain) do
      create(:pages_domain, certificate_source: old_certificate_source)
    end

    let(:old_value) { domain.public_send(attribute) }

    subject { domain.public_send(setter_name, new_value) }

    context 'when value has been changed' do
      let(:new_value) { 'new_value' }

      it "assignes new value to #{attribute}" do
        expect do
          subject
        end.to change { domain.public_send(attribute) }.from(old_value).to('new_value')
      end

      it 'changes certificate source' do
        expect do
          subject
        end.to change { domain.certificate_source }.from(old_certificate_source).to(new_certificate_source)
      end
    end

    context 'when value has not been not changed' do
      let(:new_value) { old_value }

      it 'does not change certificate source' do
        expect do
          subject
        end.not_to change { domain.certificate_source }.from(old_certificate_source)
      end
    end
  end

  describe '#user_provided_key=' do
    include_examples('certificate setter', 'key', 'user_provided_key=',
                     'gitlab_provided', 'user_provided')
  end

  describe '#gitlab_provided_key=' do
    include_examples('certificate setter', 'key', 'gitlab_provided_key=',
                     'user_provided', 'gitlab_provided')
  end

  describe '#user_provided_certificate=' do
    include_examples('certificate setter', 'certificate', 'user_provided_certificate=',
                     'gitlab_provided', 'user_provided')
  end

  describe '#gitlab_provided_certificate=' do
    include_examples('certificate setter', 'certificate', 'gitlab_provided_certificate=',
                     'user_provided', 'gitlab_provided')
  end

455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481
  describe '.for_removal' do
    subject { described_class.for_removal }

    context 'when domain is not schedule for removal' do
      let!(:domain) { create :pages_domain }

      it 'does not return domain' do
        is_expected.to be_empty
      end
    end

    context 'when domain is scheduled for removal yesterday' do
      let!(:domain) { create :pages_domain, remove_at: 1.day.ago }

      it 'returns domain' do
        is_expected.to eq([domain])
      end
    end

    context 'when domain is scheduled for removal tomorrow' do
      let!(:domain) { create :pages_domain, remove_at: 1.day.from_now }

      it 'does not return domain' do
        is_expected.to be_empty
      end
    end
  end
K
Kamil Trzcinski 已提交
482
end