summaryrefslogtreecommitdiff
path: root/spec/models/concerns
diff options
context:
space:
mode:
Diffstat (limited to 'spec/models/concerns')
-rw-r--r--spec/models/concerns/batch_destroy_dependent_associations_spec.rb31
-rw-r--r--spec/models/concerns/blocks_json_serialization_spec.rb22
-rw-r--r--spec/models/concerns/blocks_unsafe_serialization_spec.rb17
-rw-r--r--spec/models/concerns/ci/has_deployment_name_spec.rb34
-rw-r--r--spec/models/concerns/deployment_platform_spec.rb12
-rw-r--r--spec/models/concerns/issuable_link_spec.rb43
-rw-r--r--spec/models/concerns/issuable_spec.rb43
-rw-r--r--spec/models/concerns/mentionable_spec.rb1
-rw-r--r--spec/models/concerns/pg_full_text_searchable_spec.rb177
-rw-r--r--spec/models/concerns/runners_token_prefixable_spec.rb15
-rw-r--r--spec/models/concerns/sensitive_serializable_hash_spec.rb150
-rw-r--r--spec/models/concerns/spammable_spec.rb4
-rw-r--r--spec/models/concerns/token_authenticatable_spec.rb6
-rw-r--r--spec/models/concerns/token_authenticatable_strategies/base_spec.rb18
-rw-r--r--spec/models/concerns/token_authenticatable_strategies/digest_spec.rb18
-rw-r--r--spec/models/concerns/token_authenticatable_strategies/encrypted_spec.rb10
16 files changed, 546 insertions, 55 deletions
diff --git a/spec/models/concerns/batch_destroy_dependent_associations_spec.rb b/spec/models/concerns/batch_destroy_dependent_associations_spec.rb
index 993afd47a57..358000ee174 100644
--- a/spec/models/concerns/batch_destroy_dependent_associations_spec.rb
+++ b/spec/models/concerns/batch_destroy_dependent_associations_spec.rb
@@ -6,10 +6,10 @@ RSpec.describe BatchDestroyDependentAssociations do
class TestProject < ActiveRecord::Base
self.table_name = 'projects'
- has_many :builds, dependent: :destroy
+ has_many :builds
has_many :notification_settings, as: :source, dependent: :delete_all
has_many :pages_domains
- has_many :todos
+ has_many :todos, dependent: :destroy
include BatchDestroyDependentAssociations
end
@@ -18,7 +18,7 @@ RSpec.describe BatchDestroyDependentAssociations do
let_it_be(:project) { TestProject.new }
it 'returns the right associations' do
- expect(project.dependent_associations_to_destroy.map(&:name)).to match_array([:builds])
+ expect(project.dependent_associations_to_destroy.map(&:name)).to match_array([:todos])
end
end
@@ -26,36 +26,35 @@ RSpec.describe BatchDestroyDependentAssociations do
let_it_be(:project) { create(:project) }
let_it_be(:build) { create(:ci_build, project: project) }
let_it_be(:notification_setting) { create(:notification_setting, project: project) }
+ let_it_be(:note) { create(:note, project: project) }
- let!(:todos) { create(:todo, project: project) }
+ it 'destroys multiple notes' do
+ create(:note, project: project)
- it 'destroys multiple builds' do
- create(:ci_build, project: project)
-
- expect(Ci::Build.count).to eq(2)
+ expect(Note.count).to eq(2)
project.destroy_dependent_associations_in_batches
- expect(Ci::Build.count).to eq(0)
+ expect(Note.count).to eq(0)
end
- it 'destroys builds in batches' do
- expect(project).to receive_message_chain(:builds, :find_each).and_yield(build)
- expect(build).to receive(:destroy).and_call_original
+ it 'destroys note in batches' do
+ expect(project).to receive_message_chain(:notes, :find_each).and_yield(note)
+ expect(note).to receive(:destroy).and_call_original
project.destroy_dependent_associations_in_batches
- expect(Ci::Build.count).to eq(0)
- expect(Todo.count).to eq(1)
+ expect(Ci::Build.count).to eq(1)
+ expect(Note.count).to eq(0)
expect(User.count).to be > 0
expect(NotificationSetting.count).to eq(User.count)
end
it 'excludes associations' do
- project.destroy_dependent_associations_in_batches(exclude: [:builds])
+ project.destroy_dependent_associations_in_batches(exclude: [:notes])
+ expect(Note.count).to eq(1)
expect(Ci::Build.count).to eq(1)
- expect(Todo.count).to eq(1)
expect(User.count).to be > 0
expect(NotificationSetting.count).to eq(User.count)
end
diff --git a/spec/models/concerns/blocks_json_serialization_spec.rb b/spec/models/concerns/blocks_json_serialization_spec.rb
deleted file mode 100644
index d811b47fa35..00000000000
--- a/spec/models/concerns/blocks_json_serialization_spec.rb
+++ /dev/null
@@ -1,22 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe BlocksJsonSerialization do
- before do
- stub_const('DummyModel', Class.new)
- DummyModel.class_eval do
- include BlocksJsonSerialization
- end
- end
-
- it 'blocks as_json' do
- expect { DummyModel.new.as_json }
- .to raise_error(described_class::JsonSerializationError, /DummyModel/)
- end
-
- it 'blocks to_json' do
- expect { DummyModel.new.to_json }
- .to raise_error(described_class::JsonSerializationError, /DummyModel/)
- end
-end
diff --git a/spec/models/concerns/blocks_unsafe_serialization_spec.rb b/spec/models/concerns/blocks_unsafe_serialization_spec.rb
new file mode 100644
index 00000000000..5c8f5035a58
--- /dev/null
+++ b/spec/models/concerns/blocks_unsafe_serialization_spec.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe BlocksUnsafeSerialization do
+ before do
+ stub_const('DummyModel', Class.new)
+ DummyModel.class_eval do
+ include ActiveModel::Serializers::JSON
+ include BlocksUnsafeSerialization
+ end
+ end
+
+ it_behaves_like 'blocks unsafe serialization' do
+ let(:object) { DummyModel.new }
+ end
+end
diff --git a/spec/models/concerns/ci/has_deployment_name_spec.rb b/spec/models/concerns/ci/has_deployment_name_spec.rb
new file mode 100644
index 00000000000..8c7338638b1
--- /dev/null
+++ b/spec/models/concerns/ci/has_deployment_name_spec.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ci::HasDeploymentName do
+ describe 'deployment_name?' do
+ let(:build) { create(:ci_build) }
+
+ subject { build.branch? }
+
+ it 'does detect deployment names' do
+ build.name = 'deployment'
+
+ expect(build.deployment_name?).to be_truthy
+ end
+
+ it 'does detect partial deployment names' do
+ build.name = 'do a really cool deploy'
+
+ expect(build.deployment_name?).to be_truthy
+ end
+
+ it 'does not detect non-deployment names' do
+ build.name = 'testing'
+
+ expect(build.deployment_name?).to be_falsy
+ end
+
+ it 'is case insensitive' do
+ build.name = 'DEPLOY'
+ expect(build.deployment_name?).to be_truthy
+ end
+ end
+end
diff --git a/spec/models/concerns/deployment_platform_spec.rb b/spec/models/concerns/deployment_platform_spec.rb
index 7fa55184cf1..bd1afe844ac 100644
--- a/spec/models/concerns/deployment_platform_spec.rb
+++ b/spec/models/concerns/deployment_platform_spec.rb
@@ -12,16 +12,28 @@ RSpec.describe DeploymentPlatform do
let(:group) { create(:group) }
let(:project) { create(:project, group: group) }
+ shared_examples 'certificate_based_clusters is disabled' do
+ before do
+ stub_feature_flags(certificate_based_clusters: false)
+ end
+
+ it { is_expected.to be_nil }
+ end
+
shared_examples 'matching environment scope' do
it 'returns environment specific cluster' do
is_expected.to eq(cluster.platform_kubernetes)
end
+
+ it_behaves_like 'certificate_based_clusters is disabled'
end
shared_examples 'not matching environment scope' do
it 'returns default cluster' do
is_expected.to eq(default_cluster.platform_kubernetes)
end
+
+ it_behaves_like 'certificate_based_clusters is disabled'
end
context 'multiple clusters use the same management project' do
diff --git a/spec/models/concerns/issuable_link_spec.rb b/spec/models/concerns/issuable_link_spec.rb
new file mode 100644
index 00000000000..7be6d8a074d
--- /dev/null
+++ b/spec/models/concerns/issuable_link_spec.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe IssuableLink do
+ let(:test_class) do
+ Class.new(ApplicationRecord) do
+ include IssuableLink
+
+ self.table_name = 'issue_links'
+
+ belongs_to :source, class_name: 'Issue'
+ belongs_to :target, class_name: 'Issue'
+
+ def self.name
+ 'TestClass'
+ end
+ end
+ end
+
+ describe '.inverse_link_type' do
+ it 'returns the inverse type of link' do
+ expect(test_class.inverse_link_type('relates_to')).to eq('relates_to')
+ end
+ end
+
+ describe '.issuable_type' do
+ let_it_be(:source_issue) { create(:issue) }
+ let_it_be(:target_issue) { create(:issue) }
+
+ before do
+ test_class.create!(source: source_issue, target: target_issue)
+ end
+
+ context 'when opposite relation already exists' do
+ it 'raises NotImplementedError when performing validations' do
+ instance = test_class.new(source: target_issue, target: source_issue)
+
+ expect { instance.save! }.to raise_error(NotImplementedError)
+ end
+ end
+ end
+end
diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb
index 832d5b44e5d..e3c0e3a7a2b 100644
--- a/spec/models/concerns/issuable_spec.rb
+++ b/spec/models/concerns/issuable_spec.rb
@@ -18,7 +18,6 @@ RSpec.describe Issuable do
it { is_expected.to have_many(:notes).dependent(:destroy) }
it { is_expected.to have_many(:todos) }
it { is_expected.to have_many(:labels) }
- it { is_expected.to have_many(:note_authors).through(:notes) }
context 'Notes' do
let!(:note) { create(:note, noteable: issue, project: issue.project) }
@@ -28,6 +27,23 @@ RSpec.describe Issuable do
expect(issue.notes).not_to be_authors_loaded
expect(scoped_issue.notes).to be_authors_loaded
end
+
+ describe 'note_authors' do
+ it { is_expected.to have_many(:note_authors).through(:notes) }
+ end
+
+ describe 'user_note_authors' do
+ let_it_be(:system_user) { create(:user) }
+
+ let!(:system_note) { create(:system_note, author: system_user, noteable: issue, project: issue.project) }
+
+ it 'filters the authors to those of user notes' do
+ authors = issue.user_note_authors
+
+ expect(authors).to include(note.author)
+ expect(authors).not_to include(system_user)
+ end
+ end
end
end
@@ -572,6 +588,27 @@ RSpec.describe Issuable do
issue.to_hook_data(user, old_associations: { severity: 'unknown' })
end
end
+
+ context 'escalation status is updated' do
+ let(:issue) { create(:incident, :with_escalation_status) }
+ let(:acknowledged) { IncidentManagement::IssuableEscalationStatus::STATUSES[:acknowledged] }
+
+ before do
+ issue.escalation_status.update!(status: acknowledged)
+
+ expect(Gitlab::HookData::IssuableBuilder).to receive(:new).with(issue).and_return(builder)
+ end
+
+ it 'delegates to Gitlab::HookData::IssuableBuilder#build' do
+ expect(builder).to receive(:build).with(
+ user: user,
+ changes: hash_including(
+ 'escalation_status' => %i(triggered acknowledged)
+ ))
+
+ issue.to_hook_data(user, old_associations: { escalation_status: :triggered })
+ end
+ end
end
describe '#labels_array' do
@@ -761,7 +798,7 @@ RSpec.describe Issuable do
it 'updates issues updated_at' do
issue
- Timecop.travel(1.minute.from_now) do
+ travel_to(2.minutes.from_now) do
expect { spend_time(1800) }.to change { issue.updated_at }
end
end
@@ -786,7 +823,7 @@ RSpec.describe Issuable do
context 'when time to subtract exceeds the total time spent' do
it 'raise a validation error' do
- Timecop.travel(1.minute.from_now) do
+ travel_to(1.minute.from_now) do
expect do
expect do
spend_time(-3600)
diff --git a/spec/models/concerns/mentionable_spec.rb b/spec/models/concerns/mentionable_spec.rb
index 3c095477ea9..9daea3438cb 100644
--- a/spec/models/concerns/mentionable_spec.rb
+++ b/spec/models/concerns/mentionable_spec.rb
@@ -9,6 +9,7 @@ RSpec.describe Mentionable do
include Mentionable
attr_accessor :project, :message
+
attr_mentionable :message
def author
diff --git a/spec/models/concerns/pg_full_text_searchable_spec.rb b/spec/models/concerns/pg_full_text_searchable_spec.rb
new file mode 100644
index 00000000000..db7f652f494
--- /dev/null
+++ b/spec/models/concerns/pg_full_text_searchable_spec.rb
@@ -0,0 +1,177 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe PgFullTextSearchable do
+ let(:project) { create(:project) }
+
+ let(:model_class) do
+ Class.new(ActiveRecord::Base) do
+ include PgFullTextSearchable
+
+ self.table_name = 'issues'
+
+ belongs_to :project
+ has_one :search_data, class_name: 'Issues::SearchData'
+
+ def persist_pg_full_text_search_vector(search_vector)
+ Issues::SearchData.upsert({ project_id: project_id, issue_id: id, search_vector: search_vector }, unique_by: %i(project_id issue_id))
+ end
+
+ def self.name
+ 'Issue'
+ end
+ end
+ end
+
+ describe '.pg_full_text_searchable' do
+ it 'sets pg_full_text_searchable_columns' do
+ model_class.pg_full_text_searchable columns: [{ name: 'title', weight: 'A' }]
+
+ expect(model_class.pg_full_text_searchable_columns).to eq({ 'title' => 'A' })
+ end
+
+ it 'raises an error when called twice' do
+ model_class.pg_full_text_searchable columns: [{ name: 'title', weight: 'A' }]
+
+ expect { model_class.pg_full_text_searchable columns: [{ name: 'title', weight: 'A' }] }.to raise_error('Full text search columns already defined!')
+ end
+ end
+
+ describe 'after commit hook' do
+ let(:model) { model_class.create!(project: project) }
+
+ before do
+ model_class.pg_full_text_searchable columns: [{ name: 'title', weight: 'A' }]
+ end
+
+ context 'when specified columns are changed' do
+ it 'calls update_search_data!' do
+ expect(model).to receive(:update_search_data!)
+
+ model.update!(title: 'A new title')
+ end
+ end
+
+ context 'when specified columns are not changed' do
+ it 'does not enqueue worker' do
+ expect(model).not_to receive(:update_search_data!)
+
+ model.update!(description: 'A new description')
+ end
+ end
+ end
+
+ describe '.pg_full_text_search' do
+ let(:english) { model_class.create!(project: project, title: 'title', description: 'something english') }
+ let(:with_accent) { model_class.create!(project: project, title: 'Jürgen', description: 'Ærøskøbing') }
+ let(:japanese) { model_class.create!(project: project, title: '日本語 title', description: 'another english description') }
+
+ before do
+ model_class.pg_full_text_searchable columns: [{ name: 'title', weight: 'A' }, { name: 'description', weight: 'B' }]
+
+ [english, with_accent, japanese].each(&:update_search_data!)
+ end
+
+ it 'searches across all fields' do
+ expect(model_class.pg_full_text_search('title english')).to contain_exactly(english, japanese)
+ end
+
+ it 'searches for exact term with quotes' do
+ expect(model_class.pg_full_text_search('"something english"')).to contain_exactly(english)
+ end
+
+ it 'ignores accents' do
+ expect(model_class.pg_full_text_search('jurgen')).to contain_exactly(with_accent)
+ end
+
+ it 'does not support searching by non-Latin characters' do
+ expect(model_class.pg_full_text_search('日本')).to be_empty
+ end
+ end
+
+ describe '#update_search_data!' do
+ let(:model) { model_class.create!(project: project, title: 'title', description: 'description') }
+
+ before do
+ model_class.pg_full_text_searchable columns: [{ name: 'title', weight: 'A' }, { name: 'description', weight: 'B' }]
+ end
+
+ it 'sets the correct weights' do
+ model.update_search_data!
+
+ expect(model.search_data.search_vector).to match(/'titl':1A/)
+ expect(model.search_data.search_vector).to match(/'descript':2B/)
+ end
+
+ context 'with accented and non-Latin characters' do
+ let(:model) { model_class.create!(project: project, title: '日本語', description: 'Jürgen') }
+
+ it 'transliterates accented characters and removes non-Latin ones' do
+ model.update_search_data!
+
+ expect(model.search_data.search_vector).not_to match(/日本語/)
+ expect(model.search_data.search_vector).to match(/jurgen/)
+ end
+ end
+
+ context 'with long words' do
+ let(:model) { model_class.create!(project: project, title: 'title ' + 'long/sequence+1' * 4, description: 'description ' + '@user1' * 20) }
+
+ it 'strips words that are 50 characters or longer' do
+ model.update_search_data!
+
+ expect(model.search_data.search_vector).to match(/'titl':1A/)
+ expect(model.search_data.search_vector).not_to match(/long/)
+ expect(model.search_data.search_vector).not_to match(/sequence/)
+
+ expect(model.search_data.search_vector).to match(/'descript':2B/)
+ expect(model.search_data.search_vector).not_to match(/@user1/)
+ end
+ end
+
+ context 'when upsert times out' do
+ it 're-raises the exception' do
+ expect(Issues::SearchData).to receive(:upsert).once.and_raise(ActiveRecord::StatementTimeout)
+
+ expect { model.update_search_data! }.to raise_error(ActiveRecord::StatementTimeout)
+ end
+ end
+
+ context 'with strings that go over tsvector limit', :delete do
+ let(:long_string) { Array.new(30_000) { SecureRandom.hex }.join(' ') }
+ let(:model) { model_class.create!(project: project, title: 'title', description: long_string) }
+
+ it 'does not raise an exception' do
+ expect(Gitlab::AppJsonLogger).to receive(:error).with(
+ a_hash_including(class: model_class.name, model_id: model.id)
+ )
+
+ expect { model.update_search_data! }.not_to raise_error
+
+ expect(model.search_data).to eq(nil)
+ end
+ end
+
+ context 'when model class does not implement persist_pg_full_text_search_vector' do
+ let(:model_class) do
+ Class.new(ActiveRecord::Base) do
+ include PgFullTextSearchable
+
+ self.table_name = 'issues'
+
+ belongs_to :project
+ has_one :search_data, class_name: 'Issues::SearchData'
+
+ def self.name
+ 'Issue'
+ end
+ end
+ end
+
+ it 'raises an error' do
+ expect { model.update_search_data! }.to raise_error(NotImplementedError)
+ end
+ end
+ end
+end
diff --git a/spec/models/concerns/runners_token_prefixable_spec.rb b/spec/models/concerns/runners_token_prefixable_spec.rb
index 6127203987f..29e7b8cf4f4 100644
--- a/spec/models/concerns/runners_token_prefixable_spec.rb
+++ b/spec/models/concerns/runners_token_prefixable_spec.rb
@@ -3,18 +3,11 @@
require 'spec_helper'
RSpec.describe RunnersTokenPrefixable do
- before do
- stub_const('DummyModel', Class.new)
- DummyModel.class_eval do
- include RunnersTokenPrefixable
- end
- end
-
- describe '.runners_token_prefix' do
- subject { DummyModel.new }
+ describe 'runners token prefix' do
+ subject { described_class::RUNNERS_TOKEN_PREFIX }
- it 'returns RUNNERS_TOKEN_PREFIX' do
- expect(subject.runners_token_prefix).to eq(RunnersTokenPrefixable::RUNNERS_TOKEN_PREFIX)
+ it 'has the correct value' do
+ expect(subject).to eq('GR1348941')
end
end
end
diff --git a/spec/models/concerns/sensitive_serializable_hash_spec.rb b/spec/models/concerns/sensitive_serializable_hash_spec.rb
new file mode 100644
index 00000000000..923f9e80c1f
--- /dev/null
+++ b/spec/models/concerns/sensitive_serializable_hash_spec.rb
@@ -0,0 +1,150 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe SensitiveSerializableHash do
+ describe '.prevent_from_serialization' do
+ let(:test_class) do
+ Class.new do
+ include ActiveModel::Serialization
+ include SensitiveSerializableHash
+
+ attr_accessor :name, :super_secret
+
+ prevent_from_serialization :super_secret
+
+ def attributes
+ { 'name' => nil, 'super_secret' => nil }
+ end
+ end
+ end
+
+ let(:model) { test_class.new }
+
+ it 'does not include the field in serializable_hash' do
+ expect(model.serializable_hash).not_to include('super_secret')
+ end
+
+ context 'unsafe_serialization_hash option' do
+ it 'includes the field in serializable_hash' do
+ expect(model.serializable_hash(unsafe_serialization_hash: true)).to include('super_secret')
+ end
+ end
+
+ context 'when prevent_sensitive_fields_from_serializable_hash feature flag is disabled' do
+ before do
+ stub_feature_flags(prevent_sensitive_fields_from_serializable_hash: false)
+ end
+
+ it 'includes the field in serializable_hash' do
+ expect(model.serializable_hash).to include('super_secret')
+ end
+ end
+ end
+
+ describe '#serializable_hash' do
+ shared_examples "attr_encrypted attribute" do |klass, attribute_name|
+ context "#{klass.name}\##{attribute_name}" do
+ let(:attributes) { [attribute_name, "encrypted_#{attribute_name}", "encrypted_#{attribute_name}_iv"] }
+
+ it 'has a encrypted_attributes field' do
+ expect(klass.encrypted_attributes).to include(attribute_name.to_sym)
+ end
+
+ it 'does not include the attribute in serializable_hash', :aggregate_failures do
+ attributes.each do |attribute|
+ expect(model.attributes).to include(attribute) # double-check the attribute does exist
+
+ expect(model.serializable_hash).not_to include(attribute)
+ expect(model.to_json).not_to include(attribute)
+ expect(model.as_json).not_to include(attribute)
+ end
+ end
+
+ context 'unsafe_serialization_hash option' do
+ it 'includes the field in serializable_hash' do
+ attributes.each do |attribute|
+ expect(model.attributes).to include(attribute) # double-check the attribute does exist
+
+ expect(model.serializable_hash(unsafe_serialization_hash: true)).to include(attribute)
+ expect(model.to_json(unsafe_serialization_hash: true)).to include(attribute)
+ expect(model.as_json(unsafe_serialization_hash: true)).to include(attribute)
+ end
+ end
+ end
+ end
+ end
+
+ it_behaves_like 'attr_encrypted attribute', WebHook, 'token' do
+ let_it_be(:model) { create(:system_hook) }
+ end
+
+ it_behaves_like 'attr_encrypted attribute', Ci::InstanceVariable, 'value' do
+ let_it_be(:model) { create(:ci_instance_variable) }
+ end
+
+ shared_examples "add_authentication_token_field attribute" do |klass, attribute_name, encrypted_attribute: true, digest_attribute: false|
+ context "#{klass.name}\##{attribute_name}" do
+ let(:attributes) do
+ if digest_attribute
+ ["#{attribute_name}_digest"]
+ elsif encrypted_attribute
+ [attribute_name, "#{attribute_name}_encrypted"]
+ else
+ [attribute_name]
+ end
+ end
+
+ it 'has a add_authentication_token_field field' do
+ expect(klass.token_authenticatable_fields).to include(attribute_name.to_sym)
+ end
+
+ it 'does not include the attribute in serializable_hash', :aggregate_failures do
+ attributes.each do |attribute|
+ expect(model.attributes).to include(attribute) # double-check the attribute does exist
+
+ expect(model.serializable_hash).not_to include(attribute)
+ expect(model.to_json).not_to include(attribute)
+ expect(model.as_json).not_to include(attribute)
+ end
+ end
+
+ context 'unsafe_serialization_hash option' do
+ it 'includes the field in serializable_hash' do
+ attributes.each do |attribute|
+ expect(model.attributes).to include(attribute) # double-check the attribute does exist
+
+ expect(model.serializable_hash(unsafe_serialization_hash: true)).to include(attribute)
+ expect(model.to_json(unsafe_serialization_hash: true)).to include(attribute)
+ expect(model.as_json(unsafe_serialization_hash: true)).to include(attribute)
+ end
+ end
+ end
+ end
+ end
+
+ it_behaves_like 'add_authentication_token_field attribute', Ci::Runner, 'token' do
+ let_it_be(:model) { create(:ci_runner) }
+
+ it 'does not include token_expires_at in serializable_hash' do
+ attribute = 'token_expires_at'
+
+ expect(model.attributes).to include(attribute) # double-check the attribute does exist
+
+ expect(model.serializable_hash).not_to include(attribute)
+ expect(model.to_json).not_to include(attribute)
+ expect(model.as_json).not_to include(attribute)
+ end
+ end
+
+ it_behaves_like 'add_authentication_token_field attribute', ApplicationSetting, 'health_check_access_token', encrypted_attribute: false do
+ # health_check_access_token_encrypted column does not exist
+ let_it_be(:model) { create(:application_setting) }
+ end
+
+ it_behaves_like 'add_authentication_token_field attribute', PersonalAccessToken, 'token', encrypted_attribute: false, digest_attribute: true do
+ # PersonalAccessToken only has token_digest column
+ let_it_be(:model) { create(:personal_access_token) }
+ end
+ end
+end
diff --git a/spec/models/concerns/spammable_spec.rb b/spec/models/concerns/spammable_spec.rb
index 5edaab56e2d..baa2d75705a 100644
--- a/spec/models/concerns/spammable_spec.rb
+++ b/spec/models/concerns/spammable_spec.rb
@@ -55,7 +55,7 @@ RSpec.describe Spammable do
subject { invalidate_if_spam(needs_recaptcha: true) }
it 'has an error related to spam on the model' do
- expect(subject.errors.messages[:base]).to match_array /solve the reCAPTCHA/
+ expect(subject.errors.messages[:base]).to match_array /content or solve the/
end
end
@@ -63,7 +63,7 @@ RSpec.describe Spammable do
subject { invalidate_if_spam(is_spam: true, needs_recaptcha: true) }
it 'has an error related to spam on the model' do
- expect(subject.errors.messages[:base]).to match_array /solve the reCAPTCHA/
+ expect(subject.errors.messages[:base]).to match_array /content or solve the/
end
end
diff --git a/spec/models/concerns/token_authenticatable_spec.rb b/spec/models/concerns/token_authenticatable_spec.rb
index 4534fd3664e..d7bfcc3f579 100644
--- a/spec/models/concerns/token_authenticatable_spec.rb
+++ b/spec/models/concerns/token_authenticatable_spec.rb
@@ -9,6 +9,12 @@ RSpec.shared_examples 'TokenAuthenticatable' do
it { is_expected.to respond_to("set_#{token_field}") }
it { is_expected.to respond_to("reset_#{token_field}!") }
end
+
+ describe 'SensitiveSerializableHash' do
+ it 'includes the token field in list of sensitive attributes prevented from serialization' do
+ expect(described_class.attributes_exempt_from_serializable_hash).to include(token_field)
+ end
+ end
end
RSpec.describe User, 'TokenAuthenticatable' do
diff --git a/spec/models/concerns/token_authenticatable_strategies/base_spec.rb b/spec/models/concerns/token_authenticatable_strategies/base_spec.rb
index bccef9b9554..89ddc797a9d 100644
--- a/spec/models/concerns/token_authenticatable_strategies/base_spec.rb
+++ b/spec/models/concerns/token_authenticatable_strategies/base_spec.rb
@@ -6,6 +6,24 @@ RSpec.describe TokenAuthenticatableStrategies::Base do
let(:instance) { double(:instance) }
let(:field) { double(:field) }
+ describe '#token_fields' do
+ let(:strategy) { described_class.new(instance, field, options) }
+ let(:field) { 'some_token' }
+ let(:options) { {} }
+
+ it 'includes the token field' do
+ expect(strategy.token_fields).to contain_exactly(field)
+ end
+
+ context 'with expires_at option' do
+ let(:options) { { expires_at: true } }
+
+ it 'includes the token_expires_at field' do
+ expect(strategy.token_fields).to contain_exactly(field, 'some_token_expires_at')
+ end
+ end
+ end
+
describe '.fabricate' do
context 'when digest stragegy is specified' do
it 'fabricates digest strategy object' do
diff --git a/spec/models/concerns/token_authenticatable_strategies/digest_spec.rb b/spec/models/concerns/token_authenticatable_strategies/digest_spec.rb
new file mode 100644
index 00000000000..bcd6e1e7316
--- /dev/null
+++ b/spec/models/concerns/token_authenticatable_strategies/digest_spec.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe TokenAuthenticatableStrategies::Digest do
+ let(:model) { class_double('Project') }
+ let(:options) { { digest: true } }
+
+ subject(:strategy) do
+ described_class.new(model, 'some_field', options)
+ end
+
+ describe '#token_fields' do
+ it 'includes the digest field' do
+ expect(strategy.token_fields).to contain_exactly('some_field', 'some_field_digest')
+ end
+ end
+end
diff --git a/spec/models/concerns/token_authenticatable_strategies/encrypted_spec.rb b/spec/models/concerns/token_authenticatable_strategies/encrypted_spec.rb
index 458dfb47394..e0ebb86585a 100644
--- a/spec/models/concerns/token_authenticatable_strategies/encrypted_spec.rb
+++ b/spec/models/concerns/token_authenticatable_strategies/encrypted_spec.rb
@@ -14,10 +14,18 @@ RSpec.describe TokenAuthenticatableStrategies::Encrypted do
Gitlab::CryptoHelper.aes256_gcm_encrypt('my-value')
end
- subject do
+ subject(:strategy) do
described_class.new(model, 'some_field', options)
end
+ describe '#token_fields' do
+ let(:options) { { encrypted: :required } }
+
+ it 'includes the encrypted field' do
+ expect(strategy.token_fields).to contain_exactly('some_field', 'some_field_encrypted')
+ end
+ end
+
describe '#find_token_authenticatable' do
context 'when encryption is required' do
let(:options) { { encrypted: :required } }