diff options
Diffstat (limited to 'spec/models/design_management/design_at_version_spec.rb')
-rw-r--r-- | spec/models/design_management/design_at_version_spec.rb | 426 |
1 files changed, 426 insertions, 0 deletions
diff --git a/spec/models/design_management/design_at_version_spec.rb b/spec/models/design_management/design_at_version_spec.rb new file mode 100644 index 00000000000..f6fa8df243c --- /dev/null +++ b/spec/models/design_management/design_at_version_spec.rb @@ -0,0 +1,426 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe DesignManagement::DesignAtVersion do + include DesignManagementTestHelpers + + let_it_be(:issue, reload: true) { create(:issue) } + let_it_be(:issue_b, reload: true) { create(:issue) } + let_it_be(:design, reload: true) { create(:design, issue: issue) } + let_it_be(:version) { create(:design_version, designs: [design]) } + + describe '#id' do + subject { described_class.new(design: design, version: version) } + + it 'combines design.id and version.id' do + expect(subject.id).to include(design.id.to_s, version.id.to_s) + end + end + + describe '#==' do + it 'identifies objects created with the same parameters as equal' do + design = build_stubbed(:design, issue: issue) + version = build_stubbed(:design_version, designs: [design], issue: issue) + + this = build_stubbed(:design_at_version, design: design, version: version) + other = build_stubbed(:design_at_version, design: design, version: version) + + expect(this).to eq(other) + expect(other).to eq(this) + end + + it 'identifies unequal objects as unequal, by virtue of their version' do + design = build_stubbed(:design, issue: issue) + version_a = build_stubbed(:design_version, designs: [design]) + version_b = build_stubbed(:design_version, designs: [design]) + + this = build_stubbed(:design_at_version, design: design, version: version_a) + other = build_stubbed(:design_at_version, design: design, version: version_b) + + expect(this).not_to eq(nil) + expect(this).not_to eq(design) + expect(this).not_to eq(other) + expect(other).not_to eq(this) + end + + it 'identifies unequal objects as unequal, by virtue of their design' do + design_a = build_stubbed(:design, issue: issue) + design_b = build_stubbed(:design, issue: issue) + version = build_stubbed(:design_version, designs: [design_a, design_b]) + + this = build_stubbed(:design_at_version, design: design_a, version: version) + other = build_stubbed(:design_at_version, design: design_b, version: version) + + expect(this).not_to eq(other) + expect(other).not_to eq(this) + end + + it 'rejects objects with the same id and the wrong class' do + dav = build_stubbed(:design_at_version) + + expect(dav).not_to eq(OpenStruct.new(id: dav.id)) + end + + it 'expects objects to be of the same type, not subtypes' do + subtype = Class.new(described_class) + dav = build_stubbed(:design_at_version) + other = subtype.new(design: dav.design, version: dav.version) + + expect(dav).not_to eq(other) + end + end + + describe 'status methods' do + let!(:design_a) { create(:design, issue: issue) } + let!(:design_b) { create(:design, issue: issue) } + + let!(:version_a) do + create(:design_version, designs: [design_a]) + end + let!(:version_b) do + create(:design_version, designs: [design_b]) + end + let!(:version_mod) do + create(:design_version, modified_designs: [design_a, design_b]) + end + let!(:version_c) do + create(:design_version, deleted_designs: [design_a]) + end + let!(:version_d) do + create(:design_version, deleted_designs: [design_b]) + end + let!(:version_e) do + create(:design_version, designs: [design_a]) + end + + describe 'a design before it has been created' do + subject { build(:design_at_version, design: design_b, version: version_a) } + + it 'is not deleted' do + expect(subject).not_to be_deleted + end + + it 'has the status :not_created_yet' do + expect(subject).to have_attributes(status: :not_created_yet) + end + end + + describe 'a design as of its creation' do + subject { build(:design_at_version, design: design_a, version: version_a) } + + it 'is not deleted' do + expect(subject).not_to be_deleted + end + + it 'has the status :current' do + expect(subject).to have_attributes(status: :current) + end + end + + describe 'a design after it has been created, but before deletion' do + subject { build(:design_at_version, design: design_b, version: version_c) } + + it 'is not deleted' do + expect(subject).not_to be_deleted + end + + it 'has the status :current' do + expect(subject).to have_attributes(status: :current) + end + end + + describe 'a design as of its modification' do + subject { build(:design_at_version, design: design_a, version: version_mod) } + + it 'is not deleted' do + expect(subject).not_to be_deleted + end + + it 'has the status :current' do + expect(subject).to have_attributes(status: :current) + end + end + + describe 'a design as of its deletion' do + subject { build(:design_at_version, design: design_a, version: version_c) } + + it 'is deleted' do + expect(subject).to be_deleted + end + + it 'has the status :deleted' do + expect(subject).to have_attributes(status: :deleted) + end + end + + describe 'a design after its deletion' do + subject { build(:design_at_version, design: design_b, version: version_e) } + + it 'is deleted' do + expect(subject).to be_deleted + end + + it 'has the status :deleted' do + expect(subject).to have_attributes(status: :deleted) + end + end + + describe 'a design on its recreation' do + subject { build(:design_at_version, design: design_a, version: version_e) } + + it 'is not deleted' do + expect(subject).not_to be_deleted + end + + it 'has the status :current' do + expect(subject).to have_attributes(status: :current) + end + end + end + + describe 'validations' do + subject(:design_at_version) { build(:design_at_version) } + + it { is_expected.to be_valid } + + describe 'a design-at-version without a design' do + subject { described_class.new(design: nil, version: build(:design_version)) } + + it { is_expected.to be_invalid } + + it 'mentions the design in the errors' do + subject.valid? + + expect(subject.errors[:design]).to be_present + end + end + + describe 'a design-at-version without a version' do + subject { described_class.new(design: build(:design), version: nil) } + + it { is_expected.to be_invalid } + + it 'mentions the version in the errors' do + subject.valid? + + expect(subject.errors[:version]).to be_present + end + end + + describe 'design_and_version_belong_to_the_same_issue' do + context 'both design and version are supplied' do + subject(:design_at_version) { build(:design_at_version, design: design, version: version) } + + context 'the design belongs to the same issue as the version' do + it { is_expected.to be_valid } + end + + context 'the design does not belong to the same issue as the version' do + let(:design) { create(:design) } + let(:version) { create(:design_version) } + + it { is_expected.to be_invalid } + end + end + + context 'the factory is just supplied with a design' do + let(:design) { create(:design) } + + subject(:design_at_version) { build(:design_at_version, design: design) } + + it { is_expected.to be_valid } + end + + context 'the factory is just supplied with a version' do + let(:version) { create(:design_version) } + + subject(:design_at_version) { build(:design_at_version, version: version) } + + it { is_expected.to be_valid } + end + end + + describe 'design_and_version_have_issue_id' do + subject(:design_at_version) { build(:design_at_version, design: design, version: version) } + + context 'the design has no issue_id, because it is being imported' do + let(:design) { create(:design, :importing) } + + it { is_expected.to be_invalid } + end + + context 'the version has no issue_id, because it is being imported' do + let(:version) { create(:design_version, :importing) } + + it { is_expected.to be_invalid } + end + + context 'both the design and the version are being imported' do + let(:version) { create(:design_version, :importing) } + let(:design) { create(:design, :importing) } + + it { is_expected.to be_invalid } + end + end + end + + def id_of(design, version) + build(:design_at_version, design: design, version: version).id + end + + describe '.instantiate' do + context 'when attrs are valid' do + subject do + described_class.instantiate(design: design, version: version) + end + + it { is_expected.to be_a(described_class).and(be_valid) } + end + + context 'when attrs are invalid' do + subject do + described_class.instantiate( + design: create(:design), + version: create(:design_version) + ) + end + + it 'raises a validation error' do + expect { subject }.to raise_error(ActiveModel::ValidationError) + end + end + end + + describe '.lazy_find' do + let!(:version_a) do + create(:design_version, designs: create_list(:design, 3, issue: issue)) + end + let!(:version_b) do + create(:design_version, designs: create_list(:design, 1, issue: issue)) + end + let!(:version_c) do + create(:design_version, designs: create_list(:design, 1, issue: issue_b)) + end + + let(:id_a) { id_of(version_a.designs.first, version_a) } + let(:id_b) { id_of(version_a.designs.second, version_a) } + let(:id_c) { id_of(version_a.designs.last, version_a) } + let(:id_d) { id_of(version_b.designs.first, version_b) } + let(:id_e) { id_of(version_c.designs.first, version_c) } + let(:bad_id) { id_of(version_c.designs.first, version_a) } + + def find(the_id) + described_class.lazy_find(the_id) + end + + let(:db_calls) { 2 } + + it 'issues fewer queries than the naive approach would' do + expect do + dav_a = find(id_a) + dav_b = find(id_b) + dav_c = find(id_c) + dav_d = find(id_d) + dav_e = find(id_e) + should_not_exist = find(bad_id) + + expect(dav_a.version).to eq(version_a) + expect(dav_b.version).to eq(version_a) + expect(dav_c.version).to eq(version_a) + expect(dav_d.version).to eq(version_b) + expect(dav_e.version).to eq(version_c) + expect(should_not_exist).not_to be_present + + expect(version_a.designs).to include(dav_a.design, dav_b.design, dav_c.design) + expect(version_b.designs).to include(dav_d.design) + expect(version_c.designs).to include(dav_e.design) + end.not_to exceed_query_limit(db_calls) + end + end + + describe '.find' do + let(:results) { described_class.find(ids) } + + # 2 versions, with 5 total designs on issue A, so 2*5 = 10 + let!(:version_a) do + create(:design_version, designs: create_list(:design, 3, issue: issue)) + end + let!(:version_b) do + create(:design_version, designs: create_list(:design, 2, issue: issue)) + end + # 1 version, with 3 designs on issue B, so 1*3 = 3 + let!(:version_c) do + create(:design_version, designs: create_list(:design, 3, issue: issue_b)) + end + + context 'invalid ids' do + let(:ids) do + version_b.designs.map { |d| id_of(d, version_c) } + end + + describe '#count' do + it 'counts 0 records' do + expect(results.count).to eq(0) + end + end + + describe '#empty?' do + it 'is empty' do + expect(results).to be_empty + end + end + + describe '#to_a' do + it 'finds no records' do + expect(results.to_a).to eq([]) + end + end + end + + context 'valid ids' do + let(:red_herrings) { issue_b.designs.sample(2).map { |d| id_of(d, version_a) } } + + let(:ids) do + a_ids = issue.designs.sample(2).map { |d| id_of(d, version_a) } + b_ids = issue.designs.sample(2).map { |d| id_of(d, version_b) } + c_ids = issue_b.designs.sample(2).map { |d| id_of(d, version_c) } + + a_ids + b_ids + c_ids + red_herrings + end + + before do + ids.size # force IDs + end + + describe '#count' do + it 'counts 2 records' do + expect(results.count).to eq(6) + end + + it 'issues at most two queries' do + expect { results.count }.not_to exceed_query_limit(2) + end + end + + describe '#to_a' do + it 'finds 6 records' do + expect(results.size).to eq(6) + expect(results).to all(be_a(described_class)) + end + + it 'only returns records with matching IDs' do + expect(results.map(&:id)).to match_array(ids - red_herrings) + end + + it 'only returns valid records' do + expect(results).to all(be_valid) + end + + it 'issues at most two queries' do + expect { results.to_a }.not_to exceed_query_limit(2) + end + end + end + end +end |