summaryrefslogtreecommitdiff
path: root/spec/models/concerns/atomic_internal_id_spec.rb
blob: 35b0f107676b7796320c9d878674a291a3f45e9a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe AtomicInternalId do
  let(:milestone) { build(:milestone) }
  let(:iid) { double('iid', to_i: 42) }
  let(:external_iid) { 100 }
  let(:scope_attrs) { { project: milestone.project } }
  let(:usage) { :milestones }

  describe '#save!' do
    context 'when IID is provided' do
      before do
        milestone.iid = external_iid
      end

      it 'tracks the value' do
        expect(milestone).to receive(:track_project_iid!)

        milestone.save!
      end

      context 'when importing' do
        before do
          milestone.importing = true
        end

        it 'does not track the value' do
          expect(milestone).not_to receive(:track_project_iid!)

          milestone.save!
        end
      end
    end
  end

  describe '#track_project_iid!' do
    subject { milestone.track_project_iid! }

    it 'tracks the present value' do
      milestone.iid = external_iid

      expect(InternalId).to receive(:track_greatest).once.with(milestone, scope_attrs, usage, external_iid, anything)
      expect(InternalId).not_to receive(:generate_next)

      subject
    end

    context 'when value is set by ensure_project_iid!' do
      it 'does not track the value' do
        expect(InternalId).not_to receive(:track_greatest)

        milestone.ensure_project_iid!
        subject
      end

      it 'tracks the iid for the scope that is actually present' do
        milestone.iid = external_iid

        expect(InternalId).to receive(:track_greatest).once.with(milestone, scope_attrs, usage, external_iid, anything)
        expect(InternalId).not_to receive(:generate_next)

        # group scope is not present here, the milestone does not have a group
        milestone.track_group_iid!
        subject
      end
    end
  end

  describe '#ensure_project_iid!' do
    subject { milestone.ensure_project_iid! }

    it 'generates a new value if non is present' do
      expect(InternalId).to receive(:generate_next).with(milestone, scope_attrs, usage, anything).and_return(iid)

      expect { subject }.to change { milestone.iid }.from(nil).to(iid.to_i)
    end

    it 'generates a new value if first set with iid= but later set to nil' do
      expect(InternalId).to receive(:generate_next).with(milestone, scope_attrs, usage, anything).and_return(iid)

      milestone.iid = external_iid
      milestone.iid = nil

      expect { subject }.to change { milestone.iid }.from(nil).to(iid.to_i)
    end
  end

  describe '#clear_scope_iid!' do
    context 'when no ensure_if condition is given' do
      it 'clears automatically set IIDs' do
        expect(milestone).to receive(:clear_project_iid!).and_call_original

        expect_iid_to_be_set_and_rollback(milestone)

        expect(milestone.iid).to be_nil
      end

      it 'does not clear manually set IIDS' do
        milestone.iid = external_iid

        expect(milestone).to receive(:clear_project_iid!).and_call_original

        expect_iid_to_be_set_and_rollback(milestone)

        expect(milestone.iid).to eq(external_iid)
      end
    end

    context 'when an ensure_if condition is given' do
      let(:test_class) do
        Class.new(ApplicationRecord) do
          include AtomicInternalId
          include Importable

          self.table_name = :milestones

          belongs_to :project

          has_internal_id :iid, scope: :project, track_if: -> { !importing }, ensure_if: -> { !importing }

          def self.name
            'TestClass'
          end
        end
      end

      let(:instance) { test_class.new(milestone.attributes) }

      context 'when the ensure_if condition evaluates to true' do
        it 'clears automatically set IIDs' do
          expect(instance).to receive(:clear_project_iid!).and_call_original

          expect_iid_to_be_set_and_rollback(instance)

          expect(instance.iid).to be_nil
        end

        it 'does not clear manually set IIDs' do
          instance.iid = external_iid

          expect(instance).to receive(:clear_project_iid!).and_call_original

          expect_iid_to_be_set_and_rollback(instance)

          expect(instance.iid).to eq(external_iid)
        end
      end

      context 'when the ensure_if condition evaluates to false' do
        before do
          instance.importing = true
        end

        it 'does not clear IIDs' do
          instance.iid = external_iid

          expect(instance).not_to receive(:clear_project_iid!)

          expect_iid_to_be_set_and_rollback(instance)

          expect(instance.iid).to eq(external_iid)
        end
      end
    end

    def expect_iid_to_be_set_and_rollback(instance)
      ActiveRecord::Base.transaction(requires_new: true) do
        instance.save!

        expect(instance.iid).not_to be_nil

        raise ActiveRecord::Rollback
      end
    end
  end

  describe '#validate_scope_iid_exists!' do
    let(:test_class) do
      Class.new(ApplicationRecord) do
        include AtomicInternalId
        include Importable

        self.table_name = :milestones

        belongs_to :project

        def self.name
          'TestClass'
        end
      end
    end

    let(:instance) { test_class.new(milestone.attributes) }

    before do
      test_class.has_internal_id :iid, scope: :project, presence: presence, ensure_if: -> { !importing }

      instance.importing = true
    end

    context 'when the presence flag is set' do
      let(:presence) { true }

      it 'raises an error for blank iids on create' do
        expect do
          instance.save!
        end.to raise_error(described_class::MissingValueError, 'iid was unexpectedly blank!')
      end

      it 'raises an error for blank iids on update' do
        instance.iid = 100
        instance.save!

        instance.iid = nil

        expect do
          instance.save!
        end.to raise_error(described_class::MissingValueError, 'iid was unexpectedly blank!')
      end
    end

    context 'when the presence flag is not set' do
      let(:presence) { false }

      it 'does not raise an error for blank iids on create' do
        expect { instance.save! }.not_to raise_error
      end

      it 'does not raise an error for blank iids on update' do
        instance.iid = 100
        instance.save!

        instance.iid = nil

        expect { instance.save! }.not_to raise_error
      end
    end
  end

  describe '.with_project_iid_supply' do
    let(:iid) { 100 }

    it 'wraps generate and track_greatest in a concurrency-safe lock' do
      expect_next_instance_of(InternalId::InternalIdGenerator) do |g|
        expect(g).to receive(:with_lock).and_call_original
        expect(g.record).to receive(:last_value).and_return(iid)
        expect(g).to receive(:track_greatest).with(iid + 4)
      end

      ::Milestone.with_project_iid_supply(milestone.project) do |supply|
        4.times { supply.next_value }
      end
    end
  end
end