persistent_enum 1.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,758 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubocop:disable Style/Semicolon, Lint/MissingCopEnableDirective
4
+
5
+ require 'persistent_enum'
6
+ require 'byebug'
7
+
8
+ require_relative '../spec_helper'
9
+
10
+ RSpec.describe PersistentEnum, :database do
11
+ CONSTANTS = [:One, :Two, :Three, :Four].freeze
12
+
13
+ before(:context) do
14
+ DatabaseHelper.initialize_database
15
+ end
16
+
17
+ before(:each) do
18
+ if ActiveRecord::Base.logger
19
+ allow(ActiveRecord::Base.logger).to receive(:warn).and_call_original
20
+ else
21
+ ActiveRecord::Base.logger = spy(:logger)
22
+ end
23
+ initialize_test_models
24
+ end
25
+
26
+ after(:each) do
27
+ destroy_test_models
28
+ unless ActiveRecord::Base.logger.is_a?(Logger)
29
+ ActiveRecord::Base.logger = nil
30
+ end
31
+ end
32
+
33
+ shared_examples 'acts like an enum' do
34
+ # abstract :model
35
+
36
+ it 'looks up each value' do
37
+ CONSTANTS.each do |c|
38
+ e = model.value_of(c)
39
+ expect(e).to be_present
40
+ expect(e.enum_constant).to be_a(String)
41
+ expect(e.to_sym).to eq(c)
42
+ expect(e).to eq(model[e.ordinal])
43
+ expect(e).to eq(model.const_get(c.upcase))
44
+ expect(e).to be_frozen
45
+ expect(e.enum_constant).to be_frozen
46
+ end
47
+ end
48
+
49
+ it 'returns all values from the cache' do
50
+ expect(model.values.map(&:to_sym)).to contain_exactly(*CONSTANTS)
51
+ end
52
+ end
53
+
54
+ shared_examples 'acts like a persisted enum' do
55
+ # abstract :model
56
+
57
+ include_examples 'acts like an enum'
58
+
59
+ context 'a referring model' do
60
+ let(:foreign_name) { model.model_name.singular }
61
+ let(:foreign_key) { foreign_name + '_id' }
62
+
63
+ let(:other_model) do
64
+ foreign_name = foreign_name()
65
+ foreign_key_type = model.columns.detect { |x| x.name == 'id' }.sql_type
66
+
67
+ create_table = ->(t) {
68
+ t.references foreign_name, type: foreign_key_type, foreign_key: true
69
+ }
70
+
71
+ create_test_model(:referrer, create_table) do
72
+ belongs_to_enum foreign_name
73
+ end
74
+ end
75
+
76
+ it 'can be created from enum value' do
77
+ model.values.each do |v|
78
+ t = other_model.new(foreign_name => v)
79
+ expect(t).to be_valid
80
+ .and have_attributes(foreign_name => v,
81
+ foreign_key => v.ordinal)
82
+ end
83
+ end
84
+
85
+ it 'can be created from constant name' do
86
+ model.values.each do |v|
87
+ t = other_model.new(foreign_name => v.enum_constant)
88
+ expect(t).to be_valid
89
+ .and have_attributes(foreign_name => v,
90
+ foreign_key => v.ordinal)
91
+ end
92
+ end
93
+
94
+ it 'can be created from ordinal' do
95
+ model.values.each do |v|
96
+ t = other_model.new(foreign_key => v.ordinal)
97
+ expect(t).to be_valid
98
+ .and have_attributes(foreign_name => v,
99
+ foreign_key => v.ordinal)
100
+ end
101
+ end
102
+
103
+ it 'can be created with null foreign key' do
104
+ t = other_model.new
105
+ expect(t).to be_valid
106
+ end
107
+
108
+ it 'can not be created with invalid foreign key' do
109
+ t = other_model.new(foreign_key => -1)
110
+ expect(t).not_to be_valid
111
+ end
112
+
113
+ it 'can not be created with invalid foreign constant' do
114
+ expect {
115
+ other_model.new(foreign_name => :BadConstant)
116
+ }.to raise_error(NameError)
117
+ end
118
+ end
119
+ end
120
+
121
+ context 'with an enum model' do
122
+ let(:model) do
123
+ create_test_model(:with_table, ->(t) { t.string :name; t.index [:name], unique: true }) do
124
+ acts_as_enum(CONSTANTS)
125
+ end
126
+ end
127
+
128
+ it_behaves_like 'acts like a persisted enum'
129
+
130
+ it 'returns all values from the database' do
131
+ expect(model.all.map(&:to_sym)).to contain_exactly(*CONSTANTS)
132
+ end
133
+
134
+ it 'is immutable' do
135
+ expect { model.create(name: 'foo') }
136
+ .to raise_error(ActiveRecord::ReadOnlyRecord)
137
+
138
+ expect { model::ONE.name = 'foo' }
139
+ .to raise_error(RuntimeError, /can't modify frozen/i) # Frozen object
140
+
141
+ expect { model.first.update_attribute(:name, 'foo') }
142
+ .to raise_error(ActiveRecord::ReadOnlyRecord)
143
+
144
+ expect { model.first.destroy }
145
+ .to raise_error(ActiveRecord::ReadOnlyRecord)
146
+ end
147
+ end
148
+
149
+ context 'with a validation on the model' do
150
+ let(:model) do
151
+ create_test_model(:with_table, ->(t) { t.string :name; t.index [:name], unique: true }) do
152
+ yield if block_given?
153
+ validates :name, exclusion: { in: [CONSTANTS.first.to_s] }
154
+ acts_as_enum(CONSTANTS)
155
+ end
156
+ end
157
+
158
+ it 'does not admit invalid values' do
159
+ expect { model }
160
+ .to raise_error(ActiveRecord::RecordInvalid)
161
+ end
162
+
163
+ context 'with existing invalid values' do
164
+ let(:model) do
165
+ super() do
166
+ create(name: CONSTANTS.first.to_s)
167
+ end
168
+ end
169
+
170
+ it 'does not admit invalid values' do
171
+ expect { model }
172
+ .to raise_error(ActiveRecord::RecordInvalid)
173
+ end
174
+ end
175
+ end
176
+
177
+ def self.with_dummy_rake(&block)
178
+ context 'with rake defined' do
179
+ around(:each) do |example|
180
+ rake = OpenStruct.new(application: OpenStruct.new(top_level_tasks: [:fake]))
181
+ Object.const_set(:Rake, rake)
182
+ example.run
183
+ Object.send(:remove_const, :Rake)
184
+ end
185
+
186
+ instance_exec(&block)
187
+ end
188
+ end
189
+
190
+ shared_examples 'falls back to a dummy model' do |name_attr: 'name', sql_enum: false|
191
+ context 'without rake defined' do
192
+ it 'raises the error directly' do
193
+ expect { model }.to raise_error(PersistentEnum::EnumTableInvalid)
194
+ end
195
+ end
196
+
197
+ with_dummy_rake do
198
+ it 'warns that it is falling back' do
199
+ expect(model).to be_present
200
+ expect(ActiveRecord::Base.logger)
201
+ .to have_received(:warn)
202
+ .with(a_string_matching(/Database table initialization error.*dummy records/))
203
+ end
204
+
205
+ it 'makes a dummy model' do
206
+ dummy_model = PersistentEnum.dummy_class(model, name_attr)
207
+ expect(dummy_model).to be_present
208
+ expect(model.values).to all(be_kind_of(dummy_model))
209
+ end
210
+
211
+ it 'initializes dummy values correctly' do
212
+ model.values.each do |val|
213
+ i = val.ordinal
214
+ enum_type = sql_enum ? String : Integer
215
+ expect(i).to be_a(enum_type)
216
+ expect(val.id).to eq(i)
217
+ expect(val['id']).to eq(i)
218
+ expect(val[:id]).to eq(i)
219
+
220
+ c = val.enum_constant
221
+ expect(c).to be_a(String)
222
+ expect(val.name).to eq(c)
223
+ expect(val['name']).to eq(c)
224
+ expect(val[:name]).to eq(c)
225
+ end
226
+ end
227
+ end
228
+ end
229
+
230
+ context 'with a table-less enum' do
231
+ let(:model) do
232
+ create_test_model(:without_table, nil, create_table: false) do
233
+ acts_as_enum(CONSTANTS)
234
+ end
235
+ end
236
+
237
+ it_behaves_like 'falls back to a dummy model'
238
+
239
+ with_dummy_rake do
240
+ it_behaves_like 'acts like an enum'
241
+ end
242
+ end
243
+
244
+ context 'with existing data' do
245
+ let(:initial_ordinal) { 9998 }
246
+ let(:initial_constant) { CONSTANTS.first }
247
+
248
+ let(:existing_ordinal) { 9999 }
249
+ let(:existing_constant) { :Hello }
250
+
251
+ let!(:model) do
252
+ model = create_test_model(:with_existing, ->(t) { t.string :name; t.index [:name], unique: true })
253
+ @initial_value = model.create(id: initial_ordinal, name: initial_constant.to_s)
254
+ @existing_value = model.create(id: existing_ordinal, name: existing_constant.to_s)
255
+ model.acts_as_enum(CONSTANTS)
256
+ model
257
+ end
258
+
259
+ it_behaves_like 'acts like a persisted enum'
260
+
261
+ let(:expected_all) { (CONSTANTS + [existing_constant]) }
262
+ let(:expected_required) { CONSTANTS }
263
+
264
+ it 'caches required values' do
265
+ expect(model.values.map(&:to_sym)).to contain_exactly(*expected_required)
266
+ end
267
+
268
+ it 'caches all values' do
269
+ expect(model.all_values.map(&:to_sym)).to contain_exactly(*expected_all)
270
+ end
271
+
272
+ it 'loads all values' do
273
+ expect(model.all.map(&:to_sym)).to contain_exactly(*expected_all)
274
+ end
275
+
276
+ let(:required_ordinals) { expected_required.map { |name| model.value_of!(name).ordinal } }
277
+ let(:all_ordinals) { expected_all.map { |name| model.value_of!(name).ordinal } }
278
+
279
+ it 'caches required ordinals' do
280
+ expect(model.ordinals).to contain_exactly(*required_ordinals)
281
+ end
282
+
283
+ it 'caches all ordinals' do
284
+ expect(model.all_ordinals).to contain_exactly(*all_ordinals)
285
+ end
286
+
287
+ it 'loads all ordinals' do
288
+ expect(model.pluck(:id)).to contain_exactly(*all_ordinals)
289
+ end
290
+
291
+ it 'respects initial value' do
292
+ expect(model[initial_ordinal]).to eq(@initial_value)
293
+ expect(model.value_of(initial_constant)).to eq(@initial_value)
294
+ expect(model.where(name: initial_constant).first).to eq(@initial_value)
295
+ end
296
+
297
+ it 'respects existing value' do
298
+ expect(model[existing_ordinal]).to eq(@existing_value)
299
+ expect(model.value_of(existing_constant)).to eq(@existing_value)
300
+ expect(model.where(name: existing_constant).first).to eq(@existing_value)
301
+ end
302
+
303
+ it 'marks existing model as non-active' do
304
+ expect(model[existing_ordinal]).to_not be_active
305
+ end
306
+ end
307
+
308
+ context 'with cached constants' do
309
+ let(:model) do
310
+ create_test_model(:with_constants, ->(t) { t.string :name; t.index [:name], unique: true }) do
311
+ PersistentEnum.cache_constants(self, CONSTANTS)
312
+ end
313
+ end
314
+
315
+ it 'caches all the constants' do
316
+ CONSTANTS.each do |c|
317
+ cached = model.const_get(c.upcase)
318
+ expect(cached).to be_present
319
+ expect(cached.name).to eq(c.to_s)
320
+
321
+ loaded = model.find_by(name: c.to_s)
322
+ expect(loaded).to be_present.and eq(cached)
323
+ end
324
+ end
325
+ end
326
+
327
+ context 'with complex constant names' do
328
+ let(:test_constants) do
329
+ {
330
+ 'CamelCase' => 'CAMEL_CASE',
331
+ :Symbolic => 'SYMBOLIC',
332
+ 'with.punctuation' => 'WITH_PUNCTUATION',
333
+ 'multiple_.underscores' => 'MULTIPLE_UNDERSCORES',
334
+ }
335
+ end
336
+
337
+ let(:model) do
338
+ test_constants = test_constants()
339
+ create_test_model(:with_complex_names, ->(t) { t.string :name; t.index [:name], unique: true }) do
340
+ PersistentEnum.cache_constants(self, test_constants.keys)
341
+ end
342
+ end
343
+
344
+ it 'caches the constant name as we expect' do
345
+ test_constants.each do |expected_name, expected_constant|
346
+ val = model.const_get(expected_constant)
347
+ expect(val).to be_present
348
+ expect(val.name).to eq(expected_name.to_s)
349
+ end
350
+ end
351
+ end
352
+
353
+ context 'with extra fields' do
354
+ let(:fields) {
355
+ [:count]
356
+ }
357
+ let(:defaults) do
358
+ { count: 4 }
359
+ end
360
+
361
+ let(:members) do
362
+ {
363
+ :One => { count: 1 },
364
+ :Two => { count: 2 },
365
+ :Three => { count: 3 },
366
+ :Four => {},
367
+ }
368
+ end
369
+
370
+ shared_examples 'acts like an enum with extra fields' do
371
+ it 'has all expected members with expected values' do
372
+ members.each do |name, member_fields|
373
+ ev = model.value_of(name)
374
+
375
+ # Ensure it exists and is correctly saved
376
+ expect(ev).to be_present
377
+ expect(model.values).to include(ev)
378
+ expect(model.all_values).to include(ev)
379
+ expect(ev).to eq(model[ev.ordinal])
380
+
381
+ # Ensure it's correctly saved
382
+ if model.table_exists?
383
+ expect(model.where(name: name).first).to eq(ev)
384
+ end
385
+
386
+ # and that fields have been correctly set
387
+ fields.each do |field|
388
+ expected =
389
+ member_fields.fetch(field) { defaults[field] if model.table_exists? }
390
+ expect(ev[field]).to eq(expected)
391
+ end
392
+ end
393
+ end
394
+ end
395
+
396
+ shared_examples 'acts like a persisted enum with extra fields' do
397
+ include_examples 'acts like an enum with extra fields'
398
+ end
399
+
400
+ context 'providing a hash' do
401
+ let(:model) do
402
+ members = members()
403
+ create_test_model(:with_extra_field, ->(t) {
404
+ t.string :name
405
+ t.integer :count, default: 4
406
+ t.index [:name], unique: true
407
+ }) do
408
+ # pre-existing matching, non-matching, and outdated data
409
+ create(name: 'One', count: 3)
410
+ create(name: 'Two', count: 2)
411
+ create(name: 'Zero', count: 0)
412
+
413
+ acts_as_enum(members)
414
+ end
415
+ end
416
+
417
+ it_behaves_like 'acts like a persisted enum'
418
+ it_behaves_like 'acts like a persisted enum with extra fields'
419
+
420
+ it 'keeps outdated data' do
421
+ z = model.value_of('Zero')
422
+ expect(z).to be_present
423
+ expect(model[z.ordinal]).to eq(z)
424
+ expect(z.count).to eq(0)
425
+ expect(model.all_values).to include(z)
426
+ expect(model.values).not_to include(z)
427
+ end
428
+
429
+ context 'when reinitializing' do
430
+ let!(:model) { super() }
431
+
432
+ it 'uses the same constant' do
433
+ old_constant = model.const_get(:ONE)
434
+ model.reinitialize_acts_as_enum
435
+ new_constant = model.const_get(:ONE)
436
+ expect(new_constant).to equal(old_constant)
437
+ expect(new_constant).to equal(model.value_of('One'))
438
+ end
439
+
440
+ it 'handles a change in an attribute' do
441
+ old_constant = model.const_get(:ONE)
442
+
443
+ state = model._acts_as_enum_state
444
+ new_spec =
445
+ PersistentEnum::EnumSpec.new(
446
+ nil,
447
+ state.required_members.merge({ 'One' => { count: 1111 } }),
448
+ state.name_attr,
449
+ state.sql_enum_type)
450
+ model.initialize_acts_as_enum(new_spec)
451
+
452
+ new_constant = model.const_get(:ONE)
453
+ expect(new_constant.count).to eq(1111)
454
+ expect(new_constant).to equal(old_constant)
455
+ expect(new_constant).to equal(model.value_of('One'))
456
+ end
457
+
458
+ it 'handles a change in enum constant by replacing' do
459
+ model.where(name: 'One').update_all(name: 'MoreThanOne')
460
+ old_constant = model.const_get(:ONE).reload
461
+ # Note this has completely broken the enum indexing. Not relevant to test.
462
+ expect(old_constant.name).to eq('MoreThanOne')
463
+
464
+ model.reinitialize_acts_as_enum
465
+ new_constant = model.const_get(:ONE)
466
+ expect(new_constant.name).to eq('One')
467
+ expect(new_constant).not_to equal(old_constant)
468
+ expect(new_constant).to equal(model.value_of('One'))
469
+ end
470
+ end
471
+ end
472
+
473
+ context 'using builder interface' do
474
+ let(:model) do
475
+ create_test_model(:with_extra_field_using_builder, ->(t) { t.string :name; t.integer :count; t.index [:name], unique: true }) do
476
+ acts_as_enum(nil) do
477
+ One(count: 1)
478
+ Two(count: 2)
479
+ constant!(:Three, count: 3)
480
+ Four(count: 4)
481
+ end
482
+ end
483
+ end
484
+
485
+ it_behaves_like 'acts like a persisted enum'
486
+ it_behaves_like 'acts like a persisted enum with extra fields'
487
+ end
488
+
489
+ context 'with closed over builder' do
490
+ let(:model) do
491
+ v = 0
492
+ create_test_model(:with_dynamic_builder, ->(t) { t.string :name; t.integer :count; t.index [:name], unique: true }) do
493
+ acts_as_enum(nil) do
494
+ Member(count: (v += 1))
495
+ end
496
+ end
497
+ end
498
+
499
+ it 'can reinitialize with a changed value' do
500
+ expect(model.value_of('Member').count).to eq(1)
501
+ model.reinitialize_acts_as_enum
502
+ expect(model.value_of('Member').count).to eq(2)
503
+ end
504
+ end
505
+
506
+ context 'without table' do
507
+ let(:model) do
508
+ members = members()
509
+ create_test_model(:with_extra_field_without_table, nil, create_table: false) do
510
+ acts_as_enum(members)
511
+ end
512
+ end
513
+
514
+ it_behaves_like 'falls back to a dummy model'
515
+
516
+ with_dummy_rake do
517
+ it_behaves_like 'acts like an enum'
518
+ it_behaves_like 'acts like an enum with extra fields'
519
+ end
520
+ end
521
+
522
+ context 'with missing required attributes' do
523
+ let(:model) do
524
+ create_test_model(:test_invalid_args_a, ->(t) { t.string :name; t.integer :count, null: false; t.index [:name], unique: true }) do
525
+ acts_as_enum([:Bad])
526
+ end
527
+ end
528
+
529
+ it_behaves_like 'falls back to a dummy model'
530
+
531
+ with_dummy_rake do
532
+ it 'warns that the required attributes were missing' do
533
+ expect(model.logger)
534
+ .to have_received(:warn)
535
+ .with(a_string_matching(/required attributes.*not provided/))
536
+ end
537
+ end
538
+ end
539
+
540
+ context 'with attributes with defaults' do
541
+ def prepopulate(table); end
542
+
543
+ let(:model) do
544
+ pp_handle = method(:prepopulate)
545
+ create_test_model(:test_invalid_args_b, ->(t) {
546
+ t.string :name
547
+ t.integer :count, default: 1, null: false
548
+ t.integer :maybe
549
+ t.index [:name], unique: true
550
+ }) do
551
+ pp_handle.call(self)
552
+ acts_as_enum(nil) do
553
+ One()
554
+ Two(count: 2)
555
+ Three(maybe: 1)
556
+ end
557
+ end
558
+ end
559
+
560
+ it 'allows defaults to be omitted' do
561
+ o = model.value_of('One')
562
+ expect(o).to be_present
563
+ expect(o.count).to eq(1)
564
+ expect(o.maybe).to eq(nil)
565
+
566
+ t = model.value_of('Two')
567
+ expect(t).to be_present
568
+ expect(t.count).to eq(2)
569
+ expect(t.maybe).to eq(nil)
570
+
571
+ r = model.value_of('Three')
572
+ expect(r).to be_present
573
+ expect(r.count).to eq(1)
574
+ expect(r.maybe).to eq(1)
575
+ end
576
+
577
+ context 'with non-default existing values' do
578
+ def prepopulate(table)
579
+ table.create!(name: 'One', count: 10, maybe: 10)
580
+ end
581
+
582
+ it 'asserts the defaults when omitted' do
583
+ o = model.value_of('One')
584
+ expect(o).to be_present
585
+ expect(o.count).to eq(1)
586
+ expect(o.maybe).to eq(nil)
587
+ end
588
+ end
589
+ end
590
+
591
+ it 'warns if nonexistent attributes are provided' do
592
+ create_test_model(:test_invalid_args_c, ->(t) { t.string :name; t.index [:name], unique: true }) do
593
+ acts_as_enum({ :One => { incorrect: 1 } })
594
+ end
595
+
596
+ expect(ActiveRecord::Base.logger)
597
+ .to have_received(:warn)
598
+ .with(a_string_matching(/missing from table/))
599
+ end
600
+
601
+ context 'with array typed extra fields', :postgresql do
602
+ let(:fields) {
603
+ [:counts]
604
+ }
605
+
606
+ let(:defaults) do
607
+ { counts: [4, 40] }
608
+ end
609
+
610
+ let(:members) do
611
+ {
612
+ :One => { counts: [1, 10] },
613
+ :Two => { counts: [2, 20] },
614
+ :Three => { counts: [3, 30] },
615
+ :Four => {},
616
+ }
617
+ end
618
+
619
+ let(:model) do
620
+ members = members()
621
+ create_test_model(:with_extra_array_field, ->(t) {
622
+ t.string :name;
623
+ t.column :counts, 'integer[]', default: '{4, 40}'
624
+ t.index [:name], unique: true
625
+ }) do
626
+ # pre-existing matching, non-matching, and outdated data
627
+ create(name: 'One', counts: [3])
628
+ create(name: 'Two', counts: [2])
629
+ create(name: 'Zero', counts: [0])
630
+
631
+ acts_as_enum(members)
632
+ end
633
+ end
634
+
635
+ it_behaves_like 'acts like a persisted enum'
636
+ it_behaves_like 'acts like a persisted enum with extra fields'
637
+ end
638
+ end
639
+
640
+ context 'using a postgresql enum valued id', :postgresql do
641
+ let(:name) { 'with_enum_id' }
642
+ let(:enum_type) { "#{name}_type" }
643
+
644
+ context 'with table' do
645
+ before(:each) do
646
+ ActiveRecord::Base.connection.execute("CREATE TYPE #{enum_type} AS ENUM ()")
647
+ ActiveRecord::Base.connection.create_table(name.pluralize, id: false) do |t|
648
+ t.column :id, enum_type, primary_key: true, null: false
649
+ t.string :name
650
+ t.index [:name], unique: true
651
+ end
652
+ end
653
+
654
+ after(:each) do
655
+ ActiveRecord::Base.connection.execute("DROP TYPE #{enum_type} CASCADE")
656
+ end
657
+
658
+ let(:model) do
659
+ enum_type = enum_type()
660
+ create_test_model(:with_enum_id, nil, create_table: false) do
661
+ acts_as_enum(CONSTANTS, sql_enum_type: enum_type)
662
+ end
663
+ end
664
+
665
+ it_behaves_like 'acts like a persisted enum'
666
+ end
667
+
668
+ context 'without table' do
669
+ let(:model) do
670
+ enum_type = enum_type()
671
+ create_test_model(:no_table_enum_id, nil, create_table: false) do
672
+ acts_as_enum(CONSTANTS, sql_enum_type: enum_type)
673
+ end
674
+ end
675
+
676
+ it_behaves_like 'falls back to a dummy model', sql_enum: true
677
+ with_dummy_rake do
678
+ it_behaves_like 'acts like an enum'
679
+ end
680
+ end
681
+ end
682
+
683
+ context 'with the name of the enum value column changed' do
684
+ let(:model) do
685
+ create_test_model(:test_new_name, ->(t) { t.string :namey; t.index [:namey], unique: true }) do
686
+ acts_as_enum(CONSTANTS, name_attr: :namey)
687
+ end
688
+ end
689
+ it_behaves_like 'acts like a persisted enum'
690
+ end
691
+
692
+ it 'refuses to create a table in a transaction' do
693
+ expect {
694
+ ActiveRecord::Base.transaction do
695
+ create_test_model(:test_create_in_transaction, ->(t) { t.string :name; t.index [:name], unique: true }) do
696
+ acts_as_enum([:A, :B])
697
+ end
698
+ end
699
+ }.to raise_error(RuntimeError, /unsafe class initialization during/)
700
+ end
701
+
702
+ context 'without an index on the enum constant' do
703
+ let(:model) do
704
+ create_test_model(:test_create_without_index, ->(t) { t.string :name }) do
705
+ acts_as_enum([:A, :B])
706
+ end
707
+ end
708
+
709
+ it_behaves_like 'falls back to a dummy model'
710
+
711
+ with_dummy_rake do
712
+ it 'warns that the unique index was missing' do
713
+ expect(model.logger)
714
+ .to have_received(:warn)
715
+ .with(a_string_matching(/missing unique index/))
716
+ end
717
+ end
718
+ end
719
+
720
+ context 'without a unique index on the enum constant' do
721
+ let(:model) do
722
+ create_test_model(:test_create_without_index, ->(t) { t.string :name; t.index [:name] }) do
723
+ acts_as_enum([:A, :B])
724
+ end
725
+ end
726
+
727
+ it_behaves_like 'falls back to a dummy model'
728
+
729
+ with_dummy_rake do
730
+ it 'warns that the unique index was missing' do
731
+ expect(model.logger)
732
+ .to have_received(:warn)
733
+ .with(a_string_matching(/missing unique index/))
734
+ end
735
+ end
736
+ end
737
+
738
+ context 'with an empty constants array' do
739
+ let(:initial_ordinal) { 9998 }
740
+ let(:initial_constant) { CONSTANTS.first }
741
+
742
+ let(:model) do
743
+ model = create_test_model(:with_empty_constants, ->(t) { t.string :name; t.index [:name], unique: true })
744
+ @prior_value = model.create!(id: initial_ordinal, name: initial_constant.to_s)
745
+ model.acts_as_enum([])
746
+ model
747
+ end
748
+
749
+ it 'looks up the existing value' do
750
+ expect(model.value_of(initial_constant)).to eq(@prior_value)
751
+ expect(model[initial_ordinal]).to eq(@prior_value)
752
+ end
753
+
754
+ it 'caches the existing value' do
755
+ expect(model.all_values).to eq([@prior_value])
756
+ end
757
+ end
758
+ end