Skip to content

Commit bc8b308

Browse files
committed
Add rubocop cop to prevent model usage in migration specs
Migration specs roll back the schema before running, so model classes may reference columns that don't exist yet. Using raw Sequel operations ensures specs are independent of the current model code, as documented in spec/migrations/Readme.md. - Add Migration/NoModelInSpecs cop that flags any method call on a *Model constant receiver in spec/migrations/ - Replace model .make calls with raw db[:table].insert in four migration specs: sidecars, revision_sidecars, sidecar_process_types, and revision_process_commands
1 parent 802a98b commit bc8b308

7 files changed

Lines changed: 158 additions & 38 deletions

.rubocop_cc.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ plugins:
1111
require:
1212
- ./spec/linters/migration/add_constraint_name.rb
1313
- ./spec/linters/migration/include_string_size.rb
14+
- ./spec/linters/migration/no_model_in_specs.rb
1415
- ./spec/linters/migration/require_primary_key.rb
1516
- ./spec/linters/migration/too_many_migration_runs.rb
1617
- ./spec/linters/match_requires_with_includes.rb
@@ -67,6 +68,12 @@ Migration/IncludeStringSize: # Exclude for old Migrations
6768
Exclude:
6869
- !ruby/regexp /db/migrations/201([0-9]).+\.rb$/
6970
- !ruby/regexp /db/migrations/202([0-2]).+\.rb$/
71+
Migration/NoModelInSpecs:
72+
Include:
73+
- spec/migrations/**/*
74+
Exclude:
75+
# Uses custom tmp_migrations_dir so schema is never rolled back
76+
- spec/migrations/20190712210940_backfill_status_for_deployments_spec.rb
7077
Migration/RequirePrimaryKey: # Exclude for old Migrations
7178
Include:
7279
- db/migrations/**/*
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
module RuboCop
2+
module Cop
3+
module Migration
4+
class NoModelInSpecs < RuboCop::Cop::Base
5+
MSG = 'Do not use model classes in migration specs. ' \
6+
'Use raw Sequel operations (e.g. db[:table].insert) instead. ' \
7+
'See spec/migrations/Readme.md for details.'
8+
9+
def on_send(node)
10+
add_offense(node) if model_receiver?(node.receiver)
11+
end
12+
13+
private
14+
15+
def model_receiver?(receiver)
16+
return false unless receiver
17+
return false unless receiver.const_type?
18+
19+
name = receiver.const_name.to_s
20+
name.end_with?('Model') && name != 'Sequel::Model'
21+
end
22+
end
23+
end
24+
end
25+
end
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
require 'spec_helper'
2+
require 'rubocop'
3+
require 'rubocop/rspec/cop_helper'
4+
require 'rubocop/config'
5+
require 'linters/migration/no_model_in_specs'
6+
7+
RSpec.describe RuboCop::Cop::Migration::NoModelInSpecs do
8+
include CopHelper
9+
10+
subject(:cop) { described_class.new(RuboCop::Config.new({})) }
11+
12+
let(:message) do
13+
'Do not use model classes in migration specs. ' \
14+
'Use raw Sequel operations (e.g. db[:table].insert) instead. ' \
15+
'See spec/migrations/Readme.md for details.'
16+
end
17+
18+
it 'registers an offense for Model.make' do
19+
result = inspect_source(<<~RUBY)
20+
VCAP::CloudController::AppModel.make
21+
RUBY
22+
23+
expect(result.size).to eq(1)
24+
expect(result.map(&:message)).to eq([message])
25+
end
26+
27+
it 'registers an offense for Model.make!' do
28+
result = inspect_source(<<~RUBY)
29+
VCAP::CloudController::AppModel.make!
30+
RUBY
31+
32+
expect(result.size).to eq(1)
33+
end
34+
35+
it 'registers an offense for Model.create' do
36+
result = inspect_source(<<~RUBY)
37+
VCAP::CloudController::DeploymentModel.create(guid: 'test')
38+
RUBY
39+
40+
expect(result.size).to eq(1)
41+
end
42+
43+
it 'registers an offense for Model.where' do
44+
result = inspect_source(<<~RUBY)
45+
VCAP::CloudController::AppModel.where(guid: 'test').first
46+
RUBY
47+
48+
expect(result.size).to eq(1)
49+
end
50+
51+
it 'does not register an offense for raw Sequel inserts' do
52+
result = inspect_source(<<~RUBY)
53+
db[:apps].insert(guid: 'test', name: 'test-app')
54+
RUBY
55+
56+
expect(result.size).to eq(0)
57+
end
58+
59+
it 'does not register an offense for Sequel::Model' do
60+
result = inspect_source(<<~RUBY)
61+
Sequel::Model.db
62+
RUBY
63+
64+
expect(result.size).to eq(0)
65+
end
66+
67+
it 'does not register an offense for non-Model constants' do
68+
result = inspect_source(<<~RUBY)
69+
SecureRandom.uuid
70+
RUBY
71+
72+
expect(result.size).to eq(0)
73+
end
74+
end

