Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2022-11-07 09:08:10 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2022-11-07 09:08:10 +0300
commit44c74f7b06002162c0d6bcc7c8f94f6b1a56d438 (patch)
tree8605081c4e334380388e46ec1f27719af136d8f0
parenta31408ba64f61275813cc3ffd5aa9bc9ce9f3319 (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--.rubocop_todo/layout/line_length.yml2
-rw-r--r--.rubocop_todo/rspec/context_wording.yml1
-rw-r--r--.rubocop_todo/style/if_unless_modifier.yml1
-rw-r--r--app/assets/javascripts/pipelines/components/test_reports/test_case_details.vue1
-rw-r--r--app/assets/javascripts/reports/components/issue_body.js2
-rw-r--r--app/assets/javascripts/reports/grouped_test_report/components/modal.vue74
-rw-r--r--app/assets/javascripts/reports/grouped_test_report/components/test_issue_body.vue64
-rw-r--r--app/assets/javascripts/reports/grouped_test_report/grouped_test_reports_app.vue204
-rw-r--r--app/assets/javascripts/reports/grouped_test_report/store/actions.js82
-rw-r--r--app/assets/javascripts/reports/grouped_test_report/store/getters.js13
-rw-r--r--app/assets/javascripts/reports/grouped_test_report/store/index.js17
-rw-r--r--app/assets/javascripts/reports/grouped_test_report/store/mutation_types.js7
-rw-r--r--app/assets/javascripts/reports/grouped_test_report/store/mutations.js79
-rw-r--r--app/assets/javascripts/reports/grouped_test_report/store/state.js71
-rw-r--r--app/assets/javascripts/reports/grouped_test_report/store/utils.js111
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue15
-rw-r--r--app/controllers/projects/merge_requests_controller.rb1
-rw-r--r--app/services/members/update_service.rb86
-rw-r--r--config/feature_flags/development/bulk_update_membership_roles.yml (renamed from config/feature_flags/development/refactor_mr_widget_test_summary.yml)10
-rw-r--r--danger/rubygems/Dangerfile11
-rw-r--r--doc/user/project/import/github.md2
-rw-r--r--lib/gitlab/github_import/importer/protected_branch_importer.rb10
-rw-r--r--locale/gitlab.pot53
-rw-r--r--spec/features/merge_request/user_sees_merge_widget_spec.rb130
-rw-r--r--spec/frontend/reports/components/__snapshots__/grouped_issues_list_spec.js.snap2
-rw-r--r--spec/frontend/reports/components/grouped_issues_list_spec.js2
-rw-r--r--spec/frontend/reports/components/report_item_spec.js4
-rw-r--r--spec/frontend/reports/grouped_test_report/components/modal_spec.js68
-rw-r--r--spec/frontend/reports/grouped_test_report/components/test_issue_body_spec.js96
-rw-r--r--spec/frontend/reports/grouped_test_report/grouped_test_reports_app_spec.js355
-rw-r--r--spec/frontend/reports/grouped_test_report/store/actions_spec.js168
-rw-r--r--spec/frontend/reports/grouped_test_report/store/mutations_spec.js162
-rw-r--r--spec/frontend/reports/grouped_test_report/store/utils_spec.js255
-rw-r--r--spec/lib/gitlab/github_import/importer/protected_branch_importer_spec.rb29
-rw-r--r--spec/services/members/update_service_spec.rb354
-rw-r--r--spec/support/rspec_order_todo.yml1
36 files changed, 429 insertions, 2114 deletions
diff --git a/.rubocop_todo/layout/line_length.yml b/.rubocop_todo/layout/line_length.yml
index 3cdede96163..68a316e9f84 100644
--- a/.rubocop_todo/layout/line_length.yml
+++ b/.rubocop_todo/layout/line_length.yml
@@ -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'
diff --git a/.rubocop_todo/rspec/context_wording.yml b/.rubocop_todo/rspec/context_wording.yml
index f54d3b38f7a..34f30725a4f 100644
--- a/.rubocop_todo/rspec/context_wording.yml
+++ b/.rubocop_todo/rspec/context_wording.yml
@@ -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'
diff --git a/.rubocop_todo/style/if_unless_modifier.yml b/.rubocop_todo/style/if_unless_modifier.yml
index 16d163ebdfc..fc05a8bc163 100644
--- a/.rubocop_todo/style/if_unless_modifier.yml
+++ b/.rubocop_todo/style/if_unless_modifier.yml
@@ -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'
diff --git a/app/assets/javascripts/pipelines/components/test_reports/test_case_details.vue b/app/assets/javascripts/pipelines/components/test_reports/test_case_details.vue
index 69509c9088b..2d1f1945e5a 100644
--- a/app/assets/javascripts/pipelines/components/test_reports/test_case_details.vue
+++ b/app/assets/javascripts/pipelines/components/test_reports/test_case_details.vue
@@ -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"
diff --git a/app/assets/javascripts/reports/components/issue_body.js b/app/assets/javascripts/reports/components/issue_body.js
index a76a6f45c07..4f418216024 100644
--- a/app/assets/javascripts/reports/components/issue_body.js
+++ b/app/assets/javascripts/reports/components/issue_body.js
@@ -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 = {
diff --git a/app/assets/javascripts/reports/grouped_test_report/components/modal.vue b/app/assets/javascripts/reports/grouped_test_report/components/modal.vue
deleted file mode 100644
index ca518aea743..00000000000
--- a/app/assets/javascripts/reports/grouped_test_report/components/modal.vue
+++ /dev/null
@@ -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>
diff --git a/app/assets/javascripts/reports/grouped_test_report/components/test_issue_body.vue b/app/assets/javascripts/reports/grouped_test_report/components/test_issue_body.vue
deleted file mode 100644
index 8913046d62f..00000000000
--- a/app/assets/javascripts/reports/grouped_test_report/components/test_issue_body.vue
+++ /dev/null
@@ -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>
diff --git a/app/assets/javascripts/reports/grouped_test_report/grouped_test_reports_app.vue b/app/assets/javascripts/reports/grouped_test_report/grouped_test_reports_app.vue
deleted file mode 100644
index be49a03a9a5..00000000000
--- a/app/assets/javascripts/reports/grouped_test_report/grouped_test_reports_app.vue
+++ /dev/null
@@ -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>
diff --git a/app/assets/javascripts/reports/grouped_test_report/store/actions.js b/app/assets/javascripts/reports/grouped_test_report/store/actions.js
deleted file mode 100644
index 73f8df016b6..00000000000
--- a/app/assets/javascripts/reports/grouped_test_report/store/actions.js
+++ /dev/null
@@ -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);
diff --git a/app/assets/javascripts/reports/grouped_test_report/store/getters.js b/app/assets/javascripts/reports/grouped_test_report/store/getters.js
deleted file mode 100644
index e7d1675f74a..00000000000
--- a/app/assets/javascripts/reports/grouped_test_report/store/getters.js
+++ /dev/null
@@ -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;
-};
diff --git a/app/assets/javascripts/reports/grouped_test_report/store/index.js b/app/assets/javascripts/reports/grouped_test_report/store/index.js
deleted file mode 100644
index a2edfa94a48..00000000000
--- a/app/assets/javascripts/reports/grouped_test_report/store/index.js
+++ /dev/null
@@ -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());
diff --git a/app/assets/javascripts/reports/grouped_test_report/store/mutation_types.js b/app/assets/javascripts/reports/grouped_test_report/store/mutation_types.js
deleted file mode 100644
index ff839c564b6..00000000000
--- a/app/assets/javascripts/reports/grouped_test_report/store/mutation_types.js
+++ /dev/null
@@ -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';
diff --git a/app/assets/javascripts/reports/grouped_test_report/store/mutations.js b/app/assets/javascripts/reports/grouped_test_report/store/mutations.js
deleted file mode 100644
index 2b88776815b..00000000000
--- a/app/assets/javascripts/reports/grouped_test_report/store/mutations.js
+++ /dev/null
@@ -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,
- };
- });
- },
-};
diff --git a/app/assets/javascripts/reports/grouped_test_report/store/state.js b/app/assets/javascripts/reports/grouped_test_report/store/state.js
deleted file mode 100644
index 46909bde337..00000000000
--- a/app/assets/javascripts/reports/grouped_test_report/store/state.js
+++ /dev/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,
- },
- },
- },
-});
diff --git a/app/assets/javascripts/reports/grouped_test_report/store/utils.js b/app/assets/javascripts/reports/grouped_test_report/store/utils.js
deleted file mode 100644
index df5dd73b66c..00000000000
--- a/app/assets/javascripts/reports/grouped_test_report/store/utils.js
+++ /dev/null
@@ -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(/^\.?\/*/, '');
-};
diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
index af0328edb23..c658d3a38f4 100644
--- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
@@ -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
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index cfc565a8b56..12356607494 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -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)
diff --git a/app/services/members/update_service.rb b/app/services/members/update_service.rb
index 3d8a76c50c2..0e6b02f7a80 100644
--- a/app/services/members/update_service.rb
+++ b/app/services/members/update_service.rb
@@ -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_expiry_map = members.to_h do |member|
+ [member.id, { human_access: member.human_access, expires_at: member.expires_at }]
+ end
+
+ if Feature.enabled?(:bulk_update_membership_roles, current_user)
+ multiple_members_update(members, permission, old_access_level_expiry_map)
+ else
+ single_member_update(members.first, permission, old_access_level_expiry_map)
+ end
+
+ prepare_response(members)
+ end
+
+ private
- old_access_level = member.human_access
- old_expiry = member.expires_at
+ def single_member_update(member, permission, old_access_level_expiry_map)
+ raise Gitlab::Access::AccessDeniedError unless has_update_permissions?(member, permission)
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)
+ post_update(member, permission, old_access_level_expiry_map) if member.save
+ end
- # Deletes only confidential issues todos for guests
- enqueue_delete_todos(member) if downgrading_to_guest?
+ 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
- if member.errors.any?
- error(member.errors.full_messages.to_sentence, pass_back: { member: member })
- else
- success(member: member)
- end
+ updated_members.each { |member| post_update(member, permission, old_access_level_expiry_map) }
end
- private
+ 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]
- 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
+ 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
- true
+ 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?
diff --git a/config/feature_flags/development/refactor_mr_widget_test_summary.yml b/config/feature_flags/development/bulk_update_membership_roles.yml
index 902248f1d85..701f72db3e1 100644
--- a/config/feature_flags/development/refactor_mr_widget_test_summary.yml
+++ b/config/feature_flags/development/bulk_update_membership_roles.yml
@@ -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
diff --git a/danger/rubygems/Dangerfile b/danger/rubygems/Dangerfile
new file mode 100644
index 00000000000..8e404d88fc1
--- /dev/null
+++ b/danger/rubygems/Dangerfile
@@ -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
diff --git a/doc/user/project/import/github.md b/doc/user/project/import/github.md
index c5488741f24..4e8115dda25 100644
--- a/doc/user/project/import/github.md
+++ b/doc/user/project/import/github.md
@@ -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.
diff --git a/lib/gitlab/github_import/importer/protected_branch_importer.rb b/lib/gitlab/github_import/importer/protected_branch_importer.rb
index dc0366f855f..801a0840c52 100644
--- a/lib/gitlab/github_import/importer/protected_branch_importer.rb
+++ b/lib/gitlab/github_import/importer/protected_branch_importer.rb
@@ -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
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 8ce7e1972f4..e1f184dbd36 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -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 ""
diff --git a/spec/features/merge_request/user_sees_merge_widget_spec.rb b/spec/features/merge_request/user_sees_merge_widget_spec.rb
index 3cd4394c732..e2c93f39e9c 100644
--- a/spec/features/merge_request/user_sees_merge_widget_spec.rb
+++ b/spec/features/merge_request/user_sees_merge_widget_spec.rb
@@ -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
diff --git a/spec/frontend/reports/components/__snapshots__/grouped_issues_list_spec.js.snap b/spec/frontend/reports/components/__snapshots__/grouped_issues_list_spec.js.snap
index 111757e2d30..311a67a3e31 100644
--- a/spec/frontend/reports/components/__snapshots__/grouped_issues_list_spec.js.snap
+++ b/spec/frontend/reports/components/__snapshots__/grouped_issues_list_spec.js.snap
@@ -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 {
diff --git a/spec/frontend/reports/components/grouped_issues_list_spec.js b/spec/frontend/reports/components/grouped_issues_list_spec.js
index 6c0275dc47d..cacbde590d6 100644
--- a/spec/frontend/reports/components/grouped_issues_list_spec.js
+++ b/spec/frontend/reports/components/grouped_issues_list_spec.js
@@ -74,7 +74,7 @@ describe('Grouped Issues List', () => {
createComponent({
propsData: {
resolvedIssues: [{ name: 'foo' }],
- component: 'TestIssueBody',
+ component: 'CodequalityIssueBody',
},
stubs: {
ReportItem,
diff --git a/spec/frontend/reports/components/report_item_spec.js b/spec/frontend/reports/components/report_item_spec.js
index b52c163eb26..60c7e5f2b44 100644
--- a/spec/frontend/reports/components/report_item_spec.js
+++ b/spec/frontend/reports/components/report_item_spec.js
@@ -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,
},
});
diff --git a/spec/frontend/reports/grouped_test_report/components/modal_spec.js b/spec/frontend/reports/grouped_test_report/components/modal_spec.js
deleted file mode 100644
index e8564d2428d..00000000000
--- a/spec/frontend/reports/grouped_test_report/components/modal_spec.js
+++ /dev/null
@@ -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([[]]);
- });
-});
diff --git a/spec/frontend/reports/grouped_test_report/components/test_issue_body_spec.js b/spec/frontend/reports/grouped_test_report/components/test_issue_body_spec.js
deleted file mode 100644
index 8a854a92ad7..00000000000
--- a/spec/frontend/reports/grouped_test_report/components/test_issue_body_spec.js
+++ /dev/null
@@ -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,
- });
- });
- });
-});
diff --git a/spec/frontend/reports/grouped_test_report/grouped_test_reports_app_spec.js b/spec/frontend/reports/grouped_test_report/grouped_test_reports_app_spec.js
deleted file mode 100644
index 90edb27d1d6..00000000000
--- a/spec/frontend/reports/grouped_test_report/grouped_test_reports_app_spec.js
+++ /dev/null
@@ -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');
- });
- });
-});
diff --git a/spec/frontend/reports/grouped_test_report/store/actions_spec.js b/spec/frontend/reports/grouped_test_report/store/actions_spec.js
deleted file mode 100644
index 7469c31cf84..00000000000
--- a/spec/frontend/reports/grouped_test_report/store/actions_spec.js
+++ /dev/null
@@ -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: {} }],
- [],
- );
- });
- });
-});
diff --git a/spec/frontend/reports/grouped_test_report/store/mutations_spec.js b/spec/frontend/reports/grouped_test_report/store/mutations_spec.js
deleted file mode 100644
index b2890d7285f..00000000000
--- a/spec/frontend/reports/grouped_test_report/store/mutations_spec.js
+++ /dev/null
@@ -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);
- });
- });
-});
diff --git a/spec/frontend/reports/grouped_test_report/store/utils_spec.js b/spec/frontend/reports/grouped_test_report/store/utils_spec.js
deleted file mode 100644
index 760afe1c11a..00000000000
--- a/spec/frontend/reports/grouped_test_report/store/utils_spec.js
+++ /dev/null
@@ -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);
- });
- });
-});
diff --git a/spec/lib/gitlab/github_import/importer/protected_branch_importer_spec.rb b/spec/lib/gitlab/github_import/importer/protected_branch_importer_spec.rb
index 0032d65a6c9..d6b7411e640 100644
--- a/spec/lib/gitlab/github_import/importer/protected_branch_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/protected_branch_importer_spec.rb
@@ -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 }
diff --git a/spec/services/members/update_service_spec.rb b/spec/services/members/update_service_spec.rb
index eca40860bda..eb8fae03c39 100644
--- a/spec/services/members/update_service_spec.rb
+++ b/spec/services/members/update_service_spec.rb
@@ -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
+
+ shared_examples 'returns error status when params are invalid' do
+ let_it_be(:params) { { expires_at: 2.days.ago } }
- expect(updated_member).to be_valid
- expect(updated_member.access_level).to eq(access_level)
+ 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(subject.fetch(:status)).to eq(:success)
+ end
+
+ 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
- expect(result).to eq(:success)
+ subject
end
- context 'when member is 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
+ it 'authorization update callback is triggered' do
+ expect(members).to all(receive(:refresh_member_authorized_projects).once)
- updated_member = subject.fetch(:member)
+ 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
- expect(updated_member).to be_valid
- expect(updated_member.access_level).to eq(Gitlab::Access::GUEST)
+ subject
+ end
+
+ context 'when members are downgraded to guest' do
+ shared_examples 'schedules to delete confidential todos' do
+ it do
+ 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
+
+ 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
- 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 'authorization update callback is not triggered' do
+ members.each { |member| expect(member).not_to receive(:refresh_member_authorized_projects) }
- it_behaves_like 'a service updating a member' do
- let(:source) { group }
+ subject
+ end
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
+ stub_feature_flags(bulk_update_membership_roles: false)
+ end
+
+ 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
- group.add_owner(current_user)
+ allow(member2).to receive(:save!) do
+ member2.errors.add(:user_id)
+ member2.errors.add(:access_level)
+ end.and_raise(ActiveRecord::RecordInvalid)
+ end
+
+ 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'
- include_examples 'manages authorization updates'
+ context 'with a single member' do
+ let(:member) { create(:group_member, group: group) }
+ let(:members) { member }
+
+ before do
+ group.add_owner(current_user)
end
- context 'project member' do
- let(:source) { project }
+ 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
- project.add_maintainer(current_user)
+ 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
- include_examples 'manages authorization updates'
+ it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError'
end
end
end
diff --git a/spec/support/rspec_order_todo.yml b/spec/support/rspec_order_todo.yml
index 77d36cbee62..7ab519032fa 100644
--- a/spec/support/rspec_order_todo.yml
+++ b/spec/support/rspec_order_todo.yml
@@ -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'