gitlab-ce/spec/models/virtual_registries/packages/maven/cached_response_spec.rb

327 lines
10 KiB
Ruby

# frozen_string_literal: true
require 'spec_helper'
RSpec.describe VirtualRegistries::Packages::Maven::CachedResponse, type: :model, feature_category: :virtual_registry do
subject(:cached_response) { build(:virtual_registries_packages_maven_cached_response) }
it { is_expected.to include_module(FileStoreMounter) }
describe 'validations' do
%i[group file relative_path content_type downloads_count size].each do |attr|
it { is_expected.to validate_presence_of(attr) }
end
%i[relative_path upstream_etag content_type].each do |attr|
it { is_expected.to validate_length_of(attr).is_at_most(255) }
end
it { is_expected.to validate_numericality_of(:downloads_count).only_integer.is_greater_than(0) }
it { is_expected.to validate_length_of(:file_final_path).is_at_most(1024) }
context 'with persisted cached response' do
before do
cached_response.save!
end
it { is_expected.to validate_uniqueness_of(:relative_path).scoped_to(:upstream_id, :status) }
context 'when upstream_id is nil' do
let(:new_cached_response) { build(:virtual_registries_packages_maven_cached_response) }
before do
cached_response.update!(upstream_id: nil)
new_cached_response.upstream = nil
end
it 'does not validate uniqueness of relative_path' do
new_cached_response.validate
expect(new_cached_response.errors.messages_for(:relative_path)).not_to include 'has already been taken'
end
end
context 'with a similar cached response in a different status' do
let!(:cached_response_in_error) do
create(
:virtual_registries_packages_maven_cached_response,
:error,
group_id: cached_response.group_id,
upstream_id: cached_response.upstream_id,
relative_path: cached_response.relative_path
)
end
let(:new_cached_response) do
build(
:virtual_registries_packages_maven_cached_response,
:error,
group_id: cached_response.group_id,
upstream_id: cached_response.upstream_id,
relative_path: cached_response.relative_path
)
end
it 'does not validate uniqueness of relative_path' do
new_cached_response.validate
expect(new_cached_response.errors.messages_for(:relative_path)).not_to include 'has already been taken'
end
end
end
end
describe 'associations' do
it do
is_expected.to belong_to(:upstream)
.class_name('VirtualRegistries::Packages::Maven::Upstream')
.inverse_of(:cached_responses)
end
end
describe 'scopes' do
describe '.orphan' do
subject { described_class.orphan }
let_it_be(:cached_response) { create(:virtual_registries_packages_maven_cached_response) }
let_it_be(:orphan_cached_response) { create(:virtual_registries_packages_maven_cached_response, :orphan) }
it { is_expected.to contain_exactly(orphan_cached_response) }
end
describe '.pending_destruction' do
subject { described_class.pending_destruction }
let_it_be(:cached_response) { create(:virtual_registries_packages_maven_cached_response, :orphan, :processing) }
let_it_be(:pending_destruction_cached_response) do
create(:virtual_registries_packages_maven_cached_response, :orphan)
end
it { is_expected.to contain_exactly(pending_destruction_cached_response) }
end
end
describe '.next_pending_destruction' do
subject { described_class.next_pending_destruction }
let_it_be(:cached_response) { create(:virtual_registries_packages_maven_cached_response) }
let_it_be(:pending_destruction_cached_response) do
create(:virtual_registries_packages_maven_cached_response, :orphan)
end
it { is_expected.to eq(pending_destruction_cached_response) }
end
describe 'object storage key' do
it 'can not be null' do
cached_response.object_storage_key = nil
cached_response.relative_path = nil
expect(cached_response).to be_invalid
expect(cached_response.errors.full_messages).to include("Object storage key can't be blank")
end
it 'can not be too large' do
cached_response.object_storage_key = 'a' * 256
cached_response.relative_path = nil
expect(cached_response).to be_invalid
expect(cached_response.errors.full_messages)
.to include('Object storage key is too long (maximum is 255 characters)')
end
it 'is set before saving' do
expect { cached_response.save! }
.to change { cached_response.object_storage_key }.from(nil).to(an_instance_of(String))
end
context 'with a persisted cached response' do
let(:key) { cached_response.object_storage_key }
before do
cached_response.save!
end
it 'does not change after an update' do
expect(key).to be_present
cached_response.update!(
file: CarrierWaveStringFile.new('test'),
size: 2.kilobytes
)
expect(cached_response.object_storage_key).to eq(key)
end
it 'is read only' do
expect(key).to be_present
cached_response.object_storage_key = 'new-key'
cached_response.save!
expect(cached_response.reload.object_storage_key).to eq(key)
end
end
end
describe '.search_by_relative_path' do
let_it_be(:cached_response) { create(:virtual_registries_packages_maven_cached_response) }
let_it_be(:other_cached_response) do
create(:virtual_registries_packages_maven_cached_response, relative_path: 'other/path')
end
subject { described_class.search_by_relative_path(relative_path) }
context 'with a matching relative path' do
let(:relative_path) { cached_response.relative_path.slice(3, 8) }
it { is_expected.to contain_exactly(cached_response) }
end
end
describe '.create_or_update_by!' do
let_it_be(:upstream) { create(:virtual_registries_packages_maven_upstream) }
let(:size) { 10.bytes }
subject(:create_or_update) do
with_threads do
file = Tempfile.new('test.txt').tap { |f| f.write('test') }
described_class.create_or_update_by!(
upstream: upstream,
group_id: upstream.group_id,
relative_path: '/test',
updates: { file: file, size: size }
)
ensure
file.close
file.unlink
end
end
it 'creates or update the existing record' do
expect { create_or_update }.to change { described_class.count }.by(1)
# downloads count don't behave accurately in a race condition situation.
# That's an accepted tradeoff for now.
# https://gitlab.com/gitlab-org/gitlab/-/issues/473152 should fix this problem.
expect(described_class.last.downloads_count).to be_between(2, 5).inclusive
end
context 'with invalid updates' do
let(:size) { nil }
it 'bubbles up the error' do
expect { create_or_update }.to not_change { described_class.count }
.and raise_error(ActiveRecord::RecordInvalid)
end
end
end
describe '#filename' do
let(:cached_response) { build(:virtual_registries_packages_maven_cached_response) }
subject { cached_response.filename }
it { is_expected.to eq(File.basename(cached_response.relative_path)) }
context 'when relative_path is nil' do
before do
cached_response.relative_path = nil
end
it { is_expected.to be_nil }
end
end
describe '#stale?' do
let(:cached_response) do
build(:virtual_registries_packages_maven_cached_response, upstream_checked_at: 10.hours.ago)
end
let(:threshold) do
cached_response.upstream_checked_at + cached_response.upstream.registry.cache_validity_hours.hours
end
subject { cached_response.stale?(registry: cached_response.upstream.registry) }
context 'when before the threshold' do
before do
allow(Time.zone).to receive(:now).and_return(threshold - 1.hour)
end
it { is_expected.to eq(false) }
end
context 'when on the threshold' do
before do
allow(Time.zone).to receive(:now).and_return(threshold)
end
it { is_expected.to eq(false) }
end
context 'when after the threshold' do
before do
allow(Time.zone).to receive(:now).and_return(threshold + 1.hour)
end
it { is_expected.to eq(true) }
end
context 'with no registry' do
before do
cached_response.upstream.registry = nil
end
it { is_expected.to eq(true) }
end
end
describe '#bump_statistics', :freeze_time do
let_it_be_with_reload(:cached_response) { create(:virtual_registries_packages_maven_cached_response) }
subject(:bump) { cached_response.bump_statistics }
it 'updates the correct statistics' do
expect { bump }
.to change { cached_response.downloaded_at }.to(Time.zone.now)
.and change { cached_response.downloads_count }.by(1)
end
context 'with include_upstream_checked_at' do
subject(:bump) { cached_response.bump_statistics(include_upstream_checked_at: true) }
it 'updates the correct statistics' do
expect { bump }
.to change { cached_response.reload.downloaded_at }.to(Time.zone.now)
.and change { cached_response.upstream_checked_at }.to(Time.zone.now)
.and change { cached_response.downloads_count }.by(1)
end
end
end
context 'with loose foreign key on virtual_registries_packages_maven_cached_responses.upstream_id' do
it_behaves_like 'cleanup by a loose foreign key' do
let_it_be(:parent) { create(:virtual_registries_packages_maven_upstream) }
let_it_be(:model) { create(:virtual_registries_packages_maven_cached_response, upstream: parent) }
end
end
def with_threads(count: 5, &block)
return unless block
# create a race condition - structure from https://blog.arkency.com/2015/09/testing-race-conditions/
wait_for_it = true
threads = Array.new(count) do |i|
Thread.new do
# A loop to make threads busy until we `join` them
true while wait_for_it
yield(i)
end
end
wait_for_it = false
threads.each(&:join)
end
end