Commit 01443314 authored by Eugenia Grieff's avatar Eugenia Grieff

Extract common model code to a concern

Extract commond specs code to shared examples
parent f2a49bb2
# frozen_string_literal: true
# == IssuableLink concern
#
# Contains common functionality shared between related Issues and related Epics
#
# Used by IssueLink, Epic::RelatedEpicLink
#
module IssuableLink
extend ActiveSupport::Concern
TYPE_RELATES_TO = 'relates_to'
TYPE_BLOCKS = 'blocks'
# we don't store is_blocked_by in the db but need it for displaying the relation
# from the target
TYPE_IS_BLOCKED_BY = 'is_blocked_by'
class_methods do
def inverse_link_type(type)
type
end
end
included do
validates :source, presence: true
validates :target, presence: true
validates :source, uniqueness: { scope: :target_id, message: 'is already related' }
validate :check_self_relation
validate :check_opposite_relation
enum link_type: { TYPE_RELATES_TO => 0, TYPE_BLOCKS => 1 }
private
def check_self_relation
return unless source && target
if source == target
errors.add(:source, 'cannot be related to itself')
end
end
def check_opposite_relation
return unless source && target
if self.class.base_class.find_by(source: target, target: source)
errors.add(:source, "is already related to this #{issuable_type}")
end
end
def issuable_type
raise NotImplementedError
end
end
end
......@@ -2,47 +2,18 @@
class IssueLink < ApplicationRecord
include FromUnion
include IssuableLink
belongs_to :source, class_name: 'Issue'
belongs_to :target, class_name: 'Issue'
validates :source, presence: true
validates :target, presence: true
validates :source, uniqueness: { scope: :target_id, message: 'is already related' }
validate :check_self_relation
validate :check_opposite_relation
scope :for_source_issue, ->(issue) { where(source_id: issue.id) }
scope :for_target_issue, ->(issue) { where(target_id: issue.id) }
TYPE_RELATES_TO = 'relates_to'
TYPE_BLOCKS = 'blocks'
# we don't store is_blocked_by in the db but need it for displaying the relation
# from the target (used in IssueLink.inverse_link_type)
TYPE_IS_BLOCKED_BY = 'is_blocked_by'
enum link_type: { TYPE_RELATES_TO => 0, TYPE_BLOCKS => 1 }
def self.inverse_link_type(type)
type
end
private
def check_self_relation
return unless source && target
if source == target
errors.add(:source, 'cannot be related to itself')
end
end
def check_opposite_relation
return unless source && target
if IssueLink.find_by(source: target, target: source)
errors.add(:source, 'is already related to this issue')
end
def issuable_type
:issue
end
end
......
# frozen_string_literal: true
class Epic::RelatedEpicLink < ApplicationRecord
self.table_name = 'related_epic_links'
include IssuableLink
belongs_to :source, class_name: 'Epic'
belongs_to :target, class_name: 'Epic'
validates :source, presence: true
validates :target, presence: true
validates :source, uniqueness: { scope: :target_id, message: 'is already related' }
validate :check_self_relation
validate :check_opposite_relation
self.table_name = 'related_epic_links'
scope :for_source_epic, ->(epic) { where(source_id: epic.id) }
scope :for_target_epic, ->(epic) { where(target_id: epic.id) }
TYPE_RELATES_TO = 'relates_to'
TYPE_BLOCKS = 'blocks'
TYPE_IS_BLOCKED_BY = 'is_blocked_by'
enum link_type: { TYPE_RELATES_TO => 0, TYPE_BLOCKS => 1 }
private
def check_self_relation
return unless source && target
if source == target
errors.add(:source, 'cannot be related to itself')
end
end
def check_opposite_relation
return unless source && target
if Epic::RelatedEpicLink.find_by(source: target, target: source)
errors.add(:source, 'is already related to this epic')
end
def issuable_type
:epic
end
end
......@@ -3,59 +3,11 @@
require 'spec_helper'
RSpec.describe Epic::RelatedEpicLink do
describe 'Associations' do
it { is_expected.to belong_to(:source).class_name('Epic') }
it { is_expected.to belong_to(:target).class_name('Epic') }
end
describe 'link_type' do
it { is_expected.to define_enum_for(:link_type).with_values(relates_to: 0, blocks: 1) }
it 'provides the "related" as default link_type' do
expect(create(:related_epic_link).link_type).to eq 'relates_to'
end
end
describe 'Validation' do
subject { create :related_epic_link }
it { is_expected.to validate_presence_of(:source) }
it { is_expected.to validate_presence_of(:target) }
it do
is_expected.to validate_uniqueness_of(:source)
.scoped_to(:target_id)
.with_message(/already related/)
end
it 'is not valid if an opposite link already exists' do
related_epic_link = build(:related_epic_link, source: subject.target, target: subject.source)
expect(related_epic_link).to be_invalid
expect(related_epic_link.errors[:source]).to include('is already related to this epic')
end
context 'when it relates to itself' do
let(:epic) { create :epic }
context 'cannot be validated' do
it 'does not invalidate object with self relation error' do
related_epic_link = build(:related_epic_link, source: epic, target: nil)
related_epic_link.valid?
expect(related_epic_link.errors[:source]).to be_empty
end
end
context 'can be invalidated' do
it 'invalidates object' do
related_epic_link = build(:related_epic_link, source: epic, target: epic)
expect(related_epic_link).to be_invalid
expect(related_epic_link.errors[:source]).to include('cannot be related to itself')
end
end
end
it_behaves_like 'issuable link' do
let_it_be_with_reload(:issuable_link) { create(:related_epic_link) }
let_it_be(:issuable) { create(:epic) }
let(:issuable_class) { 'Epic' }
let(:issuable_link_factory) { :related_epic_link }
end
describe 'Scopes' do
......
# 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')
expect(test_class.inverse_link_type('is_blocked_by')).to eq('is_blocked_by')
expect(test_class.inverse_link_type('blocks')).to eq('blocks')
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
......@@ -3,58 +3,10 @@
require 'spec_helper'
RSpec.describe IssueLink do
describe 'Associations' do
it { is_expected.to belong_to(:source).class_name('Issue') }
it { is_expected.to belong_to(:target).class_name('Issue') }
end
describe 'link_type' do
it { is_expected.to define_enum_for(:link_type).with_values(relates_to: 0, blocks: 1) }
it 'provides the "related" as default link_type' do
expect(create(:issue_link).link_type).to eq 'relates_to'
end
end
describe 'Validation' do
subject { create :issue_link }
it { is_expected.to validate_presence_of(:source) }
it { is_expected.to validate_presence_of(:target) }
it do
is_expected.to validate_uniqueness_of(:source)
.scoped_to(:target_id)
.with_message(/already related/)
end
it 'is not valid if an opposite link already exists' do
issue_link = build(:issue_link, source: subject.target, target: subject.source)
expect(issue_link).to be_invalid
expect(issue_link.errors[:source]).to include('is already related to this issue')
end
context 'when it relates to itself' do
let(:issue) { create :issue }
context 'cannot be validated' do
it 'does not invalidate object with self relation error' do
issue_link = build :issue_link, source: issue, target: nil
issue_link.valid?
expect(issue_link.errors[:source]).to be_empty
end
end
context 'can be invalidated' do
it 'invalidates object' do
issue_link = build :issue_link, source: issue, target: issue
expect(issue_link).to be_invalid
expect(issue_link.errors[:source]).to include('cannot be related to itself')
end
end
end
it_behaves_like 'issuable link' do
let_it_be_with_reload(:issuable_link) { create(:issue_link) }
let_it_be(:issuable) { create(:issue) }
let(:issuable_class) { 'Issue' }
let(:issuable_link_factory) { :issue_link }
end
end
# frozen_string_literal: true
# This shared example requires the following variables
# issuable_link
# issuable
# issuable_class
# issuable_link_factory
RSpec.shared_examples 'issuable link' do
describe 'Associations' do
it { is_expected.to belong_to(:source).class_name(issuable.class.name) }
it { is_expected.to belong_to(:target).class_name(issuable.class.name) }
end
describe 'Validation' do
subject { issuable_link }
it { is_expected.to validate_presence_of(:source) }
it { is_expected.to validate_presence_of(:target) }
it do
is_expected.to validate_uniqueness_of(:source)
.scoped_to(:target_id)
.with_message(/already related/)
end
it 'is not valid if an opposite link already exists' do
issuable_link = create_issuable_link(subject.target, subject.source)
expect(issuable_link).to be_invalid
expect(issuable_link.errors[:source]).to include("is already related to this #{issuable.class.name.downcase}")
end
context 'when it relates to itself' do
context 'when target is nil' do
it 'does not invalidate object with self relation error' do
issuable_link = create_issuable_link(issuable, nil)
issuable_link.valid?
expect(issuable_link.errors[:source]).to be_empty
end
end
context 'when source and target are present' do
it 'invalidates object' do
issuable_link = create_issuable_link(issuable, issuable)
expect(issuable_link).to be_invalid
expect(issuable_link.errors[:source]).to include('cannot be related to itself')
end
end
end
def create_issuable_link(source, target)
build(issuable_link_factory, source: source, target: target)
end
end
describe '.link_type' do
it { is_expected.to define_enum_for(:link_type).with_values(relates_to: 0, blocks: 1) }
it 'provides the "related" as default link_type' do
expect(issuable_link.link_type).to eq 'relates_to'
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment