Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-11-07 06:08:10 +00:00
parent a31408ba64
commit 44c74f7b06
36 changed files with 438 additions and 2123 deletions

View File

@ -597,7 +597,6 @@ Layout/LineLength:
- 'app/services/loose_foreign_keys/cleaner_service.rb' - 'app/services/loose_foreign_keys/cleaner_service.rb'
- 'app/services/members/destroy_service.rb' - 'app/services/members/destroy_service.rb'
- 'app/services/members/invitation_reminder_email_service.rb' - 'app/services/members/invitation_reminder_email_service.rb'
- 'app/services/members/update_service.rb'
- 'app/services/merge_requests/add_context_service.rb' - 'app/services/merge_requests/add_context_service.rb'
- 'app/services/merge_requests/assign_issues_service.rb' - 'app/services/merge_requests/assign_issues_service.rb'
- 'app/services/merge_requests/base_service.rb' - 'app/services/merge_requests/base_service.rb'
@ -5539,7 +5538,6 @@ Layout/LineLength:
- 'spec/services/members/destroy_service_spec.rb' - 'spec/services/members/destroy_service_spec.rb'
- 'spec/services/members/invitation_reminder_email_service_spec.rb' - 'spec/services/members/invitation_reminder_email_service_spec.rb'
- 'spec/services/members/unassign_issuables_service_spec.rb' - 'spec/services/members/unassign_issuables_service_spec.rb'
- 'spec/services/members/update_service_spec.rb'
- 'spec/services/merge_requests/add_context_service_spec.rb' - 'spec/services/merge_requests/add_context_service_spec.rb'
- 'spec/services/merge_requests/after_create_service_spec.rb' - 'spec/services/merge_requests/after_create_service_spec.rb'
- 'spec/services/merge_requests/assign_issues_service_spec.rb' - 'spec/services/merge_requests/assign_issues_service_spec.rb'

View File

@ -2972,7 +2972,6 @@ RSpec/ContextWording:
- 'spec/services/members/destroy_service_spec.rb' - 'spec/services/members/destroy_service_spec.rb'
- 'spec/services/members/groups/creator_service_spec.rb' - 'spec/services/members/groups/creator_service_spec.rb'
- 'spec/services/members/projects/creator_service_spec.rb' - 'spec/services/members/projects/creator_service_spec.rb'
- 'spec/services/members/update_service_spec.rb'
- 'spec/services/merge_requests/add_todo_when_build_fails_service_spec.rb' - 'spec/services/merge_requests/add_todo_when_build_fails_service_spec.rb'
- 'spec/services/merge_requests/after_create_service_spec.rb' - 'spec/services/merge_requests/after_create_service_spec.rb'
- 'spec/services/merge_requests/approval_service_spec.rb' - 'spec/services/merge_requests/approval_service_spec.rb'

View File

@ -309,7 +309,6 @@ Style/IfUnlessModifier:
- 'app/services/issues/update_service.rb' - 'app/services/issues/update_service.rb'
- 'app/services/lfs/lock_file_service.rb' - 'app/services/lfs/lock_file_service.rb'
- 'app/services/members/destroy_service.rb' - 'app/services/members/destroy_service.rb'
- 'app/services/members/update_service.rb'
- 'app/services/merge_requests/add_context_service.rb' - 'app/services/merge_requests/add_context_service.rb'
- 'app/services/merge_requests/base_service.rb' - 'app/services/merge_requests/base_service.rb'
- 'app/services/merge_requests/build_service.rb' - 'app/services/merge_requests/build_service.rb'

View File

