Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-05-28 09:08:05 +00:00
parent 616129d41c
commit 4c788f43cb
66 changed files with 1714 additions and 284 deletions

View file

@ -13,3 +13,6 @@ indent_size = 2
[*.{js,json,vue,scss,rb,haml,yml,md}]
indent_style = space
charset = utf-8
[*.{md,markdown}]
trim_trailing_whitespace = false

View file

@ -83,13 +83,7 @@ class List {
}
destroy() {
const index = boardsStore.state.lists.indexOf(this);
boardsStore.state.lists.splice(index, 1);
boardsStore.updateNewListDropdown(this.id);
boardsStore.destroyList(this.id).catch(() => {
// TODO: handle request error
});
boardsStore.destroy(this);
}
update() {

View file

@ -547,6 +547,15 @@ const boardsStore = {
destroyList(id) {
return axios.delete(`${this.state.endpoints.listsEndpoint}/${id}`);
},
destroy(list) {
const index = this.state.lists.indexOf(list);
this.state.lists.splice(index, 1);
this.updateNewListDropdown(list.id);
this.destroyList(list.id).catch(() => {
// TODO: handle request error
});
},
saveList(list) {
const entity = list.label || list.assignee || list.milestone;

View file

@ -1,3 +1,10 @@
export const dateFormatMask = 'mmm dd HH:MM:ss.l';
export const TOKEN_TYPE_POD_NAME = 'TOKEN_TYPE_POD_NAME';
export const tracking = {
USED_SEARCH_BAR: 'used_search_bar',
POD_LOG_CHANGED: 'pod_log_changed',
TIME_RANGE_SET: 'time_range_set',
ENVIRONMENT_SELECTED: 'environment_selected',
};

View file

@ -0,0 +1,18 @@
import Tracking from '~/tracking';
/**
* The value of 1 in count, means there was one action performed
* related to the tracked action, in either of the following categories
* 1. Refreshing the logs
* 2. Select an environment
* 3. Change the time range
* 4. Use the search bar
*/
const trackLogs = label =>
Tracking.event(document.body.dataset.page, 'logs_view', {
label,
property: 'count',
value: 1,
});
export default trackLogs;

View file

@ -2,7 +2,8 @@ import { backOff } from '~/lib/utils/common_utils';
import httpStatusCodes from '~/lib/utils/http_status';
import axios from '~/lib/utils/axios_utils';
import { convertToFixedRange } from '~/lib/utils/datetime_range';
import { TOKEN_TYPE_POD_NAME } from '../constants';
import { TOKEN_TYPE_POD_NAME, tracking } from '../constants';
import trackLogs from '../logs_tracking_helper';
import * as types from './mutation_types';
@ -81,22 +82,22 @@ export const showFilteredLogs = ({ dispatch, commit }, filters = []) => {
commit(types.SET_CURRENT_POD_NAME, podName);
commit(types.SET_SEARCH, search);
dispatch('fetchLogs');
dispatch('fetchLogs', tracking.USED_SEARCH_BAR);
};
export const showPodLogs = ({ dispatch, commit }, podName) => {
commit(types.SET_CURRENT_POD_NAME, podName);
dispatch('fetchLogs');
dispatch('fetchLogs', tracking.POD_LOG_CHANGED);
};
export const setTimeRange = ({ dispatch, commit }, timeRange) => {
commit(types.SET_TIME_RANGE, timeRange);
dispatch('fetchLogs');
dispatch('fetchLogs', tracking.TIME_RANGE_SET);
};
export const showEnvironment = ({ dispatch, commit }, environmentName) => {
commit(types.SET_PROJECT_ENVIRONMENT, environmentName);
dispatch('fetchLogs');
dispatch('fetchLogs', tracking.ENVIRONMENT_SELECTED);
};
/**
@ -111,19 +112,22 @@ export const fetchEnvironments = ({ commit, dispatch }, environmentsPath) => {
.get(environmentsPath)
.then(({ data }) => {
commit(types.RECEIVE_ENVIRONMENTS_DATA_SUCCESS, data.environments);
dispatch('fetchLogs');
dispatch('fetchLogs', tracking.ENVIRONMENT_SELECTED);
})
.catch(() => {
commit(types.RECEIVE_ENVIRONMENTS_DATA_ERROR);
});
};
export const fetchLogs = ({ commit, state }) => {
export const fetchLogs = ({ commit, state }, trackingLabel) => {
commit(types.REQUEST_LOGS_DATA);
return requestLogsUntilData({ commit, state })
.then(({ data }) => {
const { pod_name, pods, logs, cursor } = data;
if (logs && logs.length > 0) {
trackLogs(trackingLabel);
}
commit(types.RECEIVE_LOGS_DATA_SUCCESS, { logs, cursor });
commit(types.SET_CURRENT_POD_NAME, pod_name);
commit(types.RECEIVE_PODS_DATA_SUCCESS, pods);

View file

@ -1,24 +1,12 @@
<script>
import { GlPagination, GlTooltipDirective, GlDeprecatedButton, GlIcon } from '@gitlab/ui';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import {
ASYNC_DELETE_IMAGE_ERROR_MESSAGE,
LIST_DELETE_BUTTON_DISABLED,
REMOVE_REPOSITORY_LABEL,
ROW_SCHEDULED_FOR_DELETION,
} from '../constants';
import { GlPagination } from '@gitlab/ui';
import ImageListRow from './image_list_row.vue';
export default {
name: 'ImageList',
components: {
GlPagination,
ClipboardButton,
GlDeprecatedButton,
GlIcon,
},
directives: {
GlTooltip: GlTooltipDirective,
ImageListRow,
},
props: {
images: {
@ -30,12 +18,6 @@ export default {
required: true,
},
},
i18n: {
LIST_DELETE_BUTTON_DISABLED,
REMOVE_REPOSITORY_LABEL,
ROW_SCHEDULED_FOR_DELETION,
ASYNC_DELETE_IMAGE_ERROR_MESSAGE,
},
computed: {
currentPage: {
get() {
@ -46,79 +28,25 @@ export default {
},
},
},
methods: {
encodeListItem(item) {
const params = JSON.stringify({ name: item.path, tags_path: item.tags_path, id: item.id });
return window.btoa(params);
},
},
};
</script>
<template>
<div class="gl-display-flex gl-flex-direction-column">
<div
<image-list-row
v-for="(listItem, index) in images"
:key="index"
v-gl-tooltip="{
placement: 'left',
disabled: !listItem.deleting,
title: $options.i18n.ROW_SCHEDULED_FOR_DELETION,
}"
data-testid="rowItem"
>
<div
class="gl-display-flex gl-justify-content-space-between gl-align-items-center gl-py-2 gl-px-1 border-bottom"
:class="{ 'border-top': index === 0, 'disabled-content': listItem.deleting }"
>
<div class="gl-display-flex gl-align-items-center">
<router-link
data-testid="detailsLink"
:to="{ name: 'details', params: { id: encodeListItem(listItem) } }"
>
{{ listItem.path }}
</router-link>
<clipboard-button
v-if="listItem.location"
:disabled="listItem.deleting"
:text="listItem.location"
:title="listItem.location"
css-class="btn-default btn-transparent btn-clipboard"
/>
<gl-icon
v-if="listItem.failedDelete"
v-gl-tooltip
:title="$options.i18n.ASYNC_DELETE_IMAGE_ERROR_MESSAGE"
name="warning"
class="text-warning align-middle"
/>
</div>
<div
v-gl-tooltip="{ disabled: listItem.destroy_path }"
class="d-none d-sm-block"
:title="$options.i18n.LIST_DELETE_BUTTON_DISABLED"
>
<gl-deprecated-button
v-gl-tooltip
data-testid="deleteImageButton"
:disabled="!listItem.destroy_path || listItem.deleting"
:title="$options.i18n.REMOVE_REPOSITORY_LABEL"
:aria-label="$options.i18n.REMOVE_REPOSITORY_LABEL"
class="btn-inverted"
variant="danger"
@click="$emit('delete', listItem)"
>
<gl-icon name="remove" />
</gl-deprecated-button>
</div>
</div>
</div>
:item="listItem"
:show-top-border="index === 0"
@delete="$emit('delete', $event)"
/>
<gl-pagination
v-model="currentPage"
:per-page="pagination.perPage"
:total-items="pagination.total"
align="center"
class="w-100 gl-mt-2"
class="w-100 gl-mt-3"
/>
</div>
</template>

View file

@ -0,0 +1,136 @@
<script>
import { GlTooltipDirective, GlButton, GlIcon, GlSprintf } from '@gitlab/ui';
import { n__ } from '~/locale';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import {
ASYNC_DELETE_IMAGE_ERROR_MESSAGE,
LIST_DELETE_BUTTON_DISABLED,
REMOVE_REPOSITORY_LABEL,
ROW_SCHEDULED_FOR_DELETION,
} from '../constants';
export default {
name: 'ImageListrow',
components: {
ClipboardButton,
GlButton,
GlSprintf,
GlIcon,
},
directives: {
GlTooltip: GlTooltipDirective,
},
props: {
item: {
type: Object,
required: true,
},
showTopBorder: {
type: Boolean,
default: false,
required: false,
},
},
i18n: {
LIST_DELETE_BUTTON_DISABLED,
REMOVE_REPOSITORY_LABEL,
ROW_SCHEDULED_FOR_DELETION,
ASYNC_DELETE_IMAGE_ERROR_MESSAGE,
},
computed: {
encodedItem() {
const params = JSON.stringify({
name: this.item.path,
tags_path: this.item.tags_path,
id: this.item.id,
});
return window.btoa(params);
},
disabledDelete() {
return !this.item.destroy_path || this.item.deleting;
},
tagsCountText() {
return n__(
'ContainerRegistry|%{count} Tag',
'ContainerRegistry|%{count} Tags',
this.item.tags_count,
);
},
},
};
</script>
<template>
<div
v-gl-tooltip="{
placement: 'left',
disabled: !item.deleting,
title: $options.i18n.ROW_SCHEDULED_FOR_DELETION,
}"
>
<div
class="gl-display-flex gl-justify-content-space-between gl-align-items-center gl-py-2 gl-px-1 gl-border-gray-200 gl-border-b-solid gl-border-b-1 gl-py-4 "
:class="{
'gl-border-t-solid gl-border-t-1': showTopBorder,
'disabled-content': item.deleting,
}"
>
<div class="gl-display-flex gl-flex-direction-column">
<div class="gl-display-flex gl-align-items-center">
<router-link
class="gl-text-black-normal gl-font-weight-bold"
data-testid="detailsLink"
:to="{ name: 'details', params: { id: encodedItem } }"
>
{{ item.path }}
</router-link>
<clipboard-button
v-if="item.location"
:disabled="item.deleting"
:text="item.location"
:title="item.location"
css-class="btn-default btn-transparent btn-clipboard gl-text-gray-500"
/>
<gl-icon
v-if="item.failedDelete"
v-gl-tooltip
:title="$options.i18n.ASYNC_DELETE_IMAGE_ERROR_MESSAGE"
name="warning"
class="text-warning"
/>
</div>
<div class="gl-font-sm gl-text-gray-500">
<span class="gl-display-flex gl-align-items-center" data-testid="tagsCount">
<gl-icon name="tag" class="gl-mr-2" />
<gl-sprintf :message="tagsCountText">
<template #count>
{{ item.tags_count }}
</template>
</gl-sprintf>
</span>
</div>
</div>
<div
v-gl-tooltip="{
disabled: item.destroy_path,
title: $options.i18n.LIST_DELETE_BUTTON_DISABLED,
}"
class="d-none d-sm-block"
data-testid="deleteButtonWrapper"
>
<gl-button
v-gl-tooltip
data-testid="deleteImageButton"
:disabled="disabledDelete"
:title="$options.i18n.REMOVE_REPOSITORY_LABEL"
:aria-label="$options.i18n.REMOVE_REPOSITORY_LABEL"
class="btn-inverted"
variant="danger"
icon="remove"
@click="$emit('delete', item)"
/>
</div>
</div>
</div>
</template>

View file

@ -42,7 +42,6 @@
}
.broadcast-message-dismiss {
height: 100%;
color: $gray-800;
}
}

View file

@ -241,7 +241,8 @@ class Admin::UsersController < Admin::ApplicationController
:theme_id,
:twitter,
:username,
:website_url
:website_url,
:note
]
end

View file

@ -162,8 +162,13 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic
def renderable_notes
define_diff_comment_vars unless @notes
@notes
draft_notes =
if current_user
merge_request.draft_notes.authored_by(current_user)
else
[]
end
@notes.concat(draft_notes)
end
end
Projects::MergeRequests::DiffsController.prepend_if_ee('EE::Projects::MergeRequests::DiffsController')

View file

@ -0,0 +1,129 @@
# frozen_string_literal: true
class Projects::MergeRequests::DraftsController < Projects::MergeRequests::ApplicationController
include Gitlab::Utils::StrongMemoize
respond_to :json
before_action :authorize_create_note!, only: [:create, :publish]
before_action :authorize_admin_draft!, only: [:update, :destroy]
before_action :authorize_admin_draft!, if: -> { action_name == 'publish' && params[:id].present? }
def index
drafts = prepare_notes_for_rendering(draft_notes)
render json: DraftNoteSerializer.new(current_user: current_user).represent(drafts)
end
def create
create_params = draft_note_params.merge(in_reply_to_discussion_id: params[:in_reply_to_discussion_id])
create_service = DraftNotes::CreateService.new(merge_request, current_user, create_params)
draft_note = create_service.execute
prepare_notes_for_rendering(draft_note)
render json: DraftNoteSerializer.new(current_user: current_user).represent(draft_note)
end
def update
draft_note.update!(draft_note_params)
prepare_notes_for_rendering(draft_note)
render json: DraftNoteSerializer.new(current_user: current_user).represent(draft_note)
end
def destroy
DraftNotes::DestroyService.new(merge_request, current_user).execute(draft_note)
head :ok
end
def publish
result = DraftNotes::PublishService.new(merge_request, current_user).execute(draft_note(allow_nil: true))
if result[:status] == :success
head :ok
else
render json: { message: result[:message] }, status: result[:status]
end
end
def discard
DraftNotes::DestroyService.new(merge_request, current_user).execute
head :ok
end
private
def draft_note(allow_nil: false)
strong_memoize(:draft_note) do
draft_notes.find(params[:id])
end
rescue ActiveRecord::RecordNotFound => ex
# draft_note is allowed to be nil in #publish
raise ex unless allow_nil
end
def draft_notes
return unless current_user
strong_memoize(:draft_notes) do
merge_request.draft_notes.authored_by(current_user)
end
end
# rubocop: disable CodeReuse/ActiveRecord
def merge_request
@merge_request ||= MergeRequestsFinder.new(current_user, project_id: @project.id).find_by!(iid: params[:merge_request_id])
end
# rubocop: enable CodeReuse/ActiveRecord
def draft_note_params
params.require(:draft_note).permit(
:commit_id,
:note,
:position,
:resolve_discussion
).tap do |h|
# Old FE version will still be sending `draft_note[commit_id]` as 'undefined'.
# That can result to having a note linked to a commit with 'undefined' ID
# which is non-existent.
h[:commit_id] = nil if h[:commit_id] == 'undefined'
end
end
def prepare_notes_for_rendering(notes)
return [] unless notes
notes = Array.wrap(notes)
# Preload author and access-level information
DraftNote.preload_author(notes)
user_ids = notes.map(&:author_id)
project.team.max_member_access_for_user_ids(user_ids)
notes.map(&method(:render_draft_note))
end
def render_draft_note(note)
params = { target_id: merge_request.id, target_type: 'MergeRequest', text: note.note }
result = PreviewMarkdownService.new(@project, current_user, params).execute
markdown_params = { markdown_engine: result[:markdown_engine], issuable_state_filter_enabled: true }
note.rendered_note = view_context.markdown(result[:text], markdown_params)
note.users_referenced = result[:users]
note.commands_changes = view_context.markdown(result[:commands])
note
end
def authorize_admin_draft!
access_denied! unless can?(current_user, :admin_note, draft_note)
end
def authorize_create_note!
access_denied! unless can?(current_user, :create_note, merge_request)
end
end

View file

@ -61,8 +61,8 @@ module NotesHelper
class: 'add-diff-note js-add-diff-note-button',
type: 'submit', name: 'button',
data: diff_view_line_data(line_code, position, line_type),
title: 'Add a comment to this line' do
icon('comment-o')
title: _('Add a comment to this line') do
sprite_icon('comment', size: 12)
end
end

View file

@ -0,0 +1,13 @@
# frozen_string_literal: true
class DraftNotePolicy < BasePolicy
delegate { @subject.merge_request }
condition(:is_author) { @user && @subject.author == @user }
rule { is_author }.policy do
enable :read_note
enable :admin_note
enable :resolve_note
end
end

View file

@ -0,0 +1,39 @@
# frozen_string_literal: true
class DraftNoteEntity < Grape::Entity
include RequestAwareEntity
expose :id
expose :author, using: NoteUserEntity
expose :merge_request_id
expose :position, if: -> (note, _) { note.on_diff? }
expose :line_code
expose :file_identifier_hash
expose :file_hash
expose :file_path
expose :note
expose :rendered_note, as: :note_html
expose :references
expose :discussion_id
expose :resolve_discussion
expose :noteable_type
expose :current_user do
expose :can_edit do |note|
can?(current_user, :admin_note, note)
end
expose :can_award_emoji do |note|
note.emoji_awardable?
end
expose :can_resolve do |note|
note.resolvable? && can?(current_user, :resolve_note, note)
end
end
private
def current_user
request.current_user
end
end

View file

@ -0,0 +1,4 @@
# frozen_string_literal: true
class DraftNoteSerializer < BaseSerializer
entity DraftNoteEntity
end

View file

@ -82,7 +82,8 @@ module Users
:organization,
:location,
:public_email,
:user_type
:user_type,
:note
]
end

View file

@ -0,0 +1,7 @@
%fieldset
%legend= _('Admin notes')
.form-group.row
.col-sm-2.col-form-label.text-right
= f.label :note, s_('AdminNote|Note')
.col-sm-10
= f.text_area :note, class: 'form-control'

View file

@ -83,7 +83,7 @@
.col-sm-10
= f.text_field :website_url, class: 'form-control'
= render_if_exists 'admin/users/admin_notes', f: f
= render 'admin/users/admin_notes', f: f
.form-actions
- if @user.new_record?

View file

@ -6,7 +6,7 @@
= image_tag avatar_icon_for_user(user), class: 'avatar s16 d-xs-flex d-md-none mr-1 gl-mt-2', alt: _('Avatar for %{name}') % { name: sanitize_name(user.name) }
= link_to user.name, admin_user_path(user), class: 'text-plain js-user-link', data: { user_id: user.id, qa_selector: 'username_link' }
= render_if_exists 'admin/users/user_listing_note', user: user
= render 'admin/users/user_listing_note', user: user
- user_badges_in_admin_section(user).each do |badge|
- css_badge = "badge badge-#{badge[:variant]}" if badge[:variant].present?

View file

@ -0,0 +1,7 @@
- if @user.note.present?
- text = @user.note
.card.border-info
.card-header.bg-info.text-white
= _('Admin Note')
.card-body
%p= text

View file

@ -0,0 +1,3 @@
- if user.note.present?
%span.has-tooltip.user-note{ title: user.note }
= icon("sticky-note-o cgrey")

View file

@ -154,7 +154,7 @@
%br
= link_to 'Confirm user', confirm_admin_user_path(@user), method: :put, class: "btn btn-info", data: { confirm: 'Are you sure?', qa_selector: 'confirm_user_button' }
= render_if_exists 'admin/users/user_detail_note'
= render 'admin/users/user_detail_note'
- if @user.deactivated?
.card.border-info

View file

@ -22,8 +22,8 @@
.header-action-buttons
- if defined?(@notes_count) && @notes_count > 0
%span.btn.disabled.btn-grouped.d-none.d-sm-block.append-right-10
= icon('comment')
%span.btn.disabled.btn-grouped.d-none.d-sm-block.append-right-10.has-tooltip{ title: n_("%d comment on this commit", "%d comments on this commit", @notes_count) % @notes_count }
= sprite_icon('comment')
= @notes_count
= link_to project_tree_path(@project, @commit), class: "btn btn-default append-right-10 d-none d-sm-none d-md-inline" do
#{ _('Browse files') }

View file

@ -14,7 +14,7 @@
.file-actions.d-none.d-sm-block
- if blob&.readable_text?
= link_to '#', class: 'js-toggle-diff-comments btn active has-tooltip', title: _("Toggle comments for this file"), disabled: @diff_notes_disabled do
= icon('comment')
= sprite_icon('comment', size: 16)
\
- if editable_diff?(diff_file)
- link_opts = @merge_request.persisted? ? { from_merge_request_iid: @merge_request.iid } : {}

View file

@ -1,4 +1,4 @@
- page_title _('CI / CD Charts')
- page_title _('CI / CD Analytics')
#js-project-pipelines-charts-app{ data: { counts: @counts, success_ratio: success_ratio(@counts),
times_chart: { labels: @charts[:pipeline_times].labels, values: @charts[:pipeline_times].pipeline_times },

View file

@ -19,14 +19,11 @@ class NewNoteWorker # rubocop:disable Scalability/IdempotentWorker
Gitlab::AppLogger.error("NewNoteWorker: couldn't find note with ID=#{note_id}, skipping job")
end
end
# rubocop: enable CodeReuse/ActiveRecord
private
# EE-only method
def skip_notification?(note)
false
note.review.present?
end
# rubocop: enable CodeReuse/ActiveRecord
end
NewNoteWorker.prepend_if_ee('EE::NewNoteWorker')

View file

@ -0,0 +1,5 @@
---
title: Include tag count in the image repository list
merge_request: 33027
author:
type: changed

View file

@ -0,0 +1,5 @@
---
title: Add note to ECS CI template
merge_request: 32597
author:
type: added

View file

@ -0,0 +1,5 @@
---
title: Move review related controllers/workers outside EE
merge_request: 32663
author:
type: changed

View file

@ -0,0 +1,5 @@
---
title: Remove destroy function logic from list model
merge_request: 32237
author: nuwe1
type: other

View file

@ -0,0 +1,5 @@
---
title: Use sprites for comment icons on Commits
merge_request: 31696
author:
type: changed

View file

@ -0,0 +1,5 @@
---
title: Add snowplow tracking for logs page
merge_request: 32704
author:
type: other

View file

@ -0,0 +1,5 @@
---
title: Move broadcast notification dismiss button to the top
merge_request: 33174
author:
type: changed

View file

@ -0,0 +1,5 @@
---
title: Move Admin note feature to GitLab Core
merge_request: 31457
author: Rajendra
type: added

View file

@ -55,6 +55,15 @@ resources :merge_requests, concerns: :awardable, except: [:new, :create, :show],
delete :resolve, action: :unresolve
end
end
scope module: :merge_requests do
resources :drafts, only: [:index, :update, :create, :destroy] do
collection do
post :publish
delete :discard
end
end
end
end
scope path: 'merge_requests', controller: 'merge_requests/creations' do

View file

@ -121,6 +121,12 @@ Note the following when promoting a secondary:
gitlab-ctl promote-to-primary-node
```
If you have already run the [preflight checks](planned_failover.md#preflight-checks), you can skip them with:
```shell
gitlab-ctl promote-to-primary-node --skip-preflight-check
```
1. Verify you can connect to the newly promoted **primary** node using the URL used
previously for the **secondary** node.
1. If successful, the **secondary** node has now been promoted to the **primary** node.

View file

@ -784,9 +784,7 @@ If the time frame is not specified, dead replication jobs from the last six hour
sudo /opt/gitlab/embedded/bin/praefect -config /var/opt/gitlab/praefect/config.toml dataloss
Failed replication jobs between [2020-01-02 00:00:00 +0000 UTC, 2020-01-02 06:00:00 +0000 UTC):
example/repository-1: 1 jobs
example/repository-2: 4 jobs
example/repository-3: 2 jobs
@hashed/fa/53/fa539965395b8382145f8370b34eab249cf610d2d6f2943c95b9b9d08a63d4a3.git: 2 jobs
```
To specify a time frame in UTC, run:
@ -797,7 +795,8 @@ sudo /opt/gitlab/embedded/bin/praefect -config /var/opt/gitlab/praefect/config.t
### Checking repository checksums
To check a project's checksums across all nodes, the Praefect replicas Rake task can be used:
To check a project's repository checksums across on all Gitaly nodes, the
replicas Rake task can be run on the main GitLab node:
```shell
sudo gitlab-rake "gitlab:praefect:replicas[project_id]"

View file

@ -8309,11 +8309,6 @@ type Project {
"""
after: String
"""
Filter requirements by author username
"""
authorUsername: [String!]
"""
Returns the elements in the list that come before the specified cursor.
"""
@ -8455,6 +8450,11 @@ type Project {
"""
after: String
"""
Filter requirements by author username
"""
authorUsername: [String!]
"""
Returns the elements in the list that come before the specified cursor.
"""

View file

@ -24398,34 +24398,6 @@
"ofType": null
},
"defaultValue": null
},
{
"name": "search",
"description": "Filter requirements by title search",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "authorUsername",
"description": "Filter requirements by author username",
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
}
},
"defaultValue": null
}
],
"type": {
@ -24488,34 +24460,6 @@
},
"defaultValue": null
},
{
"name": "search",
"description": "Filter requirements by title search",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "authorUsername",
"description": "Filter requirements by author username",
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
}
},
"defaultValue": null
},
{
"name": "after",
"description": "Returns the elements in the list that come after the specified cursor.",
@ -24766,6 +24710,34 @@
"ofType": null
},
"defaultValue": null
},
{
"name": "search",
"description": "Filter requirements by title search",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "authorUsername",
"description": "Filter requirements by author username",
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
}
},
"defaultValue": null
}
],
"type": {
@ -24842,6 +24814,34 @@
},
"defaultValue": null
},
{
"name": "search",
"description": "Filter requirements by title search",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "authorUsername",
"description": "Filter requirements by author username",
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
}
},
"defaultValue": null
},
{
"name": "after",
"description": "Returns the elements in the list that come after the specified cursor.",

View file

@ -104,6 +104,7 @@ GET /users
"color_scheme_id": 2,
"projects_limit": 100,
"current_sign_in_at": "2012-06-02T06:36:55Z",
"note": "DMCA Request: 2018-11-05 | DMCA Violation | Abuse | https://gitlab.zendesk.com/agent/tickets/123",
"identities": [
{"provider": "github", "extern_uid": "2435223452345"},
{"provider": "bitbucket", "extern_uid": "john.smith"},
@ -154,7 +155,7 @@ GET /users
]
```
Users on GitLab [Starter, Bronze, or higher](https://about.gitlab.com/pricing/) will also see the `shared_runners_minutes_limit`, `extra_shared_runners_minutes_limit`, and `note` parameters.
Users on GitLab [Starter, Bronze, or higher](https://about.gitlab.com/pricing/) will also see the `shared_runners_minutes_limit`, and `extra_shared_runners_minutes_limit` parameters.
```json
[
@ -163,7 +164,6 @@ Users on GitLab [Starter, Bronze, or higher](https://about.gitlab.com/pricing/)
...
"shared_runners_minutes_limit": 133,
"extra_shared_runners_minutes_limit": 133,
"note": "DMCA Request: 2018-11-05 | DMCA Violation | Abuse | https://gitlab.zendesk.com/agent/tickets/123",
...
}
]
@ -296,6 +296,7 @@ Example Responses:
"color_scheme_id": 2,
"projects_limit": 100,
"current_sign_in_at": "2012-06-02T06:36:55Z",
"note": "DMCA Request: 2018-11-05 | DMCA Violation | Abuse | https://gitlab.zendesk.com/agent/tickets/123",
"identities": [
{"provider": "github", "extern_uid": "2435223452345"},
{"provider": "bitbucket", "extern_uid": "john.smith"},
@ -316,7 +317,7 @@ Example Responses:
NOTE: **Note:** The `plan` and `trial` parameters are only available on GitLab Enterprise Edition.
Users on GitLab [Starter, Bronze, or higher](https://about.gitlab.com/pricing/) will also see
the `shared_runners_minutes_limit`, `extra_shared_runners_minutes_limit`, and `note` parameters.
the `shared_runners_minutes_limit`, and `extra_shared_runners_minutes_limit` parameters.
```json
{
@ -324,7 +325,6 @@ the `shared_runners_minutes_limit`, `extra_shared_runners_minutes_limit`, and `n
"username": "john_smith",
"shared_runners_minutes_limit": 133,
"extra_shared_runners_minutes_limit": 133,
"note": "DMCA Request: 2018-11-05 | DMCA Violation | Abuse | https://gitlab.zendesk.com/agent/tickets/123",
...
}
```
@ -338,7 +338,6 @@ see the `group_saml` option:
"username": "john_smith",
"shared_runners_minutes_limit": 133,
"extra_shared_runners_minutes_limit": 133,
"note": "DMCA Request: 2018-11-05 | DMCA Violation | Abuse | https://gitlab.zendesk.com/agent/tickets/123",
"identities": [
{"provider": "github", "extern_uid": "2435223452345"},
{"provider": "bitbucket", "extern_uid": "john.smith"},
@ -391,6 +390,7 @@ Parameters:
| `linkedin` | No | LinkedIn |
| `location` | No | User's location |
| `name` | Yes | Name |
| `note` | No | Admin notes for this user |
| `organization` | No | Organization name |
| `password` | No | Password |
| `private_profile` | No | User's profile is private - true, false (default), or null (will be converted to false) |
@ -432,7 +432,7 @@ Parameters:
| `linkedin` | No | LinkedIn |
| `location` | No | User's location |
| `name` | No | Name |
| `note` | No | Admin notes for this user **(STARTER)** |
| `note` | No | Admin notes for this user |
| `organization` | No | Organization name |
| `password` | No | Password |
| `private_profile` | No | User's profile is private - true, false (default), or null (will be converted to false) |

View file

@ -123,6 +123,17 @@ After you're all set up on AWS ECS, follow these steps:
task definition, making the cluster pull the newest version of your
application.
CAUTION: **Warning:**
The [`Deploy-ECS.gitlab-ci.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Deploy-ECS.gitlab-ci.yml)
template includes both the [`Jobs/Build.gitlab-ci.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml)
and [`Jobs/Deploy/ECS.gitlab-ci.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Jobs/Deploy/ECS.gitlab-ci.yml)
"sub-templates". Do not include these "sub-templates" on their own, and only include the main
`Deploy-ECS.gitlab-ci.yml` template. The "sub-templates" are designed to only be
used along with the main template. They may move or change unexpectedly causing your
pipeline to fail if you didn't include the main template. Also, the job names within
these templates may change. Do not override these jobs names in your own pipeline,
as the override will stop working when the name changes.
Alternatively, if you don't wish to use the `Deploy-ECS.gitlab-ci.yml` template
to deploy to AWS ECS, you can always use our
`aws-base` Docker image to run your own [AWS CLI commands for ECS](https://docs.aws.amazon.com/cli/latest/reference/ecs/index.html#cli-aws-ecs).

View file

@ -232,6 +232,15 @@ NOTE: **Note:**
If you have both a valid `AUTO_DEVOPS_PLATFORM_TARGET` variable and a Kubernetes cluster tied to your project,
only the deployment to Kubernetes will run.
CAUTION: **Warning:**
Setting the `AUTO_DEVOPS_PLATFORM_TARGET` variable to `ECS` will trigger jobs
defined in the [`Jobs/Deploy/ECS.gitlab-ci.yml` template](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Jobs/Deploy/ECS.gitlab-ci.yml).
However, it is not recommended to [include](../../ci/yaml/README.md#includetemplate)
it on its own. This template is designed to be used with Auto DevOps only. It may change
unexpectedly causing your pipeline to fail if included on its own. Also, the job
names within this template may also change. Do not override these jobs names in your
own pipeline, as the override will stop working when the name changes.
## Auto DevOps base domain
The Auto DevOps base domain is required to use

View file

@ -4,8 +4,7 @@ module API
module Entities
class UserWithAdmin < UserPublic
expose :admin?, as: :is_admin
expose :note
end
end
end
API::Entities::UserWithAdmin.prepend_if_ee('EE::API::Entities::UserWithAdmin', with_descendants: true)

View file

@ -55,6 +55,7 @@ module API
optional :theme_id, type: Integer, desc: 'The GitLab theme for the user'
optional :color_scheme_id, type: Integer, desc: 'The color scheme for the file viewer'
optional :private_profile, type: Boolean, desc: 'Flag indicating the user has a private profile'
optional :note, type: String, desc: 'Admin note for this user'
all_or_none_of :extern_uid, :provider
use :optional_params_ee

View file

@ -1,3 +1,13 @@
# WARNING (post-GitLab 13.0):
#
# This CI template should NOT be included in your own CI configuration files:
# 'review_ecs' and 'production_ecs' are two temporary names given to the jobs below.
#
# Should this template be included in your CI configuration, the upcoming name changes could
# then result in potentially breaking your future pipelines.
#
# More about including CI templates: https://docs.gitlab.com/ee/ci/yaml/#includetemplate
.deploy_to_ecs:
image: 'registry.gitlab.com/gitlab-org/cloud-deploy/aws-ecs:latest'
script:

View file

@ -12,14 +12,15 @@ module Gitlab
WIKI = RepoType.new(
name: :wiki,
access_checker_class: Gitlab::GitAccessWiki,
repository_resolver: -> (project) { project&.wiki&.repository },
repository_resolver: -> (container) { container&.wiki&.repository },
project_resolver: -> (container) { container.is_a?(Project) ? container : nil },
suffix: :wiki
).freeze
SNIPPET = RepoType.new(
name: :snippet,
access_checker_class: Gitlab::GitAccessSnippet,
repository_resolver: -> (snippet) { snippet&.repository },
container_resolver: -> (id) { Snippet.find_by_id(id) },
container_class: Snippet,
project_resolver: -> (snippet) { snippet&.project },
guest_read_ability: :read_snippet
).freeze
@ -42,16 +43,12 @@ module Gitlab
end
def self.parse(gl_repository)
type_name, _id = gl_repository.split('-').first
type = types[type_name]
result = ::Gitlab::GlRepository::Identifier.new(gl_repository)
unless type
raise ArgumentError, "Invalid GL Repository \"#{gl_repository}\""
end
repo_type = result.repo_type
container = result.fetch_container!
container = type.fetch_container!(gl_repository)
[container, type.project_for(container), type]
[container, repo_type.project_for(container), repo_type]
end
def self.default_type

View file

@ -0,0 +1,74 @@
# frozen_string_literal: true
module Gitlab
class GlRepository
class Identifier
attr_reader :gl_repository, :repo_type
def initialize(gl_repository)
@gl_repository = gl_repository
@segments = gl_repository.split('-')
raise_error if segments.size > 3
@repo_type = find_repo_type
@container_id = find_container_id
@container_class = find_container_class
end
def fetch_container!
container_class.find_by_id(container_id)
end
private
attr_reader :segments, :container_class, :container_id
def find_repo_type
type_name = three_segments_format? ? segments.last : segments.first
type = Gitlab::GlRepository.types[type_name]
raise_error unless type
type
end
def find_container_class
if three_segments_format?
case segments[0]
when 'project'
Project
when 'group'
Group
else
raise_error
end
else
repo_type.container_class
end
end
def find_container_id
id = Integer(segments[1], 10, exception: false)
raise_error unless id
id
end
# gl_repository can either have 2 or 3 segments:
# "wiki-1" is the older 2-segment format, where container is implied.
# "group-1-wiki" is the newer 3-segment format, including container information.
#
# TODO: convert all 2-segment format to 3-segment:
# https://gitlab.com/gitlab-org/gitlab/-/issues/219192
def three_segments_format?
segments.size == 3
end
def raise_error
raise ArgumentError, "Invalid GL Repository \"#{gl_repository}\""
end
end
end
end

View file

@ -6,7 +6,7 @@ module Gitlab
attr_reader :name,
:access_checker_class,
:repository_resolver,
:container_resolver,
:container_class,
:project_resolver,
:guest_read_ability,
:suffix
@ -15,36 +15,27 @@ module Gitlab
name:,
access_checker_class:,
repository_resolver:,
container_resolver: default_container_resolver,
container_class: default_container_class,
project_resolver: nil,
guest_read_ability: :download_code,
suffix: nil)
@name = name
@access_checker_class = access_checker_class
@repository_resolver = repository_resolver
@container_resolver = container_resolver
@container_class = container_class
@project_resolver = project_resolver
@guest_read_ability = guest_read_ability
@suffix = suffix
end
def identifier_for_container(container)
if container.is_a?(Group)
return "#{container.class.name.underscore}-#{container.id}-#{name}"
end
"#{name}-#{container.id}"
end
def fetch_id(identifier)
match = /\A#{name}-(?<id>\d+)\z/.match(identifier)
match[:id] if match
end
def fetch_container!(identifier)
id = fetch_id(identifier)
raise ArgumentError, "Invalid GL Repository \"#{identifier}\"" unless id
container_resolver.call(id)
end
def wiki?
self == WIKI
end
@ -85,8 +76,8 @@ module Gitlab
private
def default_container_resolver
-> (id) { Project.find_by_id(id) }
def default_container_class
Project
end
end
end

View file

@ -104,6 +104,23 @@ module Gitlab
command :target_branch do |branch_name|
@updates[:target_branch] = branch_name if project.repository.branch_exists?(branch_name)
end
desc _('Submit a review')
explanation _('Submit the current review.')
types MergeRequest
condition do
quick_action_target.persisted?
end
command :submit_review do
next if params[:review_id]
result = DraftNotes::PublishService.new(quick_action_target, current_user).execute
@execution_message[:submit_review] = if result[:status] == :success
_('Submitted the current review.')
else
result[:message]
end
end
end
def merge_orchestration_service

View file

@ -96,6 +96,11 @@ msgid_plural "%d comments"
msgstr[0] ""
msgstr[1] ""
msgid "%d comment on this commit"
msgid_plural "%d comments on this commit"
msgstr[0] ""
msgstr[1] ""
msgid "%d commit"
msgid_plural "%d commits"
msgstr[0] ""
@ -1218,6 +1223,9 @@ msgstr ""
msgid "Add a bullet list"
msgstr ""
msgid "Add a comment to this line"
msgstr ""
msgid "Add a general comment to this %{noteableDisplayName}."
msgstr ""
@ -1419,6 +1427,9 @@ msgstr ""
msgid "Admin Area"
msgstr ""
msgid "Admin Note"
msgstr ""
msgid "Admin Overview"
msgstr ""
@ -3605,7 +3616,7 @@ msgstr ""
msgid "CI / CD"
msgstr ""
msgid "CI / CD Charts"
msgid "CI / CD Analytics"
msgstr ""
msgid "CI / CD Settings"
@ -5838,6 +5849,11 @@ msgid_plural "ContainerRegistry|%{count} Image repositories"
msgstr[0] ""
msgstr[1] ""
msgid "ContainerRegistry|%{count} Tag"
msgid_plural "ContainerRegistry|%{count} Tags"
msgstr[0] ""
msgstr[1] ""
msgid "ContainerRegistry|%{imageName} tags"
msgstr ""
@ -6407,6 +6423,9 @@ msgstr ""
msgid "Create issue"
msgstr ""
msgid "Create iteration"
msgstr ""
msgid "Create lists from labels. Issues with that label appear in that list."
msgstr ""
@ -23333,6 +23352,9 @@ msgstr ""
msgid "Unable to resolve"
msgstr ""
msgid "Unable to save iteration. Please try again"
msgstr ""
msgid "Unable to save your changes. Please try again."
msgstr ""

View file

@ -254,6 +254,18 @@ describe Admin::UsersController do
errors = assigns[:user].errors
expect(errors).to contain_exactly(errors.full_message(:email, I18n.t('errors.messages.invalid')))
end
context 'admin notes' do
it 'creates the user with note' do
note = '2020-05-12 | Note | DCMA | Link'
user_params = attributes_for(:user, note: note)
expect { post :create, params: { user: user_params } }.to change { User.count }.by(1)
new_user = User.last
expect(new_user.note).to eq(note)
end
end
end
describe 'POST update' do
@ -338,6 +350,20 @@ describe Admin::UsersController do
end
end
end
context 'admin notes' do
it 'updates the note for the user' do
note = '2020-05-12 | Note | DCMA | Link'
params = {
id: user.to_param,
user: {
note: note
}
}
expect { post :update, params: params }.to change { user.reload.note }.to(note)
end
end
end
describe "DELETE #remove_email" do

View file

@ -0,0 +1,455 @@
# frozen_string_literal: true
require 'spec_helper'
describe Projects::MergeRequests::DraftsController do
include RepoHelpers
let(:project) { create(:project, :repository) }
let(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project) }
let(:user) { project.owner }
let(:user2) { create(:user) }
let(:params) do
{
namespace_id: project.namespace.to_param,
project_id: project.to_param,
merge_request_id: merge_request.iid
}
end
before do
sign_in(user)
stub_licensed_features(multiple_merge_request_assignees: true)
stub_commonmark_sourcepos_disabled
end
describe 'GET #index' do
let!(:draft_note) { create(:draft_note, merge_request: merge_request, author: user) }
it 'list merge request draft notes for current user' do
get :index, params: params
expect(json_response.first['merge_request_id']).to eq(merge_request.id)
expect(json_response.first['author']['id']).to eq(user.id)
expect(json_response.first['note_html']).not_to be_empty
end
end
describe 'POST #create' do
def create_draft_note(draft_overrides: {}, overrides: {})
post_params = params.merge({
draft_note: {
note: 'This is a unpublished comment'
}.merge(draft_overrides)
}.merge(overrides))
post :create, params: post_params
end
context 'without permissions' do
let(:project) { create(:project, :private) }
before do
sign_in(user2)
end
it 'does not allow draft note creation' do
expect { create_draft_note }.to change { DraftNote.count }.by(0)
expect(response).to have_gitlab_http_status(:not_found)
end
end
it 'creates a draft note' do
expect { create_draft_note }.to change { DraftNote.count }.by(1)
end
it 'creates draft note with position' do
diff_refs = project.commit(sample_commit.id).try(:diff_refs)
position = Gitlab::Diff::Position.new(
old_path: "files/ruby/popen.rb",
new_path: "files/ruby/popen.rb",
old_line: nil,
new_line: 14,
diff_refs: diff_refs
)
create_draft_note(draft_overrides: { position: position.to_json })
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['position']).to be_present
expect(json_response['file_hash']).to be_present
expect(json_response['file_identifier_hash']).to be_present
expect(json_response['line_code']).to match(/\w+_\d+_\d+/)
expect(json_response['note_html']).to eq('<p dir="auto">This is a unpublished comment</p>')
end
it 'creates a draft note with quick actions' do
create_draft_note(draft_overrides: { note: "#{user2.to_reference}\n/assign #{user.to_reference}" })
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['note_html']).to match(/#{user2.to_reference}/)
expect(json_response['references']['commands']).to match(/Assigns/)
expect(json_response['references']['users']).to include(user2.username)
end
context 'in a thread' do
let(:discussion) { create(:discussion_note_on_merge_request, noteable: merge_request, project: project).discussion }
it 'creates draft note as a reply' do
expect do
create_draft_note(overrides: { in_reply_to_discussion_id: discussion.reply_id })
end.to change { DraftNote.count }.by(1)
draft_note = DraftNote.last
expect(draft_note).to be_valid
expect(draft_note.discussion_id).to eq(discussion.reply_id)
end
it 'creates a draft note that will resolve a thread' do
expect do
create_draft_note(
overrides: { in_reply_to_discussion_id: discussion.reply_id },
draft_overrides: { resolve_discussion: true }
)
end.to change { DraftNote.count }.by(1)
draft_note = DraftNote.last
expect(draft_note).to be_valid
expect(draft_note.discussion_id).to eq(discussion.reply_id)
expect(draft_note.resolve_discussion).to eq(true)
end
it 'cannot create more than one draft note per thread' do
expect do
create_draft_note(
overrides: { in_reply_to_discussion_id: discussion.reply_id },
draft_overrides: { resolve_discussion: true }
)
end.to change { DraftNote.count }.by(1)
expect do
create_draft_note(
overrides: { in_reply_to_discussion_id: discussion.reply_id },
draft_overrides: { resolve_discussion: true, note: 'A note' }
)
end.to change { DraftNote.count }.by(0)
end
end
context 'commit_id is present' do
let(:commit) { project.commit(sample_commit.id) }
let(:position) do
Gitlab::Diff::Position.new(
old_path: "files/ruby/popen.rb",
new_path: "files/ruby/popen.rb",
old_line: nil,
new_line: 14,
diff_refs: commit.diff_refs
)
end
before do
create_draft_note(draft_overrides: { commit_id: commit_id, position: position.to_json })
end
context 'value is a commit sha' do
let(:commit_id) { commit.id }
it 'creates the draft note with commit ID' do
expect(DraftNote.last.commit_id).to eq(commit_id)
end
end
context 'value is "undefined"' do
let(:commit_id) { 'undefined' }
it 'creates the draft note with nil commit ID' do
expect(DraftNote.last.commit_id).to be_nil
end
end
end
end
describe 'PUT #update' do
let(:draft) { create(:draft_note, merge_request: merge_request, author: user) }
def update_draft_note(overrides = {})
put_params = params.merge({
id: draft.id,
draft_note: {
note: 'This is an updated unpublished comment'
}.merge(overrides)
})
put :update, params: put_params
end
context 'without permissions' do
before do
sign_in(user2)
project.add_developer(user2)
end
it 'does not allow editing draft note belonging to someone else' do
update_draft_note
expect(response).to have_gitlab_http_status(:not_found)
expect(draft.reload.note).not_to eq('This is an updated unpublished comment')
end
end
it 'updates the draft' do
expect(draft.note).not_to be_empty
expect { update_draft_note }.not_to change { DraftNote.count }
draft.reload
expect(draft.note).to eq('This is an updated unpublished comment')
expect(json_response['note_html']).not_to be_empty
end
end
describe 'POST #publish' do
context 'without permissions' do
shared_examples_for 'action that does not allow publishing draft note' do
it 'does not allow publishing draft note' do
expect { action }
.to not_change { Note.count }
.and not_change { DraftNote.count }
expect(response).to have_gitlab_http_status(:not_found)
end
end
before do
sign_in(user2)
end
context 'when note belongs to someone else' do
before do
project.add_developer(user2)
end
it_behaves_like 'action that does not allow publishing draft note' do
let!(:draft) { create(:draft_note, merge_request: merge_request, author: user) }
let(:action) { post :publish, params: params.merge(id: draft.id) }
end
end
context 'when merge request discussion is locked' do
let(:project) { create(:project, :public, :merge_requests_public, :repository) }
before do
create(:draft_note, merge_request: merge_request, author: user2)
merge_request.update!(discussion_locked: true)
end
it_behaves_like 'action that does not allow publishing draft note' do
let(:action) { post :publish, params: params }
end
end
end
context 'when PublishService errors' do
it 'returns message and 500 response' do
create(:draft_note, merge_request: merge_request, author: user)
error_message = "Something went wrong"
expect_next_instance_of(DraftNotes::PublishService) do |service|
allow(service).to receive(:execute).and_return({ message: error_message, status: :error })
end
post :publish, params: params
expect(response).to have_gitlab_http_status(:error)
expect(json_response["message"]).to include(error_message)
end
end
it 'publishes draft notes with position' do
diff_refs = project.commit(sample_commit.id).try(:diff_refs)
position = Gitlab::Diff::Position.new(
old_path: "files/ruby/popen.rb",
new_path: "files/ruby/popen.rb",
old_line: nil,
new_line: 14,
diff_refs: diff_refs
)
draft = create(:draft_note_on_text_diff, merge_request: merge_request, author: user, position: position)
expect { post :publish, params: params }.to change { Note.count }.by(1)
.and change { DraftNote.count }.by(-1)
note = merge_request.notes.reload.last
expect(note.note).to eq(draft.note)
expect(note.position).to eq(draft.position)
end
it 'does nothing if there are no draft notes' do
expect { post :publish, params: params }.to change { Note.count }.by(0).and change { DraftNote.count }.by(0)
end
it 'publishes a draft note with quick actions and applies them' do
project.add_developer(user2)
create(:draft_note, merge_request: merge_request, author: user,
note: "/assign #{user2.to_reference}")
expect(merge_request.assignees).to be_empty
expect { post :publish, params: params }.to change { Note.count }.by(1)
.and change { DraftNote.count }.by(-1)
expect(response).to have_gitlab_http_status(:ok)
expect(merge_request.reload.assignee_ids).to match_array([user2.id])
expect(Note.last.system?).to be true
end
it 'publishes all draft notes for an MR' do
draft_params = { merge_request: merge_request, author: user }
drafts = create_list(:draft_note, 4, draft_params)
note = create(:discussion_note_on_merge_request, noteable: merge_request, project: project)
draft_reply = create(:draft_note, draft_params.merge(discussion_id: note.discussion_id))
diff_note = create(:diff_note_on_merge_request, noteable: merge_request, project: project)
diff_draft_reply = create(:draft_note, draft_params.merge(discussion_id: diff_note.discussion_id))
expect { post :publish, params: params }.to change { Note.count }.by(6)
.and change { DraftNote.count }.by(-6)
expect(response).to have_gitlab_http_status(:ok)
notes = merge_request.notes.reload
expect(notes.pluck(:note)).to include(*drafts.map(&:note))
expect(note.discussion.notes.last.note).to eq(draft_reply.note)
expect(diff_note.discussion.notes.last.note).to eq(diff_draft_reply.note)
end
it 'can publish just a single draft note' do
draft_params = { merge_request: merge_request, author: user }
drafts = create_list(:draft_note, 4, draft_params)
expect { post :publish, params: params.merge(id: drafts.first.id) }.to change { Note.count }.by(1)
.and change { DraftNote.count }.by(-1)
end
context 'when publishing drafts in a thread' do
let(:note) { create(:discussion_note_on_merge_request, noteable: merge_request, project: project) }
def create_reply(discussion_id, resolves: false)
create(:draft_note,
merge_request: merge_request,
author: user,
discussion_id: discussion_id,
resolve_discussion: resolves
)
end
it 'resolves a thread if the draft note resolves it' do
draft_reply = create_reply(note.discussion_id, resolves: true)
post :publish, params: params
discussion = note.discussion
expect(discussion.notes.last.note).to eq(draft_reply.note)
expect(discussion.resolved?).to eq(true)
expect(discussion.resolved_by.id).to eq(user.id)
end
it 'unresolves a thread if the draft note unresolves it' do
note.discussion.resolve!(user)
expect(note.discussion.resolved?).to eq(true)
draft_reply = create_reply(note.discussion_id, resolves: false)
post :publish, params: params
discussion = note.discussion
expect(discussion.notes.last.note).to eq(draft_reply.note)
expect(discussion.resolved?).to eq(false)
end
end
end
describe 'DELETE #destroy' do
let(:draft) { create(:draft_note, merge_request: merge_request, author: user) }
def create_draft
create(:draft_note, merge_request: merge_request, author: user)
end
context 'without permissions' do
before do
sign_in(user2)
project.add_developer(user2)
end
it 'does not allow destroying a draft note belonging to someone else' do
draft = create(:draft_note, merge_request: merge_request, author: user)
expect { post :destroy, params: params.merge(id: draft.id) }
.not_to change { DraftNote.count }
expect(response).to have_gitlab_http_status(:not_found)
end
end
it 'destroys the draft note when ID is given' do
draft = create_draft
expect { delete :destroy, params: params.merge(id: draft.id) }.to change { DraftNote.count }.by(-1)
expect(response).to have_gitlab_http_status(:ok)
end
context 'without permissions' do
before do
sign_in(user2)
end
it 'does not allow editing draft note belonging to someone else' do
draft = create_draft
expect { delete :destroy, params: params.merge(id: draft.id) }.to change { DraftNote.count }.by(0)
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
describe 'DELETE #discard' do
it 'deletes all DraftNotes belonging to a user in a Merge Request' do
create_list(:draft_note, 6, merge_request: merge_request, author: user)
expect { delete :discard, params: params }.to change { DraftNote.count }.by(-6)
expect(response).to have_gitlab_http_status(:ok)
end
context 'without permissions' do
before do
sign_in(user2)
project.add_developer(user2)
end
it 'does not destroys a draft note belonging to someone else' do
create(:draft_note, merge_request: merge_request, author: user)
expect { post :discard, params: params }
.not_to change { DraftNote.count }
expect(response).to have_gitlab_http_status(:ok)
end
end
end
end

View file

@ -183,6 +183,10 @@ FactoryBot.define do
confidential { true }
end
trait :with_review do
review
end
transient do
in_reply_to { nil }
end

View file

@ -1,6 +1,6 @@
import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
import Tracking from '~/tracking';
import * as types from '~/logs/stores/mutation_types';
import { convertToFixedRange } from '~/lib/utils/datetime_range';
import logsPageState from '~/logs/stores/state';
@ -104,7 +104,7 @@ describe('Logs Store actions', () => {
{ type: types.SET_CURRENT_POD_NAME, payload: null },
{ type: types.SET_SEARCH, payload: '' },
],
[{ type: 'fetchLogs' }],
[{ type: 'fetchLogs', payload: 'used_search_bar' }],
));
it('text search should filter with a search term', () =>
@ -116,7 +116,7 @@ describe('Logs Store actions', () => {
{ type: types.SET_CURRENT_POD_NAME, payload: null },
{ type: types.SET_SEARCH, payload: mockSearch },
],
[{ type: 'fetchLogs' }],
[{ type: 'fetchLogs', payload: 'used_search_bar' }],
));
it('pod search should filter with a search term', () =>
@ -128,7 +128,7 @@ describe('Logs Store actions', () => {
{ type: types.SET_CURRENT_POD_NAME, payload: mockPodName },
{ type: types.SET_SEARCH, payload: '' },
],
[{ type: 'fetchLogs' }],
[{ type: 'fetchLogs', payload: 'used_search_bar' }],
));
it('pod search should filter with a pod selection and a search term', () =>
@ -140,7 +140,7 @@ describe('Logs Store actions', () => {
{ type: types.SET_CURRENT_POD_NAME, payload: mockPodName },
{ type: types.SET_SEARCH, payload: mockSearch },
],
[{ type: 'fetchLogs' }],
[{ type: 'fetchLogs', payload: 'used_search_bar' }],
));
it('pod search should filter with a pod selection and two search terms', () =>
@ -152,7 +152,7 @@ describe('Logs Store actions', () => {
{ type: types.SET_CURRENT_POD_NAME, payload: null },
{ type: types.SET_SEARCH, payload: `term1 term2` },
],
[{ type: 'fetchLogs' }],
[{ type: 'fetchLogs', payload: 'used_search_bar' }],
));
it('pod search should filter with a pod selection and a search terms before and after', () =>
@ -168,7 +168,7 @@ describe('Logs Store actions', () => {
{ type: types.SET_CURRENT_POD_NAME, payload: mockPodName },
{ type: types.SET_SEARCH, payload: `term1 term2` },
],
[{ type: 'fetchLogs' }],
[{ type: 'fetchLogs', payload: 'used_search_bar' }],
));
});
@ -179,7 +179,7 @@ describe('Logs Store actions', () => {
mockPodName,
state,
[{ type: types.SET_CURRENT_POD_NAME, payload: mockPodName }],
[{ type: 'fetchLogs' }],
[{ type: 'fetchLogs', payload: 'pod_log_changed' }],
));
});
@ -198,7 +198,7 @@ describe('Logs Store actions', () => {
{ type: types.REQUEST_ENVIRONMENTS_DATA },
{ type: types.RECEIVE_ENVIRONMENTS_DATA_SUCCESS, payload: mockEnvironments },
],
[{ type: 'fetchLogs' }],
[{ type: 'fetchLogs', payload: 'environment_selected' }],
);
});
@ -471,3 +471,58 @@ describe('Logs Store actions', () => {
});
});
});
describe('Tracking user interaction', () => {
let commit;
let dispatch;
let state;
let mock;
beforeEach(() => {
jest.spyOn(Tracking, 'event');
commit = jest.fn();
dispatch = jest.fn();
state = logsPageState();
state.environments.options = mockEnvironments;
state.environments.current = mockEnvName;
mock = new MockAdapter(axios);
});
afterEach(() => {
mock.reset();
});
describe('Logs with data', () => {
beforeEach(() => {
mock.onGet(mockLogsEndpoint).reply(200, mockResponse);
mock.onGet(mockLogsEndpoint).replyOnce(202); // mock reactive cache
});
it('tracks fetched logs with data', () => {
return fetchLogs({ state, commit, dispatch }, 'environment_selected').then(() => {
expect(Tracking.event).toHaveBeenCalledWith(document.body.dataset.page, 'logs_view', {
label: 'environment_selected',
property: 'count',
value: 1,
});
});
});
});
describe('Logs without data', () => {
beforeEach(() => {
mock.onGet(mockLogsEndpoint).reply(200, {
...mockResponse,
logs: [],
});
mock.onGet(mockLogsEndpoint).replyOnce(202); // mock reactive cache
});
it('does not track empty log responses', () => {
return fetchLogs({ state, commit, dispatch }).then(() => {
expect(Tracking.event).not.toHaveBeenCalled();
});
});
});
});

View file

@ -0,0 +1,140 @@
import { shallowMount } from '@vue/test-utils';
import { GlIcon, GlSprintf } from '@gitlab/ui';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import Component from '~/registry/explorer/components/image_list_row.vue';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import {
ROW_SCHEDULED_FOR_DELETION,
LIST_DELETE_BUTTON_DISABLED,
} from '~/registry/explorer/constants';
import { RouterLink } from '../stubs';
import { imagesListResponse } from '../mock_data';
describe('Image List Row', () => {
let wrapper;
const item = imagesListResponse.data[0];
const findDeleteBtn = () => wrapper.find('[data-testid="deleteImageButton"]');
const findDetailsLink = () => wrapper.find('[data-testid="detailsLink"]');
const findTagsCount = () => wrapper.find('[data-testid="tagsCount"]');
const findDeleteButtonWrapper = () => wrapper.find('[data-testid="deleteButtonWrapper"]');
const findClipboardButton = () => wrapper.find(ClipboardButton);
const mountComponent = props => {
wrapper = shallowMount(Component, {
stubs: {
RouterLink,
GlSprintf,
},
propsData: {
item,
...props,
},
directives: {
GlTooltip: createMockDirective(),
},
});
};
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
describe('main tooltip', () => {
it(`the title is ${ROW_SCHEDULED_FOR_DELETION}`, () => {
mountComponent();
const tooltip = getBinding(wrapper.element, 'gl-tooltip');
expect(tooltip).toBeDefined();
expect(tooltip.value.title).toBe(ROW_SCHEDULED_FOR_DELETION);
});
it('is disabled when item is being deleted', () => {
mountComponent({ item: { ...item, deleting: true } });
const tooltip = getBinding(wrapper.element, 'gl-tooltip');
expect(tooltip.value.disabled).toBe(false);
});
});
describe('image title and path', () => {
it('contains a link to the details page', () => {
mountComponent();
const link = findDetailsLink();
expect(link.html()).toContain(item.path);
expect(link.props('to').name).toBe('details');
});
it('contains a clipboard button', () => {
mountComponent();
const button = findClipboardButton();
expect(button.exists()).toBe(true);
expect(button.props('text')).toBe(item.location);
expect(button.props('title')).toBe(item.location);
});
});
describe('delete button wrapper', () => {
it('has a tooltip', () => {
mountComponent();
const tooltip = getBinding(findDeleteButtonWrapper().element, 'gl-tooltip');
expect(tooltip).toBeDefined();
expect(tooltip.value.title).toBe(LIST_DELETE_BUTTON_DISABLED);
});
it('tooltip is enabled when destroy_path is falsy', () => {
mountComponent({ item: { ...item, destroy_path: null } });
const tooltip = getBinding(findDeleteButtonWrapper().element, 'gl-tooltip');
expect(tooltip.value.disabled).toBeFalsy();
});
});
describe('delete button', () => {
it('exists', () => {
mountComponent();
expect(findDeleteBtn().exists()).toBe(true);
});
it('emits a delete event', () => {
mountComponent();
findDeleteBtn().vm.$emit('click');
expect(wrapper.emitted('delete')).toEqual([[item]]);
});
it.each`
destroy_path | deleting | state
${null} | ${null} | ${'true'}
${null} | ${true} | ${'true'}
${'foo'} | ${true} | ${'true'}
${'foo'} | ${false} | ${undefined}
`(
'disabled is $state when destroy_path is $destroy_path and deleting is $deleting',
({ destroy_path, deleting, state }) => {
mountComponent({ item: { ...item, destroy_path, deleting } });
expect(findDeleteBtn().attributes('disabled')).toBe(state);
},
);
});
describe('tags count', () => {
it('exists', () => {
mountComponent();
expect(findTagsCount().exists()).toBe(true);
});
it('contains a tag icon', () => {
mountComponent();
const icon = findTagsCount().find(GlIcon);
expect(icon.exists()).toBe(true);
expect(icon.props('name')).toBe('tag');
});
describe('tags count text', () => {
it('with one tag in the image', () => {
mountComponent({ item: { ...item, tags_count: 1 } });
expect(findTagsCount().text()).toMatchInterpolatedText('1 Tag');
});
it('with more than one tag in the image', () => {
mountComponent({ item: { ...item, tags_count: 3 } });
expect(findTagsCount().text()).toMatchInterpolatedText('3 Tags');
});
});
});
});

View file

@ -1,26 +1,18 @@
import { shallowMount } from '@vue/test-utils';
import { GlPagination } from '@gitlab/ui';
import Component from '~/registry/explorer/components/image_list.vue';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import { RouterLink } from '../stubs';
import ImageListRow from '~/registry/explorer/components/image_list_row.vue';
import { imagesListResponse, imagePagination } from '../mock_data';
describe('Image List', () => {
let wrapper;
const firstElement = imagesListResponse.data[0];
const findDeleteBtn = () => wrapper.find('[data-testid="deleteImageButton"]');
const findRowItems = () => wrapper.findAll('[data-testid="rowItem"]');
const findDetailsLink = () => wrapper.find('[data-testid="detailsLink"]');
const findClipboardButton = () => wrapper.find(ClipboardButton);
const findRow = () => wrapper.findAll(ImageListRow);
const findPagination = () => wrapper.find(GlPagination);
const mountComponent = () => {
wrapper = shallowMount(Component, {
stubs: {
RouterLink,
},
propsData: {
images: imagesListResponse.data,
pagination: imagePagination,
@ -32,26 +24,17 @@ describe('Image List', () => {
mountComponent();
});
it('contains one list element for each image', () => {
expect(findRowItems().length).toBe(imagesListResponse.data.length);
});
describe('list', () => {
it('contains one list element for each image', () => {
expect(findRow().length).toBe(imagesListResponse.data.length);
});
it('contains a link to the details page', () => {
const link = findDetailsLink();
expect(link.html()).toContain(firstElement.path);
expect(link.props('to').name).toBe('details');
});
it('contains a clipboard button', () => {
const button = findClipboardButton();
expect(button.exists()).toBe(true);
expect(button.props('text')).toBe(firstElement.location);
expect(button.props('title')).toBe(firstElement.location);
});
it('should be possible to delete a repo', () => {
const deleteBtn = findDeleteBtn();
expect(deleteBtn.exists()).toBe(true);
it('when delete event is emitted on the row it emits up a delete event', () => {
findRow()
.at(0)
.vm.$emit('delete', 'foo');
expect(wrapper.emitted('delete')).toEqual([['foo']]);
});
});
describe('pagination', () => {

View file

@ -0,0 +1,82 @@
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::GlRepository::Identifier do
let_it_be(:project) { create(:project) }
let_it_be(:personal_snippet) { create(:personal_snippet, author: project.owner) }
let_it_be(:project_snippet) { create(:project_snippet, project: project, author: project.owner) }
describe 'project repository' do
it_behaves_like 'parsing gl_repository identifier' do
let(:record_id) { project.id }
let(:identifier) { "project-#{record_id}" }
let(:expected_container) { project }
let(:expected_type) { Gitlab::GlRepository::PROJECT }
end
end
describe 'wiki' do
it_behaves_like 'parsing gl_repository identifier' do
let(:record_id) { project.id }
let(:identifier) { "wiki-#{record_id}" }
let(:expected_container) { project }
let(:expected_type) { Gitlab::GlRepository::WIKI }
end
end
describe 'snippet' do
context 'when PersonalSnippet' do
it_behaves_like 'parsing gl_repository identifier' do
let(:record_id) { personal_snippet.id }
let(:identifier) { "snippet-#{record_id}" }
let(:expected_container) { personal_snippet }
let(:expected_type) { Gitlab::GlRepository::SNIPPET }
end
end
context 'when ProjectSnippet' do
it_behaves_like 'parsing gl_repository identifier' do
let(:record_id) { project_snippet.id }
let(:identifier) { "snippet-#{record_id}" }
let(:expected_container) { project_snippet }
let(:expected_type) { Gitlab::GlRepository::SNIPPET }
end
end
end
describe 'design' do
it_behaves_like 'parsing gl_repository identifier' do
let(:record_id) { project.id }
let(:identifier) { "design-#{project.id}" }
let(:expected_container) { project }
let(:expected_type) { Gitlab::GlRepository::DESIGN }
end
end
describe 'incorrect format' do
def expect_error_raised_for(identifier)
expect { described_class.new(identifier) }.to raise_error(ArgumentError)
end
it 'raises error for incorrect id' do
expect_error_raised_for('wiki-noid')
end
it 'raises error for incorrect type' do
expect_error_raised_for('foo-2')
end
it 'raises error for incorrect three-segment container' do
expect_error_raised_for('snippet-2-wiki')
end
it 'raises error for one segment' do
expect_error_raised_for('snippet')
end
it 'raises error for more than three segments' do
expect_error_raised_for('project-1-wiki-bar')
end
end
end

View file

@ -13,7 +13,7 @@ describe Gitlab::GlRepository::RepoType do
describe Gitlab::GlRepository::PROJECT do
it_behaves_like 'a repo type' do
let(:expected_id) { project.id.to_s }
let(:expected_id) { project.id }
let(:expected_identifier) { "project-#{expected_id}" }
let(:expected_suffix) { '' }
let(:expected_container) { project }
@ -42,7 +42,7 @@ describe Gitlab::GlRepository::RepoType do
describe Gitlab::GlRepository::WIKI do
it_behaves_like 'a repo type' do
let(:expected_id) { project.id.to_s }
let(:expected_id) { project.id }
let(:expected_identifier) { "wiki-#{expected_id}" }
let(:expected_suffix) { '.wiki' }
let(:expected_container) { project }
@ -72,7 +72,7 @@ describe Gitlab::GlRepository::RepoType do
describe Gitlab::GlRepository::SNIPPET do
context 'when PersonalSnippet' do
it_behaves_like 'a repo type' do
let(:expected_id) { personal_snippet.id.to_s }
let(:expected_id) { personal_snippet.id }
let(:expected_identifier) { "snippet-#{expected_id}" }
let(:expected_suffix) { '' }
let(:expected_repository) { personal_snippet.repository }
@ -101,7 +101,7 @@ describe Gitlab::GlRepository::RepoType do
context 'when ProjectSnippet' do
it_behaves_like 'a repo type' do
let(:expected_id) { project_snippet.id.to_s }
let(:expected_id) { project_snippet.id }
let(:expected_identifier) { "snippet-#{expected_id}" }
let(:expected_suffix) { '' }
let(:expected_repository) { project_snippet.repository }
@ -131,7 +131,7 @@ describe Gitlab::GlRepository::RepoType do
describe Gitlab::GlRepository::DESIGN do
it_behaves_like 'a repo type' do
let(:expected_identifier) { "design-#{project.id}" }
let(:expected_id) { project.id.to_s }
let(:expected_id) { project.id }
let(:expected_suffix) { '.design' }
let(:expected_repository) { project.design_repository }
let(:expected_container) { project }

View file

@ -11,7 +11,7 @@ describe ::Gitlab::GlRepository do
expect(described_class.parse("project-#{project.id}")).to eq([project, project, Gitlab::GlRepository::PROJECT])
end
it 'parses a wiki gl_repository' do
it 'parses a project wiki gl_repository' do
expect(described_class.parse("wiki-#{project.id}")).to eq([project, project, Gitlab::GlRepository::WIKI])
end

View file

@ -15,6 +15,177 @@ describe API::Users, :do_not_mock_admin_mode do
let(:not_existing_pat_id) { (PersonalAccessToken.maximum('id') || 0 ) + 10 }
let(:private_user) { create(:user, private_profile: true) }
context 'admin notes' do
let(:admin) { create(:admin, note: '2019-10-06 | 2FA added | user requested | www.gitlab.com') }
let(:user) { create(:user, note: '2018-11-05 | 2FA removed | user requested | www.gitlab.com') }
describe 'POST /users' do
context 'when unauthenticated' do
it 'return authentication error' do
post api('/users')
expect(response).to have_gitlab_http_status(:unauthorized)
end
end
context 'when authenticated' do
context 'as an admin' do
it 'contains the note of the user' do
optional_attributes = { note: 'Awesome Note' }
attributes = attributes_for(:user).merge(optional_attributes)
post api('/users', admin), params: attributes
expect(response).to have_gitlab_http_status(:created)
expect(json_response['note']).to eq(optional_attributes[:note])
end
end
context 'as a regular user' do
it 'does not allow creating new user' do
post api('/users', user), params: attributes_for(:user)
expect(response).to have_gitlab_http_status(:forbidden)
end
end
end
end
describe 'GET /users/:id' do
context 'when unauthenticated' do
it 'does not contain the note of the user' do
get api("/users/#{user.id}")
expect(json_response).not_to have_key('note')
end
end
context 'when authenticated' do
context 'as an admin' do
it 'contains the note of the user' do
get api("/users/#{user.id}", admin)
expect(json_response).to have_key('note')
expect(json_response['note']).to eq(user.note)
end
end
context 'as a regular user' do
it 'does not contain the note of the user' do
get api("/users/#{user.id}", user)
expect(json_response).not_to have_key('note')
end
end
end
end
describe "PUT /users/:id" do
context 'when user is an admin' do
it "updates note of the user" do
new_note = '2019-07-07 | Email changed | user requested | www.gitlab.com'
expect do
put api("/users/#{user.id}", admin), params: { note: new_note }
end.to change { user.reload.note }
.from('2018-11-05 | 2FA removed | user requested | www.gitlab.com')
.to(new_note)
expect(response).to have_gitlab_http_status(:success)
expect(json_response['note']).to eq(new_note)
end
end
context 'when user is not an admin' do
it "cannot update their own note" do
expect do
put api("/users/#{user.id}", user), params: { note: 'new note' }
end.not_to change { user.reload.note }
expect(response).to have_gitlab_http_status(:forbidden)
end
end
end
describe 'GET /users/' do
context 'when unauthenticated' do
it "does not contain the note of users" do
get api("/users"), params: { username: user.username }
expect(json_response.first).not_to have_key('note')
end
end
context 'when authenticated' do
context 'as a regular user' do
it 'does not contain the note of users' do
get api("/users", user), params: { username: user.username }
expect(json_response.first).not_to have_key('note')
end
end
context 'as an admin' do
it 'contains the note of users' do
get api("/users", admin), params: { username: user.username }
expect(response).to have_gitlab_http_status(:success)
expect(json_response.first).to have_key('note')
expect(json_response.first['note']).to eq '2018-11-05 | 2FA removed | user requested | www.gitlab.com'
end
end
end
end
describe 'GET /user' do
context 'when authenticated' do
context 'as an admin' do
context 'accesses their own profile' do
it 'contains the note of the user' do
get api("/user", admin)
expect(json_response).to have_key('note')
expect(json_response['note']).to eq(admin.note)
end
end
context 'sudo' do
let(:admin_personal_access_token) { create(:personal_access_token, user: admin, scopes: %w[api sudo]).token }
context 'accesses the profile of another regular user' do
it 'does not contain the note of the user' do
get api("/user?private_token=#{admin_personal_access_token}&sudo=#{user.id}")
expect(json_response['id']).to eq(user.id)
expect(json_response).not_to have_key('note')
end
end
context 'accesses the profile of another admin' do
let(:admin_2) {create(:admin, note: '2010-10-10 | 2FA added | admin requested | www.gitlab.com')}
it 'contains the note of the user' do
get api("/user?private_token=#{admin_personal_access_token}&sudo=#{admin_2.id}")
expect(json_response['id']).to eq(admin_2.id)
expect(json_response).to have_key('note')
expect(json_response['note']).to eq(admin_2.note)
end
end
end
end
context 'as a regular user' do
it 'does not contain the note of the user' do
get api("/user", user)
expect(json_response).not_to have_key('note')
end
end
end
end
end
shared_examples 'rendering user status' do
it 'returns the status if there was one' do
create(:user_status, user: user)

View file

@ -1621,6 +1621,29 @@ describe QuickActions::InterpretService do
expect(message).to eq("Created branch '#{branch_name}' and a merge request to resolve this issue.")
end
end
context 'submit_review command' do
using RSpec::Parameterized::TableSyntax
where(:note) do
[
'I like it',
'/submit_review'
]
end
with_them do
let(:content) { '/submit_review' }
let!(:draft_note) { create(:draft_note, note: note, merge_request: merge_request, author: developer) }
it 'submits the users current review' do
_, _, message = service.execute(content, merge_request)
expect { draft_note.reload }.to raise_error(ActiveRecord::RecordNotFound)
expect(message).to eq('Submitted the current review.')
end
end
end
end
describe '#explain' do

View file

@ -0,0 +1,12 @@
# frozen_string_literal: true
RSpec.shared_examples 'parsing gl_repository identifier' do
subject { described_class.new(identifier) }
it 'returns correct information' do
aggregate_failures do
expect(subject.repo_type).to eq(expected_type)
expect(subject.fetch_container!).to eq(expected_container)
end
end
end

View file

@ -7,26 +7,6 @@ RSpec.shared_examples 'a repo type' do
it { is_expected.to eq(expected_identifier) }
end
describe '#fetch_id' do
it 'finds an id match in the identifier' do
expect(described_class.fetch_id(expected_identifier)).to eq(expected_id)
end
it 'does not break on other identifiers' do
expect(described_class.fetch_id('wiki-noid')).to eq(nil)
end
end
describe '#fetch_container!' do
it 'returns the container' do
expect(described_class.fetch_container!(expected_identifier)).to eq expected_container
end
it 'raises an exception if the identifier is invalid' do
expect { described_class.fetch_container!('project-noid') }.to raise_error ArgumentError
end
end
describe '#path_suffix' do
subject { described_class.path_suffix }

View file

@ -49,4 +49,14 @@ describe NewNoteWorker do
described_class.new.perform(unexistent_note_id)
end
end
context 'when note is with review' do
it 'does not create a new note notification' do
note = create(:note, :with_review)
expect_any_instance_of(NotificationService).not_to receive(:new_note)
subject.perform(note.id)
end
end
end

View file

@ -355,7 +355,7 @@ describe PostReceive do
context "webhook" do
it "fetches the correct project" do
expect(Project).to receive(:find_by).with(id: project.id.to_s)
expect(Project).to receive(:find_by).with(id: project.id)
perform
end