spec/migrations/20251030100000_add_unique_constraint_to_sidecar_process_types_spec.rb

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,22 +6,27 @@
66
let(:migration_filename) { '20251030100000_add_unique_constraint_to_sidecar_process_types.rb' }
77
end
88

9-
let!(:app) { VCAP::CloudController::AppModel.make }
10-
let!(:sidecar) { VCAP::CloudController::SidecarModel.make(app:) }
11-
let!(:revision) { VCAP::CloudController::RevisionModel.make(app:) }
12-
let!(:revision_sidecar) { VCAP::CloudController::RevisionSidecarModel.make(revision:) }
9+
let(:app_guid) { SecureRandom.uuid }
10+
let(:sidecar_guid) { SecureRandom.uuid }
11+
let(:revision_guid) { SecureRandom.uuid }
12+
let(:revision_sidecar_guid) { SecureRandom.uuid }
1313

1414
it 'removes duplicates, adds unique constraints, and is reversible' do
15+
db[:apps].insert(guid: app_guid, name: 'test-app')
16+
db[:sidecars].insert(guid: sidecar_guid, name: 'test-sidecar', command: 'command', app_guid: app_guid)
17+
db[:revisions].insert(guid: revision_guid, app_guid: app_guid)
18+
db[:revision_sidecars].insert(guid: revision_sidecar_guid, name: 'test-sidecar', command: 'command', revision_guid: revision_guid)
19+
1520
# =========================================================================================
1621
# SETUP: Create duplicate entries for both tables to test the de-duplication logic.
1722
# =========================================================================================
18-
db[:sidecar_process_types].insert(sidecar_guid: sidecar.guid, type: 'web', app_guid: app.guid, guid: SecureRandom.uuid)
19-
db[:sidecar_process_types].insert(sidecar_guid: sidecar.guid, type: 'web', app_guid: app.guid, guid: SecureRandom.uuid)
20-
expect(db[:sidecar_process_types].where(sidecar_guid: sidecar.guid, type: 'web').count).to eq(2)
23+
db[:sidecar_process_types].insert(sidecar_guid: sidecar_guid, type: 'web', app_guid: app_guid, guid: SecureRandom.uuid)
24+
db[:sidecar_process_types].insert(sidecar_guid: sidecar_guid, type: 'web', app_guid: app_guid, guid: SecureRandom.uuid)
25+
expect(db[:sidecar_process_types].where(sidecar_guid: sidecar_guid, type: 'web').count).to eq(2)
2126

22-
db[:revision_sidecar_process_types].insert(revision_sidecar_guid: revision_sidecar.guid, type: 'worker', guid: SecureRandom.uuid)
23-
db[:revision_sidecar_process_types].insert(revision_sidecar_guid: revision_sidecar.guid, type: 'worker', guid: SecureRandom.uuid)
24-
expect(db[:revision_sidecar_process_types].where(revision_sidecar_guid: revision_sidecar.guid, type: 'worker').count).to eq(2)
27+
db[:revision_sidecar_process_types].insert(revision_sidecar_guid: revision_sidecar_guid, type: 'worker', guid: SecureRandom.uuid)
28+
db[:revision_sidecar_process_types].insert(revision_sidecar_guid: revision_sidecar_guid, type: 'worker', guid: SecureRandom.uuid)
29+
expect(db[:revision_sidecar_process_types].where(revision_sidecar_guid: revision_sidecar_guid, type: 'worker').count).to eq(2)
2530

