# frozen_string_literal: true require 'spec_helper' RSpec.describe DesignManagement::Version do let_it_be(:issue) { create(:issue) } describe 'relations' do it { is_expected.to have_many(:actions) } it { is_expected.to have_many(:designs).through(:actions) } it 'constrains the designs relation correctly' do design = create(:design) version = create(:design_version, designs: [design]) expect { version.designs << design }.to raise_error(ActiveRecord::RecordNotUnique) end it 'allows adding multiple versions to a single design' do design = create(:design) versions = create_list(:design_version, 2) expect { versions.each { |v| design.versions << v } } .not_to raise_error end end describe 'validations' do subject(:design_version) { build(:design_version) } it { is_expected.to be_valid } it { is_expected.to validate_presence_of(:author) } it { is_expected.to validate_presence_of(:sha) } it { is_expected.to validate_presence_of(:designs) } it { is_expected.to validate_presence_of(:issue) } it { is_expected.to validate_uniqueness_of(:sha).scoped_to(:issue_id).case_insensitive } end describe "scopes" do let_it_be(:version_1) { create(:design_version) } let_it_be(:version_2) { create(:design_version) } describe ".for_designs" do it "only returns versions related to the specified designs" do _other_version = create(:design_version) designs = [create(:design, versions: [version_1]), create(:design, versions: [version_2])] expect(described_class.for_designs(designs)) .to contain_exactly(version_1, version_2) end end describe '.earlier_or_equal_to' do it 'only returns versions created earlier or later than the given version' do expect(described_class.earlier_or_equal_to(version_1)).to eq([version_1]) expect(described_class.earlier_or_equal_to(version_2)).to contain_exactly(version_1, version_2) end it 'can be passed either a DesignManagement::Version or an ID' do [version_1, version_1.id].each do |arg| expect(described_class.earlier_or_equal_to(arg)).to eq([version_1]) end end end describe '.by_sha' do it 'can find versions by sha' do [version_1, version_2].each do |version| expect(described_class.by_sha(version.sha)).to contain_exactly(version) end end end end describe ".create_for_designs" do def current_version_id(design) design.send(:head_version).try(:id) end def as_actions(designs, action = :create) designs.map do |d| DesignManagement::DesignAction.new(d, action, action == :delete ? nil : :content) end end let_it_be(:author) { create(:user) } let_it_be(:design_a) { create(:design, issue: issue) } let_it_be(:design_b) { create(:design, issue: issue) } let_it_be(:designs) { [design_a, design_b] } describe 'the error raised when there are no actions' do let_it_be(:sha) { 'f00' } def call_with_empty_actions described_class.create_for_designs([], sha, author) end it 'raises CouldNotCreateVersion' do expect { call_with_empty_actions } .to raise_error(described_class::CouldNotCreateVersion) end it 'has an appropriate cause' do expect { call_with_empty_actions } .to raise_error(have_attributes(cause: ActiveRecord::RecordInvalid)) end it 'provides extra data sentry can consume' do extra_info = a_hash_including(sha: sha) expect { call_with_empty_actions } .to raise_error(have_attributes(sentry_extra_data: extra_info)) end end describe 'the error raised when the designs come from different issues' do let_it_be(:sha) { 'f00' } let_it_be(:designs) { create_list(:design, 2) } let_it_be(:actions) { as_actions(designs) } def call_with_mismatched_designs described_class.create_for_designs(actions, sha, author) end it 'raises CouldNotCreateVersion' do expect { call_with_mismatched_designs } .to raise_error(described_class::CouldNotCreateVersion) end it 'has an appropriate cause' do expect { call_with_mismatched_designs } .to raise_error(have_attributes(cause: described_class::NotSameIssue)) end it 'provides extra data sentry can consume' do extra_info = a_hash_including(design_ids: designs.map(&:id)) expect { call_with_mismatched_designs } .to raise_error(have_attributes(sentry_extra_data: extra_info)) end end it 'does not leave invalid versions around if creation fails' do expect do described_class.create_for_designs([], 'abcdef', author) rescue StandardError nil end.not_to change { described_class.count } end it 'does not leave orphaned design-versions around if creation fails' do actions = as_actions(designs) expect do described_class.create_for_designs(actions, '', author) rescue StandardError nil end.not_to change { DesignManagement::Action.count } end it 'creates a version and links it to multiple designs' do actions = as_actions(designs, :create) version = described_class.create_for_designs(actions, 'abc', author) expect(version.designs).to contain_exactly(*designs) expect(designs.map(&method(:current_version_id))).to all(eq version.id) end it 'creates designs if they are new to git' do actions = as_actions(designs, :create) described_class.create_for_designs(actions, 'abc', author) expect(designs.map(&:most_recent_action)).to all(be_creation) end it 'correctly associates the version with the issue' do actions = as_actions(designs) version = described_class.create_for_designs(actions, 'abc', author) expect(version.issue).to eq(issue) end it 'correctly associates the version with the author' do actions = as_actions(designs) version = described_class.create_for_designs(actions, 'abc', author) expect(version.author).to eq(author) end it 'modifies designs if git updated them' do actions = as_actions(designs, :update) described_class.create_for_designs(actions, 'abc', author) expect(designs.map(&:most_recent_action)).to all(be_modification) end it 'deletes designs when the git action was delete' do actions = as_actions(designs, :delete) described_class.create_for_designs(actions, 'def', author) expect(designs).to all(be_deleted) end it 're-creates designs if they are deleted' do described_class.create_for_designs(as_actions(designs, :create), 'abc', author) described_class.create_for_designs(as_actions(designs, :delete), 'def', author) expect(designs).to all(be_deleted) described_class.create_for_designs(as_actions(designs, :create), 'ghi', author) expect(designs.map(&:most_recent_action)).to all(be_creation) expect(designs).not_to include(be_deleted) end it 'changes the version of the designs' do actions = as_actions([design_a]) described_class.create_for_designs(actions, 'before', author) expect do described_class.create_for_designs(actions, 'after', author) end.to change { current_version_id(design_a) } end end describe '#designs_by_event' do context 'there is a single design' do let_it_be(:design) { create(:design) } shared_examples :a_correctly_categorised_design do |kind, category| let_it_be(:version) { create(:design_version, kind => [design]) } it 'returns a hash with a single key and the single design in that bucket' do expect(version.designs_by_event).to eq(category => [design]) end end it_behaves_like :a_correctly_categorised_design, :created_designs, 'creation' it_behaves_like :a_correctly_categorised_design, :modified_designs, 'modification' it_behaves_like :a_correctly_categorised_design, :deleted_designs, 'deletion' end context 'there are a bunch of different designs in a variety of states' do let_it_be(:version) do create(:design_version, created_designs: create_list(:design, 3), modified_designs: create_list(:design, 4), deleted_designs: create_list(:design, 5)) end it 'puts them in the right buckets' do expect(version.designs_by_event).to match( a_hash_including( 'creation' => have_attributes(size: 3), 'modification' => have_attributes(size: 4), 'deletion' => have_attributes(size: 5) ) ) end it 'does not suffer from N+1 queries' do version.designs.map(&:id) # we don't care about the set-up queries expect { version.designs_by_event }.not_to exceed_query_limit(2) end end end describe '#author' do it 'returns the author' do author = build(:user) version = build(:design_version, author: author) expect(version.author).to eq(author) end it 'returns nil if author_id is nil and version is not persisted' do version = build(:design_version, author: nil) expect(version.author).to eq(nil) end it 'retrieves author from the Commit if author_id is nil and version has been persisted' do author = create(:user) version = create(:design_version, :committed, author: author) author.destroy! version.reload commit = version.issue.project.design_repository.commit(version.sha) commit_user = create(:user, email: commit.author_email, name: commit.author_name) expect(version.author_id).to eq(nil) expect(version.author).to eq(commit_user) end end describe '#diff_refs' do let(:project) { issue.project } before do expect(project.design_repository).to receive(:commit) .once .with(sha) .and_return(commit) end subject { create(:design_version, issue: issue, sha: sha) } context 'there is a commit in the repo by the SHA' do let(:commit) { build(:commit) } let(:sha) { commit.id } it { is_expected.to have_attributes(diff_refs: commit.diff_refs) } it 'memoizes calls to #diff_refs' do expect(subject.diff_refs).to eq(subject.diff_refs) end end context 'there is no commit in the repo by the SHA' do let(:commit) { nil } let(:sha) { Digest::SHA1.hexdigest("points to nothing") } it { is_expected.to have_attributes(diff_refs: be_nil) } end end describe '#reset' do subject { create(:design_version, issue: issue) } it 'removes memoized values' do expect(subject).to receive(:commit).twice.and_return(nil) subject.diff_refs subject.diff_refs subject.reset subject.diff_refs subject.diff_refs end end end