diff options
73 files changed, 694 insertions, 310 deletions
diff --git a/app/assets/javascripts/invite_members/components/invite_members_modal.vue b/app/assets/javascripts/invite_members/components/invite_members_modal.vue index d00a0f1633b..6628fc4063b 100644 --- a/app/assets/javascripts/invite_members/components/invite_members_modal.vue +++ b/app/assets/javascripts/invite_members/components/invite_members_modal.vue @@ -68,6 +68,7 @@ export default { newUsersToInvite: [], selectedDate: undefined, groupToBeSharedWith: {}, + source: 'unknown', }; }, computed: { @@ -195,6 +196,7 @@ export default { ...this.basePostData, email: usersToInviteByEmail, access_level: this.selectedAccessLevel, + invite_source: this.source, }; }, addByUserIdPostData(usersToAddById) { @@ -202,6 +204,7 @@ export default { ...this.basePostData, user_id: usersToAddById, access_level: this.selectedAccessLevel, + invite_source: this.source, }; }, shareWithGroupPostData(groupToBeSharedWith) { diff --git a/app/assets/javascripts/pages/projects/show/index.js b/app/assets/javascripts/pages/projects/show/index.js index 83e43d7ac48..26f8018a968 100644 --- a/app/assets/javascripts/pages/projects/show/index.js +++ b/app/assets/javascripts/pages/projects/show/index.js @@ -3,6 +3,8 @@ import Activities from '~/activities'; import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation'; import BlobViewer from '~/blob/viewer/index'; import { initUploadForm } from '~/blob_edit/blob_bundle'; +import initInviteMembersModal from '~/invite_members/init_invite_members_modal'; +import initInviteMembersTrigger from '~/invite_members/init_invite_members_trigger'; import leaveByUrl from '~/namespaces/leave_by_url'; import initVueNotificationsDropdown from '~/notifications'; import { initUploadFileTrigger } from '~/projects/upload_file_experiment'; @@ -44,3 +46,5 @@ initVueNotificationsDropdown(); new ShortcutsNavigation(); // eslint-disable-line no-new initUploadFileTrigger(); +initInviteMembersModal(); +initInviteMembersTrigger(); diff --git a/app/assets/javascripts/repository/components/table/row.vue b/app/assets/javascripts/repository/components/table/row.vue index 8ea5fce92fa..1b7fb6a63df 100644 --- a/app/assets/javascripts/repository/components/table/row.vue +++ b/app/assets/javascripts/repository/components/table/row.vue @@ -7,13 +7,17 @@ import { GlTooltipDirective, GlLoadingIcon, GlIcon, + GlHoverLoadDirective, } from '@gitlab/ui'; import { escapeRegExp } from 'lodash'; +import filesQuery from 'shared_queries/repository/files.query.graphql'; import { escapeFileUrl } from '~/lib/utils/url_utility'; +import { TREE_PAGE_SIZE } from '~/repository/constants'; import FileIcon from '~/vue_shared/components/file_icon.vue'; import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import getRefMixin from '../../mixins/get_ref'; +import blobInfoQuery from '../../queries/blob_info.query.graphql'; import commitQuery from '../../queries/commit.query.graphql'; export default { @@ -28,6 +32,7 @@ export default { }, directives: { GlTooltip: GlTooltipDirective, + GlHoverLoad: GlHoverLoadDirective, }, apollo: { commit: { @@ -139,6 +144,33 @@ export default { return this.commit && this.commit.lockLabel; }, }, + methods: { + handlePreload() { + return this.isFolder ? this.loadFolder() : this.loadBlob(); + }, + loadFolder() { + this.apolloQuery(filesQuery, { + projectPath: this.projectPath, + ref: this.ref, + path: this.path, + nextPageCursor: '', + pageSize: TREE_PAGE_SIZE, + }); + }, + loadBlob() { + if (!this.refactorBlobViewerEnabled) { + return; + } + + this.apolloQuery(blobInfoQuery, { + projectPath: this.projectPath, + filePath: this.path, + }); + }, + apolloQuery(query, variables) { + this.$apollo.query({ query, variables }); + }, + }, }; </script> @@ -148,6 +180,7 @@ export default { <component :is="linkComponent" ref="link" + v-gl-hover-load="handlePreload" :to="routerLinkTo" :href="url" :class="{ diff --git a/app/assets/javascripts/repository/queries/blob_info.query.graphql b/app/assets/javascripts/repository/queries/blob_info.query.graphql index 9eee8b2eef3..59d60c9f174 100644 --- a/app/assets/javascripts/repository/queries/blob_info.query.graphql +++ b/app/assets/javascripts/repository/queries/blob_info.query.graphql @@ -1,6 +1,5 @@ query getBlobInfo($projectPath: ID!, $filePath: String!) { project(fullPath: $projectPath) { - id repository { blobs(paths: [$filePath]) { nodes { diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 00b9fb1060d..a08f1f5d391 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -457,9 +457,7 @@ class ApplicationController < ActionController::Base def set_current_context(&block) Gitlab::ApplicationContext.with_context( - # Avoid loading the auth_user again after the request. Otherwise calling - # `auth_user` again would also trigger the Warden callbacks again - user: -> { auth_user if strong_memoized?(:auth_user) }, + user: -> { context_user }, project: -> { @project if @project&.persisted? }, namespace: -> { @group if @group&.persisted? }, caller_id: caller_id, @@ -542,6 +540,12 @@ class ApplicationController < ActionController::Base end end + # Avoid loading the auth_user again after the request. Otherwise calling + # `auth_user` again would also trigger the Warden callbacks again + def context_user + auth_user if strong_memoized?(:auth_user) + end + def caller_id "#{self.class.name}##{action_name}" end diff --git a/app/controllers/confirmations_controller.rb b/app/controllers/confirmations_controller.rb index e82500912fa..0b833e149a4 100644 --- a/app/controllers/confirmations_controller.rb +++ b/app/controllers/confirmations_controller.rb @@ -34,6 +34,10 @@ class ConfirmationsController < Devise::ConfirmationsController def after_sign_in(resource) after_sign_in_path_for(resource) end + + def context_user + resource + end end ConfirmationsController.prepend_mod_with('ConfirmationsController') diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb index 31f404a9974..9d7a1712698 100644 --- a/app/controllers/omniauth_callbacks_controller.rb +++ b/app/controllers/omniauth_callbacks_controller.rb @@ -287,6 +287,10 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController def fail_admin_mode_invalid_credentials redirect_to new_admin_session_path, alert: _('Invalid login or password') end + + def context_user + current_user + end end OmniauthCallbacksController.prepend_mod_with('OmniauthCallbacksController') diff --git a/app/controllers/passwords_controller.rb b/app/controllers/passwords_controller.rb index 2c0ed825daa..c764f2d0459 100644 --- a/app/controllers/passwords_controller.rb +++ b/app/controllers/passwords_controller.rb @@ -67,6 +67,10 @@ class PasswordsController < Devise::PasswordsController redirect_to new_user_session_path, notice: I18n.t('devise.passwords.send_paranoid_instructions') end + + def context_user + resource + end end PasswordsController.prepend_mod_with('PasswordsController') diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb index 0f29f6f608f..a0710c1f8e7 100644 --- a/app/controllers/registrations_controller.rb +++ b/app/controllers/registrations_controller.rb @@ -202,6 +202,10 @@ class RegistrationsController < Devise::RegistrationsController experiment(:invite_signup_page_interaction, actor: member).track(:form_submission) experiment('members/invite_email', actor: member).track(:accepted) end + + def context_user + current_user + end end RegistrationsController.prepend_mod_with('RegistrationsController') diff --git a/app/graphql/types/ci/runner_type_enum.rb b/app/graphql/types/ci/runner_type_enum.rb index f771635f4ed..12e87906179 100644 --- a/app/graphql/types/ci/runner_type_enum.rb +++ b/app/graphql/types/ci/runner_type_enum.rb @@ -5,10 +5,10 @@ module Types class RunnerTypeEnum < BaseEnum graphql_name 'CiRunnerType' - ::Ci::Runner.runner_types.keys.each do |type| - value type.upcase, - description: "A runner that is #{type.tr('_', ' ')}.", - value: type + ::Ci::Runner::AVAILABLE_TYPES.each do |runner_type| + value runner_type.upcase, + description: "A runner that is #{runner_type.tr('_', ' ')}.", + value: runner_type end end end diff --git a/app/graphql/types/member_interface.rb b/app/graphql/types/member_interface.rb index 1c7257487d9..6a21e51fe28 100644 --- a/app/graphql/types/member_interface.rb +++ b/app/graphql/types/member_interface.rb @@ -22,7 +22,7 @@ module Types field :expires_at, Types::TimeType, null: true, description: 'Date and time the membership expires.' - field :user, Types::UserType, null: false, + field :user, Types::UserType, null: true, description: 'User that is associated with the member object.' definition_methods do diff --git a/app/models/concerns/cache_markdown_field.rb b/app/models/concerns/cache_markdown_field.rb index a5cf947ba07..87a1bb877ae 100644 --- a/app/models/concerns/cache_markdown_field.rb +++ b/app/models/concerns/cache_markdown_field.rb @@ -27,7 +27,7 @@ module CacheMarkdownField # Returns the default Banzai render context for the cached markdown field. def banzai_render_context(field) raise ArgumentError, "Unknown field: #{field.inspect}" unless - cached_markdown_fields.markdown_fields.include?(field) + cached_markdown_fields.key?(field) # Always include a project key, or Banzai complains project = self.project if self.respond_to?(:project) @@ -100,7 +100,7 @@ module CacheMarkdownField def cached_html_for(markdown_field) raise ArgumentError, "Unknown field: #{markdown_field}" unless - cached_markdown_fields.markdown_fields.include?(markdown_field) + cached_markdown_fields.key?(markdown_field) __send__(cached_markdown_fields.html_field(markdown_field)) # rubocop:disable GitlabSecurity/PublicSend end @@ -108,7 +108,7 @@ module CacheMarkdownField # Updates the markdown cache if necessary, then returns the field # Unlike `cached_html_for` it returns `nil` if the field does not exist def updated_cached_html_for(markdown_field) - return unless cached_markdown_fields.markdown_fields.include?(markdown_field) + return unless cached_markdown_fields.key?(markdown_field) if attribute_invalidated?(cached_markdown_fields.html_field(markdown_field)) # Invalidated due to Markdown content change diff --git a/app/models/integrations/builds_email.rb b/app/models/integrations/builds_email.rb deleted file mode 100644 index 2628848667e..00000000000 --- a/app/models/integrations/builds_email.rb +++ /dev/null @@ -1,16 +0,0 @@ -# frozen_string_literal: true - -# This class is to be removed with 9.1 -# We should also by then remove BuildsEmailService from database -# https://gitlab.com/gitlab-org/gitlab/-/issues/331064 -module Integrations - class BuildsEmail < Integration - def self.to_param - 'builds_email' - end - - def self.supported_events - %w[] - end - end -end diff --git a/app/models/member.rb b/app/models/member.rb index b8203849ee9..0636c3c2d4e 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -108,10 +108,14 @@ class Member < ApplicationRecord scope :active_without_invites_and_requests, -> do left_join_users .where(users: { state: 'active' }) - .non_request + .without_invites_and_requests + .reorder(nil) + end + + scope :without_invites_and_requests, -> do + non_request .non_invite .non_minimal_access - .reorder(nil) end scope :invite, -> { where.not(invite_token: nil) } diff --git a/app/services/issues/zoom_link_service.rb b/app/services/issues/zoom_link_service.rb index ef48134dec4..1ce459aa7e6 100644 --- a/app/services/issues/zoom_link_service.rb +++ b/app/services/issues/zoom_link_service.rb @@ -47,11 +47,11 @@ module Issues attr_reader :issue def track_meeting_added_event - ::Gitlab::Tracking.event('IncidentManagement::ZoomIntegration', 'add_zoom_meeting', label: 'Issue ID', value: issue.id) + ::Gitlab::Tracking.event('IncidentManagement::ZoomIntegration', 'add_zoom_meeting', label: 'Issue ID', value: issue.id, user: current_user, project: @project, namespace: @project.namespace) end def track_meeting_removed_event - ::Gitlab::Tracking.event('IncidentManagement::ZoomIntegration', 'remove_zoom_meeting', label: 'Issue ID', value: issue.id) + ::Gitlab::Tracking.event('IncidentManagement::ZoomIntegration', 'remove_zoom_meeting', label: 'Issue ID', value: issue.id, user: current_user, project: @project, namespace: @project.namespace) end def add_zoom_meeting(link) diff --git a/app/services/members/create_service.rb b/app/services/members/create_service.rb index d3a0b6ca62d..f2556966708 100644 --- a/app/services/members/create_service.rb +++ b/app/services/members/create_service.rb @@ -75,7 +75,7 @@ module Members def after_execute(member:) super - Gitlab::Tracking.event(self.class.name, 'create_member', label: invite_source, property: tracking_property(member)) + Gitlab::Tracking.event(self.class.name, 'create_member', label: invite_source, property: tracking_property(member), user: current_user) end def invite_source diff --git a/app/views/projects/_invite_members.html.haml b/app/views/projects/_invite_members.html.haml index e3a512d6451..ab630d34501 100644 --- a/app/views/projects/_invite_members.html.haml +++ b/app/views/projects/_invite_members.html.haml @@ -3,6 +3,10 @@ track_event: 'render' } } = s_('InviteMember|Invite your team') %p= s_('InviteMember|Add members to this project and start collaborating with your team.') -= link_to s_('InviteMember|Invite members'), project_project_members_path(@project, sort: :access_level_desc), - class: 'gl-button btn btn-confirm gl-mb-8 gl-xs-w-full', - data: { track_event: 'click_button', track_label: 'invite_members_empty_project' } +.js-invite-members-trigger{ data: { variant: 'confirm', + classes: 'gl-mb-8 gl-xs-w-full', + display_text: s_('InviteMember|Invite members'), + event: 'click_button', + label: 'invite_members_empty_project' } } + += render 'shared/issuable/invite_members_trigger', project: @project diff --git a/db/migrate/20210603222333_remove_builds_email_service_from_services.rb b/db/migrate/20210603222333_remove_builds_email_service_from_services.rb new file mode 100644 index 00000000000..791b8b659af --- /dev/null +++ b/db/migrate/20210603222333_remove_builds_email_service_from_services.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class RemoveBuildsEmailServiceFromServices < ActiveRecord::Migration[6.1] + def up + execute("DELETE from services WHERE type = 'BuildsEmailService'") + end + + def down + # no-op + end +end diff --git a/db/schema_migrations/20210603222333 b/db/schema_migrations/20210603222333 new file mode 100644 index 00000000000..25b5055f17e --- /dev/null +++ b/db/schema_migrations/20210603222333 @@ -0,0 +1 @@ +fb02e0fee2760dad203b54d81c342dbf1461b3010503cab05da1eb14ab5d33da
\ No newline at end of file diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index b54a36b6024..3a6433722f5 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -9501,7 +9501,7 @@ Represents a Group Membership. | <a id="groupmembergroup"></a>`group` | [`Group`](#group) | Group that a User is a member of. | | <a id="groupmemberid"></a>`id` | [`ID!`](#id) | ID of the member. | | <a id="groupmemberupdatedat"></a>`updatedAt` | [`Time`](#time) | Date and time the membership was last updated. | -| <a id="groupmemberuser"></a>`user` | [`UserCore!`](#usercore) | User that is associated with the member object. | +| <a id="groupmemberuser"></a>`user` | [`UserCore`](#usercore) | User that is associated with the member object. | | <a id="groupmemberuserpermissions"></a>`userPermissions` | [`GroupPermissions!`](#grouppermissions) | Permissions for the current user on the resource. | ### `GroupPermissions` @@ -11916,7 +11916,7 @@ Represents a Project Membership. | <a id="projectmemberid"></a>`id` | [`ID!`](#id) | ID of the member. | | <a id="projectmemberproject"></a>`project` | [`Project`](#project) | Project that User is a member of. | | <a id="projectmemberupdatedat"></a>`updatedAt` | [`Time`](#time) | Date and time the membership was last updated. | -| <a id="projectmemberuser"></a>`user` | [`UserCore!`](#usercore) | User that is associated with the member object. | +| <a id="projectmemberuser"></a>`user` | [`UserCore`](#usercore) | User that is associated with the member object. | | <a id="projectmemberuserpermissions"></a>`userPermissions` | [`ProjectPermissions!`](#projectpermissions) | Permissions for the current user on the resource. | ### `ProjectPermissions` @@ -15687,7 +15687,7 @@ Implementations: | <a id="memberinterfaceexpiresat"></a>`expiresAt` | [`Time`](#time) | Date and time the membership expires. | | <a id="memberinterfaceid"></a>`id` | [`ID!`](#id) | ID of the member. | | <a id="memberinterfaceupdatedat"></a>`updatedAt` | [`Time`](#time) | Date and time the membership was last updated. | -| <a id="memberinterfaceuser"></a>`user` | [`UserCore!`](#usercore) | User that is associated with the member object. | +| <a id="memberinterfaceuser"></a>`user` | [`UserCore`](#usercore) | User that is associated with the member object. | #### `Noteable` diff --git a/doc/api/groups.md b/doc/api/groups.md index 6ea2e5c5026..5976f0f005d 100644 --- a/doc/api/groups.md +++ b/doc/api/groups.md @@ -752,7 +752,7 @@ Parameters: | `name` | string | yes | The name of the group. | | `path` | string | yes | The path of the group. | | `description` | string | no | The group's description. | -| `membership_lock` | boolean | no | **(STARTER)** Prevent adding new members to project membership within this group. | +| `membership_lock` | boolean | no | **(PREMIUM)** Prevent adding new members to project membership within this group. | | `visibility` | string | no | The group's visibility. Can be `private`, `internal`, or `public`. | | `share_with_group_lock` | boolean | no | Prevent sharing a project with another group within this group. | | `require_two_factor_authentication` | boolean | no | Require all users in this group to setup Two-factor authentication. | @@ -828,7 +828,7 @@ PUT /groups/:id | `name` | string | no | The name of the group. | | `path` | string | no | The path of the group. | | `description` | string | no | The description of the group. | -| `membership_lock` | boolean | no | **(STARTER)** Prevent adding new members to project membership within this group. | +| `membership_lock` | boolean | no | **(PREMIUM)** Prevent adding new members to project membership within this group. | | `share_with_group_lock` | boolean | no | Prevent sharing a project with another group within this group. | | `visibility` | string | no | The visibility level of the group. Can be `private`, `internal`, or `public`. | | `require_two_factor_authentication` | boolean | no | Require all users in this group to setup Two-factor authentication. | @@ -1146,7 +1146,7 @@ DELETE /groups/:id/hooks/:hook_id | `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) | | `hook_id` | integer | yes | The ID of the group hook. | -## Group Audit Events **(STARTER)** +## Group Audit Events **(PREMIUM)** Group audit events can be accessed via the [Group Audit Events API](audit_events.md#group-audit-events) @@ -1302,11 +1302,11 @@ DELETE /groups/:id/share/:group_id | `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) | | `group_id` | integer | yes | The ID of the group to share with | -## Push Rules **(STARTER)** +## Push Rules **(PREMIUM)** -> Introduced in [GitLab Starter](https://about.gitlab.com/pricing/) 13.4. +> Introduced in [GitLab](https://about.gitlab.com/pricing/) 13.4. -### Get group push rules **(STARTER)** +### Get group push rules **(PREMIUM)** Get the [push rules](../user/group/index.md#group-push-rules) of a group. @@ -1349,7 +1349,7 @@ the `commit_committer_check` and `reject_unsigned_commits` parameters: } ``` -### Add group push rule **(STARTER)** +### Add group push rule **(PREMIUM)** Adds [push rules](../user/group/index.md#group-push-rules) to the specified group. @@ -1362,17 +1362,17 @@ POST /groups/:id/push_rule | Attribute | Type | Required | Description | | --------------------------------------------- | -------------- | -------- | ----------- | | `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) | -| `deny_delete_tag` **(STARTER)** | boolean | no | Deny deleting a tag | -| `member_check` **(STARTER)** | boolean | no | Allows only GitLab users to author commits | -| `prevent_secrets` **(STARTER)** | boolean | no | [Files that are likely to contain secrets](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/lib/gitlab/checks/files_denylist.yml) are rejected | -| `commit_message_regex` **(STARTER)** | string | no | All commit messages must match the regular expression provided in this attribute, e.g. `Fixed \d+\..*` | -| `commit_message_negative_regex` **(STARTER)** | string | no | Commit messages matching the regular expression provided in this attribute aren't allowed, e.g. `ssh\:\/\/` | -| `branch_name_regex` **(STARTER)** | string | no | All branch names must match the regular expression provided in this attribute, e.g. `(feature|hotfix)\/*` | -| `author_email_regex` **(STARTER)** | string | no | All commit author emails must match the regular expression provided in this attribute, e.g. `@my-company.com$` | -| `file_name_regex` **(STARTER)** | string | no | Filenames matching the regular expression provided in this attribute are **not** allowed, e.g. `(jar|exe)$` | -| `max_file_size` **(STARTER)** | integer | no | Maximum file size (MB) allowed | -| `commit_committer_check` **(PREMIUM)** | boolean | no | Only commits pushed using verified emails are allowed | -| `reject_unsigned_commits` **(PREMIUM)** | boolean | no | Only commits signed through GPG are allowed | +| `deny_delete_tag` | boolean | no | Deny deleting a tag | +| `member_check` | boolean | no | Allows only GitLab users to author commits | +| `prevent_secrets` | boolean | no | [Files that are likely to contain secrets](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/lib/gitlab/checks/files_denylist.yml) are rejected | +| `commit_message_regex` | string | no | All commit messages must match the regular expression provided in this attribute, e.g. `Fixed \d+\..*` | +| `commit_message_negative_regex` | string | no | Commit messages matching the regular expression provided in this attribute aren't allowed, e.g. `ssh\:\/\/` | +| `branch_name_regex` | string | no | All branch names must match the regular expression provided in this attribute, e.g. `(feature|hotfix)\/*` | +| `author_email_regex` | string | no | All commit author emails must match the regular expression provided in this attribute, e.g. `@my-company.com$` | +| `file_name_regex` | string | no | Filenames matching the regular expression provided in this attribute are **not** allowed, e.g. `(jar|exe)$` | +| `max_file_size` | integer | no | Maximum file size (MB) allowed | +| `commit_committer_check` | boolean | no | Only commits pushed using verified emails are allowed | +| `reject_unsigned_commits` | boolean | no | Only commits signed through GPG are allowed | ```shell curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/groups/19/push_rule" @@ -1396,7 +1396,7 @@ Response: } ``` -### Edit group push rule **(STARTER)** +### Edit group push rule **(PREMIUM)** Edit push rules for a specified group. @@ -1409,17 +1409,17 @@ PUT /groups/:id/push_rule | Attribute | Type | Required | Description | | --------------------------------------------- | -------------- | -------- | ----------- | | `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) | -| `deny_delete_tag` **(STARTER)** | boolean | no | Deny deleting a tag | -| `member_check` **(STARTER)** | boolean | no | Restricts commits to be authored by existing GitLab users only | -| `prevent_secrets` **(STARTER)** | boolean | no | [Files that are likely to contain secrets](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/lib/gitlab/checks/files_denylist.yml) are rejected | -| `commit_message_regex` **(STARTER)** | string | no | All commit messages must match the regular expression provided in this attribute, e.g. `Fixed \d+\..*` | -| `commit_message_negative_regex` **(STARTER)** | string | no | Commit messages matching the regular expression provided in this attribute aren't allowed, e.g. `ssh\:\/\/` | -| `branch_name_regex` **(STARTER)** | string | no | All branch names must match the regular expression provided in this attribute, e.g. `(feature|hotfix)\/*` | -| `author_email_regex` **(STARTER)** | string | no | All commit author emails must match the regular expression provided in this attribute, e.g. `@my-company.com$` | -| `file_name_regex` **(STARTER)** | string | no | Filenames matching the regular expression provided in this attribute are **not** allowed, e.g. `(jar|exe)$` | -| `max_file_size` **(STARTER)** | integer | no | Maximum file size (MB) allowed | -| `commit_committer_check` **(PREMIUM)** | boolean | no | Only commits pushed using verified emails are allowed | -| `reject_unsigned_commits` **(PREMIUM)** | boolean | no | Only commits signed through GPG are allowed | +| `deny_delete_tag` | boolean | no | Deny deleting a tag | +| `member_check` | boolean | no | Restricts commits to be authored by existing GitLab users only | +| `prevent_secrets` | boolean | no | [Files that are likely to contain secrets](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/lib/gitlab/checks/files_denylist.yml) are rejected | +| `commit_message_regex` | string | no | All commit messages must match the regular expression provided in this attribute, e.g. `Fixed \d+\..*` | +| `commit_message_negative_regex` | string | no | Commit messages matching the regular expression provided in this attribute aren't allowed, e.g. `ssh\:\/\/` | +| `branch_name_regex` | string | no | All branch names must match the regular expression provided in this attribute, e.g. `(feature|hotfix)\/*` | +| `author_email_regex` | string | no | All commit author emails must match the regular expression provided in this attribute, e.g. `@my-company.com$` | +| `file_name_regex` | string | no | Filenames matching the regular expression provided in this attribute are **not** allowed, e.g. `(jar|exe)$` | +| `max_file_size` | integer | no | Maximum file size (MB) allowed | +| `commit_committer_check` | boolean | no | Only commits pushed using verified emails are allowed | +| `reject_unsigned_commits` | boolean | no | Only commits signed through GPG are allowed | ```shell curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/groups/19/push_rule" @@ -1443,7 +1443,7 @@ Response: } ``` -### Delete group push rule **(STARTER)** +### Delete group push rule **(PREMIUM)** Deletes the [push rules](../user/group/index.md#group-push-rules) of a group. diff --git a/doc/api/runners.md b/doc/api/runners.md index d8f600670ff..a6886b75a1c 100644 --- a/doc/api/runners.md +++ b/doc/api/runners.md @@ -62,8 +62,9 @@ Example response: "active": true, "description": "test-1-20150125", "id": 6, - "is_shared": false, "ip_address": "127.0.0.1", + "is_shared": false, + "runner_type": "project_type", "name": null, "online": true, "status": "online" @@ -74,6 +75,7 @@ Example response: "id": 8, "ip_address": "127.0.0.1", "is_shared": false, + "runner_type": "group_type", "name": null, "online": false, "status": "offline" @@ -115,6 +117,7 @@ Example response: "id": 1, "ip_address": "127.0.0.1", "is_shared": true, + "runner_type": "instance_type", "name": null, "online": true, "status": "online" @@ -125,6 +128,7 @@ Example response: "id": 3, "ip_address": "127.0.0.1", "is_shared": true, + "runner_type": "instance_type", "name": null, "online": false, "status": "offline" @@ -135,6 +139,7 @@ Example response: "id": 6, "ip_address": "127.0.0.1", "is_shared": false, + "runner_type": "project_type", "name": null, "online": true, "status": "paused" @@ -145,6 +150,7 @@ Example response: "id": 8, "ip_address": "127.0.0.1", "is_shared": false, + "runner_type": "group_type", "name": null, "online": false, "status": "offline" @@ -187,6 +193,7 @@ Example response: "id": 6, "ip_address": "127.0.0.1", "is_shared": false, + "runner_type": "project_type", "contacted_at": "2016-01-25T16:39:48.066Z", "name": null, "online": true, @@ -250,6 +257,7 @@ Example response: "id": 6, "ip_address": "127.0.0.1", "is_shared": false, + "runner_type": "group_type", "contacted_at": "2016-01-25T16:39:48.066Z", "name": null, "online": true, @@ -420,6 +428,7 @@ Example response: "id": 8, "ip_address": "127.0.0.1", "is_shared": false, + "runner_type": "project_type", "name": null, "online": false, "status": "offline" @@ -430,6 +439,7 @@ Example response: "id": 5, "ip_address": "127.0.0.1", "is_shared": true, + "runner_type": "instance_type", "name": null, "online": true, "status": "paused" @@ -464,6 +474,7 @@ Example response: "id": 9, "ip_address": "127.0.0.1", "is_shared": false, + "runner_type": "project_type", "name": null, "online": true, "status": "online" @@ -522,6 +533,7 @@ Example response: "ip_address": "127.0.0.1", "active": true, "is_shared": true, + "runner_type": "instance_type", "name": "gitlab-runner", "online": null, "status": "not_connected" @@ -532,6 +544,7 @@ Example response: "ip_address": "127.0.0.1", "active": true, "is_shared": true, + "runner_type": "instance_type", "name": "gitlab-runner", "online": false, "status": "offline" @@ -542,6 +555,7 @@ Example response: "ip_address": "127.0.0.1", "active": true, "is_shared": false, + "runner_type": "group_type", "name": "gitlab-runner", "online": null, "status": "not_connected" diff --git a/doc/ci/examples/authenticating-with-hashicorp-vault/index.md b/doc/ci/examples/authenticating-with-hashicorp-vault/index.md index 703b8dde94d..fc1e06e91c6 100644 --- a/doc/ci/examples/authenticating-with-hashicorp-vault/index.md +++ b/doc/ci/examples/authenticating-with-hashicorp-vault/index.md @@ -203,6 +203,10 @@ read_secrets: - export PASSWORD="$(vault kv get -field=password secret/myproject/production/db)" ``` +NOTE: +If you're using a Vault instance provided by HashiCorp Cloud Platform, +you need to export the `VAULT_NAMESPACE` variable. Its default value is `admin`. + ![read_secrets staging](img/vault-read-secrets-staging.png) The following job is able to authenticate using the `myproject-production` role and read secrets under `/secret/myproject/production/`: diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index d8357d455a8..834bb442b6b 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -3306,7 +3306,7 @@ job: > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/49775) in GitLab 13.8 > - It's [deployed behind a feature flag](../../user/feature_flags.md), disabled by default. -> - It's enabled on GitLab.com. +> - It's disabled on GitLab.com. > - It's recommended for production use. Use `artifacts:public` to determine whether the job artifacts should be @@ -3524,12 +3524,13 @@ as artifacts. The collected Metrics report uploads to GitLab as an artifact and displays in merge requests. -##### `artifacts:reports:performance` **(PREMIUM)** +##### `artifacts:reports:browser_performance` **(PREMIUM)** > - Introduced in GitLab 11.5. > - Requires GitLab Runner 11.5 and above. +> - [Name changed](https://gitlab.com/gitlab-org/gitlab/-/issues/225914) from `artifacts:reports:performance` in GitLab 14.0. -The `performance` report collects [Browser Performance Testing metrics](../../user/project/merge_requests/browser_performance_testing.md) +The `browser_performance` report collects [Browser Performance Testing metrics](../../user/project/merge_requests/browser_performance_testing.md) as artifacts. The collected Browser Performance report uploads to GitLab as an artifact and displays in merge requests. diff --git a/doc/topics/autodevops/customize.md b/doc/topics/autodevops/customize.md index b9509039cb0..75088c9a4e6 100644 --- a/doc/topics/autodevops/customize.md +++ b/doc/topics/autodevops/customize.md @@ -431,7 +431,8 @@ The following table lists variables used to disable jobs. | `license_scanning` | `LICENSE_MANAGEMENT_DISABLED` | [From GitLab 12.8](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/22773) | If the variable is present, the job isn't created. | | `load_performance` | `LOAD_PERFORMANCE_DISABLED` | From GitLab 13.2 | If the variable is present, the job isn't created. | | `nodejs-scan-sast` | `SAST_DISABLED` | | If the variable is present, the job isn't created. | -| `performance` | `PERFORMANCE_DISABLED` | From GitLab 11.0 | Browser performance. If the variable is present, the job isn't created. | +| `performance` | `PERFORMANCE_DISABLED` | GitLab 11.0 to GitLab 13.12 | Browser performance. If the variable is present, the job isn't created. Replaced by `browser_peformance`. | +| `browser_performance` | `BROWSER_PERFORMANCE_DISABLED` | From GitLab 14.0 | Browser performance. If the variable is present, the job isn't created. Replaces `performance`. | | `phpcs-security-audit-sast` | `SAST_DISABLED` | | If the variable is present, the job isn't created. | | `pmd-apex-sast` | `SAST_DISABLED` | | If the variable is present, the job isn't created. | | `retire-js-dependency_scanning` | `DEPENDENCY_SCANNING_DISABLED` | | If the variable is present, the job isn't created. | diff --git a/doc/user/project/integrations/webhooks.md b/doc/user/project/integrations/webhooks.md index d74a2bec1f6..e049c19267b 100644 --- a/doc/user/project/integrations/webhooks.md +++ b/doc/user/project/integrations/webhooks.md @@ -1161,6 +1161,7 @@ X-Gitlab-Event: Pipeline Hook "id": 380987, "description": "shared-runners-manager-6.gitlab.com", "active": true, + "runner_type": "instance_type", "is_shared": true, "tags": [ "linux", @@ -1196,7 +1197,8 @@ X-Gitlab-Event: Pipeline Hook "id":380987, "description":"shared-runners-manager-6.gitlab.com", "active":true, - "is_shared":true, + "runner_type": "instance_type", + "is_shared": true, "tags": [ "linux", "docker" @@ -1230,6 +1232,7 @@ X-Gitlab-Event: Pipeline Hook "id": 380987, "description": "shared-runners-manager-6.gitlab.com", "active": true, + "runner_type": "instance_type", "is_shared": true, "tags": [ "linux", @@ -1333,6 +1336,7 @@ X-Gitlab-Event: Job Hook }, "runner": { "active": true, + "runner_type": "project_type", "is_shared": false, "id": 380987, "description": "shared-runners-manager-6.gitlab.com", diff --git a/doc/user/project/merge_requests/browser_performance_testing.md b/doc/user/project/merge_requests/browser_performance_testing.md index bafe6795d52..58c737f375f 100644 --- a/doc/user/project/merge_requests/browser_performance_testing.md +++ b/doc/user/project/merge_requests/browser_performance_testing.md @@ -40,11 +40,11 @@ Consider the following workflow: ## How browser performance testing works First, define a job in your `.gitlab-ci.yml` file that generates the -[Browser Performance report artifact](../../../ci/yaml/README.md#artifactsreportsperformance). +[Browser Performance report artifact](../../../ci/yaml/README.md#artifactsreportsbrowser_performance). GitLab then checks this report, compares key performance metrics for each page between the source and target branches, and shows the information in the merge request. -For an example Performance job, see +For an example Browser Performance job, see [Configuring Browser Performance Testing](#configuring-browser-performance-testing). NOTE: @@ -70,18 +70,17 @@ using Docker-in-Docker. include: template: Verify/Browser-Performance.gitlab-ci.yml - performance: + browser_performance: variables: URL: https://example.com ``` WARNING: -In GitLab 14.0 and later, the job [is scheduled to be renamed](https://gitlab.com/gitlab-org/gitlab/-/issues/225914) -from `performance` to `browser_performance`. +In GitLab 13.12 and earlier, the job [was named](https://gitlab.com/gitlab-org/gitlab/-/issues/225914) `performance`. The above example: -- Creates a `performance` job in your CI/CD pipeline and runs sitespeed.io against the webpage you +- Creates a `browser_performance` job in your CI/CD pipeline and runs sitespeed.io against the webpage you defined in `URL` to gather key metrics. - Uses a template that doesn't work with Kubernetes clusters. If you are using a Kubernetes cluster, use [`template: Jobs/Browser-Performance-Testing.gitlab-ci.yml`](https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Jobs/Browser-Performance-Testing.gitlab-ci.yml) @@ -90,7 +89,7 @@ The above example: GitLab 12.3 or earlier, you must [add the configuration manually](#gitlab-versions-132-and-earlier). The template uses the [GitLab plugin for sitespeed.io](https://gitlab.com/gitlab-org/gl-performance), -and it saves the full HTML sitespeed.io report as a [Browser Performance report artifact](../../../ci/yaml/README.md#artifactsreportsperformance) +and it saves the full HTML sitespeed.io report as a [Browser Performance report artifact](../../../ci/yaml/README.md#artifactsreportsbrowser_performance) that you can later download and analyze. This implementation always takes the latest Browser Performance artifact available. If [GitLab Pages](../pages/index.md) is enabled, you can view the report directly in your browser. @@ -108,7 +107,7 @@ makes on the given URL, and change the version: include: template: Verify/Browser-Performance.gitlab-ci.yml -performance: +browser_performance: variables: URL: https://www.sitespeed.io/ SITESPEED_VERSION: 13.2.0 @@ -127,7 +126,7 @@ if the `Total Score` metric degrades by 5 points or more: include: template: Verify/Browser-Performance.gitlab-ci.yml -performance: +browser_performance: variables: URL: https://example.com DEGRADATION_THRESHOLD: 5 @@ -140,13 +139,13 @@ The `Total Score` metric is based on sitespeed.io's [coach performance score](ht The above CI YAML configuration is great for testing against static environments, and it can be extended for dynamic environments, but a few extra steps are required: -1. The `performance` job should run after the dynamic environment has started. +1. The `browser_performance` job should run after the dynamic environment has started. 1. In the `review` job: 1. Generate a URL list file with the dynamic URL. 1. Save the file as an artifact, for example with `echo $CI_ENVIRONMENT_URL > environment_url.txt` in your job's `script`. 1. Pass the list as the URL environment variable (which can be a URL or a file containing URLs) - to the `performance` job. + to the `browser_performance` job. 1. You can now run the sitespeed.io container against the desired hostname and paths. @@ -176,7 +175,7 @@ review: except: - master -performance: +browser_performance: dependencies: - review variables: diff --git a/lib/api/entities/runner.rb b/lib/api/entities/runner.rb index 6165b54cddb..e78f14cf920 100644 --- a/lib/api/entities/runner.rb +++ b/lib/api/entities/runner.rb @@ -8,6 +8,7 @@ module API expose :ip_address expose :active expose :instance_type?, as: :is_shared + expose :runner_type expose :name expose :online?, as: :online expose :status diff --git a/lib/api/members.rb b/lib/api/members.rb index 49241c8d6b2..0575eea1eab 100644 --- a/lib/api/members.rb +++ b/lib/api/members.rb @@ -117,7 +117,7 @@ module API not_allowed! # This currently can only be reached in EE elsif member.valid? && member.persisted? present_members(member) - Gitlab::Tracking.event(::Members::CreateService.name, 'create_member', label: params[:invite_source], property: 'existing_user') + Gitlab::Tracking.event(::Members::CreateService.name, 'create_member', label: params[:invite_source], property: 'existing_user', user: current_user) else render_validation_error!(member) end diff --git a/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml b/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml index a13f2046291..5af270c4cff 100644 --- a/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml @@ -11,7 +11,7 @@ # * test: TEST_DISABLED # * code_quality: CODE_QUALITY_DISABLED # * license_management: LICENSE_MANAGEMENT_DISABLED -# * performance: PERFORMANCE_DISABLED +# * browser_performance: PERFORMANCE_DISABLED # * load_performance: LOAD_PERFORMANCE_DISABLED # * sast: SAST_DISABLED # * secret_detection: SECRET_DETECTION_DISABLED diff --git a/lib/gitlab/ci/templates/Jobs/Browser-Performance-Testing.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Browser-Performance-Testing.gitlab-ci.yml index 01907ef9e2e..56899614cc6 100644 --- a/lib/gitlab/ci/templates/Jobs/Browser-Performance-Testing.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Jobs/Browser-Performance-Testing.gitlab-ci.yml @@ -1,6 +1,6 @@ # Read more about the feature here: https://docs.gitlab.com/ee/user/project/merge_requests/browser_performance_testing.html -performance: +browser_performance: stage: performance image: docker:19.03.12 allow_failure: true @@ -72,6 +72,6 @@ performance: rules: - if: '$CI_KUBERNETES_ACTIVE == null || $CI_KUBERNETES_ACTIVE == ""' when: never - - if: '$PERFORMANCE_DISABLED' + - if: '$BROWSER_PERFORMANCE_DISABLED' when: never - if: '$CI_COMMIT_TAG || $CI_COMMIT_BRANCH' diff --git a/lib/gitlab/ci/templates/Jobs/Browser-Performance-Testing.latest.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Browser-Performance-Testing.latest.gitlab-ci.yml index 5216a46745c..56899614cc6 100644 --- a/lib/gitlab/ci/templates/Jobs/Browser-Performance-Testing.latest.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Jobs/Browser-Performance-Testing.latest.gitlab-ci.yml @@ -72,6 +72,6 @@ browser_performance: rules: - if: '$CI_KUBERNETES_ACTIVE == null || $CI_KUBERNETES_ACTIVE == ""' when: never - - if: '$PERFORMANCE_DISABLED' + - if: '$BROWSER_PERFORMANCE_DISABLED' when: never - if: '$CI_COMMIT_TAG || $CI_COMMIT_BRANCH' diff --git a/lib/gitlab/ci/templates/Jobs/Code-Quality.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Code-Quality.gitlab-ci.yml index b29342216fc..48e877684f6 100644 --- a/lib/gitlab/ci/templates/Jobs/Code-Quality.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Jobs/Code-Quality.gitlab-ci.yml @@ -7,7 +7,7 @@ code_quality: variables: DOCKER_DRIVER: overlay2 DOCKER_TLS_CERTDIR: "" - CODE_QUALITY_IMAGE: "registry.gitlab.com/gitlab-org/ci-cd/codequality:0.85.23" + CODE_QUALITY_IMAGE: "registry.gitlab.com/gitlab-org/ci-cd/codequality:0.85.24" needs: [] script: - export SOURCE_CODE=$PWD diff --git a/lib/gitlab/ci/templates/Ruby.gitlab-ci.yml b/lib/gitlab/ci/templates/Ruby.gitlab-ci.yml index 275364afae4..1bdaaeede43 100644 --- a/lib/gitlab/ci/templates/Ruby.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Ruby.gitlab-ci.yml @@ -1,6 +1,6 @@ # Official language image. Look for the different tagged releases at: # https://hub.docker.com/r/library/ruby/tags/ -image: "ruby:2.5" +image: ruby:latest # Pick zero or more services to be used on all builds. # Only needed when using a docker container to run your tests in. diff --git a/lib/gitlab/ci/templates/Verify/Browser-Performance.gitlab-ci.yml b/lib/gitlab/ci/templates/Verify/Browser-Performance.gitlab-ci.yml index 404d4a4c6db..f0621165f8a 100644 --- a/lib/gitlab/ci/templates/Verify/Browser-Performance.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Verify/Browser-Performance.gitlab-ci.yml @@ -6,7 +6,7 @@ stages: - deploy - performance -performance: +browser_performance: stage: performance image: docker:git variables: diff --git a/lib/gitlab/ci/yaml_processor/result.rb b/lib/gitlab/ci/yaml_processor/result.rb index 460fad16d5c..15cc0c28296 100644 --- a/lib/gitlab/ci/yaml_processor/result.rb +++ b/lib/gitlab/ci/yaml_processor/result.rb @@ -112,7 +112,9 @@ module Gitlab end def yaml_variables_for(job_name) - job = jobs.fetch(job_name) + job = jobs[job_name] + + return [] unless job Gitlab::Ci::Variables::Helpers.inherit_yaml_variables( from: root_variables, @@ -122,9 +124,7 @@ module Gitlab end def stage_for(job_name) - job = jobs.fetch(job_name) - - job[:stage] + jobs.dig(job_name, :stage) end private diff --git a/lib/gitlab/data_builder/build.rb b/lib/gitlab/data_builder/build.rb index 4c31f986be5..91e6fc11a53 100644 --- a/lib/gitlab/data_builder/build.rb +++ b/lib/gitlab/data_builder/build.rb @@ -83,7 +83,9 @@ module Gitlab { id: runner.id, description: runner.description, + runner_type: runner.runner_type, active: runner.active?, + is_shared: runner.instance_type?, tags: runner.tags&.map(&:name) } end diff --git a/lib/gitlab/data_builder/pipeline.rb b/lib/gitlab/data_builder/pipeline.rb index 766eaf54afe..4d70e3949dd 100644 --- a/lib/gitlab/data_builder/pipeline.rb +++ b/lib/gitlab/data_builder/pipeline.rb @@ -79,7 +79,9 @@ module Gitlab { id: runner.id, description: runner.description, + runner_type: runner.runner_type, active: runner.active?, + is_shared: runner.instance_type?, tags: runner.tags&.map(&:name) } end diff --git a/lib/gitlab/markdown_cache/field_data.rb b/lib/gitlab/markdown_cache/field_data.rb index 14622c0f186..75364570640 100644 --- a/lib/gitlab/markdown_cache/field_data.rb +++ b/lib/gitlab/markdown_cache/field_data.rb @@ -9,7 +9,7 @@ module Gitlab @data = {} end - delegate :[], :[]=, to: :@data + delegate :[], :[]=, :key?, to: :@data def markdown_fields @data.keys diff --git a/lib/tasks/gitlab/db.rake b/lib/tasks/gitlab/db.rake index 399405953a7..6145e0f3540 100644 --- a/lib/tasks/gitlab/db.rake +++ b/lib/tasks/gitlab/db.rake @@ -240,9 +240,7 @@ namespace :gitlab do desc 'Run migrations with instrumentation' task migration_testing: :environment do result_dir = Gitlab::Database::Migrations::Instrumentation::RESULT_DIR - raise "Directory exists already, won't overwrite: #{result_dir}" if File.exist?(result_dir) - - Dir.mkdir(result_dir) + FileUtils.mkdir_p(result_dir) verbose_was = ActiveRecord::Migration.verbose ActiveRecord::Migration.verbose = true diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 717a74b3a89..3f2945ecdf0 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -8319,42 +8319,9 @@ msgstr "" msgid "ComplianceFramework|Edit Compliance Framework" msgstr "" -msgid "ComplianceFramework|GDPR" -msgstr "" - -msgid "ComplianceFramework|GDPR - General Data Protection Regulation" -msgstr "" - -msgid "ComplianceFramework|HIPAA" -msgstr "" - -msgid "ComplianceFramework|HIPAA - Health Insurance Portability and Accountability Act" -msgstr "" - msgid "ComplianceFramework|New Compliance Framework" msgstr "" -msgid "ComplianceFramework|PCI-DSS" -msgstr "" - -msgid "ComplianceFramework|PCI-DSS - Payment Card Industry-Data Security Standard" -msgstr "" - -msgid "ComplianceFramework|SOC 2" -msgstr "" - -msgid "ComplianceFramework|SOC 2 - Service Organization Control 2" -msgstr "" - -msgid "ComplianceFramework|SOX" -msgstr "" - -msgid "ComplianceFramework|SOX - Sarbanes-Oxley" -msgstr "" - -msgid "ComplianceFramework|This project is regulated by %{framework}." -msgstr "" - msgid "Component" msgstr "" @@ -39282,9 +39249,6 @@ msgstr "" msgid "must be greater than start date" msgstr "" -msgid "must contain only valid frameworks" -msgstr "" - msgid "my-awesome-group" msgstr "" diff --git a/qa/qa/specs/features/browser_ui/8_monitor/cluster_with_prometheus.rb b/qa/qa/specs/features/browser_ui/8_monitor/cluster_with_prometheus.rb index ed0064e8b6f..19e49400d5e 100644 --- a/qa/qa/specs/features/browser_ui/8_monitor/cluster_with_prometheus.rb +++ b/qa/qa/specs/features/browser_ui/8_monitor/cluster_with_prometheus.rb @@ -17,7 +17,7 @@ module QA %w[ CODE_QUALITY_DISABLED TEST_DISABLED LICENSE_MANAGEMENT_DISABLED SAST_DISABLED DAST_DISABLED DEPENDENCY_SCANNING_DISABLED - CONTAINER_SCANNING_DISABLED PERFORMANCE_DISABLED SECRET_DETECTION_DISABLED + CONTAINER_SCANNING_DISABLED BROWSER_PERFORMANCE_DISABLED SECRET_DETECTION_DISABLED ].each do |key| Resource::CiVariable.fabricate_via_api! do |resource| resource.project = @project diff --git a/scripts/frontend/startup_css/startup_css_changed.sh b/scripts/frontend/startup_css/startup_css_changed.sh index 2ae647557e4..f214e61cdfb 100755 --- a/scripts/frontend/startup_css/startup_css_changed.sh +++ b/scripts/frontend/startup_css/startup_css_changed.sh @@ -23,7 +23,7 @@ regenerating the Startup CSS files. **What should I do now?** -IMPORTANT: Please make sure to update your MR title with `[RUN AS-IF-FOSS]` and start a new MR pipeline +IMPORTANT: Please make sure to update your MR title with "[RUN AS-IF-FOSS]" and start a new MR pipeline To fix this job, consider one of the following options: diff --git a/spec/controllers/confirmations_controller_spec.rb b/spec/controllers/confirmations_controller_spec.rb index 49a39f257fe..c9a0ae981fc 100644 --- a/spec/controllers/confirmations_controller_spec.rb +++ b/spec/controllers/confirmations_controller_spec.rb @@ -12,7 +12,9 @@ RSpec.describe ConfirmationsController do describe '#show' do render_views - subject { get :show, params: { confirmation_token: confirmation_token } } + def perform_request + get :show, params: { confirmation_token: confirmation_token } + end context 'user is already confirmed' do let_it_be_with_reload(:user) { create(:user, :unconfirmed) } @@ -20,20 +22,37 @@ RSpec.describe ConfirmationsController do before do user.confirm - subject end it 'renders `new`' do + perform_request + expect(response).to render_template(:new) end it 'displays an error message' do + perform_request + expect(response.body).to include('Email was already confirmed, please try signing in') end it 'does not display the email of the user' do + perform_request + expect(response.body).not_to include(user.email) end + + it 'sets the username and caller_id in the context' do + expect(controller).to receive(:show).and_wrap_original do |m, *args| + m.call(*args) + + expect(Gitlab::ApplicationContext.current) + .to include('meta.user' => user.username, + 'meta.caller_id' => 'ConfirmationsController#show') + end + + perform_request + end end context 'user accesses the link after the expiry of confirmation token has passed' do @@ -42,39 +61,64 @@ RSpec.describe ConfirmationsController do before do allow(Devise).to receive(:confirm_within).and_return(1.day) - - travel_to(3.days.from_now) do - subject - end end it 'renders `new`' do + travel_to(3.days.from_now) { perform_request } + expect(response).to render_template(:new) end it 'displays an error message' do + travel_to(3.days.from_now) { perform_request } + expect(response.body).to include('Email needs to be confirmed within 1 day, please request a new one below') end it 'does not display the email of the user' do + travel_to(3.days.from_now) { perform_request } + expect(response.body).not_to include(user.email) end + + it 'sets the username and caller_id in the context' do + expect(controller).to receive(:show).and_wrap_original do |m, *args| + m.call(*args) + + expect(Gitlab::ApplicationContext.current) + .to include('meta.user' => user.username, + 'meta.caller_id' => 'ConfirmationsController#show') + end + + travel_to(3.days.from_now) { perform_request } + end end context 'with an invalid confirmation token' do let(:confirmation_token) { 'invalid_confirmation_token' } - before do - subject - end - it 'renders `new`' do + perform_request + expect(response).to render_template(:new) end it 'displays an error message' do + perform_request + expect(response.body).to include('Confirmation token is invalid') end + + it 'sets the the caller_id in the context' do + expect(controller).to receive(:show).and_wrap_original do |m, *args| + expect(Gitlab::ApplicationContext.current) + .to include('meta.caller_id' => 'ConfirmationsController#show') + + m.call(*args) + end + + perform_request + end end end end diff --git a/spec/controllers/omniauth_callbacks_controller_spec.rb b/spec/controllers/omniauth_callbacks_controller_spec.rb index 4a47a4a2a53..9a142559fca 100644 --- a/spec/controllers/omniauth_callbacks_controller_spec.rb +++ b/spec/controllers/omniauth_callbacks_controller_spec.rb @@ -293,6 +293,18 @@ RSpec.describe OmniauthCallbacksController, type: :controller do expect(request.env['warden']).to be_authenticated end + + it 'sets the username and caller_id in the context' do + expect(controller).to receive(:atlassian_oauth2).and_wrap_original do |m, *args| + m.call(*args) + + expect(Gitlab::ApplicationContext.current) + .to include('meta.user' => user.username, + 'meta.caller_id' => 'OmniauthCallbacksController#atlassian_oauth2') + end + + post :atlassian_oauth2 + end end context 'for a new user' do @@ -454,6 +466,18 @@ RSpec.describe OmniauthCallbacksController, type: :controller do it 'doesn\'t link a new identity to the user' do expect { post :saml, params: { SAMLResponse: mock_saml_response } }.not_to change { user.identities.count } end + + it 'sets the username and caller_id in the context' do + expect(controller).to receive(:saml).and_wrap_original do |m, *args| + m.call(*args) + + expect(Gitlab::ApplicationContext.current) + .to include('meta.user' => user.username, + 'meta.caller_id' => 'OmniauthCallbacksController#saml') + end + + post :saml, params: { SAMLResponse: mock_saml_response } + end end end diff --git a/spec/controllers/passwords_controller_spec.rb b/spec/controllers/passwords_controller_spec.rb index e9883107456..08d68d7cec8 100644 --- a/spec/controllers/passwords_controller_spec.rb +++ b/spec/controllers/passwords_controller_spec.rb @@ -77,6 +77,18 @@ RSpec.describe PasswordsController do expect(user.password_expires_at).not_to be_nil end end + + it 'sets the username and caller_id in the context' do + expect(controller).to receive(:update).and_wrap_original do |m, *args| + m.call(*args) + + expect(Gitlab::ApplicationContext.current) + .to include('meta.user' => user.username, + 'meta.caller_id' => 'PasswordsController#update') + end + + subject + end end end end diff --git a/spec/controllers/registrations_controller_spec.rb b/spec/controllers/registrations_controller_spec.rb index ff73c0aafe8..81486c310d4 100644 --- a/spec/controllers/registrations_controller_spec.rb +++ b/spec/controllers/registrations_controller_spec.rb @@ -434,6 +434,18 @@ RSpec.describe RegistrationsController do expect(User.last.last_name).to eq(base_user_params[:last_name]) expect(User.last.name).to eq("#{base_user_params[:first_name]} #{base_user_params[:last_name]}") end + + it 'sets the username and caller_id in the context' do + expect(controller).to receive(:create).and_wrap_original do |m, *args| + m.call(*args) + + expect(Gitlab::ApplicationContext.current) + .to include('meta.user' => base_user_params[:username], + 'meta.caller_id' => 'RegistrationsController#create') + end + + subject + end end describe '#destroy' do @@ -525,5 +537,17 @@ RSpec.describe RegistrationsController do end end end + + it 'sets the username and caller_id in the context' do + expect(controller).to receive(:destroy).and_wrap_original do |m, *args| + m.call(*args) + + expect(Gitlab::ApplicationContext.current) + .to include('meta.user' => user.username, + 'meta.caller_id' => 'RegistrationsController#destroy') + end + + post :destroy + end end end diff --git a/spec/features/groups/members/manage_members_spec.rb b/spec/features/groups/members/manage_members_spec.rb index c5e6479ec51..eef92b5839d 100644 --- a/spec/features/groups/members/manage_members_spec.rb +++ b/spec/features/groups/members/manage_members_spec.rb @@ -3,20 +3,19 @@ require 'spec_helper' RSpec.describe 'Groups > Members > Manage members' do - include Select2Helper include Spec::Support::Helpers::Features::MembersHelpers include Spec::Support::Helpers::Features::InviteMembersModalHelper - let(:user1) { create(:user, name: 'John Doe') } - let(:user2) { create(:user, name: 'Mary Jane') } - let(:group) { create(:group) } + let_it_be(:user1) { create(:user, name: 'John Doe') } + let_it_be(:user2) { create(:user, name: 'Mary Jane') } + let_it_be(:group) { create(:group) } before do sign_in(user1) end shared_examples 'includes the correct Invite link' do |should_include, should_not_include| - it 'includes either the form or the modal trigger' do + it 'includes either the form or the modal trigger', :aggregate_failures do group.add_owner(user1) visit group_group_members_path(group) @@ -27,12 +26,12 @@ RSpec.describe 'Groups > Members > Manage members' do end shared_examples 'does not include either invite modal or either invite form' do - it 'does not include either of the invite members or invite group modal buttons' do + it 'does not include either of the invite members or invite group modal buttons', :aggregate_failures do expect(page).not_to have_selector '.js-invite-members-modal' expect(page).not_to have_selector '.js-invite-group-modal' end - it 'does not include either of the invite users or invite group forms' do + it 'does not include either of the invite users or invite group forms', :aggregate_failures do expect(page).not_to have_selector '.invite-users-form' expect(page).not_to have_selector '.invite-group-form' end @@ -66,7 +65,7 @@ RSpec.describe 'Groups > Members > Manage members' do end end - it 'add user to group', :js do + it 'add user to group', :js, :snowplow, :aggregate_failures do group.add_owner(user1) visit group_group_members_path(group) @@ -77,6 +76,13 @@ RSpec.describe 'Groups > Members > Manage members' do expect(page).to have_content(user2.name) expect(page).to have_button('Reporter') end + + expect_snowplow_event( + category: 'Members::CreateService', + action: 'create_member', + label: 'unknown', + property: 'existing_user' + ) end it 'do not disclose email addresses', :js do @@ -143,11 +149,13 @@ RSpec.describe 'Groups > Members > Manage members' do wait_for_requests - expect(page).not_to have_content(user2.name) - expect(group.users).not_to include(user2) + aggregate_failures do + expect(page).not_to have_content(user2.name) + expect(group.users).not_to include(user2) + end end - it 'add yourself to group when already an owner', :js do + it 'add yourself to group when already an owner', :js, :aggregate_failures do group.add_owner(user1) visit group_group_members_path(group) @@ -160,7 +168,7 @@ RSpec.describe 'Groups > Members > Manage members' do end end - it 'invite user to group', :js do + it 'invite user to group', :js, :snowplow do group.add_owner(user1) visit group_group_members_path(group) @@ -170,14 +178,23 @@ RSpec.describe 'Groups > Members > Manage members' do expect(page).to have_link 'Invited' click_link 'Invited' - page.within(members_table) do - expect(page).to have_content('test@example.com') - expect(page).to have_content('Invited') - expect(page).to have_button('Reporter') + aggregate_failures do + page.within(members_table) do + expect(page).to have_content('test@example.com') + expect(page).to have_content('Invited') + expect(page).to have_button('Reporter') + end + + expect_snowplow_event( + category: 'Members::InviteService', + action: 'create_member', + label: 'unknown', + property: 'net_new_user' + ) end end - context 'as a guest', :js do + context 'when user is a guest' do before do group.add_guest(user1) group.add_developer(user2) @@ -187,7 +204,7 @@ RSpec.describe 'Groups > Members > Manage members' do it_behaves_like 'does not include either invite modal or either invite form' - it 'does not include a button on the members page list to manage or remove the existing member', :js do + it 'does not include a button on the members page list to manage or remove the existing member', :js, :aggregate_failures do page.within(second_row) do # Can not modify user2 role expect(page).not_to have_button 'Developer' @@ -198,7 +215,7 @@ RSpec.describe 'Groups > Members > Manage members' do end end - context 'As a guest when the :invite_members_group_modal feature flag is disabled', :js do + context 'when user is a guest and the :invite_members_group_modal feature flag is disabled' do before do stub_feature_flags(invite_members_group_modal: false) group.add_guest(user1) @@ -209,7 +226,7 @@ RSpec.describe 'Groups > Members > Manage members' do it_behaves_like 'does not include either invite modal or either invite form' - it 'does not include a button on the members page list to manage or remove the existing member', :js do + it 'does not include a button on the members page list to manage or remove the existing member', :js, :aggregate_failures do page.within(second_row) do # Can not modify user2 role expect(page).not_to have_button 'Developer' diff --git a/spec/features/projects/members/list_spec.rb b/spec/features/projects/members/list_spec.rb index 42d7f658455..c3cc6b02439 100644 --- a/spec/features/projects/members/list_spec.rb +++ b/spec/features/projects/members/list_spec.rb @@ -6,17 +6,17 @@ RSpec.describe 'Project members list', :js do include Spec::Support::Helpers::Features::MembersHelpers include Spec::Support::Helpers::Features::InviteMembersModalHelper - let(:user1) { create(:user, name: 'John Doe') } - let(:user2) { create(:user, name: 'Mary Jane') } - let(:group) { create(:group) } - let(:project) { create(:project, :internal, namespace: group) } + let_it_be(:user1) { create(:user, name: 'John Doe') } + let_it_be(:user2) { create(:user, name: 'Mary Jane') } + let_it_be(:group) { create(:group) } + let_it_be(:project) { create(:project, :internal, namespace: group) } before do sign_in(user1) group.add_owner(user1) end - it 'show members from project and group' do + it 'show members from project and group', :aggregate_failures do project.add_developer(user2) visit_members_page @@ -25,7 +25,7 @@ RSpec.describe 'Project members list', :js do expect(second_row).to have_content(user2.name) end - it 'show user once if member of both group and project' do + it 'show user once if member of both group and project', :aggregate_failures do project.add_developer(user1) visit_members_page @@ -47,7 +47,7 @@ RSpec.describe 'Project members list', :js do end end - it 'add user to project' do + it 'add user to project', :snowplow, :aggregate_failures do visit_members_page invite_member(user2.name, role: 'Reporter') @@ -55,9 +55,16 @@ RSpec.describe 'Project members list', :js do page.within find_member_row(user2) do expect(page).to have_button('Reporter') end + + expect_snowplow_event( + category: 'Members::CreateService', + action: 'create_member', + label: 'unknown', + property: 'existing_user' + ) end - it 'uses ProjectMember access_level_roles for the invite members modal access option' do + it 'uses ProjectMember access_level_roles for the invite members modal access option', :aggregate_failures do visit_members_page click_on 'Invite members' @@ -95,7 +102,7 @@ RSpec.describe 'Project members list', :js do expect(members_table).not_to have_content(other_user.name) end - it 'invite user to project' do + it 'invite user to project', :snowplow, :aggregate_failures do visit_members_page invite_member('test@example.com', role: 'Reporter') @@ -105,6 +112,13 @@ RSpec.describe 'Project members list', :js do page.within find_invited_member_row('test@example.com') do expect(page).to have_button('Reporter') end + + expect_snowplow_event( + category: 'Members::InviteService', + action: 'create_member', + label: 'unknown', + property: 'net_new_user' + ) end context 'as a signed out visitor viewing a public project' do @@ -128,7 +142,7 @@ RSpec.describe 'Project members list', :js do project.add_maintainer(project_bot) end - it 'does not show form used to change roles and "Expiration date" or the remove user button' do + it 'does not show form used to change roles and "Expiration date" or the remove user button', :aggregate_failures do visit_members_page page.within find_member_row(project_bot) do diff --git a/spec/features/projects/user_views_empty_project_spec.rb b/spec/features/projects/user_views_empty_project_spec.rb index 3d4d9a7ea96..cce38456df9 100644 --- a/spec/features/projects/user_views_empty_project_spec.rb +++ b/spec/features/projects/user_views_empty_project_spec.rb @@ -3,8 +3,8 @@ require 'spec_helper' RSpec.describe 'User views an empty project' do - let(:project) { create(:project, :empty_repo) } - let(:user) { create(:user) } + let_it_be(:project) { create(:project, :empty_repo) } + let_it_be(:user) { create(:user) } shared_examples 'allowing push to default branch' do it 'shows push-to-master instructions' do @@ -14,17 +14,25 @@ RSpec.describe 'User views an empty project' do end end - describe 'as a maintainer' do + context 'when user is a maintainer' do before do project.add_maintainer(user) sign_in(user) end it_behaves_like 'allowing push to default branch' + + it 'shows a link for inviting members and launches invite modal', :js do + visit project_path(project) + + click_button 'Invite members' + + expect(page).to have_content("You're inviting members to the") + end end - describe 'as an admin' do - let(:user) { create(:user, :admin) } + context 'when user is an admin' do + let_it_be(:user) { create(:user, :admin) } context 'when admin mode is enabled' do before do @@ -44,16 +52,17 @@ RSpec.describe 'User views an empty project' do end end - describe 'as a developer' do + context 'when user is a developer' do before do project.add_developer(user) sign_in(user) end - it 'does not show push-to-master instructions' do + it 'does not show push-to-master instructions nor invite members link', :aggregate_failures, :js do visit project_path(project) expect(page).not_to have_content('git push -u origin master') + expect(page).not_to have_button(text: 'Invite members') end end end diff --git a/spec/frontend/fixtures/runner.rb b/spec/frontend/fixtures/runner.rb new file mode 100644 index 00000000000..49d657c69c6 --- /dev/null +++ b/spec/frontend/fixtures/runner.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Runner (JavaScript fixtures)' do + include AdminModeHelper + include ApiHelpers + include JavaScriptFixturesHelpers + include GraphqlHelpers + + let_it_be(:admin) { create(:admin) } + let_it_be(:group) { create(:group) } + let_it_be(:project) { create(:project, :repository, :public) } + + let_it_be(:instance_runner) { create(:ci_runner, :instance, version: '1.0.0', revision: '123', description: 'Instance runner', ip_address: '127.0.0.1') } + let_it_be(:group_runner) { create(:ci_runner, :group, groups: [group], active: false, version: '2.0.0', revision: '456', description: 'Group runner', ip_address: '127.0.0.1') } + let_it_be(:project_runner) { create(:ci_runner, :project, projects: [project], active: false, version: '2.0.0', revision: '456', description: 'Project runner', ip_address: '127.0.0.1') } + + query_path = 'runner/graphql/' + fixtures_path = 'graphql/runner/' + + before(:all) do + clean_frontend_fixtures(fixtures_path) + end + + after(:all) do + remove_repository(project) + end + + before do + sign_in(admin) + enable_admin_mode!(admin) + end + + describe GraphQL::Query, type: :request do + get_runners_query_name = 'get_runners.query.graphql' + + let_it_be(:query) do + get_graphql_query_as_string("#{query_path}#{get_runners_query_name}", [ + 'runner/graphql/runner_node.fragment.graphql', + 'graphql_shared/fragments/pageInfo.fragment.graphql' + ]) + end + + it "#{fixtures_path}#{get_runners_query_name}.json" do + post_graphql(query, current_user: admin, variables: {}) + + expect_graphql_errors_to_be_empty + end + + it "#{fixtures_path}#{get_runners_query_name}.paginated.json" do + post_graphql(query, current_user: admin, variables: { first: 2 }) + + expect_graphql_errors_to_be_empty + end + end + + describe GraphQL::Query, type: :request do + get_runner_query_name = 'get_runner.query.graphql' + + let_it_be(:query) { get_graphql_query_as_string("#{query_path}#{get_runner_query_name}") } + + it "#{fixtures_path}#{get_runner_query_name}.json" do + post_graphql(query, current_user: admin, variables: { + id: instance_runner.to_global_id.to_s + }) + + expect_graphql_errors_to_be_empty + end + end +end diff --git a/spec/frontend/invite_members/components/invite_members_modal_spec.js b/spec/frontend/invite_members/components/invite_members_modal_spec.js index 7ed18775693..eabbea84234 100644 --- a/spec/frontend/invite_members/components/invite_members_modal_spec.js +++ b/spec/frontend/invite_members/components/invite_members_modal_spec.js @@ -15,6 +15,7 @@ const isProject = false; const inviteeType = 'members'; const accessLevels = { Guest: 10, Reporter: 20, Developer: 30, Maintainer: 40, Owner: 50 }; const defaultAccessLevel = 10; +const inviteSource = 'unknown'; const helpLink = 'https://example.com'; const user1 = { id: 1, name: 'Name One', username: 'one_1', avatar_url: '' }; @@ -173,6 +174,7 @@ describe('InviteMembersModal', () => { user_id: '1', access_level: defaultAccessLevel, expires_at: undefined, + invite_source: inviteSource, format: 'json', }; @@ -245,6 +247,7 @@ describe('InviteMembersModal', () => { access_level: defaultAccessLevel, expires_at: undefined, email: 'email@example.com', + invite_source: inviteSource, format: 'json', }; @@ -293,6 +296,7 @@ describe('InviteMembersModal', () => { const postData = { access_level: defaultAccessLevel, expires_at: undefined, + invite_source: inviteSource, format: 'json', }; @@ -308,20 +312,39 @@ describe('InviteMembersModal', () => { jest.spyOn(Api, 'addGroupMembersByUserId').mockResolvedValue({ data: postData }); jest.spyOn(wrapper.vm, 'showToastMessageSuccess'); jest.spyOn(wrapper.vm, 'trackInvite'); - - clickInviteButton(); }); - it('calls Api inviteGroupMembersByEmail with the correct params', () => { - expect(Api.inviteGroupMembersByEmail).toHaveBeenCalledWith(id, emailPostData); - }); + describe('when triggered from regular mounting', () => { + beforeEach(() => { + clickInviteButton(); + }); - it('calls Api addGroupMembersByUserId with the correct params', () => { - expect(Api.addGroupMembersByUserId).toHaveBeenCalledWith(id, idPostData); + it('calls Api inviteGroupMembersByEmail with the correct params', () => { + expect(Api.inviteGroupMembersByEmail).toHaveBeenCalledWith(id, emailPostData); + }); + + it('calls Api addGroupMembersByUserId with the correct params', () => { + expect(Api.addGroupMembersByUserId).toHaveBeenCalledWith(id, idPostData); + }); + + it('displays the successful toastMessage', () => { + expect(wrapper.vm.showToastMessageSuccess).toHaveBeenCalled(); + }); }); - it('displays the successful toastMessage', () => { - expect(wrapper.vm.showToastMessageSuccess).toHaveBeenCalled(); + it('calls Apis with the invite source passed through to openModal', () => { + wrapper.vm.openModal({ inviteeType: 'members', source: '_invite_source_' }); + + clickInviteButton(); + + expect(Api.inviteGroupMembersByEmail).toHaveBeenCalledWith(id, { + ...emailPostData, + invite_source: '_invite_source_', + }); + expect(Api.addGroupMembersByUserId).toHaveBeenCalledWith(id, { + ...idPostData, + invite_source: '_invite_source_', + }); }); }); @@ -403,18 +426,11 @@ describe('InviteMembersModal', () => { }); describe('tracking', () => { - const postData = { - user_id: '1', - access_level: defaultAccessLevel, - expires_at: undefined, - format: 'json', - }; - beforeEach(() => { wrapper = createComponent({ newUsersToInvite: [user3] }); wrapper.vm.$toast = { show: jest.fn() }; - jest.spyOn(Api, 'inviteGroupMembersByEmail').mockResolvedValue({ data: postData }); + jest.spyOn(Api, 'inviteGroupMembersByEmail').mockResolvedValue({}); }); it('tracks the invite', () => { diff --git a/spec/frontend/repository/components/table/row_spec.js b/spec/frontend/repository/components/table/row_spec.js index 6ba6f993db1..254e2fc07b4 100644 --- a/spec/frontend/repository/components/table/row_spec.js +++ b/spec/frontend/repository/components/table/row_spec.js @@ -1,5 +1,6 @@ import { GlBadge, GlLink, GlIcon } from '@gitlab/ui'; import { shallowMount, RouterLinkStub } from '@vue/test-utils'; +import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; import TableRow from '~/repository/components/table/row.vue'; import FileIcon from '~/vue_shared/components/file_icon.vue'; import { FILE_SYMLINK_MODE } from '~/vue_shared/constants'; @@ -19,6 +20,9 @@ function factory(propsData = {}) { projectPath: 'gitlab-org/gitlab-ce', url: `https://test.com`, }, + directives: { + GlHoverLoad: createMockDirective(), + }, provide: { glFeatures: { refactorBlobViewer: true }, }, @@ -34,6 +38,8 @@ function factory(propsData = {}) { } describe('Repository table row component', () => { + const findRouterLink = () => vm.find(RouterLinkStub); + afterEach(() => { vm.destroy(); }); @@ -81,6 +87,21 @@ describe('Repository table row component', () => { }); }); + it('renders a gl-hover-load directive', () => { + factory({ + id: '1', + sha: '123', + path: 'test', + type: 'blob', + currentPath: '/', + }); + + const hoverLoadDirective = getBinding(findRouterLink().element, 'gl-hover-load'); + + expect(hoverLoadDirective).not.toBeUndefined(); + expect(hoverLoadDirective.value).toBeInstanceOf(Function); + }); + it.each` type | component | componentName ${'tree'} | ${RouterLinkStub} | ${'RouterLink'} diff --git a/spec/frontend/runner/components/runner_list_spec.js b/spec/frontend/runner/components/runner_list_spec.js index 09093c2ce64..d88d7b3fbee 100644 --- a/spec/frontend/runner/components/runner_list_spec.js +++ b/spec/frontend/runner/components/runner_list_spec.js @@ -68,7 +68,7 @@ describe('RunnerList', () => { }); it('Displays a list of runners', () => { - expect(findRows()).toHaveLength(2); + expect(findRows()).toHaveLength(3); expect(findSkeletonLoader().exists()).toBe(false); }); @@ -77,7 +77,7 @@ describe('RunnerList', () => { const { id, description, version, ipAddress, shortSha } = mockRunners[0]; // Badges - expect(findCell({ fieldKey: 'type' }).text()).toMatchInterpolatedText('shared locked'); + expect(findCell({ fieldKey: 'type' }).text()).toMatchInterpolatedText('specific paused'); // Runner identifier expect(findCell({ fieldKey: 'name' }).text()).toContain( diff --git a/spec/frontend/runner/mock_data.js b/spec/frontend/runner/mock_data.js index 744942bfa73..8f551feca6e 100644 --- a/spec/frontend/runner/mock_data.js +++ b/spec/frontend/runner/mock_data.js @@ -1,44 +1,6 @@ -export const runnersData = { - data: { - runners: { - nodes: [ - { - id: 'gid://gitlab/Ci::Runner/1', - description: 'runner-1', - runnerType: 'INSTANCE_TYPE', - shortSha: '2P6oDVDm', - version: '13.12.0', - revision: '11223344', - ipAddress: '127.0.0.1', - active: true, - locked: true, - tagList: [], - contactedAt: '2021-05-14T11:44:03Z', - __typename: 'CiRunner', - }, - { - id: 'gid://gitlab/Ci::Runner/2', - description: 'runner-2', - runnerType: 'GROUP_TYPE', - shortSha: 'dpSCAC31', - version: '13.12.0', - revision: '11223344', - ipAddress: '127.0.0.1', - active: true, - locked: true, - tagList: [], - contactedAt: '2021-05-14T11:44:02Z', - __typename: 'CiRunner', - }, - ], - pageInfo: { - endCursor: 'GRAPHQL_END_CURSOR', - startCursor: 'GRAPHQL_START_CURSOR', - hasNextPage: true, - hasPreviousPage: false, - __typename: 'PageInfo', - }, - __typename: 'CiRunnerConnection', - }, - }, -}; +// Fixtures generated by: spec/frontend/fixtures/runner.rb +export const runnersData = getJSONFixture('graphql/runner/get_runners.query.graphql.json'); +export const runnersDataPaginated = getJSONFixture( + 'graphql/runner/get_runners.query.graphql.paginated.json', +); +export const runnerData = getJSONFixture('graphql/runner/get_runner.query.graphql.json'); diff --git a/spec/frontend/runner/runner_detail/runner_details_app_spec.js b/spec/frontend/runner/runner_detail/runner_details_app_spec.js index c61cb647ae6..d0bd701458d 100644 --- a/spec/frontend/runner/runner_detail/runner_details_app_spec.js +++ b/spec/frontend/runner/runner_detail/runner_details_app_spec.js @@ -3,12 +3,15 @@ import VueApollo from 'vue-apollo'; import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; +import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import RunnerTypeBadge from '~/runner/components/runner_type_badge.vue'; -import { INSTANCE_TYPE } from '~/runner/constants'; import getRunnerQuery from '~/runner/graphql/get_runner.query.graphql'; import RunnerDetailsApp from '~/runner/runner_details/runner_details_app.vue'; -const mockRunnerId = '55'; +import { runnerData } from '../mock_data'; + +const mockRunnerGraphqlId = runnerData.data.runner.id; +const mockRunnerId = `${getIdFromGraphQLId(mockRunnerGraphqlId)}`; const localVue = createLocalVue(); localVue.use(VueApollo); @@ -35,15 +38,7 @@ describe('RunnerDetailsApp', () => { }; beforeEach(async () => { - mockRunnerQuery = jest.fn().mockResolvedValue({ - data: { - runner: { - id: `gid://gitlab/Ci::Runner/${mockRunnerId}`, - runnerType: INSTANCE_TYPE, - __typename: 'CiRunner', - }, - }, - }); + mockRunnerQuery = jest.fn().mockResolvedValue(runnerData); }); afterEach(() => { @@ -54,13 +49,13 @@ describe('RunnerDetailsApp', () => { it('expect GraphQL ID to be requested', async () => { await createComponentWithApollo(); - expect(mockRunnerQuery).toHaveBeenCalledWith({ id: `gid://gitlab/Ci::Runner/${mockRunnerId}` }); + expect(mockRunnerQuery).toHaveBeenCalledWith({ id: mockRunnerGraphqlId }); }); it('displays the runner id', async () => { await createComponentWithApollo(); - expect(wrapper.text()).toContain('Runner #55'); + expect(wrapper.text()).toContain(`Runner #${mockRunnerId}`); }); it('displays the runner type', async () => { diff --git a/spec/frontend/runner/runner_list/runner_list_app_spec.js b/spec/frontend/runner/runner_list/runner_list_app_spec.js index e908e62db4f..dd913df7143 100644 --- a/spec/frontend/runner/runner_list/runner_list_app_spec.js +++ b/spec/frontend/runner/runner_list/runner_list_app_spec.js @@ -24,12 +24,10 @@ import { import getRunnersQuery from '~/runner/graphql/get_runners.query.graphql'; import RunnerListApp from '~/runner/runner_list/runner_list_app.vue'; -import { runnersData } from '../mock_data'; +import { runnersData, runnersDataPaginated } from '../mock_data'; const mockRegistrationToken = 'MOCK_REGISTRATION_TOKEN'; const mockActiveRunnersCount = 2; -const mocKRunners = runnersData.data.runners.nodes; -const mockPageInfo = runnersData.data.runners.pageInfo; jest.mock('@sentry/browser'); jest.mock('~/lib/utils/url_utility', () => ({ @@ -98,7 +96,7 @@ describe('RunnerListApp', () => { }); it('shows the runners list', () => { - expect(mocKRunners).toMatchObject(findRunnerList().props('runners')); + expect(runnersData.data.runners.nodes).toMatchObject(findRunnerList().props('runners')); }); it('requests the runners with no filters', () => { @@ -205,6 +203,8 @@ describe('RunnerListApp', () => { describe('Pagination', () => { beforeEach(() => { + mockRunnersQuery = jest.fn().mockResolvedValue(runnersDataPaginated); + createComponentWithApollo({ mountFn: mount }); }); @@ -225,13 +225,7 @@ describe('RunnerListApp', () => { expect(mockRunnersQuery).toHaveBeenLastCalledWith({ sort: CREATED_DESC, first: RUNNER_PAGE_SIZE, - after: expect.any(String), - }); - - expect(mockRunnersQuery).toHaveBeenLastCalledWith({ - sort: CREATED_DESC, - first: RUNNER_PAGE_SIZE, - after: mockPageInfo.endCursor, + after: runnersDataPaginated.data.runners.pageInfo.endCursor, }); }); }); diff --git a/spec/lib/gitlab/ci/yaml_processor/result_spec.rb b/spec/lib/gitlab/ci/yaml_processor/result_spec.rb index a0a79dcad36..25705fd4260 100644 --- a/spec/lib/gitlab/ci/yaml_processor/result_spec.rb +++ b/spec/lib/gitlab/ci/yaml_processor/result_spec.rb @@ -54,14 +54,22 @@ module Gitlab YAML end - subject(:yaml_variables_for) { result.yaml_variables_for(:job) } + let(:job_name) { :job } - it do + subject(:yaml_variables_for) { result.yaml_variables_for(job_name) } + + it 'returns calculated variables with root and job variables' do is_expected.to match_array([ { key: 'VAR1', value: 'value 11', public: true }, { key: 'VAR2', value: 'value 2', public: true } ]) end + + context 'when an absent job is sent' do + let(:job_name) { :invalid_job } + + it { is_expected.to eq([]) } + end end describe '#stage_for' do @@ -72,10 +80,16 @@ module Gitlab YAML end - subject(:stage_for) { result.stage_for(:job) } + let(:job_name) { :job } + + subject(:stage_for) { result.stage_for(job_name) } + + it { is_expected.to eq('test') } + + context 'when an absent job is sent' do + let(:job_name) { :invalid_job } - it do - is_expected.to eq('test') + it { is_expected.to be_nil } end end end diff --git a/spec/lib/gitlab/data_builder/build_spec.rb b/spec/lib/gitlab/data_builder/build_spec.rb index a31e5a1d1e2..325fdb90929 100644 --- a/spec/lib/gitlab/data_builder/build_spec.rb +++ b/spec/lib/gitlab/data_builder/build_spec.rb @@ -47,6 +47,8 @@ RSpec.describe Gitlab::DataBuilder::Build do it { expect(data[:runner][:id]).to eq(build.runner.id) } it { expect(data[:runner][:tags]).to match_array(tag_names) } it { expect(data[:runner][:description]).to eq(build.runner.description) } + it { expect(data[:runner][:runner_type]).to eq(build.runner.runner_type) } + it { expect(data[:runner][:is_shared]).to eq(build.runner.instance_type?) } it { expect(data[:environment]).to be_nil } context 'commit author_url' do diff --git a/spec/lib/gitlab/data_builder/pipeline_spec.rb b/spec/lib/gitlab/data_builder/pipeline_spec.rb index bec1e612c02..c05a044f0de 100644 --- a/spec/lib/gitlab/data_builder/pipeline_spec.rb +++ b/spec/lib/gitlab/data_builder/pipeline_spec.rb @@ -58,8 +58,10 @@ RSpec.describe Gitlab::DataBuilder::Pipeline do it 'has runner attributes', :aggregate_failures do expect(runner_data[:id]).to eq(ci_runner.id) expect(runner_data[:description]).to eq(ci_runner.description) + expect(runner_data[:runner_type]).to eq(ci_runner.runner_type) expect(runner_data[:active]).to eq(ci_runner.active) expect(runner_data[:tags]).to match_array(tag_names) + expect(runner_data[:is_shared]).to eq(ci_runner.instance_type?) end end diff --git a/spec/lib/gitlab/markdown_cache/field_data_spec.rb b/spec/lib/gitlab/markdown_cache/field_data_spec.rb index 76d8cbe6b7d..6d4b57254f2 100644 --- a/spec/lib/gitlab/markdown_cache/field_data_spec.rb +++ b/spec/lib/gitlab/markdown_cache/field_data_spec.rb @@ -12,4 +12,11 @@ RSpec.describe Gitlab::MarkdownCache::FieldData do it 'translates a markdown field name into a html field name' do expect(field_data.html_field(:description)).to eq("description_html") end + + describe '#key?' do + specify do + expect(field_data.key?(:description)).to be_truthy + expect(field_data.key?(:something_else)).to be_falsy + end + end end diff --git a/spec/migrations/20210603222333_remove_builds_email_service_from_services_spec.rb b/spec/migrations/20210603222333_remove_builds_email_service_from_services_spec.rb new file mode 100644 index 00000000000..c457be79834 --- /dev/null +++ b/spec/migrations/20210603222333_remove_builds_email_service_from_services_spec.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +require 'spec_helper' + +require_migration!('remove_builds_email_service_from_services') + +RSpec.describe RemoveBuildsEmailServiceFromServices do + let(:namespaces) { table(:namespaces) } + let(:projects) { table(:projects) } + let(:services) { table(:services) } + let(:namespace) { namespaces.create!(name: 'foo', path: 'bar') } + let(:project) { projects.create!(namespace_id: namespace.id) } + + it 'correctly deletes `BuildsEmailService` services' do + services.create!(project_id: project.id, type: 'BuildsEmailService') + services.create!(project_id: project.id, type: 'OtherService') + + expect(services.all.pluck(:type)).to match_array %w[BuildsEmailService OtherService] + + migrate! + + expect(services.all.pluck(:type)).to eq %w[OtherService] + end +end diff --git a/spec/models/member_spec.rb b/spec/models/member_spec.rb index b0575b63c8a..372fc40afcc 100644 --- a/spec/models/member_spec.rb +++ b/spec/models/member_spec.rb @@ -408,6 +408,20 @@ RSpec.describe Member do it { is_expected.not_to include @member_with_minimal_access } end + describe '.without_invites_and_requests' do + subject { described_class.without_invites_and_requests.to_a } + + it { is_expected.to include @owner } + it { is_expected.to include @maintainer } + it { is_expected.not_to include @invited_member } + it { is_expected.to include @accepted_invite_member } + it { is_expected.not_to include @requested_member } + it { is_expected.to include @accepted_request_member } + it { is_expected.to include @blocked_maintainer } + it { is_expected.to include @blocked_developer } + it { is_expected.not_to include @member_with_minimal_access } + end + describe '.connected_to_user' do subject { described_class.connected_to_user.to_a } diff --git a/spec/requests/api/ci/runners_spec.rb b/spec/requests/api/ci/runners_spec.rb index 1727bc830fc..82fb4440429 100644 --- a/spec/requests/api/ci/runners_spec.rb +++ b/spec/requests/api/ci/runners_spec.rb @@ -137,11 +137,11 @@ RSpec.describe API::Ci::Runners do get api('/runners/all', admin) expect(json_response).to match_array [ - a_hash_including('description' => 'Project runner'), - a_hash_including('description' => 'Two projects runner'), - a_hash_including('description' => 'Group runner A'), - a_hash_including('description' => 'Group runner B'), - a_hash_including('description' => 'Shared runner') + a_hash_including('description' => 'Project runner', 'is_shared' => false, 'runner_type' => 'project_type'), + a_hash_including('description' => 'Two projects runner', 'is_shared' => false, 'runner_type' => 'project_type'), + a_hash_including('description' => 'Group runner A', 'is_shared' => false, 'runner_type' => 'group_type'), + a_hash_including('description' => 'Group runner B', 'is_shared' => false, 'runner_type' => 'group_type'), + a_hash_including('description' => 'Shared runner', 'is_shared' => true, 'runner_type' => 'instance_type') ] end diff --git a/spec/requests/api/graphql/group/group_members_spec.rb b/spec/requests/api/graphql/group/group_members_spec.rb index 452610ab18f..31cb0393d7f 100644 --- a/spec/requests/api/graphql/group/group_members_spec.rb +++ b/spec/requests/api/graphql/group/group_members_spec.rb @@ -14,6 +14,23 @@ RSpec.describe 'getting group members information' do [user_1, user_2].each { |user| parent_group.add_guest(user) } end + context 'when a member is invited only via email' do + before do + create(:group_member, :invited, source: parent_group) + end + + it 'returns null in the user field' do + fetch_members(group: parent_group, args: { relations: [:DIRECT] }) + + expect(graphql_errors).to be_nil + expect(graphql_data_at(:group, :group_members, :edges, :node)).to contain_exactly( + { 'user' => { 'id' => global_id_of(user_1) } }, + { 'user' => { 'id' => global_id_of(user_2) } }, + 'user' => nil + ) + end + end + context 'when the request is correct' do it_behaves_like 'a working graphql query' do before do diff --git a/spec/requests/api/graphql/project/project_members_spec.rb b/spec/requests/api/graphql/project/project_members_spec.rb index c08bb8dc0a0..466464f600c 100644 --- a/spec/requests/api/graphql/project/project_members_spec.rb +++ b/spec/requests/api/graphql/project/project_members_spec.rb @@ -50,6 +50,20 @@ RSpec.describe 'getting project members information' do invited_group.add_guest(invited_user) end + context 'when a member is invited only via email and current_user is a maintainer' do + before do + parent_project.add_maintainer(user) + create(:project_member, :invited, source: parent_project) + end + + it 'returns null in the user field' do + fetch_members(project: parent_project, args: { relations: [:DIRECT] }) + + expect(graphql_errors).to be_nil + expect(graphql_data_at(:project, :project_members, :edges, :node)).to contain_exactly({ 'user' => { 'id' => global_id_of(user) } }, 'user' => nil) + end + end + it 'returns direct members' do fetch_members(project: child_project, args: { relations: [:DIRECT] }) diff --git a/spec/requests/api/invitations_spec.rb b/spec/requests/api/invitations_spec.rb index a6eae2d5963..e4f394eed0e 100644 --- a/spec/requests/api/invitations_spec.rb +++ b/spec/requests/api/invitations_spec.rb @@ -162,7 +162,8 @@ RSpec.describe API::Invitations do category: 'Members::InviteService', action: 'create_member', label: 'api', - property: 'net_new_user' + property: 'net_new_user', + user: maintainer ) end @@ -173,7 +174,8 @@ RSpec.describe API::Invitations do category: 'Members::InviteService', action: 'create_member', label: '_invite_source_', - property: 'net_new_user' + property: 'net_new_user', + user: maintainer ) end end diff --git a/spec/requests/api/members_spec.rb b/spec/requests/api/members_spec.rb index 7f04d169710..93fa36205ab 100644 --- a/spec/requests/api/members_spec.rb +++ b/spec/requests/api/members_spec.rb @@ -266,7 +266,8 @@ RSpec.describe API::Members do category: 'Members::CreateService', action: 'create_member', label: 'api', - property: 'existing_user' + property: 'existing_user', + user: maintainer ) end @@ -278,7 +279,8 @@ RSpec.describe API::Members do category: 'Members::CreateService', action: 'create_member', label: '_invite_source_', - property: 'existing_user' + property: 'existing_user', + user: maintainer ) end end @@ -321,7 +323,8 @@ RSpec.describe API::Members do category: 'Members::CreateService', action: 'create_member', label: 'api', - property: 'existing_user' + property: 'existing_user', + user: maintainer ) end @@ -333,7 +336,8 @@ RSpec.describe API::Members do category: 'Members::CreateService', action: 'create_member', label: '_invite_source_', - property: 'existing_user' + property: 'existing_user', + user: maintainer ) end end diff --git a/spec/services/issues/zoom_link_service_spec.rb b/spec/services/issues/zoom_link_service_spec.rb index 19db892fcae..d662d9fa978 100644 --- a/spec/services/issues/zoom_link_service_spec.rb +++ b/spec/services/issues/zoom_link_service_spec.rb @@ -53,7 +53,10 @@ RSpec.describe Issues::ZoomLinkService do category: 'IncidentManagement::ZoomIntegration', action: 'add_zoom_meeting', label: 'Issue ID', - value: issue.id + value: issue.id, + user: user, + project: project, + namespace: project.namespace ) end @@ -192,7 +195,10 @@ RSpec.describe Issues::ZoomLinkService do category: 'IncidentManagement::ZoomIntegration', action: 'remove_zoom_meeting', label: 'Issue ID', - value: issue.id + value: issue.id, + user: user, + project: project, + namespace: project.namespace ) end end diff --git a/spec/services/members/create_service_spec.rb b/spec/services/members/create_service_spec.rb index 523da9e522a..803b0695345 100644 --- a/spec/services/members/create_service_spec.rb +++ b/spec/services/members/create_service_spec.rb @@ -93,7 +93,8 @@ RSpec.describe Members::CreateService, :aggregate_failures, :clean_gitlab_redis_ category: described_class.name, action: 'create_member', label: 'unknown', - property: 'existing_user' + property: 'existing_user', + user: user ) end end @@ -108,7 +109,8 @@ RSpec.describe Members::CreateService, :aggregate_failures, :clean_gitlab_redis_ category: described_class.name, action: 'create_member', label: '_invite_source_', - property: 'existing_user' + property: 'existing_user', + user: user ) end end @@ -123,7 +125,8 @@ RSpec.describe Members::CreateService, :aggregate_failures, :clean_gitlab_redis_ category: described_class.name, action: 'create_member', label: 'unknown', - property: 'net_new_user' + property: 'net_new_user', + user: user ) end end diff --git a/spec/services/security/ci_configuration/sast_parser_service_spec.rb b/spec/services/security/ci_configuration/sast_parser_service_spec.rb index cc6543cddc2..9ebc6be78e6 100644 --- a/spec/services/security/ci_configuration/sast_parser_service_spec.rb +++ b/spec/services/security/ci_configuration/sast_parser_service_spec.rb @@ -67,6 +67,23 @@ RSpec.describe Security::CiConfiguration::SastParserService do expect(sast_brakeman_level['value']).to eql('1') end end + + context 'when .gitlab-ci.yml does not include the sast job' do + before do + allow(project.repository).to receive(:blob_data_at).and_return( + File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml')) + ) + end + + it 'populates the current values with the default values' do + expect(secure_analyzers_prefix['value']).to eql('registry.gitlab.com/gitlab-org/security-products/analyzers') + expect(sast_excluded_paths['value']).to eql('spec, test, tests, tmp') + expect(sast_pipeline_stage['value']).to eql('test') + expect(sast_search_max_depth['value']).to eql('4') + expect(brakeman['enabled']).to be(true) + expect(sast_brakeman_level['value']).to eql('1') + end + end end end end diff --git a/spec/tasks/gitlab/db_rake_spec.rb b/spec/tasks/gitlab/db_rake_spec.rb index c4623061944..1df7e2d4e98 100644 --- a/spec/tasks/gitlab/db_rake_spec.rb +++ b/spec/tasks/gitlab/db_rake_spec.rb @@ -306,7 +306,7 @@ RSpec.describe 'gitlab:db namespace rake task' do let(:all_migrations) { [double('migration1', version: 1), pending_migration] } let(:pending_migration) { double('migration2', version: 2) } let(:filename) { Gitlab::Database::Migrations::Instrumentation::STATS_FILENAME } - let!(:directory) { Dir.mktmpdir } + let(:result_dir) { Dir.mktmpdir } let(:observations) { %w[some data] } before do @@ -316,19 +316,17 @@ RSpec.describe 'gitlab:db namespace rake task' do allow(instrumentation).to receive(:observe).and_yield - allow(Dir).to receive(:mkdir) - allow(File).to receive(:exist?).with(directory).and_return(false) - stub_const('Gitlab::Database::Migrations::Instrumentation::RESULT_DIR', directory) + stub_const('Gitlab::Database::Migrations::Instrumentation::RESULT_DIR', result_dir) end after do - FileUtils.rm_rf([directory]) + FileUtils.rm_rf(result_dir) end - it 'fails when the directory already exists' do - expect(File).to receive(:exist?).with(directory).and_return(true) + it 'creates result directory when one does not exist' do + FileUtils.rm_rf(result_dir) - expect { subject }.to raise_error(/Directory exists/) + expect { subject }.to change { Dir.exist?(result_dir) }.from(false).to(true) end it 'instruments the pending migration' do @@ -346,7 +344,7 @@ RSpec.describe 'gitlab:db namespace rake task' do it 'writes observations out to JSON file' do subject - expect(File.read(File.join(directory, filename))).to eq(observations.to_json) + expect(File.read(File.join(result_dir, filename))).to eq(observations.to_json) end end diff --git a/spec/views/projects/empty.html.haml_spec.rb b/spec/views/projects/empty.html.haml_spec.rb index 09fb9da598e..7fa95507f75 100644 --- a/spec/views/projects/empty.html.haml_spec.rb +++ b/spec/views/projects/empty.html.haml_spec.rb @@ -57,20 +57,24 @@ RSpec.describe 'projects/empty' do render expect(rendered).to have_selector('[data-track-event=render]') - expect(rendered).to have_selector('[data-track-label=invite_members_empty_project]', count: 2) + expect(rendered).to have_selector('[data-track-label=invite_members_empty_project]') expect(rendered).to have_content('Invite your team') expect(rendered).to have_content('Add members to this project and start collaborating with your team.') - expect(rendered).to have_link('Invite members', href: project_project_members_path(project, sort: :access_level_desc)) - expect(rendered).to have_selector('[data-track-event=click_button]') + expect(rendered).to have_selector('.js-invite-members-trigger') + expect(rendered).to have_selector('.js-invite-members-modal') + expect(rendered).to have_selector('[data-label=invite_members_empty_project]') + expect(rendered).to have_selector('[data-event=click_button]') end context 'when user does not have permissions to invite members' do let(:can_import_members) { false } - it 'does not show invite member info' do + it 'does not show invite member info', :aggregate_failures do render expect(rendered).not_to have_content('Invite your team') + expect(rendered).not_to have_selector('.js-invite-members-trigger') + expect(rendered).not_to have_selector('.js-invite-members-modal') end end end |