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
|
# frozen_string_literal: true
require 'spec_helper'
require_migration!('add_projects_foreign_key_to_namespaces')
require_migration!
# In order to test the CleanupProjectsWithMissingNamespace migration, we need
# to first create an orphaned project (one with an invalid namespace_id)
# and then run the migration to check that the project was properly cleaned up
#
# The problem is that the CleanupProjectsWithMissingNamespace migration comes
# after the FK has been added with a previous migration (AddProjectsForeignKeyToNamespaces)
# That means that while testing the current class we can not insert projects with an
# invalid namespace_id as the existing FK is correctly blocking us from doing so
#
# The approach that solves that problem is to:
# - Set the schema of this test to the one prior to AddProjectsForeignKeyToNamespaces
# - We could hardcode it to `20200508091106` (which currently is the previous
# migration before adding the FK) but that would mean that this test depends
# on migration 20200508091106 not being reverted or deleted
# - So, we use SchemaVersionFinder that finds the previous migration and returns
# its schema, which we then use in the describe
#
# That means that we lock the schema version to the one returned by
# SchemaVersionFinder.previous_migration and only test the cleanup migration
# *without* the migration that adds the Foreign Key ever running
# That's acceptable as the cleanup script should not be affected in any way
# by the migration that adds the Foreign Key
class SchemaVersionFinder
def self.migrations_paths
ActiveRecord::Migrator.migrations_paths
end
def self.migration_context
ActiveRecord::MigrationContext.new(migrations_paths, ActiveRecord::SchemaMigration)
end
def self.migrations
migration_context.migrations
end
def self.previous_migration
migrations.each_cons(2) do |previous, migration|
break previous.version if migration.name == AddProjectsForeignKeyToNamespaces.name
end
end
end
RSpec.describe CleanupProjectsWithMissingNamespace, :migration, schema: SchemaVersionFinder.previous_migration do
let(:projects) { table(:projects) }
let(:namespaces) { table(:namespaces) }
let(:users) { table(:users) }
before do
namespace = namespaces.create!(name: 'existing_namespace', path: 'existing_namespace')
projects.create!(
name: 'project_with_existing_namespace',
path: 'project_with_existing_namespace',
visibility_level: 20,
archived: false,
namespace_id: namespace.id
)
projects.create!(
name: 'project_with_non_existing_namespace',
path: 'project_with_non_existing_namespace',
visibility_level: 20,
archived: false,
namespace_id: non_existing_record_id
)
end
it 'creates the ghost user' do
expect(users.where(user_type: described_class::User::USER_TYPE_GHOST).count).to eq(0)
disable_migrations_output { migrate! }
expect(users.where(user_type: described_class::User::USER_TYPE_GHOST).count).to eq(1)
end
it 'creates the lost-and-found group, owned by the ghost user' do
expect(
described_class::Group.where(
described_class::Group
.arel_table[:name]
.matches("#{described_class::User::LOST_AND_FOUND_GROUP}%")
).count
).to eq(0)
disable_migrations_output { migrate! }
ghost_user = users.find_by(user_type: described_class::User::USER_TYPE_GHOST)
expect(
described_class::Group
.joins('INNER JOIN members ON namespaces.id = members.source_id')
.where(namespaces: { type: 'Group' })
.where(members: { type: 'GroupMember' })
.where(members: { source_type: 'Namespace' })
.where(members: { user_id: ghost_user.id })
.where(members: { requested_at: nil })
.where(members: { access_level: described_class::ACCESS_LEVEL_OWNER })
.where(
described_class::Group
.arel_table[:name]
.matches("#{described_class::User::LOST_AND_FOUND_GROUP}%")
)
.count
).to eq(1)
end
it 'moves the orphaned project to the lost-and-found group' do
orphaned_project = projects.find_by(name: 'project_with_non_existing_namespace')
expect(orphaned_project.visibility_level).to eq(20)
expect(orphaned_project.archived).to eq(false)
expect(orphaned_project.namespace_id).to eq(non_existing_record_id)
disable_migrations_output { migrate! }
lost_and_found_group = described_class::Group.find_by(
described_class::Group
.arel_table[:name]
.matches("#{described_class::User::LOST_AND_FOUND_GROUP}%")
)
orphaned_project = projects.find_by(id: orphaned_project.id)
expect(orphaned_project.visibility_level).to eq(0)
expect(orphaned_project.namespace_id).to eq(lost_and_found_group.id)
expect(orphaned_project.name).to eq("project_with_non_existing_namespace_#{orphaned_project.id}")
expect(orphaned_project.path).to eq("project_with_non_existing_namespace_#{orphaned_project.id}")
expect(orphaned_project.archived).to eq(true)
valid_project = projects.find_by(name: 'project_with_existing_namespace')
existing_namespace = namespaces.find_by(name: 'existing_namespace')
expect(valid_project.visibility_level).to eq(20)
expect(valid_project.namespace_id).to eq(existing_namespace.id)
expect(valid_project.path).to eq('project_with_existing_namespace')
expect(valid_project.archived).to eq(false)
end
end
|