2631
# =========================================================================================
2732
# UP MIGRATION: Run the migration to apply the unique constraints.
@@ -31,13 +36,13 @@
3136
# =========================================================================================
3237
# ASSERT UP MIGRATION: Verify that duplicates are removed and constraints are enforced.
3338
# =========================================================================================
34-
expect(db[:sidecar_process_types].where(sidecar_guid: sidecar.guid, type: 'web').count).to eq(1)
39+
expect(db[:sidecar_process_types].where(sidecar_guid: sidecar_guid, type: 'web').count).to eq(1)
3540
expect(db.indexes(:sidecar_process_types)).to include(:sidecar_process_types_sidecar_guid_type_index)
36-
expect { db[:sidecar_process_types].insert(sidecar_guid: sidecar.guid, type: 'web', app_guid: app.guid, guid: SecureRandom.uuid) }.to raise_error(Sequel::UniqueConstraintViolation)
41+
expect { db[:sidecar_process_types].insert(sidecar_guid: sidecar_guid, type: 'web', app_guid: app_guid, guid: SecureRandom.uuid) }.to raise_error(Sequel::UniqueConstraintViolation)
3742

38-
expect(db[:revision_sidecar_process_types].where(revision_sidecar_guid: revision_sidecar.guid, type: 'worker').count).to eq(1)
43+
expect(db[:revision_sidecar_process_types].where(revision_sidecar_guid: revision_sidecar_guid, type: 'worker').count).to eq(1)
3944
expect(db.indexes(:revision_sidecar_process_types)).to include(:revision_sidecar_process_types_revision_sidecar_guid_type_index)
40-
expect { db[:revision_sidecar_process_types].insert(revision_sidecar_guid: revision_sidecar.guid, type: 'worker', guid: SecureRandom.uuid) }.to raise_error(Sequel::UniqueConstraintViolation)
45+
expect { db[:revision_sidecar_process_types].insert(revision_sidecar_guid: revision_sidecar_guid, type: 'worker', guid: SecureRandom.uuid) }.to raise_error(Sequel::UniqueConstraintViolation)
4146

4247
# =========================================================================================
4348
# TEST IDEMPOTENCY: Running the migration again should not cause any errors.
@@ -53,9 +58,9 @@
5358
# ASSERT DOWN MIGRATION: Verify that constraints are removed and duplicates can be re-inserted.
5459
# =========================================================================================
5560
expect(db.indexes(:sidecar_process_types)).not_to include(:sidecar_process_types_sidecar_guid_type_index)
56-
expect { db[:sidecar_process_types].insert(sidecar_guid: sidecar.guid, type: 'web', app_guid: app.guid, guid: SecureRandom.uuid) }.not_to raise_error
61+
expect { db[:sidecar_process_types].insert(sidecar_guid: sidecar_guid, type: 'web', app_guid: app_guid, guid: SecureRandom.uuid) }.not_to raise_error
5762

5863
expect(db.indexes(:revision_sidecar_process_types)).not_to include(:revision_sidecar_process_types_revision_sidecar_guid_type_index)
59-
expect { db[:revision_sidecar_process_types].insert(revision_sidecar_guid: revision_sidecar.guid, type: 'worker', guid: SecureRandom.uuid) }.not_to raise_error
64+
expect { db[:revision_sidecar_process_types].insert(revision_sidecar_guid: revision_sidecar_guid, type: 'worker', guid: SecureRandom.uuid) }.not_to raise_error
6065
end
6166
end

spec/migrations/20260320141005_add_unique_constraint_to_revision_sidecars_spec.rb

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,22 +5,25 @@
55
let(:migration_filename) { '20260320141005_add_unique_constraint_to_revision_sidecars.rb' }
66
end
77

8-
let!(:app) { VCAP::CloudController::AppModel.make }
9-
let!(:revision) { VCAP::CloudController::RevisionModel.make(:app) }
8+
let(:app_guid) { SecureRandom.uuid }
9+
let(:revision_guid) { SecureRandom.uuid }
1010

