Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-06-16 21:08:20 +00:00
parent 4f5c8572e9
commit a2dbe2a8d8
45 changed files with 1156 additions and 46 deletions

View File

@ -215,10 +215,10 @@ export default {
</div>
<div class="form-group flex flex-wrap">
<gl-form-checkbox :checked="wafLogEnabled" @input="wafLogChanged">
<strong>{{ s__('ClusterIntegration|Send ModSecurity Logs') }}</strong>
<strong>{{ s__('ClusterIntegration|Send Web Application Firewall Logs') }}</strong>
</gl-form-checkbox>
<gl-form-checkbox :checked="ciliumLogEnabled" @input="ciliumLogChanged">
<strong>{{ s__('ClusterIntegration|Send Cilium Logs') }}</strong>
<strong>{{ s__('ClusterIntegration|Send Container Network Policies Logs') }}</strong>
</gl-form-checkbox>
</div>
<div v-if="showButtons" class="mt-3">

View File

@ -13,12 +13,12 @@ import {
GlIcon,
} from '@gitlab/ui';
import eventHub from '~/clusters/event_hub';
import modSecurityLogo from 'images/cluster_app_logos/modsecurity.png';
import modSecurityLogo from 'images/cluster_app_logos/gitlab.png';
const { UPDATING, UNINSTALLING, INSTALLING, INSTALLED, UPDATED } = APPLICATION_STATUS;
export default {
title: 'ModSecurity Web Application Firewall',
title: __('Web Application Firewall'),
modsecurityUrl: 'https://modsecurity.org/about.html',
components: {
GlAlert,

View File

@ -1,9 +1,13 @@
<script>
import { __ } from '~/locale';
import { mapGetters } from 'vuex';
import { GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui';
import resolvedStatusMixin from '~/batch_comments/mixins/resolved_status';
import Icon from '~/vue_shared/components/icon.vue';
import ReplyButton from './note_actions/reply_button.vue';
import eventHub from '~/sidebar/event_hub';
import Api from '~/api';
import flash from '~/flash';
export default {
name: 'NoteActions',
@ -17,6 +21,10 @@ export default {
},
mixins: [resolvedStatusMixin],
props: {
author: {
type: Object,
required: true,
},
authorId: {
type: Number,
required: true,
@ -87,7 +95,7 @@ export default {
},
},
computed: {
...mapGetters(['getUserDataByProp']),
...mapGetters(['getUserDataByProp', 'getNoteableData']),
shouldShowActionsDropdown() {
return this.currentUserId && (this.canEdit || this.canReportAsAbuse);
},
@ -100,6 +108,26 @@ export default {
currentUserId() {
return this.getUserDataByProp('id');
},
isUserAssigned() {
return this.assignees && this.assignees.some(({ id }) => id === this.author.id);
},
displayAssignUserText() {
return this.isUserAssigned
? __('Unassign from commenting user')
: __('Assign to commenting user');
},
sidebarAction() {
return this.isUserAssigned ? 'sidebar.addAssignee' : 'sidebar.removeAssignee';
},
targetType() {
return this.getNoteableData.targetType;
},
assignees() {
return this.getNoteableData.assignees || [];
},
isIssue() {
return this.targetType === 'issue';
},
},
methods: {
onEdit() {
@ -116,6 +144,29 @@ export default {
this.$root.$emit('bv::hide::tooltip');
});
},
handleAssigneeUpdate(assignees) {
this.$emit('updateAssignees', assignees);
eventHub.$emit(this.sidebarAction, this.author);
eventHub.$emit('sidebar.saveAssignees');
},
assignUser() {
let { assignees } = this;
const { project_id, iid } = this.getNoteableData;
if (this.isUserAssigned) {
assignees = assignees.filter(assignee => assignee.id !== this.author.id);
} else {
assignees.push({ id: this.author.id });
}
if (this.targetType === 'issue') {
Api.updateIssue(project_id, iid, {
assignee_ids: assignees.map(assignee => assignee.id),
})
.then(() => this.handleAssigneeUpdate(assignees))
.catch(() => flash(__('Something went wrong while updating assignees')));
}
},
},
};
</script>
@ -215,6 +266,16 @@ export default {
<span class="text-danger">{{ __('Delete comment') }}</span>
</button>
</li>
<li v-if="isIssue">
<button
class="btn-default btn-transparent"
data-testid="assign-user"
type="button"
@click="assignUser"
>
{{ displayAssignUserText }}
</button>
</li>
</ul>
</div>
</div>

View File

@ -184,6 +184,7 @@ export default {
'updateNote',
'toggleResolveNote',
'scrollToNoteIfNeeded',
'updateAssignees',
]),
editHandler() {
this.isEditing = true;
@ -299,6 +300,9 @@ export default {
getLineClasses(lineNumber) {
return getLineClasses(lineNumber);
},
assigneesUpdate(assignees) {
this.updateAssignees(assignees);
},
},
};
</script>
@ -355,6 +359,7 @@ export default {
<span v-else-if="note.created_at" class="d-none d-sm-inline">&middot;</span>
</note-header>
<note-actions
:author="author"
:author-id="author.id"
:note-id="note.id"
:note-url="note.noteable_note_url"
@ -377,6 +382,7 @@ export default {
@handleDelete="deleteHandler"
@handleResolve="resolveHandler"
@startReplying="$emit('startReplying')"
@updateAssignees="assigneesUpdate"
/>
</div>
<div class="timeline-discussion-body">

View File

@ -647,5 +647,9 @@ export const receiveDeleteDescriptionVersionError = ({ commit }, error) => {
commit(types.RECEIVE_DELETE_DESCRIPTION_VERSION_ERROR, error);
};
export const updateAssignees = ({ commit }, assignees) => {
commit(types.UPDATE_ASSIGNEES, assignees);
};
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};

View File

@ -23,6 +23,7 @@ export const REMOVE_SUGGESTION_FROM_BATCH = 'REMOVE_SUGGESTION_FROM_BATCH';
export const CLEAR_SUGGESTION_BATCH = 'CLEAR_SUGGESTION_BATCH';
export const CONVERT_TO_DISCUSSION = 'CONVERT_TO_DISCUSSION';
export const REMOVE_CONVERTED_DISCUSSION = 'REMOVE_CONVERTED_DISCUSSION';
export const UPDATE_ASSIGNEES = 'UPDATE_ASSIGNEES';
// DISCUSSION
export const COLLAPSE_DISCUSSION = 'COLLAPSE_DISCUSSION';

View File

@ -355,4 +355,7 @@ export default {
[types.RECEIVE_DELETE_DESCRIPTION_VERSION_ERROR](state) {
state.isLoadingDescriptionVersion = false;
},
[types.UPDATE_ASSIGNEES](state, assignees) {
state.noteableData.assignees = assignees;
},
};

View File

@ -1,10 +1,12 @@
import Vue from 'vue';
import { __ } from '~/locale';
import { GlColumnChart } from '@gitlab/ui/dist/charts';
import CodeCoverage from '../components/code_coverage.vue';
import SeriesDataMixin from './series_data_mixin';
document.addEventListener('DOMContentLoaded', () => {
const languagesContainer = document.getElementById('js-languages-chart');
const codeCoverageContainer = document.getElementById('js-code-coverage-chart');
const monthContainer = document.getElementById('js-month-chart');
const weekdayContainer = document.getElementById('js-weekday-chart');
const hourContainer = document.getElementById('js-hour-chart');
@ -57,6 +59,18 @@ document.addEventListener('DOMContentLoaded', () => {
},
});
// eslint-disable-next-line no-new
new Vue({
el: codeCoverageContainer,
render(h) {
return h(CodeCoverage, {
props: {
graphEndpoint: codeCoverageContainer.dataset?.graphEndpoint,
},
});
},
});
// eslint-disable-next-line no-new
new Vue({
el: monthContainer,

View File

@ -0,0 +1,177 @@
<script>
import { GlAlert, GlDropdown, GlDropdownItem, GlIcon, GlSprintf } from '@gitlab/ui';
import { GlAreaChart } from '@gitlab/ui/dist/charts';
import dateFormat from 'dateformat';
import axios from '~/lib/utils/axios_utils';
import { get } from 'lodash';
import { __ } from '~/locale';
export default {
components: {
GlAlert,
GlAreaChart,
GlDropdown,
GlDropdownItem,
GlIcon,
GlSprintf,
},
props: {
graphEndpoint: {
type: String,
required: true,
},
},
data() {
return {
dailyCoverageData: [],
hasFetchError: false,
isLoading: true,
selectedCoverageIndex: 0,
tooltipTitle: '',
coveragePercentage: '',
chartOptions: {
yAxis: {
name: __('Bi-weekly code coverage'),
type: 'value',
min: 0,
max: 100,
},
xAxis: {
name: '',
type: 'category',
},
},
};
},
computed: {
hasData() {
return this.dailyCoverageData.length > 0;
},
isReady() {
return !this.isLoading && !this.hasFetchError;
},
canShowData() {
return this.isReady && this.hasData;
},
noDataAvailable() {
return this.isReady && !this.hasData;
},
selectedDailyCoverage() {
return this.hasData && this.dailyCoverageData[this.selectedCoverageIndex];
},
selectedDailyCoverageName() {
return this.selectedDailyCoverage?.group_name;
},
formattedData() {
if (this.selectedDailyCoverage?.data) {
return this.selectedDailyCoverage.data.map(value => [
dateFormat(value.date, 'mmm dd'),
value.coverage,
]);
}
// If the fetching failed, we return an empty array which
// allow the graph to render while empty
return [];
},
chartData() {
return [
{
// The default string 'data' will get shown in the legend if we fail to fetch the data
name: this.canShowData ? this.selectedDailyCoverageName : __('data'),
data: this.formattedData,
type: 'line',
smooth: true,
},
];
},
},
created() {
axios
.get(this.graphEndpoint)
.then(({ data }) => {
this.dailyCoverageData = data;
})
.catch(() => {
this.hasFetchError = true;
})
.finally(() => {
this.isLoading = false;
});
},
methods: {
setSelectedCoverage(index) {
this.selectedCoverageIndex = index;
},
formatTooltipText(params) {
this.tooltipTitle = params.value;
this.coveragePercentage = get(params, 'seriesData[0].data[1]', '');
},
},
height: 200,
};
</script>
<template>
<div>
<div class="gl-mt-3 gl-mb-3">
<gl-alert
v-if="hasFetchError"
variant="danger"
:title="s__('Code Coverage|Couldn\'t fetch the code coverage data')"
:dismissible="false"
/>
<gl-alert
v-if="noDataAvailable"
variant="info"
:title="s__('Code Coverage| Empty code coverage data')"
:dismissible="false"
>
<span>
{{ __('It seems that there is currently no available data for code coverage') }}
</span>
</gl-alert>
<gl-dropdown v-if="canShowData" :text="selectedDailyCoverageName">
<gl-dropdown-item
v-for="({ group_name }, index) in dailyCoverageData"
:key="index"
:value="group_name"
@click="setSelectedCoverage(index)"
>
<div class="gl-display-flex">
<gl-icon
v-if="index === selectedCoverageIndex"
name="mobile-issue-close"
class="gl-absolute"
/>
<span class="gl-display-flex align-items-center ml-4">
{{ group_name }}
</span>
</div>
</gl-dropdown-item>
</gl-dropdown>
</div>
<gl-area-chart
v-if="!isLoading"
:height="$options.height"
:data="chartData"
:option="chartOptions"
:format-tooltip-text="formatTooltipText"
>
<template v-if="canShowData" #tooltipTitle>
{{ tooltipTitle }}
</template>
<template v-if="canShowData" #tooltipContent>
<gl-sprintf :message="__('Code Coverage: %{coveragePercentage}%{percentSymbol}')">
<template #coveragePercentage>
{{ coveragePercentage }}
</template>
<template #percentSymbol>
%
</template>
</gl-sprintf>
</template>
</gl-area-chart>
</div>
</template>

View File

@ -402,7 +402,6 @@ img.emoji {
.prepend-top-default { margin-top: $gl-padding !important; }
.prepend-top-16 { margin-top: 16px; }
.prepend-top-20 { margin-top: 20px; }
.prepend-top-32 { margin-top: 32px; }
.prepend-left-5 { margin-left: 5px; }
.prepend-left-10 { margin-left: 10px; }
.prepend-left-15 { margin-left: 15px; }

View File

@ -149,17 +149,17 @@ $orange-800: #a35200 !default;
$orange-900: #853c00 !default;
$orange-950: #592800 !default;
$red-50: #fef6f5 !default;
$red-100: #fbe5e1 !default;
$red-200: #f2b4a9 !default;
$red-300: #ea8271 !default;
$red-400: #e05842 !default;
$red-500: #db3b21 !default;
$red-600: #c0341d !default;
$red-700: #a62d19 !default;
$red-800: #8b2615 !default;
$red-900: #711e11 !default;
$red-950: #4b140b !default;
$red-50: #fcf1ef !default;
$red-100: #fdd4cd !default;
$red-200: #fcb5aa !default;
$red-300: #f57f6c !default;
$red-400: #ec5941 !default;
$red-500: #dd2b0e !default;
$red-600: #c91c00 !default;
$red-700: #ae1800 !default;
$red-800: #8d1300 !default;
$red-900: #660e00 !default;
$red-950: #4d0a00 !default;
$gray-10: #fafafa !default;
$gray-50: #f0f0f0 !default;

View File

@ -35,7 +35,11 @@ module AlertManagement
attr_reader :alert, :current_user, :params
def allowed?
current_user.can?(:update_alert_management_alert, alert)
current_user&.can?(:update_alert_management_alert, alert)
end
def assignee_todo_allowed?
assignee&.can?(:read_alert_management_alert, alert)
end
def todo_service
@ -80,9 +84,10 @@ module AlertManagement
end
def assign_todo
return unless assignee
# Remove check in follow-up issue https://gitlab.com/gitlab-org/gitlab/-/issues/222672
return unless assignee_todo_allowed?
todo_service.assign_alert(alert, assignee)
todo_service.assign_alert(alert, current_user)
end
def add_assignee_system_note(old_assignees)

View File

@ -22,7 +22,7 @@
= render 'stat_anchor_list', anchors: @project.empty_repo_statistics_buttons
- if can?(current_user, :push_code, @project)
.empty-wrapper.prepend-top-32
.empty-wrapper.gl-mt-7
%h3#repo-command-line-instructions.page-title-empty
= _('Command line instructions')
%p

View File

@ -28,7 +28,7 @@
%a.btn.btn-sm{ href: "#{download_path}?#{@daily_coverage_options[:base_params].to_query}" }
%small
= _("Download raw data (.csv)")
#js-code-coverage-chart{ data: { daily_coverage_options: @daily_coverage_options.to_json.html_safe } }
#js-code-coverage-chart{ data: { graph_endpoint: "#{@daily_coverage_options[:graph_api_path]}?#{@daily_coverage_options[:base_params].to_query}" } }
.repo-charts
.sub-header-block.border-top

View File

@ -1,4 +1,4 @@
.row.prepend-top-32.append-bottom-default
.row.gl-mt-7.append-bottom-default
.col-lg-3
%h4.gl-mt-0
Recent Deliveries

View File

@ -0,0 +1,5 @@
---
title: Resolve Add a button to assign users who have commented on an issue
merge_request: 23883
author:
type: added

View File

@ -0,0 +1,5 @@
---
title: Update red hex values to match GitLab UI
merge_request: 34544
author:
type: other

View File

@ -0,0 +1,5 @@
---
title: Add :section to approval_merge_request_rule unique index
merge_request: 33121
author:
type: other

View File

@ -0,0 +1,5 @@
---
title: GraphQL - properly handle pagination of millisecond-precision timestamps
merge_request: 34352
author:
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Resolve Graph code coverage changes over time for a project
merge_request: 26174
author:
type: added

View File

@ -0,0 +1,5 @@
---
title: Change from vendor specific to Gitlab
merge_request: 34576
author:
type: changed

View File

@ -0,0 +1,6 @@
---
title: Updated Auto DevOps with a fix to delete PostgreSQL PVC on environment cleanup,
a fix for multiline K8S_SECRET variables, updated Helm to 2.16.7 and glibc to 2.31
merge_request: 34399
author: verenion
type: fixed

View File

@ -0,0 +1,112 @@
# frozen_string_literal: true
class UpdateIndexApprovalRuleNameForCodeOwnersRuleType < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
LEGACY_INDEX_NAME_RULE_TYPE = "index_approval_rule_name_for_code_owners_rule_type"
LEGACY_INDEX_NAME_CODE_OWNERS = "approval_rule_name_index_for_code_owners"
SECTIONAL_INDEX_NAME = "index_approval_rule_name_for_sectional_code_owners_rule_type"
CODE_OWNER_RULE_TYPE = 2
def up
unless index_exists_by_name?(:approval_merge_request_rules, SECTIONAL_INDEX_NAME)
# Ensure only 1 code_owner rule with the same name and section per merge_request
#
add_concurrent_index(
:approval_merge_request_rules,
[:merge_request_id, :name, :section],
unique: true,
where: "rule_type = #{CODE_OWNER_RULE_TYPE}",
name: SECTIONAL_INDEX_NAME
)
end
remove_concurrent_index_by_name :approval_merge_request_rules, LEGACY_INDEX_NAME_RULE_TYPE
remove_concurrent_index_by_name :approval_merge_request_rules, LEGACY_INDEX_NAME_CODE_OWNERS
add_concurrent_index(
:approval_merge_request_rules,
[:merge_request_id, :name],
unique: true,
where: "rule_type = #{CODE_OWNER_RULE_TYPE} AND section IS NULL",
name: LEGACY_INDEX_NAME_RULE_TYPE
)
add_concurrent_index(
:approval_merge_request_rules,
[:merge_request_id, :code_owner, :name],
unique: true,
where: "code_owner = true AND section IS NULL",
name: LEGACY_INDEX_NAME_CODE_OWNERS
)
end
def down
# In a rollback situation, we can't guarantee that there will not be
# records that were allowed under the more specific SECTIONAL_INDEX_NAME
# index but would cause uniqueness violations under both the
# LEGACY_INDEX_NAME_RULE_TYPE and LEGACY_INDEX_NAME_CODE_OWNERS indices.
# Therefore, we need to first find all the MergeRequests with
# ApprovalMergeRequestRules that would violate these "new" indices and
# delete those approval rules, then create the new index, then finally
# recreate the approval rules for those merge requests.
#
# First, find all MergeRequests with ApprovalMergeRequestRules that will
# violate the new index.
#
merge_request_ids = ApprovalMergeRequestRule
.select(:merge_request_id)
.where(rule_type: CODE_OWNER_RULE_TYPE)
.group(:merge_request_id, :rule_type, :name)
.includes(:merge_request)
.having("count(*) > 1")
.collect(&:merge_request_id)
# Delete ALL their code_owner approval rules
#
merge_request_ids.each_slice(10) do |ids|
ApprovalMergeRequestRule.where(merge_request_id: ids).code_owner.delete_all
end
# Remove legacy partial indices that only apply to `section IS NULL` records
#
remove_concurrent_index_by_name :approval_merge_request_rules, LEGACY_INDEX_NAME_RULE_TYPE
remove_concurrent_index_by_name :approval_merge_request_rules, LEGACY_INDEX_NAME_CODE_OWNERS
# Reconstruct original "legacy" indices
#
add_concurrent_index(
:approval_merge_request_rules,
[:merge_request_id, :name],
unique: true,
where: "rule_type = #{CODE_OWNER_RULE_TYPE}",
name: LEGACY_INDEX_NAME_RULE_TYPE
)
add_concurrent_index(
:approval_merge_request_rules,
[:merge_request_id, :code_owner, :name],
unique: true,
where: "code_owner = true",
name: LEGACY_INDEX_NAME_CODE_OWNERS
)
# MergeRequest::SyncCodeOwnerApprovalRules recreates the code_owner rules
# from scratch, adding them to the index. Duplicates will be rejected.
#
merge_request_ids.each_slice(10) do |ids|
MergeRequest.where(id: ids).each do |merge_request|
MergeRequests::SyncCodeOwnerApprovalRules.new(merge_request).execute
end
end
remove_concurrent_index_by_name :approval_merge_request_rules, SECTIONAL_INDEX_NAME
end
end

View File

@ -0,0 +1,17 @@
# frozen_string_literal: true
class AddIndexToComposerMetadata < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
add_concurrent_index(:packages_composer_metadata, [:package_id, :target_sha], unique: true)
end
def down
remove_concurrent_index(:packages_composer_metadata, [:package_id, :target_sha])
end
end

View File

@ -9156,7 +9156,7 @@ CREATE UNIQUE INDEX any_approver_merge_request_rule_type_unique_index ON public.
CREATE UNIQUE INDEX any_approver_project_rule_type_unique_index ON public.approval_project_rules USING btree (project_id) WHERE (rule_type = 3);
CREATE UNIQUE INDEX approval_rule_name_index_for_code_owners ON public.approval_merge_request_rules USING btree (merge_request_id, code_owner, name) WHERE (code_owner = true);
CREATE UNIQUE INDEX approval_rule_name_index_for_code_owners ON public.approval_merge_request_rules USING btree (merge_request_id, code_owner, name) WHERE ((code_owner = true) AND (section IS NULL));
CREATE INDEX ci_builds_gitlab_monitor_metrics ON public.ci_builds USING btree (status, created_at, project_id) WHERE ((type)::text = 'Ci::Build'::text);
@ -9334,7 +9334,9 @@ CREATE UNIQUE INDEX index_approval_project_rules_users_1 ON public.approval_proj
CREATE INDEX index_approval_project_rules_users_2 ON public.approval_project_rules_users USING btree (user_id);
CREATE UNIQUE INDEX index_approval_rule_name_for_code_owners_rule_type ON public.approval_merge_request_rules USING btree (merge_request_id, name) WHERE (rule_type = 2);
CREATE UNIQUE INDEX index_approval_rule_name_for_code_owners_rule_type ON public.approval_merge_request_rules USING btree (merge_request_id, name) WHERE ((rule_type = 2) AND (section IS NULL));
CREATE UNIQUE INDEX index_approval_rule_name_for_sectional_code_owners_rule_type ON public.approval_merge_request_rules USING btree (merge_request_id, name, section) WHERE (rule_type = 2);
CREATE INDEX index_approval_rules_code_owners_rule_type ON public.approval_merge_request_rules USING btree (merge_request_id) WHERE (rule_type = 2);
@ -10430,6 +10432,8 @@ CREATE UNIQUE INDEX index_packages_build_infos_on_package_id ON public.packages_
CREATE INDEX index_packages_build_infos_on_pipeline_id ON public.packages_build_infos USING btree (pipeline_id);
CREATE UNIQUE INDEX index_packages_composer_metadata_on_package_id_and_target_sha ON public.packages_composer_metadata USING btree (package_id, target_sha);
CREATE UNIQUE INDEX index_packages_conan_file_metadata_on_package_file_id ON public.packages_conan_file_metadata USING btree (package_file_id);
CREATE UNIQUE INDEX index_packages_conan_metadata_on_package_id_username_channel ON public.packages_conan_metadata USING btree (package_id, package_username, package_channel);
@ -13959,6 +13963,7 @@ COPY "schema_migrations" (version) FROM STDIN;
20200526153844
20200526164946
20200526164947
20200526231421
20200527092027
20200527094322
20200527095401
@ -13995,5 +14000,6 @@ COPY "schema_migrations" (version) FROM STDIN;
20200615083635
20200615121217
20200615123055
20200615232735
\.

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

View File

@ -134,15 +134,18 @@ in the jobs table.
A few examples of known coverage tools for a variety of languages can be found
in the pipelines settings page.
### Download test coverage history
### Code Coverage history
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/209121) in GitLab 12.10.
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/209121) the ability to download a `.csv` in GitLab 12.10.
> - [Graph introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/33743) in GitLab 13.1.
If you want to see the evolution of your project code coverage over time,
you can download a CSV file with this data. From your project:
you can view a graph or download a CSV file with this data. From your project:
1. Go to **{chart}** **Project Analytics > Repository**.
1. Click **Download raw data (`.csv`)**
1. Go to **{chart}** **Project Analytics > Repository** to see the historic data for each job listed in the dropdown above the graph.
1. If you want a CSV file of that data, click **Download raw data (.csv)**
![Code coverage graph of a project over time](img/code_coverage_graph_v13_1.png)
### Removing color codes

View File

@ -33,6 +33,7 @@ The data in the charts are updated soon after each commit in the default branch.
Available charts:
- Programming languages used in the repository
- Code coverage history (last 3 months) ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/33743) in GitLab 13.1)
- Commit statistics (last month)
- Commits per day of month
- Commits per weekday

Binary file not shown.

After

Width:  |  Height:  |  Size: 212 KiB

View File

@ -532,3 +532,15 @@ to the original comment, so a note about when it was last edited will appear und
This feature only exists for Issues, Merge requests, and Epics. Commits, Snippets and Merge request diff threads are
not supported yet.
## Assign an issue to the commenting user
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/191455) in GitLab 13.1.
You can assign an issue to a user who made a comment.
In the comment, click the **More Actions** menu and click **Assign to commenting user**.
Click the button again to unassign the commenter.
![Assign to commenting user](img/quickly_assign_commenter_v13_1.png)

View File

@ -1,5 +1,5 @@
.dast-auto-deploy:
image: "registry.gitlab.com/gitlab-org/cluster-integration/auto-deploy-image:v0.16.1"
image: "registry.gitlab.com/gitlab-org/cluster-integration/auto-deploy-image:v0.17.0"
dast_environment_deploy:
extends: .dast-auto-deploy

View File

@ -1,5 +1,5 @@
.auto-deploy:
image: "registry.gitlab.com/gitlab-org/cluster-integration/auto-deploy-image:v0.16.1"
image: "registry.gitlab.com/gitlab-org/cluster-integration/auto-deploy-image:v0.17.0"
include:
- template: Jobs/Deploy/ECS.gitlab-ci.yml

View File

@ -194,7 +194,12 @@ module Gitlab
order_list.each do |field|
field_name = field.attribute_name
ordering[field_name] = node[field_name].to_s
field_value = node[field_name]
ordering[field_name] = if field_value.is_a?(Time)
field_value.strftime('%Y-%m-%d %H:%M:%S.%N %Z')
else
field_value.to_s
end
end
encode(ordering.to_json)

View File

@ -2970,6 +2970,9 @@ msgstr ""
msgid "Assign to"
msgstr ""
msgid "Assign to commenting user"
msgstr ""
msgid "Assign yourself to these issues"
msgstr ""
@ -3415,6 +3418,9 @@ msgstr ""
msgid "Beta"
msgstr ""
msgid "Bi-weekly code coverage"
msgstr ""
msgid "Billing"
msgstr ""
@ -5362,10 +5368,10 @@ msgstr ""
msgid "ClusterIntegration|Select zone to choose machine type"
msgstr ""
msgid "ClusterIntegration|Send Cilium Logs"
msgid "ClusterIntegration|Send Container Network Policies Logs"
msgstr ""
msgid "ClusterIntegration|Send ModSecurity Logs"
msgid "ClusterIntegration|Send Web Application Firewall Logs"
msgstr ""
msgid "ClusterIntegration|Service Token"
@ -5575,6 +5581,15 @@ msgstr ""
msgid "Code"
msgstr ""
msgid "Code Coverage: %{coveragePercentage}%{percentSymbol}"
msgstr ""
msgid "Code Coverage| Empty code coverage data"
msgstr ""
msgid "Code Coverage|Couldn't fetch the code coverage data"
msgstr ""
msgid "Code Owners"
msgstr ""
@ -12512,6 +12527,9 @@ msgstr ""
msgid "It seems like the Dependency Scanning job ran successfully, but no dependencies have been detected in your project."
msgstr ""
msgid "It seems that there is currently no available data for code coverage"
msgstr ""
msgid "It's you"
msgstr ""
@ -20950,6 +20968,9 @@ msgstr ""
msgid "Something went wrong while updating a requirement."
msgstr ""
msgid "Something went wrong while updating assignees"
msgstr ""
msgid "Something went wrong while updating your list settings"
msgstr ""
@ -24059,6 +24080,9 @@ msgstr ""
msgid "Unarchiving the project will restore people's ability to make changes to it. The repository can be committed to, and issues, comments, and other entities can be created. %{strong_start}Once active, this project shows up in the search and on the dashboard.%{strong_end}"
msgstr ""
msgid "Unassign from commenting user"
msgstr ""
msgid "Unblock"
msgstr ""
@ -25317,6 +25341,9 @@ msgstr ""
msgid "We've found no vulnerabilities"
msgstr ""
msgid "Web Application Firewall"
msgstr ""
msgid "Web IDE"
msgstr ""
@ -26707,6 +26734,9 @@ msgstr ""
msgid "customize"
msgstr ""
msgid "data"
msgstr ""
msgid "date must not be after 9999-12-31"
msgstr ""

View File

@ -70,12 +70,12 @@ describe('FluentdOutputSettings', () => {
});
describe.each`
desc | changeFn | key | value
${'when protocol dropdown is triggered'} | ${() => changeProtocol(1)} | ${'protocol'} | ${'udp'}
${'when host is changed'} | ${() => changeHost('test-host')} | ${'host'} | ${'test-host'}
${'when port is changed'} | ${() => changePort(123)} | ${'port'} | ${123}
${'when wafLogEnabled changes'} | ${() => changeCheckbox(findCheckbox('Send ModSecurity Logs'))} | ${'wafLogEnabled'} | ${!defaultSettings.wafLogEnabled}
${'when ciliumLogEnabled changes'} | ${() => changeCheckbox(findCheckbox('Send Cilium Logs'))} | ${'ciliumLogEnabled'} | ${!defaultSettings.ciliumLogEnabled}
desc | changeFn | key | value
${'when protocol dropdown is triggered'} | ${() => changeProtocol(1)} | ${'protocol'} | ${'udp'}
${'when host is changed'} | ${() => changeHost('test-host')} | ${'host'} | ${'test-host'}
${'when port is changed'} | ${() => changePort(123)} | ${'port'} | ${123}
${'when wafLogEnabled changes'} | ${() => changeCheckbox(findCheckbox('Send Web Application Firewall Logs'))} | ${'wafLogEnabled'} | ${!defaultSettings.wafLogEnabled}
${'when ciliumLogEnabled changes'} | ${() => changeCheckbox(findCheckbox('Send Container Network Policies Logs'))} | ${'ciliumLogEnabled'} | ${!defaultSettings.ciliumLogEnabled}
`('$desc', ({ changeFn, key, value }) => {
beforeEach(() => {
changeFn();

View File

@ -4,26 +4,33 @@ import { TEST_HOST } from 'spec/test_constants';
import createStore from '~/notes/stores';
import noteActions from '~/notes/components/note_actions.vue';
import { userDataMock } from '../mock_data';
import AxiosMockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
describe('noteActions', () => {
let wrapper;
let store;
let props;
let actions;
let axiosMock;
const shallowMountNoteActions = propsData => {
const shallowMountNoteActions = (propsData, computed) => {
const localVue = createLocalVue();
return shallowMount(localVue.extend(noteActions), {
store,
propsData,
localVue,
computed,
});
};
beforeEach(() => {
store = createStore();
props = {
accessLevel: 'Maintainer',
authorId: 26,
authorId: 1,
author: userDataMock,
canDelete: true,
canEdit: true,
canAwardEmoji: true,
@ -33,10 +40,17 @@ describe('noteActions', () => {
reportAbusePath: `${TEST_HOST}/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F7%23note_539&user_id=26`,
showReply: false,
};
actions = {
updateAssignees: jest.fn(),
};
axiosMock = new AxiosMockAdapter(axios);
});
afterEach(() => {
wrapper.destroy();
axiosMock.restore();
});
describe('user is logged in', () => {
@ -76,6 +90,14 @@ describe('noteActions', () => {
it('should not show copy link action when `noteUrl` prop is empty', done => {
wrapper.setProps({
...props,
author: {
avatar_url: 'mock_path',
id: 26,
name: 'Example Maintainer',
path: '/ExampleMaintainer',
state: 'active',
username: 'ExampleMaintainer',
},
noteUrl: '',
});
@ -104,6 +126,25 @@ describe('noteActions', () => {
})
.catch(done.fail);
});
it('should be possible to assign or unassign the comment author', () => {
wrapper = shallowMountNoteActions(props, {
targetType: () => 'issue',
});
const assignUserButton = wrapper.find('[data-testid="assign-user"]');
expect(assignUserButton.exists()).toBe(true);
assignUserButton.trigger('click');
axiosMock.onPut(`${TEST_HOST}/api/v4/projects/group/project/issues/1`).reply(() => {
expect(actions.updateAssignees).toHaveBeenCalled();
});
});
it('should not be possible to assign or unassign the comment author in a merge request', () => {
const assignUserButton = wrapper.find('[data-testid="assign-user"]');
expect(assignUserButton.exists()).toBe(false);
});
});
});

View File

@ -1141,4 +1141,17 @@ describe('Actions Notes Store', () => {
});
});
});
describe('updateAssignees', () => {
it('update the assignees state', done => {
testAction(
actions.updateAssignees,
[userDataMock.id],
{ state: noteableDataMock },
[{ type: mutationTypes.UPDATE_ASSIGNEES, payload: [userDataMock.id] }],
[],
done,
);
});
});
});

View File

@ -805,4 +805,16 @@ describe('Notes Store mutations', () => {
expect(state.batchSuggestionsInfo.length).toEqual(0);
});
});
describe('UPDATE_ASSIGNEES', () => {
it('should update assignees', () => {
const state = {
noteableData: noteableDataMock,
};
mutations.UPDATE_ASSIGNEES(state, [userDataMock.id]);
expect(state.noteableData.assignees).toEqual([userDataMock.id]);
});
});
});

View File

@ -0,0 +1,88 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Code Coverage when fetching data is successful matches the snapshot 1`] = `
<div>
<div
class="gl-mt-3 gl-mb-3"
>
<!---->
<!---->
<gl-dropdown-stub
text="rspec"
>
<gl-dropdown-item-stub
value="rspec"
>
<div
class="gl-display-flex"
>
<gl-icon-stub
class="gl-absolute"
name="mobile-issue-close"
size="16"
/>
<span
class="gl-display-flex align-items-center ml-4"
>
rspec
</span>
</div>
</gl-dropdown-item-stub>
<gl-dropdown-item-stub
value="cypress"
>
<div
class="gl-display-flex"
>
<!---->
<span
class="gl-display-flex align-items-center ml-4"
>
cypress
</span>
</div>
</gl-dropdown-item-stub>
<gl-dropdown-item-stub
value="karma"
>
<div
class="gl-display-flex"
>
<!---->
<span
class="gl-display-flex align-items-center ml-4"
>
karma
</span>
</div>
</gl-dropdown-item-stub>
</gl-dropdown-stub>
</div>
<gl-area-chart-stub
annotations=""
data="[object Object]"
formattooltiptext="function () { [native code] }"
height="200"
includelegendavgmax="true"
legendaveragetext="Avg"
legendcurrenttext="Current"
legendlayout="inline"
legendmaxtext="Max"
legendmintext="Min"
option="[object Object]"
thresholds=""
/>
</div>
`;

View File

@ -0,0 +1,164 @@
import MockAdapter from 'axios-mock-adapter';
import { shallowMount } from '@vue/test-utils';
import { GlAlert, GlIcon, GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { GlAreaChart } from '@gitlab/ui/dist/charts';
import axios from '~/lib/utils/axios_utils';
import CodeCoverage from '~/pages/projects/graphs/components/code_coverage.vue';
import codeCoverageMockData from './mock_data';
import waitForPromises from 'helpers/wait_for_promises';
import httpStatusCodes from '~/lib/utils/http_status';
describe('Code Coverage', () => {
let wrapper;
let mockAxios;
const graphEndpoint = '/graph';
const findAlert = () => wrapper.find(GlAlert);
const findAreaChart = () => wrapper.find(GlAreaChart);
const findAllDropdownItems = () => wrapper.findAll(GlDropdownItem);
const findFirstDropdownItem = () => findAllDropdownItems().at(0);
const findSecondDropdownItem = () => findAllDropdownItems().at(1);
const createComponent = () => {
wrapper = shallowMount(CodeCoverage, {
propsData: {
graphEndpoint,
},
});
};
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
describe('when fetching data is successful', () => {
beforeEach(() => {
mockAxios = new MockAdapter(axios);
mockAxios.onGet().replyOnce(httpStatusCodes.OK, codeCoverageMockData);
createComponent();
return waitForPromises();
});
afterEach(() => {
mockAxios.restore();
});
it('renders the area chart', () => {
expect(findAreaChart().exists()).toBe(true);
});
it('matches the snapshot', () => {
expect(wrapper.element).toMatchSnapshot();
});
it('shows no error messages', () => {
expect(findAlert().exists()).toBe(false);
});
});
describe('when fetching data fails', () => {
beforeEach(() => {
mockAxios = new MockAdapter(axios);
mockAxios.onGet().replyOnce(httpStatusCodes.BAD_REQUEST);
createComponent();
return waitForPromises();
});
afterEach(() => {
mockAxios.restore();
});
it('renders an error message', () => {
expect(findAlert().exists()).toBe(true);
expect(findAlert().attributes().variant).toBe('danger');
});
it('still renders an empty graph', () => {
expect(findAreaChart().exists()).toBe(true);
});
});
describe('when fetching data succeed but returns an empty state', () => {
beforeEach(() => {
mockAxios = new MockAdapter(axios);
mockAxios.onGet().replyOnce(httpStatusCodes.OK, []);
createComponent();
return waitForPromises();
});
afterEach(() => {
mockAxios.restore();
});
it('renders an information message', () => {
expect(findAlert().exists()).toBe(true);
expect(findAlert().attributes().variant).toBe('info');
});
it('still renders an empty graph', () => {
expect(findAreaChart().exists()).toBe(true);
});
});
describe('dropdown options', () => {
beforeEach(() => {
mockAxios = new MockAdapter(axios);
mockAxios.onGet().replyOnce(httpStatusCodes.OK, codeCoverageMockData);
createComponent();
return waitForPromises();
});
it('renders the dropdown with all custom names as options', () => {
expect(wrapper.contains(GlDropdown)).toBeDefined();
expect(findAllDropdownItems()).toHaveLength(codeCoverageMockData.length);
expect(findFirstDropdownItem().text()).toBe(codeCoverageMockData[0].group_name);
});
});
describe('interactions', () => {
beforeEach(() => {
mockAxios = new MockAdapter(axios);
mockAxios.onGet().replyOnce(httpStatusCodes.OK, codeCoverageMockData);
createComponent();
return waitForPromises();
});
it('updates the selected dropdown option with an icon', async () => {
findSecondDropdownItem().vm.$emit('click');
await wrapper.vm.$nextTick();
expect(
findFirstDropdownItem()
.find(GlIcon)
.exists(),
).toBe(false);
expect(findSecondDropdownItem().contains(GlIcon)).toBe(true);
});
it('updates the graph data when selecting a different option in dropdown', async () => {
const originalSelectedData = wrapper.vm.selectedDailyCoverage;
const expectedData = codeCoverageMockData[1];
findSecondDropdownItem().vm.$emit('click');
await wrapper.vm.$nextTick();
expect(wrapper.vm.selectedDailyCoverage).not.toBe(originalSelectedData);
expect(wrapper.vm.selectedDailyCoverage).toBe(expectedData);
});
});
});

View File

@ -0,0 +1,60 @@
export default [
{
group_name: 'rspec',
data: [
{ date: '2020-04-30', coverage: 40.0 },
{ date: '2020-05-01', coverage: 80.0 },
{ date: '2020-05-02', coverage: 99.0 },
{ date: '2020-05-10', coverage: 80.0 },
{ date: '2020-05-15', coverage: 70.0 },
{ date: '2020-05-20', coverage: 69.0 },
],
},
{
group_name: 'cypress',
data: [
{ date: '2022-07-30', coverage: 1.0 },
{ date: '2022-08-01', coverage: 2.4 },
{ date: '2022-08-02', coverage: 5.0 },
{ date: '2022-08-10', coverage: 15.0 },
{ date: '2022-08-15', coverage: 30.0 },
{ date: '2022-08-20', coverage: 40.0 },
],
},
{
group_name: 'karma',
data: [
{ date: '2020-05-01', coverage: 94.0 },
{ date: '2020-05-02', coverage: 94.0 },
{ date: '2020-05-03', coverage: 94.0 },
{ date: '2020-05-04', coverage: 94.0 },
{ date: '2020-05-05', coverage: 92.0 },
{ date: '2020-05-06', coverage: 91.0 },
{ date: '2020-05-07', coverage: 78.0 },
{ date: '2020-05-08', coverage: 94.0 },
{ date: '2020-05-09', coverage: 94.0 },
{ date: '2020-05-10', coverage: 94.0 },
{ date: '2020-05-11', coverage: 94.0 },
{ date: '2020-05-12', coverage: 94.0 },
{ date: '2020-05-13', coverage: 92.0 },
{ date: '2020-05-14', coverage: 91.0 },
{ date: '2020-05-15', coverage: 78.0 },
{ date: '2020-05-16', coverage: 94.0 },
{ date: '2020-05-17', coverage: 94.0 },
{ date: '2020-05-18', coverage: 93.0 },
{ date: '2020-05-19', coverage: 92.0 },
{ date: '2020-05-20', coverage: 91.0 },
{ date: '2020-05-21', coverage: 90.0 },
{ date: '2020-05-22', coverage: 91.0 },
{ date: '2020-05-23', coverage: 92.0 },
{ date: '2020-05-24', coverage: 75.0 },
{ date: '2020-05-25', coverage: 74.0 },
{ date: '2020-05-26', coverage: 74.0 },
{ date: '2020-05-27', coverage: 74.0 },
{ date: '2020-05-28', coverage: 80.0 },
{ date: '2020-05-29', coverage: 85.0 },
{ date: '2020-05-30', coverage: 92.0 },
{ date: '2020-05-31', coverage: 91.0 },
],
},
];

View File

@ -33,7 +33,7 @@ describe Gitlab::Graphql::Pagination::Keyset::Connection do
let(:nodes) { Project.order(:updated_at) }
it 'returns the encoded value of the order' do
expect(decoded_cursor(cursor)).to include('updated_at' => project.updated_at.to_s)
expect(decoded_cursor(cursor)).to include('updated_at' => project.updated_at.strftime('%Y-%m-%d %H:%M:%S.%N %Z'))
end
it 'includes the :id even when not specified in the order' do
@ -45,7 +45,7 @@ describe Gitlab::Graphql::Pagination::Keyset::Connection do
let(:nodes) { Project.order(:updated_at).order(:created_at) }
it 'returns the encoded value of the order' do
expect(decoded_cursor(cursor)).to include('updated_at' => project.updated_at.to_s)
expect(decoded_cursor(cursor)).to include('updated_at' => project.updated_at.strftime('%Y-%m-%d %H:%M:%S.%N %Z'))
end
end
@ -53,7 +53,7 @@ describe Gitlab::Graphql::Pagination::Keyset::Connection do
let(:nodes) { Project.order(Arel.sql('projects.updated_at IS NULL')).order(:updated_at).order(:id) }
it 'returns the encoded value of the order' do
expect(decoded_cursor(cursor)).to include('updated_at' => project.updated_at.to_s)
expect(decoded_cursor(cursor)).to include('updated_at' => project.updated_at.strftime('%Y-%m-%d %H:%M:%S.%N %Z'))
end
end
end

View File

@ -0,0 +1,142 @@
# frozen_string_literal: true
require 'spec_helper'
require Rails.root.join('db', 'migrate', '20200526231421_update_index_approval_rule_name_for_code_owners_rule_type.rb')
describe UpdateIndexApprovalRuleNameForCodeOwnersRuleType do
let(:migration) { described_class.new }
let(:approval_rules) { table(:approval_merge_request_rules) }
let(:namespace) { table(:namespaces).create!(name: 'gitlab', path: 'gitlab') }
let(:project) do
table(:projects).create!(
namespace_id: namespace.id,
name: 'gitlab',
path: 'gitlab'
)
end
let(:merge_request) do
table(:merge_requests).create!(
target_project_id: project.id,
source_project_id: project.id,
target_branch: 'feature',
source_branch: 'master'
)
end
let(:index_names) do
ActiveRecord::Base.connection
.indexes(:approval_merge_request_rules)
.collect(&:name)
end
def create_sectional_approval_rules
approval_rules.create!(
merge_request_id: merge_request.id,
name: "*.rb",
code_owner: true,
rule_type: 2,
section: "First Section"
)
approval_rules.create!(
merge_request_id: merge_request.id,
name: "*.rb",
code_owner: true,
rule_type: 2,
section: "Second Section"
)
end
def create_two_matching_nil_section_approval_rules
2.times do
approval_rules.create!(
merge_request_id: merge_request.id,
name: "nil_section",
code_owner: true,
rule_type: 2
)
end
end
before do
approval_rules.delete_all
end
describe "#up" do
it "creates the new index and removes the 'legacy' indices" do
# Confirm that existing legacy indices prevent duplicate entries
#
expect { create_sectional_approval_rules }
.to raise_exception(ActiveRecord::RecordNotUnique)
expect { create_two_matching_nil_section_approval_rules }
.to raise_exception(ActiveRecord::RecordNotUnique)
approval_rules.delete_all
disable_migrations_output { migrate! }
# After running the migration, expect `section == nil` rules to still
# be blocked by the legacy indices, but sectional rules are allowed.
#
expect { create_sectional_approval_rules }
.to change { approval_rules.count }.by(2)
expect { create_two_matching_nil_section_approval_rules }
.to raise_exception(ActiveRecord::RecordNotUnique)
# Attempt to rerun the creation of sectional rules, and see that sectional
# rules are unique by section
#
expect { create_sectional_approval_rules }
.to raise_exception(ActiveRecord::RecordNotUnique)
expect(index_names).to include(
described_class::SECTIONAL_INDEX_NAME,
described_class::LEGACY_INDEX_NAME_RULE_TYPE,
described_class::LEGACY_INDEX_NAME_CODE_OWNERS
)
end
end
describe "#down" do
it "recreates 'legacy' indices and removes duplicate code owner approval rules" do
disable_migrations_output { migrate! }
expect { create_sectional_approval_rules }
.to change { approval_rules.count }.by(2)
expect { create_two_matching_nil_section_approval_rules }
.to raise_exception(ActiveRecord::RecordNotUnique)
# Run the down migration. This will remove the 2 approval rules we create
# above, and call MergeRequests::SyncCodeOwnerApprovalRules to recreate
# new ones.
#
expect(MergeRequests::SyncCodeOwnerApprovalRules)
.to receive(:new).with(MergeRequest.find(merge_request.id)).once.and_call_original
# We expect approval_rules.count to be changed by -2 as we're deleting the
# 3 rules created above, and MergeRequests::SyncCodeOwnerApprovalRules
# will not be able to create new one with an empty (or missing)
# CODEOWNERS file.
#
expect { disable_migrations_output { migration.down } }
.to change { approval_rules.count }.by(-3)
# Test that the index does not allow us to create the same rules as the
# previous sectional index.
#
expect { create_sectional_approval_rules }
.to raise_exception(ActiveRecord::RecordNotUnique)
expect { create_two_matching_nil_section_approval_rules }
.to raise_exception(ActiveRecord::RecordNotUnique)
expect(index_names).not_to include(described_class::SECTIONAL_INDEX_NAME)
expect(index_names).to include(
described_class::LEGACY_INDEX_NAME_RULE_TYPE,
described_class::LEGACY_INDEX_NAME_CODE_OWNERS
)
end
end
end

View File

@ -187,4 +187,62 @@ describe 'GraphQL' do
end
end
end
describe 'keyset pagination' do
let_it_be(:project) { create(:project, :public) }
let_it_be(:issues) { create_list(:issue, 10, project: project, created_at: Time.now.change(usec: 200)) }
let(:page_size) { 6 }
let(:issues_edges) { %w(data project issues edges) }
let(:end_cursor) { %w(data project issues pageInfo endCursor) }
let(:query) do
<<~GRAPHQL
query project($fullPath: ID!, $first: Int, $after: String) {
project(fullPath: $fullPath) {
issues(first: $first, after: $after) {
edges { node { iid } }
pageInfo { endCursor }
}
}
}
GRAPHQL
end
# TODO: Switch this to use `post_graphql`
# This is not performing an actual GraphQL request because the
# variables end up being strings when passed through the `post_graphql`
# helper.
#
# https://gitlab.com/gitlab-org/gitlab/-/issues/222432
def execute_query(after: nil)
GitlabSchema.execute(
query,
context: { current_user: nil },
variables: {
fullPath: project.full_path,
first: page_size,
after: after
}
)
end
it 'paginates datetimes correctly when they have millisecond data' do
# let's make sure we're actually querying a timestamp, just in case
expect(Gitlab::Graphql::Pagination::Keyset::QueryBuilder)
.to receive(:new).with(anything, anything, hash_including('created_at'), anything).and_call_original
first_page = execute_query
edges = first_page.dig(*issues_edges)
cursor = first_page.dig(*end_cursor)
expect(edges.count).to eq(6)
expect(edges.last['node']['iid']).to eq(issues[4].iid.to_s)
second_page = execute_query(after: cursor)
edges = second_page.dig(*issues_edges)
expect(edges.count).to eq(4)
expect(edges.last['node']['iid']).to eq(issues[0].iid.to_s)
end
end
end

View File

@ -20,6 +20,15 @@ describe AlertManagement::Alerts::UpdateService do
describe '#execute' do
subject(:response) { service.execute }
context 'when the current_user is nil' do
let(:current_user) { nil }
it 'results in an error' do
expect(response).to be_error
expect(response.message).to eq('You have no permissions')
end
end
context 'when user does not have permission to update alerts' do
let(:current_user) { user_without_permissions }
@ -81,6 +90,37 @@ describe AlertManagement::Alerts::UpdateService do
expect { response }.to change { Todo.where(user: user_with_permissions).count }.by(1)
end
context 'when current user is not the assignee' do
let(:assignee_user) { create(:user) }
let(:params) { { assignees: [assignee_user] } }
it 'skips adding todo for assignee without permission to read alert' do
expect { response }.not_to change(Todo, :count)
end
context 'when assignee has read permission' do
before do
project.add_developer(assignee_user)
end
it 'adds a todo' do
response
expect(Todo.first.author).to eq(current_user)
end
end
context 'when current_user is nil' do
let(:current_user) { nil }
it 'skips adding todo if current_user is nil' do
project.add_developer(assignee_user)
expect { response }.not_to change(Todo, :count)
end
end
end
context 'with multiple users included' do
let(:params) { { assignees: [user_with_permissions, user_without_permissions] } }