diff --git a/.rubocop_todo/gitlab/namespaced_class.yml b/.rubocop_todo/gitlab/namespaced_class.yml index 925b5188c8b98a128c933935baa702650922d199..3a81130a47c6a3bd9985a78f5830f0f1e76f46a6 100644 --- a/.rubocop_todo/gitlab/namespaced_class.yml +++ b/.rubocop_todo/gitlab/namespaced_class.yml @@ -244,6 +244,7 @@ Gitlab/NamespacedClass: - 'app/models/notification_setting.rb' - 'app/models/oauth_access_grant.rb' - 'app/models/oauth_access_token.rb' + - 'app/models/organization.rb' - 'app/models/out_of_context_discussion.rb' - 'app/models/pages_deployment.rb' - 'app/models/pages_domain.rb' diff --git a/app/models/organization.rb b/app/models/organization.rb index 73a7e84305fd57de06d971980de302d85da0f6ce..cfbbbf1183ef57e94d0f55f7fe21ebf1cb93f753 100644 --- a/app/models/organization.rb +++ b/app/models/organization.rb @@ -1,6 +1,26 @@ # frozen_string_literal: true -# rubocop: disable Gitlab/NamespacedClass class Organization < ApplicationRecord + DEFAULT_ORGANIZATION_ID = 1 + + scope :without_default, -> { where.not(id: DEFAULT_ORGANIZATION_ID) } + + before_destroy :check_if_default_organization + + validates :name, + presence: true, + length: { maximum: 255 }, + uniqueness: { case_sensitive: false } + + def default? + id == DEFAULT_ORGANIZATION_ID + end + + private + + def check_if_default_organization + return unless default? + + raise ActiveRecord::RecordNotDestroyed, _('Cannot delete the default organization') + end end -# rubocop: enable Gitlab/NamespacedClass diff --git a/db/migrate/20230509085428_change_organizations_sequence.rb b/db/migrate/20230509085428_change_organizations_sequence.rb new file mode 100644 index 0000000000000000000000000000000000000000..59ec8c6e1ea78df0d6e2893eaa4f99d19efd19fb --- /dev/null +++ b/db/migrate/20230509085428_change_organizations_sequence.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +class ChangeOrganizationsSequence < Gitlab::Database::Migration[2.1] + def up + # Modify sequence for organizations.id so id '1' is never automatically taken + execute "ALTER SEQUENCE organizations_id_seq START WITH 1000 MINVALUE 1000 RESTART" + end + + def down + execute "ALTER SEQUENCE organizations_id_seq START WITH 1 MINVALUE 1" + end +end diff --git a/db/migrate/20230509115525_add_name_to_organization.rb b/db/migrate/20230509115525_add_name_to_organization.rb new file mode 100644 index 0000000000000000000000000000000000000000..d77fa84a70c9f0e75a8bc16fa9191a7a4f6f1c7f --- /dev/null +++ b/db/migrate/20230509115525_add_name_to_organization.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +# rubocop:disable Migration/AddLimitToTextColumns, Migration/AddIndex +# limit is added in 20230515111314_add_text_limit_on_organization_name.rb +class AddNameToOrganization < Gitlab::Database::Migration[2.1] + disable_ddl_transaction! + + INDEX_NAME = 'unique_organizations_on_name_lower' + + def up + add_column :organizations, :name, :text, null: false, default: '' + + add_index :organizations, 'lower(name)', name: INDEX_NAME, unique: true + end + + def down + remove_column :organizations, :name, if_exists: true + end +end +# rubocop:enable Migration/AddLimitToTextColumns, Migration/AddIndex diff --git a/db/migrate/20230509131736_add_default_organization.rb b/db/migrate/20230509131736_add_default_organization.rb new file mode 100644 index 0000000000000000000000000000000000000000..a63e7171f539881efb6db3deb4bfceabe9bf33c7 --- /dev/null +++ b/db/migrate/20230509131736_add_default_organization.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +class AddDefaultOrganization < Gitlab::Database::Migration[2.1] + restrict_gitlab_migration gitlab_schema: :gitlab_main + + class Organization < MigrationRecord + end + + def up + Organization.create(id: 1, name: 'Default') + end + + def down + Organization.where(id: 1).delete_all + end +end diff --git a/db/migrate/20230515111314_add_text_limit_on_organization_name.rb b/db/migrate/20230515111314_add_text_limit_on_organization_name.rb new file mode 100644 index 0000000000000000000000000000000000000000..c0b687fab94c4c026da6a011a25455ab6d71d849 --- /dev/null +++ b/db/migrate/20230515111314_add_text_limit_on_organization_name.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class AddTextLimitOnOrganizationName < Gitlab::Database::Migration[2.1] + disable_ddl_transaction! + + def up + add_text_limit :organizations, :name, 255 + end + + def down + remove_text_limit :organizations, :name + end +end diff --git a/db/schema_migrations/20230509085428 b/db/schema_migrations/20230509085428 new file mode 100644 index 0000000000000000000000000000000000000000..cf7214ceadcb6a0b83e0aa0e71e93e62cd2262c3 --- /dev/null +++ b/db/schema_migrations/20230509085428 @@ -0,0 +1 @@ +6179fe3d8c419c58e028fc1fe5d554678976229eff88f087beec174cb669d4ce \ No newline at end of file diff --git a/db/schema_migrations/20230509115525 b/db/schema_migrations/20230509115525 new file mode 100644 index 0000000000000000000000000000000000000000..e3c0ada40cd03795354406fc619df81093f34f6f --- /dev/null +++ b/db/schema_migrations/20230509115525 @@ -0,0 +1 @@ +92b70129d19796653569fb730be43ea6eed7dacbce224e1323124fdf03b0a0b0 \ No newline at end of file diff --git a/db/schema_migrations/20230509131736 b/db/schema_migrations/20230509131736 new file mode 100644 index 0000000000000000000000000000000000000000..593c9495eb237cc11e4ce1a0c102dafb8e008249 --- /dev/null +++ b/db/schema_migrations/20230509131736 @@ -0,0 +1 @@ +f9545a27756e5ca05220ffebcf89e8268e0231cbd8c7af0a89d13c70f5a070ec \ No newline at end of file diff --git a/db/schema_migrations/20230515111314 b/db/schema_migrations/20230515111314 new file mode 100644 index 0000000000000000000000000000000000000000..d2d7d2c94c4aaac9c1b14fe2be5fede20f0334d3 --- /dev/null +++ b/db/schema_migrations/20230515111314 @@ -0,0 +1 @@ +2a011d12459e0c21832df777569a12f4f2bbdaa5f57da7dc3823147f948d7772 \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 75087462e3a52ac294090df2ab03803ffbc82fa5..8ba5cedf74047d6b61908811d4017315642c41e1 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -19179,13 +19179,15 @@ ALTER SEQUENCE operations_user_lists_id_seq OWNED BY operations_user_lists.id; CREATE TABLE organizations ( id bigint NOT NULL, created_at timestamp with time zone NOT NULL, - updated_at timestamp with time zone NOT NULL + updated_at timestamp with time zone NOT NULL, + name text DEFAULT ''::text NOT NULL, + CONSTRAINT check_d130d769e0 CHECK ((char_length(name) <= 255)) ); CREATE SEQUENCE organizations_id_seq - START WITH 1 + START WITH 1000 INCREMENT BY 1 - NO MINVALUE + MINVALUE 1000 NO MAXVALUE CACHE 1; @@ -33170,6 +33172,8 @@ CREATE UNIQUE INDEX unique_index_on_system_note_metadata_id ON resource_link_eve CREATE UNIQUE INDEX unique_merge_request_metrics_by_merge_request_id ON merge_request_metrics USING btree (merge_request_id); +CREATE UNIQUE INDEX unique_organizations_on_name_lower ON organizations USING btree (lower(name)); + CREATE UNIQUE INDEX unique_packages_project_id_and_name_and_version_when_debian ON packages_packages USING btree (project_id, name, version) WHERE ((package_type = 9) AND (status <> 4)); CREATE UNIQUE INDEX unique_postgres_async_fk_validations_name_and_table_name ON postgres_async_foreign_key_validations USING btree (name, table_name); diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 99ac0af463ec9e8fab0ca6adcf6a286982ae998e..01e661c41e6f2ab0534577bc83438211100349a0 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -8745,6 +8745,9 @@ msgstr "" msgid "Cannot delete the default framework" msgstr "" +msgid "Cannot delete the default organization" +msgstr "" + msgid "Cannot have multiple Jira imports running at the same time" msgstr "" diff --git a/spec/factories/organizations.rb b/spec/factories/organizations.rb index a6684a8f95f7653074482f9ea26ee363ff1c4eab..7ff0493d140b293b0e35f62accbc43201c30ee42 100644 --- a/spec/factories/organizations.rb +++ b/spec/factories/organizations.rb @@ -1,5 +1,16 @@ # frozen_string_literal: true FactoryBot.define do - factory :organization + factory :organization do + sequence(:name) { |n| "Organization ##{n}" } + + trait :default do + id { Organization::DEFAULT_ORGANIZATION_ID } + name { 'Default' } + initialize_with do + # Ensure we only use one default organization + Organization.find_by(id: Organization::DEFAULT_ORGANIZATION_ID) || new(**attributes) + end + end + end end diff --git a/spec/migrations/20230509131736_add_default_organization_spec.rb b/spec/migrations/20230509131736_add_default_organization_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..539216c57eed8bd09018034c9ff9faf996e7c77a --- /dev/null +++ b/spec/migrations/20230509131736_add_default_organization_spec.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +require 'spec_helper' + +require_migration! + +RSpec.describe AddDefaultOrganization, feature_category: :cell do + let(:organization) { table(:organizations) } + + it "correctly migrates up and down" do + reversible_migration do |migration| + migration.before -> { + expect(organization.where(id: 1, name: 'Default')).to be_empty + } + migration.after -> { + expect(organization.where(id: 1, name: 'Default')).not_to be_empty + } + end + end +end diff --git a/spec/models/organization_spec.rb b/spec/models/organization_spec.rb index 9966a7132cebf4d32d5480c173176c11ab9e8927..e1aac88e6408cab8e5acde182374a7f6a6fcf203 100644 --- a/spec/models/organization_spec.rb +++ b/spec/models/organization_spec.rb @@ -2,9 +2,97 @@ require 'spec_helper' -# rubocop: disable Lint/EmptyBlock -# rubocop: disable RSpec/EmptyExampleGroup RSpec.describe Organization, type: :model, feature_category: :cell do + let_it_be(:organization) { create(:organization) } + let_it_be(:default_organization) { create(:organization, :default) } + + describe 'validations' do + subject { create(:organization) } + + it { is_expected.to validate_presence_of(:name) } + it { is_expected.to validate_uniqueness_of(:name).case_insensitive } + it { is_expected.to validate_length_of(:name).is_at_most(255) } + end + + context 'when using scopes' do + describe '.without_default' do + it 'excludes default organization' do + expect(described_class.without_default).not_to include(default_organization) + end + + it 'includes other organizations organization' do + expect(described_class.without_default).to include(organization) + end + end + end + + describe '#id' do + context 'when organization is default' do + it 'has id 1' do + expect(default_organization.id).to eq(1) + end + end + + context 'when organization is not default' do + it 'does not have id 1' do + expect(organization.id).not_to eq(1) + end + end + end + + describe '#destroy!' do + context 'when trying to delete the default organization' do + it 'raises an error' do + expect do + default_organization.destroy! + end.to raise_error(ActiveRecord::RecordNotDestroyed, _('Cannot delete the default organization')) + end + end + + context 'when trying to delete a non-default organization' do + let(:to_be_removed) { create(:organization) } + + it 'does not raise error' do + expect { to_be_removed.destroy! }.not_to raise_error + end + end + end + + describe '#destroy' do + context 'when trying to delete the default organization' do + it 'returns false' do + expect(default_organization.destroy).to eq(false) + end + end + + context 'when trying to delete a non-default organization' do + let(:to_be_removed) { create(:organization) } + + it 'returns true' do + expect(to_be_removed.destroy).to eq(to_be_removed) + end + end + end + + describe '#default?' do + context 'when organization is default' do + it 'returns true' do + expect(default_organization.default?).to eq(true) + end + end + + context 'when organization is not default' do + it 'returns false' do + expect(organization.default?).to eq(false) + end + end + end + + describe '#name' do + context 'when organization is default' do + it 'returns Default' do + expect(default_organization.name).to eq('Default') + end + end + end end -# rubocop: enable RSpec/EmptyExampleGroup -# rubocop: enable Lint/EmptyBlock diff --git a/spec/support/helpers/test_env.rb b/spec/support/helpers/test_env.rb index a53e1e1002c744da2ee6b7f0896e14f7a8256aa7..ceb567e54c433742e10bbc087fdd3312c8c9ce1e 100644 --- a/spec/support/helpers/test_env.rb +++ b/spec/support/helpers/test_env.rb @@ -372,6 +372,7 @@ def forked_repo_bundle_path def seed_db Gitlab::DatabaseImporters::WorkItems::BaseTypeImporter.upsert_types Gitlab::DatabaseImporters::WorkItems::HierarchyRestrictionsImporter.upsert_restrictions + FactoryBot.create(:organization, :default) end private