1111
it 'remove duplicates, add constraint and revert migration' do
12+
db[:apps].insert(guid: app_guid, name: 'test-app')
13+
db[:revisions].insert(guid: revision_guid, app_guid: app_guid)
14+
1215
# create duplicate entries
13-
db[:revision_sidecars].insert(guid: SecureRandom.uuid, name: 'app', command: 'command', revision_guid: revision.guid)
14-
db[:revision_sidecars].insert(guid: SecureRandom.uuid, name: 'app', command: 'command', revision_guid: revision.guid)
15-
expect(db[:revision_sidecars].where(name: 'app', revision_guid: revision.guid).count).to eq(2)
16+
db[:revision_sidecars].insert(guid: SecureRandom.uuid, name: 'app', command: 'command', revision_guid: revision_guid)
17+
db[:revision_sidecars].insert(guid: SecureRandom.uuid, name: 'app', command: 'command', revision_guid: revision_guid)
18+
expect(db[:revision_sidecars].where(name: 'app', revision_guid: revision_guid).count).to eq(2)
1619

1720
# run the migration
1821
Sequel::Migrator.run(db, migrations_path, target: current_migration_index, allow_missing_migration_files: true)
1922

2023
# verify duplicates are removed and constraint is enforced
21-
expect(db[:revision_sidecars].where(name: 'app', revision_guid: revision.guid).count).to eq(1)
24+
expect(db[:revision_sidecars].where(name: 'app', revision_guid: revision_guid).count).to eq(1)
2225
expect(db.indexes(:revision_sidecars)).to include(:revision_sidecars_revision_guid_name_index)
23-
expect { db[:revision_sidecars].insert(guid: SecureRandom.uuid, name: 'app', command: 'command', revision_guid: revision.guid) }.to raise_error(Sequel::UniqueConstraintViolation)
26+
expect { db[:revision_sidecars].insert(guid: SecureRandom.uuid, name: 'app', command: 'command', revision_guid: revision_guid) }.to raise_error(Sequel::UniqueConstraintViolation)
2427

2528
# running the migration again should not cause any errors
2629
expect { Sequel::Migrator.run(db, migrations_path, target: current_migration_index, allow_missing_migration_files: true) }.not_to raise_error
@@ -30,6 +33,6 @@
3033

3134
# verify constraint is removed and duplicates can be re-inserted
3235
expect(db.indexes(:revision_sidecars)).not_to include(:revision_sidecars_revision_guid_name_index)
33-
expect { db[:revision_sidecars].insert(guid: SecureRandom.uuid, name: 'app', command: 'command', revision_guid: revision.guid) }.not_to raise_error
36+
expect { db[:revision_sidecars].insert(guid: SecureRandom.uuid, name: 'app', command: 'command', revision_guid: revision_guid) }.not_to raise_error
3437
end
3538
end

spec/migrations/20260323092954_add_unique_constraint_to_sidecars_spec.rb

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,23 @@
55
let(:migration_filename) { '20260323092954_add_unique_constraint_to_sidecars.rb' }
66
end
77

8-
let!(:app) { VCAP::CloudController::AppModel.make }
8+
let(:app_guid) { SecureRandom.uuid }
99

1010
it 'remove duplicates, add constraint and revert migration' do
11+
db[:apps].insert(guid: app_guid, name: 'test-app')
12+
1113
# create duplicate entries
12-
db[:sidecars].insert(guid: SecureRandom.uuid, name: 'app', command: 'command', app_guid: app.guid)
13-
db[:sidecars].insert(guid: SecureRandom.uuid, name: 'app', command: 'command', app_guid: app.guid)
14-
expect(db[:sidecars].where(name: 'app', app_guid: app.guid).count).to eq(2)
14+
db[:sidecars].insert(guid: SecureRandom.uuid, name: 'app', command: 'command', app_guid: app_guid)
15+
db[:sidecars].insert(guid: SecureRandom.uuid, name: 'app', command: 'command', app_guid: app_guid)
16+
expect(db[:sidecars].where(name: 'app', app_guid: app_guid).count).to eq(2)
1517

1618
# run the migration
1719
Sequel::Migrator.run(db, migrations_path, target: current_migration_index, allow_missing_migration_files: true)
1820

