diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-09-29 12:11:43 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-09-29 12:11:43 +0300 |
commit | c724e639a91a4d112b7f0a05b3c6a0ffa6baa7a4 (patch) | |
tree | dccd51e5f480459820f1f908ad22584e8fe8689c /spec | |
parent | cba55463a02fe6f9c9e8b6ed0b9ed38a0f087342 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
-rw-r--r-- | spec/frontend/fixtures/startup_css.rb | 4 | ||||
-rw-r--r-- | spec/frontend/issues_list/components/issues_list_app_spec.js | 30 | ||||
-rw-r--r-- | spec/frontend/issues_list/components/new_issue_dropdown_spec.js | 131 | ||||
-rw-r--r-- | spec/frontend/issues_list/mock_data.js | 34 | ||||
-rw-r--r-- | spec/helpers/issues_helper_spec.rb | 4 | ||||
-rw-r--r-- | spec/helpers/routing/pseudonymization_helper_spec.rb | 7 | ||||
-rw-r--r-- | spec/lib/gitlab/database/schema_migrations/context_spec.rb | 14 | ||||
-rw-r--r-- | spec/support/database_cleaner.rb | 27 | ||||
-rw-r--r-- | spec/support/db_cleaner.rb | 75 |
9 files changed, 274 insertions, 52 deletions
diff --git a/spec/frontend/fixtures/startup_css.rb b/spec/frontend/fixtures/startup_css.rb index 1bd99f5cd7f..067753207d2 100644 --- a/spec/frontend/fixtures/startup_css.rb +++ b/spec/frontend/fixtures/startup_css.rb @@ -18,6 +18,10 @@ RSpec.describe 'Startup CSS fixtures', type: :controller do let(:project) { create(:project, :public, :repository, description: 'Code and stuff', creator: user) } before do + # We want vNext badge to be included and com/canary don't remove/hide any other elements. + # This is why we're turning com and canary on by default for now. + allow(Gitlab).to receive(:com?).and_return(true) + allow(Gitlab).to receive(:canary?).and_return(true) sign_in(user) end diff --git a/spec/frontend/issues_list/components/issues_list_app_spec.js b/spec/frontend/issues_list/components/issues_list_app_spec.js index 8d79a5eed35..3a1f732a4e9 100644 --- a/spec/frontend/issues_list/components/issues_list_app_spec.js +++ b/spec/frontend/issues_list/components/issues_list_app_spec.js @@ -24,6 +24,7 @@ import IssuableByEmail from '~/issuable/components/issuable_by_email.vue'; import IssuableList from '~/issuable_list/components/issuable_list_root.vue'; import { IssuableListTabs, IssuableStates } from '~/issuable_list/constants'; import IssuesListApp from '~/issues_list/components/issues_list_app.vue'; +import NewIssueDropdown from '~/issues_list/components/new_issue_dropdown.vue'; import { CREATED_DESC, DUE_DATE_OVERDUE, @@ -65,6 +66,7 @@ describe('IssuesListApp component', () => { exportCsvPath: 'export/csv/path', fullPath: 'path/to/project', hasAnyIssues: true, + hasAnyProjects: true, hasBlockedIssuesFeature: true, hasIssueWeightsFeature: true, hasIterationsFeature: true, @@ -93,6 +95,7 @@ describe('IssuesListApp component', () => { const findGlEmptyState = () => wrapper.findComponent(GlEmptyState); const findGlLink = () => wrapper.findComponent(GlLink); const findIssuableList = () => wrapper.findComponent(IssuableList); + const findNewIssueDropdown = () => wrapper.findComponent(NewIssueDropdown); const mountComponent = ({ provide = {}, @@ -190,10 +193,7 @@ describe('IssuesListApp component', () => { beforeEach(() => { setWindowLocation(search); - wrapper = mountComponent({ - provide: { isSignedIn: true }, - mountFn: mount, - }); + wrapper = mountComponent({ provide: { isSignedIn: true }, mountFn: mount }); jest.runOnlyPendingTimers(); }); @@ -208,7 +208,7 @@ describe('IssuesListApp component', () => { describe('when user is not signed in', () => { it('does not render', () => { - wrapper = mountComponent({ provide: { isSignedIn: false } }); + wrapper = mountComponent({ provide: { isSignedIn: false }, mountFn: mount }); expect(findCsvImportExportButtons().exists()).toBe(false); }); @@ -216,7 +216,7 @@ describe('IssuesListApp component', () => { describe('when in a group context', () => { it('does not render', () => { - wrapper = mountComponent({ provide: { isProject: false } }); + wrapper = mountComponent({ provide: { isProject: false }, mountFn: mount }); expect(findCsvImportExportButtons().exists()).toBe(false); }); @@ -231,7 +231,7 @@ describe('IssuesListApp component', () => { }); it('does not render when user does not have permissions', () => { - wrapper = mountComponent({ provide: { canBulkUpdate: false } }); + wrapper = mountComponent({ provide: { canBulkUpdate: false }, mountFn: mount }); expect(findGlButtons().filter((button) => button.text() === 'Edit issues')).toHaveLength(0); }); @@ -258,11 +258,25 @@ describe('IssuesListApp component', () => { }); it('does not render when user does not have permissions', () => { - wrapper = mountComponent({ provide: { showNewIssueLink: false } }); + wrapper = mountComponent({ provide: { showNewIssueLink: false }, mountFn: mount }); expect(findGlButtons().filter((button) => button.text() === 'New issue')).toHaveLength(0); }); }); + + describe('new issue split dropdown', () => { + it('does not render in a project context', () => { + wrapper = mountComponent({ provide: { isProject: true }, mountFn: mount }); + + expect(findNewIssueDropdown().exists()).toBe(false); + }); + + it('renders in a group context', () => { + wrapper = mountComponent({ provide: { isProject: false }, mountFn: mount }); + + expect(findNewIssueDropdown().exists()).toBe(true); + }); + }); }); describe('initial url params', () => { diff --git a/spec/frontend/issues_list/components/new_issue_dropdown_spec.js b/spec/frontend/issues_list/components/new_issue_dropdown_spec.js new file mode 100644 index 00000000000..1fcaa99cf5a --- /dev/null +++ b/spec/frontend/issues_list/components/new_issue_dropdown_spec.js @@ -0,0 +1,131 @@ +import { GlDropdown, GlDropdownItem, GlSearchBoxByType } from '@gitlab/ui'; +import { createLocalVue, mount, shallowMount } from '@vue/test-utils'; +import VueApollo from 'vue-apollo'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import NewIssueDropdown from '~/issues_list/components/new_issue_dropdown.vue'; +import searchProjectsQuery from '~/issues_list/queries/search_projects.query.graphql'; +import { DASH_SCOPE, joinPaths } from '~/lib/utils/url_utility'; +import { + emptySearchProjectsQueryResponse, + project1, + project2, + searchProjectsQueryResponse, +} from '../mock_data'; + +describe('NewIssueDropdown component', () => { + let wrapper; + + const localVue = createLocalVue(); + localVue.use(VueApollo); + + const mountComponent = ({ + search = '', + queryResponse = searchProjectsQueryResponse, + mountFn = shallowMount, + } = {}) => { + const requestHandlers = [[searchProjectsQuery, jest.fn().mockResolvedValue(queryResponse)]]; + const apolloProvider = createMockApollo(requestHandlers); + + return mountFn(NewIssueDropdown, { + localVue, + apolloProvider, + provide: { + fullPath: 'mushroom-kingdom', + }, + data() { + return { search }; + }, + }); + }; + + const findDropdown = () => wrapper.findComponent(GlDropdown); + const findInput = () => wrapper.findComponent(GlSearchBoxByType); + const showDropdown = async () => { + findDropdown().vm.$emit('shown'); + await wrapper.vm.$apollo.queries.projects.refetch(); + jest.runOnlyPendingTimers(); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + it('renders a split dropdown', () => { + wrapper = mountComponent(); + + expect(findDropdown().props('split')).toBe(true); + }); + + it('renders a label for the dropdown toggle button', () => { + wrapper = mountComponent(); + + expect(findDropdown().attributes('toggle-text')).toBe(NewIssueDropdown.i18n.toggleButtonLabel); + }); + + it('focuses on input when dropdown is shown', async () => { + wrapper = mountComponent({ mountFn: mount }); + + const inputSpy = jest.spyOn(findInput().vm, 'focusInput'); + + await showDropdown(); + + expect(inputSpy).toHaveBeenCalledTimes(1); + }); + + it('renders expected dropdown items', async () => { + wrapper = mountComponent({ mountFn: mount }); + + await showDropdown(); + + const listItems = wrapper.findAll('li'); + + expect(listItems.at(0).text()).toBe(project1.nameWithNamespace); + expect(listItems.at(1).text()).toBe(project2.nameWithNamespace); + }); + + it('renders `No matches found` when there are no matches', async () => { + wrapper = mountComponent({ + search: 'no matches', + queryResponse: emptySearchProjectsQueryResponse, + mountFn: mount, + }); + + await showDropdown(); + + expect(wrapper.find('li').text()).toBe(NewIssueDropdown.i18n.noMatchesFound); + }); + + describe('when no project is selected', () => { + beforeEach(() => { + wrapper = mountComponent(); + }); + + it('dropdown button is not a link', () => { + expect(findDropdown().attributes('split-href')).toBeUndefined(); + }); + + it('displays default text on the dropdown button', () => { + expect(findDropdown().props('text')).toBe(NewIssueDropdown.i18n.defaultDropdownText); + }); + }); + + describe('when a project is selected', () => { + beforeEach(async () => { + wrapper = mountComponent({ mountFn: mount }); + + await showDropdown(); + + wrapper.findComponent(GlDropdownItem).vm.$emit('click', project1); + }); + + it('dropdown button is a link', () => { + const href = joinPaths(project1.webUrl, DASH_SCOPE, 'issues/new'); + + expect(findDropdown().attributes('split-href')).toBe(href); + }); + + it('displays project name on the dropdown button', () => { + expect(findDropdown().props('text')).toBe(`New issue in ${project1.name}`); + }); + }); +}); diff --git a/spec/frontend/issues_list/mock_data.js b/spec/frontend/issues_list/mock_data.js index 720f9cac986..3be256d8094 100644 --- a/spec/frontend/issues_list/mock_data.js +++ b/spec/frontend/issues_list/mock_data.js @@ -221,3 +221,37 @@ export const urlParamsWithSpecialValues = { epic_id: 'None', weight: 'None', }; + +export const project1 = { + id: 'gid://gitlab/Group/26', + name: 'Super Mario Project', + nameWithNamespace: 'Mushroom Kingdom / Super Mario Project', + webUrl: 'https://127.0.0.1:3000/mushroom-kingdom/super-mario-project', +}; + +export const project2 = { + id: 'gid://gitlab/Group/59', + name: 'Mario Kart Project', + nameWithNamespace: 'Mushroom Kingdom / Mario Kart Project', + webUrl: 'https://127.0.0.1:3000/mushroom-kingdom/mario-kart-project', +}; + +export const searchProjectsQueryResponse = { + data: { + group: { + projects: { + nodes: [project1, project2], + }, + }, + }, +}; + +export const emptySearchProjectsQueryResponse = { + data: { + group: { + projects: { + nodes: [], + }, + }, + }, +}; diff --git a/spec/helpers/issues_helper_spec.rb b/spec/helpers/issues_helper_spec.rb index f5f26d306fb..850051c7875 100644 --- a/spec/helpers/issues_helper_spec.rb +++ b/spec/helpers/issues_helper_spec.rb @@ -354,6 +354,7 @@ RSpec.describe IssuesHelper do let(:group) { create(:group) } let(:current_user) { double.as_null_object } let(:issues) { [] } + let(:projects) { [] } it 'returns expected result' do allow(helper).to receive(:current_user).and_return(current_user) @@ -367,13 +368,14 @@ RSpec.describe IssuesHelper do empty_state_svg_path: '#', full_path: group.full_path, has_any_issues: issues.to_a.any?.to_s, + has_any_projects: any_projects?(projects).to_s, is_signed_in: current_user.present?.to_s, jira_integration_path: help_page_url('integration/jira/issues', anchor: 'view-jira-issues'), rss_path: '#', sign_in_path: new_user_session_path } - expect(helper.group_issues_list_data(group, current_user, issues)).to include(expected) + expect(helper.group_issues_list_data(group, current_user, issues, projects)).to include(expected) end end diff --git a/spec/helpers/routing/pseudonymization_helper_spec.rb b/spec/helpers/routing/pseudonymization_helper_spec.rb index 68c1ce5b625..a28a86d1f53 100644 --- a/spec/helpers/routing/pseudonymization_helper_spec.rb +++ b/spec/helpers/routing/pseudonymization_helper_spec.rb @@ -6,6 +6,7 @@ RSpec.describe ::Routing::PseudonymizationHelper do let_it_be(:group) { create(:group) } let_it_be(:subgroup) { create(:group, parent: group) } let_it_be(:project) { create(:project, group: group) } + let_it_be(:subproject) { create(:project, group: subgroup) } let_it_be(:issue) { create(:issue, project: project) } let(:merge_request) { create(:merge_request, source_project: project) } @@ -56,16 +57,16 @@ RSpec.describe ::Routing::PseudonymizationHelper do end context 'with controller for groups with subgroups and project' do - let(:masked_url) { "http://test.host/namespace:#{subgroup.id}/project:#{project.id}"} + let(:masked_url) { "http://test.host/namespace:#{subgroup.id}/project:#{subproject.id}"} before do allow(helper).to receive(:group).and_return(subgroup) - allow(helper.project).to receive(:namespace).and_return(subgroup) + allow(helper).to receive(:project).and_return(subproject) allow(Rails.application.routes).to receive(:recognize_path).and_return({ controller: 'projects', action: 'show', namespace_id: subgroup.name, - id: project.name + id: subproject.name }) end diff --git a/spec/lib/gitlab/database/schema_migrations/context_spec.rb b/spec/lib/gitlab/database/schema_migrations/context_spec.rb index c9fdcdb079c..0323fa22b78 100644 --- a/spec/lib/gitlab/database/schema_migrations/context_spec.rb +++ b/spec/lib/gitlab/database/schema_migrations/context_spec.rb @@ -24,16 +24,6 @@ RSpec.describe Gitlab::Database::SchemaMigrations::Context do end context 'multiple databases', :reestablished_active_record_base do - let(:connection_class) do - Class.new(::ApplicationRecord) do - self.abstract_class = true - - def self.name - 'Gitlab::Database::SchemaMigrations::Context::TestConnection' - end - end - end - before do connection_class.establish_connection( ActiveRecord::Base @@ -44,10 +34,6 @@ RSpec.describe Gitlab::Database::SchemaMigrations::Context do ) end - after do - connection_class.remove_connection - end - context 'when `schema_migrations_path` is configured as string' do let(:configuration_overrides) do { "schema_migrations_path" => "db/ci_schema_migrations" } diff --git a/spec/support/database_cleaner.rb b/spec/support/database_cleaner.rb index b31881e3082..8f706fdebc9 100644 --- a/spec/support/database_cleaner.rb +++ b/spec/support/database_cleaner.rb @@ -17,32 +17,9 @@ RSpec.configure do |config| delete_from_all_tables!(except: ['work_item_types']) # Postgres maximum number of columns in a table is 1600 (https://github.com/postgres/postgres/blob/de41869b64d57160f58852eab20a27f248188135/src/include/access/htup_details.h#L23-L47). - # And since: - # "The DROP COLUMN form does not physically remove the column, but simply makes - # it invisible to SQL operations. Subsequent insert and update operations in the - # table will store a null value for the column. Thus, dropping a column is quick - # but it will not immediately reduce the on-disk size of your table, as the space - # occupied by the dropped column is not reclaimed. - # The space will be reclaimed over time as existing rows are updated." - # according to https://www.postgresql.org/docs/current/sql-altertable.html. # We drop and recreate the database if any table has more than 1200 columns, just to be safe. - max_allowed_columns = 1200 - tables_with_more_than_allowed_columns = - ApplicationRecord.connection.execute("SELECT attrelid::regclass::text AS table, COUNT(*) AS column_count FROM pg_attribute GROUP BY attrelid HAVING COUNT(*) > #{max_allowed_columns}") - - if tables_with_more_than_allowed_columns.any? - tables_with_more_than_allowed_columns.each do |result| - puts "The #{result['table']} table has #{result['column_count']} columns." - end - puts "Recreating the database" - start = Gitlab::Metrics::System.monotonic_time - - ActiveRecord::Tasks::DatabaseTasks.drop_current - ActiveRecord::Tasks::DatabaseTasks.create_current - ActiveRecord::Tasks::DatabaseTasks.load_schema_current - ActiveRecord::Tasks::DatabaseTasks.migrate - - puts "Database re-creation done in #{Gitlab::Metrics::System.monotonic_time - start}" + if any_connection_class_with_more_than_allowed_columns? + recreate_all_databases! end end diff --git a/spec/support/db_cleaner.rb b/spec/support/db_cleaner.rb index 940ff2751d3..316d645f99f 100644 --- a/spec/support/db_cleaner.rb +++ b/spec/support/db_cleaner.rb @@ -2,7 +2,7 @@ module DbCleaner def all_connection_classes - ::ActiveRecord::Base.connection_handler.connection_pool_names.map(&:constantize) + ::BeforeAllAdapter.all_connection_classes end def delete_from_all_tables!(except: []) @@ -20,6 +20,79 @@ module DbCleaner DatabaseCleaner[:active_record, { connection: connection_class }] end end + + def any_connection_class_with_more_than_allowed_columns? + all_connection_classes.any? do |connection_class| + more_than_allowed_columns?(connection_class) + end + end + + def more_than_allowed_columns?(connection_class) + # Postgres maximum number of columns in a table is 1600 (https://github.com/postgres/postgres/blob/de41869b64d57160f58852eab20a27f248188135/src/include/access/htup_details.h#L23-L47). + # And since: + # "The DROP COLUMN form does not physically remove the column, but simply makes + # it invisible to SQL operations. Subsequent insert and update operations in the + # table will store a null value for the column. Thus, dropping a column is quick + # but it will not immediately reduce the on-disk size of your table, as the space + # occupied by the dropped column is not reclaimed. + # The space will be reclaimed over time as existing rows are updated." + # according to https://www.postgresql.org/docs/current/sql-altertable.html. + # We drop and recreate the database if any table has more than 1200 columns, just to be safe. + max_allowed_columns = 1200 + tables_with_more_than_allowed_columns = connection_class.connection.execute(<<-SQL) + SELECT attrelid::regclass::text AS table, COUNT(*) AS column_count + FROM pg_attribute + GROUP BY attrelid + HAVING COUNT(*) > #{max_allowed_columns} + SQL + + tables_with_more_than_allowed_columns.each do |result| + puts "The #{result['table']} (#{connection_class.connection_db_config.name}) table has #{result['column_count']} columns." + end + + tables_with_more_than_allowed_columns.any? + end + + def recreate_all_databases! + start = Gitlab::Metrics::System.monotonic_time + + puts "Recreating the database" + + force_disconnect_all_connections! + + ActiveRecord::Tasks::DatabaseTasks.drop_current + ActiveRecord::Tasks::DatabaseTasks.create_current + ActiveRecord::Tasks::DatabaseTasks.load_schema_current + + # Migrate each database individually + with_reestablished_active_record_base do + all_connection_classes.each do |connection_class| + ActiveRecord::Base.establish_connection(connection_class.connection_db_config) + + ActiveRecord::Tasks::DatabaseTasks.migrate + end + end + + puts "Databases re-creation done in #{Gitlab::Metrics::System.monotonic_time - start}" + end + + def force_disconnect_all_connections! + all_connection_classes.each do |connection_class| + # We use `connection_pool` to avoid going through + # Load Balancer since it does retry ops + pool = connection_class.connection_pool + + # Force disconnect https://www.cybertec-postgresql.com/en/terminating-database-connections-in-postgresql/ + pool.connection.execute(<<-SQL) + SELECT pg_terminate_backend(pid) + FROM pg_stat_activity + WHERE datname = #{pool.connection.quote(pool.db_config.database)} + AND pid != pg_backend_pid(); + SQL + + connection_class.connection_pool.disconnect! + end + end end DbCleaner.prepend_mod_with('DbCleaner') |