diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-11-07 09:08:10 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-11-07 09:08:10 +0300 |
commit | 44c74f7b06002162c0d6bcc7c8f94f6b1a56d438 (patch) | |
tree | 8605081c4e334380388e46ec1f27719af136d8f0 | |
parent | a31408ba64f61275813cc3ffd5aa9bc9ce9f3319 (diff) |
Add latest changes from gitlab-org/gitlab@master
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' |