diff options
63 files changed, 1222 insertions, 227 deletions
diff --git a/app/assets/javascripts/diffs/components/diff_file_row.vue b/app/assets/javascripts/diffs/components/diff_file_row.vue index 3888eb781fb..6c5d9170c9e 100644 --- a/app/assets/javascripts/diffs/components/diff_file_row.vue +++ b/app/assets/javascripts/diffs/components/diff_file_row.vue @@ -41,10 +41,6 @@ export default { return !this.hideFileStats && this.file.type === 'blob'; }, fileClasses() { - if (!this.glFeatures.highlightCurrentDiffRow) { - return ''; - } - return this.file.type === 'blob' && !this.viewedFiles[this.file.fileHash] ? 'gl-font-weight-bold' : ''; diff --git a/app/assets/javascripts/members/components/filter_sort/filter_sort_container.vue b/app/assets/javascripts/members/components/filter_sort/filter_sort_container.vue index f2acc3215cd..f869ecd392f 100644 --- a/app/assets/javascripts/members/components/filter_sort/filter_sort_container.vue +++ b/app/assets/javascripts/members/components/filter_sort/filter_sort_container.vue @@ -1,18 +1,26 @@ <script> import { mapState } from 'vuex'; import MembersFilteredSearchBar from './members_filtered_search_bar.vue'; +import SortDropdown from './sort_dropdown.vue'; export default { name: 'FilterSortContainer', - components: { MembersFilteredSearchBar }, + components: { MembersFilteredSearchBar, SortDropdown }, computed: { - ...mapState(['filteredSearchBar']), + ...mapState(['filteredSearchBar', 'tableSortableFields']), + showContainer() { + return this.filteredSearchBar.show || this.showSortDropdown; + }, + showSortDropdown() { + return this.tableSortableFields.length; + }, }, }; </script> <template> - <div v-if="filteredSearchBar.show" class="gl-bg-gray-10 gl-p-5"> - <members-filtered-search-bar /> + <div v-if="showContainer" class="gl-bg-gray-10 gl-p-3 gl-display-md-flex"> + <members-filtered-search-bar v-if="filteredSearchBar.show" class="gl-p-3 gl-flex-grow-1" /> + <sort-dropdown v-if="showSortDropdown" class="gl-p-3 gl-flex-shrink-0" /> </div> </template> diff --git a/app/assets/javascripts/members/components/filter_sort/sort_dropdown.vue b/app/assets/javascripts/members/components/filter_sort/sort_dropdown.vue new file mode 100644 index 00000000000..e2fbb074fcd --- /dev/null +++ b/app/assets/javascripts/members/components/filter_sort/sort_dropdown.vue @@ -0,0 +1,66 @@ +<script> +import { mapState } from 'vuex'; +import { GlDropdown, GlDropdownItem, GlFormGroup } from '@gitlab/ui'; +import { parseSortParam, buildSortUrl } from '~/members/utils'; +import { FIELDS } from '~/members/constants'; + +export default { + name: 'SortDropdown', + components: { GlDropdown, GlDropdownItem, GlFormGroup }, + computed: { + ...mapState(['tableSortableFields', 'filteredSearchBar']), + sort() { + return parseSortParam(this.tableSortableFields); + }, + filteredOptions() { + const buildOption = (field, sortDesc) => ({ + ...(sortDesc ? field.sort.desc : field.sort.asc), + key: field.key, + sortDesc, + url: buildSortUrl({ + sortBy: field.key, + sortDesc, + filteredSearchBarTokens: this.filteredSearchBar.tokens, + filteredSearchBarSearchParam: this.filteredSearchBar.searchParam, + }), + }); + + return FIELDS.filter( + field => this.tableSortableFields.includes(field.key) && field.sort, + ).flatMap(field => [buildOption(field, false), buildOption(field, true)]); + }, + }, + methods: { + isChecked(key, sortDesc) { + return this.sort?.sortBy === key && this.sort?.sortDesc === sortDesc; + }, + }, +}; +</script> + +<template> + <gl-form-group + :label="__('Sort by')" + class="gl-mb-0" + label-cols="auto" + label-class="gl-align-self-center gl-pb-0!" + > + <gl-dropdown + :text="sort.sortByLabel" + block + toggle-class="gl-mb-0" + data-testid="members-sort-dropdown" + right + > + <gl-dropdown-item + v-for="option in filteredOptions" + :key="option.param" + :href="option.url" + is-check-item + :is-checked="isChecked(option.key, option.sortDesc)" + > + {{ option.label }} + </gl-dropdown-item> + </gl-dropdown> + </gl-form-group> +</template> diff --git a/app/assets/javascripts/members/constants.js b/app/assets/javascripts/members/constants.js index a23e9b942ef..874e934e5b0 100644 --- a/app/assets/javascripts/members/constants.js +++ b/app/assets/javascripts/members/constants.js @@ -1,9 +1,21 @@ -import { __ } from '~/locale'; +import { __, s__ } from '~/locale'; + +const ACCOUNT_SORT_ASC_LABEL = s__('Members|Account, ascending'); export const FIELDS = [ { key: 'account', label: __('Account'), + sort: { + asc: { + param: 'name_asc', + label: ACCOUNT_SORT_ASC_LABEL, + }, + desc: { + param: 'name_desc', + label: s__('Members|Account, descending'), + }, + }, }, { key: 'source', @@ -16,6 +28,16 @@ export const FIELDS = [ label: __('Access granted'), thClass: 'col-meta', tdClass: 'col-meta', + sort: { + asc: { + param: 'last_joined', + label: s__('Members|Access granted, ascending'), + }, + desc: { + param: 'oldest_joined', + label: s__('Members|Access granted, descending'), + }, + }, }, { key: 'invited', @@ -40,6 +62,16 @@ export const FIELDS = [ label: __('Max role'), thClass: 'col-max-role', tdClass: 'col-max-role', + sort: { + asc: { + param: 'access_level_asc', + label: s__('Members|Max role, ascending'), + }, + desc: { + param: 'access_level_desc', + label: s__('Members|Max role, descending'), + }, + }, }, { key: 'expiration', @@ -48,6 +80,19 @@ export const FIELDS = [ tdClass: 'col-expiration', }, { + key: 'lastSignIn', + sort: { + asc: { + param: 'recent_sign_in', + label: s__('Members|Last sign-in, ascending'), + }, + desc: { + param: 'oldest_sign_in', + label: s__('Members|Last sign-in, descending'), + }, + }, + }, + { key: 'actions', thClass: 'col-actions', tdClass: 'col-actions', @@ -55,6 +100,12 @@ export const FIELDS = [ }, ]; +export const DEFAULT_SORT = { + sortBy: 'account', + sortDesc: false, + sortByLabel: ACCOUNT_SORT_ASC_LABEL, +}; + export const AVATAR_SIZE = 48; export const MEMBER_TYPES = { diff --git a/app/assets/javascripts/members/utils.js b/app/assets/javascripts/members/utils.js index 4229a62c0a7..5c58c4a9f6c 100644 --- a/app/assets/javascripts/members/utils.js +++ b/app/assets/javascripts/members/utils.js @@ -1,4 +1,7 @@ import { __ } from '~/locale'; +import { getParameterByName } from '~/lib/utils/common_utils'; +import { setUrlParams } from '~/lib/utils/url_utility'; +import { FIELDS, DEFAULT_SORT } from './constants'; export const generateBadges = (member, isCurrentUser) => [ { @@ -44,5 +47,54 @@ export const canUpdate = (member, currentUserId, sourceId) => { ); }; +export const parseSortParam = sortableFields => { + const sortParam = getParameterByName('sort'); + + const sortedField = FIELDS.filter(field => sortableFields.includes(field.key)).find( + field => field.sort?.asc?.param === sortParam || field.sort?.desc?.param === sortParam, + ); + + if (!sortedField) { + return DEFAULT_SORT; + } + + const isDesc = sortedField?.sort?.desc?.param === sortParam; + + return { + sortBy: sortedField.key, + sortDesc: isDesc, + sortByLabel: isDesc ? sortedField?.sort?.desc?.label : sortedField?.sort?.asc?.label, + }; +}; + +export const buildSortUrl = ({ + sortBy, + sortDesc, + filteredSearchBarTokens, + filteredSearchBarSearchParam, +}) => { + const sortDefinition = FIELDS.find(field => field.key === sortBy)?.sort; + + if (!sortDefinition) { + return ''; + } + + const sortParam = sortDesc ? sortDefinition.desc.param : sortDefinition.asc.param; + + const filterParams = + filteredSearchBarTokens?.reduce((accumulator, token) => { + return { + ...accumulator, + [token]: getParameterByName(token), + }; + }, {}) || {}; + + if (filteredSearchBarSearchParam) { + filterParams[filteredSearchBarSearchParam] = getParameterByName(filteredSearchBarSearchParam); + } + + return setUrlParams({ ...filterParams, sort: sortParam }, window.location.href, true); +}; + // Defined in `ee/app/assets/javascripts/vue_shared/components/members/utils.js` export const canOverride = () => false; diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 69aec044863..838b1925f34 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -36,7 +36,6 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo push_frontend_feature_flag(:hide_jump_to_next_unresolved_in_threads, default_enabled: true) push_frontend_feature_flag(:merge_request_widget_graphql, @project) push_frontend_feature_flag(:unified_diff_components, @project) - push_frontend_feature_flag(:highlight_current_diff_row, @project) push_frontend_feature_flag(:default_merge_ref_for_diffs, @project) push_frontend_feature_flag(:core_security_mr_widget, @project, default_enabled: true) push_frontend_feature_flag(:core_security_mr_widget_counts, @project) diff --git a/app/helpers/members_helper.rb b/app/helpers/members_helper.rb index d66f67fbb60..5dc636ad996 100644 --- a/app/helpers/members_helper.rb +++ b/app/helpers/members_helper.rb @@ -6,14 +6,14 @@ module MembersHelper text = 'Are you sure you want to' action = - if member.request? + if member.invite? + "revoke the invitation for #{member.invite_email} to join" + elsif member.request? if member.user == user 'withdraw your access request for' else "deny #{member.user.name}'s request to join" end - elsif member.invite? - "revoke the invitation for #{member.invite_email} to join" else if member.user "remove #{member.user.name} from" diff --git a/app/presenters/alert_management/alert_presenter.rb b/app/presenters/alert_management/alert_presenter.rb index 4bfa3dc9a13..1cebf5c561a 100644 --- a/app/presenters/alert_management/alert_presenter.rb +++ b/app/presenters/alert_management/alert_presenter.rb @@ -8,7 +8,6 @@ module AlertManagement MARKDOWN_LINE_BREAK = " \n" HORIZONTAL_LINE = "\n\n---\n\n" - INCIDENT_LABEL_NAME = ::IncidentManagement::CreateIncidentLabelService::LABEL_PROPERTIES[:title] delegate :metrics_dashboard_url, :runbook, to: :parsed_payload @@ -48,7 +47,7 @@ module AlertManagement end def incident_issues_link - project_issues_url(project, label_name: INCIDENT_LABEL_NAME) + project_incidents_url(project) end def performance_dashboard_link diff --git a/app/services/issues/base_service.rb b/app/services/issues/base_service.rb index 978ea6fe9bc..25f319da03b 100644 --- a/app/services/issues/base_service.rb +++ b/app/services/issues/base_service.rb @@ -73,22 +73,6 @@ module Issues Milestones::IssuesCountService.new(milestone).delete_cache end - - # Applies label "incident" (creates it if missing) to incident issues. - # Please use in "after" hooks only to ensure we are not appyling - # labels prematurely. - def add_incident_label(issue) - return unless issue.incident? - - label = ::IncidentManagement::CreateIncidentLabelService - .new(project, current_user) - .execute - .payload[:label] - - return if issue.label_ids.include?(label.id) - - issue.labels << label - end end end diff --git a/app/services/issues/create_service.rb b/app/services/issues/create_service.rb index fb7683f940d..44de8eb6389 100644 --- a/app/services/issues/create_service.rb +++ b/app/services/issues/create_service.rb @@ -49,6 +49,22 @@ module Issues def user_agent_detail_service UserAgentDetailService.new(@issue, @request) end + + # Applies label "incident" (creates it if missing) to incident issues. + # For use in "after" hooks only to ensure we are not appyling + # labels prematurely. + def add_incident_label(issue) + return unless issue.incident? + + label = ::IncidentManagement::CreateIncidentLabelService + .new(project, current_user) + .execute + .payload[:label] + + return if issue.label_ids.include?(label.id) + + issue.labels << label + end end end diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb index 4f40ff5f535..127ed04cf51 100644 --- a/app/services/issues/update_service.rb +++ b/app/services/issues/update_service.rb @@ -34,7 +34,6 @@ module Issues end def after_update(issue) - add_incident_label(issue) IssuesChannel.broadcast_to(issue, event: 'updated') if Gitlab::ActionCable::Config.in_app? || Feature.enabled?(:broadcast_issue_updates, issue.project) end diff --git a/app/services/members/invite_service.rb b/app/services/members/invite_service.rb index cfab5c3ef9d..60ebbaface2 100644 --- a/app/services/members/invite_service.rb +++ b/app/services/members/invite_service.rb @@ -20,8 +20,8 @@ module Members emails.each do |email| next if existing_member?(source, email) - next if existing_invite?(source, email) + next if existing_request?(source, email) if existing_user?(email) add_existing_user_as_member(current_user, source, params, email) @@ -44,8 +44,7 @@ module Members access_level: params[:access_level], invite_email: email, created_by_id: current_user.id, - expires_at: params[:expires_at], - requested_at: Time.current.utc) + expires_at: params[:expires_at]) unless new_member.valid? && new_member.persisted? errors[params[:email]] = new_member.errors.full_messages.to_sentence @@ -92,6 +91,17 @@ module Members false end + def existing_request?(source, email) + existing_request = source.requesters.with_user_by_email(email).exists? + + if existing_request + errors[email] = "Member cannot be invited because they already requested to join #{source.name}" + return true + end + + false + end + def existing_user(email) User.find_by_email(email) end diff --git a/app/services/packages/create_event_service.rb b/app/services/packages/create_event_service.rb index 74d57f2ad98..f0328ceb08a 100644 --- a/app/services/packages/create_event_service.rb +++ b/app/services/packages/create_event_service.rb @@ -4,7 +4,9 @@ module Packages class CreateEventService < BaseService def execute if Feature.enabled?(:collect_package_events_redis) && redis_event_name - unless guest? + if guest? + ::Gitlab::UsageDataCounters::GuestPackageEventCounter.count(redis_event_name) + else ::Gitlab::UsageDataCounters::HLLRedisCounter.track_event(current_user.id, redis_event_name) end end diff --git a/changelogs/unreleased/205578-trigger-pkg-guest-events.yml b/changelogs/unreleased/205578-trigger-pkg-guest-events.yml new file mode 100644 index 00000000000..2e92b37c2f7 --- /dev/null +++ b/changelogs/unreleased/205578-trigger-pkg-guest-events.yml @@ -0,0 +1,5 @@ +--- +title: Tracks guest package events +merge_request: 48547 +author: +type: added diff --git a/changelogs/unreleased/268282-remove-feature-flag.yml b/changelogs/unreleased/268282-remove-feature-flag.yml new file mode 100644 index 00000000000..fc08aad3e2c --- /dev/null +++ b/changelogs/unreleased/268282-remove-feature-flag.yml @@ -0,0 +1,5 @@ +--- +title: Enable file tree highlighting by default +merge_request: 49356 +author: +type: changed diff --git a/changelogs/unreleased/285076-400-bad-request-during-authentication-due-to-password-format-lengt.yml b/changelogs/unreleased/285076-400-bad-request-during-authentication-due-to-password-format-lengt.yml new file mode 100644 index 00000000000..a2dfc320f49 --- /dev/null +++ b/changelogs/unreleased/285076-400-bad-request-during-authentication-due-to-password-format-lengt.yml @@ -0,0 +1,5 @@ +--- +title: Add different string encoding method in rack middleware +merge_request: 49044 +author: +type: fixed diff --git a/changelogs/unreleased/290006-error-500-on-members-page-after-invitation-sent-via-api.yml b/changelogs/unreleased/290006-error-500-on-members-page-after-invitation-sent-via-api.yml new file mode 100644 index 00000000000..8d393e47a61 --- /dev/null +++ b/changelogs/unreleased/290006-error-500-on-members-page-after-invitation-sent-via-api.yml @@ -0,0 +1,5 @@ +--- +title: Resolve Members page 500 error after Invitation sent via API +merge_request: 48937 +author: +type: fixed diff --git a/changelogs/unreleased/sy-allow-to-unlabel-incidents.yml b/changelogs/unreleased/sy-allow-to-unlabel-incidents.yml new file mode 100644 index 00000000000..68eeffccf63 --- /dev/null +++ b/changelogs/unreleased/sy-allow-to-unlabel-incidents.yml @@ -0,0 +1,5 @@ +--- +title: Do not automatically reapply incident label after user removes it +merge_request: 49188 +author: +type: fixed diff --git a/config/feature_flags/development/highlight_current_diff_row.yml b/config/feature_flags/development/highlight_current_diff_row.yml deleted file mode 100644 index fc872ea47fc..00000000000 --- a/config/feature_flags/development/highlight_current_diff_row.yml +++ /dev/null @@ -1,8 +0,0 @@ ---- -name: highlight_current_diff_row -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/27937 -rollout_issue_url: -milestone: '13.4' -type: development -group: group::source code -default_enabled: false diff --git a/doc/api/README.md b/doc/api/README.md index 63bccbddb50..dced721b018 100644 --- a/doc/api/README.md +++ b/doc/api/README.md @@ -107,7 +107,7 @@ curl: (22) The requested URL returned error: 404 ``` The HTTP exit code can help you diagnose the success or failure of your REST call. - + ## Authentication Most API requests require authentication, or only return public data when @@ -591,7 +591,7 @@ We can call the API with `array` and `hash` types parameters as follows: curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" \ -d "import_sources[]=github" \ -d "import_sources[]=bitbucket" \ -https://gitlab.example.com/api/v4/some_endpoint +"https://gitlab.example.com/api/v4/some_endpoint" ``` ### `hash` @@ -605,7 +605,7 @@ curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" \ --form "file=@/path/to/somefile.txt" --form "override_params[visibility]=private" \ --form "override_params[some_other_param]=some_value" \ -https://gitlab.example.com/api/v4/projects/import +"https://gitlab.example.com/api/v4/projects/import" ``` ### Array of hashes diff --git a/doc/api/audit_events.md b/doc/api/audit_events.md index 315059fa87a..83b244ccb8d 100644 --- a/doc/api/audit_events.md +++ b/doc/api/audit_events.md @@ -258,7 +258,7 @@ are paginated. Read more on [pagination](README.md#pagination). ```shell -curl --header "PRIVATE-TOKEN: <your_access_token>" https://primary.example.com/api/v4/projects/7/audit_events +curl --header "PRIVATE-TOKEN: <your_access_token>" "https://primary.example.com/api/v4/projects/7/audit_events" ``` Example response: @@ -318,7 +318,7 @@ GET /projects/:id/audit_events/:audit_event_id | `audit_event_id` | integer | yes | The ID of the audit event | ```shell -curl --header "PRIVATE-TOKEN: <your_access_token>" https://primary.example.com/api/v4/projects/7/audit_events/5 +curl --header "PRIVATE-TOKEN: <your_access_token>" "https://primary.example.com/api/v4/projects/7/audit_events/5" ``` Example response: diff --git a/doc/api/commits.md b/doc/api/commits.md index 8987704c8b2..81014956fc5 100644 --- a/doc/api/commits.md +++ b/doc/api/commits.md @@ -635,7 +635,7 @@ GET /projects/:id/repository/commits/:sha/statuses | `all` | boolean | no | Return all statuses, not only the latest ones ```shell -curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/17/repository/commits/18f3e63d05582537db6d183d9d557be09e1f90c8/statuses +curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/17/repository/commits/18f3e63d05582537db6d183d9d557be09e1f90c8/statuses" ``` Example response: diff --git a/doc/api/feature_flags.md b/doc/api/feature_flags.md index 45028408491..e8d6911463d 100644 --- a/doc/api/feature_flags.md +++ b/doc/api/feature_flags.md @@ -103,7 +103,7 @@ GET /projects/:id/feature_flags/:feature_flag_name | `feature_flag_name` | string | yes | The name of the feature flag. | ```shell -curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/1/feature_flags/awesome_feature +curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/feature_flags/awesome_feature" ``` Example response: diff --git a/doc/api/feature_flags_legacy.md b/doc/api/feature_flags_legacy.md index eae4dbc36e1..8a5af39a37f 100644 --- a/doc/api/feature_flags_legacy.md +++ b/doc/api/feature_flags_legacy.md @@ -36,7 +36,7 @@ GET /projects/:id/feature_flags | `scope` | string | no | The condition of feature flags, one of: `enabled`, `disabled`. | ```shell -curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/1/feature_flags +curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/feature_flags" ``` Example response: @@ -174,7 +174,7 @@ POST /projects/:id/feature_flags | `scopes:strategies` | JSON | no | The [strategies](../operations/feature_flags.md#feature-flag-strategies) of the feature flag spec. | ```shell -curl https://gitlab.example.com/api/v4/projects/1/feature_flags \ +curl "https://gitlab.example.com/api/v4/projects/1/feature_flags" \ --header "PRIVATE-TOKEN: <your_access_token>" \ --header "Content-type: application/json" \ --data @- << EOF @@ -244,7 +244,7 @@ GET /projects/:id/feature_flags/:name | `name` | string | yes | The name of the feature flag. | ```shell -curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/1/feature_flags/new_live_trace +curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/feature_flags/new_live_trace" ``` Example response: @@ -320,5 +320,5 @@ DELETE /projects/:id/feature_flags/:name | `name` | string | yes | The name of the feature flag. | ```shell -curl --header "PRIVATE-TOKEN: <your_access_token>" --request DELETE https://gitlab.example.com/api/v4/projects/1/feature_flags/awesome_feature +curl --header "PRIVATE-TOKEN: <your_access_token>" --request DELETE "https://gitlab.example.com/api/v4/projects/1/feature_flags/awesome_feature" ``` diff --git a/doc/api/freeze_periods.md b/doc/api/freeze_periods.md index d26c9248084..ae640e254d1 100644 --- a/doc/api/freeze_periods.md +++ b/doc/api/freeze_periods.md @@ -101,7 +101,7 @@ Example request: ```shell curl --header 'Content-Type: application/json' --header "PRIVATE-TOKEN: <your_access_token>" \ --data '{ "freeze_start": "0 23 * * 5", "freeze_end": "0 7 * * 1", "cron_timezone": "UTC" }' \ - --request POST https://gitlab.example.com/api/v4/projects/19/freeze_periods + --request POST "https://gitlab.example.com/api/v4/projects/19/freeze_periods" ``` Example response: diff --git a/doc/api/graphql/getting_started.md b/doc/api/graphql/getting_started.md index 49886ec8595..e5f2dde325b 100644 --- a/doc/api/graphql/getting_started.md +++ b/doc/api/graphql/getting_started.md @@ -29,7 +29,7 @@ Example: ```shell GRAPHQL_TOKEN=<your-token> -curl 'https://gitlab.com/api/graphql' --header "Authorization: Bearer $GRAPHQL_TOKEN" --header "Content-Type: application/json" --request POST --data "{\"query\": \"query {currentUser {name}}\"}" +curl "https://gitlab.com/api/graphql" --header "Authorization: Bearer $GRAPHQL_TOKEN" --header "Content-Type: application/json" --request POST --data "{\"query\": \"query {currentUser {name}}\"}" ``` ### GraphiQL diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql index bbb2f483a3c..e9924bd52d7 100644 --- a/doc/api/graphql/reference/gitlab_schema.graphql +++ b/doc/api/graphql/reference/gitlab_schema.graphql @@ -2195,6 +2195,11 @@ type BoardListUpdateLimitMetricsPayload { list: BoardList } +""" +Identifier of Boards::EpicBoard +""" +scalar BoardsEpicBoardID + type Branch { """ Commit for the branch @@ -7944,6 +7949,56 @@ type EpicAddIssuePayload { } """ +Represents an epic board +""" +type EpicBoard { + """ + Global ID of the board + """ + id: BoardsEpicBoardID! + + """ + Name of the board + """ + name: String +} + +""" +The connection type for EpicBoard. +""" +type EpicBoardConnection { + """ + A list of edges. + """ + edges: [EpicBoardEdge] + + """ + A list of nodes. + """ + nodes: [EpicBoard] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! +} + +""" +An edge in a connection. +""" +type EpicBoardEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: EpicBoard +} + +""" The connection type for Epic. """ type EpicConnection { @@ -9238,6 +9293,41 @@ type Group { ): Epic """ + Find a single epic board + """ + epicBoard( + """ + Find an epic board by ID + """ + id: BoardsEpicBoardID! + ): EpicBoard + + """ + Find epic boards + """ + epicBoards( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): EpicBoardConnection + + """ Find epics """ epics( diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json index 97b19afa6d5..3782960aece 100644 --- a/doc/api/graphql/reference/gitlab_schema.json +++ b/doc/api/graphql/reference/gitlab_schema.json @@ -5844,6 +5844,16 @@ }, { "kind": "SCALAR", + "name": "BoardsEpicBoardID", + "description": "Identifier of Boards::EpicBoard", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", "name": "Boolean", "description": "Represents `true` or `false` values.", "fields": null, @@ -22239,6 +22249,163 @@ }, { "kind": "OBJECT", + "name": "EpicBoard", + "description": "Represents an epic board", + "fields": [ + { + "name": "id", + "description": "Global ID of the board", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "BoardsEpicBoardID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "name", + "description": "Name of the board", + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "EpicBoardConnection", + "description": "The connection type for EpicBoard.", + "fields": [ + { + "name": "edges", + "description": "A list of edges.", + "args": [ + + ], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "EpicBoardEdge", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "nodes", + "description": "A list of nodes.", + "args": [ + + ], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "EpicBoard", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "pageInfo", + "description": "Information to aid in pagination.", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "PageInfo", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "EpicBoardEdge", + "description": "An edge in a connection.", + "fields": [ + { + "name": "cursor", + "description": "A cursor for use in pagination.", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "node", + "description": "The item at the end of the edge.", + "args": [ + + ], + "type": { + "kind": "OBJECT", + "name": "EpicBoard", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", "name": "EpicConnection", "description": "The connection type for Epic.", "fields": [ @@ -25699,6 +25866,86 @@ "deprecationReason": null }, { + "name": "epicBoard", + "description": "Find a single epic board", + "args": [ + { + "name": "id", + "description": "Find an epic board by ID", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "BoardsEpicBoardID", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "EpicBoard", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "epicBoards", + "description": "Find epic boards", + "args": [ + { + "name": "after", + "description": "Returns the elements in the list that come after the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "before", + "description": "Returns the elements in the list that come before the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "first", + "description": "Returns the first _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "last", + "description": "Returns the last _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "EpicBoardConnection", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { "name": "epics", "description": "Find epics", "args": [ diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 316476f290f..4380152d79e 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -1352,6 +1352,15 @@ Autogenerated return type of EpicAddIssue. | `epicIssue` | EpicIssue | The epic-issue relation | | `errors` | String! => Array | Errors encountered during execution of the mutation. | +### EpicBoard + +Represents an epic board. + +| Field | Type | Description | +| ----- | ---- | ----------- | +| `id` | BoardsEpicBoardID! | Global ID of the board | +| `name` | String | Name of the board | + ### EpicDescendantCount Counts of descendent epics. @@ -1532,6 +1541,8 @@ Autogenerated return type of EpicTreeReorder. | `descriptionHtml` | String | The GitLab Flavored Markdown rendering of `description` | | `emailsDisabled` | Boolean | Indicates if a group has email notifications disabled | | `epic` | Epic | Find a single epic | +| `epicBoard` | EpicBoard | Find a single epic board | +| `epicBoards` | EpicBoardConnection | Find epic boards | | `epics` | EpicConnection | Find epics | | `epicsEnabled` | Boolean | Indicates if Epics are enabled for namespace | | `fullName` | String! | Full name of the namespace | diff --git a/doc/api/import.md b/doc/api/import.md index f1292be5d6e..3a1edcb732d 100644 --- a/doc/api/import.md +++ b/doc/api/import.md @@ -71,7 +71,7 @@ POST /import/bitbucket_server ```shell curl --request POST \ - --url https://gitlab.example.com/api/v4/import/bitbucket_server \ + --url "https://gitlab.example.com/api/v4/import/bitbucket_server" \ --header "content-type: application/json" \ --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" \ --data '{ diff --git a/doc/api/invitations.md b/doc/api/invitations.md index d241c56dc9e..e5663f6efe1 100644 --- a/doc/api/invitations.md +++ b/doc/api/invitations.md @@ -97,7 +97,7 @@ Example response: { "id": 1, "invite_email": "member@example.org", - "invited_at": "2020-10-22T14:13:35Z", + "created_at": "2020-10-22T14:13:35Z", "access_level": 30, "expires_at": "2020-11-22T14:13:35Z", "user_name": "Raymond Smith", diff --git a/doc/api/issues.md b/doc/api/issues.md index 3157acc5d08..cbb0dad9841 100644 --- a/doc/api/issues.md +++ b/doc/api/issues.md @@ -1652,13 +1652,13 @@ Supported attributes: Example request: ```shell -curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/5/issues/11/notes?body=Lets%20promote%20this%20to%20an%20epic%0A%0A%2Fpromote +curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/issues/11/notes?body=Lets%20promote%20this%20to%20an%20epic%0A%0A%2Fpromote" ``` Example response: ```json -{ +{ "id":699, "type":null, "body":"Lets promote this to an epic", diff --git a/doc/api/jobs.md b/doc/api/jobs.md index d1286c98e2f..b7b742ebe66 100644 --- a/doc/api/jobs.md +++ b/doc/api/jobs.md @@ -293,7 +293,7 @@ GET /projects/:id/pipelines/:pipeline_id/bridges | `scope` | string **or** array of strings | no | Scope of jobs to show. Either one of or an array of the following: `created`, `pending`, `running`, `failed`, `success`, `canceled`, `skipped`, or `manual`. All jobs are returned if `scope` is not provided. | ```shell -curl --header "PRIVATE-TOKEN: <your_access_token>" 'https://gitlab.example.com/api/v4/projects/1/pipelines/6/bridges?scope[]=pending&scope[]=running' +curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/pipelines/6/bridges?scope[]=pending&scope[]=running" ``` Example of response diff --git a/doc/api/lint.md b/doc/api/lint.md index bd84e1dfcf8..48a7be13a28 100644 --- a/doc/api/lint.md +++ b/doc/api/lint.md @@ -246,7 +246,7 @@ GitLab API using `curl` and `jq` in a one-line command: ```shell jq --null-input --arg yaml "$(<example-gitlab-ci.yml)" '.content=$yaml' \ -| curl 'https://gitlab.com/api/v4/ci/lint?include_merged_yaml=true' \ +| curl "https://gitlab.com/api/v4/ci/lint?include_merged_yaml=true" \ --header 'Content-Type: application/json' \ --data @- ``` @@ -293,7 +293,7 @@ With a one-line command, you can: ```shell jq --null-input --arg yaml "$(<example-gitlab-ci.yml)" '.content=$yaml' \ -| curl 'https://gitlab.com/api/v4/ci/lint?include_merged_yaml=true' \ +| curl "https://gitlab.com/api/v4/ci/lint?include_merged_yaml=true" \ --header 'Content-Type: application/json' --data @- \ | jq --raw-output '.merged_yaml | fromjson' ``` diff --git a/doc/api/releases/index.md b/doc/api/releases/index.md index 62e08eab7db..af89cd206a7 100644 --- a/doc/api/releases/index.md +++ b/doc/api/releases/index.md @@ -376,7 +376,7 @@ Example request: ```shell curl --header 'Content-Type: application/json' --header "PRIVATE-TOKEN: gDybLx3yrUK_HLp3qPjS" \ --data '{ "name": "New release", "tag_name": "v0.3", "description": "Super nice release", "milestones": ["v1.0", "v1.0-rc"], "assets": { "links": [{ "name": "hoge", "url": "https://google.com", "filepath": "/binaries/linux-amd64", "link_type":"other" }] } }' \ - --request POST https://gitlab.example.com/api/v4/projects/24/releases + --request POST "https://gitlab.example.com/api/v4/projects/24/releases" ``` Example response: diff --git a/doc/api/scim.md b/doc/api/scim.md index fe344ea112f..a9622525478 100644 --- a/doc/api/scim.md +++ b/doc/api/scim.md @@ -39,7 +39,7 @@ Pagination follows the [SCIM spec](https://tools.ietf.org/html/rfc7644#section-3 Example request: ```shell -curl 'https://gitlab.example.com/api/scim/v2/groups/test_group/Users?filter=id%20eq%20"0b1d561c-21ff-4092-beab-8154b17f82f2"' --header "Authorization: Bearer <your_scim_token>" --header "Content-Type: application/scim+json" +curl "https://gitlab.example.com/api/scim/v2/groups/test_group/Users?filter=id%20eq%20%220b1d561c-21ff-4092-beab-8154b17f82f2%22" --header "Authorization: Bearer <your_scim_token>" --header "Content-Type: application/scim+json" ``` Example response: diff --git a/doc/api/templates/dockerfiles.md b/doc/api/templates/dockerfiles.md index 14451cae892..a86dc36e141 100644 --- a/doc/api/templates/dockerfiles.md +++ b/doc/api/templates/dockerfiles.md @@ -20,7 +20,7 @@ GET /templates/dockerfiles ``` ```shell -curl https://gitlab.example.com/api/v4/templates/dockerfiles +curl "https://gitlab.example.com/api/v4/templates/dockerfiles" ``` Example response: @@ -119,7 +119,7 @@ GET /templates/dockerfiles/:key | `key` | string | yes | The key of the Dockerfile template | ```shell -curl https://gitlab.example.com/api/v4/templates/dockerfiles/Binary +curl "https://gitlab.example.com/api/v4/templates/dockerfiles/Binary" ``` Example response: diff --git a/doc/api/templates/gitignores.md b/doc/api/templates/gitignores.md index 2f489b2d6a7..6f2e5a83903 100644 --- a/doc/api/templates/gitignores.md +++ b/doc/api/templates/gitignores.md @@ -21,7 +21,7 @@ GET /templates/gitignores Example request: ```shell -curl https://gitlab.example.com/api/v4/templates/gitignores +curl "https://gitlab.example.com/api/v4/templates/gitignores" ``` Example response: @@ -126,7 +126,7 @@ GET /templates/gitignores/:key Example request: ```shell -curl https://gitlab.example.com/api/v4/templates/gitignores/Ruby +curl "https://gitlab.example.com/api/v4/templates/gitignores/Ruby" ``` Example response: diff --git a/doc/api/templates/licenses.md b/doc/api/templates/licenses.md index 29c7f69c7f7..fb76bd81e50 100644 --- a/doc/api/templates/licenses.md +++ b/doc/api/templates/licenses.md @@ -25,7 +25,7 @@ GET /templates/licenses | `popular` | boolean | no | If passed, returns only popular licenses | ```shell -curl https://gitlab.example.com/api/v4/templates/licenses?popular=1 +curl "https://gitlab.example.com/api/v4/templates/licenses?popular=1" ``` Example response: @@ -128,7 +128,7 @@ If you omit the `fullname` parameter but authenticate your request, the name of the authenticated user replaces the copyright holder placeholder. ```shell -curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/templates/licenses/mit?project=My+Cool+Project +curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/templates/licenses/mit?project=My+Cool+Project" ``` Example response: diff --git a/doc/api/vulnerability_exports.md b/doc/api/vulnerability_exports.md index 98e349d6235..e4853920cab 100644 --- a/doc/api/vulnerability_exports.md +++ b/doc/api/vulnerability_exports.md @@ -83,7 +83,7 @@ POST /security/groups/:id/vulnerability_exports | `id` | integer or string | yes | The ID or [URL-encoded path](README.md#namespaced-path-encoding) of the group which the authenticated user is a member of | ```shell -curl --header POST "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/security/groups/1/vulnerability_exports +curl --header POST "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/security/groups/1/vulnerability_exports" ``` The created vulnerability export is automatically deleted after 1 hour. @@ -116,7 +116,7 @@ POST /security/vulnerability_exports ``` ```shell -curl --header POST "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/security/vulnerability_exports +curl --header POST "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/security/vulnerability_exports" ``` The created vulnerability export is automatically deleted after one hour. diff --git a/doc/development/testing_guide/frontend_testing.md b/doc/development/testing_guide/frontend_testing.md index b098b8418b4..cc7222426a7 100644 --- a/doc/development/testing_guide/frontend_testing.md +++ b/doc/development/testing_guide/frontend_testing.md @@ -961,6 +961,45 @@ describe "Admin::AbuseReports", :js do end ``` +### Jest test timeout due to async imports + +If a module asynchronously imports some other modules at runtime, these modules will need to be +transpiled by the Jest loaders at runtime. It's possible that this will cause [Jest to timeout](https://gitlab.com/gitlab-org/gitlab/-/issues/280809). + +If you run into this issue, consider eager importing the module so that Jest will compile +and cache it at compile-time, fixing the runtime timeout. + +Consider the following example: + +```javascript +// the_subject.js + +export default { + components: { + // Async import Thing because it is large and isn't always needed. + Thing: () => import(/* webpackChunkName: 'thing' */ './path/to/thing.vue'), + } +}; +``` + +Jest will not automatically transpile the `thing.vue` module, and depending on it's size, could +cause Jest to timeout. We can force Jest to transpile and cache this module by eagerly importing +it like so: + +```javascript +// the_subject_spec.js + +import Subject from '~/feature/the_subject.vue'; + +// Force Jest to transpile and cache +// eslint-disable-next-line import/order, no-unused-vars +import _Thing from '~/feature/path/to/thing.vue'; +``` + +**PLEASE NOTE:** Do not simply disregard test timeouts. This could be a sign that there's +actually a production problem. Use this opportunity to analyze the production webpack bundles and +chunks and confirm that there is not a production issue with the async imports. + ## Overview of Frontend Testing Levels Main information on frontend testing levels can be found in the [Testing Levels page](testing_levels.md). diff --git a/doc/user/markdown.md b/doc/user/markdown.md index f2633749cd3..15bb77efa17 100644 --- a/doc/user/markdown.md +++ b/doc/user/markdown.md @@ -431,7 +431,7 @@ GFM recognizes the following: | merge request | `!123` | `namespace/project!123` | `project!123` | | snippet | `$123` | `namespace/project$123` | `project$123` | | epic **(ULTIMATE)** | `&123` | `group1/subgroup&123` | | -| vulnerability **(ULTIMATE)** *(1)* | `[vulnerability:123]` | `[vulnerability:namespace/project/123]` | `[vulnerability:project/123]` | +| vulnerability **(ULTIMATE)** | `[vulnerability:123]` | `[vulnerability:namespace/project/123]` | `[vulnerability:project/123]` | | label by ID | `~123` | `namespace/project~123` | `project~123` | | one-word label by name | `~bug` | `namespace/project~bug` | `project~bug` | | multi-word label by name | `~"feature request"` | `namespace/project~"feature request"` | `project~"feature request"` | @@ -445,26 +445,6 @@ GFM recognizes the following: | repository file line references | `[README](doc/README#L13)` | | | | [alert](../operations/incident_management/alerts.md) | `^alert#123` | `namespace/project^alert#123` | `project^alert#123` | -1. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/281035) in GitLab 13.6. - - The Vulnerability special references feature is under development but ready for production use. - It is deployed behind a feature flag that is **disabled by default**. - [GitLab administrators with access to the GitLab Rails console](../administration/feature_flags.md) - can opt to enable it for your instance. - It's disabled on GitLab.com. - - To disable it: - - ```ruby - Feature.disable(:vulnerability_special_references) - ``` - - To enable it: - - ```ruby - Feature.enable(:vulnerability_special_references) - ``` - For example, referencing an issue by using `#123` will format the output as a link to issue number 123 with text `#123`. Likewise, a link to issue number 123 will be recognized and formatted with text `#123`. diff --git a/lib/api/entities/invitation.rb b/lib/api/entities/invitation.rb index 342f4804cf3..517b89529d7 100644 --- a/lib/api/entities/invitation.rb +++ b/lib/api/entities/invitation.rb @@ -4,7 +4,7 @@ module API module Entities class Invitation < Grape::Entity expose :access_level - expose :requested_at + expose :created_at expose :expires_at expose :invite_email expose :invite_token diff --git a/lib/gitlab/middleware/handle_malformed_strings.rb b/lib/gitlab/middleware/handle_malformed_strings.rb index 84f7e2e1b14..b966395ee32 100644 --- a/lib/gitlab/middleware/handle_malformed_strings.rb +++ b/lib/gitlab/middleware/handle_malformed_strings.rb @@ -93,7 +93,8 @@ module Gitlab # We try to encode the string from ASCII-8BIT to UTF8. If we failed to do # so for certain characters in the string, those chars are probably incomplete # multibyte characters. - string.encode(Encoding::UTF_8).match?(NULL_BYTE_REGEX) + string.dup.force_encoding(Encoding::UTF_8).match?(NULL_BYTE_REGEX) + rescue ArgumentError, Encoding::UndefinedConversionError # If we're here, we caught a malformed string. Return true true diff --git a/lib/gitlab/usage_data_counters/guest_package_events.yml b/lib/gitlab/usage_data_counters/guest_package_events.yml new file mode 100644 index 00000000000..a9b9f8ea235 --- /dev/null +++ b/lib/gitlab/usage_data_counters/guest_package_events.yml @@ -0,0 +1,34 @@ +--- +- i_package_composer_guest_delete +- i_package_composer_guest_pull +- i_package_composer_guest_push +- i_package_conan_guest_delete +- i_package_conan_guest_pull +- i_package_conan_guest_push +- i_package_container_guest_delete +- i_package_container_guest_pull +- i_package_container_guest_push +- i_package_debian_guest_delete +- i_package_debian_guest_pull +- i_package_debian_guest_push +- i_package_generic_guest_delete +- i_package_generic_guest_pull +- i_package_generic_guest_push +- i_package_golang_guest_delete +- i_package_golang_guest_pull +- i_package_golang_guest_push +- i_package_maven_guest_delete +- i_package_maven_guest_pull +- i_package_maven_guest_push +- i_package_npm_guest_delete +- i_package_npm_guest_pull +- i_package_npm_guest_push +- i_package_nuget_guest_delete +- i_package_nuget_guest_pull +- i_package_nuget_guest_push +- i_package_pypi_guest_delete +- i_package_pypi_guest_pull +- i_package_pypi_guest_push +- i_package_tag_guest_delete +- i_package_tag_guest_pull +- i_package_tag_guest_push diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 17dbe063104..ec87de01384 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -16900,6 +16900,18 @@ msgstr "" msgid "Members|2FA" msgstr "" +msgid "Members|Access granted, ascending" +msgstr "" + +msgid "Members|Access granted, descending" +msgstr "" + +msgid "Members|Account, ascending" +msgstr "" + +msgid "Members|Account, descending" +msgstr "" + msgid "Members|An error occurred while trying to enable LDAP override, please try again." msgstr "" @@ -16963,9 +16975,21 @@ msgstr "" msgid "Members|LDAP override enabled." msgstr "" +msgid "Members|Last sign-in, ascending" +msgstr "" + +msgid "Members|Last sign-in, descending" +msgstr "" + msgid "Members|Leave \"%{source}\"" msgstr "" +msgid "Members|Max role, ascending" +msgstr "" + +msgid "Members|Max role, descending" +msgstr "" + msgid "Members|Membership" msgstr "" diff --git a/spec/features/groups/members/sort_members_spec.rb b/spec/features/groups/members/sort_members_spec.rb index 74c736f6e2c..a7a6104a1f7 100644 --- a/spec/features/groups/members/sort_members_spec.rb +++ b/spec/features/groups/members/sort_members_spec.rb @@ -9,87 +9,167 @@ RSpec.describe 'Groups > Members > Sort members', :js do let(:developer) { create(:user, name: 'Mary Jane', last_sign_in_at: 5.days.ago) } let(:group) { create(:group) } - dropdown_toggle_selector = '[data-testid="user-sort-dropdown"] [data-testid="dropdown-toggle"]' - before do - stub_feature_flags(group_members_filtered_search: false) - create(:group_member, :owner, user: owner, group: group, created_at: 5.days.ago) create(:group_member, :developer, user: developer, group: group, created_at: 3.days.ago) sign_in(owner) end - it 'sorts alphabetically by default' do - visit_members_list(sort: nil) + context 'when `group_members_filtered_search` feature flag is enabled' do + dropdown_toggle_selector = '[data-testid="members-sort-dropdown"] > button' - expect(first_row.text).to include(owner.name) - expect(second_row.text).to include(developer.name) - expect(page).to have_css(dropdown_toggle_selector, text: 'Name, ascending') - end + it 'sorts account by default' do + visit_members_list(sort: nil) - it 'sorts by access level ascending' do - visit_members_list(sort: :access_level_asc) + expect(first_row.text).to include(owner.name) + expect(second_row.text).to include(developer.name) + expect(page).to have_css(dropdown_toggle_selector, text: 'Account, ascending') + end - expect(first_row.text).to include(developer.name) - expect(second_row.text).to include(owner.name) - expect(page).to have_css(dropdown_toggle_selector, text: 'Access level, ascending') - end + it 'sorts by max role ascending' do + visit_members_list(sort: :access_level_asc) - it 'sorts by access level descending' do - visit_members_list(sort: :access_level_desc) + expect(first_row.text).to include(developer.name) + expect(second_row.text).to include(owner.name) + expect(page).to have_css(dropdown_toggle_selector, text: 'Max role, ascending') + end - expect(first_row.text).to include(owner.name) - expect(second_row.text).to include(developer.name) - expect(page).to have_css(dropdown_toggle_selector, text: 'Access level, descending') - end + it 'sorts by max role descending' do + visit_members_list(sort: :access_level_desc) - it 'sorts by last joined' do - visit_members_list(sort: :last_joined) + expect(first_row.text).to include(owner.name) + expect(second_row.text).to include(developer.name) + expect(page).to have_css(dropdown_toggle_selector, text: 'Max role, descending') + end - expect(first_row.text).to include(developer.name) - expect(second_row.text).to include(owner.name) - expect(page).to have_css(dropdown_toggle_selector, text: 'Last joined') - end + it 'sorts by access granted ascending' do + visit_members_list(sort: :last_joined) - it 'sorts by oldest joined' do - visit_members_list(sort: :oldest_joined) + expect(first_row.text).to include(developer.name) + expect(second_row.text).to include(owner.name) + expect(page).to have_css(dropdown_toggle_selector, text: 'Access granted, ascending') + end - expect(first_row.text).to include(owner.name) - expect(second_row.text).to include(developer.name) - expect(page).to have_css(dropdown_toggle_selector, text: 'Oldest joined') - end + it 'sorts by access granted descending' do + visit_members_list(sort: :oldest_joined) - it 'sorts by name ascending' do - visit_members_list(sort: :name_asc) + expect(first_row.text).to include(owner.name) + expect(second_row.text).to include(developer.name) + expect(page).to have_css(dropdown_toggle_selector, text: 'Access granted, descending') + end - expect(first_row.text).to include(owner.name) - expect(second_row.text).to include(developer.name) - expect(page).to have_css(dropdown_toggle_selector, text: 'Name, ascending') - end + it 'sorts by account ascending' do + visit_members_list(sort: :name_asc) - it 'sorts by name descending' do - visit_members_list(sort: :name_desc) + expect(first_row.text).to include(owner.name) + expect(second_row.text).to include(developer.name) + expect(page).to have_css(dropdown_toggle_selector, text: 'Account, ascending') + end - expect(first_row.text).to include(developer.name) - expect(second_row.text).to include(owner.name) - expect(page).to have_css(dropdown_toggle_selector, text: 'Name, descending') - end + it 'sorts by account descending' do + visit_members_list(sort: :name_desc) + + expect(first_row.text).to include(developer.name) + expect(second_row.text).to include(owner.name) + expect(page).to have_css(dropdown_toggle_selector, text: 'Account, descending') + end + + it 'sorts by last sign-in ascending', :clean_gitlab_redis_shared_state do + visit_members_list(sort: :recent_sign_in) + + expect(first_row.text).to include(owner.name) + expect(second_row.text).to include(developer.name) + expect(page).to have_css(dropdown_toggle_selector, text: 'Last sign-in, ascending') + end - it 'sorts by recent sign in', :clean_gitlab_redis_shared_state do - visit_members_list(sort: :recent_sign_in) + it 'sorts by last sign-in descending', :clean_gitlab_redis_shared_state do + visit_members_list(sort: :oldest_sign_in) - expect(first_row.text).to include(owner.name) - expect(second_row.text).to include(developer.name) - expect(page).to have_css(dropdown_toggle_selector, text: 'Recent sign in') + expect(first_row.text).to include(developer.name) + expect(second_row.text).to include(owner.name) + expect(page).to have_css(dropdown_toggle_selector, text: 'Last sign-in, descending') + end end - it 'sorts by oldest sign in', :clean_gitlab_redis_shared_state do - visit_members_list(sort: :oldest_sign_in) + context 'when `group_members_filtered_search` feature flag is disabled' do + dropdown_toggle_selector = '[data-testid="user-sort-dropdown"] [data-testid="dropdown-toggle"]' + + before do + stub_feature_flags(group_members_filtered_search: false) + end + + it 'sorts alphabetically by default' do + visit_members_list(sort: nil) + + expect(first_row.text).to include(owner.name) + expect(second_row.text).to include(developer.name) + expect(page).to have_css(dropdown_toggle_selector, text: 'Name, ascending') + end + + it 'sorts by access level ascending' do + visit_members_list(sort: :access_level_asc) + + expect(first_row.text).to include(developer.name) + expect(second_row.text).to include(owner.name) + expect(page).to have_css(dropdown_toggle_selector, text: 'Access level, ascending') + end + + it 'sorts by access level descending' do + visit_members_list(sort: :access_level_desc) + + expect(first_row.text).to include(owner.name) + expect(second_row.text).to include(developer.name) + expect(page).to have_css(dropdown_toggle_selector, text: 'Access level, descending') + end + + it 'sorts by last joined' do + visit_members_list(sort: :last_joined) + + expect(first_row.text).to include(developer.name) + expect(second_row.text).to include(owner.name) + expect(page).to have_css(dropdown_toggle_selector, text: 'Last joined') + end + + it 'sorts by oldest joined' do + visit_members_list(sort: :oldest_joined) + + expect(first_row.text).to include(owner.name) + expect(second_row.text).to include(developer.name) + expect(page).to have_css(dropdown_toggle_selector, text: 'Oldest joined') + end + + it 'sorts by name ascending' do + visit_members_list(sort: :name_asc) + + expect(first_row.text).to include(owner.name) + expect(second_row.text).to include(developer.name) + expect(page).to have_css(dropdown_toggle_selector, text: 'Name, ascending') + end + + it 'sorts by name descending' do + visit_members_list(sort: :name_desc) + + expect(first_row.text).to include(developer.name) + expect(second_row.text).to include(owner.name) + expect(page).to have_css(dropdown_toggle_selector, text: 'Name, descending') + end + + it 'sorts by recent sign in', :clean_gitlab_redis_shared_state do + visit_members_list(sort: :recent_sign_in) + + expect(first_row.text).to include(owner.name) + expect(second_row.text).to include(developer.name) + expect(page).to have_css(dropdown_toggle_selector, text: 'Recent sign in') + end + + it 'sorts by oldest sign in', :clean_gitlab_redis_shared_state do + visit_members_list(sort: :oldest_sign_in) - expect(first_row.text).to include(developer.name) - expect(second_row.text).to include(owner.name) - expect(page).to have_css(dropdown_toggle_selector, text: 'Oldest sign in') + expect(first_row.text).to include(developer.name) + expect(second_row.text).to include(owner.name) + expect(page).to have_css(dropdown_toggle_selector, text: 'Oldest sign in') + end end def visit_members_list(sort:) diff --git a/spec/frontend/diffs/components/diff_file_row_spec.js b/spec/frontend/diffs/components/diff_file_row_spec.js index 23adc8f9da4..7403a7918a9 100644 --- a/spec/frontend/diffs/components/diff_file_row_spec.js +++ b/spec/frontend/diffs/components/diff_file_row_spec.js @@ -7,12 +7,9 @@ import ChangedFileIcon from '~/vue_shared/components/changed_file_icon.vue'; describe('Diff File Row component', () => { let wrapper; - const createComponent = (props = {}, highlightCurrentDiffRow = false) => { + const createComponent = (props = {}) => { wrapper = shallowMount(DiffFileRow, { propsData: { ...props }, - provide: { - glFeatures: { highlightCurrentDiffRow }, - }, }); }; @@ -60,26 +57,23 @@ describe('Diff File Row component', () => { }); it.each` - features | fileType | isViewed | expected - ${{ highlightCurrentDiffRow: true }} | ${'blob'} | ${false} | ${'gl-font-weight-bold'} - ${{}} | ${'blob'} | ${true} | ${''} - ${{}} | ${'tree'} | ${false} | ${''} - ${{}} | ${'tree'} | ${true} | ${''} + fileType | isViewed | expected + ${'blob'} | ${false} | ${'gl-font-weight-bold'} + ${'blob'} | ${true} | ${''} + ${'tree'} | ${false} | ${''} + ${'tree'} | ${true} | ${''} `( - 'with (features="$features", fileType="$fileType", isViewed=$isViewed), sets fileClasses="$expected"', - ({ features, fileType, isViewed, expected }) => { - createComponent( - { - file: { - type: fileType, - fileHash: '#123456789', - }, - level: 0, - hideFileStats: false, - viewedFiles: isViewed ? { '#123456789': true } : {}, + 'with (fileType="$fileType", isViewed=$isViewed), sets fileClasses="$expected"', + ({ fileType, isViewed, expected }) => { + createComponent({ + file: { + type: fileType, + fileHash: '#123456789', }, - features.highlightCurrentDiffRow, - ); + level: 0, + hideFileStats: false, + viewedFiles: isViewed ? { '#123456789': true } : {}, + }); expect(wrapper.find(FileRow).props('fileClasses')).toBe(expected); }, ); diff --git a/spec/frontend/members/components/filter_sort/filter_sort_container_spec.js b/spec/frontend/members/components/filter_sort/filter_sort_container_spec.js index 4abf9f50959..91277ae6d03 100644 --- a/spec/frontend/members/components/filter_sort/filter_sort_container_spec.js +++ b/spec/frontend/members/components/filter_sort/filter_sort_container_spec.js @@ -2,6 +2,7 @@ import { shallowMount, createLocalVue } from '@vue/test-utils'; import Vuex from 'vuex'; import FilterSortContainer from '~/members/components/filter_sort/filter_sort_container.vue'; import MembersFilteredSearchBar from '~/members/components/filter_sort/members_filtered_search_bar.vue'; +import SortDropdown from '~/members/components/filter_sort/sort_dropdown.vue'; const localVue = createLocalVue(); localVue.use(Vuex); @@ -19,6 +20,7 @@ describe('FilterSortContainer', () => { placeholder: 'Filter members', recentSearchesStorageKey: 'group_members', }, + tableSortableFields: ['account'], ...state, }, }); @@ -29,12 +31,13 @@ describe('FilterSortContainer', () => { }); }; - describe('when `filteredSearchBar.show` is `false`', () => { + describe('when `filteredSearchBar.show` is `false` and `tableSortableFields` is empty', () => { it('renders nothing', () => { createComponent({ filteredSearchBar: { show: false, }, + tableSortableFields: [], }); expect(wrapper.html()).toBe(''); @@ -52,4 +55,14 @@ describe('FilterSortContainer', () => { expect(wrapper.find(MembersFilteredSearchBar).exists()).toBe(true); }); }); + + describe('when `tableSortableFields` is set', () => { + it('renders `SortDropdown`', () => { + createComponent({ + tableSortableFields: ['account'], + }); + + expect(wrapper.find(SortDropdown).exists()).toBe(true); + }); + }); }); diff --git a/spec/frontend/members/components/filter_sort/sort_dropdown_spec.js b/spec/frontend/members/components/filter_sort/sort_dropdown_spec.js new file mode 100644 index 00000000000..22c1eea1653 --- /dev/null +++ b/spec/frontend/members/components/filter_sort/sort_dropdown_spec.js @@ -0,0 +1,135 @@ +import { mount, createLocalVue } from '@vue/test-utils'; +import { within } from '@testing-library/dom'; +import Vuex from 'vuex'; +import { GlDropdownItem } from '@gitlab/ui'; +import SortDropdown from '~/members/components/filter_sort/sort_dropdown.vue'; + +const localVue = createLocalVue(); +localVue.use(Vuex); + +describe('SortDropdown', () => { + let wrapper; + + const URL_HOST = 'https://localhost/'; + + const createComponent = state => { + const store = new Vuex.Store({ + state: { + sourceId: 1, + tableSortableFields: ['account', 'granted', 'expires', 'maxRole', 'lastSignIn'], + filteredSearchBar: { + show: true, + tokens: ['two_factor'], + searchParam: 'search', + placeholder: 'Filter members', + recentSearchesStorageKey: 'group_members', + }, + ...state, + }, + }); + + wrapper = mount(SortDropdown, { + localVue, + store, + }); + }; + + const findDropdownToggle = () => wrapper.find('button[aria-haspopup="true"]'); + const findDropdownItemByText = text => + wrapper + .findAll(GlDropdownItem) + .wrappers.find(dropdownItemWrapper => dropdownItemWrapper.text() === text); + + describe('dropdown options', () => { + beforeEach(() => { + delete window.location; + window.location = new URL(URL_HOST); + }); + + it('adds dropdown items for all the sortable fields', () => { + const URL_FILTER_PARAMS = '?two_factor=enabled&search=foobar'; + const EXPECTED_BASE_URL = `${URL_HOST}${URL_FILTER_PARAMS}&sort=`; + + window.location.search = URL_FILTER_PARAMS; + + const expectedDropdownItems = [ + { + label: 'Account, ascending', + url: `${EXPECTED_BASE_URL}name_asc`, + }, + { + label: 'Account, descending', + url: `${EXPECTED_BASE_URL}name_desc`, + }, + { + label: 'Access granted, ascending', + url: `${EXPECTED_BASE_URL}last_joined`, + }, + { + label: 'Access granted, descending', + url: `${EXPECTED_BASE_URL}oldest_joined`, + }, + { + label: 'Max role, ascending', + url: `${EXPECTED_BASE_URL}access_level_asc`, + }, + { + label: 'Max role, descending', + url: `${EXPECTED_BASE_URL}access_level_desc`, + }, + { + label: 'Last sign-in, ascending', + url: `${EXPECTED_BASE_URL}recent_sign_in`, + }, + { + label: 'Last sign-in, descending', + url: `${EXPECTED_BASE_URL}oldest_sign_in`, + }, + ]; + + createComponent(); + + expectedDropdownItems.forEach(expectedDropdownItem => { + const dropdownItem = findDropdownItemByText(expectedDropdownItem.label); + + expect(dropdownItem).not.toBe(null); + expect(dropdownItem.find('a').attributes('href')).toBe(expectedDropdownItem.url); + }); + }); + + it('checks selected sort option', () => { + window.location.search = '?sort=access_level_asc'; + + createComponent(); + + expect(findDropdownItemByText('Max role, ascending').props('isChecked')).toBe(true); + }); + }); + + describe('dropdown toggle', () => { + beforeEach(() => { + delete window.location; + window.location = new URL(URL_HOST); + }); + + it('defaults to sorting by "Account, ascending"', () => { + createComponent(); + + expect(findDropdownToggle().text()).toBe('Account, ascending'); + }); + + it('sets text as selected sort option', () => { + window.location.search = '?sort=access_level_asc'; + + createComponent(); + + expect(findDropdownToggle().text()).toBe('Max role, ascending'); + }); + }); + + it('renders dropdown label', () => { + createComponent(); + + expect(within(wrapper.element).queryByText('Sort by')).not.toBe(null); + }); +}); diff --git a/spec/frontend/members/utils_spec.js b/spec/frontend/members/utils_spec.js index a598b51d8dd..2dcd084b241 100644 --- a/spec/frontend/members/utils_spec.js +++ b/spec/frontend/members/utils_spec.js @@ -7,13 +7,17 @@ import { canResend, canUpdate, canOverride, + parseSortParam, + buildSortUrl, } from '~/members/utils'; +import { DEFAULT_SORT } from '~/members/constants'; import { member as memberMock, group, invite } from './mock_data'; const DIRECT_MEMBER_ID = 178; const INHERITED_MEMBER_ID = 179; const IS_CURRENT_USER_ID = 123; const IS_NOT_CURRENT_USER_ID = 124; +const URL_HOST = 'https://localhost/'; describe('Members Utils', () => { describe('generateBadges', () => { @@ -119,4 +123,110 @@ describe('Members Utils', () => { expect(canOverride(memberMock)).toBe(false); }); }); + + describe('parseSortParam', () => { + beforeEach(() => { + delete window.location; + window.location = new URL(URL_HOST); + }); + + describe('when `sort` param is not present', () => { + it('returns default sort options', () => { + window.location.search = ''; + + expect(parseSortParam(['account'])).toEqual(DEFAULT_SORT); + }); + }); + + describe('when field passed in `sortableFields` argument does not have `sort` key defined', () => { + it('returns default sort options', () => { + window.location.search = '?sort=source_asc'; + + expect(parseSortParam(['source'])).toEqual(DEFAULT_SORT); + }); + }); + + describe.each` + sortParam | expected + ${'name_asc'} | ${{ sortBy: 'account', sortDesc: false, sortByLabel: 'Account, ascending' }} + ${'name_desc'} | ${{ sortBy: 'account', sortDesc: true, sortByLabel: 'Account, descending' }} + ${'last_joined'} | ${{ sortBy: 'granted', sortDesc: false, sortByLabel: 'Access granted, ascending' }} + ${'oldest_joined'} | ${{ sortBy: 'granted', sortDesc: true, sortByLabel: 'Access granted, descending' }} + ${'access_level_asc'} | ${{ sortBy: 'maxRole', sortDesc: false, sortByLabel: 'Max role, ascending' }} + ${'access_level_desc'} | ${{ sortBy: 'maxRole', sortDesc: true, sortByLabel: 'Max role, descending' }} + ${'recent_sign_in'} | ${{ sortBy: 'lastSignIn', sortDesc: false, sortByLabel: 'Last sign-in, ascending' }} + ${'oldest_sign_in'} | ${{ sortBy: 'lastSignIn', sortDesc: true, sortByLabel: 'Last sign-in, descending' }} + `('when `sort` query string param is `$sortParam`', ({ sortParam, expected }) => { + it(`returns ${JSON.stringify(expected)}`, async () => { + window.location.search = `?sort=${sortParam}`; + + expect(parseSortParam(['account', 'granted', 'expires', 'maxRole', 'lastSignIn'])).toEqual( + expected, + ); + }); + }); + }); + + describe('buildSortUrl', () => { + beforeEach(() => { + delete window.location; + window.location = new URL(URL_HOST); + }); + + describe('when field passed in `sortBy` argument does not have `sort` key defined', () => { + it('returns an empty string', () => { + expect( + buildSortUrl({ + sortBy: 'source', + sortDesc: false, + filteredSearchBarTokens: [], + filteredSearchBarSearchParam: 'search', + }), + ).toBe(''); + }); + }); + + describe('when there are no filter params set', () => { + it('sets `sort` param', () => { + expect( + buildSortUrl({ + sortBy: 'account', + sortDesc: false, + filteredSearchBarTokens: [], + filteredSearchBarSearchParam: 'search', + }), + ).toBe(`${URL_HOST}?sort=name_asc`); + }); + }); + + describe('when filter params are set', () => { + it('merges the `sort` param with the filter params', () => { + window.location.search = '?two_factor=enabled&with_inherited_permissions=exclude'; + + expect( + buildSortUrl({ + sortBy: 'account', + sortDesc: false, + filteredSearchBarTokens: ['two_factor', 'with_inherited_permissions'], + filteredSearchBarSearchParam: 'search', + }), + ).toBe(`${URL_HOST}?two_factor=enabled&with_inherited_permissions=exclude&sort=name_asc`); + }); + }); + + describe('when search param is set', () => { + it('merges the `sort` param with the search param', () => { + window.location.search = '?search=foobar'; + + expect( + buildSortUrl({ + sortBy: 'account', + sortDesc: false, + filteredSearchBarTokens: ['two_factor', 'with_inherited_permissions'], + filteredSearchBarSearchParam: 'search', + }), + ).toBe(`${URL_HOST}?search=foobar&sort=name_asc`); + }); + }); + }); }); diff --git a/spec/helpers/members_helper_spec.rb b/spec/helpers/members_helper_spec.rb index 84b3f99b89a..c671379c4b4 100644 --- a/spec/helpers/members_helper_spec.rb +++ b/spec/helpers/members_helper_spec.rb @@ -33,6 +33,16 @@ RSpec.describe MembersHelper do expect(remove_member_message(group_member_invite)).to eq "Are you sure you want to remove this orphaned member from the #{group.name} group and any subresources?" end end + + context 'a pending member invitation with no user associated' do + before do + project_member_invite.update_columns(invite_email: "#{SecureRandom.hex}@example.com", invite_token: 'some-token', user_id: nil) + end + + it 'does not error when there is an invitation for the requestor' do + expect(remove_member_message(project_member_invite)).to eq "Are you sure you want to revoke the invitation for #{project_member_invite.invite_email} to join the #{project.full_name} project?" + end + end end describe '#remove_member_title' do diff --git a/spec/lib/gitlab/middleware/handle_malformed_strings_spec.rb b/spec/lib/gitlab/middleware/handle_malformed_strings_spec.rb index e806f6478b7..cf7b0dbb5fd 100644 --- a/spec/lib/gitlab/middleware/handle_malformed_strings_spec.rb +++ b/spec/lib/gitlab/middleware/handle_malformed_strings_spec.rb @@ -1,5 +1,4 @@ # frozen_string_literal: true - require 'spec_helper' require "rack/test" @@ -104,6 +103,12 @@ RSpec.describe Gitlab::Middleware::HandleMalformedStrings do expect(subject.call(env)).not_to eq error_400 end + + it 'does not reject correct encoded password with special characters' do + env = env_for.merge(auth_env("username", "RçKszEwéC5kFnû∆f243fycGu§Gh9ftDj!U", nil)) + + expect(subject.call(env)).not_to eq error_400 + end end context 'in params' do diff --git a/spec/mailers/emails/projects_spec.rb b/spec/mailers/emails/projects_spec.rb index 6c23625d4a3..a1f19a972f1 100644 --- a/spec/mailers/emails/projects_spec.rb +++ b/spec/mailers/emails/projects_spec.rb @@ -19,11 +19,9 @@ RSpec.describe Emails::Projects do create(:project_incident_management_setting, project: project, create_issue: true) end - let(:incident_issues_url) do - project_issues_url(project, label_name: 'incident') - end + let(:incidents_url) { project_incidents_url(project) } - it { is_expected.to have_body_text(incident_issues_url) } + it { is_expected.to have_body_text(incidents_url) } end end diff --git a/spec/requests/api/invitations_spec.rb b/spec/requests/api/invitations_spec.rb index 75586970abb..aeb8e3642ed 100644 --- a/spec/requests/api/invitations_spec.rb +++ b/spec/requests/api/invitations_spec.rb @@ -58,7 +58,7 @@ RSpec.describe API::Invitations do it 'does not transform the requester into a proper member' do expect do post api("/#{source_type.pluralize}/#{source.id}/invitations", maintainer), - params: { email: email, access_level: Member::MAINTAINER } + params: { email: access_requester.email, access_level: Member::MAINTAINER } expect(response).to have_gitlab_http_status(:created) end.not_to change { source.members.count } @@ -71,7 +71,7 @@ RSpec.describe API::Invitations do params: { email: email, access_level: Member::DEVELOPER } expect(response).to have_gitlab_http_status(:created) - end.to change { source.requesters.count }.by(1) + end.to change { source.members.invite.count }.by(1) end it 'invites a list of new email addresses' do @@ -82,7 +82,7 @@ RSpec.describe API::Invitations do params: { email: email_list, access_level: Member::DEVELOPER } expect(response).to have_gitlab_http_status(:created) - end.to change { source.requesters.count }.by(2) + end.to change { source.members.invite.count }.by(2) end end @@ -140,7 +140,7 @@ RSpec.describe API::Invitations do it 'invites a member' do expect do subject - end.to change { source.requesters.count }.by(1) + end.to change { source.members.invite.count }.by(1) expect(response).to have_gitlab_http_status(:created) end diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb index 479edf5e873..bc89dc2fa77 100644 --- a/spec/requests/git_http_spec.rb +++ b/spec/requests/git_http_spec.rb @@ -280,6 +280,20 @@ RSpec.describe 'Git HTTP requests' do project.add_developer(user) end + context 'when user is using credentials with special characters' do + context 'with password with special characters' do + before do + user.update!(password: 'RKszEwéC5kFnû∆f243fycGu§Gh9ftDj!U') + end + + it 'allows clones' do + download(path, user: user.username, password: user.password) do |response| + expect(response).to have_gitlab_http_status(:ok) + end + end + end + end + context 'but the repo is disabled' do let(:project) { create(:project, :wiki_repo, :private, :repository_disabled, :wiki_enabled) } diff --git a/spec/services/incident_management/incidents/create_service_spec.rb b/spec/services/incident_management/incidents/create_service_spec.rb index 1330f3ae033..4601bd807d0 100644 --- a/spec/services/incident_management/incidents/create_service_spec.rb +++ b/spec/services/incident_management/incidents/create_service_spec.rb @@ -37,6 +37,8 @@ RSpec.describe IncidentManagement::Incidents::CreateService do end let(:issue) { new_issue } + + include_examples 'has incident label' end context 'with default severity' do diff --git a/spec/services/issues/create_service_spec.rb b/spec/services/issues/create_service_spec.rb index eeac7fb9923..cc6a49fc4cf 100644 --- a/spec/services/issues/create_service_spec.rb +++ b/spec/services/issues/create_service_spec.rb @@ -63,6 +63,7 @@ RSpec.describe Issues::CreateService do subject { issue } it_behaves_like 'incident issue' + it_behaves_like 'has incident label' it_behaves_like 'an incident management tracked event', :incident_management_incident_created it 'does create an incident label' do diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb index 55d6d5cfe2d..06a6a52bc41 100644 --- a/spec/services/issues/update_service_spec.rb +++ b/spec/services/issues/update_service_spec.rb @@ -99,31 +99,18 @@ RSpec.describe Issues::UpdateService, :mailer do context 'when issue type is incident' do let(:issue) { create(:incident, project: project) } - it 'changes updates the severity' do + before do update_issue(opts) - - expect(issue.severity).to eq('low') - end - - it_behaves_like 'incident issue' do - before do - update_issue(opts) - end end - context 'with existing incident label' do - let_it_be(:incident_label) { create(:label, :incident, project: project) } + it_behaves_like 'incident issue' - before do - opts.delete(:label_ids) # don't override but retain existing labels - issue.labels << incident_label - end + it 'changes updates the severity' do + expect(issue.severity).to eq('low') + end - it_behaves_like 'incident issue' do - before do - update_issue(opts) - end - end + it 'does not apply incident labels' do + expect(issue.labels).to match_array [label] end end @@ -155,7 +142,6 @@ RSpec.describe Issues::UpdateService, :mailer do context 'issue in incident type' do let(:current_user) { user } - let(:incident_label_attributes) { attributes_for(:label, :incident) } before do opts.merge!(issue_type: 'incident', confidential: true) @@ -170,21 +156,6 @@ RSpec.describe Issues::UpdateService, :mailer do subject end end - - it 'does create an incident label' do - expect { subject } - .to change { Label.where(incident_label_attributes).count }.by(1) - end - - context 'when invalid' do - before do - opts.merge!(title: '') - end - - it 'does not create an incident label prematurely' do - expect { subject }.not_to change(Label, :count) - end - end end it 'updates open issue counter for assignees when issue is reassigned' do diff --git a/spec/services/members/invite_service_spec.rb b/spec/services/members/invite_service_spec.rb index 12a1a54696b..08cdf0d3ae1 100644 --- a/spec/services/members/invite_service_spec.rb +++ b/spec/services/members/invite_service_spec.rb @@ -63,4 +63,15 @@ RSpec.describe Members::InviteService do expect(result[:status]).to eq(:error) expect(result[:message][invited_member.invite_email]).to eq("Member already invited to #{project.name}") end + + it 'does not add a member with an access_request' do + requested_member = create(:project_member, :access_request, project: project) + + params = { email: requested_member.user.email, + access_level: Gitlab::Access::GUEST } + result = described_class.new(user, params).execute(project) + + expect(result[:status]).to eq(:error) + expect(result[:message][requested_member.user.email]).to eq("Member cannot be invited because they already requested to join #{project.name}") + end end diff --git a/spec/services/packages/create_event_service_spec.rb b/spec/services/packages/create_event_service_spec.rb index 4db7687bb24..f581d704087 100644 --- a/spec/services/packages/create_event_service_spec.rb +++ b/spec/services/packages/create_event_service_spec.rb @@ -70,12 +70,34 @@ RSpec.describe Packages::CreateEventService do end it 'tracks the event' do + expect(::Gitlab::UsageDataCounters::GuestPackageEventCounter).not_to receive(:count) expect(::Gitlab::UsageDataCounters::HLLRedisCounter).to receive(:track_event).with(user.id, Packages::Event.allowed_event_name(expected_scope, event_name, originator_type)) subject end end + shared_examples 'redis package guest event creation' do |originator_type, expected_scope| + context 'with feature flag disabled' do + before do + stub_feature_flags(collect_package_events_redis: false) + end + + it 'does not track the event' do + expect(::Gitlab::UsageDataCounters::GuestPackageEventCounter).not_to receive(:count) + + subject + end + end + + it 'tracks the event' do + expect(::Gitlab::UsageDataCounters::HLLRedisCounter).not_to receive(:track_event) + expect(::Gitlab::UsageDataCounters::GuestPackageEventCounter).to receive(:count).with(Packages::Event.allowed_event_name(expected_scope, event_name, originator_type)) + + subject + end + end + context 'with a user' do let(:user) { create(:user) } @@ -94,6 +116,7 @@ RSpec.describe Packages::CreateEventService do let(:user) { nil } it_behaves_like 'db package event creation', 'guest', 'container' + it_behaves_like 'redis package guest event creation', 'guest', 'container' end context 'with a package as scope' do @@ -103,6 +126,7 @@ RSpec.describe Packages::CreateEventService do let(:user) { nil } it_behaves_like 'db package event creation', 'guest', 'npm' + it_behaves_like 'redis package guest event creation', 'guest', 'npm' end context 'with user' do diff --git a/spec/support/shared_examples/services/incident_shared_examples.rb b/spec/support/shared_examples/services/incident_shared_examples.rb index 39c22ac8aa3..9fced12b543 100644 --- a/spec/support/shared_examples/services/incident_shared_examples.rb +++ b/spec/support/shared_examples/services/incident_shared_examples.rb @@ -11,11 +11,13 @@ # # include_examples 'incident issue' RSpec.shared_examples 'incident issue' do - let(:label_properties) { attributes_for(:label, :incident) } - it 'has incident as issue type' do expect(issue.issue_type).to eq('incident') end +end + +RSpec.shared_examples 'has incident label' do + let(:label_properties) { attributes_for(:label, :incident) } it 'has exactly one incident label' do expect(issue.labels).to be_one do |label| diff --git a/spec/tasks/gitlab/packages/events_rake_spec.rb b/spec/tasks/gitlab/packages/events_rake_spec.rb index 2c3885855be..a485dc2ce58 100644 --- a/spec/tasks/gitlab/packages/events_rake_spec.rb +++ b/spec/tasks/gitlab/packages/events_rake_spec.rb @@ -27,14 +27,14 @@ RSpec.describe 'gitlab:packages:events namespace rake task' do end Packages::Event::EVENT_SCOPES.keys.each do |event_scope| - it "includes includes `#{event_scope}` scope" do + it "includes `#{event_scope}` scope" do expect(subject.find { |event| event['name'].include?(event_scope) }).not_to be_nil end end it 'excludes some event types' do - expect(subject.find { |event| event['name'].include?("search_package") }).to be_nil - expect(subject.find { |event| event['name'].include?("list_package") }).to be_nil + expect(subject.grep(/search_package/)).to be_empty + expect(subject.grep(/list_package/)).to be_empty end end @@ -42,7 +42,7 @@ RSpec.describe 'gitlab:packages:events namespace rake task' do let(:task) { 'generate_guest' } Packages::Event::EVENT_SCOPES.keys.each do |event_scope| - it "includes includes `#{event_scope}` scope" do + it "includes `#{event_scope}` scope" do expect(subject.find { |event| event.include?(event_scope) }).not_to be_nil end end |