Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2024-01-23 18:10:52 +00:00
parent 118083ac69
commit abd2c6b32a
55 changed files with 518 additions and 221 deletions

View File

@ -277,7 +277,6 @@ Layout/SpaceInLambdaLiteral:
- 'ee/lib/ee/api/entities/list.rb'
- 'ee/lib/ee/api/entities/member.rb'
- 'ee/lib/ee/api/entities/project_approval_rule.rb'
- 'ee/lib/ee/api/entities/user_basic.rb'
- 'ee/lib/ee/api/entities/vulnerability_issue_link.rb'
- 'ee/lib/ee/gitlab/background_migration/backfill_epic_cache_counts.rb'
- 'ee/lib/ee/gitlab/background_migration/delete_approval_rules_with_vulnerability.rb'

View File

@ -1,12 +1,14 @@
import $ from 'jquery';
import initVueAlerts from '~/vue_alerts';
import NoEmojiValidator from '~/emoji/no_emoji_validator';
import { initLanguageSwitcher } from '~/language_switcher';
import LengthValidator from '~/validators/length_validator';
import mountEmailVerificationApplication from '~/sessions/new';
import { renderGFM } from '~/behaviors/markdown/render_gfm';
import OAuthRememberMe from './oauth_remember_me';
import preserveUrlFragment from './preserve_url_fragment';
import {
appendUrlFragment,
appendRedirectQuery,
toggleRememberMeQuery,
} from './preserve_url_fragment';
import SigninTabsMemoizer from './signin_tabs_memoizer';
import UsernameValidator from './username_validator';
@ -15,13 +17,9 @@ new LengthValidator(); // eslint-disable-line no-new
new SigninTabsMemoizer(); // eslint-disable-line no-new
new NoEmojiValidator(); // eslint-disable-line no-new
new OAuthRememberMe({
container: $('.js-oauth-login'),
}).bindEvents();
// Save the URL fragment from the current window location. This will be present if the user was
// redirected to sign-in after attempting to access a protected URL that included a fragment.
preserveUrlFragment(window.location.hash);
appendUrlFragment();
appendRedirectQuery();
toggleRememberMeQuery();
initVueAlerts();
initLanguageSwitcher();
mountEmailVerificationApplication();

View File

@ -1,34 +0,0 @@
import $ from 'jquery';
import { mergeUrlParams, removeParams } from '~/lib/utils/url_utility';
/**
* OAuth-based login buttons have a separate "remember me" checkbox.
*
* Toggling this checkbox adds/removes a `remember_me` parameter to the
* login buttons' parent form action, which is passed on to the omniauth callback.
*/
export default class OAuthRememberMe {
constructor(opts = {}) {
this.container = opts.container || '';
}
bindEvents() {
$('#remember_me_omniauth', this.container).on('click', this.toggleRememberMe);
}
toggleRememberMe(event) {
const rememberMe = $(event.target).is(':checked');
$('.js-oauth-login form', this.container).each((_, form) => {
const $form = $(form);
const href = $form.attr('action');
if (rememberMe) {
$form.attr('action', mergeUrlParams({ remember_me: 1 }, href));
} else {
$form.attr('action', removeParams(['remember_me'], href));
}
});
}
}

View File

