Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
a31408ba64
commit
44c74f7b06
36 changed files with 438 additions and 2123 deletions
|
@ -597,7 +597,6 @@ Layout/LineLength:
|
|||
- 'app/services/loose_foreign_keys/cleaner_service.rb'
|
||||
- 'app/services/members/destroy_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/assign_issues_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/invitation_reminder_email_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/after_create_service_spec.rb'
|
||||
- 'spec/services/merge_requests/assign_issues_service_spec.rb'
|
||||
|
|
|
@ -2972,7 +2972,6 @@ RSpec/ContextWording:
|
|||
- 'spec/services/members/destroy_service_spec.rb'
|
||||
- 'spec/services/members/groups/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/after_create_service_spec.rb'
|
||||
- 'spec/services/merge_requests/approval_service_spec.rb'
|
||||
|
|
|
@ -309,7 +309,6 @@ Style/IfUnlessModifier:
|
|||
- 'app/services/issues/update_service.rb'
|
||||
- 'app/services/lfs/lock_file_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/base_service.rb'
|
||||
- 'app/services/merge_requests/build_service.rb'
|
||||
|
|
|
@ -73,6 +73,7 @@ export default {
|
|||
|
||||
<template>
|
||||
<gl-modal
|
||||
data-testid="test-case-details-modal"
|
||||
:modal-id="modalId"
|
||||
:title="testCase.classname"
|
||||
:action-primary="$options.modalCloseButton"
|
||||
|
|
|
@ -2,12 +2,10 @@ import IssueStatusIcon from '~/reports/components/issue_status_icon.vue';
|
|||
|
||||
export const components = {
|
||||
CodequalityIssueBody: () => import('../codequality_report/components/codequality_issue_body.vue'),
|
||||
TestIssueBody: () => import('../grouped_test_report/components/test_issue_body.vue'),
|
||||
};
|
||||
|
||||
export const componentNames = {
|
||||
CodequalityIssueBody: 'CodequalityIssueBody',
|
||||
TestIssueBody: 'TestIssueBody',
|
||||
};
|
||||
|
||||
export const iconComponents = {
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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);
|
|
@ -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;
|
||||
};
|
|
@ -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());
|
|
@ -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';
|
|
@ -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,
|
||||
};
|
||||
});
|
||||
},
|
||||
};
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
|
@ -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(/^\.?\/*/, '');
|
||||
};
|
|
@ -82,8 +82,6 @@ export default {
|
|||
MrWidgetAutoMergeFailed: AutoMergeFailed,
|
||||
MrWidgetRebase: RebaseState,
|
||||
SourceBranchRemovalStatus,
|
||||
GroupedTestReportsApp: () =>
|
||||
import('../reports/grouped_test_report/grouped_test_reports_app.vue'),
|
||||
MrWidgetApprovals,
|
||||
SecurityReportsApp: () => import('~/vue_shared/security_reports/security_reports_app.vue'),
|
||||
MergeChecksFailed: () => import('./components/states/merge_checks_failed.vue'),
|
||||
|
@ -183,9 +181,6 @@ export default {
|
|||
shouldRenderTestReport() {
|
||||
return Boolean(this.mr?.testResultsPath);
|
||||
},
|
||||
shouldRenderRefactoredTestReport() {
|
||||
return window.gon?.features?.refactorMrWidgetTestSummary;
|
||||
},
|
||||
mergeError() {
|
||||
let { mergeError } = this.mr;
|
||||
|
||||
|
@ -519,7 +514,7 @@ export default {
|
|||
}
|
||||
},
|
||||
registerTestReportExtension() {
|
||||
if (this.shouldRenderTestReport && this.shouldRenderRefactoredTestReport) {
|
||||
if (this.shouldRenderTestReport) {
|
||||
registerExtension(testReportExtension);
|
||||
}
|
||||
},
|
||||
|
@ -596,14 +591,6 @@ export default {
|
|||
: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">
|
||||
<component :is="componentName" :mr="mr" :service="service" />
|
||||
<ready-to-merge
|
||||
|
|
|
@ -34,7 +34,6 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
|
|||
before_action only: [:show] do
|
||||
push_frontend_feature_flag(:merge_request_widget_graphql, 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(:realtime_labels, project)
|
||||
push_frontend_feature_flag(:refactor_security_extension, @project)
|
||||
|
|
|
@ -2,40 +2,84 @@
|
|||
|
||||
module Members
|
||||
class UpdateService < Members::BaseService
|
||||
# returns the updated member
|
||||
def execute(member, permission: :update)
|
||||
raise Gitlab::Access::AccessDeniedError unless can?(current_user, action_member_permission(permission, member), member)
|
||||
raise Gitlab::Access::AccessDeniedError if prevent_upgrade_to_owner?(member) || prevent_downgrade_from_owner?(member)
|
||||
# @param members [Member, Array<Member>]
|
||||
# returns the updated member(s)
|
||||
def execute(members, permission: :update)
|
||||
members = Array.wrap(members)
|
||||
|
||||
return success(member: member) if update_results_in_no_change?(member)
|
||||
|
||||
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?
|
||||
old_access_level_expiry_map = members.to_h do |member|
|
||||
[member.id, { human_access: member.human_access, expires_at: member.expires_at }]
|
||||
end
|
||||
|
||||
if member.errors.any?
|
||||
error(member.errors.full_messages.to_sentence, pass_back: { member: member })
|
||||
if Feature.enabled?(:bulk_update_membership_roles, current_user)
|
||||
multiple_members_update(members, permission, old_access_level_expiry_map)
|
||||
else
|
||||
success(member: member)
|
||||
single_member_update(members.first, permission, old_access_level_expiry_map)
|
||||
end
|
||||
|
||||
prepare_response(members)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def update_results_in_no_change?(member)
|
||||
return false if params[:expires_at]&.to_date != member.expires_at
|
||||
return false if params[:access_level] != member.access_level
|
||||
def single_member_update(member, permission, old_access_level_expiry_map)
|
||||
raise Gitlab::Access::AccessDeniedError unless has_update_permissions?(member, permission)
|
||||
|
||||
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
|
||||
|
||||
def downgrading_to_guest?
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
---
|
||||
name: refactor_mr_widget_test_summary
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/83631
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/358208
|
||||
milestone: '15.0'
|
||||
name: bulk_update_membership_roles
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/96745
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/373257
|
||||
milestone: '15.6'
|
||||
type: development
|
||||
group: group::pipeline insights
|
||||
group: group::workspace
|
||||
default_enabled: false
|
11
danger/rubygems/Dangerfile
Normal file
11
danger/rubygems/Dangerfile
Normal 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
|
|
@ -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.
|
||||
- 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.
|
||||
- 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.
|
||||
You can still create [status checks](../merge_requests/status_checks.md) in GitLab yourself.
|
||||
|
||||
|
|
|
@ -43,10 +43,14 @@ module Gitlab
|
|||
end
|
||||
|
||||
def allow_force_push?
|
||||
if ProtectedBranch.protected?(project, protected_branch.id)
|
||||
ProtectedBranch.allow_force_push?(project, protected_branch.id) && protected_branch.allow_force_pushes
|
||||
return false unless 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
|
||||
protected_branch.allow_force_pushes
|
||||
true
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -248,21 +248,11 @@ msgid_plural "%d epics"
|
|||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
msgid "%d error"
|
||||
msgid_plural "%d errors"
|
||||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
msgid "%d exporter"
|
||||
msgid_plural "%d exporters"
|
||||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
msgid "%d failed"
|
||||
msgid_plural "%d failed"
|
||||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
msgid "%d failed security job"
|
||||
msgid_plural "%d failed security jobs"
|
||||
msgstr[0] ""
|
||||
|
@ -273,11 +263,6 @@ msgid_plural "%d files"
|
|||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
msgid "%d fixed test result"
|
||||
msgid_plural "%d fixed test results"
|
||||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
msgid "%d fork"
|
||||
msgid_plural "%d forks"
|
||||
msgstr[0] ""
|
||||
|
@ -857,12 +842,6 @@ msgstr ""
|
|||
msgid "%{name} (Busy)"
|
||||
msgstr ""
|
||||
|
||||
msgid "%{name} contained %{resultsString}"
|
||||
msgstr ""
|
||||
|
||||
msgid "%{name} found %{resultsString}"
|
||||
msgstr ""
|
||||
|
||||
msgid "%{name} is already being used for another emoji"
|
||||
msgstr ""
|
||||
|
||||
|
@ -1193,9 +1172,6 @@ msgstr ""
|
|||
msgid "%{value} is not included in the list"
|
||||
msgstr ""
|
||||
|
||||
msgid "%{value} s"
|
||||
msgstr ""
|
||||
|
||||
msgid "%{verb} %{time_spent_value} spent time."
|
||||
msgstr ""
|
||||
|
||||
|
@ -34159,18 +34135,12 @@ msgstr ""
|
|||
msgid "Reports|Base report parsing error:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Reports|Classname"
|
||||
msgstr ""
|
||||
|
||||
msgid "Reports|Copy failed test names to run locally"
|
||||
msgstr ""
|
||||
|
||||
msgid "Reports|Copy failed tests"
|
||||
msgstr ""
|
||||
|
||||
msgid "Reports|Execution time"
|
||||
msgstr ""
|
||||
|
||||
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"
|
||||
msgstr[0] ""
|
||||
|
@ -34181,12 +34151,6 @@ msgid_plural "Reports|Failed %{count} times in %{base_branch} in the last 14 day
|
|||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
msgid "Reports|Failure"
|
||||
msgstr ""
|
||||
|
||||
msgid "Reports|Filename"
|
||||
msgstr ""
|
||||
|
||||
msgid "Reports|Fixed"
|
||||
msgstr ""
|
||||
|
||||
|
@ -34229,21 +34193,12 @@ msgstr ""
|
|||
msgid "Reports|Severity"
|
||||
msgstr ""
|
||||
|
||||
msgid "Reports|System output"
|
||||
msgstr ""
|
||||
|
||||
msgid "Reports|Test summary"
|
||||
msgstr ""
|
||||
|
||||
msgid "Reports|Test summary failed loading results"
|
||||
msgstr ""
|
||||
|
||||
msgid "Reports|Test summary failed to load results"
|
||||
msgstr ""
|
||||
|
||||
msgid "Reports|Test summary results are being parsed"
|
||||
msgstr ""
|
||||
|
||||
msgid "Reports|Test summary results are loading"
|
||||
msgstr ""
|
||||
|
||||
|
@ -34259,9 +34214,6 @@ msgstr ""
|
|||
msgid "Reports|metrics report"
|
||||
msgstr ""
|
||||
|
||||
msgid "Reports|no changed test results"
|
||||
msgstr ""
|
||||
|
||||
msgid "Repositories"
|
||||
msgstr ""
|
||||
|
||||
|
@ -48971,11 +48923,6 @@ msgstr ""
|
|||
msgid "organizations can only be added to root groups"
|
||||
msgstr ""
|
||||
|
||||
msgid "out of %d total test"
|
||||
msgid_plural "out of %d total tests"
|
||||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
msgid "packages"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -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) }
|
||||
|
||||
def click_expand_button
|
||||
find('[data-testid="report-section-expand-button"]').click
|
||||
find('[data-testid="toggle-button"]').click
|
||||
end
|
||||
|
||||
before do
|
||||
project.add_maintainer(user)
|
||||
project_only_mwps.add_maintainer(user)
|
||||
sign_in(user)
|
||||
|
||||
stub_feature_flags(refactor_mr_widget_test_summary: false)
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
|
@ -545,7 +543,7 @@ RSpec.describe 'Merge request > User sees merge widget', :js do
|
|||
end
|
||||
|
||||
it 'shows parsed results' do
|
||||
expect(page).to have_content('Test summary contained')
|
||||
expect(page).to have_content('Test summary:')
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -559,7 +557,7 @@ RSpec.describe 'Merge request > User sees merge widget', :js do
|
|||
end
|
||||
|
||||
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
|
||||
|
||||
|
@ -606,13 +604,13 @@ RSpec.describe 'Merge request > User sees merge widget', :js do
|
|||
end
|
||||
|
||||
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
|
||||
|
||||
expect(page).to have_content('Test summary contained 1 failed out of 2 total tests')
|
||||
within(".js-report-section-container") do
|
||||
expect(page).to have_content('rspec found no changed test results out of 1 total test')
|
||||
expect(page).to have_content('junit found 1 failed out of 1 total test')
|
||||
expect(page).to have_content('Test summary: 1 failed, 2 total tests')
|
||||
within('[data-testid="widget-extension-collapsed-section"]') do
|
||||
expect(page).to have_content('rspec: no changed test results, 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('addTest')
|
||||
end
|
||||
|
@ -621,15 +619,15 @@ RSpec.describe 'Merge request > User sees merge widget', :js do
|
|||
|
||||
context 'when user clicks the new failure' do
|
||||
it 'shows the test report detail' do
|
||||
within(".js-reports-container") do
|
||||
within('[data-testid="widget-extension"]') do
|
||||
click_expand_button
|
||||
|
||||
within(".js-report-section-container") do
|
||||
click_button 'addTest'
|
||||
within('[data-testid="widget-extension-collapsed-section"]') do
|
||||
click_link 'addTest'
|
||||
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('6.66')
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
expect(page).to have_content('Test summary contained 1 failed out of 2 total tests')
|
||||
within(".js-report-section-container") do
|
||||
expect(page).to have_content('rspec found 1 failed out of 1 total test')
|
||||
expect(page).to have_content('junit found no changed test results out of 1 total test')
|
||||
expect(page).to have_content('Test summary: 1 failed, 2 total tests')
|
||||
within('[data-testid="widget-extension-collapsed-section"]') do
|
||||
expect(page).to have_content('rspec: 1 failed, 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')
|
||||
end
|
||||
end
|
||||
|
@ -668,15 +666,15 @@ RSpec.describe 'Merge request > User sees merge widget', :js do
|
|||
|
||||
context 'when user clicks the existing failure' do
|
||||
it 'shows test report detail of it' do
|
||||
within(".js-reports-container") do
|
||||
within('[data-testid="widget-extension"]') do
|
||||
click_expand_button
|
||||
|
||||
within(".js-report-section-container") do
|
||||
click_button 'Test#sum when a is 1 and b is 3 returns summary'
|
||||
within('[data-testid="widget-extension-collapsed-section"]') do
|
||||
click_link 'Test#sum when a is 1 and b is 3 returns summary'
|
||||
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('2.22')
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
expect(page).to have_content('Test summary contained 1 fixed test result out of 2 total tests')
|
||||
within(".js-report-section-container") do
|
||||
expect(page).to have_content('rspec found no changed test results out of 1 total test')
|
||||
expect(page).to have_content('junit found 1 fixed test result out of 1 total test')
|
||||
expect(page).to have_content('Test summary: 1 fixed test result, 2 total tests')
|
||||
within('[data-testid="widget-extension-collapsed-section"]') do
|
||||
expect(page).to have_content('rspec: no changed test results, 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')
|
||||
end
|
||||
end
|
||||
|
@ -715,15 +714,15 @@ RSpec.describe 'Merge request > User sees merge widget', :js do
|
|||
|
||||
context 'when user clicks the resolved failure' do
|
||||
it 'shows test report detail of it' do
|
||||
within(".js-reports-container") do
|
||||
within('[data-testid="widget-extension"]') do
|
||||
click_expand_button
|
||||
|
||||
within(".js-report-section-container") do
|
||||
click_button 'addTest'
|
||||
within('[data-testid="widget-extension-collapsed-section"]') do
|
||||
click_link 'addTest'
|
||||
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('5.55')
|
||||
end
|
||||
|
@ -747,13 +746,13 @@ RSpec.describe 'Merge request > User sees merge widget', :js do
|
|||
end
|
||||
|
||||
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
|
||||
|
||||
expect(page).to have_content('Test summary contained 1 error out of 2 total tests')
|
||||
within(".js-report-section-container") do
|
||||
expect(page).to have_content('rspec found no changed test results out of 1 total test')
|
||||
expect(page).to have_content('junit found 1 error out of 1 total test')
|
||||
expect(page).to have_content('Test summary: 1 error, 2 total tests')
|
||||
within('[data-testid="widget-extension-collapsed-section"]') do
|
||||
expect(page).to have_content('rspec: no changed test results, 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('addTest')
|
||||
end
|
||||
|
@ -762,15 +761,15 @@ RSpec.describe 'Merge request > User sees merge widget', :js do
|
|||
|
||||
context 'when user clicks the new error' do
|
||||
it 'shows the test report detail' do
|
||||
within(".js-reports-container") do
|
||||
within('[data-testid="widget-extension"]') do
|
||||
click_expand_button
|
||||
|
||||
within(".js-report-section-container") do
|
||||
click_button 'addTest'
|
||||
within('[data-testid="widget-extension-collapsed-section"]') do
|
||||
click_link 'addTest'
|
||||
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('8.88')
|
||||
end
|
||||
|
@ -794,13 +793,13 @@ RSpec.describe 'Merge request > User sees merge widget', :js do
|
|||
end
|
||||
|
||||
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
|
||||
|
||||
expect(page).to have_content('Test summary contained 1 error out of 2 total tests')
|
||||
within(".js-report-section-container") do
|
||||
expect(page).to have_content('rspec found 1 error out of 1 total test')
|
||||
expect(page).to have_content('junit found no changed test results out of 1 total test')
|
||||
expect(page).to have_content('Test summary: 1 error, 2 total tests')
|
||||
within('[data-testid="widget-extension-collapsed-section"]') do
|
||||
expect(page).to have_content('rspec: 1 error, 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')
|
||||
end
|
||||
end
|
||||
|
@ -808,15 +807,15 @@ RSpec.describe 'Merge request > User sees merge widget', :js do
|
|||
|
||||
context 'when user clicks the existing error' do
|
||||
it 'shows test report detail of it' do
|
||||
within(".js-reports-container") do
|
||||
within('[data-testid="widget-extension"]') do
|
||||
click_expand_button
|
||||
|
||||
within(".js-report-section-container") do
|
||||
click_button 'Test#sum when a is 4 and b is 4 returns summary'
|
||||
within('[data-testid="widget-extension-collapsed-section"]') do
|
||||
click_link 'Test#sum when a is 4 and b is 4 returns summary'
|
||||
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('4.44')
|
||||
end
|
||||
|
@ -840,13 +839,14 @@ RSpec.describe 'Merge request > User sees merge widget', :js do
|
|||
end
|
||||
|
||||
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
|
||||
|
||||
expect(page).to have_content('Test summary contained 1 fixed test result out of 2 total tests')
|
||||
within(".js-report-section-container") do
|
||||
expect(page).to have_content('rspec found no changed test results out of 1 total test')
|
||||
expect(page).to have_content('junit found 1 fixed test result out of 1 total test')
|
||||
expect(page).to have_content('Test summary: 1 fixed test result, 2 total tests')
|
||||
within('[data-testid="widget-extension-collapsed-section"]') do
|
||||
expect(page).to have_content('rspec: no changed test results, 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')
|
||||
end
|
||||
end
|
||||
|
@ -854,15 +854,15 @@ RSpec.describe 'Merge request > User sees merge widget', :js do
|
|||
|
||||
context 'when user clicks the resolved error' do
|
||||
it 'shows test report detail of it' do
|
||||
within(".js-reports-container") do
|
||||
within('[data-testid="widget-extension"]') do
|
||||
click_expand_button
|
||||
|
||||
within(".js-report-section-container") do
|
||||
click_button 'addTest'
|
||||
within('[data-testid="widget-extension-collapsed-section"]') do
|
||||
click_link 'addTest'
|
||||
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('5.55')
|
||||
end
|
||||
|
@ -894,13 +894,13 @@ RSpec.describe 'Merge request > User sees merge widget', :js do
|
|||
end
|
||||
|
||||
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
|
||||
|
||||
expect(page).to have_content('Test summary contained 20 failed out of 20 total tests')
|
||||
within(".js-report-section-container") do
|
||||
expect(page).to have_content('rspec found 10 failed out of 10 total tests')
|
||||
expect(page).to have_content('junit found 10 failed out of 10 total tests')
|
||||
expect(page).to have_content('Test summary: 20 failed, 20 total tests')
|
||||
within('[data-testid="widget-extension-collapsed-section"]') do
|
||||
expect(page).to have_content('rspec: 10 failed, 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)
|
||||
end
|
||||
|
|
|
@ -13,7 +13,7 @@ Object {
|
|||
|
||||
exports[`Grouped Issues List with data renders a report item with the correct props 1`] = `
|
||||
Object {
|
||||
"component": "TestIssueBody",
|
||||
"component": "CodequalityIssueBody",
|
||||
"iconComponent": "IssueStatusIcon",
|
||||
"isNew": false,
|
||||
"issue": Object {
|
||||
|
|
|
@ -74,7 +74,7 @@ describe('Grouped Issues List', () => {
|
|||
createComponent({
|
||||
propsData: {
|
||||
resolvedIssues: [{ name: 'foo' }],
|
||||
component: 'TestIssueBody',
|
||||
component: 'CodequalityIssueBody',
|
||||
},
|
||||
stubs: {
|
||||
ReportItem,
|
||||
|
|
|
@ -10,7 +10,7 @@ describe('ReportItem', () => {
|
|||
const wrapper = shallowMount(ReportItem, {
|
||||
propsData: {
|
||||
issue: { foo: 'bar' },
|
||||
component: componentNames.TestIssueBody,
|
||||
component: componentNames.CodequalityIssueBody,
|
||||
status: STATUS_SUCCESS,
|
||||
showReportSectionStatusIcon: false,
|
||||
},
|
||||
|
@ -23,7 +23,7 @@ describe('ReportItem', () => {
|
|||
const wrapper = shallowMount(ReportItem, {
|
||||
propsData: {
|
||||
issue: { foo: 'bar' },
|
||||
component: componentNames.TestIssueBody,
|
||||
component: componentNames.CodequalityIssueBody,
|
||||
status: STATUS_SUCCESS,
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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([[]]);
|
||||
});
|
||||
});
|
|
@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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: {} }],
|
||||
[],
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -6,14 +6,14 @@ RSpec.describe Gitlab::GithubImport::Importer::ProtectedBranchImporter do
|
|||
subject(:importer) { described_class.new(github_protected_branch, project, client) }
|
||||
|
||||
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(:required_conversation_resolution) { false }
|
||||
let(:required_signatures) { false }
|
||||
let(:required_pull_request_reviews) { false }
|
||||
let(:expected_push_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(:github_protected_branch) do
|
||||
Gitlab::GithubImport::Representation::ProtectedBranch.new(
|
||||
|
@ -102,6 +102,7 @@ RSpec.describe Gitlab::GithubImport::Importer::ProtectedBranchImporter do
|
|||
end
|
||||
|
||||
context 'when branch is not protected on GitLab' do
|
||||
let(:allow_force_pushes_on_github) { true }
|
||||
let(:expected_allow_force_push) { true }
|
||||
|
||||
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)
|
||||
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
|
||||
let(:required_conversation_resolution) { true }
|
||||
|
||||
|
|
|
@ -3,23 +3,34 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Members::UpdateService do
|
||||
let(:project) { create(:project, :public) }
|
||||
let(:group) { create(:group, :public) }
|
||||
let(:current_user) { create(:user) }
|
||||
let(:member_user) { create(:user) }
|
||||
let(:permission) { :update }
|
||||
let(:member) { source.members_and_requesters.find_by!(user_id: member_user.id) }
|
||||
let(:access_level) { Gitlab::Access::MAINTAINER }
|
||||
let(:params) do
|
||||
{ access_level: access_level }
|
||||
let_it_be(:project) { create(:project, :public) }
|
||||
let_it_be(:group) { create(:group, :public) }
|
||||
let_it_be(:current_user) { create(:user) }
|
||||
let_it_be(:member_user1) { create(:user) }
|
||||
let_it_be(:member_user2) { create(:user) }
|
||||
let_it_be(:member_users) { [member_user1, member_user2] }
|
||||
let_it_be(:permission) { :update }
|
||||
let_it_be(:access_level) { Gitlab::Access::MAINTAINER }
|
||||
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
|
||||
|
||||
before do
|
||||
project.add_developer(member_user)
|
||||
group.add_developer(member_user)
|
||||
end
|
||||
member_users.first.tap do |member_user|
|
||||
expires_at = 10.days.from_now
|
||||
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
|
||||
it 'raises Gitlab::Access::AccessDeniedError' do
|
||||
|
@ -28,209 +39,326 @@ RSpec.describe Members::UpdateService do
|
|||
end
|
||||
end
|
||||
|
||||
shared_examples 'a service updating a member' do
|
||||
it 'updates the member' do
|
||||
expect(TodosDestroyer::EntityLeaveWorker).not_to receive(:perform_in).with(Todo::WAIT_FOR_DELETE, member.user_id, member.source_id, source.class.name)
|
||||
shared_examples 'current user cannot update the given members' do
|
||||
it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do
|
||||
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
|
||||
expect(updated_member.access_level).to eq(access_level)
|
||||
shared_examples 'returns error status when params are invalid' do
|
||||
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
|
||||
|
||||
it 'returns success status' do
|
||||
result = subject.fetch(:status)
|
||||
|
||||
expect(result).to eq(:success)
|
||||
expect(subject.fetch(:status)).to eq(:success)
|
||||
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
|
||||
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)
|
||||
|
||||
expect(updated_member).to be_valid
|
||||
expect(updated_member.access_level).to eq(Gitlab::Access::GUEST)
|
||||
new_access_levels = updated_members.map(&:access_level)
|
||||
expect(updated_members).to all(be_valid)
|
||||
expect(new_access_levels).to all(be Gitlab::Access::GUEST)
|
||||
end
|
||||
end
|
||||
|
||||
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'
|
||||
end
|
||||
|
||||
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'
|
||||
end
|
||||
end
|
||||
|
||||
context 'when access_level is invalid' do
|
||||
let(:params) { { access_level: 'invalid' } }
|
||||
let_it_be(:params) { { access_level: 'invalid' } }
|
||||
|
||||
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
|
||||
|
||||
context 'when member is not valid' do
|
||||
let(:params) { { expires_at: 2.days.ago } }
|
||||
context 'when members update results in no change' do
|
||||
let(:params) { { access_level: members.first.access_level } }
|
||||
|
||||
it 'returns error status' do
|
||||
result = subject
|
||||
it 'does not invoke update! and post_update' do
|
||||
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
|
||||
|
||||
context 'when current user cannot update the given member' 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
|
||||
shared_examples 'updating a project' do
|
||||
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
|
||||
group_project.add_maintainer(current_user)
|
||||
end
|
||||
|
||||
context 'cannot update a member to OWNER' do
|
||||
before do
|
||||
group_project.add_developer(member_user)
|
||||
end
|
||||
it_behaves_like 'a service updating members'
|
||||
|
||||
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
|
||||
let(:access_level) { Gitlab::Access::OWNER }
|
||||
let_it_be(:access_level) { Gitlab::Access::OWNER }
|
||||
end
|
||||
end
|
||||
|
||||
context 'cannot update themselves to OWNER' do
|
||||
let(:member) { source.members_and_requesters.find_by!(user_id: current_user.id) }
|
||||
|
||||
before do
|
||||
group_project.add_developer(member_user)
|
||||
end
|
||||
context 'and updating themselves to OWNER' do
|
||||
let(:members) { source.members_and_requesters.find_by!(user_id: current_user.id) }
|
||||
|
||||
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
|
||||
|
||||
context 'cannot downgrade a member from OWNER' do
|
||||
context 'and downgrading members from OWNER' do
|
||||
before do
|
||||
group_project.add_owner(member_user)
|
||||
member_users.each { |member_user| group_project.add_owner(member_user) }
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
context 'owners' do
|
||||
context 'when current_user is considered an owner in the project via inheritance' do
|
||||
before do
|
||||
# so that `current_user` is considered an `OWNER` in the project via inheritance.
|
||||
group_project.group.add_owner(current_user)
|
||||
end
|
||||
|
||||
context 'can update a member to OWNER' do
|
||||
context 'and can update members to OWNER' do
|
||||
before do
|
||||
group_project.add_developer(member_user)
|
||||
member_users.each { |member_user| group_project.add_developer(member_user) }
|
||||
end
|
||||
|
||||
it_behaves_like 'a service updating a member' do
|
||||
let(:access_level) { Gitlab::Access::OWNER }
|
||||
it_behaves_like 'a service updating members' do
|
||||
let_it_be(:access_level) { Gitlab::Access::OWNER }
|
||||
end
|
||||
end
|
||||
|
||||
context 'can downgrade a member from OWNER' do
|
||||
context 'and can downgrade members from OWNER' do
|
||||
before do
|
||||
group_project.add_owner(member_user)
|
||||
member_users.each { |member_user| group_project.add_owner(member_user) }
|
||||
end
|
||||
|
||||
it_behaves_like 'a service updating a member' do
|
||||
let(:access_level) { Gitlab::Access::MAINTAINER }
|
||||
it_behaves_like 'a service updating members' do
|
||||
let_it_be(:access_level) { Gitlab::Access::MAINTAINER }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'authorization updates' do
|
||||
let_it_be(:user) { create(:user) }
|
||||
shared_examples 'updating a group' do
|
||||
let_it_be(:source) { group }
|
||||
|
||||
shared_examples 'manages authorization updates' do
|
||||
context 'access level changes' do
|
||||
let(:params) do
|
||||
{ access_level: Gitlab::Access::MAINTAINER }
|
||||
end
|
||||
before do
|
||||
group.add_owner(current_user)
|
||||
end
|
||||
|
||||
it 'authorization update callback is triggered' do
|
||||
expect(member).to receive(:refresh_member_authorized_projects).once
|
||||
it_behaves_like 'a service updating members'
|
||||
|
||||
described_class.new(current_user, params).execute(member, permission: permission)
|
||||
end
|
||||
context 'when member update results in an error' do
|
||||
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
|
||||
|
||||
context 'no attribute changes' do
|
||||
let(:params) do
|
||||
{ access_level: Gitlab::Access::DEVELOPER }
|
||||
it 'emails the users that their group membership expiry has changed' do
|
||||
members.each do |member|
|
||||
expect(notification_service).to receive(:updated_group_member_expiration).with(member)
|
||||
end
|
||||
|
||||
it 'authorization update callback is not triggered' do
|
||||
expect(member).not_to receive(:refresh_member_authorized_projects)
|
||||
subject
|
||||
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
|
||||
.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
|
||||
|
||||
context 'group member' do
|
||||
let(:source) { group }
|
||||
|
||||
before do
|
||||
group.add_owner(current_user)
|
||||
end
|
||||
|
||||
include_examples 'manages authorization updates'
|
||||
before do
|
||||
stub_feature_flags(bulk_update_membership_roles: false)
|
||||
end
|
||||
|
||||
context 'project member' do
|
||||
let(:source) { project }
|
||||
it_behaves_like 'current user cannot update the given members'
|
||||
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
|
||||
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
|
||||
|
||||
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
|
||||
|
|
|
@ -10079,7 +10079,6 @@
|
|||
- './spec/services/members/request_access_service_spec.rb'
|
||||
- './spec/services/members/standard_member_builder_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_spent_time_service_spec.rb'
|
||||
- './spec/services/merge_requests/add_todo_when_build_fails_service_spec.rb'
|
||||
|
|
Loading…
Reference in a new issue