summaryrefslogtreecommitdiff
path: root/spec/support/shared_examples/models/atomic_internal_id_shared_examples.rb
blob: fe85daa7235ae8217d746d336d3bbbd4befd0100 (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
# frozen_string_literal: true

RSpec.shared_examples 'AtomicInternalId' do |validate_presence: true|
  describe '.has_internal_id' do
    describe 'Module inclusion' do
      subject { described_class }

      it { is_expected.to include_module(AtomicInternalId) }
    end

    describe 'Validation' do
      context 'when presence validation is required' do
        before do
          skip unless validate_presence
        end

        context 'when creating an object' do
          before do
            allow_any_instance_of(described_class).to receive(:"ensure_#{scope}_#{internal_id_attribute}!")
          end

          it 'raises an error if the internal id is blank' do
            expect { instance.save! }.to raise_error(AtomicInternalId::MissingValueError)
          end
        end

        context 'when updating an object' do
          it 'raises an error if the internal id is blank' do
            instance.save!

            write_internal_id(nil)
            allow(instance).to receive(:"ensure_#{scope}_#{internal_id_attribute}!")

            expect { instance.save! }.to raise_error(AtomicInternalId::MissingValueError)
          end
        end
      end

      context 'when presence validation is not required' do
        before do
          skip if validate_presence
        end

        context 'when creating an object' do
          before do
            allow_any_instance_of(described_class).to receive(:"ensure_#{scope}_#{internal_id_attribute}!")
          end

          it 'does not raise an error if the internal id is blank' do
            expect(read_internal_id).to be_nil

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

        context 'when updating an object' do
          it 'does not raise an error if the internal id is blank' do
            instance.save!

            write_internal_id(nil)
            allow(instance).to receive(:"ensure_#{scope}_#{internal_id_attribute}!")

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

    describe 'Creating an instance' do
      subject { instance.save! }

      it 'saves a new instance properly' do
        expect { subject }.not_to raise_error
      end
    end

    describe 'internal id generation' do
      subject { instance.save! }

      it 'calls InternalId.generate_next and sets internal id attribute' do
        iid = rand(1..1000)

        # Need to do this before evaluating instance otherwise it gets set
        # already in factory
        allow(InternalId).to receive(:generate_next).and_return(iid)

        subject
        expect(read_internal_id).to eq(iid)

        expect(InternalId).to have_received(:generate_next).with(instance, scope_attrs, usage, any_args)
      end

      it 'does not overwrite an existing internal id' do
        write_internal_id(4711)

        allow_cross_database_modification_within_transaction(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/347091') do
          expect { subject }.not_to change { read_internal_id }
        end
      end

      context 'when the instance has an internal ID set' do
        let(:internal_id) { 9001 }

        it 'calls InternalId.update_last_value and sets the `last_value` to that of the instance' do
          write_internal_id(internal_id)

          expect(InternalId)
            .to receive(:track_greatest)
            .with(instance, scope_attrs, usage, internal_id, any_args)
            .and_return(internal_id)

          subject
        end
      end
    end

    describe 'unsetting the instance internal id on rollback' do
      context 'when the internal id has been changed' do
        context 'when the internal id is automatically set' do
          it 'clears it on the instance' do
            write_internal_id(nil)

            allow_cross_database_modification_within_transaction(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/347091') do
              expect_iid_to_be_set_and_rollback
            end

            expect(read_internal_id).to be_nil
          end
        end

        context 'when the internal id is manually set' do
          it 'does not clear it on the instance' do
            write_internal_id(100)

            allow_cross_database_modification_within_transaction(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/347091') do
              expect_iid_to_be_set_and_rollback
            end

            expect(read_internal_id).not_to be_nil
          end
        end
      end

      context 'when the internal id has not been changed' do
        it 'preserves the value on the instance' do
          instance.save!
          original_id = read_internal_id

          expect(original_id).not_to be_nil

          expect_iid_to_be_set_and_rollback

          expect(read_internal_id).to eq(original_id)
        end
      end

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

          expect(read_internal_id).not_to be_nil

          raise ActiveRecord::Rollback
        end
      end
    end

    describe 'supply of internal ids' do
      let(:scope_value) { scope_attrs.each_value.first }
      let(:method_name) { :"with_#{scope}_#{internal_id_attribute}_supply" }

      it 'provides a persistent supply of IID values, sensitive to the current state' do
        iid = rand(1..1000)
        write_internal_id(iid)
        instance.public_send(:"track_#{scope}_#{internal_id_attribute}!")

        # Allocate 3 IID values
        described_class.public_send(method_name, scope_value) do |supply|
          3.times { supply.next_value }
        end

        described_class.public_send(method_name, scope_value) do |supply|
          expect(supply.next_value).to eq(iid + 4)
        end
      end
    end

    describe "#reset_scope_internal_id_attribute" do
      it 'rewinds the allocated IID' do
        expect { ensure_scope_attribute! }.not_to raise_error
        expect(read_internal_id).not_to be_nil

        expect(reset_scope_attribute).to be_nil
        expect(read_internal_id).to be_nil
      end

      it 'allocates the same IID' do
        internal_id = ensure_scope_attribute!
        reset_scope_attribute
        expect(read_internal_id).to be_nil

        expect(ensure_scope_attribute!).to eq(internal_id)
      end
    end

    def ensure_scope_attribute!
      instance.public_send(:"ensure_#{scope}_#{internal_id_attribute}!")
    end

    def reset_scope_attribute
      instance.public_send(:"reset_#{scope}_#{internal_id_attribute}")
    end

    def read_internal_id
      instance.public_send(internal_id_attribute)
    end

    def write_internal_id(value)
      instance.public_send(:"#{internal_id_attribute}=", value)
    end
  end
end