@ -1,32 +1,72 @@
import { mergeUrlParams, setUrlFragment } from '~/lib/utils/url_utility';
import { mergeUrlParams, removeParams, setUrlFragment } from '~/lib/utils/url_utility';
/**
* Ensure the given URL fragment is preserved by appending it to sign-in/sign-up form actions and
* OAuth/SAML login links.
* Append the fragment to all non-OAuth login form actions so it is preserved
* when the user is eventually redirected back to the originally requested URL.
*
* @param fragment {string} - url fragment to be preserved
*/
export default function preserveUrlFragment(fragment = '') {
if (fragment) {
const normalFragment = fragment.replace(/^#/, '');
export function appendUrlFragment(fragment = document.location.hash) {
if (!fragment) {
return;
}
// Append the fragment to all sign-in/sign-up form actions so it is preserved when the user is
// eventually redirected back to the originally requested URL.
const forms = document.querySelectorAll('.js-non-oauth-login form');
Array.prototype.forEach.call(forms, (form) => {
const actionWithFragment = setUrlFragment(form.getAttribute('action'), `#${normalFragment}`);
form.setAttribute('action', actionWithFragment);
});
const normalFragment = fragment.replace(/^#/, '');
const forms = document.querySelectorAll('.js-non-oauth-login form');
forms.forEach((form) => {
const actionWithFragment = setUrlFragment(form.getAttribute('action'), `#${normalFragment}`);
form.setAttribute('action', actionWithFragment);
});
}
/**
* Append a redirect_fragment query param to all OAuth login form actions. The
* redirect_fragment query param will be available in the omniauth callback upon
* successful authentication.
*
* @param {string} fragment - url fragment to be preserved
*/
export function appendRedirectQuery(fragment = document.location.hash) {
if (!fragment) {
return;
}
const normalFragment = fragment.replace(/^#/, '');
const oauthForms = document.querySelectorAll('.js-oauth-login form');
oauthForms.forEach((oauthForm) => {
const newHref = mergeUrlParams(
{ redirect_fragment: normalFragment },
oauthForm.getAttribute('action'),
);
oauthForm.setAttribute('action', newHref);
});
}
/**
* OAuth login buttons have a separate "remember me" checkbox.
*
* Toggling this checkbox adds/removes a `remember_me` parameter to the
* login form actions, which is passed on to the omniauth callback.
*/
export function toggleRememberMeQuery() {
const oauthForms = document.querySelectorAll('.js-oauth-login form');
const checkbox = document.querySelector('#js-remember-me-omniauth');
if (oauthForms.length === 0 || !checkbox) {
return;
}
checkbox.addEventListener('change', ({ currentTarget }) => {
oauthForms.forEach((oauthForm) => {
const href = oauthForm.getAttribute('action');
let newHref;
if (currentTarget.checked) {
newHref = mergeUrlParams({ remember_me: '1' }, href);
} else {
newHref = removeParams(['remember_me'], href);
}
// Append a redirect_fragment query param to all oauth provider links. The redirect_fragment
// query param will be available in the omniauth callback upon successful authentication
const oauthForms = document.querySelectorAll('.js-oauth-login form');
Array.prototype.forEach.call(oauthForms, (oauthForm) => {
const newHref = mergeUrlParams(
{ redirect_fragment: normalFragment },
oauthForm.getAttribute('action'),
);
oauthForm.setAttribute('action', newHref);
});
}
});
}

View File

@ -26,6 +26,11 @@ export default {
type: Object,
required: true,
},
span: {
type: Number,
required: false,
default: null,
},
prevBlameLink: {
type: String,
required: false,
@ -43,6 +48,9 @@ export default {
avatarLinkAltText() {
return sprintf(__(`%{username}'s avatar`), { username: this.commit.authorName });
},
truncateAuthorName() {
return typeof this.span === 'number' && this.span < 3;
},
},
methods: {
toggleShowDescription() {
@ -102,18 +110,23 @@ export default {
@click="toggleShowDescription"
/>
</div>
<div class="committer gl-flex-basis-full">
<div
class="committer gl-flex-basis-full"
:class="truncateAuthorName ? 'gl-display-inline-flex' : ''"
data-testid="committer"
>
<gl-link
v-if="commit.author"
:href="commit.author.webPath"
class="commit-author-link js-user-link"
:class="truncateAuthorName ? 'gl-display-inline-block gl-text-truncate' : ''"
>
{{ commit.author.name }}</gl-link
>
<template v-else>
{{ commit.authorName }}
</template>
{{ $options.i18n.authored }}
{{ $options.i18n.authored }}&nbsp;
<timeago-tooltip :time="commit.authoredDate" tooltip-placement="bottom" />
</div>
<pre

View File

@ -9,10 +9,13 @@ const InternalEvents = {
/**
*
* @param {string} event
* @param {string} category - The category of the event. This is optional and
* defaults to the page name where the event was triggered. It's advised not to use
* this parameter for new events unless absolutely necessary.
*/
trackEvent(event) {
trackEvent(event, category = undefined) {
API.trackInternalEvent(event);
Tracking.event(undefined, event, {
Tracking.event(category, event, {
context: {
schema: SERVICE_PING_SCHEMA,
data: {
@ -30,8 +33,8 @@ const InternalEvents = {
mixin() {
return {
methods: {
trackEvent(event) {
InternalEvents.trackEvent(event);
trackEvent(event, category = undefined) {
InternalEvents.trackEvent(event, category);
},
},
};

View File

@ -30,6 +30,7 @@ export default {
class="gl-display-flex gl-absolute gl-px-3"
:style="{ top: blame.blameOffset }"
:commit="blame.commit"
:span="blame.span"
:prev-blame-link="blame.commitData && blame.commitData.projectBlameLink"
/>
</div>

View File

@ -136,6 +136,10 @@
.commit-author-link {
color: $gl-text-color;
}
.commit-author-link.gl-text-truncate {
max-width: 20ch;
}
}
}

View File

@ -51,7 +51,7 @@ class Groups::AutocompleteSourcesController < Groups::ApplicationController
# TODO https://gitlab.com/gitlab-org/gitlab/-/issues/388541
# type_id is a misnomer. QuickActions::TargetService actually requires an iid.
QuickActions::TargetService
.new(nil, current_user, group: @group)
.new(container: @group, current_user: current_user)
.execute(params[:type], params[:type_id])
end
# rubocop: enable CodeReuse/ActiveRecord

View File

@ -59,7 +59,7 @@ class Projects::AutocompleteSourcesController < Projects::ApplicationController
# TODO https://gitlab.com/gitlab-org/gitlab/-/issues/388541
# type_id is a misnomer. QuickActions::TargetService actually requires an iid.
QuickActions::TargetService
.new(project, current_user)
.new(container: project, current_user: current_user)
.execute(target_type, params[:type_id])
end

View File

@ -0,0 +1,25 @@
# frozen_string_literal: true
module Resolvers
module Projects
class IsForkedResolver < BaseResolver
type GraphQL::Types::Boolean, null: false
def resolve
lazy_fork_network_members = BatchLoader::GraphQL.for(object.id).batch do |ids, loader|
ForkNetworkMember.by_projects(ids)
.with_fork_network
.find_each do |fork_network_member|
loader.call(fork_network_member.project_id, fork_network_member)
end
end
Gitlab::Graphql::Lazy.with_value(lazy_fork_network_members) do |fork_network_member|
next false if fork_network_member.nil?
fork_network_member.fork_network.root_project_id != object.id
end
end
end
end
end

View File

@ -685,6 +685,12 @@ module Types
description: 'Project allows assigning multiple reviewers to a merge request.',
null: false
field :is_forked,
GraphQL::Types::Boolean,
resolver: Resolvers::Projects::IsForkedResolver,
description: 'Project is forked.',
null: false
def timelog_categories
object.project_namespace.timelog_categories if Feature.enabled?(:timelog_categories)
end

View File

@ -10,6 +10,9 @@ module Ci
include FileStoreMounter
include Lockable
include Presentable
include SafelyChangeColumnDefault
columns_changing_default :partition_id
FILE_SIZE_LIMIT = 10.megabytes.freeze
EXPIRATION_DATE = 1.week.freeze

View File

@ -3,6 +3,9 @@
module Ci
class PipelineConfig < Ci::ApplicationRecord
include Ci::Partitionable
include SafelyChangeColumnDefault
columns_changing_default :partition_id
self.table_name = 'ci_pipelines_config'
self.primary_key = :pipeline_id

View File

@ -4,6 +4,9 @@ module Ci
class PipelineMetadata < Ci::ApplicationRecord
include Ci::Partitionable
include Importable
include SafelyChangeColumnDefault
columns_changing_default :partition_id
self.primary_key = :pipeline_id

View File

@ -9,6 +9,9 @@ class ForkNetworkMember < ApplicationRecord
after_destroy :cleanup_fork_network
scope :by_projects, ->(ids) { where(project_id: ids) }
scope :with_fork_network, -> { joins(:fork_network).includes(:fork_network) }
private
def cleanup_fork_network

View File

@ -631,6 +631,8 @@ class User < MainClusterwide::ApplicationRecord
.trusted_with_spam)
end
scope :preload_user_detail, -> { preload(:user_detail) }
def self.supported_keyset_orderings
{
id: [:asc, :desc],

View File

@ -55,7 +55,7 @@ class PreviewMarkdownService < BaseService
def find_commands_target
QuickActions::TargetService
.new(project, current_user, group: params[:group])
.new(container: project, current_user: current_user, params: { group: params[:group] })
.execute(target_type, target_id)
end

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
module QuickActions
class TargetService < BaseService
class TargetService < BaseContainerService
def execute(type, type_iid)
case type&.downcase
when 'workitem'
@ -19,15 +19,15 @@ module QuickActions
# rubocop: disable CodeReuse/ActiveRecord
def work_item(type_iid)
WorkItems::WorkItemsFinder.new(current_user, project_id: project.id).find_by(iid: type_iid)
WorkItems::WorkItemsFinder.new(current_user, **parent_params).find_by(iid: type_iid)
end
# rubocop: enable CodeReuse/ActiveRecord
# rubocop: disable CodeReuse/ActiveRecord
def issue(type_iid)
return project.issues.build if type_iid.nil?
return container.issues.build if type_iid.nil?
IssuesFinder.new(current_user, project_id: project.id).find_by(iid: type_iid) || project.issues.build
IssuesFinder.new(current_user, **parent_params).find_by(iid: type_iid) || container.issues.build
end
# rubocop: enable CodeReuse/ActiveRecord
@ -42,7 +42,11 @@ module QuickActions
def commit(type_iid)
project.commit(type_iid)
end
def parent_params
group_container? ? { group_id: group.id } : { project_id: project.id }
end
end
end
QuickActions::TargetService.prepend_mod_with('QuickActions::TargetService')
QuickActions::TargetService.prepend_mod

View File

@ -12,6 +12,6 @@
data: { testid: test_id_for_provider(provider) },
id: "oauth-login-#{provider}"
- if render_remember_me
= render Pajamas::CheckboxTagComponent.new(name: 'remember_me_omniauth', value: nil) do |c|
= render Pajamas::CheckboxTagComponent.new(name: 'js-remember-me-omniauth', value: nil) do |c|
- c.with_label do
= _('Remember me')

View File

@ -0,0 +1,13 @@
# frozen_string_literal: true
class RemovePartitionIdDefaultValueForCiPipelineMetadata < Gitlab::Database::Migration[2.2]
milestone '16.9'
enable_lock_retries!
TABLE_NAME = :ci_pipeline_metadata
COLUM_NAME = :partition_id
def change
change_column_default(TABLE_NAME, COLUM_NAME, from: 100, to: nil)
end
end

View File

@ -0,0 +1,13 @@
# frozen_string_literal: true
class RemovePartitionIdDefaultValueForCiPipelineArtifact < Gitlab::Database::Migration[2.2]
milestone '16.9'
enable_lock_retries!
TABLE_NAME = :ci_pipeline_artifacts
COLUM_NAME = :partition_id
def change
change_column_default(TABLE_NAME, COLUM_NAME, from: 100, to: nil)
end
end

View File

@ -0,0 +1,13 @@
# frozen_string_literal: true
class RemovePartitionIdDefaultValueForCiPipelineConfig < Gitlab::Database::Migration[2.2]
milestone '16.9'
enable_lock_retries!
TABLE_NAME = :ci_pipelines_config
COLUM_NAME = :partition_id
def change
change_column_default(TABLE_NAME, COLUM_NAME, from: 100, to: nil)
end
end

View File

@ -0,0 +1 @@
43ff332582062a104cef5449444034363c1a71d288bcae7dfdeefbd69500186e

View File

@ -0,0 +1 @@
29392953f2fce7fb1a24dbc49f1ea30c49b1006551599bff98edc4de8061106b

View File

@ -0,0 +1 @@
d14905475e591b7fa855097434d0e810fbb5a0890d7feb7b4fe8a22d5d75335f

View File

@ -14603,7 +14603,7 @@ CREATE TABLE ci_pipeline_artifacts (
verification_checksum bytea,
verification_failure text,
locked smallint DEFAULT 2,
partition_id bigint DEFAULT 100 NOT NULL,
partition_id bigint NOT NULL,
CONSTRAINT check_191b5850ec CHECK ((char_length(file) <= 255)),
CONSTRAINT check_abeeb71caf CHECK ((file IS NOT NULL)),
CONSTRAINT ci_pipeline_artifacts_verification_failure_text_limit CHECK ((char_length(verification_failure) <= 255))
@ -14658,7 +14658,7 @@ CREATE TABLE ci_pipeline_metadata (
name text,
auto_cancel_on_new_commit smallint DEFAULT 0 NOT NULL,
auto_cancel_on_job_failure smallint DEFAULT 0 NOT NULL,
partition_id bigint DEFAULT 100 NOT NULL,
partition_id bigint NOT NULL,
CONSTRAINT check_9d3665463c CHECK ((char_length(name) <= 255))
);
@ -14782,7 +14782,7 @@ CREATE TABLE ci_pipelines (
CREATE TABLE ci_pipelines_config (
pipeline_id bigint NOT NULL,
content text NOT NULL,
partition_id bigint DEFAULT 100 NOT NULL
partition_id bigint NOT NULL
);
CREATE SEQUENCE ci_pipelines_id_seq

View File

@ -28,8 +28,10 @@ swap:
raketask: Rake task
raketasks: Rake tasks
rspec: RSpec
self hosted: self-managed
self-hosted: self-managed
GitLab self hosted: GitLab self-managed # https://docs.gitlab.com/ee/development/documentation/styleguide/word_list.html#gitlab-self-managed
GitLab self-hosted: GitLab self-managed # https://docs.gitlab.com/ee/development/documentation/styleguide/word_list.html#gitlab-self-managed
self hosted GitLab: GitLab self-managed # https://docs.gitlab.com/ee/development/documentation/styleguide/word_list.html#gitlab-self-managed
self-hosted GitLab: GitLab self-managed # https://docs.gitlab.com/ee/development/documentation/styleguide/word_list.html#gitlab-self-managed
styleguide: style guide
to login: to log in
can login: can log in

View File

@ -24351,6 +24351,7 @@ Represents vulnerability finding of a security report on the pipeline.
| <a id="projectimportstatus"></a>`importStatus` | [`String`](#string) | Status of import background job of the project. |
| <a id="projectincidentmanagementtimelineeventtags"></a>`incidentManagementTimelineEventTags` | [`[TimelineEventTagType!]`](#timelineeventtagtype) | Timeline event tags for the project. |
| <a id="projectiscatalogresource"></a>`isCatalogResource` **{warning-solid}** | [`Boolean`](#boolean) | **Introduced** in 15.11. This feature is an Experiment. It can be changed or removed at any time. Indicates if a project is a catalog resource. |
| <a id="projectisforked"></a>`isForked` | [`Boolean!`](#boolean) | Project is forked. |
| <a id="projectissuesaccesslevel"></a>`issuesAccessLevel` | [`ProjectFeatureAccess`](#projectfeatureaccess) | Access level required for issues access. |
| <a id="projectissuesenabled"></a>`issuesEnabled` | [`Boolean`](#boolean) | Indicates if Issues are enabled for the current user. |
| <a id="projectjiraimportstatus"></a>`jiraImportStatus` | [`String`](#string) | Status of Jira import background job of the project. |

View File

@ -29,8 +29,7 @@ In GitLab 14.8 and earlier, projects in personal namespaces have an `access_leve
The `group_saml_identity` attribute is only visible to group owners for [SSO-enabled groups](../user/group/saml_sso/index.md).
The `email` attribute is only visible to group owners for users provisioned by the group with [SCIM](../user/group/saml_sso/scim_setup.md).
[Issue 391453](https://gitlab.com/gitlab-org/gitlab/-/issues/391453) proposes to change the criteria for access to the `email` attribute from provisioned users to [enterprise users](../user/enterprise_user/index.md).
The `email` attribute is only visible to group owners for [enterprise users](../user/enterprise_user/index.md) of the group when an API request is sent to the group itself, or that group's subgroups or projects.
## List all members of a group or project

View File

@ -252,7 +252,7 @@ Ideally, you should use [CI/CD variables](../variables/predefined_variables.md)
to replace those values at runtime when each review app is created:
- `data-project-id` is the project ID, which can be found by the `CI_PROJECT_ID`
variable.
variable or on the [project overview page](../../user/project/working_with_projects.md#access-the-project-overview-page-by-using-the-project-id).
- `data-merge-request-id` is the merge request ID, which can be found by the
`CI_MERGE_REQUEST_IID` variable. `CI_MERGE_REQUEST_IID` is available only if
[`rules:if: $CI_PIPELINE_SOURCE == "merge_request_event`](../pipelines/merge_request_pipelines.md#use-rules-to-add-jobs)

View File

@ -78,7 +78,7 @@ In each example, replace:
- `<token>` with your trigger token.
- `<ref_name>` with a branch or tag name, like `main`.
- `<project_id>` with your project ID, like `123456`. The project ID is displayed
at the top of every project's landing page.
on the [project overview page](../../user/project/working_with_projects.md#access-the-project-overview-page-by-using-the-project-id).
### Use a CI/CD job
@ -100,8 +100,8 @@ trigger_pipeline:
In this example:
- `1234` is the project ID for `project-B`. The project ID is displayed at the top
of every project's landing page.
- `1234` is the project ID for `project-B`. The project ID is displayed on the
[project overview page](../../user/project/working_with_projects.md#access-the-project-overview-page-by-using-the-project-id).
- The [`rules`](../yaml/index.md#rules) cause the job to run every time a tag is added to `project-A`.
- `MY_TRIGGER_TOKEN` is a [masked CI/CD variables](../variables/index.md#mask-a-cicd-variable)
that contains the trigger token.
@ -119,7 +119,7 @@ Replace:
- The URL with `https://gitlab.com` or the URL of your instance.
- `<project_id>` with your project ID, like `123456`. The project ID is displayed
at the top of the project's landing page.
on the [project overview page](../../user/project/working_with_projects.md#access-the-project-overview-page-by-using-the-project-id).
- `<ref_name>` with a branch or tag name, like `main`. This value takes precedence over the `ref_name` in the webhook payload.
The payload's `ref` is the branch that fired the trigger in the source repository.
You must URL-encode the `ref_name` if it contains slashes.

View File

@ -21,7 +21,7 @@ If you are already tracking events in Snowplow, you can also start collecting me
The event triggered by Internal Events has some special properties compared to previously tracking with Snowplow directly:
1. The `label`, `property` and `value` attributes are not used within Internal Events and are always empty.
1. The `category` is automatically set to `InternalEventTracking`
1. The `category` is automatically set to the location where the event happened. For Frontend events it is the page name and for Backend events it is a class name. If the page name or class name is not used, the default value of `"InternalEventTracking"` will be used.
Make sure that you are okay with this change before you migrate and dashboards are changed accordingly.
@ -73,9 +73,11 @@ import { InternalEvents } from '~/tracking';
mixins: [InternalEvents.mixin()]
...
...
this.trackEvent('action')
this.trackEvent('action', 'category')
```
If you are currently passing `category` and need to keep it, it can be passed as the second argument in the `trackEvent` method, as illustrated in the previous example. Nonetheless, it is strongly advised against using the `category` parameter for new events. This is because, by default, the category field is populated with information about where the event was triggered.
You can use [this MR](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/123901/diffs) as an example. It migrates the `devops_adoption_app` component to use Internal Events Tracking.
If you are using `data-track-action` in the component, you have to change it to `data-event-tracking` to migrate to Internal Events Tracking.

View File

@ -81,11 +81,10 @@ For more information about our plans for language support in SAST, see the [cate
| TypeScript | [Semgrep](https://gitlab.com/gitlab-org/security-products/analyzers/semgrep) with [GitLab-managed rules](https://gitlab.com/gitlab-org/security-products/analyzers/semgrep/#sast-rules) | 13.10 |
<html>
<small>Footnotes:
Footnotes:
<ol>
<li>The SpotBugs-based analyzer supports [Gradle](https://gradle.org/), [Maven](https://maven.apache.org/), and [SBT](https://www.scala-sbt.org/). It can also be used with variants like the [Gradle wrapper](https://docs.gradle.org/current/userguide/gradle_wrapper.html), [Grails](https://grails.org/), and the [Maven wrapper](https://github.com/takari/maven-wrapper). However, SpotBugs has [limitations](https://gitlab.com/gitlab-org/gitlab/-/issues/350801) when used against [Ant](https://ant.apache.org/)-based projects. We recommend using the Semgrep-based analyzer for Ant-based Java or Scala projects.</li>
<li>The SpotBugs-based analyzer supports <a href="https://gradle.org/">Gradle</a>, <a href="https://maven.apache.org/">Maven</a>, and <a href="https://www.scala-sbt.org/">SBT</a>. It can also be used with variants like the <a href="https://docs.gradle.org/current/userguide/gradle_wrapper.html">Gradle wrapper</a>, <a href="https://grails.org/">Grails</a>, and the <a href="https://github.com/takari/maven-wrapper">Maven wrapper</a>. However, SpotBugs has <a href="https://gitlab.com/gitlab-org/gitlab/-/issues/350801">limitations</a> when used against <a href="https://ant.apache.org/">Ant</a>-based projects. You should use the Semgrep-based analyzer for Ant-based Java or Scala projects.</li>
</ol>
</small>
</html>
## End of supported analyzers

View File

@ -203,10 +203,7 @@ A top-level group Owner can [set up verified domains to bypass confirmation emai
### Get users' email addresses through the API
A top-level group Owner can use the [group and project members API](../../api/members.md) to access
users' information. For users provisioned by the group with [SCIM](../group/saml_sso/scim_setup.md),
this information includes users' email addresses.
[Issue 391453](https://gitlab.com/gitlab-org/gitlab/-/issues/391453) proposes to change the criteria for access to email addresses from provisioned users to enterprise users.
users' information. For enterprise users of the group this information includes users' email addresses.
### Remove enterprise management features from an account

View File

@ -738,6 +738,12 @@ module API
namespace: namespace,
project: project
)
rescue Gitlab::InternalEvents::UnknownEventError => e
Gitlab::ErrorTracking.track_exception(e, event_name: event_name)
# We want to keep the error silent on production to keep the behavior
# consistent with StandardError rescue
unprocessable_entity!(e.message) if Gitlab.dev_or_test_env?
rescue StandardError => e
Gitlab::ErrorTracking.track_and_raise_for_dev_exception(e, event_name: event_name)
end

View File

@ -73,7 +73,7 @@ module API
end
desc 'Updates a group or project invitation.' do
success Entities::Member
success Entities::Invitation
tags %w[invitations]
end
params do
@ -103,7 +103,7 @@ module API
updated_member = result[:members].first
if result[:status] == :success
present_members updated_member
present_member_invitations updated_member
else
render_validation_error!(updated_member)
end

View File

@ -27,7 +27,9 @@ module BulkImports
return if user_membership && user_membership[:access_level] >= data[:access_level]
# Create new membership for any other access level
portable.members.create!(data)
member = portable.members.new(data)
member.importing = true # avoid sending new member notification to the invited user
member.save!
end
private

View File

@ -2918,6 +2918,9 @@ msgstr ""
msgid "Add an impersonation token"
msgstr ""
msgid "Add another branch"
msgstr ""
msgid "Add another link"
msgstr ""
@ -44054,6 +44057,9 @@ msgstr ""
msgid "SecurityOrchestration|Add new approver"
msgstr ""
msgid "SecurityOrchestration|Add project full path after @ to following branches: %{branches}"
msgstr ""
msgid "SecurityOrchestration|Add protected branches"
msgstr ""
@ -44123,6 +44129,9 @@ msgstr ""
msgid "SecurityOrchestration|Choose approver type"
msgstr ""
msgid "SecurityOrchestration|Choose exception branches"
msgstr ""
msgid "SecurityOrchestration|Choose specific role"
msgstr ""
@ -44192,6 +44201,9 @@ msgstr ""
msgid "SecurityOrchestration|Every time a pipeline runs for %{branches}%{branchExceptionsString}"
msgstr ""
msgid "SecurityOrchestration|Exception branches"
msgstr ""
msgid "SecurityOrchestration|Exceptions"
msgstr ""
@ -44207,6 +44219,9 @@ msgstr ""
msgid "SecurityOrchestration|Failed to load images."
msgstr ""
msgid "SecurityOrchestration|Fill in branch name with project name in the format of %{boldStart}branch-name@project-path,%{boldEnd} separate with `,`"
msgstr ""
msgid "SecurityOrchestration|Following projects:"
msgstr ""
@ -44276,6 +44291,9 @@ msgstr ""
msgid "SecurityOrchestration|No actions defined - policy will not run."
msgstr ""
msgid "SecurityOrchestration|No branches yet"
msgstr ""
msgid "SecurityOrchestration|No compliance frameworks"
msgstr ""
@ -44320,6 +44338,9 @@ msgstr ""
msgid "SecurityOrchestration|Overwrite the current CI/CD code with the new file's content?"
msgstr ""
msgid "SecurityOrchestration|Please remove duplicated values"
msgstr ""
msgid "SecurityOrchestration|Policies"
msgstr ""

View File

@ -2,7 +2,7 @@
source 'https://rubygems.org'
gem 'gitlab-qa', '~> 13', '>= 13.1.0', require: 'gitlab/qa'
gem 'gitlab-qa', '~> 14', require: 'gitlab/qa'
gem 'gitlab_quality-test_tooling', '~> 1.11.0', require: false
gem 'gitlab-utils', path: '../gems/gitlab-utils'
gem 'activesupport', '~> 7.0.8' # This should stay in sync with the root's Gemfile

View File

@ -118,8 +118,8 @@ GEM
gitlab (4.19.0)
httparty (~> 0.20)
terminal-table (>= 1.5.1)
gitlab-qa (13.1.0)
activesupport (>= 6.1, < 7.1)
gitlab-qa (14.0.0)
activesupport (>= 6.1, < 7.2)
gitlab (~> 4.19)
http (~> 5.0)
nokogiri (~> 1.10)
@ -354,7 +354,7 @@ DEPENDENCIES
faraday-retry (~> 2.2)
fog-core (= 2.1.0)
fog-google (~> 1.19)
gitlab-qa (~> 13, >= 13.1.0)
gitlab-qa (~> 14)
gitlab-utils!
gitlab_quality-test_tooling (~> 1.11.0)
influxdb-client (~> 3.0)
@ -380,4 +380,4 @@ DEPENDENCIES
zeitwerk (~> 2.6, >= 2.6.12)
BUNDLED WITH
2.5.4
2.5.5

View File

@ -402,7 +402,7 @@ RSpec.describe 'Login', :clean_gitlab_redis_sessions, feature_category: :system_
it 'displays the remember me checkbox' do
visit new_user_session_path
expect(page).to have_field('remember_me_omniauth')
expect(page).to have_field('js-remember-me-omniauth')
end
context 'when remember me is not enabled' do
@ -413,7 +413,7 @@ RSpec.describe 'Login', :clean_gitlab_redis_sessions, feature_category: :system_
it 'does not display the remember me checkbox' do
visit new_user_session_path
expect(page).not_to have_field('remember_me_omniauth')
expect(page).not_to have_field('js-remember-me-omniauth')
end
end

View File

@ -1,21 +0,0 @@
<div class="js-oauth-login">
<input id="remember_me_omniauth" type="checkbox" />
<form method="post" action="http://example.com/">
<button class="twitter" type="submit">
<span>Twitter</span>
</button>
</form>
<form method="post" action="http://example.com/">
<button class="github" type="submit">
<span>GitHub</span>
</button>
</form>
<form method="post" action="http://example.com/?redirect_fragment=L1">
<button class="facebook" type="submit">
<span>Facebook</span>
</button>
</form>
</div>

View File

@ -1,36 +0,0 @@
import $ from 'jquery';
import htmlOauthRememberMe from 'test_fixtures_static/oauth_remember_me.html';
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import OAuthRememberMe from '~/pages/sessions/new/oauth_remember_me';
describe('OAuthRememberMe', () => {
const findFormAction = (selector) => {
return $(`.js-oauth-login ${selector}`).parent('form').attr('action');
};
beforeEach(() => {
setHTMLFixture(htmlOauthRememberMe);
new OAuthRememberMe({ container: $('.js-oauth-login') }).bindEvents();
});
afterEach(() => {
resetHTMLFixture();
});
it('adds and removes the "remember_me" query parameter from all OAuth login buttons', () => {
$('.js-oauth-login #remember_me_omniauth').click();
expect(findFormAction('.twitter')).toBe('http://example.com/?remember_me=1');
expect(findFormAction('.github')).toBe('http://example.com/?remember_me=1');
expect(findFormAction('.facebook')).toBe(
'http://example.com/?redirect_fragment=L1&remember_me=1',
);
$('.js-oauth-login #remember_me_omniauth').click();
expect(findFormAction('.twitter')).toBe('http://example.com/');
expect(findFormAction('.github')).toBe('http://example.com/');
expect(findFormAction('.facebook')).toBe('http://example.com/?redirect_fragment=L1');
});
});

View File

@ -1,13 +1,12 @@
import $ from 'jquery';
import htmlSessionsNew from 'test_fixtures/sessions/new.html';
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import preserveUrlFragment from '~/pages/sessions/new/preserve_url_fragment';
import {
appendUrlFragment,
appendRedirectQuery,
toggleRememberMeQuery,
} from '~/pages/sessions/new/preserve_url_fragment';
describe('preserve_url_fragment', () => {
const findFormAction = (selector) => {
return $(`.js-oauth-login ${selector}`).parent('form').attr('action');
};
beforeEach(() => {
setHTMLFixture(htmlSessionsNew);
});
@ -16,41 +15,74 @@ describe('preserve_url_fragment', () => {
resetHTMLFixture();
});
it('adds the url fragment to the login form actions', () => {
preserveUrlFragment('#L65');
describe('non-OAuth login forms', () => {
describe('appendUrlFragment', () => {
const findFormAction = () => document.querySelector('.js-non-oauth-login form').action;
expect($('#new_user').attr('action')).toBe('http://test.host/users/sign_in#L65');
it('adds the url fragment to the login form actions', () => {
appendUrlFragment('#L65');
expect(findFormAction()).toBe('http://test.host/users/sign_in#L65');
});
it('does not add an empty url fragment to the login form actions', () => {
appendUrlFragment();
expect(findFormAction()).toBe('http://test.host/users/sign_in');
});
});
});
it('does not add an empty url fragment to the login form actions', () => {
preserveUrlFragment();
describe('OAuth login forms', () => {
const findFormAction = (selector) =>
document.querySelector(`.js-oauth-login #oauth-login-${selector}`).parentElement.action;
expect($('#new_user').attr('action')).toBe('http://test.host/users/sign_in');
});
describe('appendRedirectQuery', () => {
it('does not add an empty query parameter to the login form actions', () => {
appendRedirectQuery();
it('does not add an empty query parameter to OmniAuth login buttons', () => {
preserveUrlFragment();
expect(findFormAction('auth0')).toBe('http://test.host/users/auth/auth0');
});
expect(findFormAction('#oauth-login-auth0')).toBe('http://test.host/users/auth/auth0');
});
describe('adds "redirect_fragment" query parameter to the login form actions', () => {
it('when "remember_me" is not present', () => {
appendRedirectQuery('#L65');
describe('adds "redirect_fragment" query parameter to OmniAuth login buttons', () => {
it('when "remember_me" is not present', () => {
preserveUrlFragment('#L65');
expect(findFormAction('auth0')).toBe(
'http://test.host/users/auth/auth0?redirect_fragment=L65',
);
});
expect(findFormAction('#oauth-login-auth0')).toBe(
'http://test.host/users/auth/auth0?redirect_fragment=L65',
);
it('when "remember_me" is present', () => {
document
.querySelectorAll('form')
.forEach((form) => form.setAttribute('action', `${form.action}?remember_me=1`));
appendRedirectQuery('#L65');
expect(findFormAction('auth0')).toBe(
'http://test.host/users/auth/auth0?remember_me=1&redirect_fragment=L65',
);
});
});
});
it('when "remember-me" is present', () => {
$('.js-oauth-login form').attr('action', (i, href) => `${href}?remember_me=1`);
describe('toggleRememberMeQuery', () => {
const rememberMe = () => document.querySelector('#js-remember-me-omniauth');
preserveUrlFragment('#L65');
it('toggles "remember_me" query parameter', () => {
toggleRememberMeQuery();
expect(findFormAction('#oauth-login-auth0')).toBe(
'http://test.host/users/auth/auth0?remember_me=1&redirect_fragment=L65',
);
expect(findFormAction('auth0')).toBe('http://test.host/users/auth/auth0');
rememberMe().click();
expect(findFormAction('auth0')).toBe('http://test.host/users/auth/auth0?remember_me=1');
rememberMe().click();
expect(findFormAction('auth0')).toBe('http://test.host/users/auth/auth0');
});
});
});
});

View File

@ -16,14 +16,15 @@ const commit = {
const findTextExpander = () => wrapper.findComponent(GlButton);
const findUserLink = () => wrapper.findByText(commit.author.name);
const findCommitterWrapper = () => wrapper.findByTestId('committer');
const findUserAvatarLink = () => wrapper.findComponent(UserAvatarLink);
const findAuthorName = () => wrapper.findByText(`${commit.authorName} authored`);
const findCommitRowDescription = () => wrapper.find('pre');
const findTitleHtml = () => wrapper.findByText(commit.titleHtml);
const createComponent = async ({ commitMock = {}, prevBlameLink } = {}) => {
const createComponent = async ({ commitMock = {}, prevBlameLink, span = 3 } = {}) => {
wrapper = shallowMountExtended(CommitInfo, {
propsData: { commit: { ...commit, ...commitMock }, prevBlameLink },
propsData: { commit: { ...commit, ...commitMock }, prevBlameLink, span },
});
await nextTick();
@ -46,6 +47,22 @@ describe('Repository last commit component', () => {
expect(findAuthorName().exists()).toBe(true);
});
it('truncates author name when commit spans less than 3 lines', () => {
createComponent({ span: 2 });
expect(findCommitterWrapper().classes()).toEqual([
'committer',
'gl-flex-basis-full',
'gl-display-inline-flex',
]);
expect(findUserLink().classes()).toEqual([
'commit-author-link',
'js-user-link',
'gl-display-inline-block',
'gl-text-truncate',
]);
});
it('does not render description expander when description is null', () => {
createComponent();

View File

@ -4,6 +4,7 @@ import InternalEvents from '~/tracking/internal_events';
import { LOAD_INTERNAL_EVENTS_SELECTOR } from '~/tracking/constants';
import * as utils from '~/tracking/utils';
import { Tracker } from '~/tracking/tracker';
import Tracking from '~/tracking';
jest.mock('~/api', () => ({
trackInternalEvent: jest.fn(),
@ -20,13 +21,23 @@ const event = 'TestEvent';
describe('InternalEvents', () => {
describe('trackEvent', () => {
const category = 'TestCategory';
it('trackEvent calls API.trackInternalEvent with correct arguments', () => {
InternalEvents.trackEvent(event);
InternalEvents.trackEvent(event, category);
expect(API.trackInternalEvent).toHaveBeenCalledTimes(1);
expect(API.trackInternalEvent).toHaveBeenCalledWith(event);
});
it('trackEvent calls Tracking.event with correct arguments including category', () => {
jest.spyOn(Tracking, 'event').mockImplementation(() => {});
InternalEvents.trackEvent(event, category);
expect(Tracking.event).toHaveBeenCalledWith(category, event, expect.any(Object));
});
it('trackEvent calls trackBrowserSDK with correct arguments', () => {
jest.spyOn(InternalEvents, 'trackBrowserSDK').mockImplementation(() => {});
@ -63,7 +74,7 @@ describe('InternalEvents', () => {
await wrapper.findByTestId('button').trigger('click');
expect(trackEventSpy).toHaveBeenCalledTimes(1);
expect(trackEventSpy).toHaveBeenCalledWith(event);
expect(trackEventSpy).toHaveBeenCalledWith(event, undefined);
});
});

View File

@ -42,7 +42,7 @@ RSpec.describe GitlabSchema.types['Project'], feature_category: :groups_and_proj
timelog_categories fork_targets branch_rules ci_config_variables pipeline_schedules languages
incident_management_timeline_event_tags visible_forks inherited_ci_variables autocomplete_users
ci_cd_settings detailed_import_status value_streams ml_models
allows_multiple_merge_request_assignees allows_multiple_merge_request_reviewers
allows_multiple_merge_request_assignees allows_multiple_merge_request_reviewers is_forked
]
expect(described_class).to include_graphql_fields(*expected_fields)
@ -771,6 +771,57 @@ RSpec.describe GitlabSchema.types['Project'], feature_category: :groups_and_proj
end
end
describe 'is_forked' do
let_it_be(:user) { create(:user) }
let_it_be(:unforked_project) { create(:project, :public) }
let!(:forked_project) { fork_project(unforked_project) }
let(:project) { nil }
let(:query) do
%(
query {
project(fullPath: "#{project.full_path}") {
isForked
}
}
)
end
let(:response) { GitlabSchema.execute(query).as_json }
subject(:is_forked) { response.dig('data', 'project', 'isForked') }
context 'when project has a fork network' do
context 'when fork is itself' do
let(:project) { unforked_project }
it { is_expected.to be false }
end
context 'when fork is not itself' do
let(:project) { forked_project }
it { is_expected.to be true }
it 'avoids N+1 queries' do
query_count = ActiveRecord::QueryRecorder.new { response }
expect(query_count).not_to exceed_query_limit(8)
end
end
end
context 'when project does not have a fork network' do
let(:project) { unforked_project }
before do
allow(project).to receive(:fork_network).and_return(nil)
end
it { is_expected.to be false }
end
end
describe 'branch_rules' do
let_it_be(:user) { create(:user) }
let_it_be(:project, reload: true) { create(:project, :public) }

View File

@ -860,13 +860,30 @@ RSpec.describe API::Helpers, feature_category: :shared do
)
end
it 'logs an exception for unknown event' do
it 'tracks an exception and renders 422 for unknown event', :aggregate_failures do
expect(Gitlab::InternalEvents).to receive(:track_event).and_raise(Gitlab::InternalEvents::UnknownEventError, "Unknown event: #{unknown_event}")
expect(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception)
expect(Gitlab::ErrorTracking).to receive(:track_exception)
.with(
instance_of(Gitlab::InternalEvents::UnknownEventError),
event_name: unknown_event
)
expect(helper).to receive(:unprocessable_entity!).with("Unknown event: #{unknown_event}")
helper.track_event(unknown_event,
user: user,
namespace_id: namespace.id,
project_id: project.id
)
end
it 'logs an exception for tracking errors' do
expect(Gitlab::InternalEvents).to receive(:track_event).and_raise(ArgumentError, "Error message")
expect(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception)
.with(
instance_of(ArgumentError),
event_name: unknown_event
)
helper.track_event(unknown_event,
user: user,

View File

@ -87,6 +87,12 @@ RSpec.describe BulkImports::Common::Pipelines::MembersPipeline, feature_category
expect(member.expires_at).to eq(nil)
end
it 'does not send new member notification' do
expect(NotificationService).not_to receive(:new)
subject.load(context, member_data)
end
context 'when user_id is current user id' do
it 'does not create new membership' do
data = { user_id: user.id }

View File

@ -2,7 +2,7 @@
require 'spec_helper'
RSpec.describe Gitlab::BackgroundMigration::UpdateCiPipelineArtifactsUnknownLockedStatus do
RSpec.describe Gitlab::BackgroundMigration::UpdateCiPipelineArtifactsUnknownLockedStatus, feature_category: :build_artifacts do
describe '#perform' do
let(:batch_table) { :ci_pipeline_artifacts }
let(:batch_column) { :id }
@ -30,11 +30,11 @@ RSpec.describe Gitlab::BackgroundMigration::UpdateCiPipelineArtifactsUnknownLock
let(:locked_pipeline) { pipelines.create!(locked: locked, partition_id: 100) }
# rubocop:disable Layout/LineLength
let!(:locked_artifact) { pipeline_artifacts.create!(project_id: project.id, pipeline_id: locked_pipeline.id, size: 1024, file_type: 0, file_format: 'gzip', file: 'a.gz', locked: unknown) }
let!(:unlocked_artifact_1) { pipeline_artifacts.create!(project_id: project.id, pipeline_id: unlocked_pipeline.id, size: 2048, file_type: 1, file_format: 'raw', file: 'b', locked: unknown) }
let!(:unlocked_artifact_2) { pipeline_artifacts.create!(project_id: project.id, pipeline_id: unlocked_pipeline.id, size: 4096, file_type: 2, file_format: 'gzip', file: 'c.gz', locked: unknown) }
let!(:already_unlocked_artifact) { pipeline_artifacts.create!(project_id: project.id, pipeline_id: unlocked_pipeline.id, size: 8192, file_type: 3, file_format: 'raw', file: 'd', locked: unlocked) }
let!(:already_locked_artifact) { pipeline_artifacts.create!(project_id: project.id, pipeline_id: locked_pipeline.id, size: 8192, file_type: 3, file_format: 'raw', file: 'd', locked: locked) }
let!(:locked_artifact) { pipeline_artifacts.create!(project_id: project.id, pipeline_id: locked_pipeline.id, size: 1024, file_type: 0, file_format: 'gzip', file: 'a.gz', locked: unknown, partition_id: 100) }
let!(:unlocked_artifact_1) { pipeline_artifacts.create!(project_id: project.id, pipeline_id: unlocked_pipeline.id, size: 2048, file_type: 1, file_format: 'raw', file: 'b', locked: unknown, partition_id: 100) }
let!(:unlocked_artifact_2) { pipeline_artifacts.create!(project_id: project.id, pipeline_id: unlocked_pipeline.id, size: 4096, file_type: 2, file_format: 'gzip', file: 'c.gz', locked: unknown, partition_id: 100) }
let!(:already_unlocked_artifact) { pipeline_artifacts.create!(project_id: project.id, pipeline_id: unlocked_pipeline.id, size: 8192, file_type: 3, file_format: 'raw', file: 'd', locked: unlocked, partition_id: 100) }
let!(:already_locked_artifact) { pipeline_artifacts.create!(project_id: project.id, pipeline_id: locked_pipeline.id, size: 8192, file_type: 3, file_format: 'raw', file: 'd', locked: locked, partition_id: 100) }
# rubocop:enable Layout/LineLength
subject do

View File

@ -25,4 +25,30 @@ RSpec.describe ForkNetworkMember do
expect(ForkNetwork.count).to eq(1)
end
end
describe '#by_projects' do
let_it_be(:fork_network_member_1) { create(:fork_network_member) }
let_it_be(:fork_network_member_2) { create(:fork_network_member) }
it 'returns fork network members by project ids' do
expect(
described_class.by_projects(
[fork_network_member_1.project_id, fork_network_member_2.project_id]
)
).to match_array([fork_network_member_1, fork_network_member_2])
end
end
describe '#with_fork_network' do
let_it_be(:fork_network_member_1) { create(:fork_network_member) }
let_it_be(:fork_network_member_2) { create(:fork_network_member) }
it 'avoids N+1 queries' do
query_count = ActiveRecord::QueryRecorder.new do
described_class.all.with_fork_network.find_each(&:fork_network)
end
expect(query_count).not_to exceed_query_limit(1)
end
end
end

View File

@ -14,6 +14,42 @@ RSpec.describe 'groups autocomplete', feature_category: :groups_and_projects do
sign_in(user)
end
describe '#members' do
context 'when type is WorkItem' do
let(:type) { 'Workitem' }
it 'returns the correct response', :aggregate_failures do
work_item = create(:work_item, :group_level, namespace: group, author: user)
get members_group_autocomplete_sources_path(group, type_id: work_item.iid, type: type)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to be_an(Array)
expect(json_response).to contain_exactly(
hash_including('type' => 'User', 'username' => user.username),
hash_including('type' => 'Group', 'username' => group.full_path)
)
end
end
context 'when type is Issue' do
let(:type) { 'Issue' }
it 'returns the correct response', :aggregate_failures do
issue = create(:issue, :group_level, namespace: group, author: user)
get members_group_autocomplete_sources_path(group, type_id: issue.iid, type: type)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to be_an(Array)
expect(json_response).to contain_exactly(
hash_including('type' => 'User', 'username' => user.username),
hash_including('type' => 'Group', 'username' => group.full_path)
)
end
end
end
describe '#issues' do
using RSpec::Parameterized::TableSyntax

View File

@ -3,13 +3,11 @@
require 'spec_helper'
RSpec.describe QuickActions::TargetService, feature_category: :team_planning do
let(:project) { create(:project) }
let(:user) { create(:user) }
let(:service) { described_class.new(project, user) }
before do
project.add_maintainer(user)
end
let_it_be(:group) { create(:group) }
let_it_be_with_reload(:project) { create(:project, group: group) }
let_it_be(:user) { create(:user).tap { |u| project.add_maintainer(u) } }
let(:container) { project }
let(:service) { described_class.new(container: container, current_user: user) }
describe '#execute' do
shared_examples 'no target' do |type_iid:|
@ -32,7 +30,7 @@ RSpec.describe QuickActions::TargetService, feature_category: :team_planning do
it 'builds a new target' do
target = service.execute(type, type_iid)
expect(target.project).to eq(project)
expect(target.resource_parent).to eq(container)
expect(target).to be_new_record
end
end
@ -45,6 +43,15 @@ RSpec.describe QuickActions::TargetService, feature_category: :team_planning do
it_behaves_like 'find target'
it_behaves_like 'build target', type_iid: nil
it_behaves_like 'build target', type_iid: -1
context 'when issue belongs to a group' do
let(:container) { group }
let(:target) { create(:issue, :group_level, namespace: group) }
it_behaves_like 'find target'
it_behaves_like 'build target', type_iid: nil
it_behaves_like 'build target', type_iid: -1
end
end
context 'for work item' do
@ -53,6 +60,13 @@ RSpec.describe QuickActions::TargetService, feature_category: :team_planning do
let(:type) { 'WorkItem' }
it_behaves_like 'find target'
context 'when work item belongs to a group' do
let(:container) { group }
let(:target) { create(:work_item, :group_level, namespace: group) }
it_behaves_like 'find target'
end
end
context 'for merge request' do

View File

@ -112,7 +112,7 @@ module LoginHelpers
visit new_user_session_path
expect(page).to have_css('.js-oauth-login')
check 'remember_me_omniauth' if remember_me
check 'js-remember-me-omniauth' if remember_me
click_button "oauth-login-#{provider}"
end