Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-11-29 06:08:59 +00:00
parent f51c6a69f9
commit 63c450b3e4
29 changed files with 616 additions and 145 deletions

View File

@ -0,0 +1,37 @@
<script>
import { GlAlert, GlSprintf, GlLink } from '@gitlab/ui';
import { GROUP_MODAL_ALERT_BODY } from '../constants';
const SHARE_GROUP_LINK =
'https://docs.gitlab.com/ee/user/group/manage.html#share-a-group-with-another-group';
export default {
SHARE_GROUP_LINK,
name: 'InviteGroupNotification',
components: { GlAlert, GlSprintf, GlLink },
inject: ['freeUsersLimit'],
props: {
name: {
type: String,
required: true,
},
},
i18n: {
body: GROUP_MODAL_ALERT_BODY,
},
};
</script>
<template>
<gl-alert variant="warning" :dismissible="false">
<gl-sprintf :message="$options.i18n.body">
<template #link="{ content }">
<gl-link :href="$options.SHARE_GROUP_LINK" target="_blank" class="gl-label-link">{{
content
}}</gl-link>
</template>
<template #count>{{ freeUsersLimit }}</template>
</gl-sprintf>
</gl-alert>
</template>

View File

@ -7,12 +7,14 @@ import { GROUP_FILTERS, GROUP_MODAL_LABELS } from '../constants';
import eventHub from '../event_hub';
import { getInvalidFeedbackMessage } from '../utils/get_invalid_feedback_message';
import GroupSelect from './group_select.vue';
import InviteGroupNotification from './invite_group_notification.vue';
export default {
name: 'InviteMembersModal',
components: {
GroupSelect,
InviteModalBase,
InviteGroupNotification,
},
props: {
id: {
@ -61,6 +63,10 @@ export default {
type: Array,
required: true,
},
freeUserCapEnabled: {
type: Boolean,
required: true,
},
},
data() {
return {
@ -163,6 +169,10 @@ export default {
@reset="resetFields"
@submit="sendInvite"
>
<template #alert>
<invite-group-notification v-if="freeUserCapEnabled" :name="name" />
</template>
<template #select>
<group-select
v-model="groupToBeSharedWith"

View File

@ -57,6 +57,10 @@ export const GROUP_MODAL_TO_PROJECT_DEFAULT_INTRO_TEXT = s__(
"InviteMembersModal|You're inviting a group to the %{strongStart}%{name}%{strongEnd} project.",
);
export const GROUP_MODAL_ALERT_BODY = s__(
'InviteMembersModal| Inviting a group %{linkStart}adds its members to your group%{linkEnd}, including members who join after the invite. This might put your group over the free %{count} user limit.',
);
export const GROUP_SEARCH_FIELD = s__('InviteMembersModal|Select a group to invite');
export const GROUP_PLACEHOLDER = s__('InviteMembersModal|Search for a group to invite');

View File

@ -28,6 +28,9 @@ export default function initInviteGroupsModal() {
return new Vue({
el,
provide: {
freeUsersLimit: parseInt(el.dataset.freeUsersLimit, 10),
},
render: (createElement) =>
createElement(InviteGroupsModal, {
props: {
@ -38,6 +41,7 @@ export default function initInviteGroupsModal() {
groupSelectFilter: el.dataset.groupsFilter,
groupSelectParentId: parseInt(el.dataset.parentId, 10),
invalidGroups: JSON.parse(el.dataset.invalidGroups || '[]'),
freeUserCapEnabled: parseBoolean(el.dataset.freeUserCapEnabled),
},
}),
});

View File

@ -10,6 +10,7 @@ module Ci
def initialize(current_user:, params:)
@params = params
@group = params.delete(:group)
@project = params.delete(:project)
@current_user = current_user
end
@ -36,7 +37,13 @@ module Ci
private
def search!
@group ? group_runners : all_runners
if @project && Feature.enabled?(:on_demand_scans_runner_tags, @project)
project_runners
elsif @group
group_runners
else
all_runners
end
@runners = @runners.search(@params[:search]) if @params[:search].present?
end
@ -66,6 +73,12 @@ module Ci
end
end
def project_runners
raise Gitlab::Access::AccessDeniedError unless can?(@current_user, :admin_project, @project)
@runners = ::Ci::Runner.owned_or_instance_wide(@project.id)
end
def filter_by_active!
@runners = @runners.active(@params[:active]) if @params.include?(:active)
end

View File

@ -0,0 +1,15 @@
# frozen_string_literal: true
module Resolvers
module Ci
class ProjectRunnersResolver < RunnersResolver
type Types::Ci::RunnerType.connection_type, null: true
def parent_param
raise 'Expected project missing' unless parent.is_a?(Project)
{ project: parent }
end
end
end
end

View File

@ -540,6 +540,11 @@ module Types
description: "Programming languages used in the project.",
calls_gitaly: true
field :runners, Types::Ci::RunnerType.connection_type,
null: true,
resolver: ::Resolvers::Ci::ProjectRunnersResolver,
description: "Find runners visible to the current user."
def timelog_categories
object.project_namespace.timelog_categories if Feature.enabled?(:timelog_categories)
end

View File

@ -20,6 +20,7 @@ module InviteMembersHelper
end
end
# Overridden in EE
def common_invite_group_modal_data(source, member_class, is_project)
{
id: source.id,

View File

@ -1,44 +0,0 @@
# frozen_string_literal: true
# IssueCollection can be used to reduce a list of issues down to a subset.
#
# IssueCollection is not meant to be some sort of Enumerable, instead it's meant
# to take a list of issues and return a new list of issues based on some
# criteria. For example, given a list of issues you may want to return a list of
# issues that can be read or updated by a given user.
class IssueCollection
attr_reader :collection
def initialize(collection)
@collection = collection
end
# Returns all the issues that can be updated by the user.
def updatable_by_user(user)
return collection if user.admin?
# Given all the issue projects we get a list of projects that the current
# user has at least reporter access to.
projects_with_reporter_access = user
.projects_with_reporter_access_limited_to(project_ids)
.pluck(:id)
collection.select do |issue|
if projects_with_reporter_access.include?(issue.project_id)
true
elsif issue.is_a?(Issue)
issue.assignee_or_author?(user)
else
false
end
end
end
alias_method :visible_to, :updatable_by_user
private
def project_ids
@project_ids ||= collection.map(&:project_id).uniq
end
end

View File

@ -9,7 +9,7 @@ class IssuePolicy < IssuablePolicy
desc "User can read confidential issues"
condition(:can_read_confidential) do
@user && IssueCollection.new([@subject]).visible_to(@user).any?
@user && (@user.admin? || can?(:reporter_access) || assignee_or_author?) # rubocop:disable Cop/UserAdmin
end
desc "Project belongs to a group, crm is enabled and user can read contacts in the root group"

View File

@ -0,0 +1,8 @@
---
name: on_demand_scans_runner_tags
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/103634
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/381910
milestone: '15.7'
type: development
group: group::dynamic analysis
default_enabled: false

View File

@ -0,0 +1,17 @@
- title: "DAST API variables" # (required) Actionable title. e.g., The `confidential` field for a `Note` is deprecated. Use `internal` instead.
announcement_milestone: "15.7" # (required) The milestone when this feature was first announced as deprecated.
announcement_date: "2022-12-22" # (required) The date of the milestone release when this feature was first announced as deprecated. This should almost always be the 22nd of a month (YYYY-MM-22), unless you did an out of band blog post.
removal_milestone: "16.0" # (required) The milestone when this feature is planned to be removed
removal_date: "2022-05-22" # (required) The date of the milestone release when this feature is planned to be removed. This should almost always be the 22nd of a month (YYYY-MM-22), unless you did an out of band blog post.
breaking_change: true # (required) If this deprecation is a breaking change, set this value to true
reporter: derekferguson # (required) GitLab username of the person reporting the deprecation
stage: Secure # (required) String value of the stage that the feature was created in. e.g., Growth
issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/383467 # (required) Link to the deprecation issue in GitLab
body: | # (required) Do not modify this line, instead modify the lines below.
With the switch to the new DAST API analyzer in GitLab 15.6, two legacy DAST API variables are being deprecated. The variables `DAST_API_HOST_OVERRIDE` and `DAST_API_SPECIFICATION` will no longer be used for DAST API scans.
`DAST_API_HOST_OVERRIDE` has been deprecated in favor of using the `DAST_API_TARGET_URL` to automatically override the host in the OpenAPI specification.
`DAST_API_SPECIFICATION` has been deprecated in favor of `DAST_API_OPENAPI`. To continue using an OpenAPI specification to guide the test, users must replace the `DAST_API_SPECIFICATION` variable with the `DAST_API_OPENAPI` variable. The value can remain the same, but the variable name must be replaced.
These two variables will be removed in GitLab 16.0.

View File

@ -17912,6 +17912,29 @@ four standard [pagination arguments](#connection-pagination-arguments):
| <a id="projectrequirementssort"></a>`sort` | [`Sort`](#sort) | List requirements by sort order. |
| <a id="projectrequirementsstate"></a>`state` | [`RequirementState`](#requirementstate) | Filter requirements by state. |
##### `Project.runners`
Find runners visible to the current user.
Returns [`CiRunnerConnection`](#cirunnerconnection).
This field returns a [connection](#connections). It accepts the
four standard [pagination arguments](#connection-pagination-arguments):
`before: String`, `after: String`, `first: Int`, `last: Int`.
###### Arguments
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="projectrunnersactive"></a>`active` **{warning-solid}** | [`Boolean`](#boolean) | **Deprecated** in 14.8. This was renamed. Use: `paused`. |
| <a id="projectrunnerspaused"></a>`paused` | [`Boolean`](#boolean) | Filter runners by `paused` (true) or `active` (false) status. |
| <a id="projectrunnerssearch"></a>`search` | [`String`](#string) | Filter by full token or partial text in description field. |
| <a id="projectrunnerssort"></a>`sort` | [`CiRunnerSort`](#cirunnersort) | Sort order of results. |
| <a id="projectrunnersstatus"></a>`status` | [`CiRunnerStatus`](#cirunnerstatus) | Filter runners by status. |
| <a id="projectrunnerstaglist"></a>`tagList` | [`[String!]`](#string) | Filter by tags associated with the runner (comma-separated or array). |
| <a id="projectrunnerstype"></a>`type` | [`CiRunnerType`](#cirunnertype) | Filter runners by type. |
| <a id="projectrunnersupgradestatus"></a>`upgradeStatus` | [`CiRunnerUpgradeStatus`](#cirunnerupgradestatus) | Filter by upgrade status. |
##### `Project.scanExecutionPolicies`
Scan Execution Policies of the project.

View File

@ -52,6 +52,26 @@ sole discretion of GitLab Inc.
<div class="deprecation removal-160 breaking-change">
### DAST API variables
Planned removal: GitLab <span class="removal-milestone">16.0</span> (2022-05-22)
WARNING:
This is a [breaking change](https://docs.gitlab.com/ee/development/deprecation_guidelines/).
Review the details carefully before upgrading.
With the switch to the new DAST API analyzer in GitLab 15.6, two legacy DAST API variables are being deprecated. The variables `DAST_API_HOST_OVERRIDE` and `DAST_API_SPECIFICATION` will no longer be used for DAST API scans.
`DAST_API_HOST_OVERRIDE` has been deprecated in favor of using the `DAST_API_TARGET_URL` to automatically override the host in the OpenAPI specification.
`DAST_API_SPECIFICATION` has been deprecated in favor of `DAST_API_OPENAPI`. To continue using an OpenAPI specification to guide the test, users must replace the `DAST_API_SPECIFICATION` variable with the `DAST_API_OPENAPI` variable. The value can remain the same, but the variable name must be replaced.
These two variables will be removed in GitLab 16.0.
</div>
<div class="deprecation removal-160 breaking-change">
### KAS Metrics Port in GitLab Helm Chart
End of Support: GitLab <span class="removal-milestone">16.0</span> (2023-05-22)<br />

View File

@ -22404,6 +22404,9 @@ msgstr ""
msgid "InviteMembersBanner|We noticed that you haven't invited anyone to this group. Invite your colleagues so you can discuss issues, collaborate on merge requests, and share your knowledge."
msgstr ""
msgid "InviteMembersModal| Inviting a group %{linkStart}adds its members to your group%{linkEnd}, including members who join after the invite. This might put your group over the free %{count} user limit."
msgstr ""
msgid "InviteMembersModal| To get more members, the owner of this namespace can %{trialLinkStart}start a trial%{trialLinkEnd} or %{upgradeLinkStart}upgrade%{upgradeLinkEnd} to a paid tier."
msgstr ""

View File

@ -160,15 +160,14 @@ module QA
# From https://github.com/mattheworiordan/capybara-screenshot/issues/84#issuecomment-41219326
Capybara::Screenshot.register_driver(QA::Runtime::Env.browser) do |driver, path|
QA::Runtime::Logger.info("Saving screenshot..")
driver.browser.save_screenshot(path)
end
Capybara::Screenshot.append_timestamp = false
Capybara::Screenshot.register_filename_prefix_formatter(:rspec) do |example|
::File.join(
QA::Runtime::Namespace.name(reset_cache: false),
example.full_description.downcase.parameterize(separator: "_")[0..99]
example.full_description.downcase.parameterize(separator: "_")[0..79]
)
end

View File

@ -207,6 +207,8 @@ module QA
after do |example|
next unless defined?(@import_time)
# add additional import time metric
example.metadata[:custom_test_metrics] = { fields: { import_time: @import_time } }
# save data for comparison notification creation
save_json(
"data",
@ -269,7 +271,7 @@ module QA
# fetch all objects right after import has started
fetch_github_objects
import_status = lambda do
import_status = -> {
imported_project.project_import_status.yield_self do |status|
@stats = status.dig(:stats, :imported)
@ -278,7 +280,7 @@ module QA
status[:import_status]
end
end
}
logger.info("== Waiting for import to be finished ==")
expect(import_status).to eventually_eq('finished').within(max_duration: import_max_duration, sleep_interval: 30)

View File

@ -72,6 +72,8 @@ module QA
after do |example|
next unless defined?(@import_time)
# add additional import time metric
example.metadata[:custom_test_metrics] = { fields: { import_time: @import_time } }
# save data for comparison notification creation
save_json(
"data",

View File

@ -8,6 +8,8 @@ module QA
class TestMetricsFormatter < RSpec::Core::Formatters::BaseFormatter
include Support::InfluxdbTools
CUSTOM_METRICS_KEY = :custom_test_metrics
RSpec::Core::Formatters.register(self, :stop)
# Finish test execution
@ -106,7 +108,8 @@ module QA
run_type: run_type,
stage: devops_stage(file_path),
product_group: example.metadata[:product_group],
testcase: example.metadata[:testcase]
testcase: example.metadata[:testcase],
**custom_metrics_tags(example.metadata)
},
fields: {
id: example.id,
@ -119,7 +122,8 @@ module QA
pipeline_url: env('CI_PIPELINE_URL'),
pipeline_id: env('CI_PIPELINE_ID'),
job_id: env('CI_JOB_ID'),
merge_request_iid: merge_request_iid
merge_request_iid: merge_request_iid,
**custom_metrics_fields(example.metadata)
}
}
rescue StandardError => e
@ -144,7 +148,7 @@ module QA
resource: resource,
fabrication_method: fabrication_method,
http_method: http_method,
run_type: env('QA_RUN_TYPE') || run_type,
run_type: run_type,
merge_request: merge_request
},
fields: {
@ -225,6 +229,40 @@ module QA
metadata[:retry_attempts] || 0
end
# Additional custom metrics tags
#
# @param [Hash] metadata
# @return [Hash]
def custom_metrics_tags(metadata)
custom_metrics(metadata, :tags)
end
# Additional custom metrics fields
#
# @param [Hash] metadata
# @return [Hash]
def custom_metrics_fields(metadata)
custom_metrics(metadata, :fields)
end
# Custom test metrics
#
# @param [Hash] metadata
# @param [Symbol] type type of metric, :fields or :tags
# @return [Hash]
def custom_metrics(metadata, type)
custom_metrics = metadata[CUSTOM_METRICS_KEY]
return {} unless custom_metrics
return {} unless custom_metrics.is_a?(Hash) && custom_metrics[type].is_a?(Hash)
custom_metrics[type].to_h do |key, value|
k = key.to_sym
v = value.is_a?(Numeric) || value.nil? ? value : value.to_s
[k, v]
end
end
# Print log message
#
# @param [Symbol] level

View File

@ -253,6 +253,27 @@ describe QA::Support::Formatters::TestMetricsFormatter do
end
end
context 'with additional custom metrics' do
it 'exports data to influxdb with additional metrics' do
run_spec do
it(
'spec',
custom_test_metrics: { tags: { custom_tag: "tag" }, fields: { custom_field: 1 } },
testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/1234'
) {}
end
custom_data = data.merge({
**data,
tags: data[:tags].merge({ custom_tag: "tag" }),
fields: data[:fields].merge({ custom_field: 1 })
})
expect(influx_write_api).to have_received(:write).once
expect(influx_write_api).to have_received(:write).with(data: [custom_data])
end
end
context 'with fabrication runtimes' do
let(:api_fabrication) { 4 }
let(:ui_fabrication) { 10 }

View File

@ -473,4 +473,153 @@ RSpec.describe Ci::RunnersFinder do
end
end
end
context 'project' do
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, group: group) }
let_it_be(:other_project) { create(:project) }
let(:extra_params) { {} }
let(:params) { { project: project }.merge(extra_params).reject { |_, v| v.nil? } }
describe '#execute' do
subject { described_class.new(current_user: user, params: params).execute }
context 'with user as project admin' do
before do
project.add_maintainer(user)
end
context 'with project runners' do
let_it_be(:runner_project) { create(:ci_runner, :project, contacted_at: 7.minutes.ago, projects: [project]) }
it 'returns runners available to project' do
expect(subject).to match_array([runner_project])
end
end
context 'with ancestor group runners' do
let_it_be(:runner_instance) { create(:ci_runner, contacted_at: 13.minutes.ago) }
let_it_be(:runner_group) { create(:ci_runner, :group, contacted_at: 12.minutes.ago, groups: [group]) }
it 'returns runners available to project' do
expect(subject).to match_array([runner_instance, runner_group])
end
end
context 'with allowed shared runners' do
let_it_be(:runner_instance) { create(:ci_runner, :instance, contacted_at: 13.minutes.ago) }
it 'returns runners available to project' do
expect(subject).to match_array([runner_instance])
end
end
context 'with project, ancestor group, and allowed shared runners' do
let_it_be(:runner_project) { create(:ci_runner, :project, contacted_at: 7.minutes.ago, projects: [project]) }
let_it_be(:runner_group) { create(:ci_runner, :group, contacted_at: 12.minutes.ago, groups: [group]) }
let_it_be(:runner_instance) { create(:ci_runner, :instance, contacted_at: 13.minutes.ago) }
it 'returns runners available to project' do
expect(subject).to match_array([runner_project, runner_group, runner_instance])
end
end
context 'filtering' do
let_it_be(:runner_instance_inactive) { create(:ci_runner, :instance, active: false, contacted_at: 13.minutes.ago) }
let_it_be(:runner_instance_active) { create(:ci_runner, :instance, active: true, contacted_at: 13.minutes.ago) }
let_it_be(:runner_project_active) { create(:ci_runner, :project, contacted_at: 5.minutes.ago, active: true, projects: [project]) }
let_it_be(:runner_project_inactive) { create(:ci_runner, :project, contacted_at: 5.minutes.ago, active: false, projects: [project]) }
let_it_be(:runner_other_project_inactive) { create(:ci_runner, :project, contacted_at: 5.minutes.ago, active: false, projects: [other_project]) }
context 'by search term' do
let_it_be(:runner_project_1) { create(:ci_runner, :project, contacted_at: 5.minutes.ago, description: 'runner_project_search', projects: [project]) }
let_it_be(:runner_project_2) { create(:ci_runner, :project, contacted_at: 5.minutes.ago, description: 'runner_project', projects: [project]) }
let_it_be(:runner_another_project) { create(:ci_runner, :project, contacted_at: 5.minutes.ago, description: 'runner_project_search', projects: [other_project]) }
let(:extra_params) { { search: 'runner_project_search' } }
it 'returns the correct runner' do
expect(subject).to match_array([runner_project_1])
end
end
context 'by active status' do
let(:extra_params) { { active: false } }
it 'returns the correct runners' do
expect(subject).to match_array([runner_instance_inactive, runner_project_inactive])
end
end
context 'by status' do
let(:extra_params) { { status_status: 'paused' } }
it 'returns correct runner' do
expect(subject).to match_array([runner_instance_inactive, runner_project_inactive])
end
end
context 'by tag_name' do
let_it_be(:runner_project_1) { create(:ci_runner, :project, contacted_at: 3.minutes.ago, tag_list: %w[runner_tag], projects: [project]) }
let_it_be(:runner_project_2) { create(:ci_runner, :project, contacted_at: 3.minutes.ago, tag_list: %w[other_tag], projects: [project]) }
let_it_be(:runner_other_project) { create(:ci_runner, :project, contacted_at: 3.minutes.ago, tag_list: %w[runner_tag], projects: [other_project]) }
let(:extra_params) { { tag_name: %w[runner_tag] } }
it 'returns correct runner' do
expect(subject).to match_array([runner_project_1])
end
end
context 'by runner type' do
let(:extra_params) { { type_type: 'project_type' } }
it 'returns correct runners' do
expect(subject).to match_array([runner_project_active, runner_project_inactive])
end
end
end
end
context 'with user as project developer' do
let(:user) { create(:user) }
before do
project.add_developer(user)
end
it 'returns no runners' do
expect(subject).to be_empty
end
end
context 'when user is nil' do
let_it_be(:user) { nil }
it 'returns no runners' do
expect(subject).to be_empty
end
end
context 'with nil project_full_path' do
let(:project_full_path) { nil }
it 'returns no runners' do
expect(subject).to be_empty
end
end
context 'when on_demand_scans_runner_tags feature flag is disabled' do
before do
stub_feature_flags(on_demand_scans_runner_tags: false)
end
it 'returns no runners' do
expect(subject).to be_empty
end
end
end
end
end

View File

@ -0,0 +1,42 @@
import { GlAlert, GlLink, GlSprintf } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { sprintf } from '~/locale';
import InviteGroupNotification from '~/invite_members/components/invite_group_notification.vue';
import { GROUP_MODAL_ALERT_BODY } from '~/invite_members/constants';
describe('InviteGroupNotification', () => {
let wrapper;
const findAlert = () => wrapper.findComponent(GlAlert);
const findLink = () => wrapper.findComponent(GlLink);
const createComponent = () => {
wrapper = shallowMountExtended(InviteGroupNotification, {
provide: { freeUsersLimit: 5 },
propsData: { name: 'name' },
stubs: { GlSprintf },
});
};
describe('when rendering', () => {
beforeEach(() => {
createComponent();
});
it('passes the correct props', () => {
expect(findAlert().props()).toMatchObject({ variant: 'warning', dismissible: false });
});
it('shows the correct message', () => {
const message = sprintf(GROUP_MODAL_ALERT_BODY, { count: 5 });
expect(findAlert().text()).toMatchInterpolatedText(message);
});
it('has a help link', () => {
expect(findLink().attributes('href')).toEqual(
'https://docs.gitlab.com/ee/user/group/manage.html#share-a-group-with-another-group',
);
});
});
});

View File

@ -6,6 +6,7 @@ import InviteGroupsModal from '~/invite_members/components/invite_groups_modal.v
import InviteModalBase from '~/invite_members/components/invite_modal_base.vue';
import ContentTransition from '~/vue_shared/components/content_transition.vue';
import GroupSelect from '~/invite_members/components/group_select.vue';
import InviteGroupNotification from '~/invite_members/components/invite_group_notification.vue';
import { stubComponent } from 'helpers/stub_component';
import { propsData, sharedGroup } from '../mock_data/group_modal';
@ -44,6 +45,7 @@ describe('InviteGroupsModal', () => {
const findModal = () => wrapper.findComponent(GlModal);
const findGroupSelect = () => wrapper.findComponent(GroupSelect);
const findInviteGroupAlert = () => wrapper.findComponent(InviteGroupNotification);
const findIntroText = () => wrapper.findByTestId('modal-base-intro-text').text();
const findMembersFormGroup = () => wrapper.findByTestId('members-form-group');
const membersFormGroupInvalidFeedback = () =>
@ -74,6 +76,20 @@ describe('InviteGroupsModal', () => {
});
});
describe('rendering the invite group notification', () => {
it('shows the user limit notification alert when free user cap is enabled', () => {
createComponent({ freeUserCapEnabled: true });
expect(findInviteGroupAlert().exists()).toBe(true);
});
it('does not show the user limit notification alert', () => {
createComponent();
expect(findInviteGroupAlert().exists()).toBe(false);
});
});
describe('submitting the invite form', () => {
let apiResolve;
let apiReject;

View File

@ -8,6 +8,7 @@ export const propsData = {
defaultAccessLevel: 10,
helpLink: 'https://example.com',
fullPath: 'project',
freeUserCapEnabled: false,
};
export const sharedGroup = { id: '981' };

View File

@ -0,0 +1,86 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Resolvers::Ci::ProjectRunnersResolver do
include GraphqlHelpers
describe '#resolve' do
subject do
resolve(described_class, obj: obj, ctx: { current_user: user }, args: args,
arg_style: :internal)
end
include_context 'runners resolver setup'
let(:obj) { project }
let(:args) { {} }
context 'when user cannot see runners' do
it 'returns no runners' do
expect(subject.items.to_a).to eq([])
end
end
context 'with user as project admin' do
before do
project.add_maintainer(user)
end
let(:available_runners) { [inactive_project_runner, offline_project_runner, group_runner, instance_runner] }
it 'returns all runners available to the project' do
expect(subject.items.to_a).to match_array(available_runners)
end
end
context 'with obj set to nil' do
let(:obj) { nil }
it 'raises an error' do
expect { subject }.to raise_error('Expected project missing')
end
end
context 'with obj not set to project' do
let(:obj) { build(:group) }
it 'raises an error' do
expect { subject }.to raise_error('Expected project missing')
end
end
describe 'Allowed query arguments' do
let(:finder) { instance_double(::Ci::RunnersFinder) }
let(:args) do
{
status: 'active',
type: :group_type,
tag_list: ['active_runner'],
search: 'abc',
sort: :contacted_asc
}
end
let(:expected_params) do
{
status_status: 'active',
type_type: :group_type,
tag_name: ['active_runner'],
preload: { tag_name: false },
search: 'abc',
sort: 'contacted_asc',
project: project
}
end
it 'calls RunnersFinder with expected arguments' do
allow(::Ci::RunnersFinder).to receive(:new).with(current_user: user,
params: expected_params).once.and_return(finder)
allow(finder).to receive(:execute).once.and_return([:execute_return_value])
expect(subject.items.to_a).to eq([:execute_return_value])
end
end
end
end

View File

@ -122,7 +122,7 @@ RSpec.describe Gitlab::Instrumentation::RedisInterceptor, :clean_gitlab_redis_sh
Gitlab::Redis::SharedState.with do |redis|
redis.pipelined do |pipeline|
pipeline.call(:get, '{foobar}:buz')
pipeline.call(:get, '{foobar}buz')
pipeline.call(:get, '{foobar}baz')
end
end
@ -142,37 +142,36 @@ RSpec.describe Gitlab::Instrumentation::RedisInterceptor, :clean_gitlab_redis_sh
end
describe 'commands not in the apdex' do
where(:command) do
[
[%w[brpop foobar 0.01]],
[%w[blpop foobar 0.01]],
[%w[brpoplpush foobar bazqux 0.01]],
[%w[bzpopmin foobar 0.01]],
[%w[bzpopmax foobar 0.01]],
[%w[xread block 1 streams mystream 0-0]],
[%w[xreadgroup group mygroup myconsumer block 1 streams foobar 0-0]]
]
where(:setup, :command) do
[['rpush', 'foobar', 1]] | ['brpop', 'foobar', 0]
[['rpush', 'foobar', 1]] | ['blpop', 'foobar', 0]
[['rpush', '{abc}foobar', 1]] | ['brpoplpush', '{abc}foobar', '{abc}bazqux', 0]
[['rpush', '{abc}foobar', 1]] | ['brpoplpush', '{abc}foobar', '{abc}bazqux', 0]
[['zadd', 'foobar', 1, 'a']] | ['bzpopmin', 'foobar', 0]
[['zadd', 'foobar', 1, 'a']] | ['bzpopmax', 'foobar', 0]
[['xadd', 'mystream', 1, 'myfield', 'mydata']] | ['xread', 'block', 1, 'streams', 'mystream', '0-0']
[['xadd', 'foobar', 1, 'myfield', 'mydata'], ['xgroup', 'create', 'foobar', 'mygroup', 0]] | ['xreadgroup', 'group', 'mygroup', 'myconsumer', 'block', 1, 'streams', 'foobar', '0-0']
end
with_them do
it 'skips requests we do not want in the apdex' do
Gitlab::Redis::SharedState.with { |redis| setup.each { |cmd| redis.call(*cmd) } }
expect(instrumentation_class).not_to receive(:instance_observe_duration)
begin
Gitlab::Redis::SharedState.with { |redis| redis.call(*command) }
rescue Gitlab::Instrumentation::RedisClusterValidator::CrossSlotError, ::Redis::CommandError
end
Gitlab::Redis::SharedState.with { |redis| redis.call(*command) }
end
end
context 'with pipelined commands' do
it 'skips requests that have blocking commands', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/373026' do
it 'skips requests that have blocking commands' do
expect(instrumentation_class).not_to receive(:instance_observe_duration)
Gitlab::Redis::SharedState.with do |redis|
redis.pipelined do |pipeline|
pipeline.call(:get, 'foo')
pipeline.call(:brpop, 'foobar', '0.01')
pipeline.call(:get, '{foobar}buz')
pipeline.call(:rpush, '{foobar}baz', 1)
pipeline.call(:brpop, '{foobar}baz', 0)
end
end
end

View File

@ -1,71 +0,0 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe IssueCollection do
let(:user) { create(:user) }
let(:project) { create(:project) }
let(:issue1) { create(:issue, project: project) }
let(:issue2) { create(:issue, project: project) }
let(:collection) { described_class.new([issue1, issue2]) }
describe '#collection' do
it 'returns the issues in the same order as the input Array' do
expect(collection.collection).to eq([issue1, issue2])
end
end
describe '#updatable_by_user' do
context 'using an admin user' do
it 'returns all issues' do
user = create(:admin)
expect(collection.updatable_by_user(user)).to eq([issue1, issue2])
end
end
context 'using a user that has no access to the project' do
it 'returns no issues when the user is not an assignee or author' do
expect(collection.updatable_by_user(user)).to be_empty
end
it 'returns the issues the user is assigned to' do
issue1.assignees << user
expect(collection.updatable_by_user(user)).to eq([issue1])
end
it 'returns the issues for which the user is the author' do
issue1.author = user
expect(collection.updatable_by_user(user)).to eq([issue1])
end
end
context 'using a user that has reporter access to the project' do
it 'returns the issues of the project' do
project.add_reporter(user)
expect(collection.updatable_by_user(user)).to eq([issue1, issue2])
end
end
# TODO update when we have multiple owners of a project
# https://gitlab.com/gitlab-org/gitlab/-/issues/350605
context 'using a user that is an owner of a project' do
it 'returns the issues of the project' do
expect(collection.updatable_by_user(project.namespace.owner))
.to eq([issue1, issue2])
end
end
end
describe '#visible_to' do
it 'is an alias for updatable_by_user' do
updatable_by_user = described_class.instance_method(:updatable_by_user)
visible_to = described_class.instance_method(:visible_to)
expect(visible_to).to eq(updatable_by_user)
end
end
end

View File

@ -8,6 +8,7 @@ RSpec.describe IssuePolicy do
include ProjectHelpers
include UserHelpers
let(:admin) { create(:user, :admin) }
let(:guest) { create(:user) }
let(:author) { create(:user) }
let(:assignee) { create(:user) }
@ -305,7 +306,6 @@ RSpec.describe IssuePolicy do
let(:issue) { create(:issue, project: project, author: author) }
let(:visitor) { create(:user) }
let(:admin) { create(:user, :admin) }
it 'forbids visitors from viewing issues' do
expect(permissions(visitor, issue)).to be_disallowed(:read_issue)
@ -394,12 +394,15 @@ RSpec.describe IssuePolicy do
expect(permissions(assignee, confidential_issue_no_assignee)).to be_disallowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue, :set_issue_metadata, :set_confidentiality)
end
it 'allows admins to read confidential issues' do
expect(permissions(admin, confidential_issue)).to be_allowed(:read_issue)
end
end
context 'with a hidden issue' do
let(:user) { create(:user) }
let(:banned_user) { create(:user, :banned) }
let(:admin) { create(:user, :admin) }
let(:hidden_issue) { create(:issue, project: project, author: banned_user) }
it 'does not allow non-admin user to read the issue' do

View File

@ -0,0 +1,68 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Project.runners' do
include GraphqlHelpers
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group, :public) }
let_it_be(:project) { create(:project, :public, group: group) }
let_it_be(:instance_runner) { create(:ci_runner, :instance) }
let_it_be(:project_runner) { create(:ci_runner, :project, projects: [project]) }
let_it_be(:group_runner) { create(:ci_runner, :group, groups: [group]) }
let_it_be(:other_project) { create(:project, :repository, :public) }
let_it_be(:other_project_runner) { create(:ci_runner, :project, projects: [other_project]) }
let_it_be(:query) do
%(
query {
project(fullPath: "#{project.full_path}") {
runners {
nodes {
id
}
}
}
}
)
end
context 'when the user is a project admin' do
before do
project.add_maintainer(user)
end
let(:expected_ids) { [project_runner, group_runner, instance_runner].map { |g| g.to_global_id.to_s } }
it 'returns all runners available to project' do
post_graphql(query, current_user: user)
expect(graphql_data_at(:project, :runners, :nodes).pluck('id')).to match_array(expected_ids)
end
end
context 'when the user is a project developer' do
before do
project.add_developer(user)
end
it 'returns no runners' do
post_graphql(query, current_user: user)
expect(graphql_data_at(:project, :runners, :nodes)).to be_empty
end
end
context 'when on_demand_scans_runner_tags feature flag is disabled' do
before do
stub_feature_flags(on_demand_scans_runner_tags: false)
end
it 'returns no runners' do
post_graphql(query, current_user: user)
expect(graphql_data_at(:project, :runners, :nodes)).to be_empty
end
end
end