77e2e45364
This validation prevents the domain from being saved from the UI e.g. when user tries to enable Let's Encrypt integration
541 lines
15 KiB
Ruby
541 lines
15 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require 'spec_helper'
|
|
|
|
describe PagesDomain do
|
|
using RSpec::Parameterized::TableSyntax
|
|
|
|
subject(:pages_domain) { described_class.new }
|
|
|
|
describe 'associations' do
|
|
it { is_expected.to belong_to(:project) }
|
|
end
|
|
|
|
describe 'validate domain' do
|
|
subject(:pages_domain) { build(:pages_domain, domain: domain) }
|
|
|
|
context 'is unique' do
|
|
let(:domain) { 'my.domain.com' }
|
|
|
|
it { is_expected.to validate_uniqueness_of(:domain).case_insensitive }
|
|
end
|
|
|
|
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,
|
|
auto_ssl_enabled: auto_ssl_enabled).tap do |pd|
|
|
allow(pd).to receive(:project).and_return(project)
|
|
pd.valid?
|
|
end
|
|
end
|
|
|
|
where(:pages_https_only, :certificate, :key, :auto_ssl_enabled, :errors_on) do
|
|
attributes = attributes_for(:pages_domain)
|
|
cert, key = attributes.fetch_values(:certificate, :key)
|
|
|
|
true | nil | nil | false | %i(certificate key)
|
|
true | nil | nil | true | []
|
|
true | cert | nil | false | %i(key)
|
|
true | cert | nil | true | %i(key)
|
|
true | nil | key | false | %i(certificate key)
|
|
true | nil | key | true | %i(key)
|
|
true | cert | key | false | []
|
|
true | cert | key | true | []
|
|
false | nil | nil | false | []
|
|
false | nil | nil | true | []
|
|
false | cert | nil | false | %i(key)
|
|
false | cert | nil | true | %i(key)
|
|
false | nil | key | false | %i(key)
|
|
false | nil | key | true | %i(key)
|
|
false | cert | key | false | []
|
|
false | cert | key | true | []
|
|
end
|
|
|
|
with_them do
|
|
it "is adds the expected errors" do
|
|
expect(pages_domain.errors.keys).to eq errors_on
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
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
|
|
|
|
describe 'validate certificate' do
|
|
subject { domain }
|
|
|
|
context 'with matching key' do
|
|
let(:domain) { build(:pages_domain) }
|
|
|
|
it { is_expected.to be_valid }
|
|
end
|
|
|
|
context 'when no certificate is specified' do
|
|
let(:domain) { build(:pages_domain, :without_certificate) }
|
|
|
|
it { is_expected.not_to be_valid }
|
|
end
|
|
|
|
context 'when no key is specified' do
|
|
let(:domain) { build(:pages_domain, :without_key) }
|
|
|
|
it { is_expected.not_to be_valid }
|
|
end
|
|
|
|
context 'for not matching key' do
|
|
let(:domain) { build(:pages_domain, :with_missing_chain) }
|
|
|
|
it { is_expected.not_to be_valid }
|
|
end
|
|
|
|
context 'when certificate is expired' do
|
|
let(:domain) do
|
|
build(:pages_domain, :with_trusted_expired_chain)
|
|
end
|
|
|
|
context 'when certificate is being changed' do
|
|
it "adds error to certificate" do
|
|
domain.valid?
|
|
|
|
expect(domain.errors.keys).to contain_exactly(:key, :certificate)
|
|
end
|
|
end
|
|
|
|
context 'when certificate is already saved' do
|
|
it "doesn't add error to certificate" do
|
|
domain.save(validate: false)
|
|
|
|
domain.valid?
|
|
|
|
expect(domain.errors.keys).to contain_exactly(:key)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
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
|
|
|
|
describe '#url' do
|
|
subject { domain.url }
|
|
|
|
let(:domain) { build(:pages_domain) }
|
|
|
|
it { is_expected.to eq("https://#{domain.domain}") }
|
|
|
|
context 'without the certificate' do
|
|
let(:domain) { build(:pages_domain, :without_certificate) }
|
|
|
|
it { is_expected.to eq("http://#{domain.domain}") }
|
|
end
|
|
end
|
|
|
|
describe '#has_matching_key?' do
|
|
subject { domain.has_matching_key? }
|
|
|
|
let(:domain) { build(:pages_domain) }
|
|
|
|
it { is_expected.to be_truthy }
|
|
|
|
context 'for invalid key' do
|
|
let(:domain) { build(:pages_domain, :with_missing_chain) }
|
|
|
|
it { is_expected.to be_falsey }
|
|
end
|
|
end
|
|
|
|
describe '#has_intermediates?' do
|
|
subject { domain.has_intermediates? }
|
|
|
|
context 'for self signed' do
|
|
let(:domain) { build(:pages_domain) }
|
|
|
|
it { is_expected.to be_truthy }
|
|
end
|
|
|
|
context 'for missing certificate chain' do
|
|
let(:domain) { build(:pages_domain, :with_missing_chain) }
|
|
|
|
it { is_expected.to be_falsey }
|
|
end
|
|
|
|
context 'for trusted certificate chain' do
|
|
# 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
|
|
|
|
let(:domain) { build(:pages_domain, :with_trusted_chain) }
|
|
|
|
it { is_expected.to be_truthy }
|
|
end
|
|
end
|
|
|
|
describe '#expired?' do
|
|
subject { domain.expired? }
|
|
|
|
context 'for valid' do
|
|
let(:domain) { build(:pages_domain) }
|
|
|
|
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
|
|
|
|
describe '#subject' do
|
|
let(:domain) { build(:pages_domain) }
|
|
|
|
subject { domain.subject }
|
|
|
|
it { is_expected.to eq('/CN=test-certificate') }
|
|
end
|
|
|
|
describe '#certificate_text' do
|
|
let(:domain) { build(:pages_domain) }
|
|
|
|
subject { domain.certificate_text }
|
|
|
|
# We test only existence of output, since the output is long
|
|
it { is_expected.not_to be_empty }
|
|
end
|
|
|
|
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
|
|
|
|
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
|
|
set(:domain_without_tls) { create(:pages_domain, :without_certificate, :without_key) }
|
|
set(:domain) { create(:pages_domain) }
|
|
|
|
let(:cert1) { domain.certificate }
|
|
let(:cert2) { cert1 + ' ' }
|
|
let(:key1) { domain.key }
|
|
let(:key2) { key1 + ' ' }
|
|
|
|
it 'updates when added' do
|
|
expect(domain_without_tls).to receive(:update_daemon)
|
|
|
|
domain_without_tls.update!(key: key1, certificate: cert1)
|
|
end
|
|
|
|
it 'updates when changed' do
|
|
expect(domain).to receive(:update_daemon)
|
|
|
|
domain.update!(key: key2, certificate: cert2)
|
|
end
|
|
|
|
it 'updates when removed' do
|
|
expect(domain).to receive(:update_daemon)
|
|
|
|
domain.update!(key: nil, certificate: nil)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
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
|
|
|
|
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
|
|
|
|
describe '.need_auto_ssl_renewal' do
|
|
subject { described_class.need_auto_ssl_renewal }
|
|
|
|
let!(:domain_with_user_provided_certificate) { create(:pages_domain) }
|
|
let!(:domain_with_expired_user_provided_certificate) do
|
|
create(:pages_domain, :with_expired_certificate)
|
|
end
|
|
let!(:domain_with_user_provided_certificate_and_auto_ssl) do
|
|
create(:pages_domain, auto_ssl_enabled: true)
|
|
end
|
|
|
|
let!(:domain_with_gitlab_provided_certificate) { create(:pages_domain, :letsencrypt) }
|
|
let!(:domain_with_expired_gitlab_provided_certificate) do
|
|
create(:pages_domain, :letsencrypt, :with_expired_certificate)
|
|
end
|
|
|
|
it 'contains only domains needing verification' do
|
|
is_expected.to(
|
|
contain_exactly(
|
|
domain_with_user_provided_certificate_and_auto_ssl,
|
|
domain_with_expired_gitlab_provided_certificate
|
|
)
|
|
)
|
|
end
|
|
end
|
|
end
|