diff --git a/app/assets/javascripts/profile/account/components/delete_account_modal.vue b/app/assets/javascripts/profile/account/components/delete_account_modal.vue index 0fcf200ce28..24b142050d6 100644 --- a/app/assets/javascripts/profile/account/components/delete_account_modal.vue +++ b/app/assets/javascripts/profile/account/components/delete_account_modal.vue @@ -1,12 +1,12 @@ diff --git a/app/assets/javascripts/profile/account/index.js b/app/assets/javascripts/profile/account/index.js index f0d9642a2b2..5e89002b3bc 100644 --- a/app/assets/javascripts/profile/account/index.js +++ b/app/assets/javascripts/profile/account/index.js @@ -30,6 +30,9 @@ export default () => { }, mounted() { deleteAccountButton.classList.remove('disabled'); + deleteAccountButton.addEventListener('click', () => { + this.$root.$emit('bv::show::modal', 'delete-account-modal', '#delete-account-button'); + }); }, render(createElement) { return createElement('delete-account-modal', { diff --git a/app/models/ci/pipeline_artifact.rb b/app/models/ci/pipeline_artifact.rb index e9c40180d20..ea8cb48fc9d 100644 --- a/app/models/ci/pipeline_artifact.rb +++ b/app/models/ci/pipeline_artifact.rb @@ -8,6 +8,7 @@ module Ci include UpdateProjectStatistics include Artifactable include FileStoreMounter + include Presentable FILE_STORE_SUPPORTED = [ ObjectStorage::Store::LOCAL, @@ -44,5 +45,9 @@ module Ci def self.find_with_code_coverage find_by(file_type: :code_coverage) end + + def present + super(presenter_class: "Ci::PipelineArtifacts::#{self.file_type.camelize}Presenter".constantize) + end end end diff --git a/app/models/packages/package.rb b/app/models/packages/package.rb index 618d240b7ff..0ab6b337ac0 100644 --- a/app/models/packages/package.rb +++ b/app/models/packages/package.rb @@ -38,8 +38,12 @@ class Packages::Package < ApplicationRecord validates :version, format: { with: Gitlab::Regex.conan_recipe_component_regex }, if: :conan? validates :version, format: { with: Gitlab::Regex.maven_version_regex }, if: -> { version? && maven? } validates :version, format: { with: Gitlab::Regex.pypi_version_regex }, if: :pypi? + validates :version, + presence: true, + format: { with: Gitlab::Regex.generic_package_version_regex }, + if: :generic? - enum package_type: { maven: 1, npm: 2, conan: 3, nuget: 4, pypi: 5, composer: 6 } + enum package_type: { maven: 1, npm: 2, conan: 3, nuget: 4, pypi: 5, composer: 6, generic: 7 } scope :with_name, ->(name) { where(name: name) } scope :with_name_like, ->(name) { where(arel_table[:name].matches(name)) } diff --git a/app/presenters/ci/pipeline_artifacts/code_coverage_presenter.rb b/app/presenters/ci/pipeline_artifacts/code_coverage_presenter.rb new file mode 100644 index 00000000000..098e839132c --- /dev/null +++ b/app/presenters/ci/pipeline_artifacts/code_coverage_presenter.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module Ci + module PipelineArtifacts + class CodeCoveragePresenter < ProcessablePresenter + include Gitlab::Utils::StrongMemoize + + def for_files(filenames) + coverage_files = raw_report["files"].select { |key| filenames.include?(key) } + + { files: coverage_files } + end + + private + + def raw_report + strong_memoize(:raw_report) do + self.each_blob do |blob| + Gitlab::Json.parse(blob) + end + end + end + end + end +end diff --git a/app/serializers/test_suite_entity.rb b/app/serializers/test_suite_entity.rb index d04fd5f6a84..15eb2891b22 100644 --- a/app/serializers/test_suite_entity.rb +++ b/app/serializers/test_suite_entity.rb @@ -13,7 +13,7 @@ class TestSuiteEntity < Grape::Entity with_options if: -> (_, opts) { opts[:details] } do |test_suite| expose :suite_error expose :test_cases, using: TestCaseEntity do |test_suite| - test_suite.suite_error ? [] : test_suite.test_cases.values.flat_map(&:values) + test_suite.suite_error ? [] : test_suite.sorted.test_cases.values.flat_map(&:values) end end end diff --git a/app/services/ci/generate_coverage_reports_service.rb b/app/services/ci/generate_coverage_reports_service.rb index 23e67ac1b9e..063fb966183 100644 --- a/app/services/ci/generate_coverage_reports_service.rb +++ b/app/services/ci/generate_coverage_reports_service.rb @@ -12,7 +12,7 @@ module Ci { status: :parsed, key: key(base_pipeline, head_pipeline), - data: Gitlab::Ci::Pipeline::Artifact::CodeCoverage.new(head_pipeline.pipeline_artifacts.find_with_code_coverage).for_files(merge_request.new_paths) + data: head_pipeline.pipeline_artifacts.find_with_code_coverage.present.for_files(merge_request.new_paths) } rescue => e Gitlab::ErrorTracking.track_exception(e, project_id: project.id) diff --git a/app/views/profiles/accounts/show.html.haml b/app/views/profiles/accounts/show.html.haml index 20660e61f38..c875caca94a 100644 --- a/app/views/profiles/accounts/show.html.haml +++ b/app/views/profiles/accounts/show.html.haml @@ -55,8 +55,8 @@ = s_('Profiles|Deleting an account has the following effects:') = render 'users/deletion_guidance', user: current_user - %button#delete-account-button.btn.btn-danger.disabled{ data: { toggle: 'modal', - target: '#delete-account-modal', qa_selector: 'delete_account_button' } } + -# Delete button here + %button#delete-account-button.btn.btn-danger.disabled{ data: { qa_selector: 'delete_account_button' } } = s_('Profiles|Delete account') #delete-account-modal{ data: { action_url: user_registration_path, diff --git a/changelogs/unreleased/235490-generic-packages-generic-type.yml b/changelogs/unreleased/235490-generic-packages-generic-type.yml new file mode 100644 index 00000000000..72bd2419b8e --- /dev/null +++ b/changelogs/unreleased/235490-generic-packages-generic-type.yml @@ -0,0 +1,5 @@ +--- +title: Add new "generic" package type +merge_request: 40061 +author: +type: added diff --git a/changelogs/unreleased/mo-sort-test-data-by-duration.yml b/changelogs/unreleased/mo-sort-test-data-by-duration.yml new file mode 100644 index 00000000000..8f8d97e6562 --- /dev/null +++ b/changelogs/unreleased/mo-sort-test-data-by-duration.yml @@ -0,0 +1,5 @@ +--- +title: Sort TestCase data by status and execution_time +merge_request: 40722 +author: +type: changed diff --git a/changelogs/unreleased/sh-add-if-exists-pgdump.yml b/changelogs/unreleased/sh-add-if-exists-pgdump.yml new file mode 100644 index 00000000000..774024591c9 --- /dev/null +++ b/changelogs/unreleased/sh-add-if-exists-pgdump.yml @@ -0,0 +1,5 @@ +--- +title: Add --if-exists to pg_dump command-line in backup creation +merge_request: 40792 +author: +type: other diff --git a/db/migrate/20200820204041_create_ci_platform_metrics.rb b/db/migrate/20200820204041_create_ci_platform_metrics.rb index 357f62c441b..27a5a3dc8eb 100644 --- a/db/migrate/20200820204041_create_ci_platform_metrics.rb +++ b/db/migrate/20200820204041_create_ci_platform_metrics.rb @@ -4,6 +4,7 @@ class CreateCiPlatformMetrics < ActiveRecord::Migration[6.0] include Gitlab::Database::MigrationHelpers DOWNTIME = false + CI_VARIABLES_KEY_INDEX_NAME = "index_ci_variables_on_key" disable_ddl_transaction! @@ -17,11 +18,14 @@ class CreateCiPlatformMetrics < ActiveRecord::Migration[6.0] end add_text_limit :ci_platform_metrics, :platform_target, 255 - add_concurrent_index :ci_variables, :key + add_concurrent_index :ci_variables, :key, name: CI_VARIABLES_KEY_INDEX_NAME end def down - drop_table :ci_platform_metrics - remove_concurrent_index :ci_variables, :key, name: 'index_ci_variables_on_key' + if table_exists?(:ci_platform_metrics) + drop_table :ci_platform_metrics + end + + remove_concurrent_index :ci_variables, :key, name: CI_VARIABLES_KEY_INDEX_NAME end end diff --git a/db/migrate/20200821034419_add_unique_index_for_generic_packages.rb b/db/migrate/20200821034419_add_unique_index_for_generic_packages.rb new file mode 100644 index 00000000000..f0e4749bbb5 --- /dev/null +++ b/db/migrate/20200821034419_add_unique_index_for_generic_packages.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +class AddUniqueIndexForGenericPackages < ActiveRecord::Migration[6.0] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + INDEX_NAME = 'index_packages_on_project_id_name_version_unique_when_generic' + PACKAGE_TYPE_GENERIC = 7 + + disable_ddl_transaction! + + def up + add_concurrent_index :packages_packages, [:project_id, :name, :version], unique: true, where: "package_type = #{PACKAGE_TYPE_GENERIC}", name: INDEX_NAME + end + + def down + remove_concurrent_index_by_name(:packages_packages, INDEX_NAME) + end +end diff --git a/db/migrate/20200824045812_add_generic_package_max_file_size_to_plan_limits.rb b/db/migrate/20200824045812_add_generic_package_max_file_size_to_plan_limits.rb new file mode 100644 index 00000000000..eea4f8de7bf --- /dev/null +++ b/db/migrate/20200824045812_add_generic_package_max_file_size_to_plan_limits.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class AddGenericPackageMaxFileSizeToPlanLimits < ActiveRecord::Migration[6.0] + DOWNTIME = false + + def change + add_column(:plan_limits, :generic_packages_max_file_size, :bigint, default: 5.gigabytes, null: false) + end +end diff --git a/db/schema_migrations/20200821034419 b/db/schema_migrations/20200821034419 new file mode 100644 index 00000000000..56ede8eff69 --- /dev/null +++ b/db/schema_migrations/20200821034419 @@ -0,0 +1 @@ +ddf3452bb44437324d20c9db03e998f8903f5ff9732d29cf85dd5d579507952d \ No newline at end of file diff --git a/db/schema_migrations/20200824045812 b/db/schema_migrations/20200824045812 new file mode 100644 index 00000000000..c628ca3eb18 --- /dev/null +++ b/db/schema_migrations/20200824045812 @@ -0,0 +1 @@ +4f3528d7df6e61c8b14911644f9223ac5f6e678184d1c8370d1e9a60389cd60c \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 791253dc43d..f5a8d4f9f16 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -14195,7 +14195,8 @@ CREATE TABLE public.plan_limits ( maven_max_file_size bigint DEFAULT 52428800 NOT NULL, npm_max_file_size bigint DEFAULT 52428800 NOT NULL, nuget_max_file_size bigint DEFAULT 52428800 NOT NULL, - pypi_max_file_size bigint DEFAULT 52428800 NOT NULL + pypi_max_file_size bigint DEFAULT 52428800 NOT NULL, + generic_packages_max_file_size bigint DEFAULT '5368709120'::bigint NOT NULL ); CREATE SEQUENCE public.plan_limits_id_seq @@ -20414,6 +20415,8 @@ CREATE INDEX index_packages_maven_metadata_on_package_id_and_path ON public.pack CREATE INDEX index_packages_nuget_dl_metadata_on_dependency_link_id ON public.packages_nuget_dependency_link_metadata USING btree (dependency_link_id); +CREATE UNIQUE INDEX index_packages_on_project_id_name_version_unique_when_generic ON public.packages_packages USING btree (project_id, name, version) WHERE (package_type = 7); + CREATE INDEX index_packages_package_files_file_store_is_null ON public.packages_package_files USING btree (id) WHERE (file_store IS NULL); CREATE INDEX index_packages_package_files_on_file_store ON public.packages_package_files USING btree (file_store); diff --git a/doc/.vale/gitlab/British.yml b/doc/.vale/gitlab/British.yml index 229eb324eb6..7221d7d24aa 100644 --- a/doc/.vale/gitlab/British.yml +++ b/doc/.vale/gitlab/British.yml @@ -67,7 +67,6 @@ swap: matt: matte meagre: meager metre: meter - mitre: miter modelling: modeling moustache: mustache neighbour: neighbor diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql index e97ce8b99e3..faa9cd519fd 100644 --- a/doc/api/graphql/reference/gitlab_schema.graphql +++ b/doc/api/graphql/reference/gitlab_schema.graphql @@ -10565,6 +10565,11 @@ enum PackageTypeEnum { """ CONAN + """ + Packages from the generic package manager + """ + GENERIC + """ Packages from the maven package manager """ diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json index f7ddefa46d9..5921f3719c1 100644 --- a/doc/api/graphql/reference/gitlab_schema.json +++ b/doc/api/graphql/reference/gitlab_schema.json @@ -31806,6 +31806,12 @@ "description": "Packages from the composer package manager", "isDeprecated": false, "deprecationReason": null + }, + { + "name": "GENERIC", + "description": "Packages from the generic package manager", + "isDeprecated": false, + "deprecationReason": null } ], "possibleTypes": null diff --git a/lib/backup/database.rb b/lib/backup/database.rb index 1bc32546abf..ddddd57bd60 100644 --- a/lib/backup/database.rb +++ b/lib/backup/database.rb @@ -27,6 +27,7 @@ module Backup progress.print "Dumping PostgreSQL database #{config['database']} ... " pg_env pgsql_args = ["--clean"] # Pass '--clean' to include 'DROP TABLE' statements in the DB dump. + pgsql_args << '--if-exists' if Gitlab.config.backup.pg_schema pgsql_args << '-n' diff --git a/lib/gitlab/ci/pipeline/artifact/code_coverage.rb b/lib/gitlab/ci/pipeline/artifact/code_coverage.rb deleted file mode 100644 index d8f28bde7ce..00000000000 --- a/lib/gitlab/ci/pipeline/artifact/code_coverage.rb +++ /dev/null @@ -1,33 +0,0 @@ -# frozen_string_literal: true - -module Gitlab - module Ci - module Pipeline - module Artifact - class CodeCoverage - include Gitlab::Utils::StrongMemoize - - def initialize(pipeline_artifact) - @pipeline_artifact = pipeline_artifact - end - - def for_files(filenames) - coverage_files = raw_report["files"].select { |key| filenames.include?(key) } - - { files: coverage_files } - end - - private - - def raw_report - strong_memoize(:raw_report) do - @pipeline_artifact.each_blob do |blob| - Gitlab::Json.parse(blob) - end - end - end - end - end - end - end -end diff --git a/lib/gitlab/ci/reports/test_case.rb b/lib/gitlab/ci/reports/test_case.rb index 75898745366..15a3c862c9e 100644 --- a/lib/gitlab/ci/reports/test_case.rb +++ b/lib/gitlab/ci/reports/test_case.rb @@ -8,7 +8,7 @@ module Gitlab STATUS_FAILED = 'failed' STATUS_SKIPPED = 'skipped' STATUS_ERROR = 'error' - STATUS_TYPES = [STATUS_SUCCESS, STATUS_FAILED, STATUS_SKIPPED, STATUS_ERROR].freeze + STATUS_TYPES = [STATUS_ERROR, STATUS_FAILED, STATUS_SUCCESS, STATUS_SKIPPED].freeze attr_reader :name, :classname, :execution_time, :status, :file, :system_output, :stack_trace, :key, :attachment, :job diff --git a/lib/gitlab/ci/reports/test_suite.rb b/lib/gitlab/ci/reports/test_suite.rb index 5ee779227ec..e9b78b841e4 100644 --- a/lib/gitlab/ci/reports/test_suite.rb +++ b/lib/gitlab/ci/reports/test_suite.rb @@ -78,11 +78,27 @@ module Gitlab end end + def sorted + sort_by_status + sort_by_execution_time_desc + self + end + private def existing_key?(test_case) @test_cases[test_case.status]&.key?(test_case.key) end + + def sort_by_status + @test_cases = @test_cases.sort_by { |status, _| Gitlab::Ci::Reports::TestCase::STATUS_TYPES.index(status) }.to_h + end + + def sort_by_execution_time_desc + @test_cases = @test_cases.keys.each_with_object({}) do |key, hash| + hash[key] = @test_cases[key].sort_by { |_key, test_case| -test_case.execution_time }.to_h + end + end end end end diff --git a/lib/gitlab/regex.rb b/lib/gitlab/regex.rb index 2702dfc85ae..0f52d8ab95d 100644 --- a/lib/gitlab/regex.rb +++ b/lib/gitlab/regex.rb @@ -104,6 +104,10 @@ module Gitlab \b (?# word boundary) /ix.freeze end + + def generic_package_version_regex + /\A\d+\.\d+\.\d+\z/ + end end extend self diff --git a/locale/gitlab.pot b/locale/gitlab.pot index fca2bc8cc3b..9305d66f79d 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -7947,6 +7947,9 @@ msgstr "" msgid "Delete Snippet" msgstr "" +msgid "Delete account" +msgstr "" + msgid "Delete artifacts" msgstr "" @@ -18724,15 +18727,9 @@ msgstr "" msgid "Profiles|Default notification email" msgstr "" -msgid "Profiles|Delete Account" -msgstr "" - msgid "Profiles|Delete account" msgstr "" -msgid "Profiles|Delete your account?" -msgstr "" - msgid "Profiles|Deleting an account has the following effects:" msgstr "" diff --git a/rubocop/cop/migration/complex_indexes_require_name.rb b/rubocop/cop/migration/complex_indexes_require_name.rb index 173b327be12..82deb36716d 100644 --- a/rubocop/cop/migration/complex_indexes_require_name.rb +++ b/rubocop/cop/migration/complex_indexes_require_name.rb @@ -10,8 +10,12 @@ module RuboCop MSG = 'indexes added with custom options must be explicitly named' + def_node_matcher :match_create_table_index_with_options, <<~PATTERN + (send _ {:index } _ (hash $...)) + PATTERN + def_node_matcher :match_add_index_with_options, <<~PATTERN - (send _ {:add_concurrent_index} _ _ (hash $...)) + (send _ {:add_index :add_concurrent_index} _ _ (hash $...)) PATTERN def_node_matcher :name_option?, <<~PATTERN @@ -26,7 +30,7 @@ module RuboCop return unless in_migration?(node) node.each_descendant(:send) do |send_node| - next unless add_index_offense?(send_node) + next unless create_table_with_index_offense?(send_node) || add_index_offense?(send_node) add_offense(send_node, location: :selector) end @@ -34,6 +38,10 @@ module RuboCop private + def create_table_with_index_offense?(send_node) + match_create_table_index_with_options(send_node) { |option_nodes| needs_name_option?(option_nodes) } + end + def add_index_offense?(send_node) match_add_index_with_options(send_node) { |option_nodes| needs_name_option?(option_nodes) } end diff --git a/spec/factories/packages.rb b/spec/factories/packages.rb index 0f9138ddefe..ee9079579e0 100644 --- a/spec/factories/packages.rb +++ b/spec/factories/packages.rb @@ -121,6 +121,12 @@ FactoryBot.define do conan_metadatum { build(:conan_metadatum, package: nil) } end end + + factory :generic_package do + sequence(:name) { |n| "generic-package-#{n}" } + version { '1.0.0' } + package_type { :generic } + end end factory :composer_metadatum, class: 'Packages::Composer::Metadatum' do diff --git a/spec/frontend/profile/account/components/delete_account_modal_spec.js b/spec/frontend/profile/account/components/delete_account_modal_spec.js index 4da82152818..7834456f7c4 100644 --- a/spec/frontend/profile/account/components/delete_account_modal_spec.js +++ b/spec/frontend/profile/account/components/delete_account_modal_spec.js @@ -1,21 +1,49 @@ import Vue from 'vue'; import { TEST_HOST } from 'helpers/test_constants'; -import mountComponent from 'helpers/vue_mount_component_helper'; +import { merge } from 'lodash'; +import { mount } from '@vue/test-utils'; import deleteAccountModal from '~/profile/account/components/delete_account_modal.vue'; +const GlModalStub = { + name: 'gl-modal-stub', + template: ` +
+ +
+ `, +}; + describe('DeleteAccountModal component', () => { const actionUrl = `${TEST_HOST}/delete/user`; const username = 'hasnoname'; - let Component; + let wrapper; let vm; - beforeEach(() => { - Component = Vue.extend(deleteAccountModal); - }); + const createWrapper = (options = {}) => { + wrapper = mount( + deleteAccountModal, + merge( + {}, + { + propsData: { + actionUrl, + username, + }, + stubs: { + GlModal: GlModalStub, + }, + }, + options, + ), + ); + vm = wrapper.vm; + }; afterEach(() => { - vm.$destroy(); + wrapper.destroy(); + wrapper = null; + vm = null; }); const findElements = () => { @@ -23,16 +51,16 @@ describe('DeleteAccountModal component', () => { return { form: vm.$refs.form, input: vm.$el.querySelector(`[name="${confirmation}"]`), - submitButton: vm.$el.querySelector('.btn-danger'), }; }; + const findModal = () => wrapper.find(GlModalStub); describe('with password confirmation', () => { beforeEach(done => { - vm = mountComponent(Component, { - actionUrl, - confirmWithPassword: true, - username, + createWrapper({ + propsData: { + confirmWithPassword: true, + }, }); vm.isOpen = true; @@ -43,7 +71,7 @@ describe('DeleteAccountModal component', () => { }); it('does not accept empty password', done => { - const { form, input, submitButton } = findElements(); + const { form, input } = findElements(); jest.spyOn(form, 'submit').mockImplementation(() => {}); input.value = ''; input.dispatchEvent(new Event('input')); @@ -51,8 +79,8 @@ describe('DeleteAccountModal component', () => { Vue.nextTick() .then(() => { expect(vm.enteredPassword).toBe(input.value); - expect(submitButton).toHaveAttr('disabled', 'disabled'); - submitButton.click(); + expect(findModal().attributes('ok-disabled')).toBe('true'); + findModal().vm.$emit('primary'); expect(form.submit).not.toHaveBeenCalled(); }) @@ -61,7 +89,7 @@ describe('DeleteAccountModal component', () => { }); it('submits form with password', done => { - const { form, input, submitButton } = findElements(); + const { form, input } = findElements(); jest.spyOn(form, 'submit').mockImplementation(() => {}); input.value = 'anything'; input.dispatchEvent(new Event('input')); @@ -69,8 +97,8 @@ describe('DeleteAccountModal component', () => { Vue.nextTick() .then(() => { expect(vm.enteredPassword).toBe(input.value); - expect(submitButton).not.toHaveAttr('disabled', 'disabled'); - submitButton.click(); + expect(findModal().attributes('ok-disabled')).toBeUndefined(); + findModal().vm.$emit('primary'); expect(form.submit).toHaveBeenCalled(); }) @@ -81,10 +109,10 @@ describe('DeleteAccountModal component', () => { describe('with username confirmation', () => { beforeEach(done => { - vm = mountComponent(Component, { - actionUrl, - confirmWithPassword: false, - username, + createWrapper({ + propsData: { + confirmWithPassword: false, + }, }); vm.isOpen = true; @@ -95,7 +123,7 @@ describe('DeleteAccountModal component', () => { }); it('does not accept wrong username', done => { - const { form, input, submitButton } = findElements(); + const { form, input } = findElements(); jest.spyOn(form, 'submit').mockImplementation(() => {}); input.value = 'this is wrong'; input.dispatchEvent(new Event('input')); @@ -103,8 +131,8 @@ describe('DeleteAccountModal component', () => { Vue.nextTick() .then(() => { expect(vm.enteredUsername).toBe(input.value); - expect(submitButton).toHaveAttr('disabled', 'disabled'); - submitButton.click(); + expect(findModal().attributes('ok-disabled')).toBe('true'); + findModal().vm.$emit('primary'); expect(form.submit).not.toHaveBeenCalled(); }) @@ -113,7 +141,7 @@ describe('DeleteAccountModal component', () => { }); it('submits form with correct username', done => { - const { form, input, submitButton } = findElements(); + const { form, input } = findElements(); jest.spyOn(form, 'submit').mockImplementation(() => {}); input.value = username; input.dispatchEvent(new Event('input')); @@ -121,8 +149,8 @@ describe('DeleteAccountModal component', () => { Vue.nextTick() .then(() => { expect(vm.enteredUsername).toBe(input.value); - expect(submitButton).not.toHaveAttr('disabled', 'disabled'); - submitButton.click(); + expect(findModal().attributes('ok-disabled')).toBeUndefined(); + findModal().vm.$emit('primary'); expect(form.submit).toHaveBeenCalled(); }) diff --git a/spec/graphql/types/package_type_enum_spec.rb b/spec/graphql/types/package_type_enum_spec.rb index fadec9744ed..80a20a68bc2 100644 --- a/spec/graphql/types/package_type_enum_spec.rb +++ b/spec/graphql/types/package_type_enum_spec.rb @@ -4,6 +4,6 @@ require 'spec_helper' RSpec.describe GitlabSchema.types['PackageTypeEnum'] do it 'exposes all package types' do - expect(described_class.values.keys).to contain_exactly(*%w[MAVEN NPM CONAN NUGET PYPI COMPOSER]) + expect(described_class.values.keys).to contain_exactly(*%w[MAVEN NPM CONAN NUGET PYPI COMPOSER GENERIC]) end end diff --git a/spec/lib/gitlab/ci/reports/test_suite_spec.rb b/spec/lib/gitlab/ci/reports/test_suite_spec.rb index fbe3473f6b0..15fa78444e5 100644 --- a/spec/lib/gitlab/ci/reports/test_suite_spec.rb +++ b/spec/lib/gitlab/ci/reports/test_suite_spec.rb @@ -176,6 +176,37 @@ RSpec.describe Gitlab::Ci::Reports::TestSuite do end end + describe '#sorted' do + subject { test_suite.sorted } + + context 'when there are multiple failed test cases' do + before do + test_suite.add_test_case(create_test_case_rspec_failed('test_spec_1', 1.11)) + test_suite.add_test_case(create_test_case_rspec_failed('test_spec_2', 4.44)) + end + + it 'returns test cases sorted by execution time desc' do + expect(subject.test_cases['failed'].each_value.first.execution_time).to eq(4.44) + expect(subject.test_cases['failed'].values.second.execution_time).to eq(1.11) + end + end + + context 'when there are multiple test cases' do + let(:status_ordered) { %w(error failed success skipped) } + + before do + test_suite.add_test_case(test_case_success) + test_suite.add_test_case(test_case_failed) + test_suite.add_test_case(test_case_error) + test_suite.add_test_case(test_case_skipped) + end + + it 'returns test cases sorted by status' do + expect(subject.test_cases.keys).to eq(status_ordered) + end + end + end + Gitlab::Ci::Reports::TestCase::STATUS_TYPES.each do |status_type| describe "##{status_type}" do subject { test_suite.public_send("#{status_type}") } diff --git a/spec/lib/gitlab/regex_spec.rb b/spec/lib/gitlab/regex_spec.rb index 838667675c7..f736f19ca9a 100644 --- a/spec/lib/gitlab/regex_spec.rb +++ b/spec/lib/gitlab/regex_spec.rb @@ -426,4 +426,21 @@ RSpec.describe Gitlab::Regex do it { is_expected.not_to match('1.2') } it { is_expected.not_to match('1./2.3') } end + + describe '.generic_package_version_regex' do + subject { described_class.generic_package_version_regex } + + it { is_expected.to match('1.2.3') } + it { is_expected.to match('1.3.350') } + it { is_expected.not_to match('1.3.350-20201230123456') } + it { is_expected.not_to match('..1.2.3') } + it { is_expected.not_to match(' 1.2.3') } + it { is_expected.not_to match("1.2.3 \r\t") } + it { is_expected.not_to match("\r\t 1.2.3") } + it { is_expected.not_to match('1.2.3-4/../../') } + it { is_expected.not_to match('1.2.3-4%2e%2e%') } + it { is_expected.not_to match('../../../../../1.2.3') } + it { is_expected.not_to match('%2e%2e%2f1.2.3') } + it { is_expected.not_to match('') } + end end diff --git a/spec/models/ci/pipeline_artifact_spec.rb b/spec/models/ci/pipeline_artifact_spec.rb index 14f3bcc463e..716ab4d8522 100644 --- a/spec/models/ci/pipeline_artifact_spec.rb +++ b/spec/models/ci/pipeline_artifact_spec.rb @@ -109,4 +109,14 @@ RSpec.describe Ci::PipelineArtifact, type: :model do end end end + + describe '#present' do + subject { coverage_report.present } + + context 'when file_type is code_coverage' do + it 'uses code coverage presenter' do + expect(subject.present).to be_kind_of(Ci::PipelineArtifacts::CodeCoveragePresenter) + end + end + end end diff --git a/spec/models/packages/package_spec.rb b/spec/models/packages/package_spec.rb index da15b0f0453..4867ac2a75b 100644 --- a/spec/models/packages/package_spec.rb +++ b/spec/models/packages/package_spec.rb @@ -236,6 +236,25 @@ RSpec.describe Packages::Package, type: :model do it { is_expected.not_to allow_value('%2e%2e%2f1.2.3').for(:version) } end + context 'generic package' do + subject { build_stubbed(:generic_package) } + + it { is_expected.to validate_presence_of(:version) } + it { is_expected.to allow_value('1.2.3').for(:version) } + it { is_expected.to allow_value('1.3.350').for(:version) } + it { is_expected.not_to allow_value('1.3.350-20201230123456').for(:version) } + it { is_expected.not_to allow_value('..1.2.3').for(:version) } + it { is_expected.not_to allow_value(' 1.2.3').for(:version) } + it { is_expected.not_to allow_value("1.2.3 \r\t").for(:version) } + it { is_expected.not_to allow_value("\r\t 1.2.3").for(:version) } + it { is_expected.not_to allow_value('1.2.3-4/../../').for(:version) } + it { is_expected.not_to allow_value('1.2.3-4%2e%2e%').for(:version) } + it { is_expected.not_to allow_value('../../../../../1.2.3').for(:version) } + it { is_expected.not_to allow_value('%2e%2e%2f1.2.3').for(:version) } + it { is_expected.not_to allow_value('').for(:version) } + it { is_expected.not_to allow_value(nil).for(:version) } + end + it_behaves_like 'validating version to be SemVer compliant for', :npm_package it_behaves_like 'validating version to be SemVer compliant for', :nuget_package end @@ -552,11 +571,17 @@ RSpec.describe Packages::Package, type: :model do describe 'plan_limits' do Packages::Package.package_types.keys.without('composer').each do |pt| + plan_limit_name = if pt == 'generic' + "#{pt}_packages_max_file_size" + else + "#{pt}_max_file_size" + end + context "File size limits for #{pt}" do let(:package) { create("#{pt}_package") } - it "plan_limits includes column #{pt}_max_file_size" do - expect { package.project.actual_limits.send("#{pt}_max_file_size") } + it "plan_limits includes column #{plan_limit_name}" do + expect { package.project.actual_limits.send(plan_limit_name) } .not_to raise_error(NoMethodError) end end diff --git a/spec/lib/gitlab/ci/pipeline/artifact/code_coverage_spec.rb b/spec/presenters/ci/pipeline_artifacts/code_coverage_presenter_spec.rb similarity index 89% rename from spec/lib/gitlab/ci/pipeline/artifact/code_coverage_spec.rb rename to spec/presenters/ci/pipeline_artifacts/code_coverage_presenter_spec.rb index f3bc9fd0ec2..e679f5fa144 100644 --- a/spec/lib/gitlab/ci/pipeline/artifact/code_coverage_spec.rb +++ b/spec/presenters/ci/pipeline_artifacts/code_coverage_presenter_spec.rb @@ -2,12 +2,13 @@ require 'spec_helper' -RSpec.describe Gitlab::Ci::Pipeline::Artifact::CodeCoverage do +RSpec.describe Ci::PipelineArtifacts::CodeCoveragePresenter do let(:pipeline_artifact) { create(:ci_pipeline_artifact, :with_code_coverage_with_multiple_files) } - let(:code_coverage) { described_class.new(pipeline_artifact) } + + subject(:presenter) { described_class.new(pipeline_artifact) } describe '#for_files' do - subject { code_coverage.for_files(filenames) } + subject { presenter.for_files(filenames) } context 'when code coverage has data' do context 'when filenames is empty' do diff --git a/spec/rubocop/cop/migration/complex_indexes_require_name_spec.rb b/spec/rubocop/cop/migration/complex_indexes_require_name_spec.rb index 5804b4b00fc..b769109057e 100644 --- a/spec/rubocop/cop/migration/complex_indexes_require_name_spec.rb +++ b/spec/rubocop/cop/migration/complex_indexes_require_name_spec.rb @@ -14,40 +14,120 @@ RSpec.describe RuboCop::Cop::Migration::ComplexIndexesRequireName, type: :ruboco allow(cop).to receive(:in_migration?).and_return(true) end - context 'when indexes are configured with an options hash, but no name' do - it 'registers an offense' do - expect_offense(<<~RUBY) - class TestComplexIndexes < ActiveRecord::Migration[6.0] - DOWNTIME = false + context 'when creating complex indexes as part of create_table' do + context 'when indexes are configured with an options hash, but no name' do + it 'registers an offense' do + expect_offense(<<~RUBY) + class TestComplexIndexes < ActiveRecord::Migration[6.0] + DOWNTIME = false - INDEX_NAME = 'my_test_name' + def up + create_table :test_table do |t| + t.integer :column1, null: false + t.integer :column2, null: false + t.jsonb :column3 - disable_ddl_transaction! + t.index :column1, unique: true + t.index :column2, where: 'column1 = 0' + ^^^^^ #{described_class::MSG} + t.index :column3, using: :gin + ^^^^^ #{described_class::MSG} + end + end - def up - add_concurrent_index :test_indexes, :column1 - - add_concurrent_index :test_indexes, :column2, where: "column2 = 'value'", order: { column4: :desc } - ^^^^^^^^^^^^^^^^^^^^ #{described_class::MSG} - - add_concurrent_index :test_indexes, :column3, where: 'column3 = 10', name: 'idx_equal_to_10' + def down + drop_table :test_table + end end + RUBY - def down - add_concurrent_index :test_indexes, :column4, 'unique' => true + expect(cop.offenses.map(&:cop_name)).to all(eq("Migration/#{described_class.name.demodulize}")) + end + end - add_concurrent_index :test_indexes, :column4, 'unique' => true, where: 'column4 IS NOT NULL' - ^^^^^^^^^^^^^^^^^^^^ #{described_class::MSG} + context 'when indexes are configured with an options hash and name' do + it 'registers no offense' do + expect_no_offenses(<<~RUBY) + class TestComplexIndexes < ActiveRecord::Migration[6.0] + DOWNTIME = false - add_concurrent_index :test_indexes, :column5, using: :gin, name: INDEX_NAME + def up + create_table :test_table do |t| + t.integer :column1, null: false + t.integer :column2, null: false + t.jsonb :column3 - add_concurrent_index :test_indexes, :column6, using: :gin, opclass: :gin_trgm_ops - ^^^^^^^^^^^^^^^^^^^^ #{described_class::MSG} + t.index :column1, unique: true + t.index :column2, where: 'column1 = 0', name: 'my_index_1' + t.index :column3, using: :gin, name: 'my_gin_index' + end + end + + def down + drop_table :test_table + end end - end - RUBY + RUBY + end + end + end - expect(cop.offenses.map(&:cop_name)).to all(eq("Migration/#{described_class.name.demodulize}")) + context 'when indexes are added to an existing table' do + context 'when indexes are configured with an options hash, but no name' do + it 'registers an offense' do + expect_offense(<<~RUBY) + class TestComplexIndexes < ActiveRecord::Migration[6.0] + DOWNTIME = false + + disable_ddl_transaction! + + def up + add_index :test_indexes, :column1 + + add_index :test_indexes, :column2, where: "column2 = 'value'", order: { column4: :desc } + ^^^^^^^^^ #{described_class::MSG} + end + + def down + add_index :test_indexes, :column4, 'unique' => true, where: 'column4 IS NOT NULL' + ^^^^^^^^^ #{described_class::MSG} + + add_concurrent_index :test_indexes, :column6, using: :gin, opclass: :gin_trgm_ops + ^^^^^^^^^^^^^^^^^^^^ #{described_class::MSG} + end + end + RUBY + + expect(cop.offenses.map(&:cop_name)).to all(eq("Migration/#{described_class.name.demodulize}")) + end + end + + context 'when indexes are configured with an options hash and a name' do + it 'registers no offenses' do + expect_no_offenses(<<~RUBY) + class TestComplexIndexes < ActiveRecord::Migration[6.0] + DOWNTIME = false + + INDEX_NAME = 'my_test_name' + + disable_ddl_transaction! + + def up + add_index :test_indexes, :column1 + + add_index :test_indexes, :column2, where: "column2 = 'value'", order: { column4: :desc }, name: 'my_index_1' + + add_concurrent_index :test_indexes, :column3, where: 'column3 = 10', name: 'idx_equal_to_10' + end + + def down + add_index :test_indexes, :column4, 'unique' => true, where: 'column4 IS NOT NULL', name: 'my_index_2' + + add_concurrent_index :test_indexes, :column6, using: :gin, opclass: :gin_trgm_ops, name: INDEX_NAME + end + end + RUBY + end end end end @@ -65,7 +145,13 @@ RSpec.describe RuboCop::Cop::Migration::ComplexIndexesRequireName, type: :ruboco disable_ddl_transaction! def up - add_concurrent_index :test_indexes, :column1, where: "some_column = 'value'" + create_table :test_table do |t| + t.integer :column1 + + t.index :column1, where: 'column2 IS NOT NULL' + end + + add_index :test_indexes, :column1, where: "some_column = 'value'" end def down diff --git a/spec/services/ci/generate_coverage_reports_service_spec.rb b/spec/services/ci/generate_coverage_reports_service_spec.rb index 722b92ea3b6..d39053adebc 100644 --- a/spec/services/ci/generate_coverage_reports_service_spec.rb +++ b/spec/services/ci/generate_coverage_reports_service_spec.rb @@ -16,7 +16,8 @@ RSpec.describe Ci::GenerateCoverageReportsService do let!(:base_pipeline) { nil } it 'returns status and data', :aggregate_failures do - expect_next_instance_of(Gitlab::Ci::Pipeline::Artifact::CodeCoverage) do |instance| + expect_any_instance_of(Ci::PipelineArtifact) do |instance| + expect(instance).to receive(:present) expect(instance).to receive(:for_files).with(merge_request.new_paths).and_call_original end diff --git a/spec/support/shared_examples/services/packages_shared_examples.rb b/spec/support/shared_examples/services/packages_shared_examples.rb index 45a4c2bb151..836997e78f5 100644 --- a/spec/support/shared_examples/services/packages_shared_examples.rb +++ b/spec/support/shared_examples/services/packages_shared_examples.rb @@ -161,6 +161,7 @@ RSpec.shared_examples 'filters on each package_type' do |is_project: false| let_it_be(:package4) { create(:nuget_package, project: project) } let_it_be(:package5) { create(:pypi_package, project: project) } let_it_be(:package6) { create(:composer_package, project: project) } + let_it_be(:package7) { create(:generic_package, project: project) } Packages::Package.package_types.keys.each do |package_type| context "for package type #{package_type}" do diff --git a/spec/support/test_reports/test_reports_helper.rb b/spec/support/test_reports/test_reports_helper.rb index 6ba50c83b25..ad9ecb6f460 100644 --- a/spec/support/test_reports/test_reports_helper.rb +++ b/spec/support/test_reports/test_reports_helper.rb @@ -10,12 +10,12 @@ module TestReportsHelper status: Gitlab::Ci::Reports::TestCase::STATUS_SUCCESS) end - def create_test_case_rspec_failed(name = 'test_spec') + def create_test_case_rspec_failed(name = 'test_spec', execution_time = 2.22) Gitlab::Ci::Reports::TestCase.new( name: 'Test#sum when a is 1 and b is 3 returns summary', classname: "spec.#{name}", file: './spec/test_spec.rb', - execution_time: 2.22, + execution_time: execution_time, system_output: sample_rspec_failed_message, status: Gitlab::Ci::Reports::TestCase::STATUS_FAILED) end