Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-07-28 09:10:54 +00:00
parent ce42a2ec96
commit 8018200540
37 changed files with 489 additions and 119 deletions

View File

@ -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'

View File

@ -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)

View File

@ -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);
}

View File

@ -1,6 +1,6 @@
<script>
import { GlToggle, GlAlert } from '@gitlab/ui';
import axios from '~/lib/utils/axios_utils';
import { updateGroup } from '~/api/groups_api';
import { I18N_UPDATE_ERROR_MESSAGE, I18N_REFRESH_MESSAGE } from '../constants';
export default {
@ -9,7 +9,7 @@ export default {
GlAlert,
},
inject: [
'updatePath',
'groupId',
'sharedRunnersSetting',
'parentSharedRunnersSetting',
'runnerEnabledValue',
@ -54,8 +54,7 @@ export default {
this.isLoading = true;
axios
.put(this.updatePath, { shared_runners_setting: setting })
updateGroup(this.groupId, { shared_runners_setting: setting })
.then(() => {
this.value = setting;
})

View File

@ -5,7 +5,7 @@ export default (containerId = 'update-shared-runners-form') => {
const containerEl = document.getElementById(containerId);
const {
updatePath,
groupId,
sharedRunnersSetting,
parentSharedRunnersSetting,
runnerEnabledValue,
@ -16,7 +16,7 @@ export default (containerId = 'update-shared-runners-form') => {
return new Vue({
el: containerEl,
provide: {
updatePath,
groupId,
sharedRunnersSetting,
parentSharedRunnersSetting,
runnerEnabledValue,

View File

@ -91,7 +91,7 @@ export default {
modalId: 'set-user-status-modal',
noEmoji: true,
availability: isUserBusy(this.currentAvailability),
clearStatusAfter: statusTimeRanges[0].label,
clearStatusAfter: statusTimeRanges[0],
clearStatusAfterMessage: sprintf(s__('SetStatusModal|Your status resets on %{date}.'), {
date: this.currentClearStatusAfter,
}),
@ -178,9 +178,7 @@ export default {
message,
availability: availability ? AVAILABILITY_STATUS.BUSY : AVAILABILITY_STATUS.NOT_SET,
clearStatusAfter:
clearStatusAfter === statusTimeRanges[0].label
? null
: clearStatusAfter.replace(' ', '_'),
clearStatusAfter.label === statusTimeRanges[0].label ? null : clearStatusAfter.shortcut,
})
.then(this.onUpdateSuccess)
.catch(this.onUpdateFail);
@ -279,12 +277,12 @@ export default {
<div class="form-group">
<div class="gl-display-flex gl-align-items-baseline">
<span class="gl-mr-3">{{ s__('SetStatusModal|Clear status after') }}</span>
<gl-dropdown :text="clearStatusAfter" data-testid="clear-status-at-dropdown">
<gl-dropdown :text="clearStatusAfter.label" data-testid="clear-status-at-dropdown">
<gl-dropdown-item
v-for="after in $options.statusTimeRanges"
:key="after.name"
:data-testid="after.name"
@click="setClearStatusAfter(after.label)"
@click="setClearStatusAfter(after)"
>{{ after.label }}</gl-dropdown-item
>
</gl-dropdown>

View File

@ -1,4 +1,4 @@
import { __, sprintf } from '~/locale';
import { __, n__, sprintf } from '~/locale';
import { IssuableType, WorkspaceType } from '~/issues/constants';
const INTERVALS = {
@ -15,51 +15,62 @@ export const ISO_SHORT_FORMAT = 'yyyy-mm-dd';
export const DATE_FORMATS = [SHORT_DATE_FORMAT, ISO_SHORT_FORMAT];
const getTimeLabel = (days) => n__('1 day', '%d days', days);
/* eslint-disable @gitlab/require-i18n-strings */
export const timeRanges = [
{
label: __('30 minutes'),
label: n__('1 minute', '%d minutes', 30),
shortcut: '30_minutes',
duration: { seconds: 60 * 30 },
name: 'thirtyMinutes',
interval: INTERVALS.minute,
},
{
label: __('3 hours'),
label: n__('1 hour', '%d hours', 3),
shortcut: '3_hours',
duration: { seconds: 60 * 60 * 3 },
name: 'threeHours',
interval: INTERVALS.hour,
},
{
label: __('8 hours'),
label: n__('1 hour', '%d hours', 8),
shortcut: '8_hours',
duration: { seconds: 60 * 60 * 8 },
name: 'eightHours',
default: true,
interval: INTERVALS.hour,
},
{
label: __('1 day'),
label: getTimeLabel(1),
shortcut: '1_day',
duration: { seconds: 60 * 60 * 24 * 1 },
name: 'oneDay',
interval: INTERVALS.hour,
},
{
label: __('3 days'),
label: getTimeLabel(3),
shortcut: '3_days',
duration: { seconds: 60 * 60 * 24 * 3 },
name: 'threeDays',
interval: INTERVALS.hour,
},
{
label: __('7 days'),
label: getTimeLabel(7),
shortcut: '7_days',
duration: { seconds: 60 * 60 * 24 * 7 * 1 },
name: 'oneWeek',
interval: INTERVALS.day,
},
{
label: __('30 days'),
label: getTimeLabel(30),
shortcut: '30_days',
duration: { seconds: 60 * 60 * 24 * 30 },
name: 'oneMonth',
interval: INTERVALS.day,
},
];
/* eslint-enable @gitlab/require-i18n-strings */
export const defaultTimeRange = timeRanges.find((tr) => tr.default);
export const getTimeWindow = (timeWindowName) =>

View File

@ -26,7 +26,7 @@ export default {
type: String,
required: true,
},
loading: {
disabled: {
type: Boolean,
required: false,
default: false,
@ -61,15 +61,17 @@ export default {
:id="$options.labelId"
:value="state"
:options="$options.states"
:disabled="loading"
:disabled="disabled"
class="gl-w-auto hide-select-decoration"
:class="{ 'gl-bg-transparent! gl-cursor-text!': disabled }"
@change="setState"
/>
</gl-form-group>
</template>
<style>
.hide-select-decoration:not(:focus, :hover) {
.hide-select-decoration:not(:focus, :hover),
.hide-select-decoration:disabled {
background-image: none;
box-shadow: none;
}

View File

@ -36,7 +36,7 @@ export default {
<template>
<h2
class="gl-font-weight-normal gl-sm-font-weight-bold gl-mb-5 gl-mt-0 gl-w-full"
:class="{ 'gl-cursor-not-allowed': disabled }"
:class="{ 'gl-cursor-text': disabled }"
aria-labelledby="item-title"
>
<div
@ -46,7 +46,8 @@ export default {
:aria-label="__('Title')"
:data-placeholder="placeholder"
:contenteditable="!disabled"
class="gl-pseudo-placeholder gl-px-4 gl-py-3 gl-ml-n4 gl-border gl-border-white gl-hover-border-gray-200 gl-rounded-base"
class="gl-px-4 gl-py-3 gl-ml-n4 gl-border gl-border-white gl-rounded-base"
:class="{ 'gl-hover-border-gray-200 gl-pseudo-placeholder': !disabled }"
@blur="handleBlur"
@keyup="handleInput"
@keydown.enter.exact="handleSubmit"

View File

@ -206,11 +206,13 @@ export default {
:work-item-title="workItem.title"
:work-item-type="workItemType"
:work-item-parent-id="workItemParentId"
:can-update="canUpdate"
@error="error = $event"
/>
<work-item-state
:work-item="workItem"
:work-item-parent-id="workItemParentId"
:can-update="canUpdate"
@error="error = $event"
/>
<template v-if="workItemsMvc2Enabled">

View File

@ -202,7 +202,8 @@ export default {
:dropdown-items="searchLabels"
:loading="isLoading"
:view-only="!canUpdate"
class="gl-flex-grow-1 gl-border gl-border-white gl-hover-border-gray-200 gl-rounded-base col-9 gl-align-self-start gl-px-0! gl-mx-2!"
class="gl-flex-grow-1 gl-border gl-border-white gl-rounded-base col-9 gl-align-self-start gl-px-0! gl-mx-2!"
:class="{ 'gl-hover-border-gray-200': canUpdate }"
@input="focusTokenSelector"
@text-input="debouncedSearchKeyUpdate"
@focus="handleFocus"

View File

@ -27,6 +27,11 @@ export default {
required: false,
default: null,
},
canUpdate: {
type: Boolean,
required: false,
default: false,
},
},
data() {
return {
@ -102,7 +107,7 @@ export default {
<item-state
v-if="workItem.state"
:state="workItem.state"
:loading="updateInProgress"
:disabled="updateInProgress || !canUpdate"
@changed="updateWorkItemState"
/>
</template>

View File

@ -31,6 +31,11 @@ export default {
required: false,
default: null,
},
canUpdate: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
tracking() {
@ -84,5 +89,5 @@ export default {
</script>
<template>
<item-title :title="workItemTitle" @title-changed="updateTitle" />
<item-title :title="workItemTitle" :disabled="!canUpdate" @title-changed="updateTitle" />
</template>

View File

@ -98,6 +98,7 @@ export default {
}
},
updateWeight(event) {
if (!this.canUpdate) return;
this.isEditing = false;
const weight = Number(event.target.value);

View File

@ -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,

View File

@ -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

View File

@ -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

View File

@ -0,0 +1 @@
79c7847740cb02fffeaeae55f869889f201b7a9431693bea7249ddff9d405fb4

View File

@ -101,6 +101,60 @@ curl --header "PRIVATE-TOKEN: <your_access_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: <your_access_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.

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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|

View File

@ -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 ""

View File

@ -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 });
});
});
});

View File

@ -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);

View File

@ -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);
});
});
});
});

View File

@ -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');
});

View File

@ -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();

View File

@ -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!';

View File

@ -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');

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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) }