diff options
292 files changed, 3226 insertions, 1978 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 241dcaa7832..6e5296e231e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,20 @@ documentation](doc/development/changelog.md) for instructions on adding your own entry. +## 11.4.5 (2018-11-04) + +### Fixed (4 changes, 1 of them is from the community) + +- fix link to enable usage ping from convdev index. !22545 (Anand Capur) +- Update gitlab-ui dependency to 1.8.0-hotfix.1 to fix IE11 bug. +- Remove duplicate escape in job sidebar. +- Fixed merge request fill tree toggling not respecting fluid width preference. + +### Other (1 change) + +- Fix stage dropdown not rendering in different languages. + + ## 11.4.4 (2018-10-30) ### Security (1 change) diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index bcc9c2840a7..4c2a8041846 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -0.126.0 +0.128.0 diff --git a/app/assets/javascripts/commons/gitlab_ui.js b/app/assets/javascripts/commons/gitlab_ui.js index 82a191d056b..6c18a0fd390 100644 --- a/app/assets/javascripts/commons/gitlab_ui.js +++ b/app/assets/javascripts/commons/gitlab_ui.js @@ -1,6 +1,4 @@ import Vue from 'vue'; -import { GlLoadingIcon, GlTooltipDirective } from '@gitlab-org/gitlab-ui'; +import { GlLoadingIcon } from '@gitlab-org/gitlab-ui'; Vue.component('gl-loading-icon', GlLoadingIcon); - -Vue.directive('gl-tooltip', GlTooltipDirective); diff --git a/app/assets/javascripts/diffs/components/diff_gutter_avatars.vue b/app/assets/javascripts/diffs/components/diff_gutter_avatars.vue index 1b59777f901..254bc235691 100644 --- a/app/assets/javascripts/diffs/components/diff_gutter_avatars.vue +++ b/app/assets/javascripts/diffs/components/diff_gutter_avatars.vue @@ -3,6 +3,7 @@ import { mapActions } from 'vuex'; import Icon from '~/vue_shared/components/icon.vue'; import { pluralize, truncate } from '~/lib/utils/text_utility'; import UserAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue'; +import { GlTooltipDirective } from '@gitlab-org/gitlab-ui'; import { COUNT_OF_AVATARS_IN_GUTTER, LENGTH_OF_AVATAR_TOOLTIP } from '../constants'; export default { @@ -10,6 +11,9 @@ export default { Icon, UserAvatarImage, }, + directives: { + GlTooltip: GlTooltipDirective, + }, props: { discussions: { type: Array, diff --git a/app/assets/javascripts/diffs/components/diff_line_gutter_content.vue b/app/assets/javascripts/diffs/components/diff_line_gutter_content.vue index 6eff3013dcd..f4a9be19496 100644 --- a/app/assets/javascripts/diffs/components/diff_line_gutter_content.vue +++ b/app/assets/javascripts/diffs/components/diff_line_gutter_content.vue @@ -167,7 +167,7 @@ export default { <button v-if="shouldShowCommentButton" type="button" - class="add-diff-note js-add-diff-note-button" + class="add-diff-note js-add-diff-note-button qa-diff-comment" title="Add a comment to this line" @click="handleCommentButton" > diff --git a/app/assets/javascripts/diffs/components/inline_diff_table_row.vue b/app/assets/javascripts/diffs/components/inline_diff_table_row.vue index 62fa34e835a..542acd3d930 100644 --- a/app/assets/javascripts/diffs/components/inline_diff_table_row.vue +++ b/app/assets/javascripts/diffs/components/inline_diff_table_row.vue @@ -102,7 +102,7 @@ export default { :line-type="newLineType" :is-bottom="isBottom" :is-hover="isHover" - class="diff-line-num new_line" + class="diff-line-num new_line qa-new-diff-line" /> <td :class="line.type" diff --git a/app/assets/javascripts/dirty_submit/dirty_submit_form.js b/app/assets/javascripts/dirty_submit/dirty_submit_form.js index 5bea47f23c5..d8d0fa1fac4 100644 --- a/app/assets/javascripts/dirty_submit/dirty_submit_form.js +++ b/app/assets/javascripts/dirty_submit/dirty_submit_form.js @@ -31,7 +31,7 @@ class DirtySubmitForm { updateDirtyInput(event) { const input = event.target; - if (!input.dataset.dirtySubmitOriginalValue) return; + if (!input.dataset.isDirtySubmitInput) return; this.updateDirtyInputs(input); this.toggleSubmission(); @@ -65,6 +65,7 @@ class DirtySubmitForm { } static initInput(element) { + element.dataset.isDirtySubmitInput = true; element.dataset.dirtySubmitOriginalValue = DirtySubmitForm.inputCurrentValue(element); } diff --git a/app/assets/javascripts/environments/components/environment_actions.vue b/app/assets/javascripts/environments/components/environment_actions.vue index 2bc168a6b02..0a3ae384afa 100644 --- a/app/assets/javascripts/environments/components/environment_actions.vue +++ b/app/assets/javascripts/environments/components/environment_actions.vue @@ -1,4 +1,6 @@ <script> +import { s__, sprintf } from '~/locale'; +import { formatTime } from '~/lib/utils/datetime_utility'; import Icon from '~/vue_shared/components/icon.vue'; import eventHub from '../event_hub'; import tooltip from '../../vue_shared/directives/tooltip'; @@ -28,10 +30,24 @@ export default { }, }, methods: { - onClickAction(endpoint) { + onClickAction(action) { + if (action.scheduledAt) { + const confirmationMessage = sprintf( + s__( + "DelayedJobs|Are you sure you want to run %{jobName} immediately? Otherwise this job will run automatically after it's timer finishes.", + ), + { jobName: action.name }, + ); + // https://gitlab.com/gitlab-org/gitlab-ce/issues/52156 + // eslint-disable-next-line no-alert + if (!window.confirm(confirmationMessage)) { + return; + } + } + this.isLoading = true; - eventHub.$emit('postAction', { endpoint }); + eventHub.$emit('postAction', { endpoint: action.playPath }); }, isActionDisabled(action) { @@ -41,6 +57,11 @@ export default { return !action.playable; }, + + remainingTime(action) { + const remainingMilliseconds = new Date(action.scheduledAt).getTime() - Date.now(); + return formatTime(Math.max(0, remainingMilliseconds)); + }, }, }; </script> @@ -54,7 +75,7 @@ export default { :aria-label="title" :disabled="isLoading" type="button" - class="dropdown btn btn-default dropdown-new js-dropdown-play-icon-container" + class="dropdown btn btn-default dropdown-new js-environment-actions-dropdown" data-container="body" data-toggle="dropdown" > @@ -75,12 +96,19 @@ export default { :class="{ disabled: isActionDisabled(action) }" :disabled="isActionDisabled(action)" type="button" - class="js-manual-action-link no-btn btn" - @click="onClickAction(action.play_path)" + class="js-manual-action-link no-btn btn d-flex align-items-center" + @click="onClickAction(action)" > - <span> + <span class="flex-fill"> {{ action.name }} </span> + <span + v-if="action.scheduledAt" + class="text-secondary" + > + <icon name="clock" /> + {{ remainingTime(action) }} + </span> </button> </li> </ul> diff --git a/app/assets/javascripts/environments/components/environment_item.vue b/app/assets/javascripts/environments/components/environment_item.vue index b62a5bb1940..41f59447905 100644 --- a/app/assets/javascripts/environments/components/environment_item.vue +++ b/app/assets/javascripts/environments/components/environment_item.vue @@ -13,6 +13,7 @@ import TerminalButtonComponent from './environment_terminal_button.vue'; import MonitoringButtonComponent from './environment_monitoring.vue'; import CommitComponent from '../../vue_shared/components/commit.vue'; import eventHub from '../event_hub'; +import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; /** * Environment Item Component @@ -74,21 +75,6 @@ export default { }, /** - * Verifies is the given environment has manual actions. - * Used to verify if we should render them or nor. - * - * @returns {Boolean|Undefined} - */ - hasManualActions() { - return ( - this.model && - this.model.last_deployment && - this.model.last_deployment.manual_actions && - this.model.last_deployment.manual_actions.length > 0 - ); - }, - - /** * Checkes whether the environment is protected. * (`is_protected` currently only set in EE) * @@ -154,23 +140,20 @@ export default { return ''; }, - /** - * Returns the manual actions with the name parsed. - * - * @returns {Array.<Object>|Undefined} - */ - manualActions() { - if (this.hasManualActions) { - return this.model.last_deployment.manual_actions.map(action => { - const parsedAction = { - name: humanize(action.name), - play_path: action.play_path, - playable: action.playable, - }; - return parsedAction; - }); + actions() { + if (!this.model || !this.model.last_deployment || !this.canCreateDeployment) { + return []; } - return []; + + const { manualActions, scheduledActions } = convertObjectPropsToCamelCase( + this.model.last_deployment, + { deep: true }, + ); + const combinedActions = (manualActions || []).concat(scheduledActions || []); + return combinedActions.map(action => ({ + ...action, + name: humanize(action.name), + })); }, /** @@ -443,7 +426,7 @@ export default { displayEnvironmentActions() { return ( - this.hasManualActions || + this.actions.length > 0 || this.externalURL || this.monitoringUrl || this.canStopEnvironment || @@ -619,8 +602,8 @@ export default { /> <actions-component - v-if="hasManualActions && canCreateDeployment" - :actions="manualActions" + v-if="actions.length > 0" + :actions="actions" /> <terminal-button-component diff --git a/app/assets/javascripts/jobs/components/artifacts_block.vue b/app/assets/javascripts/jobs/components/artifacts_block.vue index 17fd5321642..93c89411b4a 100644 --- a/app/assets/javascripts/jobs/components/artifacts_block.vue +++ b/app/assets/javascripts/jobs/components/artifacts_block.vue @@ -1,10 +1,12 @@ <script> +import { GlLink } from '@gitlab-org/gitlab-ui'; import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; import timeagoMixin from '~/vue_shared/mixins/timeago'; export default { components: { TimeagoTooltip, + GlLink, }, mixins: [timeagoMixin], props: { @@ -53,16 +55,16 @@ export default { class="btn-group d-flex" role="group" > - <a + <gl-link v-if="artifact.keep_path" :href="artifact.keep_path" class="js-keep-artifacts btn btn-sm btn-default" data-method="post" > {{ s__('Job|Keep') }} - </a> + </gl-link> - <a + <gl-link v-if="artifact.download_path" :href="artifact.download_path" class="js-download-artifacts btn btn-sm btn-default" @@ -70,15 +72,15 @@ export default { rel="nofollow" > {{ s__('Job|Download') }} - </a> + </gl-link> - <a + <gl-link v-if="artifact.browse_path" :href="artifact.browse_path" class="js-browse-artifacts btn btn-sm btn-default" > {{ s__('Job|Browse') }} - </a> + </gl-link> </div> </div> </template> diff --git a/app/assets/javascripts/jobs/components/commit_block.vue b/app/assets/javascripts/jobs/components/commit_block.vue index 7d51f6afd10..06fe23fedce 100644 --- a/app/assets/javascripts/jobs/components/commit_block.vue +++ b/app/assets/javascripts/jobs/components/commit_block.vue @@ -1,9 +1,11 @@ <script> +import { GlLink } from '@gitlab-org/gitlab-ui'; import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; export default { components: { ClipboardButton, + GlLink, }, props: { commit: { @@ -31,10 +33,10 @@ export default { <p> {{ __('Commit') }} - <a + <gl-link :href="commit.commit_path" class="js-commit-sha commit-sha link-commit" - >{{ commit.short_id }}</a> + >{{ commit.short_id }}</gl-link> <clipboard-button :text="commit.short_id" @@ -42,11 +44,11 @@ export default { css-class="btn btn-clipboard btn-transparent" /> - <a + <gl-link v-if="mergeRequest" :href="mergeRequest.path" class="js-link-commit link-commit" - >!{{ mergeRequest.iid }}</a> + >!{{ mergeRequest.iid }}</gl-link> </p> <p class="build-light-text append-bottom-0"> diff --git a/app/assets/javascripts/jobs/components/empty_state.vue b/app/assets/javascripts/jobs/components/empty_state.vue index ee5ceb99b0a..be7425c2d25 100644 --- a/app/assets/javascripts/jobs/components/empty_state.vue +++ b/app/assets/javascripts/jobs/components/empty_state.vue @@ -1,5 +1,10 @@ <script> +import { GlLink } from '@gitlab-org/gitlab-ui'; + export default { + components: { + GlLink, + }, props: { illustrationPath: { type: String, @@ -62,13 +67,13 @@ export default { v-if="action" class="text-center" > - <a + <gl-link :href="action.path" :data-method="action.method" class="js-job-empty-state-action btn btn-primary" > {{ action.button_title }} - </a> + </gl-link> </div> </div> </div> diff --git a/app/assets/javascripts/jobs/components/erased_block.vue b/app/assets/javascripts/jobs/components/erased_block.vue index 5ffbfb6e19a..d80e905c68e 100644 --- a/app/assets/javascripts/jobs/components/erased_block.vue +++ b/app/assets/javascripts/jobs/components/erased_block.vue @@ -1,10 +1,12 @@ <script> import _ from 'underscore'; +import { GlLink } from '@gitlab-org/gitlab-ui'; import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; export default { components: { TimeagoTooltip, + GlLink, }, props: { user: { @@ -29,9 +31,9 @@ export default { <div class="erased alert alert-warning"> <template v-if="isErasedByUser"> {{ s__("Job|Job has been erased by") }} - <a :href="user.web_url"> + <gl-link :href="user.web_url"> {{ user.username }} - </a> + </gl-link> </template> <template v-else> {{ s__("Job|Job has been erased") }} diff --git a/app/assets/javascripts/jobs/components/job_app.vue b/app/assets/javascripts/jobs/components/job_app.vue index 3cabbfc6e27..6e95e3d16f8 100644 --- a/app/assets/javascripts/jobs/components/job_app.vue +++ b/app/assets/javascripts/jobs/components/job_app.vue @@ -1,6 +1,7 @@ <script> import _ from 'underscore'; import { mapGetters, mapState, mapActions } from 'vuex'; +import { GlLoadingIcon } from '@gitlab-org/gitlab-ui'; import { isScrolledToBottom } from '~/lib/utils/scroll_utils'; import bp from '~/breakpoints'; import CiHeader from '~/vue_shared/components/header_ci_component.vue'; @@ -23,6 +24,7 @@ export default { EmptyState, EnvironmentsBlock, ErasedBlock, + GlLoadingIcon, Log, LogTopBar, StuckBlock, diff --git a/app/assets/javascripts/jobs/components/job_container_item.vue b/app/assets/javascripts/jobs/components/job_container_item.vue index 6486b25c8a7..cdac8a391d1 100644 --- a/app/assets/javascripts/jobs/components/job_container_item.vue +++ b/app/assets/javascripts/jobs/components/job_container_item.vue @@ -1,15 +1,16 @@ <script> +import { GlTooltipDirective, GlLink } from '@gitlab-org/gitlab-ui'; import CiIcon from '~/vue_shared/components/ci_icon.vue'; import Icon from '~/vue_shared/components/icon.vue'; -import tooltip from '~/vue_shared/directives/tooltip'; export default { components: { CiIcon, Icon, + GlLink, }, directives: { - tooltip, + GlTooltip: GlTooltipDirective, }, props: { job: { @@ -37,11 +38,10 @@ export default { active: isActive }" > - <a - v-tooltip + <gl-link + v-gl-tooltip :href="job.status.details_path" :title="tooltipText" - data-container="body" data-boundary="viewport" class="js-job-link" > @@ -60,6 +60,6 @@ export default { name="retry" class="js-retry-icon" /> - </a> + </gl-link> </div> </template> diff --git a/app/assets/javascripts/jobs/components/job_log_controllers.vue b/app/assets/javascripts/jobs/components/job_log_controllers.vue index 94ab1b16c84..eeefa33264f 100644 --- a/app/assets/javascripts/jobs/components/job_log_controllers.vue +++ b/app/assets/javascripts/jobs/components/job_log_controllers.vue @@ -1,7 +1,7 @@ <script> +import { GlTooltipDirective, GlLink, GlButton } from '@gitlab-org/gitlab-ui'; import { polyfillSticky } from '~/lib/utils/sticky'; import Icon from '~/vue_shared/components/icon.vue'; -import tooltip from '~/vue_shared/directives/tooltip'; import { numberToHumanSize } from '~/lib/utils/number_utils'; import { sprintf } from '~/locale'; import scrollDown from '../svg/scroll_down.svg'; @@ -9,9 +9,11 @@ import scrollDown from '../svg/scroll_down.svg'; export default { components: { Icon, + GlLink, + GlButton, }, directives: { - tooltip, + GlTooltip: GlTooltipDirective, }, scrollDown, props: { @@ -73,76 +75,70 @@ export default { <template v-if="isTraceSizeVisible"> {{ jobLogSize }} - <a + <gl-link v-if="rawPath" :href="rawPath" class="js-raw-link raw-link" > {{ s__("Job|Complete Raw") }} - </a> + </gl-link> </template> </div> <!-- eo truncate information --> <div class="controllers float-right"> <!-- links --> - <a + <gl-link v-if="rawPath" - v-tooltip + v-gl-tooltip.body :title="s__('Job|Show complete raw')" :href="rawPath" class="js-raw-link-controller controllers-buttons" - data-container="body" > <icon name="doc-text" /> - </a> + </gl-link> - <a + <gl-link v-if="erasePath" - v-tooltip + v-gl-tooltip.body :title="s__('Job|Erase job log')" :href="erasePath" :data-confirm="__('Are you sure you want to erase this build?')" class="js-erase-link controllers-buttons" - data-container="body" data-method="post" > <icon name="remove" /> - </a> + </gl-link> <!-- eo links --> <!-- scroll buttons --> <div - v-tooltip + v-gl-tooltip :title="s__('Job|Scroll to top')" class="controllers-buttons" - data-container="body" > - <button + <gl-button :disabled="isScrollTopDisabled" type="button" class="js-scroll-top btn-scroll btn-transparent btn-blank" @click="handleScrollToTop" > - <icon name="scroll_up"/> - </button> + <icon name="scroll_up" /> + </gl-button> </div> <div - v-tooltip + v-gl-tooltip :title="s__('Job|Scroll to bottom')" class="controllers-buttons" - data-container="body" > - <button + <gl-button :disabled="isScrollBottomDisabled" - type="button" class="js-scroll-bottom btn-scroll btn-transparent btn-blank" :class="{ animate: isScrollingDown }" @click="handleScrollToBottom" v-html="$options.scrollDown" - > - </button> + /> </div> <!-- eo scroll buttons --> </div> diff --git a/app/assets/javascripts/jobs/components/sidebar_detail_row.vue b/app/assets/javascripts/jobs/components/sidebar_detail_row.vue index aeafe98a70b..cfedb38e17a 100644 --- a/app/assets/javascripts/jobs/components/sidebar_detail_row.vue +++ b/app/assets/javascripts/jobs/components/sidebar_detail_row.vue @@ -1,6 +1,11 @@ <script> +import { GlLink } from '@gitlab-org/gitlab-ui'; + export default { name: 'SidebarDetailRow', + components: { + GlLink, + }, props: { title: { type: String, @@ -41,7 +46,7 @@ export default { v-if="hasHelpURL" class="help-button float-right" > - <a + <gl-link :href="helpUrl" target="_blank" rel="noopener noreferrer nofollow" @@ -50,7 +55,7 @@ export default { class="fa fa-question-circle" aria-hidden="true" ></i> - </a> + </gl-link> </span> </p> </template> diff --git a/app/assets/javascripts/jobs/components/stuck_block.vue b/app/assets/javascripts/jobs/components/stuck_block.vue index 1d5789b175a..ca4bf471363 100644 --- a/app/assets/javascripts/jobs/components/stuck_block.vue +++ b/app/assets/javascripts/jobs/components/stuck_block.vue @@ -1,8 +1,12 @@ <script> +import { GlLink } from '@gitlab-org/gitlab-ui'; /** * Renders Stuck Runners block for job's view. */ export default { + components: { + GlLink, + }, props: { hasNoRunnersForProject: { type: Boolean, @@ -52,12 +56,12 @@ export default { </p> {{ __("Go to") }} - <a + <gl-link v-if="runnersPath" :href="runnersPath" class="js-runners-path" > {{ __("Runners page") }} - </a> + </gl-link> </div> </template> diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue index b980e43b898..554db102027 100644 --- a/app/assets/javascripts/notes/components/comment_form.vue +++ b/app/assets/javascripts/notes/components/comment_form.vue @@ -390,7 +390,7 @@ append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown" :disabled="isSubmitButtonDisabled" name="button" type="button" - class="btn comment-btn note-type-toggle js-note-new-discussion dropdown-toggle" + class="btn comment-btn note-type-toggle js-note-new-discussion dropdown-toggle qa-note-dropdown" data-display="static" data-toggle="dropdown" aria-label="Open comment type dropdown"> @@ -422,7 +422,7 @@ append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown" <li :class="{ 'droplab-item-selected': noteType === 'discussion' }"> <button type="button" - class="btn btn-transparent" + class="btn btn-transparent qa-discussion-option" @click.prevent="setNoteType('discussion')"> <i aria-hidden="true" diff --git a/app/assets/javascripts/notes/components/discussion_filter.vue b/app/assets/javascripts/notes/components/discussion_filter.vue index 6e8f43048d1..affa2d1b574 100644 --- a/app/assets/javascripts/notes/components/discussion_filter.vue +++ b/app/assets/javascripts/notes/components/discussion_filter.vue @@ -1,7 +1,8 @@ <script> import $ from 'jquery'; -import Icon from '~/vue_shared/components/icon.vue'; import { mapGetters, mapActions } from 'vuex'; +import Icon from '~/vue_shared/components/icon.vue'; +import { DISCUSSION_FILTERS_DEFAULT_VALUE, HISTORY_ONLY_FILTER_VALUE } from '../constants'; export default { components: { @@ -12,14 +13,17 @@ export default { type: Array, required: true, }, - defaultValue: { + selectedValue: { type: Number, default: null, required: false, }, }, data() { - return { currentValue: this.defaultValue }; + return { + currentValue: this.selectedValue, + defaultValue: DISCUSSION_FILTERS_DEFAULT_VALUE, + }; }, computed: { ...mapGetters(['getNotesDataByProp']), @@ -28,8 +32,11 @@ export default { return this.filters.find(filter => filter.value === this.currentValue); }, }, + mounted() { + this.toggleCommentsForm(); + }, methods: { - ...mapActions(['filterDiscussion']), + ...mapActions(['filterDiscussion', 'setCommentsDisabled']), selectFilter(value) { const filter = parseInt(value, 10); @@ -39,6 +46,10 @@ export default { if (filter === this.currentValue) return; this.currentValue = filter; this.filterDiscussion({ path: this.getNotesDataByProp('discussionsPath'), filter }); + this.toggleCommentsForm(); + }, + toggleCommentsForm() { + this.setCommentsDisabled(this.currentValue === HISTORY_ONLY_FILTER_VALUE); }, }, }; @@ -73,6 +84,10 @@ export default { > {{ filter.title }} </button> + <div + v-if="filter.value === defaultValue" + class="dropdown-divider" + ></div> </li> </ul> </div> diff --git a/app/assets/javascripts/notes/components/note_form.vue b/app/assets/javascripts/notes/components/note_form.vue index 38c43e5fe08..31ee8fed984 100644 --- a/app/assets/javascripts/notes/components/note_form.vue +++ b/app/assets/javascripts/notes/components/note_form.vue @@ -187,7 +187,7 @@ export default { :data-supports-quick-actions="!isEditing" name="note[note]" class="note-textarea js-gfm-input js-note-text -js-autosize markdown-area js-vue-issue-note-form js-vue-textarea" +js-autosize markdown-area js-vue-issue-note-form js-vue-textarea qa-reply-input" aria-label="Description" placeholder="Write a comment or drag your files hereā¦" @keydown.meta.enter="handleUpdate()" diff --git a/app/assets/javascripts/notes/components/noteable_discussion.vue b/app/assets/javascripts/notes/components/noteable_discussion.vue index c5fdfa1d47c..6293dd5b7e1 100644 --- a/app/assets/javascripts/notes/components/noteable_discussion.vue +++ b/app/assets/javascripts/notes/components/noteable_discussion.vue @@ -369,7 +369,7 @@ Please check your network connection and try again.`; role="group"> <button type="button" - class="js-vue-discussion-reply btn btn-text-field mr-2" + class="js-vue-discussion-reply btn btn-text-field mr-2 qa-discussion-reply" title="Add a reply" @click="showReplyForm">Reply...</button> </div> diff --git a/app/assets/javascripts/notes/components/notes_app.vue b/app/assets/javascripts/notes/components/notes_app.vue index 7514ce8a1eb..ed5ac112dc0 100644 --- a/app/assets/javascripts/notes/components/notes_app.vue +++ b/app/assets/javascripts/notes/components/notes_app.vue @@ -60,6 +60,7 @@ export default { 'getNotesDataByProp', 'discussionCount', 'isLoading', + 'commentsDisabled', ]), noteableType() { return this.noteableData.noteableType; @@ -206,6 +207,7 @@ export default { </ul> <comment-form + v-if="!commentsDisabled" :noteable-type="noteableType" :markdown-version="markdownVersion" /> diff --git a/app/assets/javascripts/notes/constants.js b/app/assets/javascripts/notes/constants.js index 2c3e07c0506..3147dc64c27 100644 --- a/app/assets/javascripts/notes/constants.js +++ b/app/assets/javascripts/notes/constants.js @@ -15,6 +15,8 @@ export const MERGE_REQUEST_NOTEABLE_TYPE = 'MergeRequest'; export const UNRESOLVE_NOTE_METHOD_NAME = 'delete'; export const RESOLVE_NOTE_METHOD_NAME = 'post'; export const DESCRIPTION_TYPE = 'changed the description'; +export const HISTORY_ONLY_FILTER_VALUE = 2; +export const DISCUSSION_FILTERS_DEFAULT_VALUE = 0; export const NOTEABLE_TYPE_MAPPING = { Issue: ISSUE_NOTEABLE_TYPE, diff --git a/app/assets/javascripts/notes/discussion_filters.js b/app/assets/javascripts/notes/discussion_filters.js index 06eadaeea0e..5c5f38a3fb0 100644 --- a/app/assets/javascripts/notes/discussion_filters.js +++ b/app/assets/javascripts/notes/discussion_filters.js @@ -6,7 +6,7 @@ export default store => { if (discussionFilterEl) { const { defaultFilter, notesFilters } = discussionFilterEl.dataset; - const defaultValue = defaultFilter ? parseInt(defaultFilter, 10) : null; + const selectedValue = defaultFilter ? parseInt(defaultFilter, 10) : null; const filterValues = notesFilters ? JSON.parse(notesFilters) : {}; const filters = Object.keys(filterValues).map(entry => ({ title: entry, @@ -24,7 +24,7 @@ export default store => { return createElement('discussion-filter', { props: { filters, - defaultValue, + selectedValue, }, }); }, diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js index b5dd49bc6c9..88739ffb083 100644 --- a/app/assets/javascripts/notes/stores/actions.js +++ b/app/assets/javascripts/notes/stores/actions.js @@ -364,5 +364,9 @@ export const filterDiscussion = ({ dispatch }, { path, filter }) => { }); }; +export const setCommentsDisabled = ({ commit }, data) => { + commit(types.DISABLE_COMMENTS, data); +}; + // prevent babel-plugin-rewire from generating an invalid default during karma tests export default () => {}; diff --git a/app/assets/javascripts/notes/stores/getters.js b/app/assets/javascripts/notes/stores/getters.js index e4f36154fcd..8df95c279eb 100644 --- a/app/assets/javascripts/notes/stores/getters.js +++ b/app/assets/javascripts/notes/stores/getters.js @@ -192,5 +192,7 @@ export const firstUnresolvedDiscussionId = (state, getters) => diffOrder => { return getters.unresolvedDiscussionsIdsByDate[0]; }; +export const commentsDisabled = state => state.commentsDisabled; + // prevent babel-plugin-rewire from generating an invalid default during karma tests export default () => {}; diff --git a/app/assets/javascripts/notes/stores/modules/index.js b/app/assets/javascripts/notes/stores/modules/index.js index 400142668ea..8aea269ea7d 100644 --- a/app/assets/javascripts/notes/stores/modules/index.js +++ b/app/assets/javascripts/notes/stores/modules/index.js @@ -21,6 +21,7 @@ export default () => ({ noteableData: { current_user: {}, }, + commentsDisabled: false, }, actions, getters, diff --git a/app/assets/javascripts/notes/stores/mutation_types.js b/app/assets/javascripts/notes/stores/mutation_types.js index 2fa53aef1d4..dfbf3b7b34b 100644 --- a/app/assets/javascripts/notes/stores/mutation_types.js +++ b/app/assets/javascripts/notes/stores/mutation_types.js @@ -15,6 +15,7 @@ export const UPDATE_DISCUSSION = 'UPDATE_DISCUSSION'; export const SET_DISCUSSION_DIFF_LINES = 'SET_DISCUSSION_DIFF_LINES'; export const SET_NOTES_FETCHED_STATE = 'SET_NOTES_FETCHED_STATE'; export const SET_NOTES_LOADING_STATE = 'SET_NOTES_LOADING_STATE'; +export const DISABLE_COMMENTS = 'DISABLE_COMMENTS'; // DISCUSSION export const COLLAPSE_DISCUSSION = 'COLLAPSE_DISCUSSION'; diff --git a/app/assets/javascripts/notes/stores/mutations.js b/app/assets/javascripts/notes/stores/mutations.js index 65085452139..c8d9e196103 100644 --- a/app/assets/javascripts/notes/stores/mutations.js +++ b/app/assets/javascripts/notes/stores/mutations.js @@ -225,4 +225,8 @@ export default { discussion.truncated_diff_lines = diffLines; }, + + [types.DISABLE_COMMENTS](state, value) { + state.commentsDisabled = value; + }, }; diff --git a/app/assets/javascripts/pipelines/components/pipelines_actions.vue b/app/assets/javascripts/pipelines/components/pipelines_actions.vue index a7507fb3b6f..07a4af3e61e 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_actions.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_actions.vue @@ -29,7 +29,7 @@ export default { if (action.scheduled_at) { const confirmationMessage = sprintf( s__( - "DelayedJobs|Are you sure you want to run %{jobName} immediately? This job will run automatically after it's timer finishes.", + "DelayedJobs|Are you sure you want to run %{jobName} immediately? Otherwise this job will run automatically after it's timer finishes.", ), { jobName: action.name }, ); diff --git a/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue b/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue index 8950ae31627..4d461baf74d 100644 --- a/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue +++ b/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue @@ -5,7 +5,7 @@ import Icon from '~/vue_shared/components/icon.vue'; import GfmAutoComplete from '~/gfm_auto_complete'; import { __, s__ } from '~/locale'; import Api from '~/api'; -import { GlModal } from '@gitlab-org/gitlab-ui'; +import { GlModal, GlTooltipDirective } from '@gitlab-org/gitlab-ui'; import eventHub from './event_hub'; import EmojiMenuInModal from './emoji_menu_in_modal'; @@ -16,6 +16,9 @@ export default { Icon, GlModal, }, + directives: { + GlTooltip: GlTooltipDirective, + }, props: { currentEmoji: { type: String, diff --git a/app/assets/javascripts/vue_shared/components/gl_countdown.vue b/app/assets/javascripts/vue_shared/components/gl_countdown.vue index 9327a2a4a6c..a35986b2d03 100644 --- a/app/assets/javascripts/vue_shared/components/gl_countdown.vue +++ b/app/assets/javascripts/vue_shared/components/gl_countdown.vue @@ -1,10 +1,14 @@ <script> import { calculateRemainingMilliseconds, formatTime } from '~/lib/utils/datetime_utility'; +import { GlTooltipDirective } from '@gitlab-org/gitlab-ui'; /** * Counts down to a given end date. */ export default { + directives: { + GlTooltip: GlTooltipDirective, + }, props: { endDateString: { type: String, diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss index f26b1fddae5..43b7c26b272 100644 --- a/app/assets/stylesheets/framework/blocks.scss +++ b/app/assets/stylesheets/framework/blocks.scss @@ -348,6 +348,7 @@ @include media-breakpoint-down(xs) { width: 100%; + margin: $btn-side-margin 0; } } } diff --git a/app/assets/stylesheets/framework/contextual_sidebar.scss b/app/assets/stylesheets/framework/contextual_sidebar.scss index 2e7f25d975e..6f103e4e89a 100644 --- a/app/assets/stylesheets/framework/contextual_sidebar.scss +++ b/app/assets/stylesheets/framework/contextual_sidebar.scss @@ -322,15 +322,15 @@ width: $contextual-sidebar-width - 1px; transition: width $sidebar-transition-duration; position: fixed; + height: $toggle-sidebar-height; bottom: 0; - padding: $gl-padding; + padding: 0 $gl-padding; background-color: $gray-light; border: 0; border-top: 1px solid $border-color; color: $gl-text-color-secondary; display: flex; align-items: center; - line-height: 1; svg { margin-right: 8px; diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss index c030d75f5a4..9837b1a6bd0 100644 --- a/app/assets/stylesheets/framework/mixins.scss +++ b/app/assets/stylesheets/framework/mixins.scss @@ -291,7 +291,7 @@ /* * Mixin that handles the position of the controls placed on the top bar */ -@mixin build-controllers($control-font-size, $flex-direction, $with-grow, $flex-grow-size) { +@mixin build-controllers($control-font-size, $flex-direction, $with-grow, $flex-grow-size, $svg-display: 'block', $svg-top: '2px') { display: flex; font-size: $control-font-size; justify-content: $flex-direction; @@ -304,8 +304,9 @@ svg { width: 15px; height: 15px; - display: block; + display: $svg-display; fill: $gl-text-color; + top: $svg-top; } .controllers-buttons { diff --git a/app/assets/stylesheets/framework/tables.scss b/app/assets/stylesheets/framework/tables.scss index 339388392df..6954e6599b1 100644 --- a/app/assets/stylesheets/framework/tables.scss +++ b/app/assets/stylesheets/framework/tables.scss @@ -147,3 +147,9 @@ table { } } } + +.top-area + .content-list { + th { + border-top: 0; + } +} diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index ad66a0365ed..19eee4e4aba 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -10,6 +10,7 @@ $sidebar-breakpoint: 1024px; $default-transition-duration: 0.15s; $contextual-sidebar-width: 220px; $contextual-sidebar-collapsed-width: 50px; +$toggle-sidebar-height: 48px; /* * Color schema diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss index 31b258e56dd..1449723de52 100644 --- a/app/assets/stylesheets/pages/builds.scss +++ b/app/assets/stylesheets/pages/builds.scss @@ -94,7 +94,7 @@ } .controllers { - @include build-controllers(15px, center, false, 0); + @include build-controllers(15px, center, false, 0, inline, 0); } } diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss index 19a36061c45..347fcad771a 100644 --- a/app/assets/stylesheets/pages/environments.scss +++ b/app/assets/stylesheets/pages/environments.scss @@ -44,11 +44,6 @@ margin: 0; } - .icon-play { - height: 13px; - width: 12px; - } - .external-url, .dropdown-new { color: $gl-text-color-secondary; @@ -366,7 +361,7 @@ } .arrow-shadow { - content: ""; + content: ''; position: absolute; width: 7px; height: 7px; diff --git a/app/assets/stylesheets/pages/pipeline_schedules.scss b/app/assets/stylesheets/pages/pipeline_schedules.scss index 86e70955389..617b3db2fae 100644 --- a/app/assets/stylesheets/pages/pipeline_schedules.scss +++ b/app/assets/stylesheets/pages/pipeline_schedules.scss @@ -39,10 +39,6 @@ white-space: nowrap; overflow: hidden; text-overflow: ellipsis; - - svg { - vertical-align: middle; - } } .next-run-cell { @@ -52,6 +48,10 @@ a { color: $text-color; } + + svg { + vertical-align: middle; + } } .pipeline-schedules-user-callout { diff --git a/app/finders/personal_access_tokens_finder.rb b/app/finders/personal_access_tokens_finder.rb index 81fd3b7a547..bd95dcd323f 100644 --- a/app/finders/personal_access_tokens_finder.rb +++ b/app/finders/personal_access_tokens_finder.rb @@ -3,7 +3,7 @@ class PersonalAccessTokensFinder attr_accessor :params - delegate :build, :find, :find_by, :find_by_token, to: :execute + delegate :build, :find, :find_by_id, :find_by_token, to: :execute def initialize(params = {}) @params = params diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb index 0c9f69b6714..9a1c2a4c9e1 100644 --- a/app/helpers/application_settings_helper.rb +++ b/app/helpers/application_settings_helper.rb @@ -115,6 +115,7 @@ module ApplicationSettingsHelper :akismet_api_key, :akismet_enabled, :allow_local_requests_from_hooks_and_services, + :archive_builds_in_human_readable, :authorized_keys_enabled, :auto_devops_enabled, :auto_devops_domain, diff --git a/app/helpers/icons_helper.rb b/app/helpers/icons_helper.rb index 910c9e9446f..b0f63de2fb8 100644 --- a/app/helpers/icons_helper.rb +++ b/app/helpers/icons_helper.rb @@ -109,6 +109,8 @@ module IconsHelper def file_type_icon_class(type, mode, name) if type == 'folder' icon_class = 'folder' + elsif type == 'archive' + icon_class = 'archive' elsif mode == '120000' icon_class = 'share' else diff --git a/app/helpers/tree_helper.rb b/app/helpers/tree_helper.rb index 6d2da5699fb..78a11616d4c 100644 --- a/app/helpers/tree_helper.rb +++ b/app/helpers/tree_helper.rb @@ -31,11 +31,21 @@ module TreeHelper # mode - File unix mode # name - File name def tree_icon(type, mode, name) - icon("#{file_type_icon_class(type, mode, name)} fw") + icon([file_type_icon_class(type, mode, name), 'fw']) end - def tree_hex_class(content) - "file_#{hexdigest(content.name)}" + # Using Rails `*_path` methods can be slow, especially when generating + # many paths, as with a repository tree that has thousands of items. + def fast_project_blob_path(project, blob_path) + Addressable::URI.escape( + File.join(relative_url_root, project.path_with_namespace, 'blob', blob_path) + ) + end + + def fast_project_tree_path(project, tree_path) + Addressable::URI.escape( + File.join(relative_url_root, project.path_with_namespace, 'tree', tree_path) + ) end # Simple shortcut to File.join @@ -142,4 +152,8 @@ module TreeHelper def selected_branch @branch_name || tree_edit_branch end + + def relative_url_root + Gitlab.config.gitlab.relative_url_root.presence || '/' + end end diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index b66ec0ffab6..704310f53f0 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -5,6 +5,7 @@ class ApplicationSetting < ActiveRecord::Base include CacheMarkdownField include TokenAuthenticatable include IgnorableColumn + include ChronicDurationAttribute add_authentication_token_field :runners_registration_token add_authentication_token_field :health_check_access_token @@ -45,6 +46,8 @@ class ApplicationSetting < ActiveRecord::Base default_value_for :id, 1 + chronic_duration_attr_writer :archive_builds_in_human_readable, :archive_builds_in_seconds + validates :uuid, presence: true validates :session_expire_delay, @@ -184,6 +187,10 @@ class ApplicationSetting < ActiveRecord::Base validates :user_default_internal_regex, js_regex: true, allow_nil: true + validates :archive_builds_in_seconds, + allow_nil: true, + numericality: { only_integer: true, greater_than_or_equal_to: 1.day.seconds } + SUPPORTED_KEY_TYPES.each do |type| validates :"#{type}_key_restriction", presence: true, key_restriction: { type: type } end @@ -441,6 +448,10 @@ class ApplicationSetting < ActiveRecord::Base latest_terms end + def archive_builds_older_than + archive_builds_in_seconds.seconds.ago if archive_builds_in_seconds + end + private def ensure_uuid! diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index bb5d52fc78d..d7eab57763e 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -245,17 +245,37 @@ module Ci .fabricate! end - def other_actions + def other_manual_actions pipeline.manual_actions.where.not(name: name) end + def other_scheduled_actions + pipeline.scheduled_actions.where.not(name: name) + end + def pages_generator? Gitlab.config.pages.enabled && self.name == 'pages' end + # degenerated build is one that cannot be run by Runner + def degenerated? + self.options.nil? + end + + def degenerate! + self.update!(options: nil, yaml_variables: nil, commands: nil) + end + + def archived? + return true if degenerated? + + archive_builds_older_than = Gitlab::CurrentSettings.current_application_settings.archive_builds_older_than + archive_builds_older_than.present? && created_at < archive_builds_older_than + end + def playable? - action? && (manual? || scheduled? || retryable?) + action? && !archived? && (manual? || scheduled? || retryable?) end def schedulable? @@ -283,7 +303,7 @@ module Ci end def retryable? - success? || failed? || canceled? + !archived? && (success? || failed? || canceled?) end def retries_count @@ -291,7 +311,7 @@ module Ci end def retries_max - self.options.fetch(:retry, 0).to_i + self.options.to_h.fetch(:retry, 0).to_i end def latest? diff --git a/app/models/ci/job_artifact.rb b/app/models/ci/job_artifact.rb index 34a889057ab..11c88200c37 100644 --- a/app/models/ci/job_artifact.rb +++ b/app/models/ci/job_artifact.rb @@ -15,7 +15,7 @@ module Ci metadata: nil, trace: nil, junit: 'junit.xml', - codequality: 'codequality.json', + codequality: 'gl-code-quality-report.json', sast: 'gl-sast-report.json', dependency_scanning: 'gl-dependency-scanning-report.json', container_scanning: 'gl-container-scanning-report.json', diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index aeee7f0a5d2..0daf2419b67 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -238,6 +238,10 @@ module Ci end end + def self.latest_successful_ids_per_project + success.group(:project_id).select('max(id) as id') + end + def self.truncate_sha(sha) sha[0...8] end diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index 95c88e11a6e..755f8bd4d06 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -51,7 +51,8 @@ class CommitStatus < ActiveRecord::Base missing_dependency_failure: 5, runner_unsupported: 6, stale_schedule: 7, - job_execution_timeout: 8 + job_execution_timeout: 8, + archived_failure: 9 } ## @@ -167,16 +168,18 @@ class CommitStatus < ActiveRecord::Base false end - # To be overridden when inherrited from def retryable? false end - # To be overridden when inherrited from def cancelable? false end + def archived? + false + end + def stuck? false end diff --git a/app/models/deploy_token.rb b/app/models/deploy_token.rb index 0b2eedf3631..e3524305346 100644 --- a/app/models/deploy_token.rb +++ b/app/models/deploy_token.rb @@ -4,6 +4,7 @@ class DeployToken < ActiveRecord::Base include Expirable include TokenAuthenticatable include PolicyActor + include Gitlab::Utils::StrongMemoize add_authentication_token_field :token AVAILABLE_SCOPES = %i(read_repository read_registry).freeze @@ -49,7 +50,9 @@ class DeployToken < ActiveRecord::Base # to a single project, later we're going to extend # that to be for multiple projects and namespaces. def project - projects.first + strong_memoize(:project) do + projects.first + end end def expires_at diff --git a/app/models/deployment.rb b/app/models/deployment.rb index ee5b96e7454..37efbb04fce 100644 --- a/app/models/deployment.rb +++ b/app/models/deployment.rb @@ -55,7 +55,11 @@ class Deployment < ActiveRecord::Base end def manual_actions - @manual_actions ||= deployable.try(:other_actions) + @manual_actions ||= deployable.try(:other_manual_actions) + end + + def scheduled_actions + @scheduled_actions ||= deployable.try(:other_scheduled_actions) end def includes_commit?(commit) diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 7eef08aa6a3..735d9fba966 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -353,6 +353,15 @@ class MergeRequest < ActiveRecord::Base end end + # Returns true if there are commits that match at least one commit SHA. + def includes_any_commits?(shas) + if persisted? + merge_request_diff.commits_by_shas(shas).exists? + else + (commit_shas & shas).present? + end + end + # Calls `MergeWorker` to proceed with the merge process and # updates `merge_jid` with the MergeWorker#jid. # This helps tracking enqueued and ongoing merge jobs. diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb index 02c6b650f33..bb6ff8921df 100644 --- a/app/models/merge_request_diff.rb +++ b/app/models/merge_request_diff.rb @@ -140,6 +140,12 @@ class MergeRequestDiff < ActiveRecord::Base merge_request_diff_commits.map(&:sha) end + def commits_by_shas(shas) + return [] unless shas.present? + + merge_request_diff_commits.where(sha: shas) + end + def diff_refs=(new_diff_refs) self.base_commit_sha = new_diff_refs&.base_sha self.start_commit_sha = new_diff_refs&.start_sha diff --git a/app/models/namespace.rb b/app/models/namespace.rb index 74d48d0a9af..4a6627d3ca1 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -232,6 +232,12 @@ class Namespace < ActiveRecord::Base Project.inside_path(full_path) end + # Includes pipelines from this namespace and pipelines from all subgroups + # that belongs to this namespace + def all_pipelines + Ci::Pipeline.where(project: all_projects) + end + def has_parent? parent.present? end diff --git a/app/models/note.rb b/app/models/note.rb index 990689a95f5..592efb714f3 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -117,6 +117,8 @@ class Note < ActiveRecord::Base case notes_filter when UserPreference::NOTES_FILTERS[:only_comments] user + when UserPreference::NOTES_FILTERS[:only_activity] + system else all end diff --git a/app/models/pool_repository.rb b/app/models/pool_repository.rb new file mode 100644 index 00000000000..8ef74539209 --- /dev/null +++ b/app/models/pool_repository.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +class PoolRepository < ActiveRecord::Base + POOL_PREFIX = '@pools' + + belongs_to :shard + validates :shard, presence: true + + # For now, only pool repositories are tracked in the database. However, we may + # want to add other repository types in the future + self.table_name = 'repositories' + + has_many :pool_member_projects, class_name: 'Project', foreign_key: :pool_repository_id + + def shard_name + shard&.name + end + + def shard_name=(name) + self.shard = Shard.by_name(name) + end +end diff --git a/app/models/postgresql/replication_slot.rb b/app/models/postgresql/replication_slot.rb index 70c7432e6b5..e264fe88e47 100644 --- a/app/models/postgresql/replication_slot.rb +++ b/app/models/postgresql/replication_slot.rb @@ -4,6 +4,15 @@ module Postgresql class ReplicationSlot < ActiveRecord::Base self.table_name = 'pg_replication_slots' + # Returns true if there are any replication slots in use. + # PostgreSQL-compatible databases such as Aurora don't support + # replication slots, so this will return false as well. + def self.in_use? + transaction { exists? } + rescue ActiveRecord::StatementInvalid + false + end + # Returns true if the lag observed across all replication slots exceeds a # given threshold. # @@ -11,6 +20,8 @@ module Postgresql # statistics it takes between 1 and 5 seconds to replicate around # 100 MB of data. def self.lag_too_great?(max = 100.megabytes) + return false unless in_use? + lag_function = "#{Gitlab::Database.pg_wal_lsn_diff}" \ "(#{Gitlab::Database.pg_current_wal_insert_lsn}(), restart_lsn)::bigint" diff --git a/app/models/project.rb b/app/models/project.rb index fa995b5b061..d3b148d0ac0 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -95,8 +95,7 @@ class Project < ActiveRecord::Base unless: :ci_cd_settings, if: proc { ProjectCiCdSetting.available? } - after_create :set_last_activity_at - after_create :set_last_repository_updated_at + after_create :set_timestamps_for_create after_update :update_forks_visibility_level before_destroy :remove_private_deploy_keys @@ -124,6 +123,7 @@ class Project < ActiveRecord::Base alias_attribute :title, :name # Relations + belongs_to :pool_repository belongs_to :creator, class_name: 'User' belongs_to :group, -> { where(type: 'Group') }, foreign_key: 'namespace_id' belongs_to :namespace @@ -2102,13 +2102,8 @@ class Project < ActiveRecord::Base gitlab_shell.exists?(repository_storage, "#{disk_path}.git") end - # set last_activity_at to the same as created_at - def set_last_activity_at - update_column(:last_activity_at, self.created_at) - end - - def set_last_repository_updated_at - update_column(:last_repository_updated_at, self.created_at) + def set_timestamps_for_create + update_columns(last_activity_at: self.created_at, last_repository_updated_at: self.created_at) end def cross_namespace_reference?(from) diff --git a/app/models/shard.rb b/app/models/shard.rb new file mode 100644 index 00000000000..2fa22bd040c --- /dev/null +++ b/app/models/shard.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +class Shard < ActiveRecord::Base + # Store shard names from the configuration file in the database. This is not a + # list of active shards - we just want to assign an immutable, unique ID to + # every shard name for easy indexing / referencing. + def self.populate! + return unless table_exists? + + # The GitLab config does not change for the lifecycle of the process + in_config = Gitlab.config.repositories.storages.keys.map(&:to_s) + + transaction do + in_db = all.pluck(:name) + missing = in_config - in_db + + missing.map { |name| by_name(name) } + end + end + + def self.by_name(name) + find_or_create_by(name: name) + rescue ActiveRecord::RecordNotUnique + retry + end +end diff --git a/app/models/user.rb b/app/models/user.rb index d3eb7162174..039a3854edb 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -460,12 +460,6 @@ class User < ActiveRecord::Base by_username(username).take! end - def find_by_personal_access_token(token_string) - return unless token_string - - PersonalAccessTokensFinder.new(state: 'active').find_by_token(token_string)&.user # rubocop: disable CodeReuse/Finder - end - # Returns a user for the given SSH key. def find_by_ssh_key_id(key_id) Key.find_by(id: key_id)&.user diff --git a/app/models/user_preference.rb b/app/models/user_preference.rb index 6cd91abc261..32d0407800f 100644 --- a/app/models/user_preference.rb +++ b/app/models/user_preference.rb @@ -4,7 +4,7 @@ class UserPreference < ActiveRecord::Base # We could use enums, but Rails 4 doesn't support multiple # enum options with same name for multiple fields, also it creates # extra methods that aren't really needed here. - NOTES_FILTERS = { all_notes: 0, only_comments: 1 }.freeze + NOTES_FILTERS = { all_notes: 0, only_comments: 1, only_activity: 2 }.freeze belongs_to :user @@ -14,7 +14,8 @@ class UserPreference < ActiveRecord::Base def notes_filters { s_('Notes|Show all activity') => NOTES_FILTERS[:all_notes], - s_('Notes|Show comments only') => NOTES_FILTERS[:only_comments] + s_('Notes|Show comments only') => NOTES_FILTERS[:only_comments], + s_('Notes|Show history only') => NOTES_FILTERS[:only_activity] } end end diff --git a/app/policies/ci/build_policy.rb b/app/policies/ci/build_policy.rb index 3858b29c82c..0ca3e696f46 100644 --- a/app/policies/ci/build_policy.rb +++ b/app/policies/ci/build_policy.rb @@ -20,12 +20,17 @@ module Ci @subject.project.branch_allows_collaboration?(@user, @subject.ref) end + condition(:archived, scope: :subject) do + @subject.archived? + end + condition(:terminal, scope: :subject) do @subject.has_terminal? end - rule { protected_ref }.policy do + rule { protected_ref | archived }.policy do prevent :update_build + prevent :update_commit_status prevent :erase_build end diff --git a/app/policies/deployment_policy.rb b/app/policies/deployment_policy.rb index 56ac898b6ab..d4f2f3c52b1 100644 --- a/app/policies/deployment_policy.rb +++ b/app/policies/deployment_policy.rb @@ -2,4 +2,13 @@ class DeploymentPolicy < BasePolicy delegate { @subject.project } + + condition(:can_retry_deployable) do + can?(:update_build, @subject.deployable) + end + + rule { ~can_retry_deployable }.policy do + prevent :create_deployment + prevent :update_deployment + end end diff --git a/app/presenters/commit_status_presenter.rb b/app/presenters/commit_status_presenter.rb index a866e76df5a..0cd77da6303 100644 --- a/app/presenters/commit_status_presenter.rb +++ b/app/presenters/commit_status_presenter.rb @@ -10,7 +10,8 @@ class CommitStatusPresenter < Gitlab::View::Presenter::Delegated missing_dependency_failure: 'There has been a missing dependency failure', runner_unsupported: 'Your runner is outdated, please upgrade your runner', stale_schedule: 'Delayed job could not be executed by some reason, please try again', - job_execution_timeout: 'The script exceeded the maximum execution time set for the job' + job_execution_timeout: 'The script exceeded the maximum execution time set for the job', + archived_failure: 'The job is archived and cannot be run' }.freeze private_constant :CALLOUT_FAILURE_MESSAGES @@ -30,6 +31,6 @@ class CommitStatusPresenter < Gitlab::View::Presenter::Delegated end def unrecoverable? - script_failure? || missing_dependency_failure? + script_failure? || missing_dependency_failure? || archived_failure? end end diff --git a/app/serializers/deployment_entity.rb b/app/serializers/deployment_entity.rb index 344148a1fb7..aa1d9e6292c 100644 --- a/app/serializers/deployment_entity.rb +++ b/app/serializers/deployment_entity.rb @@ -25,4 +25,5 @@ class DeploymentEntity < Grape::Entity expose :commit, using: CommitEntity expose :deployable, using: JobEntity expose :manual_actions, using: JobEntity + expose :scheduled_actions, using: JobEntity end diff --git a/app/serializers/job_entity.rb b/app/serializers/job_entity.rb index aebbc18e32f..d0099ae77f2 100644 --- a/app/serializers/job_entity.rb +++ b/app/serializers/job_entity.rb @@ -7,6 +7,7 @@ class JobEntity < Grape::Entity expose :name expose :started?, as: :started + expose :archived?, as: :archived expose :build_path do |build| build_path(build) diff --git a/app/serializers/user_preference_entity.rb b/app/serializers/user_preference_entity.rb index fbdaab459b3..b99f80424db 100644 --- a/app/serializers/user_preference_entity.rb +++ b/app/serializers/user_preference_entity.rb @@ -7,4 +7,8 @@ class UserPreferenceEntity < Grape::Entity expose :notes_filters do |user_preference| UserPreference.notes_filters end + + expose :default_notes_filter do |user_preference| + UserPreference::NOTES_FILTERS[:all_notes] + end end diff --git a/app/services/ci/register_job_service.rb b/app/services/ci/register_job_service.rb index 5a7be921389..e06f1c05843 100644 --- a/app/services/ci/register_job_service.rb +++ b/app/services/ci/register_job_service.rb @@ -82,6 +82,11 @@ module Ci return false end + if build.archived? + build.drop!(:archived_failure) + return false + end + build.run! true end diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb index f01872b205e..53768ff2cbe 100644 --- a/app/services/merge_requests/refresh_service.rb +++ b/app/services/merge_requests/refresh_service.rb @@ -87,11 +87,8 @@ module MergeRequests filter_merge_requests(merge_requests).each do |merge_request| if branch_and_project_match?(merge_request) || @push.force_push? merge_request.reload_diff(current_user) - else - mr_commit_ids = merge_request.commit_shas - push_commit_ids = @commits.map(&:id) - matches = mr_commit_ids & push_commit_ids - merge_request.reload_diff(current_user) if matches.any? + elsif merge_request.includes_any_commits?(push_commit_ids) + merge_request.reload_diff(current_user) end merge_request.mark_as_unchecked @@ -104,6 +101,10 @@ module MergeRequests end # rubocop: enable CodeReuse/ActiveRecord + def push_commit_ids + @push_commit_ids ||= @commits.map(&:id) + end + def branch_and_project_match?(merge_request) merge_request.source_project == @project && merge_request.source_branch == @push.branch_name diff --git a/app/views/admin/application_settings/_ci_cd.html.haml b/app/views/admin/application_settings/_ci_cd.html.haml index 97be658cd34..adb496495d1 100644 --- a/app/views/admin/application_settings/_ci_cd.html.haml +++ b/app/views/admin/application_settings/_ci_cd.html.haml @@ -41,5 +41,13 @@ The default unit is in seconds, but you can define an alternative. For example: <code>4 mins 2 sec</code>, <code>2h42min</code>. = link_to icon('question-circle'), help_page_path('user/admin_area/settings/continuous_integration', anchor: 'default-artifacts-expiration') + .form-group + = f.label :archive_builds_in_human_readable, 'Archive builds in', class: 'label-bold' + = f.text_field :archive_builds_in_human_readable, class: 'form-control', placeholder: 'never' + .form-text.text-muted + Set the duration when build gonna be considered old. Archived builds cannot be retried. + Make it empty to never expire builds. It has to be larger than 1 day. + The default unit is in seconds, but you can define an alternative. For example: + <code>4 mins 2 sec</code>, <code>2h42min</code>. = f.submit 'Save changes', class: "btn btn-success" diff --git a/app/views/groups/labels/index.html.haml b/app/views/groups/labels/index.html.haml index 5b78ce910b8..4df3d831942 100644 --- a/app/views/groups/labels/index.html.haml +++ b/app/views/groups/labels/index.html.haml @@ -6,7 +6,7 @@ - subscribed = params[:subscribed] - labels_or_filters = @labels.exists? || search.present? || subscribed.present? -- if can_admin_label +- if @labels.present? && can_admin_label - content_for(:header_content) do .nav-controls = link_to _('New label'), new_group_label_path(@group), class: "btn btn-success" diff --git a/app/views/projects/deployments/_rollback.haml b/app/views/projects/deployments/_rollback.haml index 281e042c915..1bd538a08ff 100644 --- a/app/views/projects/deployments/_rollback.haml +++ b/app/views/projects/deployments/_rollback.haml @@ -1,4 +1,4 @@ -- if can?(current_user, :create_deployment, deployment) && deployment.deployable +- if can?(current_user, :create_deployment, deployment) - tooltip = deployment.last? ? s_('Environments|Re-deploy to environment') : s_('Environments|Rollback environment') = link_to [:retry, @project.namespace.becomes(Namespace), @project, deployment.deployable], method: :post, class: 'btn btn-build has-tooltip', title: tooltip do - if deployment.last? diff --git a/app/views/projects/labels/index.html.haml b/app/views/projects/labels/index.html.haml index 06ee883d6dc..2c6484c2c99 100644 --- a/app/views/projects/labels/index.html.haml +++ b/app/views/projects/labels/index.html.haml @@ -5,7 +5,7 @@ - subscribed = params[:subscribed] - labels_or_filters = @labels.exists? || @prioritized_labels.exists? || search.present? || subscribed.present? -- if can_admin_label +- if @labels.present? && can_admin_label - content_for(:header_content) do .nav-controls = link_to _('New label'), new_project_label_path(@project), class: "btn btn-success qa-label-create-new" diff --git a/app/views/projects/merge_requests/show.html.haml b/app/views/projects/merge_requests/show.html.haml index 5d1bbb077af..515499956a2 100644 --- a/app/views/projects/merge_requests/show.html.haml +++ b/app/views/projects/merge_requests/show.html.haml @@ -34,7 +34,7 @@ .fade-left= icon('angle-left') .fade-right= icon('angle-right') %ul.merge-request-tabs.nav-tabs.nav.nav-links.scrolling-tabs - %li.notes-tab + %li.notes-tab.qa-notes-tab = tab_link_for @merge_request, :show, force_link: @commit.present? do Discussion %span.badge.badge-pill= @merge_request.related_notes.user.count @@ -48,7 +48,7 @@ = tab_link_for @merge_request, :pipelines do Pipelines %span.badge.badge-pill.js-pipelines-mr-count= @pipelines.size - %li.diffs-tab + %li.diffs-tab.qa-diffs-tab = tab_link_for @merge_request, :diffs do Changes %span.badge.badge-pill= @merge_request.diff_size diff --git a/app/views/projects/pipelines/_with_tabs.html.haml b/app/views/projects/pipelines/_with_tabs.html.haml index 95bba47802c..66e202103a9 100644 --- a/app/views/projects/pipelines/_with_tabs.html.haml +++ b/app/views/projects/pipelines/_with_tabs.html.haml @@ -61,12 +61,14 @@ %td.responsive-table-cell.build-failure{ data: { column: _("Failure")} } = build.present.callout_failure_message %td.responsive-table-cell.build-actions - = link_to retry_project_job_path(build.project, build, return_to: request.original_url), method: :post, title: _('Retry'), class: 'btn btn-build' do - = icon('repeat') - %tr.build-trace-row.responsive-table-border-end - %td - %td.responsive-table-cell.build-trace-container{ colspan: 4 } - %pre.build-trace.build-trace-rounded - %code.bash.js-build-output - = build_summary(build) + - if can?(current_user, :update_build, job) + = link_to retry_project_job_path(build.project, build, return_to: request.original_url), method: :post, title: _('Retry'), class: 'btn btn-build' do + = icon('repeat') + - if can?(current_user, :read_build, job) + %tr.build-trace-row.responsive-table-border-end + %td + %td.responsive-table-cell.build-trace-container{ colspan: 4 } + %pre.build-trace.build-trace-rounded + %code.bash.js-build-output + = build_summary(build) = render_if_exists "projects/pipelines/tabs_content", pipeline: @pipeline, project: @project diff --git a/app/views/projects/tree/_blob_item.html.haml b/app/views/projects/tree/_blob_item.html.haml deleted file mode 100644 index f79f3af36d4..00000000000 --- a/app/views/projects/tree/_blob_item.html.haml +++ /dev/null @@ -1,12 +0,0 @@ -- is_lfs_blob = @lfs_blob_ids.include?(blob_item.id) -%tr{ class: "tree-item #{tree_hex_class(blob_item)}" } - %td.tree-item-file-name - = tree_icon(type, blob_item.mode, blob_item.name) - - file_name = blob_item.name - = link_to project_blob_path(@project, tree_join(@id || @commit.id, blob_item.name)), class: 'str-truncated', title: file_name do - %span= file_name - - if is_lfs_blob - %span.badge.label-lfs.prepend-left-5 LFS - %td.d-none.d-sm-table-cell.tree-commit - %td.tree-time-ago.cgray.text-right - = render 'projects/tree/spinner' diff --git a/app/views/projects/tree/_spinner.html.haml b/app/views/projects/tree/_spinner.html.haml deleted file mode 100644 index b47ad0f41e4..00000000000 --- a/app/views/projects/tree/_spinner.html.haml +++ /dev/null @@ -1,3 +0,0 @@ -%span.log_loading.hide - %i.fa.fa-spinner.fa-spin - Loading commit data... diff --git a/app/views/projects/tree/_submodule_item.html.haml b/app/views/projects/tree/_submodule_item.html.haml deleted file mode 100644 index e563c8c4036..00000000000 --- a/app/views/projects/tree/_submodule_item.html.haml +++ /dev/null @@ -1,6 +0,0 @@ -%tr.tree-item - %td.tree-item-file-name - %i.fa.fa-archive.fa-fw - = submodule_link(submodule_item, @ref) - %td - %td.d-none.d-sm-table-cell diff --git a/app/views/projects/tree/_tree_item.html.haml b/app/views/projects/tree/_tree_item.html.haml deleted file mode 100644 index ce0cd95b468..00000000000 --- a/app/views/projects/tree/_tree_item.html.haml +++ /dev/null @@ -1,9 +0,0 @@ -%tr{ class: "tree-item #{tree_hex_class(tree_item)}" } - %td.tree-item-file-name - = tree_icon(type, tree_item.mode, tree_item.name) - - path = flatten_tree(@path, tree_item) - = link_to project_tree_path(@project, tree_join(@id || @commit.id, path)), class: 'str-truncated', title: path do - %span= path - %td.d-none.d-sm-table-cell.tree-commit - %td.tree-time-ago.text-right - = render 'projects/tree/spinner' diff --git a/app/views/projects/tree/_tree_row.html.haml b/app/views/projects/tree/_tree_row.html.haml index 0a5c6f048f7..8a27ea66523 100644 --- a/app/views/projects/tree/_tree_row.html.haml +++ b/app/views/projects/tree/_tree_row.html.haml @@ -1,6 +1,27 @@ -- if tree_row.type == :tree - = render partial: 'projects/tree/tree_item', object: tree_row, as: 'tree_item', locals: { type: 'folder' } -- elsif tree_row.type == :blob - = render partial: 'projects/tree/blob_item', object: tree_row, as: 'blob_item', locals: { type: 'file' } -- elsif tree_row.type == :commit - = render partial: 'projects/tree/submodule_item', object: tree_row, as: 'submodule_item' +- tree_row_name = tree_row.name +- tree_row_type = tree_row.type + +%tr{ class: "tree-item file_#{hexdigest(tree_row_name)}" } + %td.tree-item-file-name + - if tree_row_type == :tree + = tree_icon('folder', tree_row.mode, tree_row.name) + - path = flatten_tree(@path, tree_row) + %a.str-truncated{ href: fast_project_tree_path(@project, tree_join(@id || @commit.id, path)), title: path } + %span= path + + - elsif tree_row_type == :blob + = tree_icon('file', tree_row.mode, tree_row_name) + %a.str-truncated{ href: fast_project_blob_path(@project, tree_join(@id || @commit.id, tree_row_name)), title: tree_row_name } + %span= tree_row_name + - if @lfs_blob_ids.include?(tree_row.id) + %span.badge.label-lfs.prepend-left-5 LFS + + - elsif tree_row_type == :commit + = tree_icon('archive', tree_row.mode, tree_row.name) + = submodule_link(tree_row, @ref) + + %td.d-none.d-sm-table-cell.tree-commit + %td.tree-time-ago.text-right + %span.log_loading.hide + %i.fa.fa-spinner.fa-spin + Loading commit data... diff --git a/app/views/shared/empty_states/_labels.html.haml b/app/views/shared/empty_states/_labels.html.haml index b629ceafeb3..9133ce8ed22 100644 --- a/app/views/shared/empty_states/_labels.html.haml +++ b/app/views/shared/empty_states/_labels.html.haml @@ -6,6 +6,9 @@ .text-content %h4= _("Labels can be applied to issues and merge requests to categorize them.") %p= _("You can also star a label to make it a priority label.") - - if can?(current_user, :admin_label, @project) - = link_to _('New label'), new_project_label_path(@project), class: 'btn btn-success', title: _('New label'), id: 'new_label_link' - = link_to _('Generate a default set of labels'), generate_project_labels_path(@project), method: :post, class: 'btn btn-success btn-inverted', title: _('Generate a default set of labels'), id: 'generate_labels_link' + .text-center + - if can?(current_user, :admin_label, @project) + = link_to _('New label'), new_project_label_path(@project), class: 'btn btn-success', title: _('New label'), id: 'new_label_link' + = link_to _('Generate a default set of labels'), generate_project_labels_path(@project), method: :post, class: 'btn btn-success btn-inverted', title: _('Generate a default set of labels'), id: 'generate_labels_link' + - if can?(current_user, :admin_label, @group) + = link_to _('New label'), new_group_label_path(@group), class: 'btn btn-success', title: _('New label'), id: 'new_label_link' diff --git a/changelogs/unreleased/51259-ci-cd-gitlab-ui.yml b/changelogs/unreleased/51259-ci-cd-gitlab-ui.yml new file mode 100644 index 00000000000..a15f1c033b3 --- /dev/null +++ b/changelogs/unreleased/51259-ci-cd-gitlab-ui.yml @@ -0,0 +1,5 @@ +--- +title: Uses gitlab-ui components in jobs components +merge_request: +author: +type: other diff --git a/changelogs/unreleased/52300-pool-repositories.yml b/changelogs/unreleased/52300-pool-repositories.yml new file mode 100644 index 00000000000..5435f3aa21f --- /dev/null +++ b/changelogs/unreleased/52300-pool-repositories.yml @@ -0,0 +1,5 @@ +--- +title: Start tracking shards and pool repositories in the database +merge_request: 22482 +author: +type: other diff --git a/changelogs/unreleased/52925-scheduled-pipelines-ui-problems.yml b/changelogs/unreleased/52925-scheduled-pipelines-ui-problems.yml new file mode 100644 index 00000000000..792b24d75ac --- /dev/null +++ b/changelogs/unreleased/52925-scheduled-pipelines-ui-problems.yml @@ -0,0 +1,5 @@ +--- +title: Fixing styling issues on the scheduled pipelines page +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/53070-fix-enable-usage-ping-link.yml b/changelogs/unreleased/53070-fix-enable-usage-ping-link.yml deleted file mode 100644 index 605d3679159..00000000000 --- a/changelogs/unreleased/53070-fix-enable-usage-ping-link.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: "fix link to enable usage ping from convdev index" -merge_request: 22545 -author: Anand Capur -type: fixed diff --git a/changelogs/unreleased/53230-remove_personal_access_tokens_finder_find_by_method.yml b/changelogs/unreleased/53230-remove_personal_access_tokens_finder_find_by_method.yml new file mode 100644 index 00000000000..d4d78a2fd06 --- /dev/null +++ b/changelogs/unreleased/53230-remove_personal_access_tokens_finder_find_by_method.yml @@ -0,0 +1,5 @@ +--- +title: Remove PersonalAccessTokensFinder#find_by method +merge_request: 22617 +author: +type: fixed diff --git a/changelogs/unreleased/53362-allow-concurrency-in-puma.yml b/changelogs/unreleased/53362-allow-concurrency-in-puma.yml new file mode 100644 index 00000000000..5fbda0161c1 --- /dev/null +++ b/changelogs/unreleased/53362-allow-concurrency-in-puma.yml @@ -0,0 +1,5 @@ +--- +title: Allow Rails concurrency when running in Puma +merge_request: 22751 +author: +type: performance diff --git a/changelogs/unreleased/53450-wrong-value-for-kubernetes_version-variable.yml b/changelogs/unreleased/53450-wrong-value-for-kubernetes_version-variable.yml new file mode 100644 index 00000000000..cd9300ca2d1 --- /dev/null +++ b/changelogs/unreleased/53450-wrong-value-for-kubernetes_version-variable.yml @@ -0,0 +1,5 @@ +--- +title: Bump KUBERNETES_VERSION for Auto DevOps to latest 1.10 series +merge_request: 22757 +author: +type: other diff --git a/changelogs/unreleased/disallow-retry-of-old-builds.yml b/changelogs/unreleased/disallow-retry-of-old-builds.yml new file mode 100644 index 00000000000..03992fc0213 --- /dev/null +++ b/changelogs/unreleased/disallow-retry-of-old-builds.yml @@ -0,0 +1,5 @@ +--- +title: Soft-archive old jobs +merge_request: +author: +type: added diff --git a/changelogs/unreleased/fast_project_blob_path.yml b/changelogs/unreleased/fast_project_blob_path.yml new file mode 100644 index 00000000000..b56c9d9cf59 --- /dev/null +++ b/changelogs/unreleased/fast_project_blob_path.yml @@ -0,0 +1,5 @@ +--- +title: Improve performance of tree rendering in repositories with lots of items +merge_request: +author: +type: performance diff --git a/changelogs/unreleased/frozen-string-enable-lib-gitlab-ci-remain.yml b/changelogs/unreleased/frozen-string-enable-lib-gitlab-ci-remain.yml new file mode 100644 index 00000000000..ecbfc323080 --- /dev/null +++ b/changelogs/unreleased/frozen-string-enable-lib-gitlab-ci-remain.yml @@ -0,0 +1,5 @@ +--- +title: Enable frozen string for remaining lib/gitlab/ci/**/*.rb +merge_request: +author: gfyoung +type: performance diff --git a/changelogs/unreleased/gl-ui-tooltip.yml b/changelogs/unreleased/gl-ui-tooltip.yml new file mode 100644 index 00000000000..99ded9f812e --- /dev/null +++ b/changelogs/unreleased/gl-ui-tooltip.yml @@ -0,0 +1,5 @@ +--- +title: Remove gitlab-ui's tooltip from global +merge_request: +author: +type: performance diff --git a/changelogs/unreleased/gt-update-project-and-group-labels-empty-state.yml b/changelogs/unreleased/gt-update-project-and-group-labels-empty-state.yml new file mode 100644 index 00000000000..d644ca86b79 --- /dev/null +++ b/changelogs/unreleased/gt-update-project-and-group-labels-empty-state.yml @@ -0,0 +1,5 @@ +--- +title: Update project and group labels empty state +merge_request: 22745 +author: George Tsiolis +type: changed diff --git a/changelogs/unreleased/issue_51323.yml b/changelogs/unreleased/issue_51323.yml new file mode 100644 index 00000000000..b0e83e303d1 --- /dev/null +++ b/changelogs/unreleased/issue_51323.yml @@ -0,0 +1,5 @@ +--- +title: Add 'only history' option to notes filter +merge_request: +author: +type: changed diff --git a/changelogs/unreleased/mr-file-tree-inline-fluid-width-fix.yml b/changelogs/unreleased/mr-file-tree-inline-fluid-width-fix.yml deleted file mode 100644 index b61f47724fc..00000000000 --- a/changelogs/unreleased/mr-file-tree-inline-fluid-width-fix.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fixed merge request fill tree toggling not respecting fluid width preference -merge_request: -author: -type: fixed diff --git a/changelogs/unreleased/scheduled-manual-jobs-environment-play-buttons.yml b/changelogs/unreleased/scheduled-manual-jobs-environment-play-buttons.yml new file mode 100644 index 00000000000..c89af78d989 --- /dev/null +++ b/changelogs/unreleased/scheduled-manual-jobs-environment-play-buttons.yml @@ -0,0 +1,5 @@ +--- +title: Add the Play button for delayed jobs in environment page +merge_request: 22106 +author: +type: added diff --git a/changelogs/unreleased/sh-fix-issue-52176.yml b/changelogs/unreleased/sh-fix-issue-52176.yml new file mode 100644 index 00000000000..7269e14d910 --- /dev/null +++ b/changelogs/unreleased/sh-fix-issue-52176.yml @@ -0,0 +1,5 @@ +--- +title: Disable replication lag check for Aurora PostgreSQL databases +merge_request: 22786 +author: +type: fixed diff --git a/changelogs/unreleased/sh-optimize-mr-commit-sha-lookup.yml b/changelogs/unreleased/sh-optimize-mr-commit-sha-lookup.yml new file mode 100644 index 00000000000..bea73f8d329 --- /dev/null +++ b/changelogs/unreleased/sh-optimize-mr-commit-sha-lookup.yml @@ -0,0 +1,5 @@ +--- +title: Optimize merge request refresh by using the database to check commit SHAs +merge_request: 22731 +author: +type: performance diff --git a/changelogs/unreleased/toggle-sidebar-alignment.yml b/changelogs/unreleased/toggle-sidebar-alignment.yml new file mode 100644 index 00000000000..428fe61da9b --- /dev/null +++ b/changelogs/unreleased/toggle-sidebar-alignment.yml @@ -0,0 +1,5 @@ +--- +title: Align toggle sidebar button across all browsers and OSs +merge_request: 22771 +author: +type: fixed diff --git a/changelogs/unreleased/update_license_management_job.yml b/changelogs/unreleased/update_license_management_job.yml new file mode 100644 index 00000000000..d6e56080e77 --- /dev/null +++ b/changelogs/unreleased/update_license_management_job.yml @@ -0,0 +1,5 @@ +--- +title: "Remove dind from license_management auto-devops job definition" +merge_request: 22732 +author: +type: performance diff --git a/changelogs/unreleased/zj-bump-gitaly-0-128.yml b/changelogs/unreleased/zj-bump-gitaly-0-128.yml new file mode 100644 index 00000000000..451df4b800e --- /dev/null +++ b/changelogs/unreleased/zj-bump-gitaly-0-128.yml @@ -0,0 +1,5 @@ +--- +title: Bump Gitaly to 0.128.0 +merge_request: +author: +type: added diff --git a/config/environments/development.rb b/config/environments/development.rb index 23790b84e3c..494ddd72556 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -45,4 +45,6 @@ Rails.application.configure do # Do not log asset requests config.assets.quiet = true + + config.allow_concurrency = defined?(::Puma) end diff --git a/config/environments/production.rb b/config/environments/production.rb index 9941987929c..71195164e7a 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -83,5 +83,5 @@ Rails.application.configure do config.eager_load = true - config.allow_concurrency = false + config.allow_concurrency = defined?(::Puma) end diff --git a/config/initializers/fill_shards.rb b/config/initializers/fill_shards.rb new file mode 100644 index 00000000000..0f45cf44621 --- /dev/null +++ b/config/initializers/fill_shards.rb @@ -0,0 +1,4 @@ +return unless Shard.connected? +return if Gitlab::Database.read_only? + +Shard.populate! diff --git a/db/migrate/20180927073410_add_index_to_project_deploy_tokens_deploy_token_id.rb b/db/migrate/20180927073410_add_index_to_project_deploy_tokens_deploy_token_id.rb new file mode 100644 index 00000000000..61d32fe16eb --- /dev/null +++ b/db/migrate/20180927073410_add_index_to_project_deploy_tokens_deploy_token_id.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +class AddIndexToProjectDeployTokensDeployTokenId < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + # MySQL already has index inserted + add_concurrent_index :project_deploy_tokens, :deploy_token_id if Gitlab::Database.postgresql? + end + + def down + remove_concurrent_index(:project_deploy_tokens, :deploy_token_id) if Gitlab::Database.postgresql? + end +end diff --git a/db/migrate/20181019032400_add_shards_table.rb b/db/migrate/20181019032400_add_shards_table.rb new file mode 100644 index 00000000000..5e0a6960548 --- /dev/null +++ b/db/migrate/20181019032400_add_shards_table.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class AddShardsTable < ActiveRecord::Migration + DOWNTIME = false + + def change + create_table :shards do |t| + t.string :name, null: false, index: { unique: true } + end + end +end diff --git a/db/migrate/20181019032408_add_repositories_table.rb b/db/migrate/20181019032408_add_repositories_table.rb new file mode 100644 index 00000000000..077f264d3ce --- /dev/null +++ b/db/migrate/20181019032408_add_repositories_table.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class AddRepositoriesTable < ActiveRecord::Migration + DOWNTIME = false + + def change + create_table :repositories, id: :bigserial do |t| + t.references :shard, null: false, index: true, foreign_key: { on_delete: :restrict } + t.string :disk_path, null: false, index: { unique: true } + end + + add_column :projects, :pool_repository_id, :bigint + add_index :projects, :pool_repository_id, where: 'pool_repository_id IS NOT NULL' + end +end diff --git a/db/migrate/20181019105553_add_projects_pool_repository_id_foreign_key.rb b/db/migrate/20181019105553_add_projects_pool_repository_id_foreign_key.rb new file mode 100644 index 00000000000..059988de38a --- /dev/null +++ b/db/migrate/20181019105553_add_projects_pool_repository_id_foreign_key.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +class AddProjectsPoolRepositoryIdForeignKey < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + add_concurrent_foreign_key( + :projects, + :repositories, + column: :pool_repository_id, + on_delete: :nullify + ) + end + + def down + remove_foreign_key(:projects, column: :pool_repository_id) + end +end diff --git a/db/migrate/20181023104858_add_archive_builds_duration_to_application_settings.rb b/db/migrate/20181023104858_add_archive_builds_duration_to_application_settings.rb new file mode 100644 index 00000000000..744748b3fad --- /dev/null +++ b/db/migrate/20181023104858_add_archive_builds_duration_to_application_settings.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class AddArchiveBuildsDurationToApplicationSettings < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def change + add_column(:application_settings, :archive_builds_in_seconds, :integer, allow_null: true) + end +end diff --git a/db/schema.rb b/db/schema.rb index 1a8b556228d..32d10e87e87 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -165,6 +165,7 @@ ActiveRecord::Schema.define(version: 20181101144347) do t.integer "usage_stats_set_by_user_id" t.integer "receive_max_input_size" t.integer "diff_max_patch_bytes", default: 102400, null: false + t.integer "archive_builds_in_seconds" end create_table "audit_events", force: :cascade do |t| @@ -1588,6 +1589,7 @@ ActiveRecord::Schema.define(version: 20181101144347) do t.datetime_with_timezone "created_at", null: false end + add_index "project_deploy_tokens", ["deploy_token_id"], name: "index_project_deploy_tokens_on_deploy_token_id", using: :btree add_index "project_deploy_tokens", ["project_id", "deploy_token_id"], name: "index_project_deploy_tokens_on_project_id_and_deploy_token_id", unique: true, using: :btree create_table "project_features", force: :cascade do |t| @@ -1703,6 +1705,7 @@ ActiveRecord::Schema.define(version: 20181101144347) do t.integer "jobs_cache_index" t.boolean "pages_https_only", default: true t.boolean "remote_mirror_available_overridden" + t.integer "pool_repository_id", limit: 8 end add_index "projects", ["ci_id"], name: "index_projects_on_ci_id", using: :btree @@ -1719,6 +1722,7 @@ ActiveRecord::Schema.define(version: 20181101144347) do add_index "projects", ["path"], name: "index_projects_on_path", using: :btree add_index "projects", ["path"], name: "index_projects_on_path_trigram", using: :gin, opclasses: {"path"=>"gin_trgm_ops"} add_index "projects", ["pending_delete"], name: "index_projects_on_pending_delete", using: :btree + add_index "projects", ["pool_repository_id"], name: "index_projects_on_pool_repository_id", where: "(pool_repository_id IS NOT NULL)", using: :btree add_index "projects", ["repository_storage", "created_at"], name: "idx_project_repository_check_partial", where: "(last_repository_check_at IS NULL)", using: :btree add_index "projects", ["repository_storage"], name: "index_projects_on_repository_storage", using: :btree add_index "projects", ["runners_token"], name: "index_projects_on_runners_token", using: :btree @@ -1851,6 +1855,14 @@ ActiveRecord::Schema.define(version: 20181101144347) do add_index "remote_mirrors", ["last_successful_update_at"], name: "index_remote_mirrors_on_last_successful_update_at", using: :btree add_index "remote_mirrors", ["project_id"], name: "index_remote_mirrors_on_project_id", using: :btree + create_table "repositories", id: :bigserial, force: :cascade do |t| + t.integer "shard_id", null: false + t.string "disk_path", null: false + end + + add_index "repositories", ["disk_path"], name: "index_repositories_on_disk_path", unique: true, using: :btree + add_index "repositories", ["shard_id"], name: "index_repositories_on_shard_id", using: :btree + create_table "repository_languages", id: false, force: :cascade do |t| t.integer "project_id", null: false t.integer "programming_language_id", null: false @@ -1931,6 +1943,12 @@ ActiveRecord::Schema.define(version: 20181101144347) do add_index "services", ["project_id"], name: "index_services_on_project_id", using: :btree add_index "services", ["template"], name: "index_services_on_template", using: :btree + create_table "shards", force: :cascade do |t| + t.string "name", null: false + end + + add_index "shards", ["name"], name: "index_shards_on_name", unique: true, using: :btree + create_table "site_statistics", force: :cascade do |t| t.integer "repositories_count", default: 0, null: false end @@ -2450,6 +2468,7 @@ ActiveRecord::Schema.define(version: 20181101144347) do add_foreign_key "project_import_data", "projects", name: "fk_ffb9ee3a10", on_delete: :cascade add_foreign_key "project_mirror_data", "projects", on_delete: :cascade add_foreign_key "project_statistics", "projects", on_delete: :cascade + add_foreign_key "projects", "repositories", column: "pool_repository_id", name: "fk_6e5c14658a", on_delete: :nullify add_foreign_key "prometheus_metrics", "projects", on_delete: :cascade add_foreign_key "protected_branch_merge_access_levels", "protected_branches", name: "fk_8a3072ccb3", on_delete: :cascade add_foreign_key "protected_branch_push_access_levels", "protected_branches", name: "fk_9ffc86a3d9", on_delete: :cascade @@ -2461,6 +2480,7 @@ ActiveRecord::Schema.define(version: 20181101144347) do add_foreign_key "push_event_payloads", "events", name: "fk_36c74129da", on_delete: :cascade add_foreign_key "releases", "projects", name: "fk_47fe2a0596", on_delete: :cascade add_foreign_key "remote_mirrors", "projects", on_delete: :cascade + add_foreign_key "repositories", "shards", on_delete: :restrict add_foreign_key "repository_languages", "projects", on_delete: :cascade add_foreign_key "resource_label_events", "issues", on_delete: :cascade add_foreign_key "resource_label_events", "labels", on_delete: :nullify diff --git a/doc/ci/services/mysql.md b/doc/ci/services/mysql.md index 338368dbbc9..b76f9618fc9 100644 --- a/doc/ci/services/mysql.md +++ b/doc/ci/services/mysql.md @@ -31,7 +31,7 @@ Database: el_duderino ``` If you are wondering why we used `mysql` for the `Host`, read more at -[How is service linked to the job](../docker/using_docker_images.md#how-is-service-linked-to-the-job). +[How services are linked to the job](../docker/using_docker_images.md#how-services-are-linked-to-the-job). You can also use any other docker image available on [Docker Hub][hub-mysql]. For example, to use MySQL 5.5 the service becomes `mysql:5.5`. diff --git a/doc/development/code_review.md b/doc/development/code_review.md index 3fe79943fdc..96f3861f8d7 100644 --- a/doc/development/code_review.md +++ b/doc/development/code_review.md @@ -23,6 +23,9 @@ one of the [Merge request coaches][team]. Depending on the areas your merge request touches, it must be **approved** by one or more [maintainers](https://about.gitlab.com/handbook/engineering/#maintainer): +For approvals, we use the approval functionality found in the merge request +widget. Reviewers can add their approval by [approving additionally](https://docs.gitlab.com/ee/user/project/merge_requests/merge_request_approvals.html#adding-or-removing-an-approval). + 1. If your merge request includes backend changes [^1], it must be **approved by a [backend maintainer](https://about.gitlab.com/handbook/engineering/projects/#gitlab-ce_maintainers_backend)**. 1. If your merge request includes frontend changes [^1], it must be @@ -97,6 +100,9 @@ If a developer who happens to also be a maintainer was involved in a merge reque as a domain expert and/or reviewer, it is recommended that they are not also picked as the maintainer to ultimately approve and merge it. +Maintainers should check before merging if the merge request is approved by the +required approvers. + ## Best practices ### Everyone diff --git a/doc/development/contributing/merge_request_workflow.md b/doc/development/contributing/merge_request_workflow.md index 1764e2d8b21..5b32b5cd46f 100644 --- a/doc/development/contributing/merge_request_workflow.md +++ b/doc/development/contributing/merge_request_workflow.md @@ -171,6 +171,7 @@ the feature you contribute through all of these steps. 1. Added to [the website](https://gitlab.com/gitlab-com/www-gitlab-com/), if relevant 1. Community questions answered 1. Answers to questions radiated (in docs/wiki/support etc.) +1. [Black-box tests/end-to-end tests](../testing_guide/testing_levels.md#black-box-tests-or-end-to-end-tests) added if required. Please contact [the quality team](https://about.gitlab.com/handbook/engineering/quality/#teams) with any questions If you add a dependency in GitLab (such as an operating system package) please consider updating the following and note the applicability of each in your @@ -185,7 +186,7 @@ merge request: 1. Omnibus package creator https://gitlab.com/gitlab-org/omnibus-gitlab [definition-of-done]: http://guide.agilealliance.org/guide/definition-of-done.html -[testing]: ../testing_guide/index.md +[testing]: ../testing_guide/index.md --- diff --git a/doc/user/permissions.md b/doc/user/permissions.md index 4359592905d..1fd230a41aa 100644 --- a/doc/user/permissions.md +++ b/doc/user/permissions.md @@ -95,6 +95,7 @@ The following table depicts the various user permission levels in a project. | Manage GitLab Pages | | | | ā | ā | | Manage GitLab Pages domains and certificates | | | | ā | ā | | Remove GitLab Pages | | | | | ā | +| View GitLab Pages protected by [access control](../administration/pages/index.md#access-control) | ā | ā | ā | ā | ā | | Manage clusters | | | | ā | ā | | Manage license policy **[ULTIMATE]** | | | | ā | ā | | Edit comments (posted by any user) | | | | ā | ā | @@ -206,7 +207,7 @@ They will, like usual users, receive a role in the project or group with all the abilities that are mentioned in the table above. They cannot however create groups or projects, and they have the same access as logged out users in all other cases. - + An administrator can flag a user as external [through the API](../api/users.md) or by checking the checkbox on the admin panel. As an administrator, navigate to **Admin > Users** to create a new user or edit an existing one. There, you @@ -217,7 +218,7 @@ by an administrator under **Admin > Application Settings**. ### Default internal users -The "Internal users" field allows specifying an e-mail address regex pattern to identify default internal users. +The "Internal users" field allows specifying an e-mail address regex pattern to identify default internal users. New users whose email address matches the regex pattern will be set to internal by default rather than an external collaborator. diff --git a/lib/api/users.rb b/lib/api/users.rb index 47382b09207..2a56506f3a5 100644 --- a/lib/api/users.rb +++ b/lib/api/users.rb @@ -512,11 +512,9 @@ module API PersonalAccessTokensFinder.new({ user: user, impersonation: true }.merge(options)) end - # rubocop: disable CodeReuse/ActiveRecord def find_impersonation_token - finder.find_by(id: declared_params[:impersonation_token_id]) || not_found!('Impersonation Token') + finder.find_by_id(declared_params[:impersonation_token_id]) || not_found!('Impersonation Token') end - # rubocop: enable CodeReuse/ActiveRecord end before { authenticated_as_admin! } diff --git a/lib/gitlab/ci/status/build/action.rb b/lib/gitlab/ci/status/build/action.rb index 6c9125647ad..45d9ba41e92 100644 --- a/lib/gitlab/ci/status/build/action.rb +++ b/lib/gitlab/ci/status/build/action.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Status diff --git a/lib/gitlab/ci/status/build/cancelable.rb b/lib/gitlab/ci/status/build/cancelable.rb index 024047d4983..43fb5cdbbe6 100644 --- a/lib/gitlab/ci/status/build/cancelable.rb +++ b/lib/gitlab/ci/status/build/cancelable.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Status diff --git a/lib/gitlab/ci/status/build/canceled.rb b/lib/gitlab/ci/status/build/canceled.rb index c83e2734a73..0518b9e673d 100644 --- a/lib/gitlab/ci/status/build/canceled.rb +++ b/lib/gitlab/ci/status/build/canceled.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Status diff --git a/lib/gitlab/ci/status/build/common.rb b/lib/gitlab/ci/status/build/common.rb index c1fc70ac266..6a75ec5c37f 100644 --- a/lib/gitlab/ci/status/build/common.rb +++ b/lib/gitlab/ci/status/build/common.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Status diff --git a/lib/gitlab/ci/status/build/created.rb b/lib/gitlab/ci/status/build/created.rb index 5be8e9de425..780fea23123 100644 --- a/lib/gitlab/ci/status/build/created.rb +++ b/lib/gitlab/ci/status/build/created.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Status diff --git a/lib/gitlab/ci/status/build/erased.rb b/lib/gitlab/ci/status/build/erased.rb index 495227c2ffb..d74cfc1ee77 100644 --- a/lib/gitlab/ci/status/build/erased.rb +++ b/lib/gitlab/ci/status/build/erased.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Status diff --git a/lib/gitlab/ci/status/build/factory.rb b/lib/gitlab/ci/status/build/factory.rb index 4a74d6d6ed1..6e4bfe23f2b 100644 --- a/lib/gitlab/ci/status/build/factory.rb +++ b/lib/gitlab/ci/status/build/factory.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Status diff --git a/lib/gitlab/ci/status/build/failed.rb b/lib/gitlab/ci/status/build/failed.rb index 4babc23a495..d40454df737 100644 --- a/lib/gitlab/ci/status/build/failed.rb +++ b/lib/gitlab/ci/status/build/failed.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Status @@ -12,7 +14,8 @@ module Gitlab missing_dependency_failure: 'missing dependency failure', runner_unsupported: 'unsupported runner', stale_schedule: 'stale schedule', - job_execution_timeout: 'job execution timeout' + job_execution_timeout: 'job execution timeout', + archived_failure: 'archived failure' }.freeze private_constant :REASONS diff --git a/lib/gitlab/ci/status/build/failed_allowed.rb b/lib/gitlab/ci/status/build/failed_allowed.rb index ca0046fb1f7..d7570fdd3e2 100644 --- a/lib/gitlab/ci/status/build/failed_allowed.rb +++ b/lib/gitlab/ci/status/build/failed_allowed.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Status diff --git a/lib/gitlab/ci/status/build/manual.rb b/lib/gitlab/ci/status/build/manual.rb index 042da6392d3..d01b09f1398 100644 --- a/lib/gitlab/ci/status/build/manual.rb +++ b/lib/gitlab/ci/status/build/manual.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Status diff --git a/lib/gitlab/ci/status/build/pending.rb b/lib/gitlab/ci/status/build/pending.rb index 9dd9a27ad57..95f668295dd 100644 --- a/lib/gitlab/ci/status/build/pending.rb +++ b/lib/gitlab/ci/status/build/pending.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Status diff --git a/lib/gitlab/ci/status/build/play.rb b/lib/gitlab/ci/status/build/play.rb index a8b9ebf0803..c66b8ca5654 100644 --- a/lib/gitlab/ci/status/build/play.rb +++ b/lib/gitlab/ci/status/build/play.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Status diff --git a/lib/gitlab/ci/status/build/retried.rb b/lib/gitlab/ci/status/build/retried.rb index 6e190e4ee3c..b489dc68733 100644 --- a/lib/gitlab/ci/status/build/retried.rb +++ b/lib/gitlab/ci/status/build/retried.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Status diff --git a/lib/gitlab/ci/status/build/retryable.rb b/lib/gitlab/ci/status/build/retryable.rb index 5aeb8e51480..eb6b3f21604 100644 --- a/lib/gitlab/ci/status/build/retryable.rb +++ b/lib/gitlab/ci/status/build/retryable.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Status diff --git a/lib/gitlab/ci/status/build/scheduled.rb b/lib/gitlab/ci/status/build/scheduled.rb index 62ad9083616..f443dbee120 100644 --- a/lib/gitlab/ci/status/build/scheduled.rb +++ b/lib/gitlab/ci/status/build/scheduled.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Status diff --git a/lib/gitlab/ci/status/build/skipped.rb b/lib/gitlab/ci/status/build/skipped.rb index 3e678d0baee..4fe2f7b3114 100644 --- a/lib/gitlab/ci/status/build/skipped.rb +++ b/lib/gitlab/ci/status/build/skipped.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Status diff --git a/lib/gitlab/ci/status/build/stop.rb b/lib/gitlab/ci/status/build/stop.rb index dea838bfa39..a620e7ad126 100644 --- a/lib/gitlab/ci/status/build/stop.rb +++ b/lib/gitlab/ci/status/build/stop.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Status diff --git a/lib/gitlab/ci/status/build/unschedule.rb b/lib/gitlab/ci/status/build/unschedule.rb index e1b7b83428c..9110839cb55 100644 --- a/lib/gitlab/ci/status/build/unschedule.rb +++ b/lib/gitlab/ci/status/build/unschedule.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Status diff --git a/lib/gitlab/ci/status/canceled.rb b/lib/gitlab/ci/status/canceled.rb index e6195a60d4f..07f37732023 100644 --- a/lib/gitlab/ci/status/canceled.rb +++ b/lib/gitlab/ci/status/canceled.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Status diff --git a/lib/gitlab/ci/status/core.rb b/lib/gitlab/ci/status/core.rb index 9d6a2f51c11..ea773ee9944 100644 --- a/lib/gitlab/ci/status/core.rb +++ b/lib/gitlab/ci/status/core.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Status diff --git a/lib/gitlab/ci/status/created.rb b/lib/gitlab/ci/status/created.rb index 846f00b83dd..fface4bb97b 100644 --- a/lib/gitlab/ci/status/created.rb +++ b/lib/gitlab/ci/status/created.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Status diff --git a/lib/gitlab/ci/status/extended.rb b/lib/gitlab/ci/status/extended.rb index 1e8101f8949..b72a28ed0b6 100644 --- a/lib/gitlab/ci/status/extended.rb +++ b/lib/gitlab/ci/status/extended.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Status diff --git a/lib/gitlab/ci/status/external/common.rb b/lib/gitlab/ci/status/external/common.rb index 9307545b5b1..4169f5b3210 100644 --- a/lib/gitlab/ci/status/external/common.rb +++ b/lib/gitlab/ci/status/external/common.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Status diff --git a/lib/gitlab/ci/status/external/factory.rb b/lib/gitlab/ci/status/external/factory.rb index 07b15bd8d97..91fafb940a8 100644 --- a/lib/gitlab/ci/status/external/factory.rb +++ b/lib/gitlab/ci/status/external/factory.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Status diff --git a/lib/gitlab/ci/status/factory.rb b/lib/gitlab/ci/status/factory.rb index 15836c699c7..3446644eff8 100644 --- a/lib/gitlab/ci/status/factory.rb +++ b/lib/gitlab/ci/status/factory.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Status diff --git a/lib/gitlab/ci/status/failed.rb b/lib/gitlab/ci/status/failed.rb index 27ce85bd3ed..770ed7d4d5a 100644 --- a/lib/gitlab/ci/status/failed.rb +++ b/lib/gitlab/ci/status/failed.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Status diff --git a/lib/gitlab/ci/status/group/common.rb b/lib/gitlab/ci/status/group/common.rb index cfd4329a923..0b5ea0712ca 100644 --- a/lib/gitlab/ci/status/group/common.rb +++ b/lib/gitlab/ci/status/group/common.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Status diff --git a/lib/gitlab/ci/status/group/factory.rb b/lib/gitlab/ci/status/group/factory.rb index d118116cfc3..ee785856fdd 100644 --- a/lib/gitlab/ci/status/group/factory.rb +++ b/lib/gitlab/ci/status/group/factory.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Status diff --git a/lib/gitlab/ci/status/manual.rb b/lib/gitlab/ci/status/manual.rb index fc387e2fd25..50c92add400 100644 --- a/lib/gitlab/ci/status/manual.rb +++ b/lib/gitlab/ci/status/manual.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Status diff --git a/lib/gitlab/ci/status/pending.rb b/lib/gitlab/ci/status/pending.rb index 6780780db32..cea7e6ed938 100644 --- a/lib/gitlab/ci/status/pending.rb +++ b/lib/gitlab/ci/status/pending.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Status diff --git a/lib/gitlab/ci/status/pipeline/blocked.rb b/lib/gitlab/ci/status/pipeline/blocked.rb index bf7e484ee9b..ed13a439be0 100644 --- a/lib/gitlab/ci/status/pipeline/blocked.rb +++ b/lib/gitlab/ci/status/pipeline/blocked.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Status diff --git a/lib/gitlab/ci/status/pipeline/common.rb b/lib/gitlab/ci/status/pipeline/common.rb index 61bb07beb0f..7b34a2ea858 100644 --- a/lib/gitlab/ci/status/pipeline/common.rb +++ b/lib/gitlab/ci/status/pipeline/common.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Status diff --git a/lib/gitlab/ci/status/pipeline/delayed.rb b/lib/gitlab/ci/status/pipeline/delayed.rb index 12736861c89..e61acdcd167 100644 --- a/lib/gitlab/ci/status/pipeline/delayed.rb +++ b/lib/gitlab/ci/status/pipeline/delayed.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Status diff --git a/lib/gitlab/ci/status/pipeline/factory.rb b/lib/gitlab/ci/status/pipeline/factory.rb index 0adf83fa197..5d1a8bbd924 100644 --- a/lib/gitlab/ci/status/pipeline/factory.rb +++ b/lib/gitlab/ci/status/pipeline/factory.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Status diff --git a/lib/gitlab/ci/status/running.rb b/lib/gitlab/ci/status/running.rb index ee13905e46d..ac7dd74cdce 100644 --- a/lib/gitlab/ci/status/running.rb +++ b/lib/gitlab/ci/status/running.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Status diff --git a/lib/gitlab/ci/status/scheduled.rb b/lib/gitlab/ci/status/scheduled.rb index 3adcfa36af2..16ad1da89e3 100644 --- a/lib/gitlab/ci/status/scheduled.rb +++ b/lib/gitlab/ci/status/scheduled.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Status diff --git a/lib/gitlab/ci/status/skipped.rb b/lib/gitlab/ci/status/skipped.rb index 0dbdc4de426..aaec1e1d201 100644 --- a/lib/gitlab/ci/status/skipped.rb +++ b/lib/gitlab/ci/status/skipped.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Status diff --git a/lib/gitlab/ci/status/stage/common.rb b/lib/gitlab/ci/status/stage/common.rb index f60a7662075..f12daaa9676 100644 --- a/lib/gitlab/ci/status/stage/common.rb +++ b/lib/gitlab/ci/status/stage/common.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Status diff --git a/lib/gitlab/ci/status/stage/factory.rb b/lib/gitlab/ci/status/stage/factory.rb index 4c37f084d07..58f4642510b 100644 --- a/lib/gitlab/ci/status/stage/factory.rb +++ b/lib/gitlab/ci/status/stage/factory.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Status diff --git a/lib/gitlab/ci/status/success.rb b/lib/gitlab/ci/status/success.rb index 731013ec017..020f2c5b89f 100644 --- a/lib/gitlab/ci/status/success.rb +++ b/lib/gitlab/ci/status/success.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Status diff --git a/lib/gitlab/ci/status/success_warning.rb b/lib/gitlab/ci/status/success_warning.rb index 32b4cf43e48..6632cd9b143 100644 --- a/lib/gitlab/ci/status/success_warning.rb +++ b/lib/gitlab/ci/status/success_warning.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Status diff --git a/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml b/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml index 734af5eba59..c759bb7098e 100644 --- a/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml @@ -49,7 +49,7 @@ variables: POSTGRES_ENABLED: "true" POSTGRES_DB: $CI_ENVIRONMENT_SLUG - KUBERNETES_VERSION: 1.8.6 + KUBERNETES_VERSION: 1.10.9 HELM_VERSION: 2.11.0 DOCKER_DRIVER: overlay2 @@ -116,12 +116,9 @@ code_quality: license_management: stage: test - image: docker:stable + image: "registry.gitlab.com/gitlab-org/security-products/license-management:$CI_SERVER_VERSION_MAJOR-$CI_SERVER_VERSION_MINOR-stable" allow_failure: true - services: - - docker:stable-dind script: - - setup_docker - license_management artifacts: paths: [gl-license-management-report.json] @@ -525,11 +522,7 @@ rollout 100%: } function license_management() { - # Extract "MAJOR.MINOR" from CI_SERVER_VERSION and generate "MAJOR-MINOR-stable" - LICENSE_MANAGEMENT_VERSION=$(echo "$CI_SERVER_VERSION" | sed 's/^\([0-9]*\)\.\([0-9]*\).*/\1-\2-stable/') - - docker run --volume "$PWD:/code" \ - "registry.gitlab.com/gitlab-org/security-products/license-management:$LICENSE_MANAGEMENT_VERSION" analyze /code + /run.sh analyze . } function sast() { diff --git a/lib/gitlab/ci/trace/chunked_io.rb b/lib/gitlab/ci/trace/chunked_io.rb index 2147f62a84a..e9b3199d56e 100644 --- a/lib/gitlab/ci/trace/chunked_io.rb +++ b/lib/gitlab/ci/trace/chunked_io.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + ## # This class is compatible with IO class (https://ruby-doc.org/core-2.3.1/IO.html) # source: https://gitlab.com/snippets/1685610 @@ -66,8 +68,8 @@ module Gitlab end end - def read(length = nil, outbuf = "") - out = "" + def read(length = nil, outbuf = nil) + out = [] length ||= size - tell @@ -83,17 +85,18 @@ module Gitlab length -= chunk_data.bytesize end + out = out.join + # If outbuf is passed, we put the output into the buffer. This supports IO.copy_stream functionality if outbuf - outbuf.slice!(0, outbuf.bytesize) - outbuf << out + outbuf.replace(out) end out end def readline - out = "" + out = [] until eof? data = chunk_slice_from_offset @@ -109,7 +112,7 @@ module Gitlab end end - out + out.join end def write(data) diff --git a/lib/gitlab/ci/trace/section_parser.rb b/lib/gitlab/ci/trace/section_parser.rb index c09089d6475..f33f8cc56c1 100644 --- a/lib/gitlab/ci/trace/section_parser.rb +++ b/lib/gitlab/ci/trace/section_parser.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci class Trace diff --git a/lib/gitlab/ci/trace/stream.rb b/lib/gitlab/ci/trace/stream.rb index a71040e5e56..bd40fdf59b1 100644 --- a/lib/gitlab/ci/trace/stream.rb +++ b/lib/gitlab/ci/trace/stream.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci class Trace @@ -129,8 +131,7 @@ module Gitlab debris = '' until (buf = read_backward(BUFFER_SIZE)).empty? - buf += debris - debris, *lines = buf.each_line.to_a + debris, *lines = (buf + debris).each_line.to_a lines.reverse_each do |line| yield(line.force_encoding(Encoding.default_external)) end diff --git a/lib/gitlab/ci/variables/collection.rb b/lib/gitlab/ci/variables/collection.rb index ad30b3f427c..a7b4e0348c2 100644 --- a/lib/gitlab/ci/variables/collection.rb +++ b/lib/gitlab/ci/variables/collection.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Variables diff --git a/lib/gitlab/ci/variables/collection/item.rb b/lib/gitlab/ci/variables/collection/item.rb index 7da6d09d440..fdf852e8788 100644 --- a/lib/gitlab/ci/variables/collection/item.rb +++ b/lib/gitlab/ci/variables/collection/item.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Variables diff --git a/lib/gitlab/import_export/import_export.yml b/lib/gitlab/import_export/import_export.yml index 2bed470514b..9790818ecaf 100644 --- a/lib/gitlab/import_export/import_export.yml +++ b/lib/gitlab/import_export/import_export.yml @@ -92,6 +92,7 @@ excluded_attributes: - :path - :namespace_id - :creator_id + - :pool_repository_id - :import_url - :import_status - :avatar diff --git a/lib/gitlab/kubernetes/helm.rb b/lib/gitlab/kubernetes/helm.rb index 4a1bdf34c3e..1cd4f9e17b7 100644 --- a/lib/gitlab/kubernetes/helm.rb +++ b/lib/gitlab/kubernetes/helm.rb @@ -2,6 +2,7 @@ module Gitlab module Kubernetes module Helm HELM_VERSION = '2.7.2'.freeze + KUBECTL_VERSION = '1.11.0'.freeze NAMESPACE = 'gitlab-managed-apps'.freeze SERVICE_ACCOUNT = 'tiller'.freeze CLUSTER_ROLE_BINDING = 'tiller-admin'.freeze diff --git a/lib/gitlab/kubernetes/helm/base_command.rb b/lib/gitlab/kubernetes/helm/base_command.rb index 6752f2cff43..008cba9d33c 100644 --- a/lib/gitlab/kubernetes/helm/base_command.rb +++ b/lib/gitlab/kubernetes/helm/base_command.rb @@ -11,12 +11,6 @@ module Gitlab def generate_script <<~HEREDOC set -eo pipefail - ALPINE_VERSION=$(cat /etc/alpine-release | cut -d '.' -f 1,2) - echo http://mirror.clarkson.edu/alpine/v$ALPINE_VERSION/main >> /etc/apk/repositories - echo http://mirror1.hs-esslingen.de/pub/Mirrors/alpine/v$ALPINE_VERSION/main >> /etc/apk/repositories - apk add -U wget ca-certificates openssl >/dev/null - wget -q -O - https://kubernetes-helm.storage.googleapis.com/helm-v#{Gitlab::Kubernetes::Helm::HELM_VERSION}-linux-amd64.tar.gz | tar zxC /tmp >/dev/null - mv /tmp/linux-amd64/helm /usr/bin/ HEREDOC end diff --git a/lib/gitlab/kubernetes/helm/pod.rb b/lib/gitlab/kubernetes/helm/pod.rb index 95192b11c0d..e9c621d96f0 100644 --- a/lib/gitlab/kubernetes/helm/pod.rb +++ b/lib/gitlab/kubernetes/helm/pod.rb @@ -25,7 +25,7 @@ module Gitlab def container_specification { name: 'helm', - image: 'alpine:3.6', + image: "registry.gitlab.com/gitlab-org/cluster-integration/helm-install-image/releases/#{Gitlab::Kubernetes::Helm::HELM_VERSION}-kube-#{Gitlab::Kubernetes::Helm::KUBECTL_VERSION}", env: generate_pod_env(command), command: %w(/bin/sh), args: %w(-c $(COMMAND_SCRIPT)) diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 324e5315821..2f4b0e900c3 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -2172,7 +2172,7 @@ msgstr "" msgid "Define a custom pattern with cron syntax" msgstr "" -msgid "DelayedJobs|Are you sure you want to run %{jobName} immediately? This job will run automatically after it's timer finishes." +msgid "DelayedJobs|Are you sure you want to run %{jobName} immediately? Otherwise this job will run automatically after it's timer finishes." msgstr "" msgid "DelayedJobs|Are you sure you want to run %{job_name} immediately? This job will run automatically after it's timer finishes." @@ -4165,6 +4165,9 @@ msgstr "" msgid "Notes|Show comments only" msgstr "" +msgid "Notes|Show history only" +msgstr "" + msgid "Notification events" msgstr "" @@ -36,42 +36,40 @@ module QA ## # GitLab QA fabrication mechanisms # - module Factory - autoload :ApiFabricator, 'qa/factory/api_fabricator' - autoload :Base, 'qa/factory/base' - - module Resource - autoload :Sandbox, 'qa/factory/resource/sandbox' - autoload :Group, 'qa/factory/resource/group' - autoload :Issue, 'qa/factory/resource/issue' - autoload :Project, 'qa/factory/resource/project' - autoload :Label, 'qa/factory/resource/label' - autoload :MergeRequest, 'qa/factory/resource/merge_request' - autoload :ProjectImportedFromGithub, 'qa/factory/resource/project_imported_from_github' - autoload :MergeRequestFromFork, 'qa/factory/resource/merge_request_from_fork' - autoload :DeployKey, 'qa/factory/resource/deploy_key' - autoload :DeployToken, 'qa/factory/resource/deploy_token' - autoload :Branch, 'qa/factory/resource/branch' - autoload :CiVariable, 'qa/factory/resource/ci_variable' - autoload :Runner, 'qa/factory/resource/runner' - autoload :PersonalAccessToken, 'qa/factory/resource/personal_access_token' - autoload :KubernetesCluster, 'qa/factory/resource/kubernetes_cluster' - autoload :User, 'qa/factory/resource/user' - autoload :ProjectMilestone, 'qa/factory/resource/project_milestone' - autoload :Wiki, 'qa/factory/resource/wiki' - autoload :File, 'qa/factory/resource/file' - autoload :Fork, 'qa/factory/resource/fork' - autoload :SSHKey, 'qa/factory/resource/ssh_key' - end + module Resource + autoload :ApiFabricator, 'qa/resource/api_fabricator' + autoload :Base, 'qa/resource/base' + + autoload :Sandbox, 'qa/resource/sandbox' + autoload :Group, 'qa/resource/group' + autoload :Issue, 'qa/resource/issue' + autoload :Project, 'qa/resource/project' + autoload :Label, 'qa/resource/label' + autoload :MergeRequest, 'qa/resource/merge_request' + autoload :ProjectImportedFromGithub, 'qa/resource/project_imported_from_github' + autoload :MergeRequestFromFork, 'qa/resource/merge_request_from_fork' + autoload :DeployKey, 'qa/resource/deploy_key' + autoload :DeployToken, 'qa/resource/deploy_token' + autoload :Branch, 'qa/resource/branch' + autoload :CiVariable, 'qa/resource/ci_variable' + autoload :Runner, 'qa/resource/runner' + autoload :PersonalAccessToken, 'qa/resource/personal_access_token' + autoload :KubernetesCluster, 'qa/resource/kubernetes_cluster' + autoload :User, 'qa/resource/user' + autoload :ProjectMilestone, 'qa/resource/project_milestone' + autoload :Wiki, 'qa/resource/wiki' + autoload :File, 'qa/resource/file' + autoload :Fork, 'qa/resource/fork' + autoload :SSHKey, 'qa/resource/ssh_key' module Repository - autoload :Push, 'qa/factory/repository/push' - autoload :ProjectPush, 'qa/factory/repository/project_push' - autoload :WikiPush, 'qa/factory/repository/wiki_push' + autoload :Push, 'qa/resource/repository/push' + autoload :ProjectPush, 'qa/resource/repository/project_push' + autoload :WikiPush, 'qa/resource/repository/wiki_push' end module Settings - autoload :HashedStorage, 'qa/factory/settings/hashed_storage' + autoload :HashedStorage, 'qa/resource/settings/hashed_storage' end end diff --git a/qa/qa/factory/README.md b/qa/qa/factory/README.md deleted file mode 100644 index 42077f60611..00000000000 --- a/qa/qa/factory/README.md +++ /dev/null @@ -1,410 +0,0 @@ -# Factory objects in GitLab QA - -In GitLab QA we are using factories to create resources. - -Factories implementation are primarily done using Browser UI steps, but can also -be done via the API. - -## Why do we need that? - -We need factory objects because we need to reduce duplication when creating -resources for our QA tests. - -## How to properly implement a factory object? - -All factories should inherit from [`Factory::Base`](./base.rb). - -There is only one mandatory method to implement to define a factory. This is the -`#fabricate!` method, which is used to build a resource via the browser UI. -Note that you should only use [Page objects](../page/README.md) to interact with -a Web page in this method. - -Here is an imaginary example: - -```ruby -module QA - module Factory - module Resource - class Shirt < Factory::Base - attr_accessor :name - - def fabricate! - Page::Dashboard::Index.perform do |dashboard_index| - dashboard_index.go_to_new_shirt - end - - Page::Shirt::New.perform do |shirt_new| - shirt_new.set_name(name) - shirt_new.create_shirt! - end - end - end - end - end -end -``` - -### Define API implementation - -A factory may also implement the three following methods to be able to create a -resource via the public GitLab API: - -- `#api_get_path`: The `GET` path to fetch an existing resource. -- `#api_post_path`: The `POST` path to create a new resource. -- `#api_post_body`: The `POST` body (as a Ruby hash) to create a new resource. - -Let's take the `Shirt` factory example, and add these three API methods: - -```ruby -module QA - module Factory - module Resource - class Shirt < Factory::Base - attr_accessor :name - - def fabricate! - # ... same as before - end - - def api_get_path - "/shirt/#{name}" - end - - def api_post_path - "/shirts" - end - - def api_post_body - { - name: name - } - end - end - end - end -end -``` - -The [`Project` factory](./resource/project.rb) is a good real example of Browser -UI and API implementations. - -#### Resource attributes - -A resource may need another resource to exist first. For instance, a project -needs a group to be created in. - -To define a resource attribute, you can use the `attribute` method with a -block using the other factory to fabricate the resource. - -That will allow access to the other resource from your resource object's -methods. You would usually use it in `#fabricate!`, `#api_get_path`, -`#api_post_path`, `#api_post_body`. - -Let's take the `Shirt` factory, and add a `project` attribute to it: - -```ruby -module QA - module Factory - module Resource - class Shirt < Factory::Base - attr_accessor :name - - attribute :project do - Factory::Resource::Project.fabricate! do |resource| - resource.name = 'project-to-create-a-shirt' - end - end - - def fabricate! - project.visit! - - Page::Project::Show.perform do |project_show| - project_show.go_to_new_shirt - end - - Page::Shirt::New.perform do |shirt_new| - shirt_new.set_name(name) - shirt_new.create_shirt! - end - end - - def api_get_path - "/project/#{project.path}/shirt/#{name}" - end - - def api_post_path - "/project/#{project.path}/shirts" - end - - def api_post_body - { - name: name - } - end - end - end - end -end -``` - -**Note that all the attributes are lazily constructed. This means if you want -a specific attribute to be fabricated first, you'll need to call the -attribute method first even if you're not using it.** - -#### Product data attributes - -Once created, you may want to populate a resource with attributes that can be -found in the Web page, or in the API response. -For instance, once you create a project, you may want to store its repository -SSH URL as an attribute. - -Again we could use the `attribute` method with a block, using a page object -to retrieve the data on the page. - -Let's take the `Shirt` factory, and define a `:brand` attribute: - -```ruby -module QA - module Factory - module Resource - class Shirt < Factory::Base - attr_accessor :name - - attribute :project do - Factory::Resource::Project.fabricate! do |resource| - resource.name = 'project-to-create-a-shirt' - end - end - - # Attribute populated from the Browser UI (using the block) - attribute :brand do - Page::Shirt::Show.perform do |shirt_show| - shirt_show.fetch_brand_from_page - end - end - - # ... same as before - end - end - end -end -``` - -**Note again that all the attributes are lazily constructed. This means if -you call `shirt.brand` after moving to the other page, it'll not properly -retrieve the data because we're no longer on the expected page.** - -Consider this: - -```ruby -shirt = - QA::Factory::Resource::Shirt.fabricate! do |resource| - resource.name = "GitLab QA" - end - -shirt.project.visit! - -shirt.brand # => FAIL! -``` - -The above example will fail because now we're on the project page, trying to -construct the brand data from the shirt page, however we moved to the project -page already. There are two ways to solve this, one is that we could try to -retrieve the brand before visiting the project again: - -```ruby -shirt = - QA::Factory::Resource::Shirt.fabricate! do |resource| - resource.name = "GitLab QA" - end - -shirt.brand # => OK! - -shirt.project.visit! - -shirt.brand # => OK! -``` - -The attribute will be stored in the instance therefore all the following calls -will be fine, using the data previously constructed. If we think that this -might be too brittle, we could eagerly construct the data right before -ending fabrication: - -```ruby -module QA - module Factory - module Resource - class Shirt < Factory::Base - # ... same as before - - def fabricate! - project.visit! - - Page::Project::Show.perform do |project_show| - project_show.go_to_new_shirt - end - - Page::Shirt::New.perform do |shirt_new| - shirt_new.set_name(name) - shirt_new.create_shirt! - end - - populate(:brand) # Eagerly construct the data - end - end - end - end -end -``` - -The `populate` method will iterate through its arguments and call each -attribute respectively. Here `populate(:brand)` has the same effect as -just `brand`. Using the populate method makes the intention clearer. - -With this, it will make sure we construct the data right after we create the -shirt. The drawback is that this will always construct the data when the resource is fabricated even if we don't need to use the data. - -Alternatively, we could just make sure we're on the right page before -constructing the brand data: - -```ruby -module QA - module Factory - module Resource - class Shirt < Factory::Base - attr_accessor :name - - attribute :project do - Factory::Resource::Project.fabricate! do |resource| - resource.name = 'project-to-create-a-shirt' - end - end - - # Attribute populated from the Browser UI (using the block) - attribute :brand do - back_url = current_url - visit! - - Page::Shirt::Show.perform do |shirt_show| - shirt_show.fetch_brand_from_page - end - - visit(back_url) - end - - # ... same as before - end - end - end -end -``` - -This will make sure it's on the shirt page before constructing brand, and -move back to the previous page to avoid breaking the state. - -#### Define an attribute based on an API response - -Sometimes, you want to define a resource attribute based on the API response -from its `GET` or `POST` request. For instance, if the creation of a shirt via -the API returns - -```ruby -{ - brand: 'a-brand-new-brand', - style: 't-shirt', - materials: [[:cotton, 80], [:polyamide, 20]] -} -``` - -you may want to store `style` as-is in the resource, and fetch the first value -of the first `materials` item in a `main_fabric` attribute. - -Let's take the `Shirt` factory, and define a `:style` and a `:main_fabric` -attributes: - -```ruby -module QA - module Factory - module Resource - class Shirt < Factory::Base - # ... same as before - - # Attribute from the Shirt factory if present, - # or fetched from the API response if present, - # or a QA::Factory::Base::NoValueError is raised otherwise - attribute :style - - # If the attribute from the Shirt factory is not present, - # and if the API does not contain this field, this block will be - # used to construct the value based on the API response. - attribute :main_fabric do - api_response.&dig(:materials, 0, 0) - end - - # ... same as before - end - end - end -end -``` - -**Notes on attributes precedence:** - -- factory instance variables have the highest precedence -- attributes from the API response take precedence over attributes from the - block (usually from Browser UI) -- attributes without a value will raise a `QA::Factory::Base::NoValueError` error - -## Creating resources in your tests - -To create a resource in your tests, you can call the `.fabricate!` method on the -factory class. -Note that if the factory supports API fabrication, this will use this -fabrication by default. - -Here is an example that will use the API fabrication method under the hood since -it's supported by the `Shirt` factory: - -```ruby -my_shirt = Factory::Resource::Shirt.fabricate! do |shirt| - shirt.name = 'my-shirt' -end - -expect(page).to have_text(my_shirt.name) # => "my-shirt" from the factory's attribute -expect(page).to have_text(my_shirt.brand) # => "a-brand-new-brand" from the API response -expect(page).to have_text(my_shirt.style) # => "t-shirt" from the API response -expect(page).to have_text(my_shirt.main_fabric) # => "cotton" from the API response via the block -``` - -If you explicitly want to use the Browser UI fabrication method, you can call -the `.fabricate_via_browser_ui!` method instead: - -```ruby -my_shirt = Factory::Resource::Shirt.fabricate_via_browser_ui! do |shirt| - shirt.name = 'my-shirt' -end - -expect(page).to have_text(my_shirt.name) # => "my-shirt" from the factory's attribute -expect(page).to have_text(my_shirt.brand) # => the brand name fetched from the `Page::Shirt::Show` page via the block -expect(page).to have_text(my_shirt.style) # => QA::Factory::Base::NoValueError will be raised because no API response nor a block is provided -expect(page).to have_text(my_shirt.main_fabric) # => QA::Factory::Base::NoValueError will be raised because no API response and the block didn't provide a value (because it's also based on the API response) -``` - -You can also explicitly use the API fabrication method, by calling the -`.fabricate_via_api!` method: - -```ruby -my_shirt = Factory::Resource::Shirt.fabricate_via_api! do |shirt| - shirt.name = 'my-shirt' -end -``` - -In this case, the result will be similar to calling `Factory::Resource::Shirt.fabricate!`. - -## Where to ask for help? - -If you need more information, ask for help on `#quality` channel on Slack -(internal, GitLab Team only). - -If you are not a Team Member, and you still need help to contribute, please -open an issue in GitLab CE issue tracker with the `~QA` label. diff --git a/qa/qa/factory/resource/branch.rb b/qa/qa/factory/resource/branch.rb deleted file mode 100644 index b05d1e252ec..00000000000 --- a/qa/qa/factory/resource/branch.rb +++ /dev/null @@ -1,77 +0,0 @@ -module QA - module Factory - module Resource - class Branch < Factory::Base - attr_accessor :project, :branch_name, - :allow_to_push, :allow_to_merge, :protected - - attribute :project do - Factory::Resource::Project.fabricate! do |resource| - resource.name = 'protected-branch-project' - end - end - - def initialize - @branch_name = 'test/branch' - @allow_to_push = true - @allow_to_merge = true - @protected = false - end - - def fabricate! - project.visit! - - Factory::Repository::ProjectPush.fabricate! do |resource| - resource.project = project - resource.file_name = 'kick-off.txt' - resource.commit_message = 'First commit' - end - - branch = Factory::Repository::ProjectPush.fabricate! do |resource| - resource.project = project - resource.file_name = 'README.md' - resource.commit_message = 'Add readme' - resource.branch_name = 'master' - resource.new_branch = false - resource.remote_branch = @branch_name - end - - Page::Project::Show.perform do |page| - page.wait { page.has_content?(branch_name) } - end - - # The upcoming process will make it access the Protected Branches page, - # select the already created branch and protect it according - # to `allow_to_push` variable. - return branch unless @protected - - Page::Project::Menu.perform(&:click_repository_settings) - - Page::Project::Settings::Repository.perform do |setting| - setting.expand_protected_branches do |page| - page.select_branch(branch_name) - - if allow_to_push - page.allow_devs_and_maintainers_to_push - else - page.allow_no_one_to_push - end - - if allow_to_merge - page.allow_devs_and_maintainers_to_merge - else - page.allow_no_one_to_merge - end - - page.wait(reload: false) do - !page.first('.btn-success').disabled? - end - - page.protect_branch - end - end - end - end - end - end -end diff --git a/qa/qa/factory/resource/ci_variable.rb b/qa/qa/factory/resource/ci_variable.rb deleted file mode 100644 index a0aefc61f9f..00000000000 --- a/qa/qa/factory/resource/ci_variable.rb +++ /dev/null @@ -1,30 +0,0 @@ -module QA - module Factory - module Resource - class CiVariable < Factory::Base - attr_accessor :key, :value - - attribute :project do - Factory::Resource::Project.fabricate! do |resource| - resource.name = 'project-with-ci-variables' - resource.description = 'project for adding CI variable test' - end - end - - def fabricate! - project.visit! - - Page::Project::Menu.perform(&:click_ci_cd_settings) - - Page::Project::Settings::CICD.perform do |setting| - setting.expand_ci_variables do |page| - page.fill_variable(key, value) - - page.save_variables - end - end - end - end - end - end -end diff --git a/qa/qa/factory/resource/deploy_key.rb b/qa/qa/factory/resource/deploy_key.rb deleted file mode 100644 index aea99c9f80d..00000000000 --- a/qa/qa/factory/resource/deploy_key.rb +++ /dev/null @@ -1,43 +0,0 @@ -module QA - module Factory - module Resource - class DeployKey < Factory::Base - attr_accessor :title, :key - - attribute :fingerprint do - Page::Project::Settings::Repository.perform do |setting| - setting.expand_deploy_keys do |key| - key_offset = key.key_titles.index do |key_title| - key_title.text == title - end - - key.key_fingerprints[key_offset].text - end - end - end - - attribute :project do - Factory::Resource::Project.fabricate! do |resource| - resource.name = 'project-to-deploy' - resource.description = 'project for adding deploy key test' - end - end - - def fabricate! - project.visit! - - Page::Project::Menu.perform(&:click_repository_settings) - - Page::Project::Settings::Repository.perform do |setting| - setting.expand_deploy_keys do |page| - page.fill_key_title(title) - page.fill_key_value(key) - - page.add_key - end - end - end - end - end - end -end diff --git a/qa/qa/factory/resource/deploy_token.rb b/qa/qa/factory/resource/deploy_token.rb deleted file mode 100644 index 68e98f0aa01..00000000000 --- a/qa/qa/factory/resource/deploy_token.rb +++ /dev/null @@ -1,50 +0,0 @@ -module QA - module Factory - module Resource - class DeployToken < Factory::Base - attr_accessor :name, :expires_at - - attribute :username do - Page::Project::Settings::Repository.perform do |page| - page.expand_deploy_tokens do |token| - token.token_username - end - end - end - - attribute :password do - Page::Project::Settings::Repository.perform do |page| - page.expand_deploy_tokens do |token| - token.token_password - end - end - end - - attribute :project do - Factory::Resource::Project.fabricate! do |resource| - resource.name = 'project-to-deploy' - resource.description = 'project for adding deploy token test' - end - end - - def fabricate! - project.visit! - - Page::Project::Menu.act do - click_repository_settings - end - - Page::Project::Settings::Repository.perform do |setting| - setting.expand_deploy_tokens do |page| - page.fill_token_name(name) - page.fill_token_expires_at(expires_at) - page.fill_scopes(read_repository: true, read_registry: false) - - page.add_token - end - end - end - end - end - end -end diff --git a/qa/qa/factory/resource/file.rb b/qa/qa/factory/resource/file.rb deleted file mode 100644 index 1148876c2d3..00000000000 --- a/qa/qa/factory/resource/file.rb +++ /dev/null @@ -1,38 +0,0 @@ -# frozen_string_literal: true - -module QA - module Factory - module Resource - class File < Factory::Base - attr_accessor :name, - :content, - :commit_message - - attribute :project do - Factory::Resource::Project.fabricate! do |resource| - resource.name = 'project-with-new-file' - end - end - - def initialize - @name = 'QA Test - File name' - @content = 'QA Test - File content' - @commit_message = 'QA Test - Commit message' - end - - def fabricate! - project.visit! - - Page::Project::Show.perform(&:create_new_file!) - - Page::File::Form.perform do |page| - page.add_name(@name) - page.add_content(@content) - page.add_commit_message(@commit_message) - page.commit_changes - end - end - end - end - end -end diff --git a/qa/qa/factory/resource/fork.rb b/qa/qa/factory/resource/fork.rb deleted file mode 100644 index b1e874af893..00000000000 --- a/qa/qa/factory/resource/fork.rb +++ /dev/null @@ -1,70 +0,0 @@ -module QA - module Factory - module Resource - class Fork < Factory::Base - attribute :push do - Factory::Repository::ProjectPush.fabricate! - end - - attribute :user do - Factory::Resource::User.fabricate! do |resource| - if Runtime::Env.forker? - resource.username = Runtime::Env.forker_username - resource.password = Runtime::Env.forker_password - end - end - end - - def visit_project_with_retry - # The user intermittently fails to stay signed in after visiting the - # project page. The new user is registered and then signs in and a - # screenshot shows that signing in was successful. Then the project - # page is visited but a screenshot shows the user is no longer signed - # in. It's difficult to reproduce locally but GDK logs don't seem to - # show anything unexpected. This method attempts to work around the - # problem and capture data to help troubleshoot. - - Capybara::Screenshot.screenshot_and_save_page - - start = Time.now - - while Time.now - start < 20 - push.project.visit! - - puts "Visited project page" - Capybara::Screenshot.screenshot_and_save_page - - return if Page::Main::Menu.act { has_personal_area?(wait: 0) } - - puts "Not signed in. Attempting to sign in again." - Capybara::Screenshot.screenshot_and_save_page - - Runtime::Browser.visit(:gitlab, Page::Main::Login) - - Page::Main::Login.perform do |login| - login.sign_in_using_credentials(user) - end - end - - raise "Failed to load project page and stay logged in" - end - - def fabricate! - populate(:push, :user) - - visit_project_with_retry - - Page::Project::Show.perform(&:fork_project) - - Page::Project::Fork::New.perform do |fork_new| - fork_new.choose_namespace(user.name) - end - - Page::Layout::Banner.perform do |page| - page.has_notice?('The project was successfully forked.') - end - end - end - end - end -end diff --git a/qa/qa/factory/resource/group.rb b/qa/qa/factory/resource/group.rb deleted file mode 100644 index 45e49da86f9..00000000000 --- a/qa/qa/factory/resource/group.rb +++ /dev/null @@ -1,68 +0,0 @@ -module QA - module Factory - module Resource - class Group < Factory::Base - attr_accessor :path, :description - - attribute :sandbox do - Factory::Resource::Sandbox.fabricate! - end - - attribute :id - - def initialize - @path = Runtime::Namespace.name - @description = "QA test run at #{Runtime::Namespace.time}" - end - - def fabricate! - sandbox.visit! - - Page::Group::Show.perform do |group_show| - if group_show.has_subgroup?(path) - group_show.go_to_subgroup(path) - else - group_show.go_to_new_subgroup - - Page::Group::New.perform do |group_new| - group_new.set_path(path) - group_new.set_description(description) - group_new.set_visibility('Public') - group_new.create - end - - # Ensure that the group was actually created - group_show.wait(time: 1) do - group_show.has_text?(path) && - group_show.has_new_project_or_subgroup_dropdown? - end - end - end - end - - def fabricate_via_api! - resource_web_url(api_get) - rescue ResourceNotFoundError - super - end - - def api_get_path - "/groups/#{CGI.escape("#{sandbox.path}/#{path}")}" - end - - def api_post_path - '/groups' - end - - def api_post_body - { - parent_id: sandbox.id, - path: path, - name: path, - visibility: 'public' - } - end - end - end - end -end diff --git a/qa/qa/factory/resource/issue.rb b/qa/qa/factory/resource/issue.rb deleted file mode 100644 index 3a28e0d5aa6..00000000000 --- a/qa/qa/factory/resource/issue.rb +++ /dev/null @@ -1,30 +0,0 @@ -module QA - module Factory - module Resource - class Issue < Factory::Base - attr_writer :description - - attribute :project do - Factory::Resource::Project.fabricate! do |resource| - resource.name = 'project-for-issues' - resource.description = 'project for adding issues' - end - end - - attribute :title - - def fabricate! - project.visit! - - Page::Project::Show.perform(&:go_to_new_issue) - - Page::Project::Issue::New.perform do |page| - page.add_title(@title) - page.add_description(@description) - page.create_new_issue - end - end - end - end - end -end diff --git a/qa/qa/factory/resource/kubernetes_cluster.rb b/qa/qa/factory/resource/kubernetes_cluster.rb deleted file mode 100644 index aac6864f42f..00000000000 --- a/qa/qa/factory/resource/kubernetes_cluster.rb +++ /dev/null @@ -1,57 +0,0 @@ -require 'securerandom' - -module QA - module Factory - module Resource - class KubernetesCluster < Factory::Base - attr_writer :project, :cluster, - :install_helm_tiller, :install_ingress, :install_prometheus, :install_runner - - attribute :ingress_ip do - Page::Project::Operations::Kubernetes::Show.perform(&:ingress_ip) - end - - def fabricate! - @project.visit! - - Page::Project::Menu.perform( - &:click_operations_kubernetes) - - Page::Project::Operations::Kubernetes::Index.perform( - &:add_kubernetes_cluster) - - Page::Project::Operations::Kubernetes::Add.perform( - &:add_existing_cluster) - - Page::Project::Operations::Kubernetes::AddExisting.perform do |page| - page.set_cluster_name(@cluster.cluster_name) - page.set_api_url(@cluster.api_url) - page.set_ca_certificate(@cluster.ca_certificate) - page.set_token(@cluster.token) - page.check_rbac! if @cluster.rbac - page.add_cluster! - end - - if @install_helm_tiller - Page::Project::Operations::Kubernetes::Show.perform do |page| - # We must wait a few seconds for permissions to be set up correctly for new cluster - sleep 10 - - # Helm must be installed before everything else - page.install!(:helm) - page.await_installed(:helm) - - page.install!(:ingress) if @install_ingress - page.install!(:prometheus) if @install_prometheus - page.install!(:runner) if @install_runner - - page.await_installed(:ingress) if @install_ingress - page.await_installed(:prometheus) if @install_prometheus - page.await_installed(:runner) if @install_runner - end - end - end - end - end - end -end diff --git a/qa/qa/factory/resource/label.rb b/qa/qa/factory/resource/label.rb deleted file mode 100644 index 32bc519b48c..00000000000 --- a/qa/qa/factory/resource/label.rb +++ /dev/null @@ -1,39 +0,0 @@ -require 'securerandom' - -module QA - module Factory - module Resource - class Label < Factory::Base - attr_accessor :description, :color - - attribute :title - - attribute :project do - Factory::Resource::Project.fabricate! do |resource| - resource.name = 'project-with-label' - end - end - - def initialize - @title = "qa-test-#{SecureRandom.hex(8)}" - @description = 'This is a test label' - @color = '#0033CC' - end - - def fabricate! - project.visit! - - Page::Project::Menu.perform(&:go_to_labels) - Page::Label::Index.perform(&:go_to_new_label) - - Page::Label::New.perform do |page| - page.fill_title(@title) - page.fill_description(@description) - page.fill_color(@color) - page.create_label - end - end - end - end - end -end diff --git a/qa/qa/factory/resource/merge_request.rb b/qa/qa/factory/resource/merge_request.rb deleted file mode 100644 index 4b7d2287f98..00000000000 --- a/qa/qa/factory/resource/merge_request.rb +++ /dev/null @@ -1,71 +0,0 @@ -require 'securerandom' - -module QA - module Factory - module Resource - class MergeRequest < Factory::Base - attr_accessor :title, - :description, - :source_branch, - :target_branch, - :assignee, - :milestone, - :labels - - attribute :project do - Factory::Resource::Project.fabricate! do |resource| - resource.name = 'project-with-merge-request' - end - end - - attribute :target do - project.visit! - - Factory::Repository::ProjectPush.fabricate! do |resource| - resource.project = project - resource.branch_name = 'master' - resource.remote_branch = target_branch - end - end - - attribute :source do - Factory::Repository::ProjectPush.fabricate! do |resource| - resource.project = project - resource.branch_name = target_branch - resource.remote_branch = source_branch - resource.new_branch = false - resource.file_name = "added_file.txt" - resource.file_content = "File Added" - end - end - - def initialize - @title = 'QA test - merge request' - @description = 'This is a test merge request' - @source_branch = "qa-test-feature-#{SecureRandom.hex(8)}" - @target_branch = "master" - @assignee = nil - @milestone = nil - @labels = [] - end - - def fabricate! - populate(:target, :source) - - project.visit! - Page::Project::Show.perform(&:new_merge_request) - Page::MergeRequest::New.perform do |page| - page.fill_title(@title) - page.fill_description(@description) - page.choose_milestone(@milestone) if @milestone - labels.each do |label| - page.select_label(label) - end - - page.create_merge_request - end - end - end - end - end -end diff --git a/qa/qa/factory/resource/merge_request_from_fork.rb b/qa/qa/factory/resource/merge_request_from_fork.rb deleted file mode 100644 index 1311bf625a6..00000000000 --- a/qa/qa/factory/resource/merge_request_from_fork.rb +++ /dev/null @@ -1,31 +0,0 @@ -module QA - module Factory - module Resource - class MergeRequestFromFork < MergeRequest - attr_accessor :fork_branch - - attribute :fork do - Factory::Resource::Fork.fabricate! - end - - attribute :push do - Factory::Repository::ProjectPush.fabricate! do |resource| - resource.project = fork - resource.branch_name = fork_branch - resource.file_name = 'file2.txt' - resource.user = fork.user - end - end - - def fabricate! - populate(:push) - - fork.visit! - - Page::Project::Show.perform(&:new_merge_request) - Page::MergeRequest::New.perform(&:create_merge_request) - end - end - end - end -end diff --git a/qa/qa/factory/resource/personal_access_token.rb b/qa/qa/factory/resource/personal_access_token.rb deleted file mode 100644 index ceb0f1c3d75..00000000000 --- a/qa/qa/factory/resource/personal_access_token.rb +++ /dev/null @@ -1,27 +0,0 @@ -module QA - module Factory - module Resource - ## - # Create a personal access token that can be used by the api - # - class PersonalAccessToken < Factory::Base - attr_accessor :name - - attribute :access_token do - Page::Profile::PersonalAccessTokens.perform(&:created_access_token) - end - - def fabricate! - Page::Main::Menu.perform(&:go_to_profile_settings) - Page::Profile::Menu.perform(&:click_access_tokens) - - Page::Profile::PersonalAccessTokens.perform do |page| - page.fill_token_name(name || 'api-test-token') - page.check_api - page.create_token - end - end - end - end - end -end diff --git a/qa/qa/factory/resource/project.rb b/qa/qa/factory/resource/project.rb deleted file mode 100644 index f691ae5a342..00000000000 --- a/qa/qa/factory/resource/project.rb +++ /dev/null @@ -1,78 +0,0 @@ -require 'securerandom' - -module QA - module Factory - module Resource - class Project < Factory::Base - attribute :name - attribute :description - - attribute :group do - Factory::Resource::Group.fabricate! - end - - attribute :repository_ssh_location do - Page::Project::Show.perform do |page| - page.choose_repository_clone_ssh - page.repository_location - end - end - - attribute :repository_http_location do - Page::Project::Show.perform do |page| - page.choose_repository_clone_http - page.repository_location - end - end - - def initialize - @description = 'My awesome project' - end - - def name=(raw_name) - @name = "#{raw_name}-#{SecureRandom.hex(8)}" - end - - def fabricate! - group.visit! - - Page::Group::Show.perform(&:go_to_new_project) - - Page::Project::New.perform do |page| - page.choose_test_namespace - page.choose_name(@name) - page.add_description(@description) - page.set_visibility('Public') - page.create_new_project - end - end - - def api_get_path - "/projects/#{name}" - end - - def api_post_path - '/projects' - end - - def api_post_body - { - namespace_id: group.id, - path: name, - name: name, - description: description, - visibility: 'public' - } - end - - private - - def transform_api_resource(resource) - resource[:repository_ssh_location] = Git::Location.new(resource[:ssh_url_to_repo]) - resource[:repository_http_location] = Git::Location.new(resource[:http_url_to_repo]) - resource - end - end - end - end -end diff --git a/qa/qa/factory/resource/project_imported_from_github.rb b/qa/qa/factory/resource/project_imported_from_github.rb deleted file mode 100644 index ce20641e6cc..00000000000 --- a/qa/qa/factory/resource/project_imported_from_github.rb +++ /dev/null @@ -1,36 +0,0 @@ -require 'securerandom' - -module QA - module Factory - module Resource - class ProjectImportedFromGithub < Resource::Project - attr_accessor :name - attr_writer :personal_access_token, :github_repository_path - - attribute :group do - Factory::Resource::Group.fabricate! - end - - def fabricate! - group.visit! - - Page::Group::Show.perform(&:go_to_new_project) - - Page::Project::New.perform do |page| - page.go_to_import_project - end - - Page::Project::New.perform do |page| - page.go_to_github_import - end - - Page::Project::Import::Github.perform do |page| - page.add_personal_access_token(@personal_access_token) - page.list_repos - page.import!(@github_repository_path, @name) - end - end - end - end - end -end diff --git a/qa/qa/factory/resource/project_milestone.rb b/qa/qa/factory/resource/project_milestone.rb deleted file mode 100644 index 383f534c12c..00000000000 --- a/qa/qa/factory/resource/project_milestone.rb +++ /dev/null @@ -1,36 +0,0 @@ -module QA - module Factory - module Resource - class ProjectMilestone < Factory::Base - attr_reader :title - attr_accessor :description - - attribute :project do - Factory::Resource::Project.fabricate! - end - - def title=(title) - @title = "#{title}-#{SecureRandom.hex(4)}" - @description = 'A milestone' - end - - def fabricate! - project.visit! - - Page::Project::Menu.perform do |page| - page.click_issues - page.click_milestones - end - - Page::Project::Milestone::Index.perform(&:click_new_milestone) - - Page::Project::Milestone::New.perform do |milestone_new| - milestone_new.set_title(@title) - milestone_new.set_description(@description) - milestone_new.create_new_milestone - end - end - end - end - end -end diff --git a/qa/qa/factory/resource/runner.rb b/qa/qa/factory/resource/runner.rb deleted file mode 100644 index 7108db1e55a..00000000000 --- a/qa/qa/factory/resource/runner.rb +++ /dev/null @@ -1,49 +0,0 @@ -require 'securerandom' - -module QA - module Factory - module Resource - class Runner < Factory::Base - attr_writer :name, :tags, :image - - attribute :project do - Factory::Resource::Project.fabricate! do |resource| - resource.name = 'project-with-ci-cd' - resource.description = 'Project with CI/CD Pipelines' - end - end - - def name - @name || "qa-runner-#{SecureRandom.hex(4)}" - end - - def tags - @tags || %w[qa e2e] - end - - def image - @image || 'gitlab/gitlab-runner:alpine' - end - - def fabricate! - project.visit! - - Page::Project::Menu.perform(&:click_ci_cd_settings) - - Service::Runner.new(name).tap do |runner| - Page::Project::Settings::CICD.perform do |settings| - settings.expand_runners_settings do |runners| - runner.pull - runner.token = runners.registration_token - runner.address = runners.coordinator_address - runner.tags = tags - runner.image = image - runner.register! - end - end - end - end - end - end - end -end diff --git a/qa/qa/factory/resource/sandbox.rb b/qa/qa/factory/resource/sandbox.rb deleted file mode 100644 index a125bac65dd..00000000000 --- a/qa/qa/factory/resource/sandbox.rb +++ /dev/null @@ -1,60 +0,0 @@ -module QA - module Factory - module Resource - ## - # Ensure we're in our sandbox namespace, either by navigating to it or by - # creating it if it doesn't yet exist. - # - class Sandbox < Factory::Base - attr_reader :path - - attribute :id - - def initialize - @path = Runtime::Namespace.sandbox_name - end - - def fabricate! - Page::Main::Menu.perform(&:go_to_groups) - - Page::Dashboard::Groups.perform do |page| - if page.has_group?(path) - page.go_to_group(path) - else - page.go_to_new_group - - Page::Group::New.perform do |group| - group.set_path(path) - group.set_description('GitLab QA Sandbox Group') - group.set_visibility('Public') - group.create - end - end - end - end - - def fabricate_via_api! - resource_web_url(api_get) - rescue ResourceNotFoundError - super - end - - def api_get_path - "/groups/#{path}" - end - - def api_post_path - '/groups' - end - - def api_post_body - { - path: path, - name: path, - visibility: 'public' - } - end - end - end - end -end diff --git a/qa/qa/factory/resource/ssh_key.rb b/qa/qa/factory/resource/ssh_key.rb deleted file mode 100644 index 6f952eda36f..00000000000 --- a/qa/qa/factory/resource/ssh_key.rb +++ /dev/null @@ -1,28 +0,0 @@ -# frozen_string_literal: true - -module QA - module Factory - module Resource - class SSHKey < Factory::Base - extend Forwardable - - attr_accessor :title - - def_delegators :key, :private_key, :public_key, :fingerprint - - def key - @key ||= Runtime::Key::RSA.new - end - - def fabricate! - Page::Main::Menu.perform(&:go_to_profile_settings) - Page::Profile::Menu.perform(&:click_ssh_keys) - - Page::Profile::SSHKeys.perform do |page| - page.add_key(public_key, title) - end - end - end - end - end -end diff --git a/qa/qa/factory/resource/user.rb b/qa/qa/factory/resource/user.rb deleted file mode 100644 index e361face1f0..00000000000 --- a/qa/qa/factory/resource/user.rb +++ /dev/null @@ -1,56 +0,0 @@ -require 'securerandom' - -module QA - module Factory - module Resource - class User < Factory::Base - attr_reader :unique_id - attr_writer :username, :password - - def initialize - @unique_id = SecureRandom.hex(8) - end - - def username - @username ||= "qa-user-#{unique_id}" - end - - def password - @password ||= 'password' - end - - def name - @name ||= username - end - - def email - @email ||= "#{username}@example.com" - end - - def credentials_given? - defined?(@username) && defined?(@password) - end - - def fabricate! - # Don't try to log-out if we're not logged-in - if Page::Main::Menu.perform { |p| p.has_personal_area?(wait: 0) } - Page::Main::Menu.perform { |main| main.sign_out } - end - - if credentials_given? - Page::Main::Login.perform do |login| - login.sign_in_using_credentials(self) - end - else - Page::Main::Login.perform do |login| - login.switch_to_register_tab - end - Page::Main::SignUp.perform do |signup| - signup.sign_up!(self) - end - end - end - end - end - end -end diff --git a/qa/qa/factory/resource/wiki.rb b/qa/qa/factory/resource/wiki.rb deleted file mode 100644 index 769f394e85c..00000000000 --- a/qa/qa/factory/resource/wiki.rb +++ /dev/null @@ -1,30 +0,0 @@ -module QA - module Factory - module Resource - class Wiki < Factory::Base - attr_accessor :title, :content, :message - - attribute :project do - Factory::Resource::Project.fabricate! do |resource| - resource.name = 'project-for-wikis' - resource.description = 'project for adding wikis' - end - end - - def fabricate! - project.visit! - - Page::Project::Menu.perform { |menu_side| menu_side.click_wiki } - - Page::Project::Wiki::New.perform do |wiki_new| - wiki_new.go_to_create_first_page - wiki_new.set_title(@title) - wiki_new.set_content(@content) - wiki_new.set_message(@message) - wiki_new.create_new_page - end - end - end - end - end -end diff --git a/qa/qa/page/main/login.rb b/qa/qa/page/main/login.rb index 94b9486b0d5..97ffe0e5716 100644 --- a/qa/qa/page/main/login.rb +++ b/qa/qa/page/main/login.rb @@ -65,7 +65,7 @@ module QA end def sign_in_using_admin_credentials - admin = QA::Factory::Resource::User.new.tap do |user| + admin = QA::Resource::User.new.tap do |user| user.username = QA::Runtime::User.admin_username user.password = QA::Runtime::User.admin_password end diff --git a/qa/qa/page/merge_request/show.rb b/qa/qa/page/merge_request/show.rb index 376606afb5d..2e69a89e386 100644 --- a/qa/qa/page/merge_request/show.rb +++ b/qa/qa/page/merge_request/show.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module QA module Page module MergeRequest @@ -23,6 +25,32 @@ module QA element :squash_checkbox end + view 'app/views/projects/merge_requests/show.html.haml' do + element :notes_tab + element :diffs_tab + end + + view 'app/assets/javascripts/diffs/components/diff_line_gutter_content.vue' do + element :diff_comment + end + + view 'app/assets/javascripts/notes/components/comment_form.vue' do + element :note_dropdown + element :discussion_option + end + + view 'app/assets/javascripts/notes/components/note_form.vue' do + element :reply_input + end + + view 'app/assets/javascripts/notes/components/noteable_discussion.vue' do + element :discussion_reply + end + + view 'app/assets/javascripts/diffs/components/inline_diff_table_row.vue' do + element :new_diff_line + end + view 'app/views/shared/issuable/_sidebar.html.haml' do element :labels_block end @@ -106,6 +134,35 @@ module QA click_element :squash_checkbox end + + def go_to_discussions_tab + click_element :notes_tab + end + + def go_to_diffs_tab + click_element :diffs_tab + end + + def add_comment_to_diff(text) + wait(time: 5) do + page.has_text?("No newline at end of file") + end + all_elements(:new_diff_line).first.hover + click_element :diff_comment + fill_element :reply_input, text + end + + def start_discussion(text) + fill_element :comment_input, text + click_element :note_dropdown + click_element :discussion_option + click_element :comment_button + end + + def reply_to_discussion(reply_text) + all_elements(:discussion_reply).last.click + fill_element :reply_input, reply_text + end end end end diff --git a/qa/qa/resource/README.md b/qa/qa/resource/README.md new file mode 100644 index 00000000000..4cdeb3f42a2 --- /dev/null +++ b/qa/qa/resource/README.md @@ -0,0 +1,392 @@ +# Resource class in GitLab QA + +Resources are primarily created using Browser UI steps, but can also +be created via the API. + +## How to properly implement a resource class? + +All resource classes should inherit from [`Resource::Base`](./base.rb). + +There is only one mandatory method to implement to define a resource class. +This is the `#fabricate!` method, which is used to build the resource via the +browser UI. Note that you should only use [Page objects](../page/README.md) to +interact with a Web page in this method. + +Here is an imaginary example: + +```ruby +module QA + module Resource + class Shirt < Base + attr_accessor :name + + def fabricate! + Page::Dashboard::Index.perform do |dashboard_index| + dashboard_index.go_to_new_shirt + end + + Page::Shirt::New.perform do |shirt_new| + shirt_new.set_name(name) + shirt_new.create_shirt! + end + end + end + end +end +``` + +### Define API implementation + +A resource class may also implement the three following methods to be able to +create the resource via the public GitLab API: + +- `#api_get_path`: The `GET` path to fetch an existing resource. +- `#api_post_path`: The `POST` path to create a new resource. +- `#api_post_body`: The `POST` body (as a Ruby hash) to create a new resource. + +Let's take the `Shirt` resource class, and add these three API methods: + +```ruby +module QA + module Resource + class Shirt < Base + attr_accessor :name + + def fabricate! + # ... same as before + end + + def api_get_path + "/shirt/#{name}" + end + + def api_post_path + "/shirts" + end + + def api_post_body + { + name: name + } + end + end + end +end +``` + +The [`Project` resource](./project.rb) is a good real example of Browser +UI and API implementations. + +#### Resource attributes + +A resource may need another resource to exist first. For instance, a project +needs a group to be created in. + +To define a resource attribute, you can use the `attribute` method with a +block using the other resource class to fabricate the resource. + +That will allow access to the other resource from your resource object's +methods. You would usually use it in `#fabricate!`, `#api_get_path`, +`#api_post_path`, `#api_post_body`. + +Let's take the `Shirt` resource class, and add a `project` attribute to it: + +```ruby +module QA + module Resource + class Shirt < Base + attr_accessor :name + + attribute :project do + Project.fabricate! do |resource| + resource.name = 'project-to-create-a-shirt' + end + end + + def fabricate! + project.visit! + + Page::Project::Show.perform do |project_show| + project_show.go_to_new_shirt + end + + Page::Shirt::New.perform do |shirt_new| + shirt_new.set_name(name) + shirt_new.create_shirt! + end + end + + def api_get_path + "/project/#{project.path}/shirt/#{name}" + end + + def api_post_path + "/project/#{project.path}/shirts" + end + + def api_post_body + { + name: name + } + end + end + end +end +``` + +**Note that all the attributes are lazily constructed. This means if you want +a specific attribute to be fabricated first, you'll need to call the +attribute method first even if you're not using it.** + +#### Product data attributes + +Once created, you may want to populate a resource with attributes that can be +found in the Web page, or in the API response. +For instance, once you create a project, you may want to store its repository +SSH URL as an attribute. + +Again we could use the `attribute` method with a block, using a page object +to retrieve the data on the page. + +Let's take the `Shirt` resource class, and define a `:brand` attribute: + +```ruby +module QA + module Resource + class Shirt < Base + attr_accessor :name + + attribute :project do + Project.fabricate! do |resource| + resource.name = 'project-to-create-a-shirt' + end + end + + # Attribute populated from the Browser UI (using the block) + attribute :brand do + Page::Shirt::Show.perform do |shirt_show| + shirt_show.fetch_brand_from_page + end + end + + # ... same as before + end + end +end +``` + +**Note again that all the attributes are lazily constructed. This means if +you call `shirt.brand` after moving to the other page, it'll not properly +retrieve the data because we're no longer on the expected page.** + +Consider this: + +```ruby +shirt = + QA::Resource::Shirt.fabricate! do |resource| + resource.name = "GitLab QA" + end + +shirt.project.visit! + +shirt.brand # => FAIL! +``` + +The above example will fail because now we're on the project page, trying to +construct the brand data from the shirt page, however we moved to the project +page already. There are two ways to solve this, one is that we could try to +retrieve the brand before visiting the project again: + +```ruby +shirt = + QA::Resource::Shirt.fabricate! do |resource| + resource.name = "GitLab QA" + end + +shirt.brand # => OK! + +shirt.project.visit! + +shirt.brand # => OK! +``` + +The attribute will be stored in the instance therefore all the following calls +will be fine, using the data previously constructed. If we think that this +might be too brittle, we could eagerly construct the data right before +ending fabrication: + +```ruby +module QA + module Resource + class Shirt < Base + # ... same as before + + def fabricate! + project.visit! + + Page::Project::Show.perform do |project_show| + project_show.go_to_new_shirt + end + + Page::Shirt::New.perform do |shirt_new| + shirt_new.set_name(name) + shirt_new.create_shirt! + end + + populate(:brand) # Eagerly construct the data + end + end + end +end +``` + +The `populate` method will iterate through its arguments and call each +attribute respectively. Here `populate(:brand)` has the same effect as +just `brand`. Using the populate method makes the intention clearer. + +With this, it will make sure we construct the data right after we create the +shirt. The drawback is that this will always construct the data when the +resource is fabricated even if we don't need to use the data. + +Alternatively, we could just make sure we're on the right page before +constructing the brand data: + +```ruby +module QA + module Resource + class Shirt < Base + attr_accessor :name + + attribute :project do + Project.fabricate! do |resource| + resource.name = 'project-to-create-a-shirt' + end + end + + # Attribute populated from the Browser UI (using the block) + attribute :brand do + back_url = current_url + visit! + + Page::Shirt::Show.perform do |shirt_show| + shirt_show.fetch_brand_from_page + end + + visit(back_url) + end + + # ... same as before + end + end +end +``` + +This will make sure it's on the shirt page before constructing brand, and +move back to the previous page to avoid breaking the state. + +#### Define an attribute based on an API response + +Sometimes, you want to define a resource attribute based on the API response +from its `GET` or `POST` request. For instance, if the creation of a shirt via +the API returns + +```ruby +{ + brand: 'a-brand-new-brand', + style: 't-shirt', + materials: [[:cotton, 80], [:polyamide, 20]] +} +``` + +you may want to store `style` as-is in the resource, and fetch the first value +of the first `materials` item in a `main_fabric` attribute. + +Let's take the `Shirt` resource class, and define a `:style` and a +`:main_fabric` attributes: + +```ruby +module QA + module Resource + class Shirt < Base + # ... same as before + + # @style from the instance if present, + # or fetched from the API response if present, + # or a QA::Resource::Base::NoValueError is raised otherwise + attribute :style + + # If @main_fabric is not present, + # and if the API does not contain this field, this block will be + # used to construct the value based on the API response, and + # store the result in @main_fabric + attribute :main_fabric do + api_response.&dig(:materials, 0, 0) + end + + # ... same as before + end + end +end +``` + +**Notes on attributes precedence:** + +- resource instance variables have the highest precedence +- attributes from the API response take precedence over attributes from the + block (usually from Browser UI) +- attributes without a value will raise a `QA::Resource::Base::NoValueError` error + +## Creating resources in your tests + +To create a resource in your tests, you can call the `.fabricate!` method on +the resource class. +Note that if the resource class supports API fabrication, this will use this +fabrication by default. + +Here is an example that will use the API fabrication method under the hood +since it's supported by the `Shirt` resource class: + +```ruby +my_shirt = Resource::Shirt.fabricate! do |shirt| + shirt.name = 'my-shirt' +end + +expect(page).to have_text(my_shirt.name) # => "my-shirt" from the resource's instance variable +expect(page).to have_text(my_shirt.brand) # => "a-brand-new-brand" from the API response +expect(page).to have_text(my_shirt.style) # => "t-shirt" from the API response +expect(page).to have_text(my_shirt.main_fabric) # => "cotton" from the API response via the block +``` + +If you explicitly want to use the Browser UI fabrication method, you can call +the `.fabricate_via_browser_ui!` method instead: + +```ruby +my_shirt = Resource::Shirt.fabricate_via_browser_ui! do |shirt| + shirt.name = 'my-shirt' +end + +expect(page).to have_text(my_shirt.name) # => "my-shirt" from the resource's instance variable +expect(page).to have_text(my_shirt.brand) # => the brand name fetched from the `Page::Shirt::Show` page via the block +expect(page).to have_text(my_shirt.style) # => QA::Resource::Base::NoValueError will be raised because no API response nor a block is provided +expect(page).to have_text(my_shirt.main_fabric) # => QA::Resource::Base::NoValueError will be raised because no API response and the block didn't provide a value (because it's also based on the API response) +``` + +You can also explicitly use the API fabrication method, by calling the +`.fabricate_via_api!` method: + +```ruby +my_shirt = Resource::Shirt.fabricate_via_api! do |shirt| + shirt.name = 'my-shirt' +end +``` + +In this case, the result will be similar to calling +`Resource::Shirt.fabricate!`. + +## Where to ask for help? + +If you need more information, ask for help on `#quality` channel on Slack +(internal, GitLab Team only). + +If you are not a Team Member, and you still need help to contribute, please +open an issue in GitLab CE issue tracker with the `~QA` label. diff --git a/qa/qa/factory/api_fabricator.rb b/qa/qa/resource/api_fabricator.rb index b1cfb6c9783..3762a94f312 100644 --- a/qa/qa/factory/api_fabricator.rb +++ b/qa/qa/resource/api_fabricator.rb @@ -5,7 +5,7 @@ require 'active_support/core_ext/object/deep_dup' require 'capybara/dsl' module QA - module Factory + module Resource module ApiFabricator include Airborne include Capybara::DSL @@ -27,7 +27,7 @@ module QA def fabricate_via_api! unless api_support? - raise NotImplementedError, "Factory #{self.class.name} does not support fabrication via the API!" + raise NotImplementedError, "Resource #{self.class.name} does not support fabrication via the API!" end resource_web_url(api_post) @@ -52,14 +52,18 @@ module QA end def api_get - url = Runtime::API::Request.new(api_client, api_get_path).url + process_api_response(parse_body(api_get_from(api_get_path))) + end + + def api_get_from(get_path) + url = Runtime::API::Request.new(api_client, get_path).url response = get(url) unless response.code == HTTP_STATUS_OK raise ResourceNotFoundError, "Resource at #{url} could not be found (#{response.code}): `#{response}`." end - process_api_response(parse_body(response)) + response end def api_post @@ -89,8 +93,8 @@ module QA self.api_resource = transform_api_resource(parsed_response.deep_dup) end - def transform_api_resource(resource) - resource + def transform_api_resource(api_resource) + api_resource end end end diff --git a/qa/qa/factory/base.rb b/qa/qa/resource/base.rb index 75438b77bf3..f3eefb70520 100644 --- a/qa/qa/factory/base.rb +++ b/qa/qa/resource/base.rb @@ -4,7 +4,7 @@ require 'forwardable' require 'capybara/dsl' module QA - module Factory + module Resource class Base extend SingleForwardable include ApiFabricator @@ -58,11 +58,11 @@ module QA def self.fabricate_via_browser_ui!(*args, &prepare_block) options = args.extract_options! - factory = options.fetch(:factory) { new } + resource = options.fetch(:resource) { new } parents = options.fetch(:parents) { [] } - do_fabricate!(factory: factory, prepare_block: prepare_block, parents: parents) do - log_fabrication(:browser_ui, factory, parents, args) { factory.fabricate!(*args) } + do_fabricate!(resource: resource, prepare_block: prepare_block, parents: parents) do + log_fabrication(:browser_ui, resource, parents, args) { resource.fabricate!(*args) } current_url end @@ -70,29 +70,29 @@ module QA def self.fabricate_via_api!(*args, &prepare_block) options = args.extract_options! - factory = options.fetch(:factory) { new } + resource = options.fetch(:resource) { new } parents = options.fetch(:parents) { [] } - raise NotImplementedError unless factory.api_support? + raise NotImplementedError unless resource.api_support? - factory.eager_load_api_client! + resource.eager_load_api_client! - do_fabricate!(factory: factory, prepare_block: prepare_block, parents: parents) do - log_fabrication(:api, factory, parents, args) { factory.fabricate_via_api! } + do_fabricate!(resource: resource, prepare_block: prepare_block, parents: parents) do + log_fabrication(:api, resource, parents, args) { resource.fabricate_via_api! } end end - def self.do_fabricate!(factory:, prepare_block:, parents: []) - prepare_block.call(factory) if prepare_block + def self.do_fabricate!(resource:, prepare_block:, parents: []) + prepare_block.call(resource) if prepare_block resource_web_url = yield - factory.web_url = resource_web_url + resource.web_url = resource_web_url - factory + resource end private_class_method :do_fabricate! - def self.log_fabrication(method, factory, parents, args) + def self.log_fabrication(method, resource, parents, args) return yield unless Runtime::Env.debug? start = Time.now @@ -111,7 +111,7 @@ module QA private_class_method :log_fabrication def self.evaluator - @evaluator ||= Factory::Base::DSL.new(self) + @evaluator ||= Base::DSL.new(self) end private_class_method :evaluator diff --git a/qa/qa/resource/branch.rb b/qa/qa/resource/branch.rb new file mode 100644 index 00000000000..bd52c4abe02 --- /dev/null +++ b/qa/qa/resource/branch.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true + +module QA + module Resource + class Branch < Base + attr_accessor :project, :branch_name, + :allow_to_push, :allow_to_merge, :protected + + attribute :project do + Project.fabricate! do |resource| + resource.name = 'protected-branch-project' + end + end + + def initialize + @branch_name = 'test/branch' + @allow_to_push = true + @allow_to_merge = true + @protected = false + end + + def fabricate! + project.visit! + + Repository::ProjectPush.fabricate! do |resource| + resource.project = project + resource.file_name = 'kick-off.txt' + resource.commit_message = 'First commit' + end + + branch = Repository::ProjectPush.fabricate! do |resource| + resource.project = project + resource.file_name = 'README.md' + resource.commit_message = 'Add readme' + resource.branch_name = 'master' + resource.new_branch = false + resource.remote_branch = @branch_name + end + + Page::Project::Show.perform do |page| + page.wait { page.has_content?(branch_name) } + end + + # The upcoming process will make it access the Protected Branches page, + # select the already created branch and protect it according + # to `allow_to_push` variable. + return branch unless @protected + + Page::Project::Menu.perform(&:click_repository_settings) + + Page::Project::Settings::Repository.perform do |setting| + setting.expand_protected_branches do |page| + page.select_branch(branch_name) + + if allow_to_push + page.allow_devs_and_maintainers_to_push + else + page.allow_no_one_to_push + end + + if allow_to_merge + page.allow_devs_and_maintainers_to_merge + else + page.allow_no_one_to_merge + end + + page.wait(reload: false) do + !page.first('.btn-success').disabled? + end + + page.protect_branch + end + end + end + end + end +end diff --git a/qa/qa/resource/ci_variable.rb b/qa/qa/resource/ci_variable.rb new file mode 100644 index 00000000000..0570c47d41c --- /dev/null +++ b/qa/qa/resource/ci_variable.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module QA + module Resource + class CiVariable < Base + attr_accessor :key, :value + + attribute :project do + Project.fabricate! do |resource| + resource.name = 'project-with-ci-variables' + resource.description = 'project for adding CI variable test' + end + end + + def fabricate! + project.visit! + + Page::Project::Menu.perform(&:click_ci_cd_settings) + + Page::Project::Settings::CICD.perform do |setting| + setting.expand_ci_variables do |page| + page.fill_variable(key, value) + + page.save_variables + end + end + end + end + end +end diff --git a/qa/qa/resource/deploy_key.rb b/qa/qa/resource/deploy_key.rb new file mode 100644 index 00000000000..9ed8fb7726e --- /dev/null +++ b/qa/qa/resource/deploy_key.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +module QA + module Resource + class DeployKey < Base + attr_accessor :title, :key + + attribute :fingerprint do + Page::Project::Settings::Repository.perform do |setting| + setting.expand_deploy_keys do |key| + key_offset = key.key_titles.index do |key_title| + key_title.text == title + end + + key.key_fingerprints[key_offset].text + end + end + end + + attribute :project do + Project.fabricate! do |resource| + resource.name = 'project-to-deploy' + resource.description = 'project for adding deploy key test' + end + end + + def fabricate! + project.visit! + + Page::Project::Menu.perform(&:click_repository_settings) + + Page::Project::Settings::Repository.perform do |setting| + setting.expand_deploy_keys do |page| + page.fill_key_title(title) + page.fill_key_value(key) + + page.add_key + end + end + end + end + end +end diff --git a/qa/qa/resource/deploy_token.rb b/qa/qa/resource/deploy_token.rb new file mode 100644 index 00000000000..cee4422f6b4 --- /dev/null +++ b/qa/qa/resource/deploy_token.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +module QA + module Resource + class DeployToken < Base + attr_accessor :name, :expires_at + + attribute :username do + Page::Project::Settings::Repository.perform do |page| + page.expand_deploy_tokens do |token| + token.token_username + end + end + end + + attribute :password do + Page::Project::Settings::Repository.perform do |page| + page.expand_deploy_tokens do |token| + token.token_password + end + end + end + + attribute :project do + Project.fabricate! do |resource| + resource.name = 'project-to-deploy' + resource.description = 'project for adding deploy token test' + end + end + + def fabricate! + project.visit! + + Page::Project::Menu.act do + click_repository_settings + end + + Page::Project::Settings::Repository.perform do |setting| + setting.expand_deploy_tokens do |page| + page.fill_token_name(name) + page.fill_token_expires_at(expires_at) + page.fill_scopes(read_repository: true, read_registry: false) + + page.add_token + end + end + end + end + end +end diff --git a/qa/qa/resource/file.rb b/qa/qa/resource/file.rb new file mode 100644 index 00000000000..effc5a7940b --- /dev/null +++ b/qa/qa/resource/file.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +module QA + module Resource + class File < Base + attr_accessor :name, + :content, + :commit_message + + attribute :project do + Project.fabricate! do |resource| + resource.name = 'project-with-new-file' + end + end + + def initialize + @name = 'QA Test - File name' + @content = 'QA Test - File content' + @commit_message = 'QA Test - Commit message' + end + + def fabricate! + project.visit! + + Page::Project::Show.perform(&:create_new_file!) + + Page::File::Form.perform do |page| + page.add_name(@name) + page.add_content(@content) + page.add_commit_message(@commit_message) + page.commit_changes + end + end + end + end +end diff --git a/qa/qa/resource/fork.rb b/qa/qa/resource/fork.rb new file mode 100644 index 00000000000..9fd66f3a36a --- /dev/null +++ b/qa/qa/resource/fork.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +module QA + module Resource + class Fork < Base + attribute :push do + Repository::ProjectPush.fabricate! + end + + attribute :user do + User.fabricate! do |resource| + if Runtime::Env.forker? + resource.username = Runtime::Env.forker_username + resource.password = Runtime::Env.forker_password + end + end + end + + def fabricate! + populate(:push, :user) + + # Sign out as admin and sign is as the fork user + Page::Main::Menu.perform(&:sign_out) + Runtime::Browser.visit(:gitlab, Page::Main::Login) + Page::Main::Login.perform do |login| + login.sign_in_using_credentials(user) + end + + push.project.visit! + + Page::Project::Show.perform(&:fork_project) + + Page::Project::Fork::New.perform do |fork_new| + fork_new.choose_namespace(user.name) + end + + Page::Layout::Banner.perform do |page| + page.has_notice?('The project was successfully forked.') + end + end + end + end +end diff --git a/qa/qa/resource/group.rb b/qa/qa/resource/group.rb new file mode 100644 index 00000000000..dce15e4f10b --- /dev/null +++ b/qa/qa/resource/group.rb @@ -0,0 +1,68 @@ +# frozen_string_literal: true + +module QA + module Resource + class Group < Base + attr_accessor :path, :description + + attribute :sandbox do + Sandbox.fabricate! + end + + attribute :id + + def initialize + @path = Runtime::Namespace.name + @description = "QA test run at #{Runtime::Namespace.time}" + end + + def fabricate! + sandbox.visit! + + Page::Group::Show.perform do |group_show| + if group_show.has_subgroup?(path) + group_show.go_to_subgroup(path) + else + group_show.go_to_new_subgroup + + Page::Group::New.perform do |group_new| + group_new.set_path(path) + group_new.set_description(description) + group_new.set_visibility('Public') + group_new.create + end + + # Ensure that the group was actually created + group_show.wait(time: 1) do + group_show.has_text?(path) && + group_show.has_new_project_or_subgroup_dropdown? + end + end + end + end + + def fabricate_via_api! + resource_web_url(api_get) + rescue ResourceNotFoundError + super + end + + def api_get_path + "/groups/#{CGI.escape("#{sandbox.path}/#{path}")}" + end + + def api_post_path + '/groups' + end + + def api_post_body + { + parent_id: sandbox.id, + path: path, + name: path, + visibility: 'public' + } + end + end + end +end diff --git a/qa/qa/resource/issue.rb b/qa/qa/resource/issue.rb new file mode 100644 index 00000000000..2c2f27fe231 --- /dev/null +++ b/qa/qa/resource/issue.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module QA + module Resource + class Issue < Base + attr_writer :description + + attribute :project do + Project.fabricate! do |resource| + resource.name = 'project-for-issues' + resource.description = 'project for adding issues' + end + end + + attribute :title + + def fabricate! + project.visit! + + Page::Project::Show.perform(&:go_to_new_issue) + + Page::Project::Issue::New.perform do |page| + page.add_title(@title) + page.add_description(@description) + page.create_new_issue + end + end + end + end +end diff --git a/qa/qa/resource/kubernetes_cluster.rb b/qa/qa/resource/kubernetes_cluster.rb new file mode 100644 index 00000000000..96c8843fb99 --- /dev/null +++ b/qa/qa/resource/kubernetes_cluster.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +require 'securerandom' + +module QA + module Resource + class KubernetesCluster < Base + attr_writer :project, :cluster, + :install_helm_tiller, :install_ingress, :install_prometheus, :install_runner + + attribute :ingress_ip do + Page::Project::Operations::Kubernetes::Show.perform(&:ingress_ip) + end + + def fabricate! + @project.visit! + + Page::Project::Menu.perform( + &:click_operations_kubernetes) + + Page::Project::Operations::Kubernetes::Index.perform( + &:add_kubernetes_cluster) + + Page::Project::Operations::Kubernetes::Add.perform( + &:add_existing_cluster) + + Page::Project::Operations::Kubernetes::AddExisting.perform do |page| + page.set_cluster_name(@cluster.cluster_name) + page.set_api_url(@cluster.api_url) + page.set_ca_certificate(@cluster.ca_certificate) + page.set_token(@cluster.token) + page.check_rbac! if @cluster.rbac + page.add_cluster! + end + + if @install_helm_tiller + Page::Project::Operations::Kubernetes::Show.perform do |page| + # We must wait a few seconds for permissions to be set up correctly for new cluster + sleep 10 + + # Helm must be installed before everything else + page.install!(:helm) + page.await_installed(:helm) + + page.install!(:ingress) if @install_ingress + page.install!(:prometheus) if @install_prometheus + page.install!(:runner) if @install_runner + + page.await_installed(:ingress) if @install_ingress + page.await_installed(:prometheus) if @install_prometheus + page.await_installed(:runner) if @install_runner + end + end + end + end + end +end diff --git a/qa/qa/resource/label.rb b/qa/qa/resource/label.rb new file mode 100644 index 00000000000..c0869cb1f2a --- /dev/null +++ b/qa/qa/resource/label.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +require 'securerandom' + +module QA + module Resource + class Label < Base + attr_accessor :description, :color + + attribute :title + + attribute :project do + Project.fabricate! do |resource| + resource.name = 'project-with-label' + end + end + + def initialize + @title = "qa-test-#{SecureRandom.hex(8)}" + @description = 'This is a test label' + @color = '#0033CC' + end + + def fabricate! + project.visit! + + Page::Project::Menu.perform(&:go_to_labels) + Page::Label::Index.perform(&:go_to_new_label) + + Page::Label::New.perform do |page| + page.fill_title(@title) + page.fill_description(@description) + page.fill_color(@color) + page.create_label + end + end + end + end +end diff --git a/qa/qa/resource/merge_request.rb b/qa/qa/resource/merge_request.rb new file mode 100644 index 00000000000..466a7942dc6 --- /dev/null +++ b/qa/qa/resource/merge_request.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: true + +require 'securerandom' + +module QA + module Resource + class MergeRequest < Base + attr_accessor :title, + :description, + :source_branch, + :target_branch, + :assignee, + :milestone, + :labels + + attribute :project do + Project.fabricate! do |resource| + resource.name = 'project-with-merge-request' + end + end + + attribute :target do + project.visit! + + Repository::ProjectPush.fabricate! do |resource| + resource.project = project + resource.branch_name = 'master' + resource.remote_branch = target_branch + end + end + + attribute :source do + Repository::ProjectPush.fabricate! do |resource| + resource.project = project + resource.branch_name = target_branch + resource.remote_branch = source_branch + resource.new_branch = false + resource.file_name = "added_file.txt" + resource.file_content = "File Added" + end + end + + def initialize + @title = 'QA test - merge request' + @description = 'This is a test merge request' + @source_branch = "qa-test-feature-#{SecureRandom.hex(8)}" + @target_branch = "master" + @assignee = nil + @milestone = nil + @labels = [] + end + + def fabricate! + populate(:target, :source) + + project.visit! + Page::Project::Show.perform(&:new_merge_request) + Page::MergeRequest::New.perform do |page| + page.fill_title(@title) + page.fill_description(@description) + page.choose_milestone(@milestone) if @milestone + labels.each do |label| + page.select_label(label) + end + + page.create_merge_request + end + end + end + end +end diff --git a/qa/qa/resource/merge_request_from_fork.rb b/qa/qa/resource/merge_request_from_fork.rb new file mode 100644 index 00000000000..f91ae299d76 --- /dev/null +++ b/qa/qa/resource/merge_request_from_fork.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +module QA + module Resource + class MergeRequestFromFork < MergeRequest + attr_accessor :fork_branch + + attribute :fork do + Fork.fabricate! + end + + attribute :push do + Repository::ProjectPush.fabricate! do |resource| + resource.project = fork + resource.branch_name = fork_branch + resource.file_name = 'file2.txt' + resource.user = fork.user + end + end + + def fabricate! + populate(:push) + + fork.visit! + + Page::Project::Show.perform(&:new_merge_request) + Page::MergeRequest::New.perform(&:create_merge_request) + end + end + end +end diff --git a/qa/qa/resource/personal_access_token.rb b/qa/qa/resource/personal_access_token.rb new file mode 100644 index 00000000000..b8dd0a3562f --- /dev/null +++ b/qa/qa/resource/personal_access_token.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module QA + module Resource + ## + # Create a personal access token that can be used by the api + # + class PersonalAccessToken < Base + attr_accessor :name + + attribute :access_token do + Page::Profile::PersonalAccessTokens.perform(&:created_access_token) + end + + def fabricate! + Page::Main::Menu.perform(&:go_to_profile_settings) + Page::Profile::Menu.perform(&:click_access_tokens) + + Page::Profile::PersonalAccessTokens.perform do |page| + page.fill_token_name(name || 'api-test-token') + page.check_api + page.create_token + end + end + end + end +end diff --git a/qa/qa/resource/project.rb b/qa/qa/resource/project.rb new file mode 100644 index 00000000000..7fdf69278f9 --- /dev/null +++ b/qa/qa/resource/project.rb @@ -0,0 +1,80 @@ +# frozen_string_literal: true + +require 'securerandom' + +module QA + module Resource + class Project < Base + attribute :name + attribute :description + + attribute :group do + Group.fabricate! + end + + attribute :repository_ssh_location do + Page::Project::Show.perform do |page| + page.choose_repository_clone_ssh + page.repository_location + end + end + + attribute :repository_http_location do + Page::Project::Show.perform do |page| + page.choose_repository_clone_http + page.repository_location + end + end + + def initialize + @description = 'My awesome project' + end + + def name=(raw_name) + @name = "#{raw_name}-#{SecureRandom.hex(8)}" + end + + def fabricate! + group.visit! + + Page::Group::Show.perform(&:go_to_new_project) + + Page::Project::New.perform do |page| + page.choose_test_namespace + page.choose_name(@name) + page.add_description(@description) + page.set_visibility('Public') + page.create_new_project + end + end + + def api_get_path + "/projects/#{name}" + end + + def api_post_path + '/projects' + end + + def api_post_body + { + namespace_id: group.id, + path: name, + name: name, + description: description, + visibility: 'public' + } + end + + private + + def transform_api_resource(api_resource) + api_resource[:repository_ssh_location] = + Git::Location.new(api_resource[:ssh_url_to_repo]) + api_resource[:repository_http_location] = + Git::Location.new(api_resource[:http_url_to_repo]) + api_resource + end + end + end +end diff --git a/qa/qa/resource/project_imported_from_github.rb b/qa/qa/resource/project_imported_from_github.rb new file mode 100644 index 00000000000..3f02fe885a9 --- /dev/null +++ b/qa/qa/resource/project_imported_from_github.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +require 'securerandom' + +module QA + module Resource + class ProjectImportedFromGithub < Project + attr_accessor :name + attr_writer :personal_access_token, :github_repository_path + + attribute :group do + Group.fabricate! + end + + def fabricate! + group.visit! + + Page::Group::Show.perform(&:go_to_new_project) + + Page::Project::New.perform do |page| + page.go_to_import_project + end + + Page::Project::New.perform do |page| + page.go_to_github_import + end + + Page::Project::Import::Github.perform do |page| + page.add_personal_access_token(@personal_access_token) + page.list_repos + page.import!(@github_repository_path, @name) + end + end + end + end +end diff --git a/qa/qa/resource/project_milestone.rb b/qa/qa/resource/project_milestone.rb new file mode 100644 index 00000000000..a4d6657caff --- /dev/null +++ b/qa/qa/resource/project_milestone.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +module QA + module Resource + class ProjectMilestone < Base + attr_reader :title + attr_accessor :description + + attribute :project do + Project.fabricate! + end + + def title=(title) + @title = "#{title}-#{SecureRandom.hex(4)}" + @description = 'A milestone' + end + + def fabricate! + project.visit! + + Page::Project::Menu.perform do |page| + page.click_issues + page.click_milestones + end + + Page::Project::Milestone::Index.perform(&:click_new_milestone) + + Page::Project::Milestone::New.perform do |milestone_new| + milestone_new.set_title(@title) + milestone_new.set_description(@description) + milestone_new.create_new_milestone + end + end + end + end +end diff --git a/qa/qa/factory/repository/project_push.rb b/qa/qa/resource/repository/project_push.rb index 272b7fc5818..c9fafe3419f 100644 --- a/qa/qa/factory/repository/project_push.rb +++ b/qa/qa/resource/repository/project_push.rb @@ -1,9 +1,11 @@ +# frozen_string_literal: true + module QA - module Factory + module Resource module Repository - class ProjectPush < Factory::Repository::Push + class ProjectPush < Repository::Push attribute :project do - Factory::Resource::Project.fabricate! do |resource| + Project.fabricate! do |resource| resource.name = 'project-with-code' resource.description = 'Project with repository' end diff --git a/qa/qa/factory/repository/push.rb b/qa/qa/resource/repository/push.rb index ffa755b9e88..c14d97ff7fb 100644 --- a/qa/qa/factory/repository/push.rb +++ b/qa/qa/resource/repository/push.rb @@ -1,9 +1,11 @@ +# frozen_string_literal: true + require 'pathname' module QA - module Factory + module Resource module Repository - class Push < Factory::Base + class Push < Base attr_accessor :file_name, :file_content, :commit_message, :branch_name, :new_branch, :output, :repository_http_uri, :repository_ssh_uri, :ssh_key, :user diff --git a/qa/qa/factory/repository/wiki_push.rb b/qa/qa/resource/repository/wiki_push.rb index 25b6ffe8323..f1c39d507fe 100644 --- a/qa/qa/factory/repository/wiki_push.rb +++ b/qa/qa/resource/repository/wiki_push.rb @@ -1,9 +1,11 @@ +# frozen_string_literal: true + module QA - module Factory + module Resource module Repository - class WikiPush < Factory::Repository::Push + class WikiPush < Repository::Push attribute :wiki do - Factory::Resource::Wiki.fabricate! do |resource| + Wiki.fabricate! do |resource| resource.title = 'Home' resource.content = '# My First Wiki Content' resource.message = 'Update home' diff --git a/qa/qa/resource/runner.rb b/qa/qa/resource/runner.rb new file mode 100644 index 00000000000..08ae3f22117 --- /dev/null +++ b/qa/qa/resource/runner.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +require 'securerandom' + +module QA + module Resource + class Runner < Base + attr_writer :name, :tags, :image + + attribute :project do + Project.fabricate! do |resource| + resource.name = 'project-with-ci-cd' + resource.description = 'Project with CI/CD Pipelines' + end + end + + def name + @name || "qa-runner-#{SecureRandom.hex(4)}" + end + + def tags + @tags || %w[qa e2e] + end + + def image + @image || 'gitlab/gitlab-runner:alpine' + end + + def fabricate! + project.visit! + + Page::Project::Menu.perform(&:click_ci_cd_settings) + + Service::Runner.new(name).tap do |runner| + Page::Project::Settings::CICD.perform do |settings| + settings.expand_runners_settings do |runners| + runner.pull + runner.token = runners.registration_token + runner.address = runners.coordinator_address + runner.tags = tags + runner.image = image + runner.register! + end + end + end + end + end + end +end diff --git a/qa/qa/resource/sandbox.rb b/qa/qa/resource/sandbox.rb new file mode 100644 index 00000000000..41ce857a8b8 --- /dev/null +++ b/qa/qa/resource/sandbox.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +module QA + module Resource + ## + # Ensure we're in our sandbox namespace, either by navigating to it or by + # creating it if it doesn't yet exist. + # + class Sandbox < Base + attr_reader :path + + attribute :id + + def initialize + @path = Runtime::Namespace.sandbox_name + end + + def fabricate! + Page::Main::Menu.perform(&:go_to_groups) + + Page::Dashboard::Groups.perform do |page| + if page.has_group?(path) + page.go_to_group(path) + else + page.go_to_new_group + + Page::Group::New.perform do |group| + group.set_path(path) + group.set_description('GitLab QA Sandbox Group') + group.set_visibility('Public') + group.create + end + end + end + end + + def fabricate_via_api! + resource_web_url(api_get) + rescue ResourceNotFoundError + super + end + + def api_get_path + "/groups/#{path}" + end + + def api_post_path + '/groups' + end + + def api_post_body + { + path: path, + name: path, + visibility: 'public' + } + end + end + end +end diff --git a/qa/qa/factory/settings/hashed_storage.rb b/qa/qa/resource/settings/hashed_storage.rb index 4e32382f910..40c06768ffe 100644 --- a/qa/qa/factory/settings/hashed_storage.rb +++ b/qa/qa/resource/settings/hashed_storage.rb @@ -1,7 +1,9 @@ +# frozen_string_literal: true + module QA - module Factory + module Resource module Settings - class HashedStorage < Factory::Base + class HashedStorage < Base def fabricate!(*traits) raise ArgumentError unless traits.include?(:enabled) diff --git a/qa/qa/resource/ssh_key.rb b/qa/qa/resource/ssh_key.rb new file mode 100644 index 00000000000..c6c97c8532f --- /dev/null +++ b/qa/qa/resource/ssh_key.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +module QA + module Resource + class SSHKey < Base + extend Forwardable + + attr_accessor :title + + def_delegators :key, :private_key, :public_key, :fingerprint + + def key + @key ||= Runtime::Key::RSA.new + end + + def fabricate! + Page::Main::Menu.perform(&:go_to_profile_settings) + Page::Profile::Menu.perform(&:click_ssh_keys) + + Page::Profile::SSHKeys.perform do |page| + page.add_key(public_key, title) + end + end + end + end +end diff --git a/qa/qa/resource/user.rb b/qa/qa/resource/user.rb new file mode 100644 index 00000000000..16f0b311fa9 --- /dev/null +++ b/qa/qa/resource/user.rb @@ -0,0 +1,92 @@ +# frozen_string_literal: true + +require 'securerandom' + +module QA + module Resource + class User < Base + attr_reader :unique_id + attr_writer :username, :password + + def initialize + @unique_id = SecureRandom.hex(8) + end + + def username + @username ||= "qa-user-#{unique_id}" + end + + def password + @password ||= 'password' + end + + def name + @name ||= username + end + + def email + @email ||= "#{username}@example.com" + end + + def credentials_given? + defined?(@username) && defined?(@password) + end + + def fabricate! + # Don't try to log-out if we're not logged-in + if Page::Main::Menu.perform { |p| p.has_personal_area?(wait: 0) } + Page::Main::Menu.perform { |main| main.sign_out } + end + + if credentials_given? + Page::Main::Login.perform do |login| + login.sign_in_using_credentials(self) + end + else + Page::Main::Login.perform do |login| + login.switch_to_register_tab + end + Page::Main::SignUp.perform do |signup| + signup.sign_up!(self) + end + end + end + + def fabricate_via_api! + resource_web_url(api_get) + rescue ResourceNotFoundError + super + end + + def api_get_path + "/users/#{fetch_id(username)}" + end + + def api_post_path + '/users' + end + + def api_post_body + { + email: email, + password: password, + username: username, + name: name, + skip_confirmation: true + } + end + + private + + def fetch_id(username) + users = parse_body(api_get_from("/users?username=#{username}")) + + unless users.size == 1 && users.first[:username] == username + raise ResourceNotFoundError, "Expected one user with username #{username} but found: `#{users}`." + end + + users.first[:id] + end + end + end +end diff --git a/qa/qa/resource/wiki.rb b/qa/qa/resource/wiki.rb new file mode 100644 index 00000000000..e942e9718a0 --- /dev/null +++ b/qa/qa/resource/wiki.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module QA + module Resource + class Wiki < Base + attr_accessor :title, :content, :message + + attribute :project do + Project.fabricate! do |resource| + resource.name = 'project-for-wikis' + resource.description = 'project for adding wikis' + end + end + + def fabricate! + project.visit! + + Page::Project::Menu.perform { |menu_side| menu_side.click_wiki } + + Page::Project::Wiki::New.perform do |wiki_new| + wiki_new.go_to_create_first_page + wiki_new.set_title(@title) + wiki_new.set_content(@content) + wiki_new.set_message(@message) + wiki_new.create_new_page + end + end + end + end +end diff --git a/qa/qa/runtime/api/client.rb b/qa/qa/runtime/api/client.rb index 0545b500e4c..aff84c89f0e 100644 --- a/qa/qa/runtime/api/client.rb +++ b/qa/qa/runtime/api/client.rb @@ -32,7 +32,7 @@ module QA def do_create_personal_access_token Page::Main::Login.act { sign_in_using_credentials } - Factory::Resource::PersonalAccessToken.fabricate!.access_token + Resource::PersonalAccessToken.fabricate!.access_token end end end diff --git a/qa/qa/specs/features/browser_ui/1_manage/login/register_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/login/register_spec.rb index 44071ec3e45..7d5fc3c4b65 100644 --- a/qa/qa/specs/features/browser_ui/1_manage/login/register_spec.rb +++ b/qa/qa/specs/features/browser_ui/1_manage/login/register_spec.rb @@ -5,7 +5,7 @@ module QA it 'user registers and logs in' do Runtime::Browser.visit(:gitlab, Page::Main::Login) - Factory::Resource::User.fabricate! + Resource::User.fabricate! # TODO, since `Signed in successfully` message was removed # this is the only way to tell if user is signed in correctly. diff --git a/qa/qa/specs/features/browser_ui/1_manage/project/add_project_member_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/project/add_project_member_spec.rb index 7bf26c22fa6..bef89d5be24 100644 --- a/qa/qa/specs/features/browser_ui/1_manage/project/add_project_member_spec.rb +++ b/qa/qa/specs/features/browser_ui/1_manage/project/add_project_member_spec.rb @@ -5,18 +5,16 @@ module QA describe 'Add project member' do it 'user adds project member' do Runtime::Browser.visit(:gitlab, Page::Main::Login) + Page::Main::Login.perform(&:sign_in_using_credentials) - user = Factory::Resource::User.fabricate! + user = Resource::User.fabricate! - Page::Main::Menu.perform { |main| main.sign_out } - Page::Main::Login.act { sign_in_using_credentials } - - project = Factory::Resource::Project.fabricate! do |resource| + project = Resource::Project.fabricate! do |resource| resource.name = 'add-member-project' end project.visit! - Page::Project::Menu.act { click_members_settings } + Page::Project::Menu.perform(&:click_members_settings) Page::Project::Settings::Members.perform do |page| page.add_member(user.username) end diff --git a/qa/qa/specs/features/browser_ui/1_manage/project/create_project_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/project/create_project_spec.rb index a242f2158da..6632c2977ef 100644 --- a/qa/qa/specs/features/browser_ui/1_manage/project/create_project_spec.rb +++ b/qa/qa/specs/features/browser_ui/1_manage/project/create_project_spec.rb @@ -7,7 +7,7 @@ module QA Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } - created_project = Factory::Resource::Project.fabricate_via_browser_ui! do |project| + created_project = Resource::Project.fabricate_via_browser_ui! do |project| project.name = 'awesome-project' project.description = 'create awesome project test' end diff --git a/qa/qa/specs/features/browser_ui/1_manage/project/import_github_repo_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/project/import_github_repo_spec.rb index a99b0522e73..3ce48de2c25 100644 --- a/qa/qa/specs/features/browser_ui/1_manage/project/import_github_repo_spec.rb +++ b/qa/qa/specs/features/browser_ui/1_manage/project/import_github_repo_spec.rb @@ -4,7 +4,7 @@ module QA context 'Manage', :orchestrated, :github do describe 'Project import from GitHub' do let(:imported_project) do - Factory::Resource::ProjectImportedFromGithub.fabricate! do |project| + Resource::ProjectImportedFromGithub.fabricate! do |project| project.name = 'imported-project' project.personal_access_token = Runtime::Env.github_access_token project.github_repository_path = 'gitlab-qa/test-project' diff --git a/qa/qa/specs/features/browser_ui/1_manage/project/view_project_activity_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/project/view_project_activity_spec.rb index 768d40f3acf..275de3d332c 100644 --- a/qa/qa/specs/features/browser_ui/1_manage/project/view_project_activity_spec.rb +++ b/qa/qa/specs/features/browser_ui/1_manage/project/view_project_activity_spec.rb @@ -7,7 +7,7 @@ module QA Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } - Factory::Repository::ProjectPush.fabricate! do |push| + Resource::Repository::ProjectPush.fabricate! do |push| push.file_name = 'README.md' push.file_content = '# This is a test project' push.commit_message = 'Add README.md' diff --git a/qa/qa/specs/features/browser_ui/2_plan/issue/create_issue_spec.rb b/qa/qa/specs/features/browser_ui/2_plan/issue/create_issue_spec.rb index e67561b3a39..f5002c8032f 100644 --- a/qa/qa/specs/features/browser_ui/2_plan/issue/create_issue_spec.rb +++ b/qa/qa/specs/features/browser_ui/2_plan/issue/create_issue_spec.rb @@ -9,7 +9,7 @@ module QA Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } - Factory::Resource::Issue.fabricate! do |issue| + Resource::Issue.fabricate! do |issue| issue.title = issue_title end end diff --git a/qa/qa/specs/features/browser_ui/2_plan/issue/filter_issue_comments_spec.rb b/qa/qa/specs/features/browser_ui/2_plan/issue/filter_issue_comments_spec.rb index 24877d937d2..83603f1cda7 100644 --- a/qa/qa/specs/features/browser_ui/2_plan/issue/filter_issue_comments_spec.rb +++ b/qa/qa/specs/features/browser_ui/2_plan/issue/filter_issue_comments_spec.rb @@ -9,7 +9,7 @@ module QA Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } - Factory::Resource::Issue.fabricate! do |issue| + Resource::Issue.fabricate! do |issue| issue.title = issue_title end diff --git a/qa/qa/specs/features/browser_ui/3_create/merge_request/create_merge_request_spec.rb b/qa/qa/specs/features/browser_ui/3_create/merge_request/create_merge_request_spec.rb index 037ff5efbd4..d33947f41da 100644 --- a/qa/qa/specs/features/browser_ui/3_create/merge_request/create_merge_request_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/merge_request/create_merge_request_spec.rb @@ -7,22 +7,22 @@ module QA Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } - current_project = Factory::Resource::Project.fabricate! do |project| + current_project = Resource::Project.fabricate! do |project| project.name = 'project-with-merge-request-and-milestone' end - current_milestone = Factory::Resource::ProjectMilestone.fabricate! do |milestone| + current_milestone = Resource::ProjectMilestone.fabricate! do |milestone| milestone.title = 'unique-milestone' milestone.project = current_project end - new_label = Factory::Resource::Label.fabricate! do |label| + new_label = Resource::Label.fabricate! do |label| label.project = current_project label.title = 'qa-mr-test-label' label.description = 'Merge Request label' end - Factory::Resource::MergeRequest.fabricate! do |merge_request| + Resource::MergeRequest.fabricate! do |merge_request| merge_request.title = 'This is a merge request with a milestone' merge_request.description = 'Great feature with milestone' merge_request.project = current_project @@ -49,11 +49,11 @@ module QA Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } - current_project = Factory::Resource::Project.fabricate! do |project| + current_project = Resource::Project.fabricate! do |project| project.name = 'project-with-merge-request' end - Factory::Resource::MergeRequest.fabricate! do |merge_request| + Resource::MergeRequest.fabricate! do |merge_request| merge_request.title = 'This is a merge request' merge_request.description = 'Great feature' merge_request.project = current_project diff --git a/qa/qa/specs/features/browser_ui/3_create/merge_request/merge_merge_request_from_fork_spec.rb b/qa/qa/specs/features/browser_ui/3_create/merge_request/merge_merge_request_from_fork_spec.rb index 058af8aebdd..6dcd74471fe 100644 --- a/qa/qa/specs/features/browser_ui/3_create/merge_request/merge_merge_request_from_fork_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/merge_request/merge_merge_request_from_fork_spec.rb @@ -7,7 +7,7 @@ module QA Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } - merge_request = Factory::Resource::MergeRequestFromFork.fabricate! do |merge_request| + merge_request = Resource::MergeRequestFromFork.fabricate! do |merge_request| merge_request.fork_branch = 'feature-branch' end diff --git a/qa/qa/specs/features/browser_ui/3_create/merge_request/rebase_merge_request_spec.rb b/qa/qa/specs/features/browser_ui/3_create/merge_request/rebase_merge_request_spec.rb index 3bcf086d332..e2d639fd150 100644 --- a/qa/qa/specs/features/browser_ui/3_create/merge_request/rebase_merge_request_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/merge_request/rebase_merge_request_spec.rb @@ -7,7 +7,7 @@ module QA Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } - project = Factory::Resource::Project.fabricate! do |project| + project = Resource::Project.fabricate! do |project| project.name = "only-fast-forward" end project.visit! @@ -15,12 +15,12 @@ module QA Page::Project::Menu.act { go_to_settings } Page::Project::Settings::MergeRequest.act { enable_ff_only } - merge_request = Factory::Resource::MergeRequest.fabricate! do |merge_request| + merge_request = Resource::MergeRequest.fabricate! do |merge_request| merge_request.project = project merge_request.title = 'Needs rebasing' end - Factory::Repository::ProjectPush.fabricate! do |push| + Resource::Repository::ProjectPush.fabricate! do |push| push.project = project push.file_name = "other.txt" push.file_content = "New file added!" diff --git a/qa/qa/specs/features/browser_ui/3_create/merge_request/squash_merge_request_spec.rb b/qa/qa/specs/features/browser_ui/3_create/merge_request/squash_merge_request_spec.rb index 724c48cd125..6ff7360c413 100644 --- a/qa/qa/specs/features/browser_ui/3_create/merge_request/squash_merge_request_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/merge_request/squash_merge_request_spec.rb @@ -7,16 +7,16 @@ module QA Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } - project = Factory::Resource::Project.fabricate! do |project| + project = Resource::Project.fabricate! do |project| project.name = "squash-before-merge" end - merge_request = Factory::Resource::MergeRequest.fabricate! do |merge_request| + merge_request = Resource::MergeRequest.fabricate! do |merge_request| merge_request.project = project merge_request.title = 'Squashing commits' end - Factory::Repository::ProjectPush.fabricate! do |push| + Resource::Repository::ProjectPush.fabricate! do |push| push.project = project push.commit_message = 'to be squashed' push.branch_name = merge_request.source_branch diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/add_file_template_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/add_file_template_spec.rb index 7705e12b95e..297485dd81e 100644 --- a/qa/qa/specs/features/browser_ui/3_create/repository/add_file_template_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/repository/add_file_template_spec.rb @@ -13,7 +13,7 @@ module QA before(:all) do login - @project = Factory::Resource::Project.fabricate! do |project| + @project = Resource::Project.fabricate! do |project| project.name = 'file-template-project' project.description = 'Add file templates via the Files view' end diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/add_ssh_key_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/add_ssh_key_spec.rb index df70b9608d9..94be66782c6 100644 --- a/qa/qa/specs/features/browser_ui/3_create/repository/add_ssh_key_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/repository/add_ssh_key_spec.rb @@ -9,7 +9,7 @@ module QA Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } - key = Factory::Resource::SSHKey.fabricate! do |resource| + key = Resource::SSHKey.fabricate! do |resource| resource.title = key_title end diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/clone_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/clone_spec.rb index b18dee53cbc..6a0add56fe0 100644 --- a/qa/qa/specs/features/browser_ui/3_create/repository/clone_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/repository/clone_spec.rb @@ -14,7 +14,7 @@ module QA Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } - project = Factory::Resource::Project.fabricate! do |scenario| + project = Resource::Project.fabricate! do |scenario| scenario.name = 'project-with-code' scenario.description = 'project for git clone tests' end diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/create_edit_delete_file_via_web_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/create_edit_delete_file_via_web_spec.rb index f65a1569fb0..46346d1b984 100644 --- a/qa/qa/specs/features/browser_ui/3_create/repository/create_edit_delete_file_via_web_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/repository/create_edit_delete_file_via_web_spec.rb @@ -12,7 +12,7 @@ module QA file_content = 'QA Test - File content' commit_message_for_create = 'QA Test - Create new file' - Factory::Resource::File.fabricate! do |file| + Resource::File.fabricate! do |file| file.name = file_name file.content = file_content file.commit_message = commit_message_for_create diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/push_http_private_token_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/push_http_private_token_spec.rb index 8e4210482a2..a63b7dce8d6 100644 --- a/qa/qa/specs/features/browser_ui/3_create/repository/push_http_private_token_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/repository/push_http_private_token_spec.rb @@ -7,14 +7,14 @@ module QA Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.perform(&:sign_in_using_credentials) - access_token = Factory::Resource::PersonalAccessToken.fabricate!.access_token + access_token = Resource::PersonalAccessToken.fabricate!.access_token - user = Factory::Resource::User.new.tap do |user| + user = Resource::User.new.tap do |user| user.username = Runtime::User.username user.password = access_token end - push = Factory::Repository::ProjectPush.fabricate! do |push| + push = Resource::Repository::ProjectPush.fabricate! do |push| push.user = user push.file_name = 'README.md' push.file_content = '# This is a test project' diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_spec.rb index 2f63a07e0c3..92f596a44d9 100644 --- a/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_spec.rb @@ -7,7 +7,7 @@ module QA Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } - Factory::Repository::ProjectPush.fabricate! do |push| + Resource::Repository::ProjectPush.fabricate! do |push| push.file_name = 'README.md' push.file_content = '# This is a test project' push.commit_message = 'Add README.md' diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/push_protected_branch_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/push_protected_branch_spec.rb index ac71cf52b6f..73a3dc14a65 100644 --- a/qa/qa/specs/features/browser_ui/3_create/repository/push_protected_branch_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/repository/push_protected_branch_spec.rb @@ -6,7 +6,7 @@ module QA let(:branch_name) { 'protected-branch' } let(:commit_message) { 'Protected push commit message' } let(:project) do - Factory::Resource::Project.fabricate! do |resource| + Resource::Project.fabricate! do |resource| resource.name = 'protected-branch-project' end end @@ -47,7 +47,7 @@ module QA end def create_protected_branch(allow_to_push:) - Factory::Resource::Branch.fabricate! do |resource| + Resource::Branch.fabricate! do |resource| resource.branch_name = branch_name resource.project = project resource.allow_to_push = allow_to_push @@ -56,7 +56,7 @@ module QA end def push_new_file(branch) - Factory::Repository::ProjectPush.fabricate! do |resource| + Resource::Repository::ProjectPush.fabricate! do |resource| resource.project = project resource.file_name = 'new_file.md' resource.file_content = '# This is a new file' diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/use_ssh_key_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/use_ssh_key_spec.rb index 36068ffba69..9c764424129 100644 --- a/qa/qa/specs/features/browser_ui/3_create/repository/use_ssh_key_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/repository/use_ssh_key_spec.rb @@ -12,11 +12,11 @@ module QA Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } - key = Factory::Resource::SSHKey.fabricate! do |resource| + key = Resource::SSHKey.fabricate! do |resource| resource.title = key_title end - Factory::Repository::ProjectPush.fabricate! do |push| + Resource::Repository::ProjectPush.fabricate! do |push| push.ssh_key = key push.file_name = 'README.md' push.file_content = '# Test Use SSH Key' diff --git a/qa/qa/specs/features/browser_ui/3_create/web_ide/add_file_template_spec.rb b/qa/qa/specs/features/browser_ui/3_create/web_ide/add_file_template_spec.rb index 07dbf39a8a3..e7374377104 100644 --- a/qa/qa/specs/features/browser_ui/3_create/web_ide/add_file_template_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/web_ide/add_file_template_spec.rb @@ -13,7 +13,7 @@ module QA before(:all) do login - @project = Factory::Resource::Project.fabricate! do |project| + @project = Resource::Project.fabricate! do |project| project.name = 'file-template-project' project.description = 'Add file templates via the Web IDE' end diff --git a/qa/qa/specs/features/browser_ui/3_create/wiki/create_edit_clone_push_wiki_spec.rb b/qa/qa/specs/features/browser_ui/3_create/wiki/create_edit_clone_push_wiki_spec.rb index 4126fd9fd3e..210271705d9 100644 --- a/qa/qa/specs/features/browser_ui/3_create/wiki/create_edit_clone_push_wiki_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/wiki/create_edit_clone_push_wiki_spec.rb @@ -18,7 +18,7 @@ module QA end it 'user creates, edits, clones, and pushes to the wiki' do - wiki = Factory::Resource::Wiki.fabricate! do |resource| + wiki = Resource::Wiki.fabricate! do |resource| resource.title = 'Home' resource.content = '# My First Wiki Content' resource.message = 'Update home' @@ -34,7 +34,7 @@ module QA validate_content('My Second Wiki Content') - Factory::Repository::WikiPush.fabricate! do |push| + Resource::Repository::WikiPush.fabricate! do |push| push.wiki = wiki push.file_name = 'Home.md' push.file_content = '# My Third Wiki Content' diff --git a/qa/qa/specs/features/browser_ui/4_verify/ci_variable/add_ci_variable_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/ci_variable/add_ci_variable_spec.rb index 58b272adcf1..0837b720df1 100644 --- a/qa/qa/specs/features/browser_ui/4_verify/ci_variable/add_ci_variable_spec.rb +++ b/qa/qa/specs/features/browser_ui/4_verify/ci_variable/add_ci_variable_spec.rb @@ -7,7 +7,7 @@ module QA Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } - Factory::Resource::CiVariable.fabricate! do |resource| + Resource::CiVariable.fabricate! do |resource| resource.key = 'VARIABLE_KEY' resource.value = 'some CI variable' end diff --git a/qa/qa/specs/features/browser_ui/4_verify/pipeline/create_and_process_pipeline_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/pipeline/create_and_process_pipeline_spec.rb index d66bcce879b..25cbe41c684 100644 --- a/qa/qa/specs/features/browser_ui/4_verify/pipeline/create_and_process_pipeline_spec.rb +++ b/qa/qa/specs/features/browser_ui/4_verify/pipeline/create_and_process_pipeline_spec.rb @@ -13,18 +13,18 @@ module QA Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } - project = Factory::Resource::Project.fabricate! do |project| + project = Resource::Project.fabricate! do |project| project.name = 'project-with-pipelines' project.description = 'Project with CI/CD Pipelines.' end - Factory::Resource::Runner.fabricate! do |runner| + Resource::Runner.fabricate! do |runner| runner.project = project runner.name = executor runner.tags = %w[qa test] end - Factory::Repository::ProjectPush.fabricate! do |push| + Resource::Repository::ProjectPush.fabricate! do |push| push.project = project push.file_name = '.gitlab-ci.yml' push.commit_message = 'Add .gitlab-ci.yml' diff --git a/qa/qa/specs/features/browser_ui/4_verify/runner/register_runner_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/runner/register_runner_spec.rb index 5d9aa00582f..3af7db751e7 100644 --- a/qa/qa/specs/features/browser_ui/4_verify/runner/register_runner_spec.rb +++ b/qa/qa/specs/features/browser_ui/4_verify/runner/register_runner_spec.rb @@ -13,7 +13,7 @@ module QA Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } - Factory::Resource::Runner.fabricate! do |runner| + Resource::Runner.fabricate! do |runner| runner.name = executor end diff --git a/qa/qa/specs/features/browser_ui/6_release/deploy_key/add_deploy_key_spec.rb b/qa/qa/specs/features/browser_ui/6_release/deploy_key/add_deploy_key_spec.rb index 64b98da8bf5..84757f25379 100644 --- a/qa/qa/specs/features/browser_ui/6_release/deploy_key/add_deploy_key_spec.rb +++ b/qa/qa/specs/features/browser_ui/6_release/deploy_key/add_deploy_key_spec.rb @@ -11,7 +11,7 @@ module QA deploy_key_title = 'deploy key title' deploy_key_value = key.public_key - deploy_key = Factory::Resource::DeployKey.fabricate! do |resource| + deploy_key = Resource::DeployKey.fabricate! do |resource| resource.title = deploy_key_title resource.key = deploy_key_value end diff --git a/qa/qa/specs/features/browser_ui/6_release/deploy_key/clone_using_deploy_key_spec.rb b/qa/qa/specs/features/browser_ui/6_release/deploy_key/clone_using_deploy_key_spec.rb index 604641e54b8..e2320c92343 100644 --- a/qa/qa/specs/features/browser_ui/6_release/deploy_key/clone_using_deploy_key_spec.rb +++ b/qa/qa/specs/features/browser_ui/6_release/deploy_key/clone_using_deploy_key_spec.rb @@ -15,13 +15,13 @@ module QA @runner_name = "qa-runner-#{Time.now.to_i}" - @project = Factory::Resource::Project.fabricate! do |resource| + @project = Resource::Project.fabricate! do |resource| resource.name = 'deploy-key-clone-project' end @repository_location = @project.repository_ssh_location - Factory::Resource::Runner.fabricate! do |resource| + Resource::Runner.fabricate! do |resource| resource.project = @project resource.name = @runner_name resource.tags = %w[qa docker] @@ -47,7 +47,7 @@ module QA login - Factory::Resource::DeployKey.fabricate! do |resource| + Resource::DeployKey.fabricate! do |resource| resource.project = @project resource.title = "deploy key #{key.name}(#{key.bits})" resource.key = key.public_key @@ -55,7 +55,7 @@ module QA deploy_key_name = "DEPLOY_KEY_#{key.name}_#{key.bits}" - Factory::Resource::CiVariable.fabricate! do |resource| + Resource::CiVariable.fabricate! do |resource| resource.project = @project resource.key = deploy_key_name resource.value = key.private_key @@ -78,7 +78,7 @@ module QA - docker YAML - Factory::Repository::ProjectPush.fabricate! do |resource| + Resource::Repository::ProjectPush.fabricate! do |resource| resource.project = @project resource.file_name = '.gitlab-ci.yml' resource.commit_message = 'Add .gitlab-ci.yml' diff --git a/qa/qa/specs/features/browser_ui/6_release/deploy_token/add_deploy_token_spec.rb b/qa/qa/specs/features/browser_ui/6_release/deploy_token/add_deploy_token_spec.rb index 263ba6a6800..9f34e4218c1 100644 --- a/qa/qa/specs/features/browser_ui/6_release/deploy_token/add_deploy_token_spec.rb +++ b/qa/qa/specs/features/browser_ui/6_release/deploy_token/add_deploy_token_spec.rb @@ -10,7 +10,7 @@ module QA deploy_token_name = 'deploy token name' deploy_token_expires_at = Date.today + 7 # 1 Week from now - deploy_token = Factory::Resource::DeployToken.fabricate! do |resource| + deploy_token = Resource::DeployToken.fabricate! do |resource| resource.name = deploy_token_name resource.expires_at = deploy_token_expires_at end diff --git a/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb b/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb index c2fce1e7df1..30ec0665973 100644 --- a/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb +++ b/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb @@ -15,21 +15,21 @@ module QA Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } - project = Factory::Resource::Project.fabricate! do |p| + project = Resource::Project.fabricate! do |p| p.name = 'project-with-autodevops' p.description = 'Project with Auto Devops' end # Disable code_quality check in Auto DevOps pipeline as it takes # too long and times out the test - Factory::Resource::CiVariable.fabricate! do |resource| + Resource::CiVariable.fabricate! do |resource| resource.project = project resource.key = 'CODE_QUALITY_DISABLED' resource.value = '1' end # Create Auto Devops compatible repo - Factory::Repository::ProjectPush.fabricate! do |push| + Resource::Repository::ProjectPush.fabricate! do |push| push.project = project push.directory = Pathname .new(__dir__) @@ -41,7 +41,7 @@ module QA # Create and connect K8s cluster @cluster = Service::KubernetesCluster.new(rbac: rbac).create! - kubernetes_cluster = Factory::Resource::KubernetesCluster.fabricate! do |cluster| + kubernetes_cluster = Resource::KubernetesCluster.fabricate! do |cluster| cluster.project = project cluster.cluster = @cluster cluster.install_helm_tiller = true diff --git a/qa/spec/factory/resource/user_spec.rb b/qa/spec/factory/resource/user_spec.rb new file mode 100644 index 00000000000..820c506b715 --- /dev/null +++ b/qa/spec/factory/resource/user_spec.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +describe QA::Resource::User do + describe "#fabricate_via_api!" do + Response = Struct.new(:code, :body) + + it 'fetches an existing user' do + existing_users = [ + { + id: '0', + name: 'name', + username: 'name', + web_url: '' + } + ] + users_response = Response.new('200', JSON.dump(existing_users)) + single_user_response = Response.new('200', JSON.dump(existing_users.first)) + + expect(subject).to receive(:api_get_from).with("/users?username=name").and_return(users_response) + expect(subject).to receive(:api_get_from).with("/users/0").and_return(single_user_response) + + subject.username = 'name' + subject.fabricate_via_api! + + expect(subject.api_response).to eq(existing_users.first) + end + + it 'tries to create a user if it does not exist' do + expect(subject).to receive(:api_get_from).with("/users?username=foo").and_return(Response.new('200', '[]')) + expect(subject).to receive(:api_post).and_return({ web_url: '' }) + + subject.username = 'foo' + subject.fabricate_via_api! + end + end +end diff --git a/qa/spec/factory/api_fabricator_spec.rb b/qa/spec/resource/api_fabricator_spec.rb index e5fbc064911..a5ed4422f6e 100644 --- a/qa/spec/factory/api_fabricator_spec.rb +++ b/qa/spec/resource/api_fabricator_spec.rb @@ -1,18 +1,18 @@ # frozen_string_literal: true -describe QA::Factory::ApiFabricator do - let(:factory_without_api_support) do +describe QA::Resource::ApiFabricator do + let(:resource_without_api_support) do Class.new do def self.name - 'FooBarFactory' + 'FooBarResource' end end end - let(:factory_with_api_support) do + let(:resource_with_api_support) do Class.new do def self.name - 'FooBarFactory' + 'FooBarResource' end def api_get_path @@ -33,22 +33,22 @@ describe QA::Factory::ApiFabricator do allow(subject).to receive(:current_url).and_return('') end - subject { factory.tap { |f| f.include(described_class) }.new } + subject { resource.tap { |f| f.include(described_class) }.new } describe '#api_support?' do let(:api_client) { spy('Runtime::API::Client') } let(:api_client_instance) { double('API Client') } - context 'when factory does not support fabrication via the API' do - let(:factory) { factory_without_api_support } + context 'when resource does not support fabrication via the API' do + let(:resource) { resource_without_api_support } it 'returns false' do expect(subject).not_to be_api_support end end - context 'when factory supports fabrication via the API' do - let(:factory) { factory_with_api_support } + context 'when resource supports fabrication via the API' do + let(:resource) { resource_with_api_support } it 'returns false' do expect(subject).to be_api_support @@ -67,20 +67,20 @@ describe QA::Factory::ApiFabricator do allow(api_client_instance).to receive(:personal_access_token).and_return('foo') end - context 'when factory does not support fabrication via the API' do - let(:factory) { factory_without_api_support } + context 'when resource does not support fabrication via the API' do + let(:resource) { resource_without_api_support } it 'raises a NotImplementedError exception' do - expect { subject.fabricate_via_api! }.to raise_error(NotImplementedError, "Factory FooBarFactory does not support fabrication via the API!") + expect { subject.fabricate_via_api! }.to raise_error(NotImplementedError, "Resource FooBarResource does not support fabrication via the API!") end end - context 'when factory supports fabrication via the API' do - let(:factory) { factory_with_api_support } + context 'when resource supports fabrication via the API' do + let(:resource) { resource_with_api_support } let(:api_request) { spy('Runtime::API::Request') } let(:resource_web_url) { 'http://example.org/api/v4/foo' } - let(:resource) { { id: 1, name: 'John Doe', web_url: resource_web_url } } - let(:raw_post) { double('Raw POST response', code: 201, body: resource.to_json) } + let(:response) { { id: 1, name: 'John Doe', web_url: resource_web_url } } + let(:raw_post) { double('Raw POST response', code: 201, body: response.to_json) } before do stub_const('QA::Runtime::API::Request', api_request) @@ -103,7 +103,7 @@ describe QA::Factory::ApiFabricator do it 'populates api_resource with the resource' do subject.fabricate_via_api! - expect(subject.api_resource).to eq(resource) + expect(subject.api_resource).to eq(response) end context 'when the POST fails' do @@ -114,17 +114,17 @@ describe QA::Factory::ApiFabricator do expect(api_request).to receive(:new).with(api_client_instance, subject.api_post_path).and_return(double(url: resource_web_url)) expect(subject).to receive(:post).with(resource_web_url, subject.api_post_body).and_return(raw_post) - expect { subject.fabricate_via_api! }.to raise_error(described_class::ResourceFabricationFailedError, "Fabrication of FooBarFactory using the API failed (400) with `#{raw_post}`.") + expect { subject.fabricate_via_api! }.to raise_error(described_class::ResourceFabricationFailedError, "Fabrication of FooBarResource using the API failed (400) with `#{raw_post}`.") expect(subject.api_resource).to be_nil end end end context '#transform_api_resource' do - let(:factory) do + let(:resource) do Class.new do def self.name - 'FooBarFactory' + 'FooBarResource' end def api_get_path @@ -146,12 +146,12 @@ describe QA::Factory::ApiFabricator do end end - let(:resource) { { existing: 'foo', web_url: resource_web_url } } + let(:response) { { existing: 'foo', web_url: resource_web_url } } let(:transformed_resource) { { existing: 'foo', new: 'foobar', web_url: resource_web_url } } it 'transforms the resource' do expect(subject).to receive(:post).with(resource_web_url, subject.api_post_body).and_return(raw_post) - expect(subject).to receive(:transform_api_resource).with(resource).and_return(transformed_resource) + expect(subject).to receive(:transform_api_resource).with(response).and_return(transformed_resource) subject.fabricate_via_api! end diff --git a/qa/spec/factory/base_spec.rb b/qa/spec/resource/base_spec.rb index 990eba76460..dc9e16792d3 100644 --- a/qa/spec/factory/base_spec.rb +++ b/qa/spec/resource/base_spec.rb @@ -1,49 +1,49 @@ # frozen_string_literal: true -describe QA::Factory::Base do +describe QA::Resource::Base do include Support::StubENV - let(:factory) { spy('factory') } + let(:resource) { spy('resource') } let(:location) { 'http://location' } shared_context 'fabrication context' do subject do Class.new(described_class) do def self.name - 'MyFactory' + 'MyResource' end end end before do allow(subject).to receive(:current_url).and_return(location) - allow(subject).to receive(:new).and_return(factory) + allow(subject).to receive(:new).and_return(resource) end end shared_examples 'fabrication method' do |fabrication_method_called, actual_fabrication_method = nil| let(:fabrication_method_used) { actual_fabrication_method || fabrication_method_called } - it 'yields factory before calling factory method' do - expect(factory).to receive(:something!).ordered - expect(factory).to receive(fabrication_method_used).ordered.and_return(location) + it 'yields resource before calling resource method' do + expect(resource).to receive(:something!).ordered + expect(resource).to receive(fabrication_method_used).ordered.and_return(location) - subject.public_send(fabrication_method_called, factory: factory) do |factory| - factory.something! + subject.public_send(fabrication_method_called, resource: resource) do |resource| + resource.something! end end - it 'does not log the factory and build method when QA_DEBUG=false' do + it 'does not log the resource and build method when QA_DEBUG=false' do stub_env('QA_DEBUG', 'false') - expect(factory).to receive(fabrication_method_used).and_return(location) + expect(resource).to receive(fabrication_method_used).and_return(location) - expect { subject.public_send(fabrication_method_called, 'something', factory: factory) } + expect { subject.public_send(fabrication_method_called, 'something', resource: resource) } .not_to output.to_stdout end end describe '.fabricate!' do - context 'when factory does not support fabrication via the API' do + context 'when resource does not support fabrication via the API' do before do expect(described_class).to receive(:fabricate_via_api!).and_raise(NotImplementedError) end @@ -55,7 +55,7 @@ describe QA::Factory::Base do end end - context 'when factory supports fabrication via the API' do + context 'when resource supports fabrication via the API' do it 'calls .fabricate_via_browser_ui!' do expect(described_class).to receive(:fabricate_via_api!) @@ -69,20 +69,20 @@ describe QA::Factory::Base do it_behaves_like 'fabrication method', :fabricate_via_api! - it 'instantiates the factory, calls factory method returns the resource' do - expect(factory).to receive(:fabricate_via_api!).and_return(location) + it 'instantiates the resource, calls resource method returns the resource' do + expect(resource).to receive(:fabricate_via_api!).and_return(location) - result = subject.fabricate_via_api!(factory: factory, parents: []) + result = subject.fabricate_via_api!(resource: resource, parents: []) - expect(result).to eq(factory) + expect(result).to eq(resource) end - it 'logs the factory and build method when QA_DEBUG=true' do + it 'logs the resource and build method when QA_DEBUG=true' do stub_env('QA_DEBUG', 'true') - expect(factory).to receive(:fabricate_via_api!).and_return(location) + expect(resource).to receive(:fabricate_via_api!).and_return(location) - expect { subject.fabricate_via_api!('something', factory: factory, parents: []) } - .to output(/==> Built a MyFactory via api in [\d\.\-e]+ seconds+/) + expect { subject.fabricate_via_api!('something', resource: resource, parents: []) } + .to output(/==> Built a MyResource via api in [\d\.\-e]+ seconds+/) .to_stdout end end @@ -92,30 +92,30 @@ describe QA::Factory::Base do it_behaves_like 'fabrication method', :fabricate_via_browser_ui!, :fabricate! - it 'instantiates the factory and calls factory method' do - subject.fabricate_via_browser_ui!('something', factory: factory, parents: []) + it 'instantiates the resource and calls resource method' do + subject.fabricate_via_browser_ui!('something', resource: resource, parents: []) - expect(factory).to have_received(:fabricate!).with('something') + expect(resource).to have_received(:fabricate!).with('something') end it 'returns fabrication resource' do - result = subject.fabricate_via_browser_ui!('something', factory: factory, parents: []) + result = subject.fabricate_via_browser_ui!('something', resource: resource, parents: []) - expect(result).to eq(factory) + expect(result).to eq(resource) end - it 'logs the factory and build method when QA_DEBUG=true' do + it 'logs the resource and build method when QA_DEBUG=true' do stub_env('QA_DEBUG', 'true') - expect { subject.fabricate_via_browser_ui!('something', factory: factory, parents: []) } - .to output(/==> Built a MyFactory via browser_ui in [\d\.\-e]+ seconds+/) + expect { subject.fabricate_via_browser_ui!('something', resource: resource, parents: []) } + .to output(/==> Built a MyResource via browser_ui in [\d\.\-e]+ seconds+/) .to_stdout end end - shared_context 'simple factory' do + shared_context 'simple resource' do subject do - Class.new(QA::Factory::Base) do + Class.new(QA::Resource::Base) do attribute :test do 'block' end @@ -132,11 +132,11 @@ describe QA::Factory::Base do end end - let(:factory) { subject.new } + let(:resource) { subject.new } end describe '.attribute' do - include_context 'simple factory' + include_context 'simple resource' it 'appends new attribute' do expect(subject.attributes_names).to eq([:no_block, :test, :web_url]) @@ -144,7 +144,7 @@ describe QA::Factory::Base do context 'when the attribute is populated via a block' do it 'returns value from the block' do - result = subject.fabricate!(factory: factory) + result = subject.fabricate!(resource: resource) expect(result).to be_a(described_class) expect(result.test).to eq('block') @@ -155,11 +155,11 @@ describe QA::Factory::Base do let(:api_resource) { { no_block: 'api' } } before do - expect(factory).to receive(:api_resource).and_return(api_resource) + expect(resource).to receive(:api_resource).and_return(api_resource) end it 'returns value from api' do - result = subject.fabricate!(factory: factory) + result = subject.fabricate!(resource: resource) expect(result).to be_a(described_class) expect(result.no_block).to eq('api') @@ -173,7 +173,7 @@ describe QA::Factory::Base do end it 'returns value from api and emits an INFO log entry' do - result = subject.fabricate!(factory: factory) + result = subject.fabricate!(resource: resource) expect(result).to be_a(described_class) expect(result.test).to eq('api_with_block') @@ -185,11 +185,11 @@ describe QA::Factory::Base do context 'when the attribute is populated via direct assignment' do before do - factory.test = 'value' + resource.test = 'value' end it 'returns value from the assignment' do - result = subject.fabricate!(factory: factory) + result = subject.fabricate!(resource: resource) expect(result).to be_a(described_class) expect(result.test).to eq('value') @@ -197,11 +197,11 @@ describe QA::Factory::Base do context 'when the api also has such response' do before do - allow(factory).to receive(:api_resource).and_return({ test: 'api' }) + allow(resource).to receive(:api_resource).and_return({ test: 'api' }) end it 'returns value from the assignment' do - result = subject.fabricate!(factory: factory) + result = subject.fabricate!(resource: resource) expect(result).to be_a(described_class) expect(result.test).to eq('value') @@ -211,36 +211,36 @@ describe QA::Factory::Base do context 'when the attribute has no value' do it 'raises an error because no values could be found' do - result = subject.fabricate!(factory: factory) + result = subject.fabricate!(resource: resource) expect { result.no_block } - .to raise_error(described_class::NoValueError, "No value was computed for no_block of #{factory.class.name}.") + .to raise_error(described_class::NoValueError, "No value was computed for no_block of #{resource.class.name}.") end end end describe '#web_url' do - include_context 'simple factory' + include_context 'simple resource' it 'sets #web_url to #current_url after fabrication' do - subject.fabricate!(factory: factory) + subject.fabricate!(resource: resource) - expect(factory.web_url).to eq(subject.current_url) + expect(resource.web_url).to eq(subject.current_url) end end describe '#visit!' do - include_context 'simple factory' + include_context 'simple resource' before do - allow(factory).to receive(:visit) + allow(resource).to receive(:visit) end it 'calls #visit with the underlying #web_url' do - factory.web_url = subject.current_url - factory.visit! + resource.web_url = subject.current_url + resource.visit! - expect(factory).to have_received(:visit).with(subject.current_url) + expect(resource).to have_received(:visit).with(subject.current_url) end end end diff --git a/qa/spec/factory/repository/push_spec.rb b/qa/spec/resource/repository/push_spec.rb index 2eb6c008248..bf3ebce0cfe 100644 --- a/qa/spec/factory/repository/push_spec.rb +++ b/qa/spec/resource/repository/push_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -describe QA::Factory::Repository::Push do +describe QA::Resource::Repository::Push do describe '.files=' do let(:files) do [ diff --git a/spec/controllers/groups/boards_controller_spec.rb b/spec/controllers/groups/boards_controller_spec.rb index f7a4a4192d6..99429c93b82 100644 --- a/spec/controllers/groups/boards_controller_spec.rb +++ b/spec/controllers/groups/boards_controller_spec.rb @@ -32,12 +32,13 @@ describe Groups::BoardsController do end it 'renders template if visited board is not found' do - visited = double + temporary_board = create(:board, group: group) + visited = create(:board_group_recent_visit, group: temporary_board.group, board: temporary_board, user: user) + temporary_board.delete - allow(visited).to receive(:board_id).and_return(12) allow_any_instance_of(Boards::Visits::LatestService).to receive(:execute).and_return(visited) - list_boards format: :html + list_boards expect(response).to render_template :index expect(response.content_type).to eq 'text/html' diff --git a/spec/controllers/projects/boards_controller_spec.rb b/spec/controllers/projects/boards_controller_spec.rb index 667eaa5e34f..8d503f6ad32 100644 --- a/spec/controllers/projects/boards_controller_spec.rb +++ b/spec/controllers/projects/boards_controller_spec.rb @@ -38,9 +38,10 @@ describe Projects::BoardsController do end it 'renders template if visited board is not found' do - visited = double + temporary_board = create(:board, project: project) + visited = create(:board_project_recent_visit, project: temporary_board.project, board: temporary_board, user: user) + temporary_board.delete - allow(visited).to receive(:board_id).and_return(12) allow_any_instance_of(Boards::Visits::LatestService).to receive(:execute).and_return(visited) list_boards diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb index 85ba7d4097d..0cacdf7931f 100644 --- a/spec/factories/ci/builds.rb +++ b/spec/factories/ci/builds.rb @@ -27,6 +27,12 @@ FactoryBot.define do pipeline factory: :ci_pipeline + trait :degenerated do + commands nil + options nil + yaml_variables nil + end + trait :started do started_at 'Di 29. Okt 09:51:28 CET 2013' end diff --git a/spec/features/projects/environments/environment_spec.rb b/spec/features/projects/environments/environment_spec.rb index 70e0879dd81..4f8f67aab22 100644 --- a/spec/features/projects/environments/environment_spec.rb +++ b/spec/features/projects/environments/environment_spec.rb @@ -53,10 +53,21 @@ describe 'Environment' do it 'does show build name' do expect(page).to have_link("#{build.name} (##{build.id})") - expect(page).to have_link('Re-deploy') + expect(page).not_to have_link('Re-deploy') expect(page).not_to have_terminal_button end + context 'when user has ability to re-deploy' do + let(:permissions) do + create(:protected_branch, :developers_can_merge, + name: build.ref, project: project) + end + + it 'does show re-deploy' do + expect(page).to have_link('Re-deploy') + end + end + context 'with manual action' do let(:action) do create(:ci_build, :manual, pipeline: pipeline, diff --git a/spec/features/projects/environments/environments_spec.rb b/spec/features/projects/environments/environments_spec.rb index 917ba495f01..22d0187ac81 100644 --- a/spec/features/projects/environments/environments_spec.rb +++ b/spec/features/projects/environments/environments_spec.rb @@ -162,7 +162,7 @@ describe 'Environments page', :js do end it 'shows a play button' do - find('.js-dropdown-play-icon-container').click + find('.js-environment-actions-dropdown').click expect(page).to have_content(action.name.humanize) end @@ -170,7 +170,7 @@ describe 'Environments page', :js do it 'allows to play a manual action', :js do expect(action).to be_manual - find('.js-dropdown-play-icon-container').click + find('.js-environment-actions-dropdown').click expect(page).to have_content(action.name.humanize) expect { find('.js-manual-action-link').click } @@ -260,6 +260,69 @@ describe 'Environments page', :js do end end end + + context 'when there is a delayed job' do + let!(:pipeline) { create(:ci_pipeline, project: project) } + let!(:build) { create(:ci_build, pipeline: pipeline) } + + let!(:delayed_job) do + create(:ci_build, :scheduled, + pipeline: pipeline, + name: 'delayed job', + stage: 'test', + commands: 'test') + end + + let!(:deployment) do + create(:deployment, + environment: environment, + deployable: build, + sha: project.commit.id) + end + + before do + visit_environments(project) + end + + it 'has a dropdown for actionable jobs' do + expect(page).to have_selector('.dropdown-new.btn.btn-default .ic-play') + end + + it "has link to the delayed job's action" do + find('.js-environment-actions-dropdown').click + + expect(page).to have_button('Delayed job') + expect(page).to have_content(/\d{2}:\d{2}:\d{2}/) + end + + context 'when delayed job is expired already' do + let!(:delayed_job) do + create(:ci_build, :expired_scheduled, + pipeline: pipeline, + name: 'delayed job', + stage: 'test', + commands: 'test') + end + + it "shows 00:00:00 as the remaining time" do + find('.js-environment-actions-dropdown').click + + expect(page).to have_content("00:00:00") + end + end + + context 'when user played a delayed job immediately' do + before do + find('.js-environment-actions-dropdown').click + page.accept_confirm { click_button('Delayed job') } + wait_for_requests + end + + it 'enqueues the delayed job', :js do + expect(delayed_job.reload).to be_pending + end + end + end end end diff --git a/spec/features/projects/pipelines/pipeline_spec.rb b/spec/features/projects/pipelines/pipeline_spec.rb index cd6c37bf54d..049bbca958f 100644 --- a/spec/features/projects/pipelines/pipeline_spec.rb +++ b/spec/features/projects/pipelines/pipeline_spec.rb @@ -388,54 +388,83 @@ describe 'Pipeline', :js do let(:pipeline_failures_page) { failures_project_pipeline_path(project, pipeline) } let!(:failed_build) { create(:ci_build, :failed, pipeline: pipeline) } + subject { visit pipeline_failures_page } + context 'with failed build' do before do failed_build.trace.set('4 examples, 1 failure') - - visit pipeline_failures_page end it 'shows jobs tab pane as active' do + subject + expect(page).to have_content('Failed Jobs') expect(page).to have_css('#js-tab-failures.active') end it 'lists failed builds' do + subject + expect(page).to have_content(failed_build.name) expect(page).to have_content(failed_build.stage) end it 'shows build failure logs' do + subject + expect(page).to have_content('4 examples, 1 failure') end it 'shows the failure reason' do + subject + expect(page).to have_content('There is an unknown failure, please try again') end - it 'shows retry button for failed build' do - page.within(find('.build-failures', match: :first)) do - expect(page).to have_link('Retry') + context 'when user does not have permission to retry build' do + it 'shows retry button for failed build' do + subject + + page.within(find('.build-failures', match: :first)) do + expect(page).not_to have_link('Retry') + end end end - end - context 'when missing build logs' do - before do - visit pipeline_failures_page + context 'when user does have permission to retry build' do + before do + create(:protected_branch, :developers_can_merge, + name: pipeline.ref, project: project) + end + + it 'shows retry button for failed build' do + subject + + page.within(find('.build-failures', match: :first)) do + expect(page).to have_link('Retry') + end + end end + end + context 'when missing build logs' do it 'shows jobs tab pane as active' do + subject + expect(page).to have_content('Failed Jobs') expect(page).to have_css('#js-tab-failures.active') end it 'lists failed builds' do + subject + expect(page).to have_content(failed_build.name) expect(page).to have_content(failed_build.stage) end it 'does not show trace' do + subject + expect(page).to have_content('No job trace') end end @@ -448,11 +477,9 @@ describe 'Pipeline', :js do end context 'when accessing failed jobs page' do - before do - visit pipeline_failures_page - end - it 'fails to access the page' do + subject + expect(page).to have_title('Access Denied') end end @@ -461,11 +488,11 @@ describe 'Pipeline', :js do context 'without failures' do before do failed_build.update!(status: :success) - - visit pipeline_failures_page end it 'displays the pipeline graph' do + subject + expect(current_path).to eq(pipeline_path(pipeline)) expect(page).not_to have_content('Failed Jobs') expect(page).to have_selector('.pipeline-visualization') diff --git a/spec/finders/notes_finder_spec.rb b/spec/finders/notes_finder_spec.rb index de9974c45e1..b51f1955ac4 100644 --- a/spec/finders/notes_finder_spec.rb +++ b/spec/finders/notes_finder_spec.rb @@ -13,7 +13,7 @@ describe NotesFinder do let!(:comment) { create(:note_on_issue, project: project) } let!(:system_note) { create(:note_on_issue, project: project, system: true) } - it 'filters system notes' do + it 'returns only user notes when using only_comments filter' do finder = described_class.new(project, user, notes_filter: UserPreference::NOTES_FILTERS[:only_comments]) notes = finder.execute @@ -21,6 +21,14 @@ describe NotesFinder do expect(notes).to match_array(comment) end + it 'returns only system notes when using only_activity filters' do + finder = described_class.new(project, user, notes_filter: UserPreference::NOTES_FILTERS[:only_activity]) + + notes = finder.execute + + expect(notes).to match_array(system_note) + end + it 'gets all notes' do finder = described_class.new(project, user, notes_filter: UserPreference::NOTES_FILTERS[:all_activity]) diff --git a/spec/finders/personal_access_tokens_finder_spec.rb b/spec/finders/personal_access_tokens_finder_spec.rb index 3f22b3a253d..3e849c9a644 100644 --- a/spec/finders/personal_access_tokens_finder_spec.rb +++ b/spec/finders/personal_access_tokens_finder_spec.rb @@ -92,7 +92,7 @@ describe PersonalAccessTokensFinder do end describe 'with id' do - subject { finder(params).find_by(id: active_personal_access_token.id) } + subject { finder(params).find_by_id(active_personal_access_token.id) } it { is_expected.to eq(active_personal_access_token) } @@ -106,7 +106,7 @@ describe PersonalAccessTokensFinder do end describe 'with token' do - subject { finder(params).find_by(token: active_personal_access_token.token) } + subject { finder(params).find_by_token(active_personal_access_token.token) } it { is_expected.to eq(active_personal_access_token) } @@ -207,7 +207,7 @@ describe PersonalAccessTokensFinder do end describe 'with id' do - subject { finder(params).find_by(id: active_personal_access_token.id) } + subject { finder(params).find_by_id(active_personal_access_token.id) } it { is_expected.to eq(active_personal_access_token) } @@ -221,7 +221,7 @@ describe PersonalAccessTokensFinder do end describe 'with token' do - subject { finder(params).find_by(token: active_personal_access_token.token) } + subject { finder(params).find_by_token(active_personal_access_token.token) } it { is_expected.to eq(active_personal_access_token) } diff --git a/spec/fixtures/api/schemas/deployment.json b/spec/fixtures/api/schemas/deployment.json index 44835386cfc..0828f113495 100644 --- a/spec/fixtures/api/schemas/deployment.json +++ b/spec/fixtures/api/schemas/deployment.json @@ -48,6 +48,10 @@ "manual_actions": { "type": "array", "items": { "$ref": "job/job.json" } + }, + "scheduled_actions": { + "type": "array", + "items": { "$ref": "job/job.json" } } }, "additionalProperties": false diff --git a/spec/fixtures/api/schemas/job/job.json b/spec/fixtures/api/schemas/job/job.json index 734c535ef70..f3d5e9b038a 100644 --- a/spec/fixtures/api/schemas/job/job.json +++ b/spec/fixtures/api/schemas/job/job.json @@ -9,7 +9,8 @@ "playable", "created_at", "updated_at", - "status" + "status", + "archived" ], "properties": { "id": { "type": "integer" }, @@ -27,7 +28,8 @@ "updated_at": { "type": "string" }, "status": { "$ref": "../status/ci_detailed_status.json" }, "callout_message": { "type": "string" }, - "recoverable": { "type": "boolean" } + "recoverable": { "type": "boolean" }, + "archived": { "type": "boolean" } }, "additionalProperties": true } diff --git a/spec/helpers/tree_helper_spec.rb b/spec/helpers/tree_helper_spec.rb index ffdf6561a53..ab4566e261b 100644 --- a/spec/helpers/tree_helper_spec.rb +++ b/spec/helpers/tree_helper_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe TreeHelper do let(:project) { create(:project, :repository) } let(:repository) { project.repository } - let(:sha) { 'ce369011c189f62c815f5971d096b26759bab0d1' } + let(:sha) { 'c1c67abbaf91f624347bb3ae96eabe3a1b742478' } describe '.render_tree' do before do @@ -32,6 +32,49 @@ describe TreeHelper do end end + describe '.fast_project_blob_path' do + it 'generates the same path as project_blob_path' do + blob_path = repository.tree(sha, 'with space').entries.first.path + fast_path = fast_project_blob_path(project, blob_path) + std_path = project_blob_path(project, blob_path) + + expect(fast_path).to eq(std_path) + end + + it 'generates the same path with encoded file names' do + tree = repository.tree(sha, 'encoding') + blob_path = tree.entries.find { |entry| entry.path == 'encoding/ćć¹ć.txt' }.path + fast_path = fast_project_blob_path(project, blob_path) + std_path = project_blob_path(project, blob_path) + + expect(fast_path).to eq(std_path) + end + + it 'respects a configured relative URL' do + allow(Gitlab.config.gitlab).to receive(:relative_url_root).and_return('/gitlab/root') + blob_path = repository.tree(sha, '').entries.first.path + fast_path = fast_project_blob_path(project, blob_path) + + expect(fast_path).to start_with('/gitlab/root') + end + end + + describe '.fast_project_tree_path' do + let(:tree_path) { repository.tree(sha, 'with space').path } + let(:fast_path) { fast_project_tree_path(project, tree_path) } + let(:std_path) { project_tree_path(project, tree_path) } + + it 'generates the same path as project_tree_path' do + expect(fast_path).to eq(std_path) + end + + it 'respects a configured relative URL' do + allow(Gitlab.config.gitlab).to receive(:relative_url_root).and_return('/gitlab/root') + + expect(fast_path).to start_with('/gitlab/root') + end + end + describe 'flatten_tree' do let(:tree) { repository.tree(sha, 'files') } let(:root_path) { 'files' } diff --git a/spec/javascripts/dirty_submit/dirty_submit_form_spec.js b/spec/javascripts/dirty_submit/dirty_submit_form_spec.js index b7b29190c31..093fec97951 100644 --- a/spec/javascripts/dirty_submit/dirty_submit_form_spec.js +++ b/spec/javascripts/dirty_submit/dirty_submit_form_spec.js @@ -1,23 +1,35 @@ import DirtySubmitForm from '~/dirty_submit/dirty_submit_form'; import { setInput, createForm } from './helper'; +function expectToToggleDisableOnDirtyUpdate(submit, input) { + const originalValue = input.value; + + expect(submit.disabled).toBe(true); + + return setInput(input, `${originalValue} changes`) + .then(() => expect(submit.disabled).toBe(false)) + .then(() => setInput(input, originalValue)) + .then(() => expect(submit.disabled).toBe(true)); +} + describe('DirtySubmitForm', () => { it('disables submit until there are changes', done => { const { form, input, submit } = createForm(); - const originalValue = input.value; new DirtySubmitForm(form); // eslint-disable-line no-new - expect(submit.disabled).toBe(true); + return expectToToggleDisableOnDirtyUpdate(submit, input) + .then(done) + .catch(done.fail); + }); + + it('disables submit until there are changes when initializing with a falsy value', done => { + const { form, input, submit } = createForm(); + input.value = ''; + + new DirtySubmitForm(form); // eslint-disable-line no-new - return setInput(input, `${originalValue} changes`) - .then(() => { - expect(submit.disabled).toBe(false); - }) - .then(() => setInput(input, originalValue)) - .then(() => { - expect(submit.disabled).toBe(true); - }) + return expectToToggleDisableOnDirtyUpdate(submit, input) .then(done) .catch(done.fail); }); diff --git a/spec/javascripts/environments/environment_actions_spec.js b/spec/javascripts/environments/environment_actions_spec.js index 223153d4e31..787df757d32 100644 --- a/spec/javascripts/environments/environment_actions_spec.js +++ b/spec/javascripts/environments/environment_actions_spec.js @@ -1,15 +1,19 @@ import Vue from 'vue'; -import actionsComp from '~/environments/components/environment_actions.vue'; +import eventHub from '~/environments/event_hub'; +import EnvironmentActions from '~/environments/components/environment_actions.vue'; +import mountComponent from 'spec/helpers/vue_mount_component_helper'; +import { TEST_HOST } from 'spec/test_constants'; -describe('Actions Component', () => { - let ActionsComponent; - let actionsMock; - let component; +describe('EnvironmentActions Component', () => { + const Component = Vue.extend(EnvironmentActions); + let vm; - beforeEach(() => { - ActionsComponent = Vue.extend(actionsComp); + afterEach(() => { + vm.$destroy(); + }); - actionsMock = [ + describe('manual actions', () => { + const actions = [ { name: 'bar', play_path: 'https://gitlab.com/play', @@ -25,43 +29,89 @@ describe('Actions Component', () => { }, ]; - component = new ActionsComponent({ - propsData: { - actions: actionsMock, - }, - }).$mount(); - }); + beforeEach(() => { + vm = mountComponent(Component, { actions }); + }); + + it('should render a dropdown button with icon and title attribute', () => { + expect(vm.$el.querySelector('.fa-caret-down')).toBeDefined(); + expect(vm.$el.querySelector('.dropdown-new').getAttribute('data-original-title')).toEqual( + 'Deploy to...', + ); - describe('computed', () => { - it('title', () => { - expect(component.title).toEqual('Deploy to...'); + expect(vm.$el.querySelector('.dropdown-new').getAttribute('aria-label')).toEqual( + 'Deploy to...', + ); }); - }); - it('should render a dropdown button with icon and title attribute', () => { - expect(component.$el.querySelector('.fa-caret-down')).toBeDefined(); - expect( - component.$el.querySelector('.dropdown-new').getAttribute('data-original-title'), - ).toEqual('Deploy to...'); + it('should render a dropdown with the provided list of actions', () => { + expect(vm.$el.querySelectorAll('.dropdown-menu li').length).toEqual(actions.length); + }); - expect(component.$el.querySelector('.dropdown-new').getAttribute('aria-label')).toEqual( - 'Deploy to...', - ); - }); + it("should render a disabled action when it's not playable", () => { + expect( + vm.$el.querySelector('.dropdown-menu li:last-child button').getAttribute('disabled'), + ).toEqual('disabled'); - it('should render a dropdown with the provided list of actions', () => { - expect(component.$el.querySelectorAll('.dropdown-menu li').length).toEqual(actionsMock.length); + expect( + vm.$el.querySelector('.dropdown-menu li:last-child button').classList.contains('disabled'), + ).toEqual(true); + }); }); - it("should render a disabled action when it's not playable", () => { - expect( - component.$el.querySelector('.dropdown-menu li:last-child button').getAttribute('disabled'), - ).toEqual('disabled'); + describe('scheduled jobs', () => { + const scheduledJobAction = { + name: 'scheduled action', + playPath: `${TEST_HOST}/scheduled/job/action`, + playable: true, + scheduledAt: '2063-04-05T00:42:00Z', + }; + const expiredJobAction = { + name: 'expired action', + playPath: `${TEST_HOST}/expired/job/action`, + playable: true, + scheduledAt: '2018-10-05T08:23:00Z', + }; + const findDropdownItem = action => { + const buttons = vm.$el.querySelectorAll('.dropdown-menu li button'); + return Array.prototype.find.call(buttons, element => + element.innerText.trim().startsWith(action.name), + ); + }; + + beforeEach(() => { + spyOn(Date, 'now').and.callFake(() => new Date('2063-04-04T00:42:00Z').getTime()); + vm = mountComponent(Component, { actions: [scheduledJobAction, expiredJobAction] }); + }); + + it('emits postAction event after confirming', () => { + const emitSpy = jasmine.createSpy('emit'); + eventHub.$on('postAction', emitSpy); + spyOn(window, 'confirm').and.callFake(() => true); + + findDropdownItem(scheduledJobAction).click(); + + expect(window.confirm).toHaveBeenCalled(); + expect(emitSpy).toHaveBeenCalledWith({ endpoint: scheduledJobAction.playPath }); + }); + + it('does not emit postAction event if confirmation is cancelled', () => { + const emitSpy = jasmine.createSpy('emit'); + eventHub.$on('postAction', emitSpy); + spyOn(window, 'confirm').and.callFake(() => false); + + findDropdownItem(scheduledJobAction).click(); - expect( - component.$el - .querySelector('.dropdown-menu li:last-child button') - .classList.contains('disabled'), - ).toEqual(true); + expect(window.confirm).toHaveBeenCalled(); + expect(emitSpy).not.toHaveBeenCalled(); + }); + + it('displays the remaining time in the dropdown', () => { + expect(findDropdownItem(scheduledJobAction)).toContainText('24:00:00'); + }); + + it('displays 00:00:00 for expired jobs in the dropdown', () => { + expect(findDropdownItem(expiredJobAction)).toContainText('00:00:00'); + }); }); }); diff --git a/spec/javascripts/notes/components/discussion_filter_spec.js b/spec/javascripts/notes/components/discussion_filter_spec.js index a81bdf618a3..9070d968cfd 100644 --- a/spec/javascripts/notes/components/discussion_filter_spec.js +++ b/spec/javascripts/notes/components/discussion_filter_spec.js @@ -19,7 +19,7 @@ describe('DiscussionFilter component', () => { }, ]; const Component = Vue.extend(DiscussionFilter); - const defaultValue = discussionFiltersMock[0].value; + const selectedValue = discussionFiltersMock[0].value; store.state.discussions = discussions; vm = mountComponentWithStore(Component, { @@ -27,7 +27,7 @@ describe('DiscussionFilter component', () => { store, props: { filters: discussionFiltersMock, - defaultValue, + selectedValue, }, }); }); @@ -63,4 +63,24 @@ describe('DiscussionFilter component', () => { expect(vm.filterDiscussion).not.toHaveBeenCalled(); }); + + it('disables commenting when "Show history only" filter is applied', () => { + const filterItem = vm.$el.querySelector('.dropdown-menu li:last-child button'); + filterItem.click(); + + expect(vm.$store.state.commentsDisabled).toBe(true); + }); + + it('enables commenting when "Show history only" filter is not applied', () => { + const filterItem = vm.$el.querySelector('.dropdown-menu li:first-child button'); + filterItem.click(); + + expect(vm.$store.state.commentsDisabled).toBe(false); + }); + + it('renders a dropdown divider for the default filter', () => { + const defaultFilter = vm.$el.querySelector('.dropdown-menu li:first-child'); + + expect(defaultFilter.lastChild.classList).toContain('dropdown-divider'); + }); }); diff --git a/spec/javascripts/notes/components/note_app_spec.js b/spec/javascripts/notes/components/note_app_spec.js index 3e289a6b8e6..0081f42c330 100644 --- a/spec/javascripts/notes/components/note_app_spec.js +++ b/spec/javascripts/notes/components/note_app_spec.js @@ -121,6 +121,13 @@ describe('note_app', () => { ).toEqual('Write a comment or drag your files hereā¦'); }); + it('should not render form when commenting is disabled', () => { + store.state.commentsDisabled = true; + vm = mountComponent(); + + expect(vm.$el.querySelector('.js-main-target-form')).toEqual(null); + }); + it('should render form comment button as disabled', () => { expect(vm.$el.querySelector('.js-note-new-discussion').getAttribute('disabled')).toEqual( 'disabled', diff --git a/spec/javascripts/notes/stores/actions_spec.js b/spec/javascripts/notes/stores/actions_spec.js index f4643fd55ed..0c0bc45b201 100644 --- a/spec/javascripts/notes/stores/actions_spec.js +++ b/spec/javascripts/notes/stores/actions_spec.js @@ -509,4 +509,17 @@ describe('Actions Notes Store', () => { expect(mrWidgetEventHub.$emit).toHaveBeenCalledWith('mr.discussion.updated'); }); }); + + describe('setCommentsDisabled', () => { + it('should set comments disabled state', done => { + testAction( + actions.setCommentsDisabled, + true, + null, + [{ type: 'DISABLE_COMMENTS', payload: true }], + [], + done, + ); + }); + }); }); diff --git a/spec/javascripts/notes/stores/mutation_spec.js b/spec/javascripts/notes/stores/mutation_spec.js index 380ab59099d..461de5a3106 100644 --- a/spec/javascripts/notes/stores/mutation_spec.js +++ b/spec/javascripts/notes/stores/mutation_spec.js @@ -427,4 +427,14 @@ describe('Notes Store mutations', () => { expect(state.discussions[0].expanded).toBe(true); }); }); + + describe('DISABLE_COMMENTS', () => { + it('should set comments disabled state', () => { + const state = {}; + + mutations.DISABLE_COMMENTS(state, true); + + expect(state.commentsDisabled).toEqual(true); + }); + }); }); diff --git a/spec/lib/gitlab/ci/config/entry/reports_spec.rb b/spec/lib/gitlab/ci/config/entry/reports_spec.rb index 8095a231cf3..1140bfdf6c3 100644 --- a/spec/lib/gitlab/ci/config/entry/reports_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/reports_spec.rb @@ -33,7 +33,7 @@ describe Gitlab::Ci::Config::Entry::Reports do where(:keyword, :file) do :junit | 'junit.xml' - :codequality | 'codequality.json' + :codequality | 'gl-code-quality-report.json' :sast | 'gl-sast-report.json' :dependency_scanning | 'gl-dependency-scanning-report.json' :container_scanning | 'gl-container-scanning-report.json' diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index a63f34b5536..f4efa450cca 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -299,6 +299,7 @@ project: - ci_cd_settings - import_export_upload - repository_languages +- pool_repository award_emoji: - awardable - user diff --git a/spec/lib/gitlab/kubernetes/helm/pod_spec.rb b/spec/lib/gitlab/kubernetes/helm/pod_spec.rb index b333b334f36..c92bc92c42d 100644 --- a/spec/lib/gitlab/kubernetes/helm/pod_spec.rb +++ b/spec/lib/gitlab/kubernetes/helm/pod_spec.rb @@ -30,7 +30,7 @@ describe Gitlab::Kubernetes::Helm::Pod do it 'should generate the appropriate specifications for the container' do container = subject.generate.spec.containers.first expect(container.name).to eq('helm') - expect(container.image).to eq('alpine:3.6') + expect(container.image).to eq('registry.gitlab.com/gitlab-org/cluster-integration/helm-install-image/releases/2.7.2-kube-1.11.0') expect(container.env.count).to eq(3) expect(container.env.map(&:name)).to match_array([:HELM_VERSION, :TILLER_NAMESPACE, :COMMAND_SCRIPT]) expect(container.command).to match_array(["/bin/sh"]) diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb index 87b91286168..95ae7bd21ab 100644 --- a/spec/models/application_setting_spec.rb +++ b/spec/models/application_setting_spec.rb @@ -594,4 +594,24 @@ describe ApplicationSetting do end end end + + describe '#archive_builds_older_than' do + subject { setting.archive_builds_older_than } + + context 'when the archive_builds_in_seconds is set' do + before do + setting.archive_builds_in_seconds = 3600 + end + + it { is_expected.to be_within(1.minute).of(1.hour.ago) } + end + + context 'when the archive_builds_in_seconds is set' do + before do + setting.archive_builds_in_seconds = nil + end + + it { is_expected.to be_nil } + end + end end diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index 13a4aaa8936..4089f099fdf 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -1314,6 +1314,14 @@ describe Ci::Build do it { is_expected.not_to be_retryable } end + + context 'when build is degenerated' do + before do + build.degenerate! + end + + it { is_expected.not_to be_retryable } + end end end @@ -1396,6 +1404,14 @@ describe Ci::Build do expect(subject.retries_max).to eq 0 end end + + context 'when build is degenerated' do + subject { create(:ci_build, :degenerated) } + + it 'returns zero' do + expect(subject.retries_max).to eq 0 + end + end end end @@ -1511,11 +1527,11 @@ describe Ci::Build do end end - describe '#other_actions' do + describe '#other_manual_actions' do let(:build) { create(:ci_build, :manual, pipeline: pipeline) } let!(:other_build) { create(:ci_build, :manual, pipeline: pipeline, name: 'other action') } - subject { build.other_actions } + subject { build.other_manual_actions } before do project.add_developer(user) @@ -1546,6 +1562,48 @@ describe Ci::Build do end end + describe '#other_scheduled_actions' do + let(:build) { create(:ci_build, :scheduled, pipeline: pipeline) } + + subject { build.other_scheduled_actions } + + before do + project.add_developer(user) + end + + context "when other build's status is success" do + let!(:other_build) { create(:ci_build, :schedulable, :success, pipeline: pipeline, name: 'other action') } + + it 'returns other actions' do + is_expected.to contain_exactly(other_build) + end + end + + context "when other build's status is failed" do + let!(:other_build) { create(:ci_build, :schedulable, :failed, pipeline: pipeline, name: 'other action') } + + it 'returns other actions' do + is_expected.to contain_exactly(other_build) + end + end + + context "when other build's status is running" do + let!(:other_build) { create(:ci_build, :schedulable, :running, pipeline: pipeline, name: 'other action') } + + it 'does not return other actions' do + is_expected.to be_empty + end + end + + context "when other build's status is scheduled" do + let!(:other_build) { create(:ci_build, :scheduled, pipeline: pipeline, name: 'other action') } + + it 'does not return other actions' do + is_expected.to contain_exactly(other_build) + end + end + end + describe '#persisted_environment' do let!(:environment) do create(:environment, project: project, name: "foo-#{project.default_branch}") @@ -1617,6 +1675,12 @@ describe Ci::Build do it { is_expected.to be_playable } end + + context 'when build is a manual and degenerated' do + subject { build_stubbed(:ci_build, :manual, :degenerated, status: :manual) } + + it { is_expected.not_to be_playable } + end end context 'when build is scheduled' do @@ -3185,4 +3249,54 @@ describe Ci::Build do it { expect(build.deployment_status).to eq(:creating) } end end + + describe '#degenerated?' do + context 'when build is degenerated' do + subject { create(:ci_build, :degenerated) } + + it { is_expected.to be_degenerated } + end + + context 'when build is valid' do + subject { create(:ci_build) } + + it { is_expected.not_to be_degenerated } + + context 'and becomes degenerated' do + before do + subject.degenerate! + end + + it { is_expected.to be_degenerated } + end + end + end + + describe '#archived?' do + context 'when build is degenerated' do + subject { create(:ci_build, :degenerated) } + + it { is_expected.to be_archived } + end + + context 'for old build' do + subject { create(:ci_build, created_at: 1.day.ago) } + + context 'when archive_builds_in is set' do + before do + stub_application_setting(archive_builds_in_seconds: 3600) + end + + it { is_expected.to be_archived } + end + + context 'when archive_builds_in is not set' do + before do + stub_application_setting(archive_builds_in_seconds: nil) + end + + it { is_expected.not_to be_archived } + end + end + end end diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 153244b2159..0f58c06864e 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -1148,6 +1148,19 @@ describe Ci::Pipeline, :mailer do end end + describe '.latest_successful_ids_per_project' do + let(:projects) { create_list(:project, 2) } + let!(:pipeline1) { create(:ci_pipeline, :success, project: projects[0]) } + let!(:pipeline2) { create(:ci_pipeline, :success, project: projects[0]) } + let!(:pipeline3) { create(:ci_pipeline, :failed, project: projects[0]) } + let!(:pipeline4) { create(:ci_pipeline, :success, project: projects[1]) } + + it 'returns expected pipeline ids' do + expect(described_class.latest_successful_ids_per_project) + .to contain_exactly(pipeline2, pipeline4) + end + end + describe '.internal_sources' do subject { described_class.internal_sources } diff --git a/spec/models/concerns/awardable_spec.rb b/spec/models/concerns/awardable_spec.rb index debc02fa51f..5713106418d 100644 --- a/spec/models/concerns/awardable_spec.rb +++ b/spec/models/concerns/awardable_spec.rb @@ -37,8 +37,8 @@ describe Awardable do create(:award_emoji, awardable: issue3, name: "star", user: award_emoji.user) create(:award_emoji, awardable: issue3, name: "star", user: award_emoji2.user) - expect(Issue.awarded(award_emoji.user)).to eq [issue, issue3] - expect(Issue.awarded(award_emoji2.user)).to eq [issue2, issue3] + expect(Issue.awarded(award_emoji.user)).to contain_exactly(issue, issue3) + expect(Issue.awarded(award_emoji2.user)).to contain_exactly(issue2, issue3) end end diff --git a/spec/models/deployment_spec.rb b/spec/models/deployment_spec.rb index b8364e0cf88..146d35122f7 100644 --- a/spec/models/deployment_spec.rb +++ b/spec/models/deployment_spec.rb @@ -16,6 +16,22 @@ describe Deployment do it { is_expected.to validate_presence_of(:ref) } it { is_expected.to validate_presence_of(:sha) } + describe '#scheduled_actions' do + subject { deployment.scheduled_actions } + + let(:project) { create(:project, :repository) } + let(:pipeline) { create(:ci_pipeline, project: project) } + let(:build) { create(:ci_build, :success, pipeline: pipeline) } + let(:deployment) { create(:deployment, deployable: build) } + + it 'delegates to other_scheduled_actions' do + expect_any_instance_of(Ci::Build) + .to receive(:other_scheduled_actions) + + subject + end + end + describe 'modules' do it_behaves_like 'AtomicInternalId' do let(:internal_id_attribute) { :iid } diff --git a/spec/models/merge_request_diff_spec.rb b/spec/models/merge_request_diff_spec.rb index 562ccaf6c0b..47e8f04e728 100644 --- a/spec/models/merge_request_diff_spec.rb +++ b/spec/models/merge_request_diff_spec.rb @@ -211,4 +211,25 @@ describe MergeRequestDiff do expect(diff_with_commits.commits_count).to eq(29) end end + + describe '#commits_by_shas' do + let(:commit_shas) { diff_with_commits.commit_shas } + + it 'returns empty if no SHAs were provided' do + expect(diff_with_commits.commits_by_shas([])).to be_empty + end + + it 'returns one SHA' do + commits = diff_with_commits.commits_by_shas([commit_shas.first, Gitlab::Git::BLANK_SHA]) + + expect(commits.count).to eq(1) + end + + it 'returns all matching SHAs' do + commits = diff_with_commits.commits_by_shas(commit_shas) + + expect(commits.count).to eq(commit_shas.count) + expect(commits.map(&:sha)).to match_array(commit_shas) + end + end end diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 7d500f9e579..2eb5e39ccfd 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -2611,6 +2611,32 @@ describe MergeRequest do end end + describe '#includes_any_commits?' do + it 'returns false' do + expect(subject.includes_any_commits?([Gitlab::Git::BLANK_SHA])).to be_falsey + end + + it 'returns true' do + expect(subject.includes_any_commits?([subject.merge_request_diff.head_commit_sha])).to be_truthy + end + + it 'returns true even when there is a non-existent comit' do + expect(subject.includes_any_commits?([Gitlab::Git::BLANK_SHA, subject.merge_request_diff.head_commit_sha])).to be_truthy + end + + context 'unpersisted merge request' do + let(:new_mr) { build(:merge_request) } + + it 'returns false' do + expect(new_mr.includes_any_commits?([Gitlab::Git::BLANK_SHA])).to be_falsey + end + + it 'returns true' do + expect(new_mr.includes_any_commits?([subject.merge_request_diff.head_commit_sha])).to be_truthy + end + end + end + describe '#can_allow_collaboration?' do let(:target_project) { create(:project, :public) } let(:source_project) { fork_project(target_project) } diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb index 8913644a3ce..2db42fe802a 100644 --- a/spec/models/namespace_spec.rb +++ b/spec/models/namespace_spec.rb @@ -562,6 +562,17 @@ describe Namespace do it { expect(group.all_projects.to_a).to match_array([project2, project1]) } end + describe '#all_pipelines' do + let(:group) { create(:group) } + let(:child) { create(:group, parent: group) } + let!(:project1) { create(:project_empty_repo, namespace: group) } + let!(:project2) { create(:project_empty_repo, namespace: child) } + let!(:pipeline1) { create(:ci_empty_pipeline, project: project1) } + let!(:pipeline2) { create(:ci_empty_pipeline, project: project2) } + + it { expect(group.all_pipelines.to_a).to match_array([pipeline1, pipeline2]) } + end + describe '#share_with_group_lock with subgroups', :nested_groups do context 'when creating a subgroup' do let(:subgroup) { create(:group, parent: root_group )} diff --git a/spec/models/postgresql/replication_slot_spec.rb b/spec/models/postgresql/replication_slot_spec.rb index 919a7526803..e100af7ddc7 100644 --- a/spec/models/postgresql/replication_slot_spec.rb +++ b/spec/models/postgresql/replication_slot_spec.rb @@ -3,7 +3,27 @@ require 'spec_helper' describe Postgresql::ReplicationSlot, :postgresql do + describe '.in_use?' do + it 'returns true when replication slots are present' do + expect(described_class).to receive(:exists?).and_return(true) + expect(described_class.in_use?).to be_truthy + end + + it 'returns false when replication slots are not present' do + expect(described_class.in_use?).to be_falsey + end + + it 'returns false if the existence check is invalid' do + expect(described_class).to receive(:exists?).and_raise(ActiveRecord::StatementInvalid.new('PG::FeatureNotSupported')) + expect(described_class.in_use?).to be_falsey + end + end + describe '.lag_too_great?' do + before do + expect(described_class).to receive(:in_use?).and_return(true) + end + it 'returns true when replication lag is too great' do expect(described_class) .to receive(:pluck) diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index d059854214f..84326724118 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -8,6 +8,7 @@ describe Project do it { is_expected.to belong_to(:group) } it { is_expected.to belong_to(:namespace) } it { is_expected.to belong_to(:creator).class_name('User') } + it { is_expected.to belong_to(:pool_repository) } it { is_expected.to have_many(:users) } it { is_expected.to have_many(:services) } it { is_expected.to have_many(:events) } diff --git a/spec/models/shard_spec.rb b/spec/models/shard_spec.rb new file mode 100644 index 00000000000..83104711b55 --- /dev/null +++ b/spec/models/shard_spec.rb @@ -0,0 +1,50 @@ +# frozen_string_literals: true +require 'spec_helper' + +describe Shard do + describe '.populate!' do + it 'creates shards based on the config file' do + expect(described_class.all).to be_empty + + stub_storage_settings(foo: {}, bar: {}, baz: {}) + + described_class.populate! + + expect(described_class.all.map(&:name)).to match_array(%w[default foo bar baz]) + end + end + + describe '.by_name' do + let(:default_shard) { described_class.find_by(name: 'default') } + + before do + described_class.populate! + end + + it 'returns an existing shard' do + expect(described_class.by_name('default')).to eq(default_shard) + end + + it 'creates a new shard' do + result = described_class.by_name('foo') + + expect(result).not_to eq(default_shard) + expect(result.name).to eq('foo') + end + + it 'retries if creation races' do + expect(described_class) + .to receive(:find_or_create_by) + .with(name: 'default') + .and_raise(ActiveRecord::RecordNotUnique, 'fail') + .once + + expect(described_class) + .to receive(:find_or_create_by) + .with(name: 'default') + .and_call_original + + expect(described_class.by_name('default')).to eq(default_shard) + end + end +end diff --git a/spec/models/user_preference_spec.rb b/spec/models/user_preference_spec.rb index 64d9d9a78b4..2898613545c 100644 --- a/spec/models/user_preference_spec.rb +++ b/spec/models/user_preference_spec.rb @@ -6,22 +6,43 @@ describe UserPreference do describe '#set_notes_filter' do let(:issuable) { build_stubbed(:issue) } let(:user_preference) { create(:user_preference) } - let(:only_comments) { described_class::NOTES_FILTERS[:only_comments] } - it 'returns updated discussion filter' do - filter_name = - user_preference.set_notes_filter(only_comments, issuable) + shared_examples 'setting system notes' do + it 'returns updated discussion filter' do + filter_name = + user_preference.set_notes_filter(filter, issuable) + + expect(filter_name).to eq(filter) + end + + it 'updates discussion filter for issuable class' do + user_preference.set_notes_filter(filter, issuable) + + expect(user_preference.reload.issue_notes_filter).to eq(filter) + end + end + + context 'when filter is set to all notes' do + let(:filter) { described_class::NOTES_FILTERS[:all_notes] } + + it_behaves_like 'setting system notes' + end + + context 'when filter is set to only comments' do + let(:filter) { described_class::NOTES_FILTERS[:only_comments] } - expect(filter_name).to eq(only_comments) + it_behaves_like 'setting system notes' end - it 'updates discussion filter for issuable class' do - user_preference.set_notes_filter(only_comments, issuable) + context 'when filter is set to only activity' do + let(:filter) { described_class::NOTES_FILTERS[:only_activity] } - expect(user_preference.reload.issue_notes_filter).to eq(only_comments) + it_behaves_like 'setting system notes' end context 'when notes_filter parameter is invalid' do + let(:only_comments) { described_class::NOTES_FILTERS[:only_comments] } + it 'returns the current notes filter' do user_preference.set_notes_filter(only_comments, issuable) diff --git a/spec/presenters/ci/build_presenter_spec.rb b/spec/presenters/ci/build_presenter_spec.rb index d7992f0a4a9..676835b3880 100644 --- a/spec/presenters/ci/build_presenter_spec.rb +++ b/spec/presenters/ci/build_presenter_spec.rb @@ -267,7 +267,7 @@ describe Ci::BuildPresenter do let(:build) { create(:ci_build, :failed, :script_failure) } context 'when is a script or missing dependency failure' do - let(:failure_reasons) { %w(script_failure missing_dependency_failure) } + let(:failure_reasons) { %w(script_failure missing_dependency_failure archived_failure) } it 'should return false' do failure_reasons.each do |failure_reason| diff --git a/spec/serializers/deployment_entity_spec.rb b/spec/serializers/deployment_entity_spec.rb index 522c92ce295..8793a762f9d 100644 --- a/spec/serializers/deployment_entity_spec.rb +++ b/spec/serializers/deployment_entity_spec.rb @@ -22,4 +22,26 @@ describe DeploymentEntity do it 'exposes creation date' do expect(subject).to include(:created_at) end + + describe 'scheduled_actions' do + let(:project) { create(:project, :repository) } + let(:pipeline) { create(:ci_pipeline, project: project, user: user) } + let(:build) { create(:ci_build, :success, pipeline: pipeline) } + let(:deployment) { create(:deployment, deployable: build) } + + context 'when the same pipeline has a scheduled action' do + let(:other_build) { create(:ci_build, :schedulable, :success, pipeline: pipeline, name: 'other build') } + let!(:other_deployment) { create(:deployment, deployable: other_build) } + + it 'returns other scheduled actions' do + expect(subject[:scheduled_actions][0][:name]).to eq 'other build' + end + end + + context 'when the same pipeline does not have a scheduled action' do + it 'does not return other actions' do + expect(subject[:scheduled_actions]).to be_empty + end + end + end end diff --git a/spec/services/ci/register_job_service_spec.rb b/spec/services/ci/register_job_service_spec.rb index a6565709641..56e2a405bcd 100644 --- a/spec/services/ci/register_job_service_spec.rb +++ b/spec/services/ci/register_job_service_spec.rb @@ -478,6 +478,20 @@ module Ci it_behaves_like 'validation is not active' end end + + context 'when build is degenerated' do + let!(:pending_job) { create(:ci_build, :pending, :degenerated, pipeline: pipeline) } + + subject { execute(specific_runner, {}) } + + it 'does not pick the build and drops the build' do + expect(subject).to be_nil + + pending_job.reload + expect(pending_job).to be_failed + expect(pending_job).to be_archived_failure + end + end end describe '#register_success' do diff --git a/spec/support/shared_examples/controllers/issuable_notes_filter_shared_examples.rb b/spec/support/shared_examples/controllers/issuable_notes_filter_shared_examples.rb index 9c9d7ad781e..95e69328080 100644 --- a/spec/support/shared_examples/controllers/issuable_notes_filter_shared_examples.rb +++ b/spec/support/shared_examples/controllers/issuable_notes_filter_shared_examples.rb @@ -34,12 +34,24 @@ shared_examples 'issuable notes filter' do expect(user.reload.notes_filter_for(issuable)).to eq(0) end - it 'returns no system note' do + it 'returns only user comments' do user.set_notes_filter(UserPreference::NOTES_FILTERS[:only_comments], issuable) get :discussions, namespace_id: project.namespace, project_id: project, id: issuable.iid + discussions = JSON.parse(response.body) - expect(JSON.parse(response.body).count).to eq(1) + expect(discussions.count).to eq(1) + expect(discussions.first["notes"].first["system"]).to be(false) + end + + it 'returns only activity notes' do + user.set_notes_filter(UserPreference::NOTES_FILTERS[:only_activity], issuable) + + get :discussions, namespace_id: project.namespace, project_id: project, id: issuable.iid + discussions = JSON.parse(response.body) + + expect(discussions.count).to eq(1) + expect(discussions.first["notes"].first["system"]).to be(true) end context 'when filter is set to "only_comments"' do diff --git a/spec/support/shared_examples/helm_generated_script.rb b/spec/support/shared_examples/helm_generated_script.rb index ef9bb7f5533..361d4220c6e 100644 --- a/spec/support/shared_examples/helm_generated_script.rb +++ b/spec/support/shared_examples/helm_generated_script.rb @@ -3,12 +3,6 @@ shared_examples 'helm commands' do let(:helm_setup) do <<~EOS set -eo pipefail - ALPINE_VERSION=$(cat /etc/alpine-release | cut -d '.' -f 1,2) - echo http://mirror.clarkson.edu/alpine/v$ALPINE_VERSION/main >> /etc/apk/repositories - echo http://mirror1.hs-esslingen.de/pub/Mirrors/alpine/v$ALPINE_VERSION/main >> /etc/apk/repositories - apk add -U wget ca-certificates openssl >/dev/null - wget -q -O - https://kubernetes-helm.storage.googleapis.com/helm-v2.7.2-linux-amd64.tar.gz | tar zxC /tmp >/dev/null - mv /tmp/linux-amd64/helm /usr/bin/ EOS end diff --git a/spec/views/projects/tree/_blob_item.html.haml_spec.rb b/spec/views/projects/tree/_tree_row.html.haml_spec.rb index 6a477c712ff..3353b7665e2 100644 --- a/spec/views/projects/tree/_blob_item.html.haml_spec.rb +++ b/spec/views/projects/tree/_tree_row.html.haml_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe 'projects/tree/_blob_item' do +describe 'projects/tree/_tree_row' do let(:project) { create(:project, :repository) } let(:repository) { project.repository } let(:blob_item) { Gitlab::Git::Tree.where(repository, SeedRepo::Commit::ID, 'files/ruby').first } @@ -31,10 +31,7 @@ describe 'projects/tree/_blob_item' do end end - def render_partial(blob_item) - render partial: 'projects/tree/blob_item', locals: { - blob_item: blob_item, - type: 'blob' - } + def render_partial(items) + render partial: 'projects/tree/tree_row', collection: [items].flatten end end |