Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
f51c6a69f9
commit
63c450b3e4
|
|
@ -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>
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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 />
|
||||
|
|
|
|||
|
|
@ -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 ""
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ export const propsData = {
|
|||
defaultAccessLevel: 10,
|
||||
helpLink: 'https://example.com',
|
||||
fullPath: 'project',
|
||||
freeUserCapEnabled: false,
|
||||
};
|
||||
|
||||
export const sharedGroup = { id: '981' };
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
Loading…
Reference in New Issue