Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-11-06 18:09:07 +00:00
parent a268b09416
commit f3db01da50
62 changed files with 741 additions and 473 deletions

View File

@ -4,12 +4,15 @@ import {
GlButton,
GlIcon,
GlLoadingIcon,
GlModal,
GlModalDirective,
GlTable,
GlTooltipDirective,
GlSprintf,
} from '@gitlab/ui';
import { s__, __ } from '~/locale';
import Tracking from '~/tracking';
import { trackAlertIntegrationsViewsOptions } from '../constants';
import { trackAlertIntegrationsViewsOptions, integrationToDeleteDefault } from '../constants';
export const i18n = {
title: s__('AlertsIntegrations|Current integrations'),
@ -36,10 +39,13 @@ export default {
GlButton,
GlIcon,
GlLoadingIcon,
GlModal,
GlTable,
GlSprintf,
},
directives: {
GlTooltip: GlTooltipDirective,
GlModal: GlModalDirective,
},
props: {
integrations: {
@ -71,6 +77,11 @@ export default {
label: __('Actions'),
},
],
data() {
return {
integrationToDelete: integrationToDeleteDefault,
};
},
computed: {
tbodyTrClass() {
return {
@ -86,6 +97,14 @@ export default {
const { category, action } = trackAlertIntegrationsViewsOptions;
Tracking.event(category, action);
},
intergrationToDelete({ name, id }) {
this.integrationToDelete.id = id;
this.integrationToDelete.name = name;
},
deleteIntergration() {
this.$emit('delete-integration', { id: this.integrationToDelete.id });
this.integrationToDelete = { ...integrationToDeleteDefault };
},
},
};
</script>
@ -127,7 +146,11 @@ export default {
<template #cell(actions)="{ item }">
<gl-button-group>
<gl-button icon="pencil" @click="$emit('edit-integration', { id: item.id })" />
<gl-button icon="remove" @click="$emit('delete-integration', { id: item.id })" />
<gl-button
v-gl-modal.deleteIntegration
icon="remove"
@click="intergrationToDelete(item)"
/>
</gl-button-group>
</template>
@ -143,5 +166,22 @@ export default {
</div>
</template>
</gl-table>
<gl-modal
modal-id="deleteIntegration"
:title="__('Are you sure?')"
:ok-title="s__('AlertSettings|Delete integration')"
ok-variant="danger"
@ok="deleteIntergration"
>
<gl-sprintf
:message="
s__(
'AlertsIntegrations|You have opted to delete the %{integrationName} integration. Do you want to proceed? It means you will no longer receive alerts from this endpoint in your alert list, and this action cannot be undone.',
)
"
>
<template #integrationName>{{ integrationToDelete.name }}</template>
</gl-sprintf>
</gl-modal>
</div>
</template>

View File

@ -22,14 +22,12 @@ import {
JSON_VALIDATE_DELAY,
targetPrometheusUrlPlaceholder,
typeSet,
defaultFormState,
} from '../constants';
export default {
targetPrometheusUrlPlaceholder,
JSON_VALIDATE_DELAY,
typeSet,
defaultFormState,
i18n: {
integrationFormSteps: {
step1: {
@ -113,14 +111,18 @@ export default {
data() {
return {
selectedIntegration: integrationTypesNew[0].value,
active: false,
options: integrationTypesNew,
active: false,
formVisible: false,
integrationTestPayload: {
json: null,
error: null,
},
};
},
computed: {
jsonIsValid() {
return this.integrationForm.integrationTestPayload.error === null;
return this.integrationTestPayload.error === null;
},
selectedIntegrationType() {
switch (this.selectedIntegration) {
@ -129,43 +131,42 @@ export default {
case this.$options.typeSet.prometheus:
return this.prometheus;
default:
return this.defaultFormState;
return {};
}
},
integrationForm() {
return {
name: this.currentIntegration?.name || '',
integrationTestPayload: {
json: null,
error: null,
},
active: this.currentIntegration?.active || false,
token: this.currentIntegration?.token || '',
url: this.currentIntegration?.url || '',
token: this.currentIntegration?.token || this.selectedIntegrationType.token,
url: this.currentIntegration?.url || this.selectedIntegrationType.url,
apiUrl: this.currentIntegration?.apiUrl || '',
};
},
},
watch: {
currentIntegration(val) {
if (val === null) {
return this.reset();
}
this.selectedIntegration = val.type;
this.active = val.active;
this.onIntegrationTypeSelect();
return this.integrationTypeSelect();
},
},
methods: {
onIntegrationTypeSelect() {
integrationTypeSelect() {
if (this.selectedIntegration === integrationTypesNew[0].value) {
this.formVisible = false;
} else {
this.formVisible = true;
}
},
onSubmitWithTestPayload() {
submitWithTestPayload() {
// TODO: Test payload before saving via GraphQL
this.onSubmit();
this.submit();
},
onSubmit() {
submit() {
const { name, apiUrl } = this.integrationForm;
const variables =
this.selectedIntegration === this.$options.typeSet.http
@ -179,27 +180,45 @@ export default {
return this.$emit('create-new-integration', integrationPayload);
},
onReset() {
this.integrationForm = this.defaultFormState;
reset() {
this.selectedIntegration = integrationTypesNew[0].value;
this.onIntegrationTypeSelect();
this.integrationTypeSelect();
if (this.currentIntegration) {
return this.$emit('clear-current-integration');
}
return this.resetFormValues();
},
onResetAuthKey() {
resetFormValues() {
this.integrationForm.name = '';
this.integrationForm.apiUrl = '';
this.integrationTestPayload = {
json: null,
error: null,
};
this.active = false;
},
resetAuthKey() {
if (!this.currentIntegration) {
return;
}
this.$emit('reset-token', {
type: this.selectedIntegration,
variables: { id: this.currentIntegration.id },
});
},
validateJson() {
this.integrationForm.integrationTestPayload.error = null;
if (this.integrationForm.integrationTestPayload.json === '') {
this.integrationTestPayload.error = null;
if (this.integrationTestPayload.json === '') {
return;
}
try {
JSON.parse(this.integrationForm.integrationTestPayload.json);
JSON.parse(this.integrationTestPayload.json);
} catch (e) {
this.integrationForm.integrationTestPayload.error = JSON.stringify(e.message);
this.integrationTestPayload.error = JSON.stringify(e.message);
}
},
},
@ -207,7 +226,7 @@ export default {
</script>
<template>
<gl-form class="gl-mt-6" @submit.prevent="onSubmit" @reset.prevent="onReset">
<gl-form class="gl-mt-6" @submit.prevent="submit" @reset.prevent="reset">
<h5 class="gl-font-lg gl-my-5">{{ s__('AlertSettings|Add new integrations') }}</h5>
<gl-form-group
@ -217,8 +236,9 @@ export default {
>
<gl-form-select
v-model="selectedIntegration"
:disabled="currentIntegration !== null"
:options="options"
@change="onIntegrationTypeSelect"
@change="integrationTypeSelect"
/>
<alert-settings-form-help-block
@ -279,7 +299,11 @@ export default {
<gl-form-input-group id="url" readonly :value="integrationForm.url">
<template #append>
<clipboard-button :text="integrationForm.url" :title="__('Copy')" class="gl-m-0!" />
<clipboard-button
:text="integrationForm.url || ''"
:title="__('Copy')"
class="gl-m-0!"
/>
</template>
</gl-form-input-group>
</div>
@ -296,7 +320,11 @@ export default {
:value="integrationForm.token"
>
<template #append>
<clipboard-button :text="integrationForm.token" :title="__('Copy')" class="gl-m-0!" />
<clipboard-button
:text="integrationForm.token || ''"
:title="__('Copy')"
class="gl-m-0!"
/>
</template>
</gl-form-input-group>
@ -308,7 +336,7 @@ export default {
:title="$options.i18n.integrationFormSteps.step3.reset"
:ok-title="$options.i18n.integrationFormSteps.step3.reset"
ok-variant="danger"
@ok="onResetAuthKey"
@ok="resetAuthKey"
>
{{ $options.i18n.integrationFormSteps.restKeyInfo.label }}
</gl-modal>
@ -318,7 +346,7 @@ export default {
id="test-integration"
:label="$options.i18n.integrationFormSteps.step4.label"
label-for="test-integration"
:invalid-feedback="integrationForm.integrationTestPayload.error"
:invalid-feedback="integrationTestPayload.error"
>
<alert-settings-form-help-block
:message="$options.i18n.integrationFormSteps.step4.help"
@ -327,8 +355,8 @@ export default {
<gl-form-textarea
id="test-integration"
v-model.trim="integrationForm.integrationTestPayload.json"
:disabled="!integrationForm.active"
v-model.trim="integrationTestPayload.json"
:disabled="!active"
:state="jsonIsValid"
:placeholder="$options.i18n.integrationFormSteps.step4.placeholder"
class="gl-my-4"
@ -354,7 +382,7 @@ export default {
category="secondary"
variant="success"
class="gl-mr-1 js-no-auto-disable"
@click="onSubmitWithTestPayload"
@click="submitWithTestPayload"
>{{ s__('AlertSettings|Save and test payload') }}</gl-button
>
<gl-button

View File

@ -1,5 +1,4 @@
<script>
import produce from 'immer';
import { s__ } from '~/locale';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { fetchPolicies } from '~/lib/graphql';
@ -9,12 +8,17 @@ import createHttpIntegrationMutation from '../graphql/mutations/create_http_inte
import createPrometheusIntegrationMutation from '../graphql/mutations/create_prometheus_integration.mutation.graphql';
import updateHttpIntegrationMutation from '../graphql/mutations/update_http_integration.mutation.graphql';
import updatePrometheusIntegrationMutation from '../graphql/mutations/update_prometheus_integration.mutation.graphql';
import destroyHttpIntegrationMutation from '../graphql/mutations/destroy_http_integration.mutation.graphql';
import resetHttpTokenMutation from '../graphql/mutations/reset_http_token.mutation.graphql';
import resetPrometheusTokenMutation from '../graphql/mutations/reset_prometheus_token.mutation.graphql';
import IntegrationsList from './alerts_integrations_list.vue';
import SettingsFormOld from './alerts_settings_form_old.vue';
import SettingsFormNew from './alerts_settings_form_new.vue';
import { typeSet } from '../constants';
import {
updateStoreAfterIntegrationDelete,
updateStoreAfterIntegrationAdd,
} from '../utils/cache_updates';
export default {
typeSet,
@ -22,6 +26,7 @@ export default {
changesSaved: s__(
'AlertsIntegrations|The integration has been successfully saved. Alerts from this new integration should now appear on your alerts list.',
),
integrationRemoved: s__('AlertsIntegrations|The integration has been successfully removed.'),
},
components: {
IntegrationsList,
@ -89,6 +94,8 @@ export default {
},
methods: {
createNewIntegration({ type, variables }) {
const { projectPath } = this;
this.isUpdating = true;
this.$apollo
.mutate({
@ -98,9 +105,11 @@ export default {
: createPrometheusIntegrationMutation,
variables: {
...variables,
projectPath: this.projectPath,
projectPath,
},
update(store, { data }) {
updateStoreAfterIntegrationAdd(store, getIntegrationsQuery, data, { projectPath });
},
update: this.updateIntegrations,
})
.then(({ data: { httpIntegrationCreate, prometheusIntegrationCreate } = {} } = {}) => {
const error = httpIntegrationCreate?.errors[0] || prometheusIntegrationCreate?.errors[0];
@ -119,41 +128,6 @@ export default {
this.isUpdating = false;
});
},
updateIntegrations(
store,
{
data: { httpIntegrationCreate, prometheusIntegrationCreate },
},
) {
const integration =
httpIntegrationCreate?.integration || prometheusIntegrationCreate?.integration;
if (!integration) {
return;
}
const sourceData = store.readQuery({
query: getIntegrationsQuery,
variables: {
projectPath: this.projectPath,
},
});
const data = produce(sourceData, draftData => {
// eslint-disable-next-line no-param-reassign
draftData.project.alertManagementIntegrations.nodes = [
integration,
...draftData.project.alertManagementIntegrations.nodes,
];
});
store.writeQuery({
query: getIntegrationsQuery,
variables: {
projectPath: this.projectPath,
},
data,
});
},
updateIntegration({ type, variables }) {
this.isUpdating = true;
this.$apollo
@ -201,6 +175,12 @@ export default {
if (error) {
return createFlash({ message: error });
}
const integration =
httpIntegrationResetToken?.integration ||
prometheusIntegrationResetToken?.integration;
this.currentIntegration = integration;
return createFlash({
message: this.$options.i18n.changesSaved,
type: FLASH_TYPES.SUCCESS,
@ -217,8 +197,41 @@ export default {
editIntegration({ id }) {
this.currentIntegration = this.integrations.list.find(integration => integration.id === id);
},
deleteIntegration() {
// TODO, handle delete via GraphQL
deleteIntegration({ id }) {
const { projectPath } = this;
this.isUpdating = true;
this.$apollo
.mutate({
mutation: destroyHttpIntegrationMutation,
variables: {
id,
},
update(store, { data }) {
updateStoreAfterIntegrationDelete(store, getIntegrationsQuery, data, { projectPath });
},
})
.then(({ data: { httpIntegrationDestroy } = {} } = {}) => {
const error = httpIntegrationDestroy?.errors[0];
if (error) {
return createFlash({ message: error });
}
this.currentIntegration = null;
return createFlash({
message: this.$options.i18n.integrationRemoved,
type: FLASH_TYPES.SUCCESS,
});
})
.catch(err => {
this.errored = true;
createFlash({ message: err });
})
.finally(() => {
this.isUpdating = false;
});
},
clearCurrentIntegration() {
this.currentIntegration = null;
},
},
};
@ -239,6 +252,7 @@ export default {
@create-new-integration="createNewIntegration"
@update-integration="updateIntegration"
@reset-token="resetToken"
@clear-current-integration="clearCurrentIntegration"
/>
<settings-form-old v-else />
</div>

View File

@ -66,6 +66,8 @@ export const defaultFormState = {
integrationTestPayload: { json: null, error: null },
};
export const integrationToDeleteDefault = { id: null, name: '' };
export const JSON_VALIDATE_DELAY = 250;
export const targetPrometheusUrlPlaceholder = 'http://prometheus.example.com/';

View File

@ -0,0 +1,10 @@
#import "../fragments/integration_item.fragment.graphql"
mutation destroyHttpIntegration($id: ID!) {
httpIntegrationDestroy(input: { id: $id }) {
errors
integration {
...IntegrationItem
}
}
}

View File

@ -0,0 +1,84 @@
import produce from 'immer';
import createFlash from '~/flash';
import { DELETE_INTEGRATION_ERROR, ADD_INTEGRATION_ERROR } from './error_messages';
const deleteIntegrationFromStore = (store, query, { httpIntegrationDestroy }, variables) => {
const integration = httpIntegrationDestroy?.integration;
if (!integration) {
return;
}
const sourceData = store.readQuery({
query,
variables,
});
const data = produce(sourceData, draftData => {
// eslint-disable-next-line no-param-reassign
draftData.project.alertManagementIntegrations.nodes = draftData.project.alertManagementIntegrations.nodes.filter(
({ id }) => id !== integration.id,
);
});
store.writeQuery({
query,
variables,
data,
});
};
const addIntegrationToStore = (
store,
query,
{ httpIntegrationCreate, prometheusIntegrationCreate },
variables,
) => {
const integration =
httpIntegrationCreate?.integration || prometheusIntegrationCreate?.integration;
if (!integration) {
return;
}
const sourceData = store.readQuery({
query,
variables,
});
const data = produce(sourceData, draftData => {
// eslint-disable-next-line no-param-reassign
draftData.project.alertManagementIntegrations.nodes = [
integration,
...draftData.project.alertManagementIntegrations.nodes,
];
});
store.writeQuery({
query,
variables,
data,
});
};
const onError = (data, message) => {
createFlash({ message });
throw new Error(data.errors);
};
export const hasErrors = ({ errors = [] }) => errors?.length;
export const updateStoreAfterIntegrationDelete = (store, query, data, variables) => {
if (hasErrors(data)) {
onError(data, DELETE_INTEGRATION_ERROR);
} else {
deleteIntegrationFromStore(store, query, data, variables);
}
};
export const updateStoreAfterIntegrationAdd = (store, query, data, variables) => {
if (hasErrors(data)) {
onError(data, ADD_INTEGRATION_ERROR);
} else {
addIntegrationToStore(store, query, data, variables);
}
};

View File

@ -0,0 +1,9 @@
import { s__ } from '~/locale';
export const DELETE_INTEGRATION_ERROR = s__(
'AlertsIntegrations|The integration could not be deleted. Please try again.',
);
export const ADD_INTEGRATION_ERROR = s__(
'AlertsIntegrations|The integration could not be added. Please try again.',
);

View File

@ -14,9 +14,9 @@ export default {
required: false,
default: () => [],
},
isDesktop: {
isMobile: {
type: Boolean,
default: false,
default: true,
required: false,
},
},
@ -34,7 +34,7 @@ export default {
return this.tags.some(tag => this.selectedItems[tag.name]);
},
showMultiDeleteButton() {
return this.tags.some(tag => tag.destroy_path) && this.isDesktop;
return this.tags.some(tag => tag.destroy_path) && !this.isMobile;
},
},
methods: {
@ -68,7 +68,7 @@ export default {
:tag="tag"
:first="index === 0"
:selected="selectedItems[tag.name]"
:is-desktop="isDesktop"
:is-mobile="isMobile"
@select="updateSelectedItems(tag.name)"
@delete="$emit('delete', { [tag.name]: true })"
/>

View File

@ -40,9 +40,9 @@ export default {
type: Object,
required: true,
},
isDesktop: {
isMobile: {
type: Boolean,
default: false,
default: true,
required: false,
},
selected: {
@ -69,7 +69,7 @@ export default {
return this.tag.layers ? n__('%d layer', '%d layers', this.tag.layers) : '';
},
mobileClasses() {
return this.isDesktop ? '' : 'mw-s';
return this.isMobile ? 'mw-s' : '';
},
shortDigest() {
// remove sha256: from the string, and show only the first 7 char

View File

@ -37,7 +37,7 @@ export default {
data() {
return {
itemsToBeDeleted: [],
isDesktop: true,
isMobile: false,
deleteAlertType: null,
dismissPartialCleanupWarning: false,
};
@ -110,7 +110,7 @@ export default {
}
},
handleResize() {
this.isDesktop = GlBreakpointInstance.isDesktop();
this.isMobile = GlBreakpointInstance.getBreakpointSize() === 'xs';
},
},
};
@ -137,7 +137,7 @@ export default {
<tags-loader v-if="isLoading" />
<template v-else>
<empty-tags-state v-if="tags.length === 0" :no-containers-image="config.noContainersImage" />
<tags-list v-else :tags="tags" :is-desktop="isDesktop" @delete="deleteTags" />
<tags-list v-else :tags="tags" :is-mobile="isMobile" @delete="deleteTags" />
</template>
<gl-pagination

View File

@ -1,10 +1,10 @@
<script>
import { GlDeprecatedDropdown, GlDeprecatedDropdownItem } from '@gitlab/ui';
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
export default {
components: {
GlDeprecatedDropdown,
GlDeprecatedDropdownItem,
GlDropdown,
GlDropdownItem,
},
props: {
commits: {
@ -18,20 +18,20 @@ export default {
<template>
<div>
<gl-deprecated-dropdown
<gl-dropdown
right
text="Use an existing commit message"
variant="link"
class="mr-commit-dropdown"
>
<gl-deprecated-dropdown-item
<gl-dropdown-item
v-for="commit in commits"
:key="commit.short_id"
class="text-nowrap text-truncate"
@click="$emit('input', commit.message)"
>
<span class="monospace mr-2">{{ commit.short_id }}</span> {{ commit.title }}
</gl-deprecated-dropdown-item>
</gl-deprecated-dropdown>
</gl-dropdown-item>
</gl-dropdown>
</div>
</template>

View File

@ -15,10 +15,6 @@
.broadcast-banner-message {
text-align: center;
.broadcast-message-dismiss {
color: inherit;
}
}
.broadcast-notification-message {
@ -36,10 +32,6 @@
&.preview {
position: static;
}
.broadcast-message-dismiss {
color: $gray-700;
}
}
.toggle-colors {

View File

@ -150,7 +150,7 @@ module IssuableCollections
common_attributes + [:project, project: :namespace]
when 'MergeRequest'
common_attributes + [
:target_project, :latest_merge_request_diff, :approvals, :approved_by_users, :reviewers,
:target_project, :latest_merge_request_diff, :approvals, :approved_by_users,
source_project: :route, head_pipeline: :project, target_project: :namespace
]
end

View File

@ -318,8 +318,6 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
end
def export_csv
return render_404 unless Feature.enabled?(:export_merge_requests_as_csv, project, default_enabled: true)
IssuableExportCsvWorker.perform_async(:merge_request, current_user.id, project.id, finder_options.to_h) # rubocop:disable CodeReuse/Worker
index_path = project_merge_requests_path(project)

View File

@ -406,7 +406,7 @@ class IssuableFinder
elsif params.filter_by_any_assignee?
items.assigned
elsif params.assignee
items_assigned_to(items, params.assignee)
items.assigned_to(params.assignee)
elsif params.assignee_id? || params.assignee_username? # assignee not found
items.none
else
@ -414,10 +414,6 @@ class IssuableFinder
end
end
def items_assigned_to(items, user)
items.assigned_to(user)
end
def by_negated_assignee(items)
# We want CE users to be able to say "Issues not assigned to either PersonA nor PersonB"
if not_params.assignees.present?

View File

@ -164,13 +164,6 @@ class MergeRequestsFinder < IssuableFinder
end
# rubocop: enable CodeReuse/Finder
# rubocop: disable CodeReuse/ActiveRecord
def items_assigned_to(items, user)
assignee_or_reviewer = MergeRequest.from_union([super, items.reviewer_assigned_to(user)])
items.where(id: assignee_or_reviewer)
end
# rubocop: enable CodeReuse/ActiveRecord
def by_deployments(items)
env = params[:environment]
before = params[:deployed_before]

View File

@ -302,7 +302,7 @@ module ProjectsHelper
end
def settings_operations_available?
can?(current_user, :read_environment, @project)
!@project.archived? && can?(current_user, :admin_operations, @project)
end
def error_tracking_setting_project_json

View File

@ -303,19 +303,6 @@ class MergeRequest < ApplicationRecord
includes(:metrics)
end
scope :reviewer_assigned_to, ->(user) do
mr_reviewers_table = MergeRequestReviewer.arel_table
inner_sql = mr_reviewers_table
.project(Arel::Nodes::True.new)
.where(
mr_reviewers_table[:merge_request_id].eq(MergeRequest.arel_table[:id])
.and(mr_reviewers_table[:user_id].eq(user.id))
).exists
where(inner_sql)
end
after_save :keep_around_commit, unless: :importing?
alias_attribute :project, :target_project

View File

@ -29,10 +29,10 @@ class ResourceTimeboxEvent < ResourceEvent
case self
when ResourceMilestoneEvent
Gitlab::UsageDataCounters::IssueActivityUniqueCounter.track_issue_milestone_changed_action(author: user)
when ResourceIterationEvent
Gitlab::UsageDataCounters::IssueActivityUniqueCounter.track_issue_iteration_changed_action(author: user)
else
# no-op
end
end
end
ResourceTimeboxEvent.prepend_if_ee('EE::ResourceTimeboxEvent')

View File

@ -423,7 +423,7 @@
= link_to project_settings_ci_cd_path(@project), title: _('CI / CD') do
%span
= _('CI / CD')
- if !@project.archived? && settings_operations_available?
- if settings_operations_available?
= nav_link(controller: [:operations]) do
= link_to project_settings_operations_path(@project), title: _('Operations'), data: { qa_selector: 'operations_settings_link' } do
= _('Operations')

View File

@ -1,6 +1,5 @@
- if Feature.enabled?(:export_merge_requests_as_csv, @project, default_enabled: true)
.btn-group
= render 'shared/issuable/csv_export/button', issuable_type: 'merge-requests'
.btn-group
= render 'shared/issuable/csv_export/button', issuable_type: 'merge-requests'
- if @can_bulk_update
= button_tag "Edit merge requests", class: "gl-button btn gl-mr-3 js-bulk-update-toggle"
@ -8,5 +7,4 @@
= link_to new_merge_request_path, class: "gl-button btn btn-success", title: "New merge request" do
New merge request
- if Feature.enabled?(:export_merge_requests_as_csv, @project, default_enabled: true)
= render 'shared/issuable/csv_export/modal', issuable_type: 'merge_requests'
= render 'shared/issuable/csv_export/modal', issuable_type: 'merge_requests'

View File

@ -1,4 +1,4 @@
- return unless can?(current_user, :read_environment, @project)
- return unless can?(current_user, :admin_operations, @project)
- setting = error_tracking_setting

View File

@ -8,5 +8,5 @@
= render_broadcast_message(message)
.gl-flex-grow-1.gl-flex-basis-0.gl-text-right
- if (message.notification? || message.dismissable?) && opts[:preview].blank?
%button.broadcast-message-dismiss.js-dismiss-current-broadcast-notification.btn.btn-link.gl-button{ 'aria-label' => _('Close'), :type => 'button', data: { id: message.id, expire_date: message.ends_at.iso8601 } }
= sprite_icon('close', size: 16, css_class: 'gl-icon gl-text-white gl-mx-3!')
%button.js-dismiss-current-broadcast-notification.btn.btn-link.gl-button{ 'aria-label' => _('Close'), :type => 'button', data: { id: message.id, expire_date: message.ends_at.iso8601 } }
= sprite_icon('close', size: 16, css_class: "gl-icon gl-mx-3! #{is_banner ? 'gl-text-white' : 'gl-text-gray-700'}")

View File

@ -0,0 +1,5 @@
---
title: 'container registry: show delete selected button on medium viewports'
merge_request: 46699
author:
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Replace-GlDeprecatedDropdown-with-GlDropdown-in-app/assets/javascripts/vue_merge_request_widget
merge_request: 41429
author: nuwe1
type: other

View File

@ -0,0 +1,5 @@
---
title: Handle nullbytes in auth headers
merge_request: 46985
author:
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Fix broadcast notification close icon appearance
merge_request: 46804
author:
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Fix operations settings when Pipelines are disabled
merge_request: 47062
author:
type: fixed

View File

@ -1,7 +0,0 @@
---
name: export_merge_requests_as_csv
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/45130
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/267129
type: development
group: group::compliance
default_enabled: true

View File

@ -18,12 +18,12 @@ needs.
## APIs
- `https://docs.gitlab.com/ee/api/audit_events.html`
- `https://docs.gitlab.com/ee/api/graphql/reference/#user`
- `https://docs.gitlab.com/ee/api/graphql/reference/#groupmember`
- `https://docs.gitlab.com/ee/api/graphql/reference/#projectmember`
- [Audit events](../api/audit_events.md)
- [GraphQL - User](../api/graphql/reference/index.md#user)
- [GraphQL - GroupMember](../api/graphql/reference/index.md#groupmember)
- [GraphQL - ProjectMember](../api/graphql/reference/index.md#projectmember)
## Features
- `https://docs.gitlab.com/ee/administration/audit_events.html`
- `https://docs.gitlab.com/ee/administration/logs.html`
- [Audit events](audit_events.md)
- [Log system](logs.md)

View File

@ -133,6 +133,9 @@ Note the following when promoting a secondary:
```
1. Promote the **secondary** node to the **primary** node.
DANGER: **Warning:**
In GitLab 13.2 and 13.3, promoting a secondary node to a primary while the secondary is paused fails. Do not pause replication before promoting a secondary. If the node is paused, please resume before promoting. This issue has been fixed in GitLab 13.4 or later.
CAUTION: **Caution:**
If the secondary node [has been paused](../../geo/index.md#pausing-and-resuming-replication), this performs
a point-in-time recovery to the last known state.
@ -167,6 +170,9 @@ conjunction with multiple servers, as it can only
perform changes on a **secondary** with only a single machine. Instead, you must
do this manually.
DANGER: **Warning:**
In GitLab 13.2 and 13.3, promoting a secondary node to a primary while the secondary is paused fails. Do not pause replication before promoting a secondary. If the node is paused, please resume before promoting. This issue has been fixed in GitLab 13.4 or later.
CAUTION: **Caution:**
If the secondary node [has been paused](../../geo/index.md#pausing-and-resuming-replication), this performs
a point-in-time recovery to the last known state.

View File

@ -227,6 +227,9 @@ conjunction with multiple servers, as it can only
perform changes on a **secondary** with only a single machine. Instead, you must
do this manually.
DANGER: **Warning:**
In GitLab 13.2 and 13.3, promoting a secondary node to a primary while the secondary is paused fails. Do not pause replication before promoting a secondary. If the node is paused, please resume before promoting. This issue has been fixed in GitLab 13.4 or later.
CAUTION: **Caution:**
If the secondary node [has been paused](../../../geo/index.md#pausing-and-resuming-replication), this performs
a point-in-time recovery to the last known state.

View File

@ -196,6 +196,9 @@ For information on how to update your Geo nodes to the latest GitLab version, se
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/35913) in [GitLab Premium](https://about.gitlab.com/pricing/) 13.2.
DANGER: **Warning:**
In GitLab 13.2 and 13.3, promoting a secondary node to a primary while the secondary is paused fails. Do not pause replication before promoting a secondary. If the node is paused, please resume before promoting. This issue has been fixed in GitLab 13.4 or later.
CAUTION: **Caution:**
Pausing and resuming of replication is currently only supported for Geo installations using an
Omnibus GitLab-managed database. External databases are currently not supported.

View File

@ -24,6 +24,13 @@ DROP SERVER gitlab_secondary CASCADE;
DROP EXTENSION IF EXISTS postgres_fdw;
```
DANGER: **Warning:**
In GitLab 13.3, promoting a secondary node to a primary while the secondary is paused fails. Do not pause replication before promoting a secondary. If the node is paused, please resume before promoting. To avoid this issue, upgrade to GitLab 13.4 or later.
## Updating to GitLab 13.2
In GitLab 13.2, promoting a secondary node to a primary while the secondary is paused fails. Do not pause replication before promoting a secondary. If the node is paused, please resume before promoting. To avoid this issue, upgrade to GitLab 13.4 or later.
## Updating to GitLab 13.0
Upgrading to GitLab 13.0 requires GitLab 12.10 to already be using PostgreSQL

View File

@ -367,7 +367,7 @@ POST /projects/:id/releases
| `assets:links` | array of hash | no | An array of assets links. |
| `assets:links:name`| string | required by: `assets:links` | The name of the link. |
| `assets:links:url` | string | required by: `assets:links` | The URL of the link. |
| `assets:links:filepath` | string | no | Optional path for a [Direct Asset link](../../user/project/releases/index.md).
| `assets:links:filepath` | string | no | Optional path for a [Direct Asset link](../../user/project/releases/index.md#permanent-links-to-release-assets).
| `assets:links:link_type` | string | no | The type of the link: `other`, `runbook`, `image`, `package`. Defaults to `other`.
| `released_at` | datetime | no | The date when the release will be/was ready. Defaults to the current time. Expected in ISO 8601 format (`2019-03-15T08:00:00Z`). |

View File

@ -97,26 +97,29 @@ POST /projects/:id/releases/:tag_name/assets/links
| `tag_name` | string | yes | The tag associated with the Release. |
| `name` | string | yes | The name of the link. |
| `url` | string | yes | The URL of the link. |
| `filepath` | string | no | Optional path for a [Direct Asset link](../../user/project/releases/index.md#permanent-links-to-release-assets).
| `link_type` | string | no | The type of the link: `other`, `runbook`, `image`, `package`. Defaults to `other`. |
Example request:
```shell
curl --request POST \
--header "PRIVATE-TOKEN: n671WNGecHugsdEDPsyo" \
--data name="awesome-v0.2.dmg" \
--data url="http://192.168.10.15:3000" \
"https://gitlab.example.com/api/v4/projects/24/releases/v0.1/assets/links"
--header "PRIVATE-TOKEN: tkhfG7HgG-LiZd3zfdDC" \
--data name="hellodarwin-amd64" \
--data url="https://gitlab.example.com/mynamespace/hello/-/jobs/688/artifacts/raw/bin/hello-darwin-amd64" \
--data filepath="/bin/hellodarwin-amd64" \
"https://gitlab.example.com/api/v4/projects/20/releases/v1.7.0/assets/links"
```
Example response:
```json
{
"id":1,
"name":"awesome-v0.2.dmg",
"url":"http://192.168.10.15:3000",
"external":true,
"id":2,
"name":"hellodarwin-amd64",
"url":"https://gitlab.example.com/mynamespace/hello/-/jobs/688/artifacts/raw/bin/hello-darwin-amd64",
"direct_asset_url":"https://gitlab.example.com/mynamespace/hello/-/releases/v1.7.0/downloads/bin/hellodarwin-amd64",
"external":false,
"link_type":"other"
}
```
@ -136,6 +139,7 @@ PUT /projects/:id/releases/:tag_name/assets/links/:link_id
| `link_id` | integer | yes | The ID of the link. |
| `name` | string | no | The name of the link. |
| `url` | string | no | The URL of the link. |
| `filepath` | string | no | Optional path for a [Direct Asset link](../../user/project/releases/index.md#permanent-links-to-release-assets).
| `link_type` | string | no | The type of the link: `other`, `runbook`, `image`, `package`. Defaults to `other`. |
NOTE: **Note:**

View File

@ -320,7 +320,7 @@ services:
command: ["--registry-mirror", "https://registry-mirror.example.com"] # Specify the registry mirror to use.
```
#### DinD service defined inside of GitLab Runner configuration
##### DinD service defined inside of GitLab Runner configuration
> [Introduced](https://gitlab.com/gitlab-org/gitlab-runner/-/issues/27173) in GitLab Runner 13.6.

View File

@ -234,23 +234,23 @@ There are also two edge cases worth mentioning:
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/29654) in GitLab 12.5
The top-level `workflow:` key applies to the entirety of a pipeline, and
determines whether or not a pipeline is created. It accepts a single
`rules:` key that operates similarly to [`rules:` defined within jobs](#rules),
enabling dynamic configuration of the pipeline.
The top-level `workflow:` keyword determines whether or not a pipeline is created.
It accepts a single `rules:` keyword that is similar to [`rules:` defined within jobs](#rules).
Use it to define what can trigger a new pipeline.
If you are new to GitLab CI/CD and `workflow: rules`, you may find the [`workflow:rules` templates](#workflowrules-templates) useful.
You can use the [`workflow:rules` templates](#workflowrules-templates) to import
a preconfigured `workflow: rules` entry.
To define your own `workflow: rules`, the available configuration options are:
`workflow: rules` accepts these keywords:
- [`if`](#rulesif): Define a rule.
- [`when`](#when): May be set to `always` or `never` only. If not provided, the default value is `always`.
- [`if`](#rulesif): Check this rule to determine when to run a pipeline.
- [`when`](#when): Specify what to do when the `if` rule evaluates to true.
- To run a pipeline, set to `always`.
- To prevent pipelines from running, set to `never`.
If a pipeline attempts to run but matches no rule, it's dropped and doesn't run.
When no rules evaluate to true, the pipeline does not run.
Use the example rules below exactly as written to allow pipelines that match the rule
to run. Add `when: never` to prevent pipelines that match the rule from running. See
the [common `if` clauses for `rules`](#common-if-clauses-for-rules) for more examples.
Some example `if` clauses for `workflow: rules`:
| Example rules | Details |
|------------------------------------------------------|-----------------------------------------------------------|
@ -259,9 +259,12 @@ the [common `if` clauses for `rules`](#common-if-clauses-for-rules) for more exa
| `if: $CI_COMMIT_TAG` | Control when tag pipelines run. |
| `if: $CI_COMMIT_BRANCH` | Control when branch pipelines run. |
See the [common `if` clauses for `rules`](#common-if-clauses-for-rules) for more examples.
For example, in the following configuration, pipelines run for all `push` events (changes to
branches and new tags). Only push events with `-wip` in the commit message are excluded. Scheduled
pipelines and merge request pipelines don't run, as there's no rule allowing them.
branches and new tags). Pipelines for push events with `-wip` in the commit message
don't run, because they are set to `when: never`. Pipelines for schedules or merge requests
don't run either, because no rules evaluate to true for them:
```yaml
workflow:
@ -271,11 +274,11 @@ workflow:
- if: '$CI_PIPELINE_SOURCE == "push"'
```
This example has strict rules, and no other pipelines can run.
This example has strict rules, and pipelines do **not** run in any other case.
Alternatively, you can have loose rules by using only `when: never` rules, followed
by a final `when: always` rule. This allows all types of pipelines, except for any
that match the `when: never` rules:
Alternatively, all of the rules can be `when: never`, with a final
`when: always` rule. Pipelines that match the `when: never` rules do not run.
All other pipeline types run:
```yaml
workflow:
@ -287,12 +290,13 @@ workflow:
- when: always
```
This example never allows pipelines for schedules or `push` (branches and tags) pipelines,
but does allow pipelines in **all** other cases, *including* merge request pipelines.
This example prevents pipelines for schedules or `push` (branches and tags) pipelines.
The final `when: always` rule lets all other pipeline types run, **including** merge
request pipelines.
Be careful not to use a configuration that might run
merge request pipelines and branch pipelines at the same time. As with `rules` defined in jobs,
it can cause [duplicate pipelines](#prevent-duplicate-pipelines).
Be careful not to have rules that match both branch pipelines
and merge request pipelines. Similar to `rules` defined in jobs, this can cause
[duplicate pipelines](#prevent-duplicate-pipelines).
#### `workflow:rules` templates

View File

@ -7,14 +7,14 @@ type: howto, reference
# Edit files through the command line
When [working with Git from the command line](start-using-git.md), you will need to
When [working with Git from the command line](start-using-git.md), you need to
use more than just the Git commands. There are several basic commands that you should
learn, in order to make full use of the command line.
## Start working on your project
To work on a Git project locally (from your own computer), with the command line,
first you will need to [clone (copy) it](start-using-git.md#clone-a-repository) to
first you need to [clone (copy) it](start-using-git.md#clone-a-repository) to
your computer.
## Working with files on the command line
@ -57,7 +57,7 @@ nano README.md
### Remove a file or directory
It is easy to delete (remove) a file or directory, but be careful:
It's easy to delete (remove) a file or directory, but be careful:
DANGER: **Warning:**
This will **permanently** delete a file.
@ -96,7 +96,7 @@ for example) . Execute the same full command with:
Not all commands can be executed from a basic user account on a computer, you may
need administrator's rights to execute commands that affect the system, or try to access
protected data, for example. You can use `sudo` to execute these commands, but you
will likely be asked for an administrator password.
might be asked for an administrator password.
```shell
sudo RESTRICTED-COMMAND
@ -108,8 +108,8 @@ damage to your data or system.
## Sample Git taskflow
If you are completely new to Git, looking through some [sample taskflows](https://rogerdudler.github.io/git-guide/)
will help you understand the best practices for using these commands as you work.
If you're completely new to Git, looking through some [sample taskflows](https://rogerdudler.github.io/git-guide/)
may help you understand the best practices for using these commands as you work.
<!-- ## Troubleshooting

View File

@ -6,23 +6,12 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Export Merge Requests to CSV **(CORE)**
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/3619) in GitLab 13.6.
> - It was [deployed behind a feature flag](../../../administration/feature_flags.md), disabled by default.
> - Became enabled by default in GitLab 13.6.
> - It's enabled on GitLab.com.
> - It's recommended for production use.
> - For GitLab self-managed instances, GitLab administrators can opt to [disable it](#enable-or-disable-export-merge-requests-to-csv). **(CORE ONLY)**
> - It can be enabled or disabled for a single project.
CAUTION: **Warning:**
This feature might not be available to you. Check the **version history** note above for details.
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/3619) in GitLab 13.6.
Exporting Merge Requests CSV enables you and your team to export all the data collected from merge requests into a comma-separated values (CSV) file, which stores tabular data in plain text.
To export Merge Requests to CSV, navigate to your **Merge Requests** from the sidebar of a project and click **Export to CSV**.
Exported files are generated asynchronously and delivered as an email attachment upon generation.
## CSV Output
The following table shows what attributes will be present in the CSV.
@ -54,28 +43,3 @@ The following table shows what attributes will be present in the CSV.
- Export merge requests to CSV is not available at the Groups merge request list.
- As the merge request CSV file is sent as an email attachment, the size is limited to 15MB to ensure successful delivery across a range of email providers. If you need to minimize the size of the file, you can narrow the search before export. For example, you can set up exports of open and closed merge requests in separate files.
### Enable or disable Export Merge Requests to CSV **(CORE ONLY)**
Export merge requests to CSV is under development but ready for production use.
It is deployed behind a feature flag that is **enabled by default**.
[GitLab administrators with access to the GitLab Rails console](../../../administration/feature_flags.md)
can opt to disable it.
To enable it:
```ruby
# For the instance
Feature.enable(:export_merge_requests_as_csv)
# For a single project
Feature.enable(:export_merge_requests_as_csv, Project.find(<project id>))
```
To disable it:
```ruby
# For the instance
Feature.disable(:export_merge_requests_as_csv)
# For a single project
Feature.disable(:export_merge_requests_as_csv, Project.find(<project id>))
```

View File

@ -32,6 +32,9 @@ To set up a project import/export:
Note the following:
- Before you can import a project, you need to export the data first.
See [Exporting a project and its data](#exporting-a-project-and-its-data)
for how you can export a project through the UI.
- Imports from a newer version of GitLab are not supported.
The Importing GitLab version must be greater than or equal to the Exporting GitLab version.
- Imports will fail unless the import and export GitLab instances are
@ -129,6 +132,11 @@ For more details on the specific data persisted in a project export, see the
## Exporting a project and its data
Full project export functionality is limited to project maintainers and owners.
You can configure such functionality through [project settings](index.md):
To export a project and its data, follow these steps:
1. Go to your project's homepage.
1. Click **Settings** in the sidebar.

View File

@ -12,14 +12,11 @@ module Gitlab
# rubocop: disable CodeReuse/ActiveRecord
def find
BatchLoader::GraphQL.for({ model: model_class, id: model_id.to_i }).batch do |loader_info, loader|
per_model = loader_info.group_by { |info| info[:model] }
per_model.each do |model, info|
ids = info.map { |i| i[:id] }
results = model.where(id: ids)
BatchLoader::GraphQL.for(model_id.to_i).batch(key: model_class) do |ids, loader, args|
model = args[:key]
results = model.where(id: ids)
results.each { |record| loader.call({ model: model, id: record.id }, record) }
end
results.each { |record| loader.call(record.id, record) }
end
end
# rubocop: enable CodeReuse/ActiveRecord

View File

@ -5,6 +5,8 @@ module Gitlab
# There is no valid reason for a request to contain a malformed string
# so just return HTTP 400 (Bad Request) if we receive one
class HandleMalformedStrings
include ActionController::HttpAuthentication::Basic
NULL_BYTE_REGEX = Regexp.new(Regexp.escape("\u0000")).freeze
attr_reader :app
@ -21,16 +23,26 @@ module Gitlab
private
def request_contains_malformed_string?(request)
def request_contains_malformed_string?(env)
return false if ENV['DISABLE_REQUEST_VALIDATION'] == '1'
request = Rack::Request.new(request)
# Duplicate the env, so it is not modified when accessing the parameters
# https://github.com/rails/rails/blob/34991a6ae2fc68347c01ea7382fa89004159e019/actionpack/lib/action_dispatch/http/parameters.rb#L59
# The modification causes problems with our multipart middleware
request = ActionDispatch::Request.new(env.dup)
return true if malformed_path?(request.path)
return true if credentials_malformed?(request)
request.params.values.any? do |value|
param_has_null_byte?(value)
end
rescue ActionController::BadRequest
# If we can't build an ActionDispatch::Request something's wrong
# This would also happen if `#params` contains invalid UTF-8
# in this case we'll return a 400
#
true
end
def malformed_path?(path)
@ -40,6 +52,13 @@ module Gitlab
true
end
def credentials_malformed?(request)
credentials = decode_credentials(request).presence
return false unless credentials
string_malformed?(credentials)
end
def param_has_null_byte?(value, depth = 0)
# Guard against possible attack sending large amounts of nested params
# Should be safe as deeply nested params are highly uncommon.

View File

@ -9,14 +9,12 @@ module Gitlab
ISSUE_CREATED = 'g_project_management_issue_created'
ISSUE_CLOSED = 'g_project_management_issue_closed'
ISSUE_DESCRIPTION_CHANGED = 'g_project_management_issue_description_changed'
ISSUE_ITERATION_CHANGED = 'g_project_management_issue_iteration_changed'
ISSUE_LABEL_CHANGED = 'g_project_management_issue_label_changed'
ISSUE_MADE_CONFIDENTIAL = 'g_project_management_issue_made_confidential'
ISSUE_MADE_VISIBLE = 'g_project_management_issue_made_visible'
ISSUE_MILESTONE_CHANGED = 'g_project_management_issue_milestone_changed'
ISSUE_REOPENED = 'g_project_management_issue_reopened'
ISSUE_TITLE_CHANGED = 'g_project_management_issue_title_changed'
ISSUE_WEIGHT_CHANGED = 'g_project_management_issue_weight_changed'
ISSUE_CROSS_REFERENCED = 'g_project_management_issue_cross_referenced'
ISSUE_MOVED = 'g_project_management_issue_moved'
ISSUE_RELATED = 'g_project_management_issue_related'
@ -24,9 +22,6 @@ module Gitlab
ISSUE_MARKED_AS_DUPLICATE = 'g_project_management_issue_marked_as_duplicate'
ISSUE_LOCKED = 'g_project_management_issue_locked'
ISSUE_UNLOCKED = 'g_project_management_issue_unlocked'
ISSUE_ADDED_TO_EPIC = 'g_project_management_issue_added_to_epic'
ISSUE_REMOVED_FROM_EPIC = 'g_project_management_issue_removed_from_epic'
ISSUE_CHANGED_EPIC = 'g_project_management_issue_changed_epic'
ISSUE_DESIGNS_ADDED = 'g_project_management_issue_designs_added'
ISSUE_DESIGNS_MODIFIED = 'g_project_management_issue_designs_modified'
ISSUE_DESIGNS_REMOVED = 'g_project_management_issue_designs_removed'
@ -78,14 +73,6 @@ module Gitlab
track_unique_action(ISSUE_MILESTONE_CHANGED, author, time)
end
def track_issue_iteration_changed_action(author:, time: Time.zone.now)
track_unique_action(ISSUE_ITERATION_CHANGED, author, time)
end
def track_issue_weight_changed_action(author:, time: Time.zone.now)
track_unique_action(ISSUE_WEIGHT_CHANGED, author, time)
end
def track_issue_cross_referenced_action(author:, time: Time.zone.now)
track_unique_action(ISSUE_CROSS_REFERENCED, author, time)
end
@ -114,18 +101,6 @@ module Gitlab
track_unique_action(ISSUE_UNLOCKED, author, time)
end
def track_issue_added_to_epic_action(author:, time: Time.zone.now)
track_unique_action(ISSUE_ADDED_TO_EPIC, author, time)
end
def track_issue_removed_from_epic_action(author:, time: Time.zone.now)
track_unique_action(ISSUE_REMOVED_FROM_EPIC, author, time)
end
def track_issue_changed_epic_action(author:, time: Time.zone.now)
track_unique_action(ISSUE_CHANGED_EPIC, author, time)
end
def track_issue_designs_added_action(author:, time: Time.zone.now)
track_unique_action(ISSUE_DESIGNS_ADDED, author, time)
end

View File

@ -2551,6 +2551,9 @@ msgstr ""
msgid "AlertSettings|Copy"
msgstr ""
msgid "AlertSettings|Delete integration"
msgstr ""
msgid "AlertSettings|Enter integration name"
msgstr ""
@ -2668,9 +2671,21 @@ msgstr ""
msgid "AlertsIntegrations|Prometheus"
msgstr ""
msgid "AlertsIntegrations|The integration could not be added. Please try again."
msgstr ""
msgid "AlertsIntegrations|The integration could not be deleted. Please try again."
msgstr ""
msgid "AlertsIntegrations|The integration has been successfully removed."
msgstr ""
msgid "AlertsIntegrations|The integration has been successfully saved. Alerts from this new integration should now appear on your alerts list."
msgstr ""
msgid "AlertsIntegrations|You have opted to delete the %{integrationName} integration. Do you want to proceed? It means you will no longer receive alerts from this endpoint in your alert list, and this action cannot be undone."
msgstr ""
msgid "Algorithm"
msgstr ""

View File

@ -1998,10 +1998,6 @@ RSpec.describe Projects::MergeRequestsController do
describe 'POST export_csv' do
subject { post :export_csv, params: { namespace_id: project.namespace, project_id: project } }
before do
stub_feature_flags(export_merge_requests_as_csv: project)
end
it 'redirects to the merge request index' do
subject
@ -2014,17 +2010,5 @@ RSpec.describe Projects::MergeRequestsController do
subject
end
context 'feature is disabled' do
before do
stub_feature_flags(export_merge_requests_as_csv: false)
end
it 'expects a 404 response' do
subject
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
end

View File

@ -52,29 +52,20 @@ RSpec.describe 'Dashboard Merge Requests' do
end
context 'merge requests exist' do
let_it_be(:author_user) { create(:user) }
let(:label) { create(:label) }
let!(:assigned_merge_request) do
create(:merge_request,
assignees: [current_user],
source_project: project,
author: author_user)
end
let!(:review_requested_merge_request) do
create(:merge_request,
reviewers: [current_user],
source_branch: 'review',
source_project: project,
author: author_user)
author: create(:user))
end
let!(:assigned_merge_request_from_fork) do
create(:merge_request,
source_branch: 'markdown', assignees: [current_user],
target_project: public_project, source_project: forked_project,
author: author_user)
author: create(:user))
end
let!(:authored_merge_request) do
@ -103,7 +94,7 @@ RSpec.describe 'Dashboard Merge Requests' do
create(:merge_request,
source_branch: 'fix',
source_project: project,
author: author_user)
author: create(:user))
end
before do
@ -120,10 +111,6 @@ RSpec.describe 'Dashboard Merge Requests' do
expect(page).not_to have_content(labeled_merge_request.title)
end
it 'shows review requested merge requests' do
expect(page).to have_content(review_requested_merge_request.title)
end
it 'shows authored merge requests', :js do
reset_filters
input_filtered_search("author:=#{current_user.to_reference}")

View File

@ -22,13 +22,13 @@ RSpec.describe 'Invalid uploads that must be rejected', :api, :js do
)
end
RSpec.shared_examples 'rejecting invalid keys' do |key_name:, message: nil|
RSpec.shared_examples 'rejecting invalid keys' do |key_name:, message: nil, status: 500|
context "with invalid key #{key_name}" do
let(:body) { { key_name => file, 'package[test][name]' => 'test' } }
it { expect { subject }.not_to change { Packages::Package.nuget.count } }
it { expect(subject.code).to eq(500) }
it { expect(subject.code).to eq(status) }
it { expect(subject.body).to include(message.presence || "invalid field: \"#{key_name}\"") }
end
@ -45,7 +45,7 @@ RSpec.describe 'Invalid uploads that must be rejected', :api, :js do
# These keys are rejected directly by rack itself.
# The request will not be received by multipart.rb (can't use the 'handling file uploads' shared example)
it_behaves_like 'rejecting invalid keys', key_name: 'x' * 11000, message: 'Puma caught this error: exceeded available parameter key space (RangeError)'
it_behaves_like 'rejecting invalid keys', key_name: 'package[]test', message: 'Puma caught this error: expected Hash (got Array)'
it_behaves_like 'rejecting invalid keys', key_name: 'package[]test', status: 400, message: 'Bad Request'
it_behaves_like 'handling file uploads', 'by rejecting uploads with an invalid key'
end

View File

@ -9,38 +9,23 @@ RSpec.describe 'Merge Requests > Exports as CSV', :js do
before do
sign_in(user)
visit(project_merge_requests_path(project))
end
subject { page.find('.nav-controls') }
context 'feature is not enabled' do
it { is_expected.to have_button('Export as CSV') }
context 'button is clicked' do
before do
stub_feature_flags(export_merge_requests_as_csv: false)
visit(project_merge_requests_path(project))
click_button('Export as CSV')
end
it { is_expected.not_to have_button('Export as CSV') }
end
it 'shows a success message' do
click_link('Export merge requests')
context 'feature is enabled for a project' do
before do
stub_feature_flags(export_merge_requests_as_csv: project)
visit(project_merge_requests_path(project))
end
it { is_expected.to have_button('Export as CSV') }
context 'button is clicked' do
before do
click_button('Export as CSV')
end
it 'shows a success message' do
click_link('Export merge requests')
expect(page).to have_content 'Your CSV export has started.'
expect(page).to have_content "It will be emailed to #{user.email} when complete"
end
expect(page).to have_content 'Your CSV export has started.'
expect(page).to have_content "It will be emailed to #{user.email} when complete"
end
end
end

View File

@ -333,8 +333,6 @@ RSpec.describe MergeRequestsFinder do
end
context 'assignee filtering' do
let_it_be(:user3) { create(:user) }
let(:issuables) { described_class.new(user, params).execute }
it_behaves_like 'assignee ID filter' do
@ -353,6 +351,7 @@ RSpec.describe MergeRequestsFinder do
merge_request3.assignees = [user2, user3]
end
let_it_be(:user3) { create(:user) }
let(:params) { { assignee_username: [user2.username, user3.username] } }
let(:expected_issuables) { [merge_request3] }
end
@ -367,6 +366,7 @@ RSpec.describe MergeRequestsFinder do
end
it_behaves_like 'no assignee filter' do
let_it_be(:user3) { create(:user) }
let(:expected_issuables) { [merge_request4, merge_request5] }
end
@ -374,54 +374,30 @@ RSpec.describe MergeRequestsFinder do
let(:expected_issuables) { [merge_request1, merge_request2, merge_request3] }
end
context 'with just reviewers' do
it_behaves_like 'assignee username filter' do
before do
merge_request4.reviewers = [user3]
merge_request4.assignees = []
end
context 'filtering by group milestone' do
let(:group_milestone) { create(:milestone, group: group) }
let(:params) { { assignee_username: [user3.username] } }
let(:expected_issuables) { [merge_request4] }
before do
merge_request1.update!(milestone: group_milestone)
merge_request2.update!(milestone: group_milestone)
end
end
context 'with an additional reviewer' do
it_behaves_like 'assignee username filter' do
before do
merge_request3.assignees = [user3]
merge_request4.reviewers = [user3]
end
it 'returns merge requests assigned to that group milestone' do
params = { milestone_title: group_milestone.title }
let(:params) { { assignee_username: [user3.username] } }
let(:expected_issuables) { [merge_request3, merge_request4] }
end
end
end
context 'filtering by group milestone' do
let(:group_milestone) { create(:milestone, group: group) }
before do
merge_request1.update!(milestone: group_milestone)
merge_request2.update!(milestone: group_milestone)
end
it 'returns merge requests assigned to that group milestone' do
params = { milestone_title: group_milestone.title }
merge_requests = described_class.new(user, params).execute
expect(merge_requests).to contain_exactly(merge_request1, merge_request2)
end
context 'using NOT' do
let(:params) { { not: { milestone_title: group_milestone.title } } }
it 'returns MRs not assigned to that group milestone' do
merge_requests = described_class.new(user, params).execute
expect(merge_requests).to contain_exactly(merge_request3, merge_request4, merge_request5)
expect(merge_requests).to contain_exactly(merge_request1, merge_request2)
end
context 'using NOT' do
let(:params) { { not: { milestone_title: group_milestone.title } } }
it 'returns MRs not assigned to that group milestone' do
merge_requests = described_class.new(user, params).execute
expect(merge_requests).to contain_exactly(merge_request3, merge_request4, merge_request5)
end
end
end
end
@ -587,27 +563,6 @@ RSpec.describe MergeRequestsFinder do
expect(mrs).to eq([mr2])
end
end
it 'does not raise any exception with complex filters' do
# available filters from MergeRequest dashboard UI
params = {
project_id: project1.id,
scope: 'authored',
state: 'opened',
author_username: user.username,
assignee_username: user.username,
approver_usernames: [user.username],
approved_by_usernames: [user.username],
milestone_title: 'none',
release_tag: 'none',
label_names: 'none',
my_reaction_emoji: 'none',
draft: 'no'
}
merge_requests = described_class.new(user, params).execute
expect { merge_requests.load }.not_to raise_error
end
end
describe '#row_count', :request_store do

View File

@ -128,18 +128,18 @@ describe('AlertsSettingsFormNew', () => {
it('allows for update-integration with the correct form values for HTTP', async () => {
createComponent({
data: {
selectedIntegration: typeSet.http,
},
props: {
currentIntegration: { id: '1' },
currentIntegration: { id: '1', name: 'Test integration pre' },
loading: false,
},
});
const options = findSelect().findAll('option');
await options.at(1).setSelected();
await findFormFields()
.at(0)
.setValue('Test integration');
.setValue('Test integration post');
await findFormToggle().trigger('click');
await wrapper.vm.$nextTick();
@ -153,27 +153,27 @@ describe('AlertsSettingsFormNew', () => {
expect(wrapper.emitted('update-integration')).toBeTruthy();
expect(wrapper.emitted('update-integration')[0]).toEqual([
{ type: typeSet.http, variables: { name: 'Test integration', active: true } },
{ type: typeSet.http, variables: { name: 'Test integration post', active: true } },
]);
});
it('allows for update-integration with the correct form values for PROMETHEUS', async () => {
createComponent({
data: {
selectedIntegration: typeSet.prometheus,
},
props: {
currentIntegration: { id: '1' },
currentIntegration: { id: '1', apiUrl: 'https://test-pre.com' },
loading: false,
},
});
const options = findSelect().findAll('option');
await options.at(2).setSelected();
await findFormFields()
.at(0)
.setValue('Test integration');
await findFormFields()
.at(1)
.setValue('https://test.com');
.setValue('https://test-post.com');
await findFormToggle().trigger('click');
await wrapper.vm.$nextTick();
@ -187,7 +187,7 @@ describe('AlertsSettingsFormNew', () => {
expect(wrapper.emitted('update-integration')).toBeTruthy();
expect(wrapper.emitted('update-integration')[0]).toEqual([
{ type: typeSet.prometheus, variables: { apiUrl: 'https://test.com', active: true } },
{ type: typeSet.prometheus, variables: { apiUrl: 'https://test-post.com', active: true } },
]);
});
});

View File

@ -1,13 +1,17 @@
import { mount } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import { mount, createLocalVue } from '@vue/test-utils';
import createMockApollo from 'jest/helpers/mock_apollo_helper';
import { GlLoadingIcon } from '@gitlab/ui';
import AlertsSettingsWrapper from '~/alerts_settings/components/alerts_settings_wrapper.vue';
import AlertsSettingsFormOld from '~/alerts_settings/components/alerts_settings_form_old.vue';
import AlertsSettingsFormNew from '~/alerts_settings/components/alerts_settings_form_new.vue';
import IntegrationsList from '~/alerts_settings/components/alerts_integrations_list.vue';
import getIntegrationsQuery from '~/alerts_settings/graphql/queries/get_integrations.query.graphql';
import createHttpIntegrationMutation from '~/alerts_settings/graphql/mutations/create_http_integration.mutation.graphql';
import createPrometheusIntegrationMutation from '~/alerts_settings/graphql/mutations/create_prometheus_integration.mutation.graphql';
import updateHttpIntegrationMutation from '~/alerts_settings/graphql/mutations/update_http_integration.mutation.graphql';
import updatePrometheusIntegrationMutation from '~/alerts_settings/graphql/mutations/update_prometheus_integration.mutation.graphql';
import destroyHttpIntegrationMutation from '~/alerts_settings/graphql/mutations/destroy_http_integration.mutation.graphql';
import resetHttpTokenMutation from '~/alerts_settings/graphql/mutations/reset_http_token.mutation.graphql';
import resetPrometheusTokenMutation from '~/alerts_settings/graphql/mutations/reset_prometheus_token.mutation.graphql';
import { typeSet } from '~/alerts_settings/constants';
@ -20,16 +24,34 @@ import {
createPrometheusVariables,
updatePrometheusVariables,
ID,
errorMsg,
getIntegrationsQueryResponse,
destroyIntegrationResponse,
integrationToDestroy,
destroyIntegrationResponseWithErrors,
} from './mocks/apollo_mock';
jest.mock('~/flash');
const localVue = createLocalVue();
describe('AlertsSettingsWrapper', () => {
let wrapper;
let fakeApollo;
let destroyIntegrationHandler;
const findLoader = () => wrapper.find(IntegrationsList).find(GlLoadingIcon);
const findIntegrations = () => wrapper.find(IntegrationsList).findAll('table tbody tr');
async function destroyHttpIntegration(localWrapper) {
await jest.runOnlyPendingTimers();
await localWrapper.vm.$nextTick();
localWrapper
.find(IntegrationsList)
.vm.$emit('delete-integration', { id: integrationToDestroy.id });
}
const createComponent = ({ data = {}, provide = {}, loading = false } = {}) => {
wrapper = mount(AlertsSettingsWrapper, {
data() {
@ -54,6 +76,29 @@ describe('AlertsSettingsWrapper', () => {
});
};
function createComponentWithApollo({
destroyHandler = jest.fn().mockResolvedValue(destroyIntegrationResponse),
} = {}) {
localVue.use(VueApollo);
destroyIntegrationHandler = destroyHandler;
const requestHandlers = [
[getIntegrationsQuery, jest.fn().mockResolvedValue(getIntegrationsQueryResponse)],
[destroyHttpIntegrationMutation, destroyIntegrationHandler],
];
fakeApollo = createMockApollo(requestHandlers);
wrapper = mount(AlertsSettingsWrapper, {
localVue,
apolloProvider: fakeApollo,
provide: {
...defaultAlertSettingsConfig,
glFeatures: { httpIntegrationsList: true },
},
});
}
afterEach(() => {
if (wrapper) {
wrapper.destroy();
@ -243,7 +288,6 @@ describe('AlertsSettingsWrapper', () => {
});
it('shows error alert when integration creation fails ', async () => {
const errorMsg = 'Something went wrong';
createComponent({
data: { integrations: { list: mockIntegrations }, currentIntegration: mockIntegrations[0] },
provide: { glFeatures: { httpIntegrationsList: true } },
@ -259,7 +303,6 @@ describe('AlertsSettingsWrapper', () => {
});
it('shows error alert when integration token reset fails ', () => {
const errorMsg = 'Something went wrong';
createComponent({
data: { integrations: { list: mockIntegrations }, currentIntegration: mockIntegrations[0] },
provide: { glFeatures: { httpIntegrationsList: true } },
@ -276,7 +319,6 @@ describe('AlertsSettingsWrapper', () => {
});
it('shows error alert when integration update fails ', () => {
const errorMsg = 'Something went wrong';
createComponent({
data: { integrations: { list: mockIntegrations }, currentIntegration: mockIntegrations[0] },
provide: { glFeatures: { httpIntegrationsList: true } },
@ -292,4 +334,41 @@ describe('AlertsSettingsWrapper', () => {
});
});
});
describe('with mocked Apollo client', () => {
it('has a selection of integrations loaded via the getIntegrationsQuery', async () => {
createComponentWithApollo();
await jest.runOnlyPendingTimers();
await wrapper.vm.$nextTick();
expect(findIntegrations()).toHaveLength(4);
});
it('calls a mutation with correct parameters and destroys a integration', async () => {
createComponentWithApollo();
await destroyHttpIntegration(wrapper);
expect(destroyIntegrationHandler).toHaveBeenCalled();
await wrapper.vm.$nextTick();
expect(findIntegrations()).toHaveLength(3);
});
it('displays flash if mutation had a recoverable error', async () => {
createComponentWithApollo({
destroyHandler: jest.fn().mockResolvedValue(destroyIntegrationResponseWithErrors),
});
await destroyHttpIntegration(wrapper);
await wrapper.vm.$nextTick(); // kick off the DOM update
await jest.runOnlyPendingTimers(); // kick off the mocked GQL stuff (promises)
await wrapper.vm.$nextTick(); // kick off the DOM update for flash
expect(createFlash).toHaveBeenCalledWith({ message: 'Houston, we have a problem' });
});
});
});

View File

@ -1,5 +1,6 @@
const projectPath = '';
export const ID = 'gid://gitlab/AlertManagement::HttpIntegration/7';
export const errorMsg = 'Something went wrong';
export const createHttpVariables = {
name: 'Test Pre',
@ -24,3 +25,99 @@ export const updatePrometheusVariables = {
active: true,
id: ID,
};
export const getIntegrationsQueryResponse = {
data: {
project: {
alertManagementIntegrations: {
nodes: [
{
id: '37',
type: 'HTTP',
active: true,
name: 'Test 5',
url:
'http://127.0.0.1:3000/h5bp/html5-boilerplate/alerts/notify/test-5/d4875758e67334f3.json',
token: '89eb01df471d990ff5162a1c640408cf',
apiUrl: null,
},
{
id: '41',
type: 'HTTP',
active: true,
name: 'Test 9999',
url:
'http://127.0.0.1:3000/h5bp/html5-boilerplate/alerts/notify/test-9999/b78a566e1776cfc2.json',
token: 'f7579aa03844e07af3b1f0fca3f79f81',
apiUrl: null,
},
{
id: '40',
type: 'HTTP',
active: true,
name: 'Test 6',
url:
'http://127.0.0.1:3000/h5bp/html5-boilerplate/alerts/notify/test-6/3e828ae28a240222.json',
token: '6536102a607a5dd74fcdde921f2349ee',
apiUrl: null,
},
{
id: '12',
type: 'PROMETHEUS',
active: false,
name: 'Prometheus',
url: 'http://127.0.0.1:3000/h5bp/html5-boilerplate/prometheus/alerts/notify.json',
token: '256f687c6225aa5d6ee50c3d68120c4c',
apiUrl: 'https://localhost.ieeeesassadasasa',
},
],
},
},
},
};
export const integrationToDestroy = {
id: '37',
type: 'HTTP',
active: true,
name: 'Test 5',
url: 'http://127.0.0.1:3000/h5bp/html5-boilerplate/alerts/notify/test-5/d4875758e67334f3.json',
token: '89eb01df471d990ff5162a1c640408cf',
apiUrl: null,
};
export const destroyIntegrationResponse = {
data: {
httpIntegrationDestroy: {
errors: [],
integration: {
id: '37',
type: 'HTTP',
active: true,
name: 'Test 5',
url:
'http://127.0.0.1:3000/h5bp/html5-boilerplate/alerts/notify/test-5/d4875758e67334f3.json',
token: '89eb01df471d990ff5162a1c640408cf',
apiUrl: null,
},
},
},
};
export const destroyIntegrationResponseWithErrors = {
data: {
httpIntegrationDestroy: {
errors: ['Houston, we have a problem'],
integration: {
id: '37',
type: 'HTTP',
active: true,
name: 'Test 5',
url:
'http://127.0.0.1:3000/h5bp/html5-boilerplate/alerts/notify/test-5/d4875758e67334f3.json',
token: '89eb01df471d990ff5162a1c640408cf',
apiUrl: null,
},
},
},
};

View File

@ -22,7 +22,7 @@ describe('tags list row', () => {
let wrapper;
const [tag] = [...tagsListResponse.data];
const defaultProps = { tag, isDesktop: true, index: 0 };
const defaultProps = { tag, isMobile: false, index: 0 };
const findCheckbox = () => wrapper.find(GlFormCheckbox);
const findName = () => wrapper.find('[data-testid="name"]');
@ -114,7 +114,7 @@ describe('tags list row', () => {
});
it('on mobile has mw-s class', () => {
mountComponent({ ...defaultProps, isDesktop: false });
mountComponent({ ...defaultProps, isMobile: true });
expect(findName().classes('mw-s')).toBe(true);
});

View File

@ -14,7 +14,7 @@ describe('Tags List', () => {
const findDeleteButton = () => wrapper.find(GlButton);
const findListTitle = () => wrapper.find('[data-testid="list-title"]');
const mountComponent = (propsData = { tags, isDesktop: true }) => {
const mountComponent = (propsData = { tags, isMobile: false }) => {
wrapper = shallowMount(component, {
propsData,
});
@ -41,15 +41,15 @@ describe('Tags List', () => {
describe('delete button', () => {
it.each`
inputTags | isDesktop | isVisible
${tags} | ${true} | ${true}
${tags} | ${false} | ${false}
${readOnlyTags} | ${true} | ${false}
${readOnlyTags} | ${false} | ${false}
inputTags | isMobile | isVisible
${tags} | ${false} | ${true}
${tags} | ${true} | ${false}
${readOnlyTags} | ${false} | ${false}
${readOnlyTags} | ${true} | ${false}
`(
'is $isVisible that delete button exists when tags is $inputTags and isDesktop is $isDesktop',
({ inputTags, isDesktop, isVisible }) => {
mountComponent({ tags: inputTags, isDesktop });
'is $isVisible that delete button exists when tags is $inputTags and isMobile is $isMobile',
({ inputTags, isMobile, isVisible }) => {
mountComponent({ tags: inputTags, isMobile });
expect(findDeleteButton().exists()).toBe(isVisible);
},
@ -110,12 +110,6 @@ describe('Tags List', () => {
expect(rows.at(0).attributes()).toMatchObject({
first: 'true',
isdesktop: 'true',
});
// The list has only two tags and for some reasons .at(-1) does not work
expect(rows.at(1).attributes()).toMatchObject({
isdesktop: 'true',
});
});

View File

@ -124,7 +124,7 @@ describe('Details Page', () => {
it('has the correct props bound', () => {
expect(findTagsList().props()).toMatchObject({
isDesktop: true,
isMobile: false,
tags: store.state.tags,
});
});

View File

@ -1,5 +1,5 @@
import { shallowMount } from '@vue/test-utils';
import { GlDeprecatedDropdownItem } from '@gitlab/ui';
import { GlDropdownItem } from '@gitlab/ui';
import CommitMessageDropdown from '~/vue_merge_request_widget/components/states/commit_message_dropdown.vue';
const commits = [
@ -39,7 +39,7 @@ describe('Commits message dropdown component', () => {
wrapper.destroy();
});
const findDropdownElements = () => wrapper.findAll(GlDeprecatedDropdownItem);
const findDropdownElements = () => wrapper.findAll(GlDropdownItem);
const findFirstDropdownElement = () => findDropdownElements().at(0);
it('should have 3 elements in dropdown list', () => {

View File

@ -4,8 +4,9 @@ require 'spec_helper'
RSpec.describe Gitlab::Graphql::Loaders::BatchModelLoader do
describe '#find' do
let(:issue) { create(:issue) }
let(:user) { create(:user) }
let_it_be(:issue) { create(:issue) }
let_it_be(:other_user) { create(:user) }
let_it_be(:user) { create(:user) }
it 'finds a model by id' do
issue_result = described_class.new(Issue, issue.id).find
@ -16,15 +17,25 @@ RSpec.describe Gitlab::Graphql::Loaders::BatchModelLoader do
end
it 'only queries once per model' do
other_user = create(:user)
user
issue
expect do
[described_class.new(User, other_user.id).find,
described_class.new(User, user.id).find,
described_class.new(Issue, issue.id).find].map(&:sync)
end.not_to exceed_query_limit(2)
end
it 'does not force values unnecessarily' do
expect do
a = described_class.new(User, user.id).find
b = described_class.new(Issue, issue.id).find
b.sync
c = described_class.new(User, other_user.id).find
a.sync
c.sync
end.not_to exceed_query_limit(2)
end
end
end

View File

@ -4,6 +4,8 @@ require 'spec_helper'
require "rack/test"
RSpec.describe Gitlab::Middleware::HandleMalformedStrings do
include GitHttpHelpers
let(:null_byte) { "\u0000" }
let(:escaped_null_byte) { "%00" }
let(:invalid_string) { "mal\xC0formed" }
@ -57,6 +59,22 @@ RSpec.describe Gitlab::Middleware::HandleMalformedStrings do
end
end
context 'in authorization headers' do
let(:problematic_input) { null_byte }
it 'rejects problematic input in the password' do
env = env_for.merge(auth_env("username", "password#{problematic_input}encoded", nil))
expect(subject.call(env)).to eq error_400
end
it 'rejects problematic input in the password' do
env = env_for.merge(auth_env("username#{problematic_input}", "password#{problematic_input}encoded", nil))
expect(subject.call(env)).to eq error_400
end
end
context 'in params' do
shared_examples_for 'checks params' do
it 'rejects bad params in a top level param' do
@ -86,21 +104,21 @@ RSpec.describe Gitlab::Middleware::HandleMalformedStrings do
expect(subject.call(env)).to eq error_400
end
it "gives up and does not reject too deeply nested params" do
env = env_for(name: [
{
inner_key: { deeper_key: [{ hash_inside_array_key: "I am #{problematic_input} bad" }] }
}
])
expect(subject.call(env)).not_to eq error_400
end
end
context 'with null byte' do
it_behaves_like 'checks params' do
let(:problematic_input) { null_byte }
let(:problematic_input) { null_byte }
it_behaves_like 'checks params'
it "gives up and does not reject too deeply nested params" do
env = env_for(name: [
{
inner_key: { deeper_key: [{ hash_inside_array_key: "I am #{problematic_input} bad" }] }
}
])
expect(subject.call(env)).not_to eq error_400
end
end
@ -124,4 +142,10 @@ RSpec.describe Gitlab::Middleware::HandleMalformedStrings do
expect(subject.call(env)).not_to eq error_400
end
end
it 'does not modify the env' do
env = env_for
expect { subject.call(env) }.not_to change { env }
end
end

View File

@ -168,36 +168,6 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git
end
end
context 'for Issue added to epic actions' do
it_behaves_like 'a tracked issue edit event' do
let(:action) { described_class::ISSUE_ADDED_TO_EPIC}
def track_action(params)
described_class.track_issue_added_to_epic_action(**params)
end
end
end
context 'for Issue removed from epic actions' do
it_behaves_like 'a tracked issue edit event' do
let(:action) { described_class::ISSUE_REMOVED_FROM_EPIC}
def track_action(params)
described_class.track_issue_removed_from_epic_action(**params)
end
end
end
context 'for Issue changed epic actions' do
it_behaves_like 'a tracked issue edit event' do
let(:action) { described_class::ISSUE_CHANGED_EPIC}
def track_action(params)
described_class.track_issue_changed_epic_action(**params)
end
end
end
context 'for Issue designs added actions' do
it_behaves_like 'a tracked issue edit event' do
let(:action) { described_class::ISSUE_DESIGNS_ADDED }

View File

@ -2,7 +2,9 @@
require 'spec_helper'
RSpec.describe 'User sends malformed strings as params' do
RSpec.describe 'User sends malformed strings' do
include GitHttpHelpers
let(:null_byte) { "\u0000" }
let(:invalid_string) { "mal\xC0formed" }
@ -17,4 +19,10 @@ RSpec.describe 'User sends malformed strings as params' do
expect(response).to have_gitlab_http_status(:bad_request)
end
it 'raises a 400 error with null bytes in the auth headers' do
clone_get("project/path", user: "hello#{null_byte}", password: "nothing to see")
expect(response).to have_gitlab_http_status(:bad_request)
end
end

View File

@ -25,7 +25,7 @@ RSpec.describe 'projects/settings/operations/show' do
end
before_all do
project.add_reporter(user)
project.add_maintainer(user)
end
before do