diff --git a/Gemfile b/Gemfile index 56a5c5ddc94..09dc76c7f39 100644 --- a/Gemfile +++ b/Gemfile @@ -493,7 +493,7 @@ gem 'kas-grpc', '~> 0.0.2' gem 'grpc', '~> 1.42.0' -gem 'google-protobuf', '~> 3.19.0' +gem 'google-protobuf', '~> 3.21' gem 'toml-rb', '~> 2.0' diff --git a/Gemfile.lock b/Gemfile.lock index bcc30e31433..723b786a02e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -580,7 +580,7 @@ GEM signet (~> 0.12) google-cloud-env (1.5.0) faraday (>= 0.17.3, < 2.0) - google-protobuf (3.19.4) + google-protobuf (3.21.3) googleapis-common-protos-types (1.3.0) google-protobuf (~> 3.14) googleauth (0.14.0) @@ -1580,7 +1580,7 @@ DEPENDENCIES gitlab_omniauth-ldap (~> 2.2.0) gon (~> 6.4.0) google-api-client (~> 0.33) - google-protobuf (~> 3.19.0) + google-protobuf (~> 3.21) gpgme (~> 2.0.19) grape (~> 1.5.2) grape-entity (~> 0.10.0) diff --git a/app/assets/javascripts/api/groups_api.js b/app/assets/javascripts/api/groups_api.js index a563afc6abb..48cf346d0e6 100644 --- a/app/assets/javascripts/api/groups_api.js +++ b/app/assets/javascripts/api/groups_api.js @@ -2,6 +2,7 @@ import { DEFAULT_PER_PAGE } from '~/api'; import axios from '../lib/utils/axios_utils'; import { buildApiUrl } from './api_utils'; +const GROUP_PATH = '/api/:version/groups/:id'; const GROUPS_PATH = '/api/:version/groups.json'; const DESCENDANT_GROUPS_PATH = '/api/:version/groups/:id/descendant_groups'; @@ -30,3 +31,9 @@ export function getDescendentGroups(parentGroupId, query, options, callback = () const url = buildApiUrl(DESCENDANT_GROUPS_PATH.replace(':id', parentGroupId)); return axiosGet(url, query, options, callback); } + +export function updateGroup(groupId, data = {}) { + const url = buildApiUrl(GROUP_PATH).replace(':id', groupId); + + return axios.put(url, data); +} diff --git a/app/assets/javascripts/group_settings/components/shared_runners_form.vue b/app/assets/javascripts/group_settings/components/shared_runners_form.vue index 06aea26830d..8011090f1cb 100644 --- a/app/assets/javascripts/group_settings/components/shared_runners_form.vue +++ b/app/assets/javascripts/group_settings/components/shared_runners_form.vue @@ -1,6 +1,6 @@ diff --git a/app/assets/javascripts/work_items/components/work_item_weight.vue b/app/assets/javascripts/work_items/components/work_item_weight.vue index db97f16a010..b0ad7c97bb1 100644 --- a/app/assets/javascripts/work_items/components/work_item_weight.vue +++ b/app/assets/javascripts/work_items/components/work_item_weight.vue @@ -98,6 +98,7 @@ export default { } }, updateWeight(event) { + if (!this.canUpdate) return; this.isEditing = false; const weight = Number(event.target.value); diff --git a/app/helpers/ci/runners_helper.rb b/app/helpers/ci/runners_helper.rb index 74318797069..852eaeca5e3 100644 --- a/app/helpers/ci/runners_helper.rb +++ b/app/helpers/ci/runners_helper.rb @@ -73,7 +73,7 @@ module Ci def group_shared_runners_settings_data(group) { - update_path: api_v4_groups_path(id: group.id), + group_id: group.id, shared_runners_setting: group.shared_runners_setting, parent_shared_runners_setting: group.parent&.shared_runners_setting, runner_enabled_value: Namespace::SR_ENABLED, diff --git a/db/post_migrate/20220520040416_schedule_set_legacy_open_source_license_available_for_non_public_projects.rb b/db/post_migrate/20220520040416_schedule_set_legacy_open_source_license_available_for_non_public_projects.rb index 47b1c169d74..a9bb09b3378 100644 --- a/db/post_migrate/20220520040416_schedule_set_legacy_open_source_license_available_for_non_public_projects.rb +++ b/db/post_migrate/20220520040416_schedule_set_legacy_open_source_license_available_for_non_public_projects.rb @@ -1,31 +1,13 @@ # frozen_string_literal: true class ScheduleSetLegacyOpenSourceLicenseAvailableForNonPublicProjects < Gitlab::Database::Migration[2.0] - MIGRATION = 'SetLegacyOpenSourceLicenseAvailableForNonPublicProjects' - INTERVAL = 2.minutes - BATCH_SIZE = 5_000 - SUB_BATCH_SIZE = 200 - - disable_ddl_transaction! - restrict_gitlab_migration gitlab_schema: :gitlab_main def up - return unless Gitlab.com? - - queue_batched_background_migration( - MIGRATION, - :projects, - :id, - job_interval: INTERVAL, - batch_size: BATCH_SIZE, - sub_batch_size: SUB_BATCH_SIZE - ) + # Replaced by 20220722110026_reschedule_set_legacy_open_source_license_available_for_non_public_projects.rb end def down - return unless Gitlab.com? - - delete_batched_background_migration(MIGRATION, :projects, :id, []) + # Replaced by 20220722110026_reschedule_set_legacy_open_source_license_available_for_non_public_projects.rb end end diff --git a/db/post_migrate/20220722110026_reschedule_set_legacy_open_source_license_available_for_non_public_projects.rb b/db/post_migrate/20220722110026_reschedule_set_legacy_open_source_license_available_for_non_public_projects.rb new file mode 100644 index 00000000000..546923141e2 --- /dev/null +++ b/db/post_migrate/20220722110026_reschedule_set_legacy_open_source_license_available_for_non_public_projects.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +class RescheduleSetLegacyOpenSourceLicenseAvailableForNonPublicProjects < Gitlab::Database::Migration[2.0] + MIGRATION = 'SetLegacyOpenSourceLicenseAvailableForNonPublicProjects' + INTERVAL = 2.minutes + BATCH_SIZE = 5_000 + MAX_BATCH_SIZE = 10_000 + SUB_BATCH_SIZE = 200 + + disable_ddl_transaction! + + restrict_gitlab_migration gitlab_schema: :gitlab_main + + def up + return unless Gitlab.com? + + delete_batched_background_migration(MIGRATION, :projects, :id, []) + + queue_batched_background_migration( + MIGRATION, + :projects, + :id, + job_interval: INTERVAL, + batch_size: BATCH_SIZE, + max_batch_size: MAX_BATCH_SIZE, + sub_batch_size: SUB_BATCH_SIZE + ) + end + + def down + return unless Gitlab.com? + + delete_batched_background_migration(MIGRATION, :projects, :id, []) + end +end diff --git a/db/schema_migrations/20220722110026 b/db/schema_migrations/20220722110026 new file mode 100644 index 00000000000..56f4699cace --- /dev/null +++ b/db/schema_migrations/20220722110026 @@ -0,0 +1 @@ +79c7847740cb02fffeaeae55f869889f201b7a9431693bea7249ddff9d405fb4 \ No newline at end of file diff --git a/doc/api/pipeline_schedules.md b/doc/api/pipeline_schedules.md index 625a92f9b89..bbaf38aaaae 100644 --- a/doc/api/pipeline_schedules.md +++ b/doc/api/pipeline_schedules.md @@ -101,6 +101,60 @@ curl --header "PRIVATE-TOKEN: " "https://gitlab.example.com/a } ``` +## Get all pipelines triggered by a pipeline schedule + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/368566) in GitLab 15.3. + +Get all pipelines triggered by a pipeline schedule in a project. + +```plaintext +GET /projects/:id/pipeline_schedules/:pipeline_schedule_id/pipelines +``` + +Supported attributes: + +| Attribute | Type | Required | Description | +|------------------------|----------------|----------|-------------| +| `id` | integer/string | yes | The ID or [URL-encoded path of the project](index.md#namespaced-path-encoding) owned by the authenticated user. | +| `pipeline_schedule_id` | integer | yes | The pipeline schedule ID. | + +Example request: + +```shell +curl --header "PRIVATE-TOKEN: " "https://gitlab.example.com/api/v4/projects/29/pipeline_schedules/13/pipelines" +``` + +Example response: + +```json +[ + { + "id": 47, + "iid": 12, + "project_id": 29, + "status": "pending", + "source": "scheduled", + "ref": "new-pipeline", + "sha": "a91957a858320c0e17f3a0eca7cfacbff50ea29a", + "web_url": "https://example.com/foo/bar/pipelines/47", + "created_at": "2016-08-11T11:28:34.085Z", + "updated_at": "2016-08-11T11:32:35.169Z" + }, + { + "id": 48, + "iid": 13, + "project_id": 29, + "status": "pending", + "source": "scheduled", + "ref": "new-pipeline", + "sha": "eb94b618fb5865b26e80fdd8ae531b7a63ad851a", + "web_url": "https://example.com/foo/bar/pipelines/48", + "created_at": "2016-08-12T10:06:04.561Z", + "updated_at": "2016-08-12T10:09:56.223Z" + } +] +``` + ## Create a new pipeline schedule Create a new pipeline schedule of a project. diff --git a/doc/development/index.md b/doc/development/index.md index 1b897db5097..34e6f466664 100644 --- a/doc/development/index.md +++ b/doc/development/index.md @@ -7,9 +7,9 @@ info: "See the Technical Writers assigned to Development Guidelines: https://abo description: "Development Guidelines: learn how to contribute to GitLab." --- -# Contributor and Development Docs +# Contribute to the development of GitLab -Learn the processes and technical information needed for contributing to GitLab. +Learn how to contribute to the development of the GitLab product. This content is intended for members of the GitLab Team as well as community contributors. Content specific to the GitLab Team should instead be included in diff --git a/doc/development/testing_guide/end_to_end/running_tests_that_require_special_setup.md b/doc/development/testing_guide/end_to_end/running_tests_that_require_special_setup.md index 04849c548b6..322f108783f 100644 --- a/doc/development/testing_guide/end_to_end/running_tests_that_require_special_setup.md +++ b/doc/development/testing_guide/end_to_end/running_tests_that_require_special_setup.md @@ -385,6 +385,43 @@ To run the LDAP tests on your local with TLS disabled, follow these steps: GITLAB_LDAP_USERNAME="tanuki" GITLAB_LDAP_PASSWORD="password" QA_LOG_LEVEL=debug WEBDRIVER_HEADLESS=false bin/qa Test::Instance::All http://localhost qa/specs/features/browser_ui/1_manage/login/log_into_gitlab_via_ldap_spec.rb ``` +## SMTP tests + +Tests that are tagged with `:smtp` meta tag are orchestrated tests that ensure email notifications are received by a user. + +These tests require a GitLab instance with SMTP enabled and integrated with an SMTP server, [MailHog](https://github.com/mailhog/MailHog). + +To run these tests locally against the GDK: + +1. Add these settings to your `gitlab.yml` file: + + ```yaml + smtp: + enabled: true + address: "mailhog.test" + port: 1025 + ``` + +1. Start MailHog in a Docker container: + + ```shell + docker network create test && docker run \ + --network test \ + --hostname mailhog.test \ + --name mailhog \ + --publish 1025:1025 \ + --publish 8025:8025 \ + mailhog/mailhog:v1.0.0 + ``` + +1. Run the test from [`gitlab/qa`](https://gitlab.com/gitlab-org/gitlab/-/tree/d5447ebb5f99d4c72780681ddf4dc25b0738acba/qa) directory: + + ```shell + QA_LOG_LEVEL=debug WEBDRIVER_HEADLESS=false bin/qa Test::Instance::All http://localhost:3000 qa/specs/features/browser_ui/2_plan/email/trigger_email_notification_spec.rb -- --tag orchestrated + ``` + +For instructions on how to run these tests using the `gitlab-qa` gem, please refer to [the GitLab QA documentation](https://gitlab.com/gitlab-org/gitlab-qa/-/blob/master/docs/what_tests_can_be_run.md#testintegrationsmtp-ceeefull-image-address). + ## Guide to the mobile suite ### What are mobile tests diff --git a/lib/api/ci/pipeline_schedules.rb b/lib/api/ci/pipeline_schedules.rb index 4b522f37524..886c3509c51 100644 --- a/lib/api/ci/pipeline_schedules.rb +++ b/lib/api/ci/pipeline_schedules.rb @@ -42,6 +42,16 @@ module API present pipeline_schedule, with: Entities::Ci::PipelineScheduleDetails, user: current_user end + desc 'Get all pipelines triggered from a pipeline schedule' do + success Entities::Ci::PipelineBasic + end + params do + requires :pipeline_schedule_id, type: Integer, desc: 'The pipeline schedule ID' + end + get ':id/pipeline_schedules/:pipeline_schedule_id/pipelines' do + present paginate(pipeline_schedule.pipelines), with: Entities::Ci::PipelineBasic + end + desc 'Create a new pipeline schedule' do success Entities::Ci::PipelineScheduleDetails end diff --git a/lib/api/unleash.rb b/lib/api/unleash.rb index 2d528ad47a2..1fbd7cf5afc 100644 --- a/lib/api/unleash.rb +++ b/lib/api/unleash.rb @@ -33,8 +33,10 @@ module API end end + # We decrease the urgency of this endpoint until the maxmemory issue of redis-cache has been resolved. + # See https://gitlab.com/gitlab-org/gitlab/-/issues/365575#note_1033611872 for more information. desc 'Get a list of features' - get 'client/features' do + get 'client/features', urgency: :low do if ::Feature.enabled?(:cache_unleash_client_api, project) present_feature_flags else diff --git a/lib/gitlab/sidekiq_daemon/memory_killer.rb b/lib/gitlab/sidekiq_daemon/memory_killer.rb index cb7d9c6f8a7..ca92fed9c40 100644 --- a/lib/gitlab/sidekiq_daemon/memory_killer.rb +++ b/lib/gitlab/sidekiq_daemon/memory_killer.rb @@ -44,7 +44,8 @@ module Gitlab sidekiq_current_rss: ::Gitlab::Metrics.gauge(:sidekiq_current_rss, 'Current RSS of Sidekiq Worker'), sidekiq_memory_killer_soft_limit_rss: ::Gitlab::Metrics.gauge(:sidekiq_memory_killer_soft_limit_rss, 'Current soft_limit_rss of Sidekiq Worker'), sidekiq_memory_killer_hard_limit_rss: ::Gitlab::Metrics.gauge(:sidekiq_memory_killer_hard_limit_rss, 'Current hard_limit_rss of Sidekiq Worker'), - sidekiq_memory_killer_phase: ::Gitlab::Metrics.gauge(:sidekiq_memory_killer_phase, 'Current phase of Sidekiq Worker') + sidekiq_memory_killer_phase: ::Gitlab::Metrics.gauge(:sidekiq_memory_killer_phase, 'Current phase of Sidekiq Worker'), + sidekiq_memory_killer_running_jobs: ::Gitlab::Metrics.counter(:sidekiq_memory_killer_running_jobs_total, 'Current running jobs when limit was reached') } end @@ -166,6 +167,8 @@ module Gitlab @soft_limit_rss, deadline_exceeded) + running_jobs = fetch_running_jobs + Sidekiq.logger.warn( class: self.class.to_s, pid: pid, @@ -175,9 +178,17 @@ module Gitlab hard_limit_rss: @hard_limit_rss, reason: reason, running_jobs: running_jobs) + + increment_worker_counters(running_jobs, deadline_exceeded) end - def running_jobs + def increment_worker_counters(running_jobs, deadline_exceeded) + running_jobs.each do |job| + @metrics[:sidekiq_memory_killer_running_jobs].increment( { worker_class: job[:worker_class], deadline_exceeded: deadline_exceeded } ) + end + end + + def fetch_running_jobs jobs = [] Gitlab::SidekiqDaemon::Monitor.instance.jobs_mutex.synchronize do jobs = Gitlab::SidekiqDaemon::Monitor.instance.jobs.map do |jid, job| diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 5f0058380c0..1259f261f8d 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -1508,15 +1508,9 @@ msgstr "" msgid "2FADevice|Registered On" msgstr "" -msgid "3 days" -msgstr "" - msgid "3 hours" msgstr "" -msgid "30 days" -msgstr "" - msgid "30 minutes" msgstr "" @@ -1544,9 +1538,6 @@ msgstr "" msgid "409|There was a conflict with your request." msgstr "" -msgid "7 days" -msgstr "" - msgid "8 hours" msgstr "" diff --git a/spec/frontend/api/groups_api_spec.js b/spec/frontend/api/groups_api_spec.js new file mode 100644 index 00000000000..e14ead0b8eb --- /dev/null +++ b/spec/frontend/api/groups_api_spec.js @@ -0,0 +1,46 @@ +import MockAdapter from 'axios-mock-adapter'; +import httpStatus from '~/lib/utils/http_status'; +import axios from '~/lib/utils/axios_utils'; +import { updateGroup } from '~/api/groups_api'; + +const mockApiVersion = 'v4'; +const mockUrlRoot = '/gitlab'; + +describe('GroupsApi', () => { + let originalGon; + let mock; + + const dummyGon = { + api_version: mockApiVersion, + relative_url_root: mockUrlRoot, + }; + + beforeEach(() => { + mock = new MockAdapter(axios); + originalGon = window.gon; + window.gon = { ...dummyGon }; + }); + + afterEach(() => { + mock.restore(); + window.gon = originalGon; + }); + + describe('updateGroup', () => { + const mockGroupId = '99'; + const mockData = { attr: 'value' }; + const expectedUrl = `${mockUrlRoot}/api/${mockApiVersion}/groups/${mockGroupId}`; + + beforeEach(() => { + mock.onPut(expectedUrl).reply(({ data }) => { + return [httpStatus.OK, { id: mockGroupId, ...JSON.parse(data) }]; + }); + }); + + it('updates group', async () => { + const res = await updateGroup(mockGroupId, mockData); + + expect(res.data).toMatchObject({ id: mockGroupId, ...mockData }); + }); + }); +}); diff --git a/spec/frontend/group_settings/components/shared_runners_form_spec.js b/spec/frontend/group_settings/components/shared_runners_form_spec.js index 70a22c86e62..5282c0ed839 100644 --- a/spec/frontend/group_settings/components/shared_runners_form_spec.js +++ b/spec/frontend/group_settings/components/shared_runners_form_spec.js @@ -1,24 +1,24 @@ import { GlAlert } from '@gitlab/ui'; -import MockAxiosAdapter from 'axios-mock-adapter'; import { nextTick } from 'vue'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import waitForPromises from 'helpers/wait_for_promises'; import SharedRunnersForm from '~/group_settings/components/shared_runners_form.vue'; -import axios from '~/lib/utils/axios_utils'; +import { updateGroup } from '~/api/groups_api'; -const UPDATE_PATH = '/test/update'; +jest.mock('~/api/groups_api'); + +const GROUP_ID = '99'; const RUNNER_ENABLED_VALUE = 'enabled'; const RUNNER_DISABLED_VALUE = 'disabled_and_unoverridable'; const RUNNER_ALLOW_OVERRIDE_VALUE = 'disabled_with_override'; describe('group_settings/components/shared_runners_form', () => { let wrapper; - let mock; const createComponent = (provide = {}) => { wrapper = shallowMountExtended(SharedRunnersForm, { provide: { - updatePath: UPDATE_PATH, + groupId: GROUP_ID, sharedRunnersSetting: RUNNER_ENABLED_VALUE, parentSharedRunnersSetting: null, runnerEnabledValue: RUNNER_ENABLED_VALUE, @@ -36,18 +36,19 @@ describe('group_settings/components/shared_runners_form', () => { .at(0); const findSharedRunnersToggle = () => wrapper.findByTestId('shared-runners-toggle'); const findOverrideToggle = () => wrapper.findByTestId('override-runners-toggle'); - const getSharedRunnersSetting = () => JSON.parse(mock.history.put[0].data).shared_runners_setting; + const getSharedRunnersSetting = () => { + return updateGroup.mock.calls[0][1].shared_runners_setting; + }; beforeEach(() => { - mock = new MockAxiosAdapter(axios); - mock.onPut(UPDATE_PATH).reply(200); + updateGroup.mockResolvedValue({}); }); afterEach(() => { wrapper.destroy(); wrapper = null; - mock.restore(); + updateGroup.mockReset(); }); describe('default state', () => { @@ -115,7 +116,7 @@ describe('group_settings/components/shared_runners_form', () => { findSharedRunnersToggle().vm.$emit('change', false); await waitForPromises(); - expect(mock.history.put.length).toBe(1); + expect(updateGroup).toHaveBeenCalledTimes(1); }); it('is not loading state after completed request', async () => { @@ -170,12 +171,14 @@ describe('group_settings/components/shared_runners_form', () => { }); describe.each` - errorObj | message + errorData | message ${{}} | ${'An error occurred while updating configuration. Refresh the page and try again.'} ${{ error: 'Undefined error' }} | ${'Undefined error Refresh the page and try again.'} - `(`with error $errorObj`, ({ errorObj, message }) => { + `(`with error $errorObj`, ({ errorData, message }) => { beforeEach(async () => { - mock.onPut(UPDATE_PATH).reply(500, errorObj); + updateGroup.mockRejectedValue({ + response: { data: errorData }, + }); createComponent(); findSharedRunnersToggle().vm.$emit('change', false); diff --git a/spec/frontend/work_items/components/item_state_spec.js b/spec/frontend/work_items/components/item_state_spec.js index 79b76f3c061..c3cc2fbc556 100644 --- a/spec/frontend/work_items/components/item_state_spec.js +++ b/spec/frontend/work_items/components/item_state_spec.js @@ -1,3 +1,4 @@ +import { GlFormSelect } from '@gitlab/ui'; import { mount } from '@vue/test-utils'; import { STATE_OPEN, STATE_CLOSED } from '~/work_items/constants'; import ItemState from '~/work_items/components/item_state.vue'; @@ -6,6 +7,7 @@ describe('ItemState', () => { let wrapper; const findLabel = () => wrapper.find('label').text(); + const findFormSelect = () => wrapper.findComponent(GlFormSelect); const selectedValue = () => wrapper.find('option:checked').element.value; const clickOpen = () => wrapper.findAll('option').at(0).setSelected(); @@ -51,4 +53,18 @@ describe('ItemState', () => { expect(wrapper.emitted('changed')).toBeUndefined(); }); + + describe('form select disabled prop', () => { + describe.each` + description | disabled | value + ${'when not disabled'} | ${false} | ${undefined} + ${'when disabled'} | ${true} | ${'disabled'} + `('$description', ({ disabled, value }) => { + it(`renders form select component with disabled=${value}`, () => { + createComponent({ disabled }); + + expect(findFormSelect().attributes('disabled')).toBe(value); + }); + }); + }); }); diff --git a/spec/frontend/work_items/components/item_title_spec.js b/spec/frontend/work_items/components/item_title_spec.js index a55f448c9a2..de20369eb1b 100644 --- a/spec/frontend/work_items/components/item_title_spec.js +++ b/spec/frontend/work_items/components/item_title_spec.js @@ -37,7 +37,7 @@ describe('ItemTitle', () => { disabled: true, }); - expect(wrapper.classes()).toContain('gl-cursor-not-allowed'); + expect(wrapper.classes()).toContain('gl-cursor-text'); expect(findInputEl().attributes('contenteditable')).toBe('false'); }); diff --git a/spec/frontend/work_items/components/work_item_state_spec.js b/spec/frontend/work_items/components/work_item_state_spec.js index b379d1fc846..6b23a6e4795 100644 --- a/spec/frontend/work_items/components/work_item_state_spec.js +++ b/spec/frontend/work_items/components/work_item_state_spec.js @@ -29,6 +29,7 @@ describe('WorkItemState component', () => { const createComponent = ({ state = STATE_OPEN, mutationHandler = mutationSuccessHandler, + canUpdate = true, } = {}) => { const { id, workItemType } = workItemQueryResponse.data.workItem; wrapper = shallowMount(WorkItemState, { @@ -39,6 +40,7 @@ describe('WorkItemState component', () => { state, workItemType, }, + canUpdate, }, }); }; @@ -53,6 +55,20 @@ describe('WorkItemState component', () => { expect(findItemState().props('state')).toBe(workItemQueryResponse.data.workItem.state); }); + describe('item state disabled prop', () => { + describe.each` + description | canUpdate | value + ${'when cannot update'} | ${false} | ${true} + ${'when can update'} | ${true} | ${false} + `('$description', ({ canUpdate, value }) => { + it(`renders item state component with disabled=${value}`, () => { + createComponent({ canUpdate }); + + expect(findItemState().props('disabled')).toBe(value); + }); + }); + }); + describe('when updating the state', () => { it('calls a mutation', () => { createComponent(); diff --git a/spec/frontend/work_items/components/work_item_title_spec.js b/spec/frontend/work_items/components/work_item_title_spec.js index a48449bb636..c0d966abab8 100644 --- a/spec/frontend/work_items/components/work_item_title_spec.js +++ b/spec/frontend/work_items/components/work_item_title_spec.js @@ -20,7 +20,11 @@ describe('WorkItemTitle component', () => { const findItemTitle = () => wrapper.findComponent(ItemTitle); - const createComponent = ({ workItemParentId, mutationHandler = mutationSuccessHandler } = {}) => { + const createComponent = ({ + workItemParentId, + mutationHandler = mutationSuccessHandler, + canUpdate = true, + } = {}) => { const { id, title, workItemType } = workItemQueryResponse.data.workItem; wrapper = shallowMount(WorkItemTitle, { apolloProvider: createMockApollo([ @@ -32,6 +36,7 @@ describe('WorkItemTitle component', () => { workItemTitle: title, workItemType: workItemType.name, workItemParentId, + canUpdate, }, }); }; @@ -46,6 +51,20 @@ describe('WorkItemTitle component', () => { expect(findItemTitle().props('title')).toBe(workItemQueryResponse.data.workItem.title); }); + describe('item title disabled prop', () => { + describe.each` + description | canUpdate | value + ${'when cannot update'} | ${false} | ${true} + ${'when can update'} | ${true} | ${false} + `('$description', ({ canUpdate, value }) => { + it(`renders item title component with disabled=${value}`, () => { + createComponent({ canUpdate }); + + expect(findItemTitle().props('disabled')).toBe(value); + }); + }); + }); + describe('when updating the title', () => { it('calls a mutation', () => { const title = 'new title!'; diff --git a/spec/frontend/work_items/components/work_item_weight_spec.js b/spec/frontend/work_items/components/work_item_weight_spec.js index 8fd2280ff19..94bdb336deb 100644 --- a/spec/frontend/work_items/components/work_item_weight_spec.js +++ b/spec/frontend/work_items/components/work_item_weight_spec.js @@ -135,7 +135,12 @@ describe('WorkItemWeight component', () => { describe('when blurred', () => { it('calls a mutation to update the weight when the input value is different', () => { const mutationSpy = jest.fn().mockResolvedValue(updateWorkItemMutationResponse); - createComponent({ isEditing: true, weight: 0, mutationHandler: mutationSpy }); + createComponent({ + isEditing: true, + weight: 0, + mutationHandler: mutationSpy, + canUpdate: true, + }); findInput().vm.$emit('blur', { target: { value: 1 } }); @@ -151,7 +156,7 @@ describe('WorkItemWeight component', () => { it('does not call a mutation to update the weight when the input value is the same', () => { const mutationSpy = jest.fn().mockResolvedValue(updateWorkItemMutationResponse); - createComponent({ isEditing: true, mutationHandler: mutationSpy }); + createComponent({ isEditing: true, mutationHandler: mutationSpy, canUpdate: true }); findInput().trigger('blur'); @@ -170,6 +175,7 @@ describe('WorkItemWeight component', () => { createComponent({ isEditing: true, mutationHandler: jest.fn().mockResolvedValue(response), + canUpdate: true, }); findInput().trigger('blur'); @@ -182,6 +188,7 @@ describe('WorkItemWeight component', () => { createComponent({ isEditing: true, mutationHandler: jest.fn().mockRejectedValue(new Error()), + canUpdate: true, }); findInput().trigger('blur'); @@ -192,7 +199,7 @@ describe('WorkItemWeight component', () => { it('tracks updating the weight', () => { const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn); - createComponent(); + createComponent({ canUpdate: true }); findInput().trigger('blur'); diff --git a/spec/helpers/ci/runners_helper_spec.rb b/spec/helpers/ci/runners_helper_spec.rb index 4d1b1c7682c..3b18572ad64 100644 --- a/spec/helpers/ci/runners_helper_spec.rb +++ b/spec/helpers/ci/runners_helper_spec.rb @@ -109,7 +109,7 @@ RSpec.describe Ci::RunnersHelper do it 'returns group data for top level group' do result = { - update_path: "/api/v4/groups/#{parent.id}", + group_id: parent.id, shared_runners_setting: Namespace::SR_ENABLED, parent_shared_runners_setting: nil }.merge(runner_constants) @@ -119,7 +119,7 @@ RSpec.describe Ci::RunnersHelper do it 'returns group data for child group' do result = { - update_path: "/api/v4/groups/#{group.id}", + group_id: group.id, shared_runners_setting: Namespace::SR_DISABLED_AND_UNOVERRIDABLE, parent_shared_runners_setting: Namespace::SR_ENABLED }.merge(runner_constants) diff --git a/spec/lib/gitlab/background_migration/set_legacy_open_source_license_available_for_non_public_projects_spec.rb b/spec/lib/gitlab/background_migration/set_legacy_open_source_license_available_for_non_public_projects_spec.rb index 035ea6eadcf..e9f73672144 100644 --- a/spec/lib/gitlab/background_migration/set_legacy_open_source_license_available_for_non_public_projects_spec.rb +++ b/spec/lib/gitlab/background_migration/set_legacy_open_source_license_available_for_non_public_projects_spec.rb @@ -4,14 +4,14 @@ require 'spec_helper' RSpec.describe Gitlab::BackgroundMigration::SetLegacyOpenSourceLicenseAvailableForNonPublicProjects, :migration, - schema: 20220520040416 do + schema: 20220722110026 do let(:namespaces_table) { table(:namespaces) } let(:projects_table) { table(:projects) } let(:project_settings_table) { table(:project_settings) } subject(:perform_migration) do - described_class.new(start_id: 1, - end_id: 30, + described_class.new(start_id: projects_table.minimum(:id), + end_id: projects_table.maximum(:id), batch_table: :projects, batch_column: :id, sub_batch_size: 2, @@ -20,35 +20,34 @@ RSpec.describe Gitlab::BackgroundMigration::SetLegacyOpenSourceLicenseAvailableF .perform end - let(:queries) { ActiveRecord::QueryRecorder.new { perform_migration } } - - before do - namespaces_table.create!(id: 1, name: 'namespace', path: 'namespace-path-1') - namespaces_table.create!(id: 2, name: 'namespace', path: 'namespace-path-2', type: 'Project') - namespaces_table.create!(id: 3, name: 'namespace', path: 'namespace-path-3', type: 'Project') - namespaces_table.create!(id: 4, name: 'namespace', path: 'namespace-path-4', type: 'Project') - - projects_table - .create!(id: 11, name: 'proj-1', path: 'path-1', namespace_id: 1, project_namespace_id: 2, visibility_level: 0) - projects_table - .create!(id: 12, name: 'proj-2', path: 'path-2', namespace_id: 1, project_namespace_id: 3, visibility_level: 10) - projects_table - .create!(id: 13, name: 'proj-3', path: 'path-3', namespace_id: 1, project_namespace_id: 4, visibility_level: 20) - - project_settings_table.create!(project_id: 11, legacy_open_source_license_available: true) - project_settings_table.create!(project_id: 12, legacy_open_source_license_available: true) - project_settings_table.create!(project_id: 13, legacy_open_source_license_available: true) - end - it 'sets `legacy_open_source_license_available` attribute to false for non-public projects', :aggregate_failures do - expect(queries.count).to eq(3) + private_project = create_legacy_license_project('private-project', visibility_level: 0) + internal_project = create_legacy_license_project('internal-project', visibility_level: 10) + public_project = create_legacy_license_project('public-project', visibility_level: 20) - expect(migrated_attribute(11)).to be_falsey - expect(migrated_attribute(12)).to be_falsey - expect(migrated_attribute(13)).to be_truthy + queries = ActiveRecord::QueryRecorder.new { perform_migration } + + expect(queries.count).to eq(5) + + expect(migrated_attribute(private_project)).to be_falsey + expect(migrated_attribute(internal_project)).to be_falsey + expect(migrated_attribute(public_project)).to be_truthy end - def migrated_attribute(project_id) - project_settings_table.find(project_id).legacy_open_source_license_available + def create_legacy_license_project(path, visibility_level:) + namespace = namespaces_table.create!(name: "namespace-#{path}", path: "namespace-#{path}") + project_namespace = namespaces_table.create!(name: "project-namespace-#{path}", path: path, type: 'Project') + project = projects_table.create!(name: path, + path: path, + namespace_id: namespace.id, + project_namespace_id: project_namespace.id, + visibility_level: visibility_level) + project_settings_table.create!(project_id: project.id, legacy_open_source_license_available: true) + + project + end + + def migrated_attribute(project) + project_settings_table.find(project.id).legacy_open_source_license_available end end diff --git a/spec/lib/gitlab/sidekiq_daemon/memory_killer_spec.rb b/spec/lib/gitlab/sidekiq_daemon/memory_killer_spec.rb index 01b7270d761..b3de80ae3bf 100644 --- a/spec/lib/gitlab/sidekiq_daemon/memory_killer_spec.rb +++ b/spec/lib/gitlab/sidekiq_daemon/memory_killer_spec.rb @@ -355,6 +355,7 @@ RSpec.describe Gitlab::SidekiqDaemon::MemoryKiller do let(:reason) { 'rss out of range reason description' } let(:queue) { 'default' } let(:running_jobs) { [{ jid: jid, worker_class: 'DummyWorker' }] } + let(:metrics) { memory_killer.instance_variable_get(:@metrics) } let(:worker) do Class.new do def self.name @@ -390,6 +391,9 @@ RSpec.describe Gitlab::SidekiqDaemon::MemoryKiller do reason: reason, running_jobs: running_jobs) + expect(metrics[:sidekiq_memory_killer_running_jobs]).to receive(:increment) + .with({ worker_class: "DummyWorker", deadline_exceeded: true }) + Gitlab::SidekiqDaemon::Monitor.instance.within_job(DummyWorker, jid, queue) do subject end diff --git a/spec/migrations/20220520040416_schedule_set_legacy_open_source_license_available_for_non_public_projects_spec.rb b/spec/migrations/20220722110026_reschedule_set_legacy_open_source_license_available_for_non_public_projects_spec.rb similarity index 86% rename from spec/migrations/20220520040416_schedule_set_legacy_open_source_license_available_for_non_public_projects_spec.rb rename to spec/migrations/20220722110026_reschedule_set_legacy_open_source_license_available_for_non_public_projects_spec.rb index e3bc832a10b..99a30c7f2a9 100644 --- a/spec/migrations/20220520040416_schedule_set_legacy_open_source_license_available_for_non_public_projects_spec.rb +++ b/spec/migrations/20220722110026_reschedule_set_legacy_open_source_license_available_for_non_public_projects_spec.rb @@ -3,8 +3,8 @@ require 'spec_helper' require_migration! -RSpec.describe ScheduleSetLegacyOpenSourceLicenseAvailableForNonPublicProjects do - context 'on gitlab.com' do +RSpec.describe RescheduleSetLegacyOpenSourceLicenseAvailableForNonPublicProjects do + context 'when on gitlab.com' do let(:migration) { described_class::MIGRATION } before do @@ -21,6 +21,7 @@ RSpec.describe ScheduleSetLegacyOpenSourceLicenseAvailableForNonPublicProjects d column_name: :id, interval: described_class::INTERVAL, batch_size: described_class::BATCH_SIZE, + max_batch_size: described_class::MAX_BATCH_SIZE, sub_batch_size: described_class::SUB_BATCH_SIZE ) ) @@ -37,7 +38,7 @@ RSpec.describe ScheduleSetLegacyOpenSourceLicenseAvailableForNonPublicProjects d end end - context 'on self-managed instance' do + context 'when on self-managed instance' do let(:migration) { described_class.new } before do diff --git a/spec/requests/api/ci/pipeline_schedules_spec.rb b/spec/requests/api/ci/pipeline_schedules_spec.rb index 5fb94976c5f..30badadde13 100644 --- a/spec/requests/api/ci/pipeline_schedules_spec.rb +++ b/spec/requests/api/ci/pipeline_schedules_spec.rb @@ -98,7 +98,7 @@ RSpec.describe API::Ci::PipelineSchedules do end matcher :return_pipeline_schedule_sucessfully do - match_unless_raises do |reponse| + match_unless_raises do |response| expect(response).to have_gitlab_http_status(:ok) expect(response).to match_response_schema('pipeline_schedule') end @@ -207,6 +207,110 @@ RSpec.describe API::Ci::PipelineSchedules do end end + describe 'GET /projects/:id/pipeline_schedules/:pipeline_schedule_id/pipelines' do + let(:pipeline_schedule) { create(:ci_pipeline_schedule, project: project, owner: developer) } + + before do + create_list(:ci_pipeline, 2, project: project, pipeline_schedule: pipeline_schedule, source: :schedule) + end + + let(:url) { "/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}/pipelines" } + + matcher :return_pipeline_schedule_pipelines_successfully do + match_unless_raises do |reponse| + expect(response).to have_gitlab_http_status(:ok) + expect(response).to include_pagination_headers + expect(response).to match_response_schema('public_api/v4/pipelines') + end + end + + shared_examples 'request with project permissions' do + context 'authenticated user with project permissions' do + before do + project.add_maintainer(user) + end + + it 'returns the details of pipelines triggered from the pipeline schedule' do + get api(url, user) + + expect(response).to return_pipeline_schedule_pipelines_successfully + end + end + end + + shared_examples 'request with schedule ownership' do + context 'authenticated user with pipeline schedule ownership' do + it 'returns the details of pipelines triggered from the pipeline schedule' do + get api(url, developer) + + expect(response).to return_pipeline_schedule_pipelines_successfully + end + end + end + + shared_examples 'request with unauthenticated user' do + context 'with unauthenticated user' do + it 'does not return the details of pipelines triggered from the pipeline schedule' do + get api(url) + + expect(response).to have_gitlab_http_status(:unauthorized) + end + end + end + + shared_examples 'request with non-existing pipeline_schedule' do + it "responds with 404 Not Found if requesting for a non-existing pipeline schedule's pipelines" do + get api("/projects/#{project.id}/pipeline_schedules/#{non_existing_record_id}/pipelines", developer) + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'with private project' do + it_behaves_like 'request with schedule ownership' + it_behaves_like 'request with project permissions' + it_behaves_like 'request with unauthenticated user' + it_behaves_like 'request with non-existing pipeline_schedule' + + context 'authenticated user with no project permissions' do + it 'does not return the details of pipelines triggered from the pipeline schedule' do + get api(url, user) + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'authenticated user with insufficient project permissions' do + before do + project.add_guest(user) + end + + it 'does not return the details of pipelines triggered from the pipeline schedule' do + get api(url, user) + + expect(response).to have_gitlab_http_status(:not_found) + end + end + end + + context 'with public project' do + let_it_be(:project) { create(:project, :repository, :public, public_builds: false) } + + it_behaves_like 'request with schedule ownership' + it_behaves_like 'request with project permissions' + it_behaves_like 'request with unauthenticated user' + it_behaves_like 'request with non-existing pipeline_schedule' + + context 'authenticated user with no project permissions' do + it 'returns the details of pipelines triggered from the pipeline schedule' do + get api(url, user) + + expect(response).to return_pipeline_schedule_pipelines_successfully + end + end + end + end + describe 'POST /projects/:id/pipeline_schedules' do let(:params) { attributes_for(:ci_pipeline_schedule) }