diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/additional_metadata.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/additional_metadata.vue index 059f038b6e7..3595f83baf0 100644 --- a/app/assets/javascripts/packages_and_registries/package_registry/components/details/additional_metadata.vue +++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/additional_metadata.vue @@ -6,6 +6,7 @@ import { PACKAGE_TYPE_CONAN, PACKAGE_TYPE_MAVEN, PACKAGE_TYPE_COMPOSER, + PACKAGE_TYPE_PYPI, } from '~/packages_and_registries/package_registry/constants'; import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; import DetailsRow from '~/vue_shared/components/registry/details_row.vue'; @@ -22,6 +23,7 @@ export default { composerJson: s__( 'PackageRegistry|Composer.json with license: %{license} and version: %{version}', ), + requiredPython: s__('PackageRegistry|Required Python: %{pythonVersion}'), }, components: { DetailsRow, @@ -43,6 +45,7 @@ export default { PACKAGE_TYPE_CONAN, PACKAGE_TYPE_MAVEN, PACKAGE_TYPE_COMPOSER, + PACKAGE_TYPE_PYPI, ].includes(this.packageEntity.packageType) && this.packageEntity.metadata ); }, @@ -58,6 +61,9 @@ export default { showComposerMetadata() { return this.packageEntity.packageType === PACKAGE_TYPE_COMPOSER; }, + showPypiMetadata() { + return this.packageEntity.packageType === PACKAGE_TYPE_PYPI; + }, }, }; @@ -141,6 +147,19 @@ export default { + + + + + + diff --git a/app/models/hooks/web_hook.rb b/app/models/hooks/web_hook.rb index 9a78fe3971c..cb5c1ac48cd 100644 --- a/app/models/hooks/web_hook.rb +++ b/app/models/hooks/web_hook.rb @@ -80,6 +80,8 @@ class WebHook < ApplicationRecord end def backoff! + return if backoff_count >= MAX_FAILURES && disabled_until && disabled_until > Time.current + assign_attributes(disabled_until: next_backoff.from_now, backoff_count: backoff_count.succ.clamp(0, MAX_FAILURES)) save(validate: false) end diff --git a/app/models/work_item/type.rb b/app/models/work_item/type.rb index 03badf95908..7038beadd62 100644 --- a/app/models/work_item/type.rb +++ b/app/models/work_item/type.rb @@ -34,6 +34,14 @@ class WorkItem::Type < ApplicationRecord validates :name, length: { maximum: 255 } validates :icon_name, length: { maximum: 255 } + def self.default_by_type(type) + find_by(namespace_id: nil, base_type: type) + end + + def self.default_issue_type + default_by_type(:issue) + end + private def strip_whitespace diff --git a/app/services/issues/base_service.rb b/app/services/issues/base_service.rb index 525447e1377..6dce9fd6e73 100644 --- a/app/services/issues/base_service.rb +++ b/app/services/issues/base_service.rb @@ -34,6 +34,13 @@ module Issues private + def find_work_item_type_id(issue_type) + work_item_type = WorkItem::Type.default_by_type(issue_type) + work_item_type ||= WorkItem::Type.default_issue_type + + work_item_type.id + end + def filter_params(issue) super diff --git a/app/services/issues/build_service.rb b/app/services/issues/build_service.rb index 5cb138946d7..7fdc8daf15c 100644 --- a/app/services/issues/build_service.rb +++ b/app/services/issues/build_service.rb @@ -6,6 +6,7 @@ module Issues def execute filter_resolve_discussion_params + @issue = project.issues.new(issue_params).tap do |issue| ensure_milestone_available(issue) end @@ -60,6 +61,13 @@ module Issues def issue_params @issue_params ||= build_issue_params + + # If :issue_type is nil then params[:issue_type] was either nil + # or not permitted. Either way, the :issue_type will default + # to the column default of `issue`. And that means we need to + # ensure the work_item_type_id is set + @issue_params[:work_item_type_id] = get_work_item_type_id(@issue_params[:issue_type]) + @issue_params end private @@ -81,6 +89,11 @@ module Issues { author: current_user } .merge(issue_params_with_info_from_discussions) .merge(allowed_issue_params) + .with_indifferent_access + end + + def get_work_item_type_id(issue_type = :issue) + find_work_item_type_id(issue_type) end end end diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb index 6ee94e014d8..d120b007af2 100644 --- a/app/services/issues/update_service.rb +++ b/app/services/issues/update_service.rb @@ -26,6 +26,8 @@ module Issues end def before_update(issue, skip_spam_check: false) + change_work_item_type(issue) + return if skip_spam_check Spam::SpamActionService.new( @@ -36,6 +38,14 @@ module Issues ).execute end + def change_work_item_type(issue) + return unless issue.changed_attributes['issue_type'] + + type_id = find_work_item_type_id(issue.issue_type) + + issue.work_item_type_id = type_id + end + def handle_changes(issue, options) super old_associations = options.fetch(:old_associations, {}) diff --git a/doc/administration/geo/setup/database.md b/doc/administration/geo/setup/database.md index 05aa026eca1..d72bb0737ae 100644 --- a/doc/administration/geo/setup/database.md +++ b/doc/administration/geo/setup/database.md @@ -501,6 +501,79 @@ two other clusters of nodes supporting a Geo **secondary** site. One for the main database and the other for the tracking database. For more information, see [High Availability with Omnibus GitLab](../../postgresql/replication_and_failover.md). +### Changing the replication password + +To change the password for the [replication user](https://wiki.postgresql.org/wiki/Streaming_Replication) +when using Omnibus-managed PostgreSQL instances: + +On the GitLab Geo **primary** server: + +1. The default value for the replication user is `gitlab_replicator`, but if you've set a custom replication + user in your `/etc/gitlab/gitlab.rb` under the `postgresql['sql_replication_user']` setting, make sure to + adapt the following instructions for your own user. + + Generate an MD5 hash of the desired password: + + ```shell + sudo gitlab-ctl pg-password-md5 gitlab_replicator + # Enter password: + # Confirm password: + # 950233c0dfc2f39c64cf30457c3b7f1e + ``` + + Edit `/etc/gitlab/gitlab.rb`: + + ```ruby + # Fill with the hash generated by `gitlab-ctl pg-password-md5 gitlab_replicator` + postgresql['sql_replication_password'] = '' + ``` + +1. Save the file and reconfigure GitLab to change the replication user's password in PostgreSQL: + + ```shell + sudo gitlab-ctl reconfigure + ``` + +1. Restart PostgreSQL for the replication password change to take effect: + + ```shell + sudo gitlab-ctl restart postgresql + ``` + +Until the password is updated on any **secondary** servers, the [PostgreSQL log](../../logs.md#postgresql-logs) on +the secondaries will report the following error message: + +```console +FATAL: could not connect to the primary server: FATAL: password authentication failed for user "gitlab_replicator" +``` + +On all GitLab Geo **secondary** servers: + +1. The first step isn't necessary from a configuration perspective, since the hashed `'sql_replication_password'` + is not used on the GitLab Geo **secondary**. However in the event that **secondary** needs to be promoted + to the GitLab Geo **primary**, make sure to match the `'sql_replication_password'` in the secondary + server configuration. + + Edit `/etc/gitlab/gitlab.rb`: + + ```ruby + # Fill with the hash generated by `gitlab-ctl pg-password-md5 gitlab_replicator` on the Geo primary + postgresql['sql_replication_password'] = '' + ``` + +1. During the initial replication setup, the `gitlab-ctl replicate-geo-database` command writes the plaintext + password for the replication user account to two locations: + + - `gitlab-geo.conf`: Used by the PostgreSQL replication process, written to the PostgreSQL data + directory, by default at `/var/opt/gitlab/postgresql/data/gitlab-geo.conf`. + - `.pgpass`: Used by the `gitlab-psql` user, located by default at `/var/opt/gitlab/postgresql/.pgpass`. + + Update the plaintext password in both of these files, and restart PostgreSQL: + + ```shell + sudo gitlab-ctl restart postgresql + ``` + ## Multi-node database replication In GitLab 14.0, Patroni replaced `repmgr` as the supported diff --git a/doc/development/i18n/externalization.md b/doc/development/i18n/externalization.md index c383a1cac73..52a7f839286 100644 --- a/doc/development/i18n/externalization.md +++ b/doc/development/i18n/externalization.md @@ -790,6 +790,28 @@ translate correctly if you extract individual words from the sentence. When in doubt, try to follow the best practices described in this [Mozilla Developer documentation](https://developer.mozilla.org/en-US/docs/Mozilla/Localization/Localization_content_best_practices#Splitting). +### Always pass string literals to the translation helpers + +The `bin/rake gettext:regenerate` script parses the codebase and extracts all the strings from the +[translation helpers](#preparing-a-page-for-translation) ready to be translated. + +The script cannot resolve the strings if they are passed as variables or function calls. Therefore, +make sure to always pass string literals to the helpers. + +```javascript +// Good +__('Some label'); +s__('Namespace', 'Label'); +s__('Namespace|Label'); +n__('%d apple', '%d apples', appleCount); + +// Bad +__(LABEL); +s__(getLabel()); +s__(NAMESPACE, LABEL); +n__(LABEL_SINGULAR, LABEL_PLURAL, appleCount); +``` + ## Updating the PO files with the new content Now that the new content is marked for translation, run this command to update the diff --git a/doc/integration/elasticsearch.md b/doc/integration/elasticsearch.md index 9842881094b..5a08a1fe7f3 100644 --- a/doc/integration/elasticsearch.md +++ b/doc/integration/elasticsearch.md @@ -902,25 +902,20 @@ Elasticsearch::Transport::Transport::Errors::BadRequest([400] { This is because we changed the index mapping in GitLab 8.12 and the old indexes should be removed and built from scratch again, see details in the [update guide](../update/upgrading_from_source.md). -- Exception `Elasticsearch::Transport::Transport::Errors::BadRequest` +### `Elasticsearch::Transport::Transport::Errors::BadRequest` - If you have this exception (just like in the case above but the actual message is different) please check if you have the correct Elasticsearch version and you met the other [requirements](#system-requirements). - There is also an easy way to check it automatically with `sudo gitlab-rake gitlab:check` command. +If you have this exception (just like in the case above but the actual message is different) please check if you have the correct Elasticsearch version and you met the other [requirements](#system-requirements). +There is also an easy way to check it automatically with `sudo gitlab-rake gitlab:check` command. -- Exception `Elasticsearch::Transport::Transport::Errors::RequestEntityTooLarge` +### `Elasticsearch::Transport::Transport::Errors::RequestEntityTooLarge` - ```plaintext - [413] {"Message":"Request size exceeded 10485760 bytes"} - ``` +```plaintext +[413] {"Message":"Request size exceeded 10485760 bytes"} +``` - This exception is seen when your Elasticsearch cluster is configured to reject - requests above a certain size (10MiB in this case). This corresponds to the - `http.max_content_length` setting in `elasticsearch.yml`. Increase it to a - larger size and restart your Elasticsearch cluster. +This exception is seen when your Elasticsearch cluster is configured to reject requests above a certain size (10MiB in this case). This corresponds to the `http.max_content_length` setting in `elasticsearch.yml`. Increase it to a larger size and restart your Elasticsearch cluster. - AWS has [fixed limits](https://docs.aws.amazon.com/elasticsearch-service/latest/developerguide/aes-limits.html) - for this setting ("Maximum Size of HTTP Request Payloads"), based on the size of - the underlying instance. +AWS has [fixed limits](https://docs.aws.amazon.com/elasticsearch-service/latest/developerguide/aes-limits.html) for this setting ("Maximum Size of HTTP Request Payloads"), based on the size of the underlying instance. ### My single node Elasticsearch cluster status never goes from `yellow` to `green` even though everything seems to be running properly diff --git a/doc/integration/jira/jira_cloud_configuration.md b/doc/integration/jira/jira_cloud_configuration.md index e42a102e030..0cfffdb8ba4 100644 --- a/doc/integration/jira/jira_cloud_configuration.md +++ b/doc/integration/jira/jira_cloud_configuration.md @@ -11,10 +11,10 @@ on Atlassian cloud. To create the API token: 1. Sign in to [`id.atlassian.com`](https://id.atlassian.com/manage-profile/security/api-tokens) with your email address. Use an account with *write* access to Jira projects. -1. Go to **Settings > API tokens**. +1. Go to **Settings > Atlassian account settings > Security > Create and manage API tokens**. 1. Select **Create API token** to display a modal window with an API token. -1. To copy the API token, select **Copy to clipboard**, or select **View** and write - down the new API token. You need this value when you +1. In the dialog, enter a label for your token and select **Create**. +1. To copy the API token, select **Copy**, then paste the token somewhere safe. You need this value when you [configure GitLab](configure.md). You need the newly created token, and the email diff --git a/lib/gitlab/issuables_count_for_state.rb b/lib/gitlab/issuables_count_for_state.rb index 660877b964a..f11dd520d2d 100644 --- a/lib/gitlab/issuables_count_for_state.rb +++ b/lib/gitlab/issuables_count_for_state.rb @@ -6,7 +6,7 @@ module Gitlab # The name of the Gitlab::SafeRequestStore cache key. CACHE_KEY = :issuables_count_for_state # The expiration time for the Rails cache. - CACHE_EXPIRES_IN = 10.minutes + CACHE_EXPIRES_IN = 1.hour THRESHOLD = 1000 # The state values that can be safely casted to a Symbol. diff --git a/lib/gitlab/saas.rb b/lib/gitlab/saas.rb index 8d9d8415cb1..4066c8a7906 100644 --- a/lib/gitlab/saas.rb +++ b/lib/gitlab/saas.rb @@ -20,6 +20,10 @@ module Gitlab def self.dev_url 'https://dev.gitlab.org' end + + def self.registry_prefix + 'registry.gitlab.com' + end end end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 21e8d2fc0b9..5dbc880fff5 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -24043,6 +24043,9 @@ msgstr "" msgid "PackageRegistry|Remove package" msgstr "" +msgid "PackageRegistry|Required Python: %{pythonVersion}" +msgstr "" + msgid "PackageRegistry|RubyGems" msgstr "" diff --git a/spec/controllers/boards/issues_controller_spec.rb b/spec/controllers/boards/issues_controller_spec.rb index 48000284264..cc60ab16d2e 100644 --- a/spec/controllers/boards/issues_controller_spec.rb +++ b/spec/controllers/boards/issues_controller_spec.rb @@ -428,17 +428,21 @@ RSpec.describe Boards::IssuesController do describe 'POST create' do context 'with valid params' do - it 'returns a successful 200 response' do + before do create_issue user: user, board: board, list: list1, title: 'New issue' + end + it 'returns a successful 200 response' do expect(response).to have_gitlab_http_status(:ok) end it 'returns the created issue' do - create_issue user: user, board: board, list: list1, title: 'New issue' - expect(response).to match_response_schema('entities/issue_board') end + + it 'sets the default work_item_type' do + expect(Issue.last.work_item_type.base_type).to eq('issue') + end end context 'with invalid params' do diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb index 0c29280316a..b3b9941062d 100644 --- a/spec/controllers/projects/issues_controller_spec.rb +++ b/spec/controllers/projects/issues_controller_spec.rb @@ -1176,12 +1176,22 @@ RSpec.describe Projects::IssuesController do project.issues.first end + context 'when creating an incident' do + it 'sets the correct issue_type' do + issue = post_new_issue(issue_type: 'incident') + + expect(issue.issue_type).to eq('incident') + expect(issue.work_item_type.base_type).to eq('incident') + end + end + it 'creates the issue successfully', :aggregate_failures do issue = post_new_issue expect(issue).to be_a(Issue) expect(issue.persisted?).to eq(true) expect(issue.issue_type).to eq('issue') + expect(issue.work_item_type.base_type).to eq('issue') end context 'resolving discussions in MergeRequest' do diff --git a/spec/factories/issues.rb b/spec/factories/issues.rb index 2d52747dece..8b53732a3c1 100644 --- a/spec/factories/issues.rb +++ b/spec/factories/issues.rb @@ -8,6 +8,7 @@ FactoryBot.define do updated_by { author } relative_position { RelativePositioning::START_POSITION } issue_type { :issue } + association :work_item_type, :default trait :confidential do confidential { true } @@ -59,6 +60,7 @@ FactoryBot.define do factory :incident do issue_type { :incident } + association :work_item_type, :default, :incident end end end diff --git a/spec/factories/work_item/work_item_types.rb b/spec/factories/work_item/work_item_types.rb index 07d6d685c57..1c586aab59b 100644 --- a/spec/factories/work_item/work_item_types.rb +++ b/spec/factories/work_item/work_item_types.rb @@ -8,6 +8,17 @@ FactoryBot.define do base_type { WorkItem::Type.base_types[:issue] } icon_name { 'issue-type-issue' } + initialize_with do + type_base_attributes = attributes.with_indifferent_access.slice(:base_type, :namespace, :namespace_id) + + # Expect base_types to exist on the DB + if type_base_attributes.slice(:namespace, :namespace_id).compact.empty? + WorkItem::Type.find_or_initialize_by(type_base_attributes).tap { |type| type.assign_attributes(attributes) } + else + WorkItem::Type.new(attributes) + end + end + trait :default do namespace { nil } end diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/additional_metadata_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/additional_metadata_spec.js index e5147b0b6b1..dda0b5bef8d 100644 --- a/spec/frontend/packages_and_registries/package_registry/components/details/additional_metadata_spec.js +++ b/spec/frontend/packages_and_registries/package_registry/components/details/additional_metadata_spec.js @@ -6,6 +6,7 @@ import { nugetMetadata, packageData, composerMetadata, + pypiMetadata, } from 'jest/packages_and_registries/package_registry/mock_data'; import component from '~/packages_and_registries/package_registry/components/details/additional_metadata.vue'; import { @@ -14,6 +15,7 @@ import { PACKAGE_TYPE_MAVEN, PACKAGE_TYPE_NPM, PACKAGE_TYPE_COMPOSER, + PACKAGE_TYPE_PYPI, } from '~/packages_and_registries/package_registry/constants'; import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; import DetailsRow from '~/vue_shared/components/registry/details_row.vue'; @@ -22,6 +24,7 @@ const mavenPackage = { packageType: PACKAGE_TYPE_MAVEN, metadata: mavenMetadata( const conanPackage = { packageType: PACKAGE_TYPE_CONAN, metadata: conanMetadata() }; const nugetPackage = { packageType: PACKAGE_TYPE_NUGET, metadata: nugetMetadata() }; const composerPackage = { packageType: PACKAGE_TYPE_COMPOSER, metadata: composerMetadata() }; +const pypiPackage = { packageType: PACKAGE_TYPE_PYPI, metadata: pypiMetadata() }; const npmPackage = { packageType: PACKAGE_TYPE_NPM, metadata: {} }; describe('Package Additional Metadata', () => { @@ -58,6 +61,7 @@ describe('Package Additional Metadata', () => { const findComposerTargetSha = () => wrapper.findByTestId('composer-target-sha'); const findComposerTargetShaCopyButton = () => wrapper.findComponent(ClipboardButton); const findComposerJson = () => wrapper.findByTestId('composer-json'); + const findPypiRequiredPython = () => wrapper.findByTestId('pypi-required-python'); it('has the correct title', () => { mountComponent(); @@ -74,6 +78,7 @@ describe('Package Additional Metadata', () => { ${conanPackage} | ${true} | ${PACKAGE_TYPE_CONAN} ${nugetPackage} | ${true} | ${PACKAGE_TYPE_NUGET} ${composerPackage} | ${true} | ${PACKAGE_TYPE_COMPOSER} + ${pypiPackage} | ${true} | ${PACKAGE_TYPE_PYPI} ${npmPackage} | ${false} | ${PACKAGE_TYPE_NPM} `( `It is $visible that the component is visible when the package is $packageType`, @@ -160,4 +165,20 @@ describe('Package Additional Metadata', () => { }); }); }); + + describe('pypi metadata', () => { + beforeEach(() => { + mountComponent({ packageEntity: pypiPackage }); + }); + + it.each` + name | finderFunction | text | icon + ${'pypi-required-python'} | ${findPypiRequiredPython} | ${'Required Python: 1.0.0'} | ${'information-o'} + `('$name element', ({ finderFunction, text, icon }) => { + const element = finderFunction(); + expect(element.exists()).toBe(true); + expect(element.text()).toBe(text); + expect(element.props('icon')).toBe(icon); + }); + }); }); diff --git a/spec/frontend/packages_and_registries/package_registry/mock_data.js b/spec/frontend/packages_and_registries/package_registry/mock_data.js index 98ff29ef728..9438a2d2d72 100644 --- a/spec/frontend/packages_and_registries/package_registry/mock_data.js +++ b/spec/frontend/packages_and_registries/package_registry/mock_data.js @@ -133,7 +133,7 @@ export const composerMetadata = () => ({ }, }); -export const pypyMetadata = () => ({ +export const pypiMetadata = () => ({ requiredPython: '1.0.0', }); @@ -157,7 +157,7 @@ export const packageDetailsQuery = (extendPackage) => ({ metadata: { ...conanMetadata(), ...composerMetadata(), - ...pypyMetadata(), + ...pypiMetadata(), ...mavenMetadata(), ...nugetMetadata(), }, diff --git a/spec/lib/gitlab/issuables_count_for_state_spec.rb b/spec/lib/gitlab/issuables_count_for_state_spec.rb index e377d1e1dde..cc4ebba863d 100644 --- a/spec/lib/gitlab/issuables_count_for_state_spec.rb +++ b/spec/lib/gitlab/issuables_count_for_state_spec.rb @@ -71,7 +71,7 @@ RSpec.describe Gitlab::IssuablesCountForState do let_it_be(:user) { create(:user) } let_it_be(:group) { create(:group) } - let(:cache_options) { { expires_in: 10.minutes } } + let(:cache_options) { { expires_in: 1.hour } } let(:cache_key) { ['group', group.id, 'issues'] } let(:threshold) { described_class::THRESHOLD } let(:states_count) { { opened: 1, closed: 1, all: 2 } } diff --git a/spec/migrations/20210804150320_create_base_work_item_types_spec.rb b/spec/migrations/20210804150320_create_base_work_item_types_spec.rb index 535472f5931..9ba29637e00 100644 --- a/spec/migrations/20210804150320_create_base_work_item_types_spec.rb +++ b/spec/migrations/20210804150320_create_base_work_item_types_spec.rb @@ -6,7 +6,17 @@ require_migration!('create_base_work_item_types') RSpec.describe CreateBaseWorkItemTypes, :migration do let!(:work_item_types) { table(:work_item_types) } + after(:all) do + # Make sure base types are recreated after running the migration + # because migration specs are not run in a transaction + WorkItem::Type.delete_all + Gitlab::DatabaseImporters::WorkItems::BaseTypeImporter.import + end + it 'creates default data' do + # Need to delete all as base types are seeded before entire test suite + WorkItem::Type.delete_all + reversible_migration do |migration| migration.before -> { # Depending on whether the migration has been run before, diff --git a/spec/migrations/20210831203408_upsert_base_work_item_types_spec.rb b/spec/migrations/20210831203408_upsert_base_work_item_types_spec.rb index b9cc9de88cc..c23110750c3 100644 --- a/spec/migrations/20210831203408_upsert_base_work_item_types_spec.rb +++ b/spec/migrations/20210831203408_upsert_base_work_item_types_spec.rb @@ -6,8 +6,18 @@ require_migration!('upsert_base_work_item_types') RSpec.describe UpsertBaseWorkItemTypes, :migration do let!(:work_item_types) { table(:work_item_types) } + after(:all) do + # Make sure base types are recreated after running the migration + # because migration specs are not run in a transaction + WorkItem::Type.delete_all + Gitlab::DatabaseImporters::WorkItems::BaseTypeImporter.import + end + context 'when no default types exist' do it 'creates default data' do + # Need to delete all as base types are seeded before entire test suite + WorkItem::Type.delete_all + expect(work_item_types.count).to eq(0) reversible_migration do |migration| @@ -26,10 +36,6 @@ RSpec.describe UpsertBaseWorkItemTypes, :migration do end context 'when default types already exist' do - before do - Gitlab::DatabaseImporters::WorkItems::BaseTypeImporter.import - end - it 'does not create default types again' do expect(work_item_types.all.pluck(:base_type)).to match_array(WorkItem::Type.base_types.values) diff --git a/spec/models/hooks/web_hook_spec.rb b/spec/models/hooks/web_hook_spec.rb index c68ad3bf0c4..59f4533a6c1 100644 --- a/spec/models/hooks/web_hook_spec.rb +++ b/spec/models/hooks/web_hook_spec.rb @@ -10,7 +10,11 @@ RSpec.describe WebHook do let(:hook) { build(:project_hook, project: project) } around do |example| - freeze_time { example.run } + if example.metadata[:skip_freeze_time] + example.run + else + freeze_time { example.run } + end end describe 'associations' do @@ -326,10 +330,28 @@ RSpec.describe WebHook do expect { hook.backoff! }.to change(hook, :backoff_count).by(1) end - it 'does not let the backoff count exceed the maximum failure count' do - hook.backoff_count = described_class::MAX_FAILURES + context 'when we have backed off MAX_FAILURES times' do + before do + stub_const("#{described_class}::MAX_FAILURES", 5) + 5.times { hook.backoff! } + end - expect { hook.backoff! }.not_to change(hook, :backoff_count) + it 'does not let the backoff count exceed the maximum failure count' do + expect { hook.backoff! }.not_to change(hook, :backoff_count) + end + + it 'does not change disabled_until', :skip_freeze_time do + travel_to(hook.disabled_until - 1.minute) do + expect { hook.backoff! }.not_to change(hook, :disabled_until) + end + end + + it 'changes disabled_until when it has elapsed', :skip_freeze_time do + travel_to(hook.disabled_until + 1.minute) do + expect { hook.backoff! }.to change { hook.disabled_until } + expect(hook.backoff_count).to eq(described_class::MAX_FAILURES) + end + end end include_examples 'is tolerant of invalid records' do diff --git a/spec/models/work_item/type_spec.rb b/spec/models/work_item/type_spec.rb index 90f551b7d63..dd5324d63a0 100644 --- a/spec/models/work_item/type_spec.rb +++ b/spec/models/work_item/type_spec.rb @@ -19,8 +19,10 @@ RSpec.describe WorkItem::Type do it 'deletes type but not unrelated issues' do type = create(:work_item_type) + expect(WorkItem::Type.count).to eq(5) + expect { type.destroy! }.not_to change(Issue, :count) - expect(WorkItem::Type.count).to eq 0 + expect(WorkItem::Type.count).to eq(4) end end @@ -28,7 +30,7 @@ RSpec.describe WorkItem::Type do type = create(:work_item_type, work_items: [work_item]) expect { type.destroy! }.to raise_error(ActiveRecord::InvalidForeignKey) - expect(Issue.count).to eq 1 + expect(Issue.count).to eq(1) end end diff --git a/spec/rake_helper.rb b/spec/rake_helper.rb index 7df1cf7444f..ca5b4d8337c 100644 --- a/spec/rake_helper.rb +++ b/spec/rake_helper.rb @@ -12,6 +12,6 @@ RSpec.configure do |config| end config.after(:all) do - delete_from_all_tables! + delete_from_all_tables!(except: deletion_except_tables) end end diff --git a/spec/requests/api/graphql/mutations/issues/create_spec.rb b/spec/requests/api/graphql/mutations/issues/create_spec.rb index 66450f8c604..886f3140086 100644 --- a/spec/requests/api/graphql/mutations/issues/create_spec.rb +++ b/spec/requests/api/graphql/mutations/issues/create_spec.rb @@ -39,11 +39,14 @@ RSpec.describe 'Create an issue' do end it 'creates the issue' do - post_graphql_mutation(mutation, current_user: current_user) + expect do + post_graphql_mutation(mutation, current_user: current_user) + end.to change(Issue, :count).by(1) expect(response).to have_gitlab_http_status(:success) expect(mutation_response['issue']).to include(input) expect(mutation_response['issue']).to include('discussionLocked' => true) + expect(Issue.last.work_item_type.base_type).to eq('issue') end end end diff --git a/spec/requests/api/graphql/mutations/issues/update_spec.rb b/spec/requests/api/graphql/mutations/issues/update_spec.rb index c3aaf090703..0f2eeb90894 100644 --- a/spec/requests/api/graphql/mutations/issues/update_spec.rb +++ b/spec/requests/api/graphql/mutations/issues/update_spec.rb @@ -44,6 +44,19 @@ RSpec.describe 'Update of an existing issue' do expect(mutation_response['issue']).to include('discussionLocked' => true) end + context 'when issue_type is updated' do + let(:input) { { 'iid' => issue.iid.to_s, 'type' => 'INCIDENT' } } + + it 'updates issue_type and work_item_type' do + expect do + post_graphql_mutation(mutation, current_user: current_user) + issue.reload + end.to change { issue.work_item_type.base_type }.from('issue').to('incident').and( + change(issue, :issue_type).from('issue').to('incident') + ) + end + end + context 'setting labels' do let(:mutation) do graphql_mutation(:update_issue, input_params) do diff --git a/spec/services/issues/build_service_spec.rb b/spec/services/issues/build_service_spec.rb index a67e634a4ff..b96dd981e0f 100644 --- a/spec/services/issues/build_service_spec.rb +++ b/spec/services/issues/build_service_spec.rb @@ -3,6 +3,8 @@ require 'spec_helper' RSpec.describe Issues::BuildService do + using RSpec::Parameterized::TableSyntax + let_it_be(:project) { create(:project, :repository) } let_it_be(:developer) { create(:user) } let_it_be(:guest) { create(:user) } @@ -144,6 +146,8 @@ RSpec.describe Issues::BuildService do issue = build_issue(milestone_id: milestone.id) expect(issue.milestone).to eq(milestone) + expect(issue.issue_type).to eq('issue') + expect(issue.work_item_type.base_type).to eq('issue') end it 'sets milestone to nil if it is not available for the project' do @@ -152,6 +156,15 @@ RSpec.describe Issues::BuildService do expect(issue.milestone).to be_nil end + + context 'when issue_type is incident' do + it 'sets the correct issue type' do + issue = build_issue(issue_type: 'incident') + + expect(issue.issue_type).to eq('incident') + expect(issue.work_item_type.base_type).to eq('incident') + end + end end context 'as guest' do @@ -165,22 +178,13 @@ RSpec.describe Issues::BuildService do end context 'setting issue type' do - it 'defaults to issue if issue_type not given' do - issue = build_issue + shared_examples 'builds an issue' do + specify do + issue = build_issue(issue_type: issue_type) - expect(issue).to be_issue - end - - it 'sets issue' do - issue = build_issue(issue_type: 'issue') - - expect(issue).to be_issue - end - - it 'sets incident' do - issue = build_issue(issue_type: 'incident') - - expect(issue).to be_incident + expect(issue.issue_type).to eq(resulting_issue_type) + expect(issue.work_item_type_id).to eq(work_item_type_id) + end end it 'cannot set invalid issue type' do @@ -188,6 +192,24 @@ RSpec.describe Issues::BuildService do expect(issue).to be_issue end + + context 'with a corresponding WorkItem::Type' do + let_it_be(:type_issue_id) { WorkItem::Type.default_issue_type.id } + let_it_be(:type_incident_id) { WorkItem::Type.default_by_type(:incident).id } + + where(:issue_type, :work_item_type_id, :resulting_issue_type) do + nil | ref(:type_issue_id) | 'issue' + 'issue' | ref(:type_issue_id) | 'issue' + 'incident' | ref(:type_incident_id) | 'incident' + 'test_case' | ref(:type_issue_id) | 'issue' # update once support for test_case is enabled + 'requirement' | ref(:type_issue_id) | 'issue' # update once support for requirement is enabled + 'invalid' | ref(:type_issue_id) | 'issue' + end + + with_them do + it_behaves_like 'builds an issue' + end + end end end end diff --git a/spec/services/issues/create_service_spec.rb b/spec/services/issues/create_service_spec.rb index d43dd437292..3988069d83a 100644 --- a/spec/services/issues/create_service_spec.rb +++ b/spec/services/issues/create_service_spec.rb @@ -43,10 +43,11 @@ RSpec.describe Issues::CreateService do expect(issue).to be_persisted expect(issue.title).to eq('Awesome issue') - expect(issue.assignees).to eq [assignee] - expect(issue.labels).to match_array labels - expect(issue.milestone).to eq milestone - expect(issue.due_date).to eq Date.tomorrow + expect(issue.assignees).to eq([assignee]) + expect(issue.labels).to match_array(labels) + expect(issue.milestone).to eq(milestone) + expect(issue.due_date).to eq(Date.tomorrow) + expect(issue.work_item_type.base_type).to eq('issue') end context 'when skip_system_notes is true' do diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb index 712b9886152..331cf291f21 100644 --- a/spec/services/issues/update_service_spec.rb +++ b/spec/services/issues/update_service_spec.rb @@ -228,15 +228,19 @@ RSpec.describe Issues::UpdateService, :mailer do context 'from incident to issue' do let(:issue) { create(:incident, project: project) } + it 'changed from an incident to an issue type' do + expect { update_issue(issue_type: 'issue') } + .to change(issue, :issue_type).from('incident').to('issue') + .and(change { issue.work_item_type.base_type }.from('incident').to('issue')) + end + context 'for an incident with multiple labels' do let(:issue) { create(:incident, project: project, labels: [label_1, label_2]) } - before do - update_issue(issue_type: 'issue') - end - it 'removes an `incident` label if one exists on the incident' do - expect(issue.labels).to eq([label_2]) + expect { update_issue(issue_type: 'issue') }.to change(issue, :label_ids) + .from(containing_exactly(label_1.id, label_2.id)) + .to([label_2.id]) end end @@ -244,12 +248,10 @@ RSpec.describe Issues::UpdateService, :mailer do let(:issue) { create(:incident, project: project, labels: [label_1, label_2]) } let(:params) { { label_ids: [label_1.id, label_2.id], remove_label_ids: [] } } - before do - update_issue(issue_type: 'issue') - end - it 'adds an incident label id to remove_label_ids for it to be removed' do - expect(issue.label_ids).to contain_exactly(label_2.id) + expect { update_issue(issue_type: 'issue') }.to change(issue, :label_ids) + .from(containing_exactly(label_1.id, label_2.id)) + .to([label_2.id]) end end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 6ccfabca101..216835e4b9f 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -220,6 +220,8 @@ RSpec.configure do |config| # Enable all features by default for testing # Reset any changes in after hook. stub_all_feature_flags + + TestEnv.seed_db end config.after(:all) do diff --git a/spec/support/database_cleaner.rb b/spec/support/database_cleaner.rb index 01bf390d9e9..b31881e3082 100644 --- a/spec/support/database_cleaner.rb +++ b/spec/support/database_cleaner.rb @@ -14,7 +14,7 @@ RSpec.configure do |config| end config.append_after(:context, :migration) do - delete_from_all_tables! + 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: @@ -61,7 +61,7 @@ RSpec.configure do |config| example.run - delete_from_all_tables! + delete_from_all_tables!(except: ['work_item_types']) self.class.use_transactional_tests = true end diff --git a/spec/support/db_cleaner.rb b/spec/support/db_cleaner.rb index 155dc3c17d9..940ff2751d3 100644 --- a/spec/support/db_cleaner.rb +++ b/spec/support/db_cleaner.rb @@ -12,7 +12,7 @@ module DbCleaner end def deletion_except_tables - [] + ['work_item_types'] end def setup_database_cleaner diff --git a/spec/support/helpers/test_env.rb b/spec/support/helpers/test_env.rb index aa5fcf222f2..badd4e8212c 100644 --- a/spec/support/helpers/test_env.rb +++ b/spec/support/helpers/test_env.rb @@ -452,6 +452,10 @@ module TestEnv example_group end + def seed_db + Gitlab::DatabaseImporters::WorkItems::BaseTypeImporter.import + end + private # These are directories that should be preserved at cleanup time diff --git a/spec/support/shared_examples/services/incident_shared_examples.rb b/spec/support/shared_examples/services/incident_shared_examples.rb index 9fced12b543..0277cce975a 100644 --- a/spec/support/shared_examples/services/incident_shared_examples.rb +++ b/spec/support/shared_examples/services/incident_shared_examples.rb @@ -13,6 +13,7 @@ RSpec.shared_examples 'incident issue' do it 'has incident as issue type' do expect(issue.issue_type).to eq('incident') + expect(issue.work_item_type.base_type).to eq('incident') end end @@ -41,6 +42,7 @@ RSpec.shared_examples 'not an incident issue' do it 'has not incident as issue type' do expect(issue.issue_type).not_to eq('incident') + expect(issue.work_item_type.base_type).not_to eq('incident') end it 'has not an incident label' do diff --git a/spec/support/shared_examples/work_item_base_types_importer.rb b/spec/support/shared_examples/work_item_base_types_importer.rb index 2c02b76d49b..7d652be8d05 100644 --- a/spec/support/shared_examples/work_item_base_types_importer.rb +++ b/spec/support/shared_examples/work_item_base_types_importer.rb @@ -2,6 +2,9 @@ RSpec.shared_examples 'work item base types importer' do it 'creates all base work item types' do + # Fixtures need to run on a pristine DB, but the test suite preloads the base types before(:suite) + WorkItem::Type.delete_all + expect { subject }.to change(WorkItem::Type, :count).from(0).to(WorkItem::Type::BASE_TYPES.count) end end