1921
# verify duplicates are removed and constraint is enforced
20-
expect(db[:sidecars].where(name: 'app', app_guid: app.guid).count).to eq(1)
22+
expect(db[:sidecars].where(name: 'app', app_guid: app_guid).count).to eq(1)
2123
expect(db.indexes(:sidecars)).to include(:sidecars_app_guid_name_index)
22-
expect { db[:sidecars].insert(guid: SecureRandom.uuid, name: 'app', command: 'command', app_guid: app.guid) }.to raise_error(Sequel::UniqueConstraintViolation)
24+
expect { db[:sidecars].insert(guid: SecureRandom.uuid, name: 'app', command: 'command', app_guid: app_guid) }.to raise_error(Sequel::UniqueConstraintViolation)
2325

2426
# running the migration again should not cause any errors
2527
expect { Sequel::Migrator.run(db, migrations_path, target: current_migration_index, allow_missing_migration_files: true) }.not_to raise_error
@@ -29,6 +31,6 @@
2931

3032
# verify constraint is removed and duplicates can be re-inserted
3133
expect(db.indexes(:sidecars)).not_to include(:sidecars_app_guid_name_index)
32-
expect { db[:sidecars].insert(guid: SecureRandom.uuid, name: 'app', command: 'command', app_guid: app.guid) }.not_to raise_error
34+
expect { db[:sidecars].insert(guid: SecureRandom.uuid, name: 'app', command: 'command', app_guid: app_guid) }.not_to raise_error
3335
end
3436
end

spec/migrations/20260323144429_add_unique_constraint_to_revision_process_commands_spec.rb

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,22 +6,26 @@
66
let(:migration_filename) { '20260323144429_add_unique_constraint_to_revision_process_commands.rb' }
77
end
88

9-
let!(:revision) { VCAP::CloudController::RevisionModel.make }
9+
let(:app_guid) { SecureRandom.uuid }
10+
let(:revision_guid) { SecureRandom.uuid }
1011

1112
it 'removes duplicates, adds constraint and reverts migration' do
13+
db[:apps].insert(guid: app_guid, name: 'test-app')
14+
db[:revisions].insert(guid: revision_guid, app_guid: app_guid)
15+
1216
# create duplicate entries
13-
db[:revision_process_commands].insert(guid: SecureRandom.uuid, revision_guid: revision.guid, process_type: 'worker')
14-
db[:revision_process_commands].insert(guid: SecureRandom.uuid, revision_guid: revision.guid, process_type: 'worker')
15-
expect(db[:revision_process_commands].where(revision_guid: revision.guid, process_type: 'worker').count).to eq(2)
17+
db[:revision_process_commands].insert(guid: SecureRandom.uuid, revision_guid: revision_guid, process_type: 'worker')
18+
db[:revision_process_commands].insert(guid: SecureRandom.uuid, revision_guid: revision_guid, process_type: 'worker')
19+
expect(db[:revision_process_commands].where(revision_guid: revision_guid, process_type: 'worker').count).to eq(2)
1620

1721
# run the migration
1822
Sequel::Migrator.run(db, migrations_path, target: current_migration_index, allow_missing_migration_files: true)
1923

2024
# verify duplicates are removed and constraint is enforced
21-
expect(db[:revision_process_commands].where(revision_guid: revision.guid, process_type: 'worker').count).to eq(1)
25+
expect(db[:revision_process_commands].where(revision_guid: revision_guid, process_type: 'worker').count).to eq(1)
2226
expect(db.indexes(:revision_process_commands)).to include(:revision_process_commands_revision_guid_process_type_index)
2327
expect do
24-
db[:revision_process_commands].insert(guid: SecureRandom.uuid, revision_guid: revision.guid, process_type: 'worker')
28+
db[:revision_process_commands].insert(guid: SecureRandom.uuid, revision_guid: revision_guid, process_type: 'worker')
2529
end.to raise_error(Sequel::UniqueConstraintViolation)
2630

2731
# running the migration again should not cause any errors
@@ -33,7 +37,7 @@
3337
# verify constraint is removed and duplicates can be re-inserted
3438
expect(db.indexes(:revision_process_commands)).not_to include(:revision_process_commands_revision_guid_process_type_index)
3539
expect do
36-
db[:revision_process_commands].insert(guid: SecureRandom.uuid, revision_guid: revision.guid, process_type: 'worker')
40+
db[:revision_process_commands].insert(guid: SecureRandom.uuid, revision_guid: revision_guid, process_type: 'worker')
3741
end.not_to raise_error
3842
end
3943
end

0 commit comments

Comments
 (0)