@ -73,6 +73,7 @@ export default {
<template> <template>
<gl-modal <gl-modal
data-testid="test-case-details-modal"
:modal-id="modalId" :modal-id="modalId"
:title="testCase.classname" :title="testCase.classname"
:action-primary="$options.modalCloseButton" :action-primary="$options.modalCloseButton"

View File

@ -2,12 +2,10 @@ import IssueStatusIcon from '~/reports/components/issue_status_icon.vue';
export const components = { export const components = {
CodequalityIssueBody: () => import('../codequality_report/components/codequality_issue_body.vue'), CodequalityIssueBody: () => import('../codequality_report/components/codequality_issue_body.vue'),
TestIssueBody: () => import('../grouped_test_report/components/test_issue_body.vue'),
}; };
export const componentNames = { export const componentNames = {
CodequalityIssueBody: 'CodequalityIssueBody', CodequalityIssueBody: 'CodequalityIssueBody',
TestIssueBody: 'TestIssueBody',
}; };
export const iconComponents = { export const iconComponents = {

View File

@ -1,74 +0,0 @@
<script>
import { GlModal, GlLink, GlSprintf } from '@gitlab/ui';
import CodeBlock from '~/vue_shared/components/code_block.vue';
import { fieldTypes } from '../../constants';
export default {
components: {
CodeBlock,
GlModal,
GlLink,
GlSprintf,
},
props: {
visible: {
type: Boolean,
required: true,
},
title: {
type: String,
required: true,
},
modalData: {
type: Object,
required: true,
},
},
computed: {
filteredModalData() {
// Filter out the properties that don't have a value
return Object.fromEntries(
Object.entries(this.modalData).filter((data) => Boolean(data[1].value)),
);
},
},
fieldTypes,
};
</script>
<template>
<gl-modal
:visible="visible"
modal-id="modal-mrwidget-reports"
:title="title"
:hide-footer="true"
@hide="$emit('hide')"
>
<div v-for="(field, key, index) in filteredModalData" :key="index" class="row gl-mt-3 gl-mb-3">
<strong class="col-sm-3 text-right"> {{ field.text }}: </strong>
<div class="col-sm-9">
<code-block v-if="field.type === $options.fieldTypes.codeBlock" :code="field.value" />
<gl-link
v-else-if="field.type === $options.fieldTypes.link"
:href="field.value.path"
target="_blank"
>
{{ field.value.text }}
</gl-link>
<gl-sprintf
v-else-if="field.type === $options.fieldTypes.seconds"
:message="__('%{value} s')"
>
<template #value>{{ field.value }}</template>
</gl-sprintf>
<template v-else-if="field.type === $options.fieldTypes.text">
{{ field.value }}
</template>
</div>
</div>
</gl-modal>
</template>

View File

@ -1,64 +0,0 @@
<script>
import { GlBadge, GlButton } from '@gitlab/ui';
import { mapActions } from 'vuex';
import { sprintf, n__ } from '~/locale';
import IssueStatusIcon from '~/reports/components/issue_status_icon.vue';
import { STATUS_NEUTRAL } from '../../constants';
export default {
name: 'TestIssueBody',
components: {
GlBadge,
GlButton,
IssueStatusIcon,
},
props: {
issue: {
type: Object,
required: true,
},
},
computed: {
recentFailureMessage() {
return sprintf(
n__(
'Reports|Failed %{count} time in %{base_branch} in the last 14 days',
'Reports|Failed %{count} times in %{base_branch} in the last 14 days',
this.issue.recent_failures?.count,
),
this.issue.recent_failures,
);
},
showRecentFailures() {
return this.issue.recent_failures?.count && this.issue.recent_failures?.base_branch;
},
status() {
return this.issue.status || STATUS_NEUTRAL;
},
},
methods: {
...mapActions(['openModal']),
},
};
</script>
<template>
<div class="gl-display-flex gl-mt-2 gl-mb-2">
<issue-status-icon :status="status" :status-icon-size="24" class="gl-mr-3" />
<gl-button
button-text-classes="gl-white-space-normal! gl-word-break-all gl-text-left"
variant="link"
data-testid="test-issue-body-description"
@click="openModal({ issue })"
>
<gl-badge
v-if="showRecentFailures"
variant="warning"
class="gl-mr-2"
data-testid="test-issue-body-recent-failures"
>
{{ recentFailureMessage }}
</gl-badge>
{{ issue.name }}
</gl-button>
</div>
</template>

View File

@ -1,204 +0,0 @@
<script>
import { GlButton, GlIcon } from '@gitlab/ui';
import { mapActions, mapGetters, mapState } from 'vuex';
import api from '~/api';
import { sprintf, s__ } from '~/locale';
import GroupedIssuesList from '../components/grouped_issues_list.vue';
import { componentNames } from '../components/issue_body';
import ReportSection from '../components/report_section.vue';
import SummaryRow from '../components/summary_row.vue';
import Modal from './components/modal.vue';
import createStore from './store';
import {
summaryTextBuilder,
reportTextBuilder,
statusIcon,
recentFailuresTextBuilder,
} from './store/utils';
export default {
name: 'GroupedTestReportsApp',
store: createStore(),
components: {
ReportSection,
SummaryRow,
GroupedIssuesList,
Modal,
GlButton,
GlIcon,
},
props: {
endpoint: {
type: String,
required: true,
},
pipelinePath: {
type: String,
required: false,
default: '',
},
headBlobPath: {
type: String,
required: true,
},
},
componentNames,
computed: {
...mapState(['reports', 'isLoading', 'hasError', 'summary']),
...mapState({
modalTitle: (state) => state.modal.title || '',
modalData: (state) => state.modal.data || {},
modalOpen: (state) => state.modal.open || false,
}),
...mapGetters(['summaryStatus']),
groupedSummaryText() {
if (this.isLoading) {
return s__('Reports|Test summary results are being parsed');
}
if (this.hasError) {
return s__('Reports|Test summary failed loading results');
}
return summaryTextBuilder(s__('Reports|Test summary'), this.summary);
},
testTabURL() {
return `${this.pipelinePath}/test_report`;
},
showViewFullReport() {
return this.pipelinePath.length;
},
},
created() {
this.setPaths({
endpoint: this.endpoint,
headBlobPath: this.headBlobPath,
});
this.fetchReports();
},
methods: {
...mapActions(['setPaths', 'fetchReports', 'closeModal']),
handleToggleEvent() {
api.trackRedisHllUserEvent(this.$options.expandEvent);
},
reportText(report) {
const { name, summary } = report || {};
if (report.status === 'error') {
return sprintf(s__('Reports|An error occurred while loading %{name} results'), { name });
}
if (!report.name) {
return s__('Reports|An error occurred while loading report');
}
return reportTextBuilder(name, summary);
},
hasRecentFailures(summary) {
return summary?.recentlyFailed > 0;
},
recentFailuresText(summary) {
return recentFailuresTextBuilder(summary);
},
getReportIcon(report) {
return statusIcon(report.status);
},
shouldRenderIssuesList(report) {
return (
report.existing_failures.length > 0 ||
report.new_failures.length > 0 ||
report.resolved_failures.length > 0 ||
report.existing_errors.length > 0 ||
report.new_errors.length > 0 ||
report.resolved_errors.length > 0
);
},
unresolvedIssues(report) {
return [
...report.new_failures,
...report.new_errors,
...report.existing_failures,
...report.existing_errors,
];
},
resolvedIssues(report) {
return report.resolved_failures.concat(report.resolved_errors);
},
},
expandEvent: 'i_testing_summary_widget_total',
};
</script>
<template>
<report-section
:status="summaryStatus"
:success-text="groupedSummaryText"
:loading-text="groupedSummaryText"
:error-text="groupedSummaryText"
:has-issues="reports.length > 0"
:should-emit-toggle-event="true"
class="mr-widget-section grouped-security-reports mr-report"
@toggleEvent.once="handleToggleEvent"
>
<template v-if="showViewFullReport" #action-buttons>
<gl-button
:href="testTabURL"
target="_blank"
icon="external-link"
data-testid="group-test-reports-full-link"
class="gl-mr-3"
>
{{ s__('ciReport|View full report') }}
</gl-button>
</template>
<template v-if="hasRecentFailures(summary)" #sub-heading>
{{ recentFailuresText(summary) }}
</template>
<template #body>
<div class="mr-widget-grouped-section report-block">
<template v-for="(report, i) in reports">
<summary-row
:key="`summary-row-${i}`"
:status-icon="getReportIcon(report)"
nested-summary
>
<template #summary>
<div class="gl-display-inline-flex gl-flex-direction-column">
<div>{{ reportText(report) }}</div>
<div v-if="report.suite_errors">
<div v-if="report.suite_errors.head">
<gl-icon name="warning" class="gl-mx-2 gl-text-orange-500" />
{{ s__('Reports|Head report parsing error:') }}
{{ report.suite_errors.head }}
</div>
<div v-if="report.suite_errors.base">
<gl-icon name="warning" class="gl-mx-2 gl-text-orange-500" />
{{ s__('Reports|Base report parsing error:') }}
{{ report.suite_errors.base }}
</div>
</div>
<div v-if="hasRecentFailures(report.summary)">
{{ recentFailuresText(report.summary) }}
</div>
</div>
</template>
</summary-row>
<grouped-issues-list
v-if="shouldRenderIssuesList(report)"
:key="`issues-list-${i}`"
:unresolved-issues="unresolvedIssues(report)"
:resolved-issues="resolvedIssues(report)"
:component="$options.componentNames.TestIssueBody"
:nested-level="2"
/>
</template>
<modal
:visible="modalOpen"
:title="modalTitle"
:modal-data="modalData"
@hide="closeModal"
/>
</div>
</template>
</report-section>
</template>

View File

@ -1,82 +0,0 @@
import Visibility from 'visibilityjs';
import axios from '~/lib/utils/axios_utils';
import httpStatusCodes from '~/lib/utils/http_status';
import Poll from '~/lib/utils/poll';
import * as types from './mutation_types';
export const setPaths = ({ commit }, paths) => commit(types.SET_PATHS, paths);
export const requestReports = ({ commit }) => commit(types.REQUEST_REPORTS);
let eTagPoll;
export const clearEtagPoll = () => {
eTagPoll = null;
};
export const stopPolling = () => {
if (eTagPoll) eTagPoll.stop();
};
export const restartPolling = () => {
if (eTagPoll) eTagPoll.restart();
};
/**
* We need to poll the reports endpoint while they are being parsed in the Backend.
* This can take up to one minute.
*
* Poll.js will handle etag response.
* While http status code is 204, it means it's parsing, and we'll keep polling
* When http status code is 200, it means parsing is done, we can show the results & stop polling
* When http status code is 500, it means parsing went wrong and we stop polling
*/
export const fetchReports = ({ state, dispatch }) => {
dispatch('requestReports');
eTagPoll = new Poll({
resource: {
getReports(endpoint) {
return axios.get(endpoint);
},
},
data: state.endpoint,
method: 'getReports',
successCallback: ({ data, status }) =>
dispatch('receiveReportsSuccess', {
data,
status,
}),
errorCallback: () => dispatch('receiveReportsError'),
});
if (!Visibility.hidden()) {
eTagPoll.makeRequest();
} else {
axios
.get(state.endpoint)
.then(({ data, status }) => dispatch('receiveReportsSuccess', { data, status }))
.catch(() => dispatch('receiveReportsError'));
}
Visibility.change(() => {
if (!Visibility.hidden()) {
dispatch('restartPolling');
} else {
dispatch('stopPolling');
}
});
};
export const receiveReportsSuccess = ({ commit }, response) => {
// With 204 we keep polling and don't update the state
if (response.status === httpStatusCodes.OK) {
commit(types.RECEIVE_REPORTS_SUCCESS, response.data);
}
};
export const receiveReportsError = ({ commit }) => commit(types.RECEIVE_REPORTS_ERROR);
export const openModal = ({ commit }, payload) => commit(types.SET_ISSUE_MODAL_DATA, payload);
export const closeModal = ({ commit }, payload) => commit(types.RESET_ISSUE_MODAL_DATA, payload);

View File

@ -1,13 +0,0 @@
import { LOADING, ERROR, SUCCESS, STATUS_FAILED } from '../../constants';
export const summaryStatus = (state) => {
if (state.isLoading) {
return LOADING;
}
if (state.hasError || state.status === STATUS_FAILED) {
return ERROR;
}
return SUCCESS;
};

View File

@ -1,17 +0,0 @@
import Vue from 'vue';
import Vuex from 'vuex';
import * as actions from './actions';
import * as getters from './getters';
import mutations from './mutations';
import state from './state';
Vue.use(Vuex);
export const getStoreConfig = () => ({
actions,
mutations,
getters,
state: state(),
});
export default () => new Vuex.Store(getStoreConfig());

View File

@ -1,7 +0,0 @@
export const SET_PATHS = 'SET_PATHS';
export const REQUEST_REPORTS = 'REQUEST_REPORTS';
export const RECEIVE_REPORTS_SUCCESS = 'RECEIVE_REPORTS_SUCCESS';
export const RECEIVE_REPORTS_ERROR = 'RECEIVE_REPORTS_ERROR';
export const SET_ISSUE_MODAL_DATA = 'SET_ISSUE_MODAL_DATA';
export const RESET_ISSUE_MODAL_DATA = 'RESET_ISSUE_MODAL_DATA';

View File

@ -1,79 +0,0 @@
import * as types from './mutation_types';
import { countRecentlyFailedTests, formatFilePath } from './utils';
export default {
[types.SET_PATHS](state, { endpoint, headBlobPath }) {
state.endpoint = endpoint;
state.headBlobPath = headBlobPath;
},
[types.REQUEST_REPORTS](state) {
state.isLoading = true;
},
[types.RECEIVE_REPORTS_SUCCESS](state, response) {
state.hasError = response.suites.some((suite) => suite.status === 'error');
state.isLoading = false;
state.summary.total = response.summary.total;
state.summary.resolved = response.summary.resolved;
state.summary.failed = response.summary.failed;
state.summary.errored = response.summary.errored;
state.summary.recentlyFailed = countRecentlyFailedTests(response.suites);
state.status = response.status;
state.reports = response.suites;
state.reports.forEach((report, i) => {
if (!state.reports[i].summary) return;
state.reports[i].summary.recentlyFailed = countRecentlyFailedTests(report);
});
},
[types.RECEIVE_REPORTS_ERROR](state) {
state.isLoading = false;
state.hasError = true;
state.reports = [];
state.summary = {
total: 0,
resolved: 0,
failed: 0,
errored: 0,
recentlyFailed: 0,
};
state.status = null;
},
[types.SET_ISSUE_MODAL_DATA](state, payload) {
const { issue } = payload;
state.modal.title = issue.name;
Object.keys(issue).forEach((key) => {
if (Object.prototype.hasOwnProperty.call(state.modal.data, key)) {
state.modal.data[key] = {
...state.modal.data[key],
value: issue[key],
};
}
});
if (issue.file) {
state.modal.data.filename.value = {
text: issue.file,
path: `${state.headBlobPath}/${formatFilePath(issue.file)}`,
};
}
state.modal.open = true;
},
[types.RESET_ISSUE_MODAL_DATA](state) {
state.modal.open = false;
// Resetting modal data
state.modal.title = null;
Object.keys(state.modal.data).forEach((key) => {
state.modal.data[key] = {
...state.modal.data[key],
value: null,
};
});
},
};

View File

@ -1,71 +0,0 @@
import { s__ } from '~/locale';
import { fieldTypes } from '../../constants';
export default () => ({
endpoint: null,
isLoading: false,
hasError: false,
status: null,
summary: {
total: 0,
resolved: 0,
failed: 0,
errored: 0,
},
/**
* Each report will have the following format:
* {
* name: {String},
* summary: {
* total: {Number},
* resolved: {Number},
* failed: {Number},
* errored: {Number},
* },
* new_failures: {Array.<Object>},
* resolved_failures: {Array.<Object>},
* existing_failures: {Array.<Object>},
* new_errors: {Array.<Object>},
* resolved_errors: {Array.<Object>},
* existing_errors: {Array.<Object>},
* }
*/
reports: [],
modal: {
title: null,
open: false,
data: {
classname: {
value: null,
text: s__('Reports|Classname'),
type: fieldTypes.text,
},
filename: {
value: null,
text: s__('Reports|Filename'),
type: fieldTypes.link,
},
execution_time: {
value: null,
text: s__('Reports|Execution time'),
type: fieldTypes.seconds,
},
failure: {
value: null,
text: s__('Reports|Failure'),
type: fieldTypes.codeBlock,
},
system_output: {
value: null,
text: s__('Reports|System output'),
type: fieldTypes.codeBlock,
},
},
},
});

View File

@ -1,111 +0,0 @@
import { sprintf, n__, s__, __ } from '~/locale';
import {
STATUS_FAILED,
STATUS_SUCCESS,
ICON_WARNING,
ICON_SUCCESS,
ICON_NOTFOUND,
} from '../../constants';
const textBuilder = (results) => {
const { failed, errored, resolved, total } = results;
const failedOrErrored = (failed || 0) + (errored || 0);
const failedString = failed ? n__('%d failed', '%d failed', failed) : null;
const erroredString = errored ? n__('%d error', '%d errors', errored) : null;
const combinedString =
failed && errored ? `${failedString}, ${erroredString}` : failedString || erroredString;
const resolvedString = resolved
? n__('%d fixed test result', '%d fixed test results', resolved)
: null;
const totalString = total ? n__('out of %d total test', 'out of %d total tests', total) : null;
let resultsString = s__('Reports|no changed test results');
if (failedOrErrored) {
if (resolved) {
resultsString = sprintf(s__('Reports|%{combinedString} and %{resolvedString}'), {
combinedString,
resolvedString,
});
} else {
resultsString = combinedString;
}
} else if (resolved) {
resultsString = resolvedString;
}
return `${resultsString} ${totalString}`;
};
export const summaryTextBuilder = (name = '', results = {}) => {
const resultsString = textBuilder(results);
return sprintf(__('%{name} contained %{resultsString}'), { name, resultsString });
};
export const reportTextBuilder = (name = '', results = {}) => {
const resultsString = textBuilder(results);
return sprintf(__('%{name} found %{resultsString}'), { name, resultsString });
};
export const recentFailuresTextBuilder = (summary = {}) => {
const { failed, recentlyFailed } = summary;
if (!failed || !recentlyFailed) return '';
if (failed < 2) {
return sprintf(
s__(
'Reports|%{recentlyFailed} out of %{failed} failed test has failed more than once in the last 14 days',
),
{ recentlyFailed, failed },
);
}
return sprintf(
n__(
'Reports|%{recentlyFailed} out of %{failed} failed tests has failed more than once in the last 14 days',
'Reports|%{recentlyFailed} out of %{failed} failed tests have failed more than once in the last 14 days',
recentlyFailed,
),
{ recentlyFailed, failed },
);
};
export const countRecentlyFailedTests = (subject) => {
// handle either a single report or an array of reports
const reports = !subject.length ? [subject] : subject;
return reports
.map((report) => {
return (
[report.new_failures, report.existing_failures, report.resolved_failures]
// only count tests which have failed more than once
.map(
(failureArray) =>
failureArray.filter((failure) => failure.recent_failures?.count > 1).length,
)
.reduce((total, count) => total + count, 0)
);
})
.reduce((total, count) => total + count, 0);
};
export const statusIcon = (status) => {
if (status === STATUS_FAILED) {
return ICON_WARNING;
}
if (status === STATUS_SUCCESS) {
return ICON_SUCCESS;
}
return ICON_NOTFOUND;
};
/**
* Removes `./` from the beginning of a file path so it can be appended onto a blob path
* @param {String} file
* @returns {String} - formatted value
*/
export const formatFilePath = (file) => {
return file.replace(/^\.?\/*/, '');
};

View File

@ -82,8 +82,6 @@ export default {
MrWidgetAutoMergeFailed: AutoMergeFailed, MrWidgetAutoMergeFailed: AutoMergeFailed,
MrWidgetRebase: RebaseState, MrWidgetRebase: RebaseState,
SourceBranchRemovalStatus, SourceBranchRemovalStatus,
GroupedTestReportsApp: () =>
import('../reports/grouped_test_report/grouped_test_reports_app.vue'),
MrWidgetApprovals, MrWidgetApprovals,
SecurityReportsApp: () => import('~/vue_shared/security_reports/security_reports_app.vue'), SecurityReportsApp: () => import('~/vue_shared/security_reports/security_reports_app.vue'),
MergeChecksFailed: () => import('./components/states/merge_checks_failed.vue'), MergeChecksFailed: () => import('./components/states/merge_checks_failed.vue'),
@ -183,9 +181,6 @@ export default {
shouldRenderTestReport() { shouldRenderTestReport() {
return Boolean(this.mr?.testResultsPath); return Boolean(this.mr?.testResultsPath);
}, },
shouldRenderRefactoredTestReport() {
return window.gon?.features?.refactorMrWidgetTestSummary;
},
mergeError() { mergeError() {
let { mergeError } = this.mr; let { mergeError } = this.mr;
@ -519,7 +514,7 @@ export default {
} }
}, },
registerTestReportExtension() { registerTestReportExtension() {
if (this.shouldRenderTestReport && this.shouldRenderRefactoredTestReport) { if (this.shouldRenderTestReport) {
registerExtension(testReportExtension); registerExtension(testReportExtension);
} }
}, },
@ -596,14 +591,6 @@ export default {
:mr-iid="mr.iid" :mr-iid="mr.iid"
/> />
<grouped-test-reports-app
v-if="shouldRenderTestReport && !shouldRenderRefactoredTestReport"
class="js-reports-container"
:endpoint="mr.testResultsPath"
:head-blob-path="mr.headBlobPath"
:pipeline-path="mr.pipeline.path"
/>
<div class="mr-widget-section" data-qa-selector="mr_widget_content"> <div class="mr-widget-section" data-qa-selector="mr_widget_content">
<component :is="componentName" :mr="mr" :service="service" /> <component :is="componentName" :mr="mr" :service="service" />
<ready-to-merge <ready-to-merge

View File

@ -34,7 +34,6 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
before_action only: [:show] do before_action only: [:show] do
push_frontend_feature_flag(:merge_request_widget_graphql, project) push_frontend_feature_flag(:merge_request_widget_graphql, project)
push_frontend_feature_flag(:core_security_mr_widget_counts, project) push_frontend_feature_flag(:core_security_mr_widget_counts, project)
push_frontend_feature_flag(:refactor_mr_widget_test_summary, project)
push_frontend_feature_flag(:issue_assignees_widget, @project) push_frontend_feature_flag(:issue_assignees_widget, @project)
push_frontend_feature_flag(:realtime_labels, project) push_frontend_feature_flag(:realtime_labels, project)
push_frontend_feature_flag(:refactor_security_extension, @project) push_frontend_feature_flag(:refactor_security_extension, @project)

View File

@ -2,40 +2,84 @@
module Members module Members
class UpdateService < Members::BaseService class UpdateService < Members::BaseService
# returns the updated member # @param members [Member, Array<Member>]
def execute(member, permission: :update) # returns the updated member(s)
raise Gitlab::Access::AccessDeniedError unless can?(current_user, action_member_permission(permission, member), member) def execute(members, permission: :update)
raise Gitlab::Access::AccessDeniedError if prevent_upgrade_to_owner?(member) || prevent_downgrade_from_owner?(member) members = Array.wrap(members)
return success(member: member) if update_results_in_no_change?(member) old_access_level_expiry_map = members.to_h do |member|
[member.id, { human_access: member.human_access, expires_at: member.expires_at }]
old_access_level = member.human_access
old_expiry = member.expires_at
member.attributes = params
return success(member: member) unless member.changed?
if member.save
after_execute(action: permission, old_access_level: old_access_level, old_expiry: old_expiry, member: member)
# Deletes only confidential issues todos for guests
enqueue_delete_todos(member) if downgrading_to_guest?
end end
if member.errors.any? if Feature.enabled?(:bulk_update_membership_roles, current_user)
error(member.errors.full_messages.to_sentence, pass_back: { member: member }) multiple_members_update(members, permission, old_access_level_expiry_map)
else else
success(member: member) single_member_update(members.first, permission, old_access_level_expiry_map)
end end
prepare_response(members)
end end
private private
def update_results_in_no_change?(member) def single_member_update(member, permission, old_access_level_expiry_map)
return false if params[:expires_at]&.to_date != member.expires_at raise Gitlab::Access::AccessDeniedError unless has_update_permissions?(member, permission)
return false if params[:access_level] != member.access_level
true member.attributes = params
return success(member: member) unless member.changed?
post_update(member, permission, old_access_level_expiry_map) if member.save
end
def multiple_members_update(members, permission, old_access_level_expiry_map)
begin
updated_members =
Member.transaction do
# Using `next` with `filter_map` avoids the `post_update` call for the member that resulted in no change
members.filter_map do |member|
raise Gitlab::Access::AccessDeniedError unless has_update_permissions?(member, permission)
member.attributes = params
next unless member.changed?
member.save!
member
end
end
rescue ActiveRecord::RecordInvalid
return
end
updated_members.each { |member| post_update(member, permission, old_access_level_expiry_map) }
end
def post_update(member, permission, old_access_level_expiry_map)
old_access_level = old_access_level_expiry_map[member.id][:human_access]
old_expiry = old_access_level_expiry_map[member.id][:expires_at]
after_execute(action: permission, old_access_level: old_access_level, old_expiry: old_expiry, member: member)
enqueue_delete_todos(member) if downgrading_to_guest? # Deletes only confidential issues todos for guests
end
def prepare_response(members)
errored_member = members.detect { |member| member.errors.any? }
if errored_member.present?
return error(errored_member.errors.full_messages.to_sentence, pass_back: { member: errored_member })
end
# TODO: Remove the :member key when removing the bulk_update_membership_roles FF and update where it's used.
# https://gitlab.com/gitlab-org/gitlab/-/issues/373257
if members.one?
success(member: members.first)
else
success(members: members)
end
end
def has_update_permissions?(member, permission)
can?(current_user, action_member_permission(permission, member), member) &&
!prevent_upgrade_to_owner?(member) &&
!prevent_downgrade_from_owner?(member)
end end
def downgrading_to_guest? def downgrading_to_guest?

View File

@ -1,8 +1,8 @@
--- ---
name: refactor_mr_widget_test_summary name: bulk_update_membership_roles
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/83631 introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/96745
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/358208 rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/373257
milestone: '15.0' milestone: '15.6'
type: development type: development
group: group::pipeline insights group: group::workspace
default_enabled: false default_enabled: false

View File

@ -0,0 +1,11 @@
# frozen_string_literal: true
all_changed_files = helper.all_changed_files
if all_changed_files.detect { |file| file == 'Gemfile' || file == 'Gemfile.lock' }
markdown <<~MSG
## Rubygems
This merge request adds, or changes a Rubygems dependency. Please review the [Gemfile guidelines](https://docs.gitlab.com/ee/development/gemfile.html).
MSG
end

View File

@ -230,6 +230,8 @@ Supported GitHub branch protection rules are mapped to GitLab branch protection
[Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/376683) in GitLab 15.6. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/376683) in GitLab 15.6.
- GitHub rule **Require signed commits** for the project's default branch is mapped to the **Reject unsigned commits** GitLab push rule. Requires GitLab Premium or higher. - GitHub rule **Require signed commits** for the project's default branch is mapped to the **Reject unsigned commits** GitLab push rule. Requires GitLab Premium or higher.
[Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/370949) in GitLab 15.5. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/370949) in GitLab 15.5.
- GitHub rule **Allow force pushes - Everyone** is mapped to the [**Allowed to force push** branch protection rule](../protected_branches.md#allow-force-push-on-a-protected-branch). [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/370943) in GitLab 15.6.
- GitHub rule **Allow force pushes - Specify who can force push** is proposed in issue [370945](https://gitlab.com/gitlab-org/gitlab/-/issues/370945).
- Support for GitHub rule **Require status checks to pass before merging** was proposed in issue [370948](https://gitlab.com/gitlab-org/gitlab/-/issues/370948). However, this rule cannot be translated during project import into GitLab due to technical difficulties. - Support for GitHub rule **Require status checks to pass before merging** was proposed in issue [370948](https://gitlab.com/gitlab-org/gitlab/-/issues/370948). However, this rule cannot be translated during project import into GitLab due to technical difficulties.
You can still create [status checks](../merge_requests/status_checks.md) in GitLab yourself. You can still create [status checks](../merge_requests/status_checks.md) in GitLab yourself.

View File

@ -43,10 +43,14 @@ module Gitlab
end end
def allow_force_push? def allow_force_push?
if ProtectedBranch.protected?(project, protected_branch.id) return false unless protected_branch.allow_force_pushes
ProtectedBranch.allow_force_push?(project, protected_branch.id) && protected_branch.allow_force_pushes
if protected_on_gitlab?
ProtectedBranch.allow_force_push?(project, protected_branch.id)
elsif default_branch?
!default_branch_protection.any?
else else
protected_branch.allow_force_pushes true
end end
end end

View File

@ -248,21 +248,11 @@ msgid_plural "%d epics"
msgstr[0] "" msgstr[0] ""
msgstr[1] "" msgstr[1] ""
msgid "%d error"
msgid_plural "%d errors"
msgstr[0] ""
msgstr[1] ""
msgid "%d exporter" msgid "%d exporter"
msgid_plural "%d exporters" msgid_plural "%d exporters"
msgstr[0] "" msgstr[0] ""
msgstr[1] "" msgstr[1] ""
msgid "%d failed"
msgid_plural "%d failed"
msgstr[0] ""
msgstr[1] ""
msgid "%d failed security job" msgid "%d failed security job"
msgid_plural "%d failed security jobs" msgid_plural "%d failed security jobs"
msgstr[0] "" msgstr[0] ""
@ -273,11 +263,6 @@ msgid_plural "%d files"
msgstr[0] "" msgstr[0] ""
msgstr[1] "" msgstr[1] ""
msgid "%d fixed test result"
msgid_plural "%d fixed test results"
msgstr[0] ""
msgstr[1] ""
msgid "%d fork" msgid "%d fork"
msgid_plural "%d forks" msgid_plural "%d forks"
msgstr[0] "" msgstr[0] ""
@ -857,12 +842,6 @@ msgstr ""
msgid "%{name} (Busy)" msgid "%{name} (Busy)"
msgstr "" msgstr ""
msgid "%{name} contained %{resultsString}"
msgstr ""
msgid "%{name} found %{resultsString}"
msgstr ""
msgid "%{name} is already being used for another emoji" msgid "%{name} is already being used for another emoji"
msgstr "" msgstr ""
@ -1193,9 +1172,6 @@ msgstr ""
msgid "%{value} is not included in the list" msgid "%{value} is not included in the list"
msgstr "" msgstr ""
msgid "%{value} s"
msgstr ""
msgid "%{verb} %{time_spent_value} spent time." msgid "%{verb} %{time_spent_value} spent time."
msgstr "" msgstr ""
@ -34159,18 +34135,12 @@ msgstr ""
msgid "Reports|Base report parsing error:" msgid "Reports|Base report parsing error:"
msgstr "" msgstr ""
msgid "Reports|Classname"
msgstr ""
msgid "Reports|Copy failed test names to run locally" msgid "Reports|Copy failed test names to run locally"
msgstr "" msgstr ""
msgid "Reports|Copy failed tests" msgid "Reports|Copy failed tests"
msgstr "" msgstr ""
msgid "Reports|Execution time"
msgstr ""
msgid "Reports|Failed %{count} time in %{baseBranch} in the last 14 days" msgid "Reports|Failed %{count} time in %{baseBranch} in the last 14 days"
msgid_plural "Reports|Failed %{count} times in %{baseBranch} in the last 14 days" msgid_plural "Reports|Failed %{count} times in %{baseBranch} in the last 14 days"
msgstr[0] "" msgstr[0] ""
@ -34181,12 +34151,6 @@ msgid_plural "Reports|Failed %{count} times in %{base_branch} in the last 14 day
msgstr[0] "" msgstr[0] ""
msgstr[1] "" msgstr[1] ""
msgid "Reports|Failure"
msgstr ""
msgid "Reports|Filename"
msgstr ""
msgid "Reports|Fixed" msgid "Reports|Fixed"
msgstr "" msgstr ""
@ -34229,21 +34193,12 @@ msgstr ""
msgid "Reports|Severity" msgid "Reports|Severity"
msgstr "" msgstr ""
msgid "Reports|System output"
msgstr ""
msgid "Reports|Test summary" msgid "Reports|Test summary"
msgstr "" msgstr ""
msgid "Reports|Test summary failed loading results"
msgstr ""
msgid "Reports|Test summary failed to load results" msgid "Reports|Test summary failed to load results"
msgstr "" msgstr ""
msgid "Reports|Test summary results are being parsed"
msgstr ""
msgid "Reports|Test summary results are loading" msgid "Reports|Test summary results are loading"
msgstr "" msgstr ""
@ -34259,9 +34214,6 @@ msgstr ""
msgid "Reports|metrics report" msgid "Reports|metrics report"
msgstr "" msgstr ""
msgid "Reports|no changed test results"
msgstr ""
msgid "Repositories" msgid "Repositories"
msgstr "" msgstr ""
@ -48971,11 +48923,6 @@ msgstr ""
msgid "organizations can only be added to root groups" msgid "organizations can only be added to root groups"
msgstr "" msgstr ""
msgid "out of %d total test"
msgid_plural "out of %d total tests"
msgstr[0] ""
msgstr[1] ""
msgid "packages" msgid "packages"
msgstr "" msgstr ""

View File

@ -14,15 +14,13 @@ RSpec.describe 'Merge request > User sees merge widget', :js do
let(:merge_request_in_only_mwps_project) { create(:merge_request, source_project: project_only_mwps) } let(:merge_request_in_only_mwps_project) { create(:merge_request, source_project: project_only_mwps) }
def click_expand_button def click_expand_button
find('[data-testid="report-section-expand-button"]').click find('[data-testid="toggle-button"]').click
end end
before do before do
project.add_maintainer(user) project.add_maintainer(user)
project_only_mwps.add_maintainer(user) project_only_mwps.add_maintainer(user)
sign_in(user) sign_in(user)
stub_feature_flags(refactor_mr_widget_test_summary: false)
end end
context 'new merge request', :sidekiq_might_not_need_inline do context 'new merge request', :sidekiq_might_not_need_inline do
@ -530,7 +528,7 @@ RSpec.describe 'Merge request > User sees merge widget', :js do
end end
it 'shows parsing status' do it 'shows parsing status' do
expect(page).to have_content('Test summary results are being parsed') expect(page).to have_content('Test summary results are loading')
end end
end end
@ -545,7 +543,7 @@ RSpec.describe 'Merge request > User sees merge widget', :js do
end end
it 'shows parsed results' do it 'shows parsed results' do
expect(page).to have_content('Test summary contained') expect(page).to have_content('Test summary:')
end end
end end
@ -559,7 +557,7 @@ RSpec.describe 'Merge request > User sees merge widget', :js do
end end
it 'shows the error state' do it 'shows the error state' do
expect(page).to have_content('Test summary failed loading results') expect(page).to have_content('Test summary failed to load results')
end end
end end
@ -606,13 +604,13 @@ RSpec.describe 'Merge request > User sees merge widget', :js do
end end
it 'shows test reports summary which includes the new failure' do it 'shows test reports summary which includes the new failure' do
within(".js-reports-container") do within('[data-testid="widget-extension"]') do
click_expand_button click_expand_button
expect(page).to have_content('Test summary contained 1 failed out of 2 total tests') expect(page).to have_content('Test summary: 1 failed, 2 total tests')
within(".js-report-section-container") do within('[data-testid="widget-extension-collapsed-section"]') do
expect(page).to have_content('rspec found no changed test results out of 1 total test') expect(page).to have_content('rspec: no changed test results, 1 total test')
expect(page).to have_content('junit found 1 failed out of 1 total test') expect(page).to have_content('junit: 1 failed, 1 total test')
expect(page).to have_content('New') expect(page).to have_content('New')
expect(page).to have_content('addTest') expect(page).to have_content('addTest')
end end
@ -621,15 +619,15 @@ RSpec.describe 'Merge request > User sees merge widget', :js do
context 'when user clicks the new failure' do context 'when user clicks the new failure' do
it 'shows the test report detail' do it 'shows the test report detail' do
within(".js-reports-container") do within('[data-testid="widget-extension"]') do
click_expand_button click_expand_button
within(".js-report-section-container") do within('[data-testid="widget-extension-collapsed-section"]') do
click_button 'addTest' click_link 'addTest'
end end
end end
within("#modal-mrwidget-reports") do within('[data-testid="test-case-details-modal"]') do
expect(page).to have_content('addTest') expect(page).to have_content('addTest')
expect(page).to have_content('6.66') expect(page).to have_content('6.66')
expect(page).to have_content(sample_java_failed_message.gsub(/\s+/, ' ').strip) expect(page).to have_content(sample_java_failed_message.gsub(/\s+/, ' ').strip)
@ -654,13 +652,13 @@ RSpec.describe 'Merge request > User sees merge widget', :js do
end end
it 'shows test reports summary which includes the existing failure' do it 'shows test reports summary which includes the existing failure' do
within(".js-reports-container") do within('[data-testid="widget-extension"]') do
click_expand_button click_expand_button
expect(page).to have_content('Test summary contained 1 failed out of 2 total tests') expect(page).to have_content('Test summary: 1 failed, 2 total tests')
within(".js-report-section-container") do within('[data-testid="widget-extension-collapsed-section"]') do
expect(page).to have_content('rspec found 1 failed out of 1 total test') expect(page).to have_content('rspec: 1 failed, 1 total test')
expect(page).to have_content('junit found no changed test results out of 1 total test') expect(page).to have_content('junit: no changed test results, 1 total test')
expect(page).to have_content('Test#sum when a is 1 and b is 3 returns summary') expect(page).to have_content('Test#sum when a is 1 and b is 3 returns summary')
end end
end end
@ -668,15 +666,15 @@ RSpec.describe 'Merge request > User sees merge widget', :js do
context 'when user clicks the existing failure' do context 'when user clicks the existing failure' do
it 'shows test report detail of it' do it 'shows test report detail of it' do
within(".js-reports-container") do within('[data-testid="widget-extension"]') do
click_expand_button click_expand_button
within(".js-report-section-container") do within('[data-testid="widget-extension-collapsed-section"]') do
click_button 'Test#sum when a is 1 and b is 3 returns summary' click_link 'Test#sum when a is 1 and b is 3 returns summary'
end end
end end
within("#modal-mrwidget-reports") do within('[data-testid="test-case-details-modal"]') do
expect(page).to have_content('Test#sum when a is 1 and b is 3 returns summary') expect(page).to have_content('Test#sum when a is 1 and b is 3 returns summary')
expect(page).to have_content('2.22') expect(page).to have_content('2.22')
expect(page).to have_content(sample_rspec_failed_message.gsub(/\s+/, ' ').strip) expect(page).to have_content(sample_rspec_failed_message.gsub(/\s+/, ' ').strip)
@ -701,13 +699,14 @@ RSpec.describe 'Merge request > User sees merge widget', :js do
end end
it 'shows test reports summary which includes the resolved failure' do it 'shows test reports summary which includes the resolved failure' do
within(".js-reports-container") do within('[data-testid="widget-extension"]') do
click_expand_button click_expand_button
expect(page).to have_content('Test summary contained 1 fixed test result out of 2 total tests') expect(page).to have_content('Test summary: 1 fixed test result, 2 total tests')
within(".js-report-section-container") do within('[data-testid="widget-extension-collapsed-section"]') do
expect(page).to have_content('rspec found no changed test results out of 1 total test') expect(page).to have_content('rspec: no changed test results, 1 total test')
expect(page).to have_content('junit found 1 fixed test result out of 1 total test') expect(page).to have_content('junit: 1 fixed test result, 1 total test')
expect(page).to have_content('Fixed')
expect(page).to have_content('addTest') expect(page).to have_content('addTest')
end end
end end
@ -715,15 +714,15 @@ RSpec.describe 'Merge request > User sees merge widget', :js do
context 'when user clicks the resolved failure' do context 'when user clicks the resolved failure' do
it 'shows test report detail of it' do it 'shows test report detail of it' do
within(".js-reports-container") do within('[data-testid="widget-extension"]') do
click_expand_button click_expand_button
within(".js-report-section-container") do within('[data-testid="widget-extension-collapsed-section"]') do
click_button 'addTest' click_link 'addTest'
end end
end end
within("#modal-mrwidget-reports") do within('[data-testid="test-case-details-modal"]') do
expect(page).to have_content('addTest') expect(page).to have_content('addTest')
expect(page).to have_content('5.55') expect(page).to have_content('5.55')
end end
@ -747,13 +746,13 @@ RSpec.describe 'Merge request > User sees merge widget', :js do
end end
it 'shows test reports summary which includes the new error' do it 'shows test reports summary which includes the new error' do
within(".js-reports-container") do within('[data-testid="widget-extension"]') do
click_expand_button click_expand_button
expect(page).to have_content('Test summary contained 1 error out of 2 total tests') expect(page).to have_content('Test summary: 1 error, 2 total tests')
within(".js-report-section-container") do within('[data-testid="widget-extension-collapsed-section"]') do
expect(page).to have_content('rspec found no changed test results out of 1 total test') expect(page).to have_content('rspec: no changed test results, 1 total test')
expect(page).to have_content('junit found 1 error out of 1 total test') expect(page).to have_content('junit: 1 error, 1 total test')
expect(page).to have_content('New') expect(page).to have_content('New')
expect(page).to have_content('addTest') expect(page).to have_content('addTest')
end end
@ -762,15 +761,15 @@ RSpec.describe 'Merge request > User sees merge widget', :js do
context 'when user clicks the new error' do context 'when user clicks the new error' do
it 'shows the test report detail' do it 'shows the test report detail' do
within(".js-reports-container") do within('[data-testid="widget-extension"]') do
click_expand_button click_expand_button
within(".js-report-section-container") do within('[data-testid="widget-extension-collapsed-section"]') do
click_button 'addTest' click_link 'addTest'
end end
end end
within("#modal-mrwidget-reports") do within('[data-testid="test-case-details-modal"]') do
expect(page).to have_content('addTest') expect(page).to have_content('addTest')
expect(page).to have_content('8.88') expect(page).to have_content('8.88')
end end
@ -794,13 +793,13 @@ RSpec.describe 'Merge request > User sees merge widget', :js do
end end
it 'shows test reports summary which includes the existing error' do it 'shows test reports summary which includes the existing error' do
within(".js-reports-container") do within('[data-testid="widget-extension"]') do
click_expand_button click_expand_button
expect(page).to have_content('Test summary contained 1 error out of 2 total tests') expect(page).to have_content('Test summary: 1 error, 2 total tests')
within(".js-report-section-container") do within('[data-testid="widget-extension-collapsed-section"]') do
expect(page).to have_content('rspec found 1 error out of 1 total test') expect(page).to have_content('rspec: 1 error, 1 total test')
expect(page).to have_content('junit found no changed test results out of 1 total test') expect(page).to have_content('junit: no changed test results, 1 total test')
expect(page).to have_content('Test#sum when a is 4 and b is 4 returns summary') expect(page).to have_content('Test#sum when a is 4 and b is 4 returns summary')
end end
end end
@ -808,15 +807,15 @@ RSpec.describe 'Merge request > User sees merge widget', :js do
context 'when user clicks the existing error' do context 'when user clicks the existing error' do
it 'shows test report detail of it' do it 'shows test report detail of it' do
within(".js-reports-container") do within('[data-testid="widget-extension"]') do
click_expand_button click_expand_button
within(".js-report-section-container") do within('[data-testid="widget-extension-collapsed-section"]') do
click_button 'Test#sum when a is 4 and b is 4 returns summary' click_link 'Test#sum when a is 4 and b is 4 returns summary'
end end
end end
within("#modal-mrwidget-reports") do within('[data-testid="test-case-details-modal"]') do
expect(page).to have_content('Test#sum when a is 4 and b is 4 returns summary') expect(page).to have_content('Test#sum when a is 4 and b is 4 returns summary')
expect(page).to have_content('4.44') expect(page).to have_content('4.44')
end end
@ -840,13 +839,14 @@ RSpec.describe 'Merge request > User sees merge widget', :js do
end end
it 'shows test reports summary which includes the resolved error' do it 'shows test reports summary which includes the resolved error' do
within(".js-reports-container") do within('[data-testid="widget-extension"]') do
click_expand_button click_expand_button
expect(page).to have_content('Test summary contained 1 fixed test result out of 2 total tests') expect(page).to have_content('Test summary: 1 fixed test result, 2 total tests')
within(".js-report-section-container") do within('[data-testid="widget-extension-collapsed-section"]') do
expect(page).to have_content('rspec found no changed test results out of 1 total test') expect(page).to have_content('rspec: no changed test results, 1 total test')
expect(page).to have_content('junit found 1 fixed test result out of 1 total test') expect(page).to have_content('junit: 1 fixed test result, 1 total test')
expect(page).to have_content('Fixed')
expect(page).to have_content('addTest') expect(page).to have_content('addTest')
end end
end end
@ -854,15 +854,15 @@ RSpec.describe 'Merge request > User sees merge widget', :js do
context 'when user clicks the resolved error' do context 'when user clicks the resolved error' do
it 'shows test report detail of it' do it 'shows test report detail of it' do
within(".js-reports-container") do within('[data-testid="widget-extension"]') do
click_expand_button click_expand_button
within(".js-report-section-container") do within('[data-testid="widget-extension-collapsed-section"]') do
click_button 'addTest' click_link 'addTest'
end end
end end
within("#modal-mrwidget-reports") do within('[data-testid="test-case-details-modal"]') do
expect(page).to have_content('addTest') expect(page).to have_content('addTest')
expect(page).to have_content('5.55') expect(page).to have_content('5.55')
end end
@ -894,13 +894,13 @@ RSpec.describe 'Merge request > User sees merge widget', :js do
end end
it 'shows test reports summary which includes the resolved failure' do it 'shows test reports summary which includes the resolved failure' do
within(".js-reports-container") do within('[data-testid="widget-extension"]') do
click_expand_button click_expand_button
expect(page).to have_content('Test summary contained 20 failed out of 20 total tests') expect(page).to have_content('Test summary: 20 failed, 20 total tests')
within(".js-report-section-container") do within('[data-testid="widget-extension-collapsed-section"]') do
expect(page).to have_content('rspec found 10 failed out of 10 total tests') expect(page).to have_content('rspec: 10 failed, 10 total tests')
expect(page).to have_content('junit found 10 failed out of 10 total tests') expect(page).to have_content('junit: 10 failed, 10 total tests')
expect(page).to have_content('Test#sum when a is 1 and b is 3 returns summary', count: 2) expect(page).to have_content('Test#sum when a is 1 and b is 3 returns summary', count: 2)
end end

View File

@ -13,7 +13,7 @@ Object {
exports[`Grouped Issues List with data renders a report item with the correct props 1`] = ` exports[`Grouped Issues List with data renders a report item with the correct props 1`] = `
Object { Object {
"component": "TestIssueBody", "component": "CodequalityIssueBody",
"iconComponent": "IssueStatusIcon", "iconComponent": "IssueStatusIcon",
"isNew": false, "isNew": false,
"issue": Object { "issue": Object {

View File

@ -74,7 +74,7 @@ describe('Grouped Issues List', () => {
createComponent({ createComponent({
propsData: { propsData: {
resolvedIssues: [{ name: 'foo' }], resolvedIssues: [{ name: 'foo' }],
component: 'TestIssueBody', component: 'CodequalityIssueBody',
}, },
stubs: { stubs: {
ReportItem, ReportItem,

View File

@ -10,7 +10,7 @@ describe('ReportItem', () => {
const wrapper = shallowMount(ReportItem, { const wrapper = shallowMount(ReportItem, {
propsData: { propsData: {
issue: { foo: 'bar' }, issue: { foo: 'bar' },
component: componentNames.TestIssueBody, component: componentNames.CodequalityIssueBody,
status: STATUS_SUCCESS, status: STATUS_SUCCESS,
showReportSectionStatusIcon: false, showReportSectionStatusIcon: false,
}, },
@ -23,7 +23,7 @@ describe('ReportItem', () => {
const wrapper = shallowMount(ReportItem, { const wrapper = shallowMount(ReportItem, {
propsData: { propsData: {
issue: { foo: 'bar' }, issue: { foo: 'bar' },
component: componentNames.TestIssueBody, component: componentNames.CodequalityIssueBody,
status: STATUS_SUCCESS, status: STATUS_SUCCESS,
}, },
}); });

View File

@ -1,68 +0,0 @@
import { GlLink, GlSprintf } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import ReportsModal from '~/reports/grouped_test_report/components/modal.vue';
import state from '~/reports/grouped_test_report/store/state';
import CodeBlock from '~/vue_shared/components/code_block.vue';
const StubbedGlModal = { template: '<div><slot></slot></div>', name: 'GlModal', props: ['title'] };
describe('Grouped Test Reports Modal', () => {
const modalDataStructure = state().modal.data;
const title = 'Test#sum when a is 1 and b is 2 returns summary';
// populate data
modalDataStructure.execution_time.value = 0.009411;
modalDataStructure.system_output.value = 'Failure/Error: is_expected.to eq(3)\n\n';
modalDataStructure.filename.value = {
text: 'link',
path: '/file/path',
};
let wrapper;
beforeEach(() => {
wrapper = extendedWrapper(
shallowMount(ReportsModal, {
propsData: {
title,
modalData: modalDataStructure,
visible: true,
},
stubs: { GlModal: StubbedGlModal, GlSprintf },
}),
);
});
afterEach(() => {
wrapper.destroy();
});
it('renders code block', () => {
expect(wrapper.findComponent(CodeBlock).props().code).toEqual(
modalDataStructure.system_output.value,
);
});
it('renders link', () => {
const link = wrapper.findComponent(GlLink);
expect(link.attributes().href).toEqual(modalDataStructure.filename.value.path);
expect(link.text()).toEqual(modalDataStructure.filename.value.text);
});
it('renders seconds', () => {
expect(wrapper.text()).toContain(`${modalDataStructure.execution_time.value} s`);
});
it('render title', () => {
expect(wrapper.findComponent(StubbedGlModal).props().title).toEqual(title);
});
it('re-emits hide event', () => {
wrapper.findComponent(StubbedGlModal).vm.$emit('hide');
expect(wrapper.emitted().hide).toEqual([[]]);
});
});

View File

@ -1,96 +0,0 @@
import { GlBadge, GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import Vuex from 'vuex';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import IssueStatusIcon from '~/reports/components/issue_status_icon.vue';
import TestIssueBody from '~/reports/grouped_test_report/components/test_issue_body.vue';
import { failedIssue, successIssue } from '../../mock_data/mock_data';
Vue.use(Vuex);
describe('Test issue body', () => {
let wrapper;
let store;
const findDescription = () => wrapper.findByTestId('test-issue-body-description');
const findStatusIcon = () => wrapper.findComponent(IssueStatusIcon);
const findBadge = () => wrapper.findComponent(GlBadge);
const actionSpies = {
openModal: jest.fn(),
};
const createComponent = ({ issue = failedIssue } = {}) => {
store = new Vuex.Store({
actions: actionSpies,
});
wrapper = extendedWrapper(
shallowMount(TestIssueBody, {
store,
propsData: {
issue,
},
stubs: {
GlBadge,
GlButton,
IssueStatusIcon,
},
}),
);
};
afterEach(() => {
wrapper.destroy();
});
describe('when issue has failed status', () => {
beforeEach(() => {
createComponent();
});
it('renders issue name', () => {
expect(findDescription().text()).toContain(failedIssue.name);
});
it('renders failed status icon', () => {
expect(findStatusIcon().props('status')).toBe('failed');
});
describe('when issue has recent failures', () => {
it('renders recent failures badge', () => {
expect(findBadge().exists()).toBe(true);
});
});
});
describe('when issue has success status', () => {
beforeEach(() => {
createComponent({ issue: successIssue });
});
it('does not render recent failures', () => {
expect(findBadge().exists()).toBe(false);
});
it('renders issue name', () => {
expect(findDescription().text()).toBe(successIssue.name);
});
it('renders success status icon', () => {
expect(findStatusIcon().props('status')).toBe('success');
});
});
describe('when clicking on an issue', () => {
it('calls openModal action', () => {
createComponent();
wrapper.findComponent(GlButton).trigger('click');
expect(actionSpies.openModal).toHaveBeenCalledWith(expect.any(Object), {
issue: failedIssue,
});
});
});
});

View File

@ -1,355 +0,0 @@
import { mount } from '@vue/test-utils';
import Vue from 'vue';
import Vuex from 'vuex';
import Api from '~/api';
import GroupedTestReportsApp from '~/reports/grouped_test_report/grouped_test_reports_app.vue';
import { getStoreConfig } from '~/reports/grouped_test_report/store';
import { failedReport } from '../mock_data/mock_data';
import mixedResultsTestReports from '../mock_data/new_and_fixed_failures_report.json';
import newErrorsTestReports from '../mock_data/new_errors_report.json';
import newFailedTestReports from '../mock_data/new_failures_report.json';
import successTestReports from '../mock_data/no_failures_report.json';
import recentFailuresTestReports from '../mock_data/recent_failures_report.json';
import resolvedFailures from '../mock_data/resolved_failures.json';
jest.mock('~/api.js');
Vue.use(Vuex);
describe('Grouped test reports app', () => {
const endpoint = 'endpoint.json';
const headBlobPath = '/blob/path';
const pipelinePath = '/path/to/pipeline';
let wrapper;
let mockStore;
const mountComponent = ({ props = { pipelinePath } } = {}) => {
wrapper = mount(GroupedTestReportsApp, {
store: mockStore,
propsData: {
endpoint,
headBlobPath,
pipelinePath,
...props,
},
});
};
const setReports = (reports) => {
mockStore.state.status = reports.status;
mockStore.state.summary = reports.summary;
mockStore.state.reports = reports.suites;
};
const findHeader = () => wrapper.find('[data-testid="report-section-code-text"]');
const findExpandButton = () => wrapper.find('[data-testid="report-section-expand-button"]');
const findFullTestReportLink = () => wrapper.find('[data-testid="group-test-reports-full-link"]');
const findSummaryDescription = () => wrapper.find('[data-testid="summary-row-description"]');
const findIssueListUnresolvedHeading = () => wrapper.find('[data-testid="unresolvedHeading"]');
const findIssueListResolvedHeading = () => wrapper.find('[data-testid="resolvedHeading"]');
const findIssueDescription = () => wrapper.find('[data-testid="test-issue-body-description"]');
const findIssueRecentFailures = () =>
wrapper.find('[data-testid="test-issue-body-recent-failures"]');
const findAllIssueDescriptions = () =>
wrapper.findAll('[data-testid="test-issue-body-description"]');
beforeEach(() => {
mockStore = new Vuex.Store({
...getStoreConfig(),
actions: {
fetchReports: () => {},
setPaths: () => {},
},
});
mountComponent();
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
describe('with success result', () => {
beforeEach(() => {
setReports(successTestReports);
mountComponent();
});
it('renders success summary text', () => {
expect(findHeader().text()).toBe(
'Test summary contained no changed test results out of 11 total tests',
);
});
});
describe('`View full report` button', () => {
it('should render the full test report link', () => {
const fullTestReportLink = findFullTestReportLink();
expect(fullTestReportLink.exists()).toBe(true);
expect(pipelinePath).not.toBe('');
expect(fullTestReportLink.attributes('href')).toBe(`${pipelinePath}/test_report`);
});
describe('Without a pipelinePath', () => {
beforeEach(() => {
mountComponent({
props: { pipelinePath: '' },
});
});
it('should not render the full test report link', () => {
expect(findFullTestReportLink().exists()).toBe(false);
});
});
});
describe('`Expand` button', () => {
beforeEach(() => {
setReports(newFailedTestReports);
});
it('tracks service ping metric', () => {
mountComponent();
findExpandButton().trigger('click');
expect(Api.trackRedisHllUserEvent).toHaveBeenCalledTimes(1);
expect(Api.trackRedisHllUserEvent).toHaveBeenCalledWith(wrapper.vm.$options.expandEvent);
});
it('only tracks the first expansion', () => {
mountComponent();
const expandButton = findExpandButton();
expandButton.trigger('click');
expandButton.trigger('click');
expandButton.trigger('click');
expect(Api.trackRedisHllUserEvent).toHaveBeenCalledTimes(1);
});
});
describe('with new failed result', () => {
beforeEach(() => {
setReports(newFailedTestReports);
mountComponent();
});
it('renders New heading', () => {
expect(findIssueListUnresolvedHeading().text()).toBe('New');
});
it('renders failed summary text', () => {
expect(findHeader().text()).toBe('Test summary contained 2 failed out of 11 total tests');
});
it('renders failed test suite', () => {
expect(findSummaryDescription().text()).toContain(
'rspec:pg found 2 failed out of 8 total tests',
);
});
it('renders failed issue in list', () => {
expect(findIssueDescription().text()).toContain(
'Test#sum when a is 1 and b is 2 returns summary',
);
});
});
describe('with new error result', () => {
beforeEach(() => {
setReports(newErrorsTestReports);
mountComponent();
});
it('renders New heading', () => {
expect(findIssueListUnresolvedHeading().text()).toBe('New');
});
it('renders error summary text', () => {
expect(findHeader().text()).toBe('Test summary contained 2 errors out of 11 total tests');
});
it('renders error test suite', () => {
expect(findSummaryDescription().text()).toContain(
'karma found 2 errors out of 3 total tests',
);
});
it('renders error issue in list', () => {
expect(findIssueDescription().text()).toContain(
'Test#sum when a is 1 and b is 2 returns summary',
);
});
});
describe('with mixed results', () => {
beforeEach(() => {
setReports(mixedResultsTestReports);
mountComponent();
});
it('renders New and Fixed headings', () => {
expect(findIssueListUnresolvedHeading().text()).toBe('New');
expect(findIssueListResolvedHeading().text()).toBe('Fixed');
});
it('renders summary text', () => {
expect(findHeader().text()).toBe(
'Test summary contained 2 failed and 2 fixed test results out of 11 total tests',
);
});
it('renders failed test suite', () => {
expect(findSummaryDescription().text()).toContain(
'rspec:pg found 1 failed and 2 fixed test results out of 8 total tests',
);
});
it('renders failed issue in list', () => {
expect(findIssueDescription().text()).toContain(
'Test#subtract when a is 2 and b is 1 returns correct result',
);
});
});
describe('with resolved failures and resolved errors', () => {
beforeEach(() => {
setReports(resolvedFailures);
mountComponent();
});
it('renders Fixed heading', () => {
expect(findIssueListResolvedHeading().text()).toBe('Fixed');
});
it('renders summary text', () => {
expect(findHeader().text()).toBe(
'Test summary contained 4 fixed test results out of 11 total tests',
);
});
it('renders resolved test suite', () => {
expect(findSummaryDescription().text()).toContain(
'rspec:pg found 4 fixed test results out of 8 total tests',
);
});
it('renders resolved failures', () => {
expect(findIssueDescription().text()).toContain(
resolvedFailures.suites[0].resolved_failures[0].name,
);
});
it('renders resolved errors', () => {
expect(findAllIssueDescriptions().at(2).text()).toContain(
resolvedFailures.suites[0].resolved_errors[0].name,
);
});
});
describe('recent failures counts', () => {
describe('with recent failures counts', () => {
beforeEach(() => {
setReports(recentFailuresTestReports);
mountComponent();
});
it('renders the recently failed tests summary', () => {
expect(findHeader().text()).toContain(
'2 out of 3 failed tests have failed more than once in the last 14 days',
);
});
it('renders the recently failed count on the test suite', () => {
expect(findSummaryDescription().text()).toContain(
'1 out of 2 failed tests has failed more than once in the last 14 days',
);
});
it('renders the recent failures count on the test case', () => {
expect(findIssueRecentFailures().text()).toBe('Failed 8 times in main in the last 14 days');
});
});
describe('without recent failures counts', () => {
beforeEach(() => {
setReports(mixedResultsTestReports);
mountComponent();
});
it('does not render the recently failed tests summary', () => {
expect(findHeader().text()).not.toContain('failed more than once in the last 14 days');
});
it('does not render the recently failed count on the test suite', () => {
expect(findSummaryDescription().text()).not.toContain(
'failed more than once in the last 14 days',
);
});
it('does not render the recent failures count on the test case', () => {
expect(findIssueDescription().text()).not.toContain('in the last 14 days');
});
});
});
describe('with a report that failed to load', () => {
beforeEach(() => {
setReports(failedReport);
mountComponent();
});
it('renders an error status for the report', () => {
const { name } = failedReport.suites[0];
expect(findSummaryDescription().text()).toContain(
`An error occurred while loading ${name} result`,
);
});
});
describe('with a report parsing errors', () => {
beforeEach(() => {
const reports = failedReport;
reports.suites[0].suite_errors = {
head: 'JUnit XML parsing failed: 2:24: FATAL: attributes construct error',
base: 'JUnit data parsing failed: string not matched',
};
setReports(reports);
mountComponent();
});
it('renders the error messages', () => {
expect(findSummaryDescription().text()).toContain(
'JUnit XML parsing failed: 2:24: FATAL: attributes construct error',
);
expect(findSummaryDescription().text()).toContain(
'JUnit data parsing failed: string not matched',
);
});
});
describe('with error', () => {
beforeEach(() => {
mockStore.state.isLoading = false;
mockStore.state.hasError = true;
mountComponent();
});
it('renders loading state', () => {
expect(findHeader().text()).toBe('Test summary failed loading results');
});
});
describe('while loading', () => {
beforeEach(() => {
mockStore.state.isLoading = true;
mountComponent();
});
it('renders loading state', () => {
expect(findHeader().text()).toBe('Test summary results are being parsed');
});
});
});

View File

@ -1,168 +0,0 @@
import MockAdapter from 'axios-mock-adapter';
import { TEST_HOST } from 'helpers/test_constants';
import testAction from 'helpers/vuex_action_helper';
import axios from '~/lib/utils/axios_utils';
import {
setPaths,
requestReports,
fetchReports,
stopPolling,
clearEtagPoll,
receiveReportsSuccess,
receiveReportsError,
openModal,
closeModal,
} from '~/reports/grouped_test_report/store/actions';
import * as types from '~/reports/grouped_test_report/store/mutation_types';
import state from '~/reports/grouped_test_report/store/state';
describe('Reports Store Actions', () => {
let mockedState;
beforeEach(() => {
mockedState = state();
});
describe('setPaths', () => {
it('should commit SET_PATHS mutation', () => {
return testAction(
setPaths,
{ endpoint: 'endpoint.json', headBlobPath: '/blob/path' },
mockedState,
[
{
type: types.SET_PATHS,
payload: { endpoint: 'endpoint.json', headBlobPath: '/blob/path' },
},
],
[],
);
});
});
describe('requestReports', () => {
it('should commit REQUEST_REPORTS mutation', () => {
return testAction(requestReports, null, mockedState, [{ type: types.REQUEST_REPORTS }], []);
});
});
describe('fetchReports', () => {
let mock;
beforeEach(() => {
mockedState.endpoint = `${TEST_HOST}/endpoint.json`;
mock = new MockAdapter(axios);
});
afterEach(() => {
mock.restore();
stopPolling();
clearEtagPoll();
});
describe('success', () => {
it('dispatches requestReports and receiveReportsSuccess', () => {
mock
.onGet(`${TEST_HOST}/endpoint.json`)
.replyOnce(200, { summary: {}, suites: [{ name: 'rspec' }] });
return testAction(
fetchReports,
null,
mockedState,
[],
[
{
type: 'requestReports',
},
{
payload: { data: { summary: {}, suites: [{ name: 'rspec' }] }, status: 200 },
type: 'receiveReportsSuccess',
},
],
);
});
});
describe('error', () => {
beforeEach(() => {
mock.onGet(`${TEST_HOST}/endpoint.json`).reply(500);
});
it('dispatches requestReports and receiveReportsError', () => {
return testAction(
fetchReports,
null,
mockedState,
[],
[
{
type: 'requestReports',
},
{
type: 'receiveReportsError',
},
],
);
});
});
});
describe('receiveReportsSuccess', () => {
it('should commit RECEIVE_REPORTS_SUCCESS mutation with 200', () => {
return testAction(
receiveReportsSuccess,
{ data: { summary: {} }, status: 200 },
mockedState,
[{ type: types.RECEIVE_REPORTS_SUCCESS, payload: { summary: {} } }],
[],
);
});
it('should not commit RECEIVE_REPORTS_SUCCESS mutation with 204', () => {
return testAction(
receiveReportsSuccess,
{ data: { summary: {} }, status: 204 },
mockedState,
[],
[],
);
});
});
describe('receiveReportsError', () => {
it('should commit RECEIVE_REPORTS_ERROR mutation', () => {
return testAction(
receiveReportsError,
null,
mockedState,
[{ type: types.RECEIVE_REPORTS_ERROR }],
[],
);
});
});
describe('openModal', () => {
it('should commit SET_ISSUE_MODAL_DATA', () => {
return testAction(
openModal,
{ name: 'foo' },
mockedState,
[{ type: types.SET_ISSUE_MODAL_DATA, payload: { name: 'foo' } }],
[],
);
});
});
describe('closeModal', () => {
it('should commit RESET_ISSUE_MODAL_DATA', () => {
return testAction(
closeModal,
{},
mockedState,
[{ type: types.RESET_ISSUE_MODAL_DATA, payload: {} }],
[],
);
});
});
});

View File

@ -1,162 +0,0 @@
import * as types from '~/reports/grouped_test_report/store/mutation_types';
import mutations from '~/reports/grouped_test_report/store/mutations';
import state from '~/reports/grouped_test_report/store/state';
import { failedIssue } from '../../mock_data/mock_data';
describe('Reports Store Mutations', () => {
let stateCopy;
beforeEach(() => {
stateCopy = state();
});
describe('SET_PATHS', () => {
it('should set endpoint', () => {
mutations[types.SET_PATHS](stateCopy, {
endpoint: 'endpoint.json',
headBlobPath: '/blob/path',
});
expect(stateCopy.endpoint).toEqual('endpoint.json');
expect(stateCopy.headBlobPath).toEqual('/blob/path');
});
});
describe('REQUEST_REPORTS', () => {
it('should set isLoading to true', () => {
mutations[types.REQUEST_REPORTS](stateCopy);
expect(stateCopy.isLoading).toEqual(true);
});
});
describe('RECEIVE_REPORTS_SUCCESS', () => {
const mockedResponse = {
summary: {
total: 14,
resolved: 0,
failed: 7,
},
suites: [
{
name: 'build:linux',
summary: {
total: 2,
resolved: 0,
failed: 1,
},
new_failures: [
{
name: 'StringHelper#concatenate when a is git and b is lab returns summary',
execution_time: 0.0092435,
system_output: "Failure/Error: is_expected.to eq('gitlab')",
recent_failures: {
count: 4,
base_branch: 'main',
},
},
],
resolved_failures: [
{
name: 'StringHelper#concatenate when a is git and b is lab returns summary',
execution_time: 0.009235,
system_output: "Failure/Error: is_expected.to eq('gitlab')",
},
],
existing_failures: [
{
name: 'StringHelper#concatenate when a is git and b is lab returns summary',
execution_time: 1232.08,
system_output: "Failure/Error: is_expected.to eq('gitlab')",
},
],
},
],
};
beforeEach(() => {
mutations[types.RECEIVE_REPORTS_SUCCESS](stateCopy, mockedResponse);
});
it('should reset isLoading', () => {
expect(stateCopy.isLoading).toEqual(false);
});
it('should reset hasError', () => {
expect(stateCopy.hasError).toEqual(false);
});
it('should set summary counts', () => {
expect(stateCopy.summary.total).toEqual(mockedResponse.summary.total);
expect(stateCopy.summary.resolved).toEqual(mockedResponse.summary.resolved);
expect(stateCopy.summary.failed).toEqual(mockedResponse.summary.failed);
expect(stateCopy.summary.recentlyFailed).toEqual(1);
});
it('should set reports', () => {
expect(stateCopy.reports).toEqual(mockedResponse.suites);
});
});
describe('RECEIVE_REPORTS_ERROR', () => {
beforeEach(() => {
mutations[types.RECEIVE_REPORTS_ERROR](stateCopy);
});
it('should reset isLoading', () => {
expect(stateCopy.isLoading).toEqual(false);
});
it('should set hasError to true', () => {
expect(stateCopy.hasError).toEqual(true);
});
it('should reset reports', () => {
expect(stateCopy.reports).toEqual([]);
});
});
describe('SET_ISSUE_MODAL_DATA', () => {
beforeEach(() => {
mutations[types.SET_ISSUE_MODAL_DATA](stateCopy, {
issue: failedIssue,
});
});
it('should set modal title', () => {
expect(stateCopy.modal.title).toEqual(failedIssue.name);
});
it('should set modal data', () => {
expect(stateCopy.modal.data.execution_time.value).toEqual(failedIssue.execution_time);
expect(stateCopy.modal.data.system_output.value).toEqual(failedIssue.system_output);
});
it('should open modal', () => {
expect(stateCopy.modal.open).toEqual(true);
});
});
describe('RESET_ISSUE_MODAL_DATA', () => {
beforeEach(() => {
mutations[types.SET_ISSUE_MODAL_DATA](stateCopy, {
issue: failedIssue,
});
mutations[types.RESET_ISSUE_MODAL_DATA](stateCopy);
});
it('should reset modal title', () => {
expect(stateCopy.modal.title).toEqual(null);
});
it('should reset modal data', () => {
expect(stateCopy.modal.data.execution_time.value).toEqual(null);
expect(stateCopy.modal.data.system_output.value).toEqual(null);
});
it('should close modal', () => {
expect(stateCopy.modal.open).toEqual(false);
});
});
});

View File

@ -1,255 +0,0 @@
import {
STATUS_FAILED,
STATUS_SUCCESS,
ICON_WARNING,
ICON_SUCCESS,
ICON_NOTFOUND,
} from '~/reports/constants';
import * as utils from '~/reports/grouped_test_report/store/utils';
describe('Reports store utils', () => {
describe('summaryTextbuilder', () => {
it('should render text for no changed results in multiple tests', () => {
const name = 'Test summary';
const data = { total: 10 };
const result = utils.summaryTextBuilder(name, data);
expect(result).toBe('Test summary contained no changed test results out of 10 total tests');
});
it('should render text for no changed results in one test', () => {
const name = 'Test summary';
const data = { total: 1 };
const result = utils.summaryTextBuilder(name, data);
expect(result).toBe('Test summary contained no changed test results out of 1 total test');
});
it('should render text for multiple failed results', () => {
const name = 'Test summary';
const data = { failed: 3, total: 10 };
const result = utils.summaryTextBuilder(name, data);
expect(result).toBe('Test summary contained 3 failed out of 10 total tests');
});
it('should render text for multiple errored results', () => {
const name = 'Test summary';
const data = { errored: 7, total: 10 };
const result = utils.summaryTextBuilder(name, data);
expect(result).toBe('Test summary contained 7 errors out of 10 total tests');
});
it('should render text for multiple fixed results', () => {
const name = 'Test summary';
const data = { resolved: 4, total: 10 };
const result = utils.summaryTextBuilder(name, data);
expect(result).toBe('Test summary contained 4 fixed test results out of 10 total tests');
});
it('should render text for multiple fixed, and multiple failed results', () => {
const name = 'Test summary';
const data = { failed: 3, resolved: 4, total: 10 };
const result = utils.summaryTextBuilder(name, data);
expect(result).toBe(
'Test summary contained 3 failed and 4 fixed test results out of 10 total tests',
);
});
it('should render text for a singular fixed, and a singular failed result', () => {
const name = 'Test summary';
const data = { failed: 1, resolved: 1, total: 10 };
const result = utils.summaryTextBuilder(name, data);
expect(result).toBe(
'Test summary contained 1 failed and 1 fixed test result out of 10 total tests',
);
});
it('should render text for singular failed, errored, and fixed results', () => {
const name = 'Test summary';
const data = { failed: 1, errored: 1, resolved: 1, total: 10 };
const result = utils.summaryTextBuilder(name, data);
expect(result).toBe(
'Test summary contained 1 failed, 1 error and 1 fixed test result out of 10 total tests',
);
});
it('should render text for multiple failed, errored, and fixed results', () => {
const name = 'Test summary';
const data = { failed: 2, errored: 3, resolved: 4, total: 10 };
const result = utils.summaryTextBuilder(name, data);
expect(result).toBe(
'Test summary contained 2 failed, 3 errors and 4 fixed test results out of 10 total tests',
);
});
});
describe('reportTextBuilder', () => {
it('should render text for no changed results in multiple tests', () => {
const name = 'Rspec';
const data = { total: 10 };
const result = utils.reportTextBuilder(name, data);
expect(result).toBe('Rspec found no changed test results out of 10 total tests');
});
it('should render text for no changed results in one test', () => {
const name = 'Rspec';
const data = { total: 1 };
const result = utils.reportTextBuilder(name, data);
expect(result).toBe('Rspec found no changed test results out of 1 total test');
});
it('should render text for multiple failed results', () => {
const name = 'Rspec';
const data = { failed: 3, total: 10 };
const result = utils.reportTextBuilder(name, data);
expect(result).toBe('Rspec found 3 failed out of 10 total tests');
});
it('should render text for multiple errored results', () => {
const name = 'Rspec';
const data = { errored: 7, total: 10 };
const result = utils.reportTextBuilder(name, data);
expect(result).toBe('Rspec found 7 errors out of 10 total tests');
});
it('should render text for multiple fixed results', () => {
const name = 'Rspec';
const data = { resolved: 4, total: 10 };
const result = utils.reportTextBuilder(name, data);
expect(result).toBe('Rspec found 4 fixed test results out of 10 total tests');
});
it('should render text for multiple fixed, and multiple failed results', () => {
const name = 'Rspec';
const data = { failed: 3, resolved: 4, total: 10 };
const result = utils.reportTextBuilder(name, data);
expect(result).toBe('Rspec found 3 failed and 4 fixed test results out of 10 total tests');
});
it('should render text for a singular fixed, and a singular failed result', () => {
const name = 'Rspec';
const data = { failed: 1, resolved: 1, total: 10 };
const result = utils.reportTextBuilder(name, data);
expect(result).toBe('Rspec found 1 failed and 1 fixed test result out of 10 total tests');
});
it('should render text for singular failed, errored, and fixed results', () => {
const name = 'Rspec';
const data = { failed: 1, errored: 1, resolved: 1, total: 10 };
const result = utils.reportTextBuilder(name, data);
expect(result).toBe(
'Rspec found 1 failed, 1 error and 1 fixed test result out of 10 total tests',
);
});
it('should render text for multiple failed, errored, and fixed results', () => {
const name = 'Rspec';
const data = { failed: 2, errored: 3, resolved: 4, total: 10 };
const result = utils.reportTextBuilder(name, data);
expect(result).toBe(
'Rspec found 2 failed, 3 errors and 4 fixed test results out of 10 total tests',
);
});
});
describe('recentFailuresTextBuilder', () => {
it.each`
recentlyFailed | failed | expected
${0} | ${1} | ${''}
${1} | ${1} | ${'1 out of 1 failed test has failed more than once in the last 14 days'}
${1} | ${2} | ${'1 out of 2 failed tests has failed more than once in the last 14 days'}
${2} | ${3} | ${'2 out of 3 failed tests have failed more than once in the last 14 days'}
`(
'should render summary for $recentlyFailed out of $failed failures',
({ recentlyFailed, failed, expected }) => {
const result = utils.recentFailuresTextBuilder({ recentlyFailed, failed });
expect(result).toBe(expected);
},
);
});
describe('countRecentlyFailedTests', () => {
it('counts tests with more than one recent failure in a report', () => {
const report = {
new_failures: [{ recent_failures: { count: 2 } }],
existing_failures: [{ recent_failures: { count: 1 } }],
resolved_failures: [{ recent_failures: { count: 20 } }, { recent_failures: { count: 5 } }],
};
const result = utils.countRecentlyFailedTests(report);
expect(result).toBe(3);
});
it('counts tests with more than one recent failure in an array of reports', () => {
const reports = [
{
new_failures: [{ recent_failures: { count: 2 } }],
existing_failures: [
{ recent_failures: { count: 20 } },
{ recent_failures: { count: 5 } },
],
resolved_failures: [{ recent_failures: { count: 2 } }],
},
{
new_failures: [{ recent_failures: { count: 8 } }, { recent_failures: { count: 14 } }],
existing_failures: [{ recent_failures: { count: 1 } }],
resolved_failures: [{ recent_failures: { count: 7 } }, { recent_failures: { count: 5 } }],
},
];
const result = utils.countRecentlyFailedTests(reports);
expect(result).toBe(8);
});
});
describe('statusIcon', () => {
describe('with failed status', () => {
it('returns ICON_WARNING', () => {
expect(utils.statusIcon(STATUS_FAILED)).toEqual(ICON_WARNING);
});
});
describe('with success status', () => {
it('returns ICON_SUCCESS', () => {
expect(utils.statusIcon(STATUS_SUCCESS)).toEqual(ICON_SUCCESS);
});
});
describe('without a status', () => {
it('returns ICON_NOTFOUND', () => {
expect(utils.statusIcon()).toEqual(ICON_NOTFOUND);
});
});
});
describe('formatFilePath', () => {
it.each`
file | expected
${'./test.js'} | ${'test.js'}
${'/test.js'} | ${'test.js'}
${'.//////////////test.js'} | ${'test.js'}
${'test.js'} | ${'test.js'}
${'mock/path./test.js'} | ${'mock/path./test.js'}
${'./mock/path./test.js'} | ${'mock/path./test.js'}
`('should format $file to be $expected', ({ file, expected }) => {
expect(utils.formatFilePath(file)).toBe(expected);
});
});
});

View File

@ -6,14 +6,14 @@ RSpec.describe Gitlab::GithubImport::Importer::ProtectedBranchImporter do
subject(:importer) { described_class.new(github_protected_branch, project, client) } subject(:importer) { described_class.new(github_protected_branch, project, client) }
let(:branch_name) { 'protection' } let(:branch_name) { 'protection' }
let(:allow_force_pushes_on_github) { true } let(:allow_force_pushes_on_github) { false }
let(:require_code_owner_reviews_on_github) { false } let(:require_code_owner_reviews_on_github) { false }
let(:required_conversation_resolution) { false } let(:required_conversation_resolution) { false }
let(:required_signatures) { false } let(:required_signatures) { false }
let(:required_pull_request_reviews) { false } let(:required_pull_request_reviews) { false }
let(:expected_push_access_level) { Gitlab::Access::MAINTAINER } let(:expected_push_access_level) { Gitlab::Access::MAINTAINER }
let(:expected_merge_access_level) { Gitlab::Access::MAINTAINER } let(:expected_merge_access_level) { Gitlab::Access::MAINTAINER }
let(:expected_allow_force_push) { true } let(:expected_allow_force_push) { false }
let(:expected_code_owner_approval_required) { false } let(:expected_code_owner_approval_required) { false }
let(:github_protected_branch) do let(:github_protected_branch) do
Gitlab::GithubImport::Representation::ProtectedBranch.new( Gitlab::GithubImport::Representation::ProtectedBranch.new(
@ -102,6 +102,7 @@ RSpec.describe Gitlab::GithubImport::Importer::ProtectedBranchImporter do
end end
context 'when branch is not protected on GitLab' do context 'when branch is not protected on GitLab' do
let(:allow_force_pushes_on_github) { true }
let(:expected_allow_force_push) { true } let(:expected_allow_force_push) { true }
it_behaves_like 'create branch protection by the strictest ruleset' it_behaves_like 'create branch protection by the strictest ruleset'
@ -112,6 +113,30 @@ RSpec.describe Gitlab::GithubImport::Importer::ProtectedBranchImporter do
allow(project).to receive(:default_branch).and_return(branch_name) allow(project).to receive(:default_branch).and_return(branch_name)
end end
context 'when "allow force pushes - everyone" rule is enabled' do
let(:allow_force_pushes_on_github) { true }
context 'when there is any default branch protection' do
before do
stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_FULL)
end
let(:expected_allow_force_push) { false }
it_behaves_like 'create branch protection by the strictest ruleset'
end
context 'when there is no default branch protection' do
before do
stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_NONE)
end
let(:expected_allow_force_push) { allow_force_pushes_on_github }
it_behaves_like 'create branch protection by the strictest ruleset'
end
end
context 'when required_conversation_resolution rule is enabled' do context 'when required_conversation_resolution rule is enabled' do
let(:required_conversation_resolution) { true } let(:required_conversation_resolution) { true }

View File

@ -3,23 +3,34 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe Members::UpdateService do RSpec.describe Members::UpdateService do
let(:project) { create(:project, :public) } let_it_be(:project) { create(:project, :public) }
let(:group) { create(:group, :public) } let_it_be(:group) { create(:group, :public) }
let(:current_user) { create(:user) } let_it_be(:current_user) { create(:user) }
let(:member_user) { create(:user) } let_it_be(:member_user1) { create(:user) }
let(:permission) { :update } let_it_be(:member_user2) { create(:user) }
let(:member) { source.members_and_requesters.find_by!(user_id: member_user.id) } let_it_be(:member_users) { [member_user1, member_user2] }
let(:access_level) { Gitlab::Access::MAINTAINER } let_it_be(:permission) { :update }
let(:params) do let_it_be(:access_level) { Gitlab::Access::MAINTAINER }
{ access_level: access_level } let(:members) { source.members_and_requesters.where(user_id: member_users).to_a }
let(:update_service) { described_class.new(current_user, params) }
let(:params) { { access_level: access_level } }
let(:updated_members) do
result = subject
Array.wrap(result[:members] || result[:member])
end end
before do before do
project.add_developer(member_user) member_users.first.tap do |member_user|
group.add_developer(member_user) expires_at = 10.days.from_now
end project.add_member(member_user, Gitlab::Access::DEVELOPER, expires_at: expires_at)
group.add_member(member_user, Gitlab::Access::DEVELOPER, expires_at: expires_at)
end
subject { described_class.new(current_user, params).execute(member, permission: permission) } member_users[1..].each do |member_user|
project.add_developer(member_user)
group.add_developer(member_user)
end
end
shared_examples 'a service raising Gitlab::Access::AccessDeniedError' do shared_examples 'a service raising Gitlab::Access::AccessDeniedError' do
it 'raises Gitlab::Access::AccessDeniedError' do it 'raises Gitlab::Access::AccessDeniedError' do
@ -28,209 +39,326 @@ RSpec.describe Members::UpdateService do
end end
end end
shared_examples 'a service updating a member' do shared_examples 'current user cannot update the given members' do
it 'updates the member' do it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do
expect(TodosDestroyer::EntityLeaveWorker).not_to receive(:perform_in).with(Todo::WAIT_FOR_DELETE, member.user_id, member.source_id, source.class.name) let_it_be(:source) { project }
end
updated_member = subject.fetch(:member) it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do
let_it_be(:source) { group }
end
end
expect(updated_member).to be_valid shared_examples 'returns error status when params are invalid' do
expect(updated_member.access_level).to eq(access_level) let_it_be(:params) { { expires_at: 2.days.ago } }
specify do
expect(subject[:status]).to eq(:error)
end
end
shared_examples 'a service updating members' do
it 'updates the members' do
new_access_levels = updated_members.map(&:access_level)
expect(updated_members).not_to be_empty
expect(updated_members).to all(be_valid)
expect(new_access_levels).to all(be access_level)
end end
it 'returns success status' do it 'returns success status' do
result = subject.fetch(:status) expect(subject.fetch(:status)).to eq(:success)
expect(result).to eq(:success)
end end
context 'when member is downgraded to guest' do it 'invokes after_execute with correct args' do
members.each do |member|
expect(update_service).to receive(:after_execute).with(
action: permission,
old_access_level: member.human_access,
old_expiry: member.expires_at,
member: member
)
end
subject
end
it 'authorization update callback is triggered' do
expect(members).to all(receive(:refresh_member_authorized_projects).once)
subject
end
it 'does not enqueues todos for deletion' do
members.each do |member|
expect(TodosDestroyer::EntityLeaveWorker)
.not_to receive(:perform_in).with(Todo::WAIT_FOR_DELETE, member.user_id, member.source_id, source.class.name)
end
subject
end
context 'when members are downgraded to guest' do
shared_examples 'schedules to delete confidential todos' do shared_examples 'schedules to delete confidential todos' do
it do it do
expect(TodosDestroyer::EntityLeaveWorker).to receive(:perform_in).with(Todo::WAIT_FOR_DELETE, member.user_id, member.source_id, source.class.name).once members.each do |member|
expect(TodosDestroyer::EntityLeaveWorker)
.to receive(:perform_in)
.with(Todo::WAIT_FOR_DELETE, member.user_id, member.source_id, source.class.name).once
end
updated_member = subject.fetch(:member) new_access_levels = updated_members.map(&:access_level)
expect(updated_members).to all(be_valid)
expect(updated_member).to be_valid expect(new_access_levels).to all(be Gitlab::Access::GUEST)
expect(updated_member.access_level).to eq(Gitlab::Access::GUEST)
end end
end end
context 'with Gitlab::Access::GUEST level as a string' do context 'with Gitlab::Access::GUEST level as a string' do
let(:params) { { access_level: Gitlab::Access::GUEST.to_s } } let_it_be(:params) { { access_level: Gitlab::Access::GUEST.to_s } }
it_behaves_like 'schedules to delete confidential todos' it_behaves_like 'schedules to delete confidential todos'
end end
context 'with Gitlab::Access::GUEST level as an integer' do context 'with Gitlab::Access::GUEST level as an integer' do
let(:params) { { access_level: Gitlab::Access::GUEST } } let_it_be(:params) { { access_level: Gitlab::Access::GUEST } }
it_behaves_like 'schedules to delete confidential todos' it_behaves_like 'schedules to delete confidential todos'
end end
end end
context 'when access_level is invalid' do context 'when access_level is invalid' do
let(:params) { { access_level: 'invalid' } } let_it_be(:params) { { access_level: 'invalid' } }
it 'raises an error' do it 'raises an error' do
expect { described_class.new(current_user, params) }.to raise_error(ArgumentError, 'invalid value for Integer(): "invalid"') expect { described_class.new(current_user, params) }
.to raise_error(ArgumentError, 'invalid value for Integer(): "invalid"')
end end
end end
context 'when member is not valid' do context 'when members update results in no change' do
let(:params) { { expires_at: 2.days.ago } } let(:params) { { access_level: members.first.access_level } }
it 'returns error status' do it 'does not invoke update! and post_update' do
result = subject expect(update_service).not_to receive(:save!)
expect(update_service).not_to receive(:post_update)
expect(result[:status]).to eq(:error) expect(subject[:status]).to eq(:success)
end
it 'authorization update callback is not triggered' do
members.each { |member| expect(member).not_to receive(:refresh_member_authorized_projects) }
subject
end end
end end
end end
context 'when current user cannot update the given member' do shared_examples 'updating a project' do
it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do
let(:source) { project }
end
it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do
let(:source) { group }
end
end
context 'when current user can update the given member' do
before do
project.add_maintainer(current_user)
group.add_owner(current_user)
end
it_behaves_like 'a service updating a member' do
let(:source) { project }
end
it_behaves_like 'a service updating a member' do
let(:source) { group }
end
end
context 'in a project' do
let_it_be(:group_project) { create(:project, group: create(:group)) } let_it_be(:group_project) { create(:project, group: create(:group)) }
let_it_be(:source) { group_project }
let(:source) { group_project } before do
member_users.each { |member_user| group_project.add_developer(member_user) }
end
context 'a project maintainer' do context 'as a project maintainer' do
before do before do
group_project.add_maintainer(current_user) group_project.add_maintainer(current_user)
end end
context 'cannot update a member to OWNER' do it_behaves_like 'a service updating members'
before do
group_project.add_developer(member_user)
end
context 'when member update results in an error' do
it_behaves_like 'a service returning an error'
end
context 'and updating members to OWNER' do
it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do
let(:access_level) { Gitlab::Access::OWNER } let_it_be(:access_level) { Gitlab::Access::OWNER }
end end
end end
context 'cannot update themselves to OWNER' do context 'and updating themselves to OWNER' do
let(:member) { source.members_and_requesters.find_by!(user_id: current_user.id) } let(:members) { source.members_and_requesters.find_by!(user_id: current_user.id) }
before do
group_project.add_developer(member_user)
end
it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do
let(:access_level) { Gitlab::Access::OWNER } let_it_be(:access_level) { Gitlab::Access::OWNER }
end end
end end
context 'cannot downgrade a member from OWNER' do context 'and downgrading members from OWNER' do
before do before do
group_project.add_owner(member_user) member_users.each { |member_user| group_project.add_owner(member_user) }
end end
it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do
let(:access_level) { Gitlab::Access::MAINTAINER } let_it_be(:access_level) { Gitlab::Access::MAINTAINER }
end end
end end
end end
context 'owners' do context 'when current_user is considered an owner in the project via inheritance' do
before do before do
# so that `current_user` is considered an `OWNER` in the project via inheritance.
group_project.group.add_owner(current_user) group_project.group.add_owner(current_user)
end end
context 'can update a member to OWNER' do context 'and can update members to OWNER' do
before do before do
group_project.add_developer(member_user) member_users.each { |member_user| group_project.add_developer(member_user) }
end end
it_behaves_like 'a service updating a member' do it_behaves_like 'a service updating members' do
let(:access_level) { Gitlab::Access::OWNER } let_it_be(:access_level) { Gitlab::Access::OWNER }
end end
end end
context 'can downgrade a member from OWNER' do context 'and can downgrade members from OWNER' do
before do before do
group_project.add_owner(member_user) member_users.each { |member_user| group_project.add_owner(member_user) }
end end
it_behaves_like 'a service updating a member' do it_behaves_like 'a service updating members' do
let(:access_level) { Gitlab::Access::MAINTAINER } let_it_be(:access_level) { Gitlab::Access::MAINTAINER }
end end
end end
end end
end end
context 'authorization updates' do shared_examples 'updating a group' do
let_it_be(:user) { create(:user) } let_it_be(:source) { group }
shared_examples 'manages authorization updates' do before do
context 'access level changes' do group.add_owner(current_user)
let(:params) do end
{ access_level: Gitlab::Access::MAINTAINER }
end
it 'authorization update callback is triggered' do it_behaves_like 'a service updating members'
expect(member).to receive(:refresh_member_authorized_projects).once
described_class.new(current_user, params).execute(member, permission: permission) context 'when member update results in an error' do
end it_behaves_like 'a service returning an error'
end
context 'when group members expiration date is updated' do
let_it_be(:params) { { expires_at: 20.days.from_now } }
let(:notification_service) { instance_double(NotificationService) }
before do
allow(NotificationService).to receive(:new).and_return(notification_service)
end end
context 'no attribute changes' do it 'emails the users that their group membership expiry has changed' do
let(:params) do members.each do |member|
{ access_level: Gitlab::Access::DEVELOPER } expect(notification_service).to receive(:updated_group_member_expiration).with(member)
end end
it 'authorization update callback is not triggered' do subject
expect(member).not_to receive(:refresh_member_authorized_projects) end
end
end
described_class.new(current_user, params).execute(member, permission: permission) context 'when :bulk_update_membership_roles feature flag is disabled' do
let(:member) { source.members_and_requesters.find_by!(user_id: member_user1.id) }
let(:members) { [member] }
subject { update_service.execute(member, permission: permission) }
shared_examples 'a service returning an error' do
before do
allow(member).to receive(:save) do
member.errors.add(:user_id)
member.errors.add(:access_level)
end end
.and_return(false)
end
it_behaves_like 'returns error status when params are invalid'
it 'returns the error' do
response = subject
expect(response[:status]).to eq(:error)
expect(response[:message]).to eq('User is invalid and Access level is invalid')
end end
end end
context 'group member' do before do
let(:source) { group } stub_feature_flags(bulk_update_membership_roles: false)
before do
group.add_owner(current_user)
end
include_examples 'manages authorization updates'
end end
context 'project member' do it_behaves_like 'current user cannot update the given members'
let(:source) { project } it_behaves_like 'updating a project'
it_behaves_like 'updating a group'
end
subject { update_service.execute(members, permission: permission) }
shared_examples 'a service returning an error' do
it_behaves_like 'returns error status when params are invalid'
context 'when a member update results in invalid record' do
let(:member2) { members.second }
before do before do
project.add_maintainer(current_user) allow(member2).to receive(:save!) do
member2.errors.add(:user_id)
member2.errors.add(:access_level)
end.and_raise(ActiveRecord::RecordInvalid)
end end
include_examples 'manages authorization updates' it 'returns the error' do
response = subject
expect(response[:status]).to eq(:error)
expect(response[:message]).to eq('User is invalid and Access level is invalid')
end
it 'rollbacks back the entire update' do
old_access_levels = members.pluck(:access_level)
subject
expect(members.each(&:reset).pluck(:access_level)).to eq(old_access_levels)
end
end
end
it_behaves_like 'current user cannot update the given members'
it_behaves_like 'updating a project'
it_behaves_like 'updating a group'
context 'with a single member' do
let(:member) { create(:group_member, group: group) }
let(:members) { member }
before do
group.add_owner(current_user)
end
it 'returns the correct response' do
expect(subject[:member]).to eq(member)
end
end
context 'when current user is an admin', :enable_admin_mode do
let_it_be(:current_user) { create(:admin) }
let_it_be(:source) { group }
context 'when all owners are being downgraded' do
before do
member_users.each { |member_user| group.add_owner(member_user) }
end
it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError'
end
context 'when all blocked owners are being downgraded' do
before do
member_users.each do |member_user|
group.add_owner(member_user)
member_user.block
end
end
it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError'
end end
end end
end end

View File

@ -10079,7 +10079,6 @@
- './spec/services/members/request_access_service_spec.rb' - './spec/services/members/request_access_service_spec.rb'
- './spec/services/members/standard_member_builder_spec.rb' - './spec/services/members/standard_member_builder_spec.rb'
- './spec/services/members/unassign_issuables_service_spec.rb' - './spec/services/members/unassign_issuables_service_spec.rb'
- './spec/services/members/update_service_spec.rb'
- './spec/services/merge_requests/add_context_service_spec.rb' - './spec/services/merge_requests/add_context_service_spec.rb'
- './spec/services/merge_requests/add_spent_time_service_spec.rb' - './spec/services/merge_requests/add_spent_time_service_spec.rb'
- './spec/services/merge_requests/add_todo_when_build_fails_service_spec.rb' - './spec/services/merge_requests/add_todo_when_build_fails_service_spec.rb'