diff options
author | Phil Hughes <me@iamphill.com> | 2018-04-25 12:51:41 +0300 |
---|---|---|
committer | Phil Hughes <me@iamphill.com> | 2018-04-25 12:51:41 +0300 |
commit | 9990afb92c28c94e89a1c81c8b2f2c02c04dc86b (patch) | |
tree | aecb679c6954c85b435b9fcad5c5199cde34b9b6 | |
parent | 10bba03843c155beb58b9d054029d2083776cc56 (diff) | |
parent | b36941bdaad03eeb5ab23235b77f1bfad0348f18 (diff) |
Merge branch 'master' into ide-temp-file-folder-fixes
384 files changed, 4400 insertions, 1836 deletions
@@ -1,6 +1,9 @@ { "presets": [["latest", { "es2015": { "modules": false } }], "stage-2"], "env": { + "karma": { + "plugins": ["rewire"] + }, "coverage": { "plugins": [ [ @@ -14,7 +17,8 @@ { "process.env.BABEL_ENV": "coverage" } - ] + ], + "rewire" ] } } diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index d443238b9e1..16b0b5c95e2 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -143,7 +143,7 @@ Lint/MissingCopEnableDirective: Lint/NestedPercentLiteral: Exclude: - 'lib/gitlab/git/repository.rb' - - 'spec/support/email_format_shared_examples.rb' + - 'spec/support/shared_examples/email_format_shared_examples.rb' # Offense count: 1 Lint/ReturnInVoidContext: @@ -195,8 +195,8 @@ Naming/HeredocDelimiterCase: - 'spec/lib/gitlab/diff/parser_spec.rb' - 'spec/lib/json_web_token/rsa_token_spec.rb' - 'spec/models/commit_spec.rb' - - 'spec/support/repo_helpers.rb' - - 'spec/support/seed_repo.rb' + - 'spec/support/helpers/repo_helpers.rb' + - 'spec/support/helpers/seed_repo.rb' # Offense count: 112 # Configuration parameters: Blacklist. @@ -496,7 +496,7 @@ Style/EmptyLiteral: - 'spec/lib/gitlab/request_context_spec.rb' - 'spec/lib/gitlab/workhorse_spec.rb' - 'spec/requests/api/jobs_spec.rb' - - 'spec/support/chat_slash_commands_shared_examples.rb' + - 'spec/support/shared_examples/chat_slash_commands_shared_examples.rb' # Offense count: 102 # Cop supports --auto-correct. diff --git a/CHANGELOG.md b/CHANGELOG.md index a423e93d691..8278119cf10 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,34 @@ documentation](doc/development/changelog.md) for instructions on adding your own entry. +## 10.7.1 (2018-04-23) + +### Fixed (11 changes) + +- [API] Fix URLs in the `Link` header for `GET /projects/:id/repository/contributors` when no value is passed for `order_by` or `sort`. !18393 +- Fix a case with secret variables being empty sometimes. !18400 +- Fix `Trace::HttpIO` can not render multi-byte chars. !18417 +- Fix specifying a non-default ref when requesting an archive using the legacy URL. !18468 +- Respect visibility options and description when importing project from template. !18473 +- Removes 'No Job log' message from build trace. !18523 +- Align action icons in pipeline graph. +- Fix direct_upload when records with null file_store are used. +- Removed alert box in IDE when redirecting to new merge request. +- Fixed IDE not loading for sub groups. +- Fixed IDE not showing loading state when tree is loading. + +### Performance (4 changes) + +- Validate project path prior to hitting the database. !18322 +- Add index to file_store on ci_job_artifacts. !18444 +- Fix N+1 queries when loading participants for a commit note. +- Support Markdown rendering using multiple projects. + +### Added (1 change) + +- Add an API endpoint to download git repository snapshots. !18173 + + ## 10.7.0 (2018-04-22) ### Security (6 changes, 2 of them are from the community) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 02599907af7..29009010f0e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -26,7 +26,9 @@ _This notice should stay as the first item in the CONTRIBUTING.md file._ - [Type labels (~"feature proposal", ~bug, ~customer, etc.)](#type-labels-feature-proposal-bug-customer-etc) - [Subject labels (~wiki, ~"container registry", ~ldap, ~api, etc.)](#subject-labels-wiki-container-registry-ldap-api-etc) - [Team labels (~"CI/CD", ~Discussion, ~Edge, ~Platform, etc.)](#team-labels-cicd-discussion-edge-platform-etc) - - [Priority labels (~Deliverable, ~Stretch, ~"Next Patch Release")](#priority-labels-deliverable-stretch-next-patch-release) + - [Milestone labels (~Deliverable, ~Stretch, ~"Next Patch Release")](#milestone-labels-deliverable-stretch-next-patch-release) + - [Priority labels (~Deliverable, ~Stretch, ~"Next Patch Release")](#bug-priority-labels-p1-p2-p3-etc) + - [Severity labels (~Deliverable, ~Stretch, ~"Next Patch Release")](#bug-severity-labels-s1-s2-s3-etc) - [Label for community contributors (~"Accepting Merge Requests")](#label-for-community-contributors-accepting-merge-requests) - [Implement design & UI elements](#implement-design-ui-elements) - [Issue tracker](#issue-tracker) @@ -127,6 +129,8 @@ Most issues will have labels for at least one of the following: - Subject: ~wiki, ~"container registry", ~ldap, ~api, ~frontend, etc. - Team: ~"CI/CD", ~Discussion, ~Edge, ~Platform, etc. - Milestone: ~Deliverable, ~Stretch, ~"Next Patch Release" +- Priority: ~P1, ~P2, ~P3, ~P4 +- Severity: ~S1, ~S2, ~S3, ~S4 All labels, their meaning and priority are defined on the [labels page][labels-page]. @@ -210,7 +214,7 @@ This label documents the planned timeline & urgency which is used to measure aga | Label | Meaning | Estimate time to fix | Guidance | |-------|-----------------|------------------------------------------------------------------|----------| -| ~P1 | Urgent Priority | The current release | | +| ~P1 | Urgent Priority | The current release + potentially immediate hotfix to GitLab.com | | | ~P2 | High Priority | The next release | | | ~P3 | Medium Priority | Within the next 3 releases (approx one quarter) | | | ~P4 | Low Priority | Anything outside the next 3 releases (approx beyond one quarter) | The issue is prominent but does not impact user workflow and a workaround is documented | diff --git a/Gemfile.lock b/Gemfile.lock index 745732f3537..d5e1c428e25 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -178,7 +178,7 @@ GEM docile (1.1.5) domain_name (0.5.20170404) unf (>= 0.0.5, < 1.0.0) - doorkeeper (4.3.1) + doorkeeper (4.3.2) railties (>= 4.2) doorkeeper-openid_connect (1.3.0) doorkeeper (~> 4.3) @@ -483,10 +483,11 @@ GEM logging (2.2.2) little-plugger (~> 1.1) multi_json (~> 1.10) - lograge (0.5.1) - actionpack (>= 4, < 5.2) - activesupport (>= 4, < 5.2) - railties (>= 4, < 5.2) + lograge (0.10.0) + actionpack (>= 4) + activesupport (>= 4) + railties (>= 4) + request_store (~> 1.0) loofah (2.2.2) crass (~> 1.0.2) nokogiri (>= 1.5.9) @@ -1 +1 @@ -10.7.0-pre +10.8.0-pre diff --git a/app/assets/javascripts/ide/components/file_finder/index.vue b/app/assets/javascripts/ide/components/file_finder/index.vue new file mode 100644 index 00000000000..ea2b13a8b21 --- /dev/null +++ b/app/assets/javascripts/ide/components/file_finder/index.vue @@ -0,0 +1,245 @@ +<script> +import { mapActions, mapGetters, mapState } from 'vuex'; +import fuzzaldrinPlus from 'fuzzaldrin-plus'; +import VirtualList from 'vue-virtual-scroll-list'; +import Item from './item.vue'; +import router from '../../ide_router'; +import { + MAX_FILE_FINDER_RESULTS, + FILE_FINDER_ROW_HEIGHT, + FILE_FINDER_EMPTY_ROW_HEIGHT, +} from '../../constants'; +import { + UP_KEY_CODE, + DOWN_KEY_CODE, + ENTER_KEY_CODE, + ESC_KEY_CODE, +} from '../../../lib/utils/keycodes'; + +export default { + components: { + Item, + VirtualList, + }, + data() { + return { + focusedIndex: 0, + searchText: '', + mouseOver: false, + cancelMouseOver: false, + }; + }, + computed: { + ...mapGetters(['allBlobs']), + ...mapState(['fileFindVisible', 'loading']), + filteredBlobs() { + const searchText = this.searchText.trim(); + + if (searchText === '') { + return this.allBlobs.slice(0, MAX_FILE_FINDER_RESULTS); + } + + return fuzzaldrinPlus + .filter(this.allBlobs, searchText, { + key: 'path', + maxResults: MAX_FILE_FINDER_RESULTS, + }) + .sort((a, b) => b.lastOpenedAt - a.lastOpenedAt); + }, + filteredBlobsLength() { + return this.filteredBlobs.length; + }, + listShowCount() { + return this.filteredBlobsLength ? Math.min(this.filteredBlobsLength, 5) : 1; + }, + listHeight() { + return this.filteredBlobsLength ? FILE_FINDER_ROW_HEIGHT : FILE_FINDER_EMPTY_ROW_HEIGHT; + }, + showClearInputButton() { + return this.searchText.trim() !== ''; + }, + }, + watch: { + fileFindVisible() { + this.$nextTick(() => { + if (!this.fileFindVisible) { + this.searchText = ''; + } else { + this.focusedIndex = 0; + + if (this.$refs.searchInput) { + this.$refs.searchInput.focus(); + } + } + }); + }, + searchText() { + this.focusedIndex = 0; + }, + focusedIndex() { + if (!this.mouseOver) { + this.$nextTick(() => { + const el = this.$refs.virtualScrollList.$el; + const scrollTop = this.focusedIndex * FILE_FINDER_ROW_HEIGHT; + const bottom = this.listShowCount * FILE_FINDER_ROW_HEIGHT; + + if (this.focusedIndex === 0) { + // if index is the first index, scroll straight to start + el.scrollTop = 0; + } else if (this.focusedIndex === this.filteredBlobsLength - 1) { + // if index is the last index, scroll to the end + el.scrollTop = this.filteredBlobsLength * FILE_FINDER_ROW_HEIGHT; + } else if (scrollTop >= bottom + el.scrollTop) { + // if element is off the bottom of the scroll list, scroll down one item + el.scrollTop = scrollTop - bottom + FILE_FINDER_ROW_HEIGHT; + } else if (scrollTop < el.scrollTop) { + // if element is off the top of the scroll list, scroll up one item + el.scrollTop = scrollTop; + } + }); + } + }, + }, + methods: { + ...mapActions(['toggleFileFinder']), + clearSearchInput() { + this.searchText = ''; + + this.$nextTick(() => { + this.$refs.searchInput.focus(); + }); + }, + onKeydown(e) { + switch (e.keyCode) { + case UP_KEY_CODE: + e.preventDefault(); + this.mouseOver = false; + this.cancelMouseOver = true; + if (this.focusedIndex > 0) { + this.focusedIndex -= 1; + } else { + this.focusedIndex = this.filteredBlobsLength - 1; + } + break; + case DOWN_KEY_CODE: + e.preventDefault(); + this.mouseOver = false; + this.cancelMouseOver = true; + if (this.focusedIndex < this.filteredBlobsLength - 1) { + this.focusedIndex += 1; + } else { + this.focusedIndex = 0; + } + break; + default: + break; + } + }, + onKeyup(e) { + switch (e.keyCode) { + case ENTER_KEY_CODE: + this.openFile(this.filteredBlobs[this.focusedIndex]); + break; + case ESC_KEY_CODE: + this.toggleFileFinder(false); + break; + default: + break; + } + }, + openFile(file) { + this.toggleFileFinder(false); + router.push(`/project${file.url}`); + }, + onMouseOver(index) { + if (!this.cancelMouseOver) { + this.mouseOver = true; + this.focusedIndex = index; + } + }, + onMouseMove(index) { + this.cancelMouseOver = false; + this.onMouseOver(index); + }, + }, +}; +</script> + +<template> + <div + class="ide-file-finder-overlay" + @mousedown.self="toggleFileFinder(false)" + > + <div + class="dropdown-menu diff-file-changes ide-file-finder show" + > + <div class="dropdown-input"> + <input + type="search" + class="dropdown-input-field" + :placeholder="__('Search files')" + autocomplete="off" + v-model="searchText" + ref="searchInput" + @keydown="onKeydown($event)" + @keyup="onKeyup($event)" + /> + <i + aria-hidden="true" + class="fa fa-search dropdown-input-search" + :class="{ + hidden: showClearInputButton + }" + ></i> + <i + role="button" + :aria-label="__('Clear search input')" + class="fa fa-times dropdown-input-clear" + :class="{ + show: showClearInputButton + }" + @click="clearSearchInput" + ></i> + </div> + <div> + <virtual-list + :size="listHeight" + :remain="listShowCount" + wtag="ul" + ref="virtualScrollList" + > + <template v-if="filteredBlobsLength"> + <li + v-for="(file, index) in filteredBlobs" + :key="file.key" + > + <item + class="disable-hover" + :file="file" + :search-text="searchText" + :focused="index === focusedIndex" + :index="index" + @click="openFile" + @mouseover="onMouseOver" + @mousemove="onMouseMove" + /> + </li> + </template> + <li + v-else + class="dropdown-menu-empty-item" + > + <div class="append-right-default prepend-left-default prepend-top-8 append-bottom-8"> + <template v-if="loading"> + {{ __('Loading...') }} + </template> + <template v-else> + {{ __('No files found.') }} + </template> + </div> + </li> + </virtual-list> + </div> + </div> + </div> +</template> diff --git a/app/assets/javascripts/ide/components/file_finder/item.vue b/app/assets/javascripts/ide/components/file_finder/item.vue new file mode 100644 index 00000000000..d4427420207 --- /dev/null +++ b/app/assets/javascripts/ide/components/file_finder/item.vue @@ -0,0 +1,113 @@ +<script> +import fuzzaldrinPlus from 'fuzzaldrin-plus'; +import FileIcon from '../../../vue_shared/components/file_icon.vue'; +import ChangedFileIcon from '../changed_file_icon.vue'; + +const MAX_PATH_LENGTH = 60; + +export default { + components: { + ChangedFileIcon, + FileIcon, + }, + props: { + file: { + type: Object, + required: true, + }, + focused: { + type: Boolean, + required: true, + }, + searchText: { + type: String, + required: true, + }, + index: { + type: Number, + required: true, + }, + }, + computed: { + pathWithEllipsis() { + const path = this.file.path; + + return path.length < MAX_PATH_LENGTH + ? path + : `...${path.substr(path.length - MAX_PATH_LENGTH)}`; + }, + nameSearchTextOccurences() { + return fuzzaldrinPlus.match(this.file.name, this.searchText); + }, + pathSearchTextOccurences() { + return fuzzaldrinPlus.match(this.pathWithEllipsis, this.searchText); + }, + }, + methods: { + clickRow() { + this.$emit('click', this.file); + }, + mouseOverRow() { + this.$emit('mouseover', this.index); + }, + mouseMove() { + this.$emit('mousemove', this.index); + }, + }, +}; +</script> + +<template> + <button + type="button" + class="diff-changed-file" + :class="{ + 'is-focused': focused, + }" + @click.prevent="clickRow" + @mouseover="mouseOverRow" + @mousemove="mouseMove" + > + <file-icon + :file-name="file.name" + :size="16" + css-classes="diff-file-changed-icon append-right-8" + /> + <span class="diff-changed-file-content append-right-8"> + <strong + class="diff-changed-file-name" + > + <span + v-for="(char, index) in file.name.split('')" + :key="index + char" + :class="{ + highlighted: nameSearchTextOccurences.indexOf(index) >= 0, + }" + v-text="char" + > + </span> + </strong> + <span + class="diff-changed-file-path prepend-top-5" + > + <span + v-for="(char, index) in pathWithEllipsis.split('')" + :key="index + char" + :class="{ + highlighted: pathSearchTextOccurences.indexOf(index) >= 0, + }" + v-text="char" + > + </span> + </span> + </span> + <span + v-if="file.changed || file.tempFile" + class="diff-changed-stats" + > + <changed-file-icon + :file="file" + /> + </span> + </button> +</template> diff --git a/app/assets/javascripts/ide/components/ide.vue b/app/assets/javascripts/ide/components/ide.vue index 1c237c0ec97..0274fc7d299 100644 --- a/app/assets/javascripts/ide/components/ide.vue +++ b/app/assets/javascripts/ide/components/ide.vue @@ -1,55 +1,91 @@ <script> -import { mapState, mapGetters } from 'vuex'; -import ideSidebar from './ide_side_bar.vue'; -import ideContextbar from './ide_context_bar.vue'; -import repoTabs from './repo_tabs.vue'; -import ideStatusBar from './ide_status_bar.vue'; -import repoEditor from './repo_editor.vue'; + import { mapActions, mapState, mapGetters } from 'vuex'; + import Mousetrap from 'mousetrap'; + import ideSidebar from './ide_side_bar.vue'; + import ideContextbar from './ide_context_bar.vue'; + import repoTabs from './repo_tabs.vue'; + import ideStatusBar from './ide_status_bar.vue'; + import repoEditor from './repo_editor.vue'; + import FindFile from './file_finder/index.vue'; -export default { - components: { - ideSidebar, - ideContextbar, - repoTabs, - ideStatusBar, - repoEditor, - }, - props: { - emptyStateSvgPath: { - type: String, - required: true, + const originalStopCallback = Mousetrap.stopCallback; + + export default { + components: { + ideSidebar, + ideContextbar, + repoTabs, + ideStatusBar, + repoEditor, + FindFile, }, - noChangesStateSvgPath: { - type: String, - required: true, + props: { + emptyStateSvgPath: { + type: String, + required: true, + }, + noChangesStateSvgPath: { + type: String, + required: true, + }, + committedStateSvgPath: { + type: String, + required: true, + }, }, - committedStateSvgPath: { - type: String, - required: true, + computed: { + ...mapState([ + 'changedFiles', + 'openFiles', + 'viewer', + 'currentMergeRequestId', + 'fileFindVisible', + ]), + ...mapGetters(['activeFile', 'hasChanges']), }, - }, - computed: { - ...mapState(['changedFiles', 'openFiles', 'viewer', 'currentMergeRequestId']), - ...mapGetters(['activeFile', 'hasChanges']), - }, - mounted() { - const returnValue = 'Are you sure you want to lose unsaved changes?'; - window.onbeforeunload = e => { - if (!this.changedFiles.length) return undefined; + mounted() { + const returnValue = 'Are you sure you want to lose unsaved changes?'; + window.onbeforeunload = e => { + if (!this.changedFiles.length) return undefined; + + Object.assign(e, { + returnValue, + }); + return returnValue; + }; - Object.assign(e, { - returnValue, + Mousetrap.bind(['t', 'command+p', 'ctrl+p'], e => { + if (e.preventDefault) { + e.preventDefault(); + } + + this.toggleFileFinder(!this.fileFindVisible); }); - return returnValue; - }; - }, -}; + + Mousetrap.stopCallback = (e, el, combo) => this.mousetrapStopCallback(e, el, combo); + }, + methods: { + ...mapActions(['toggleFileFinder']), + mousetrapStopCallback(e, el, combo) { + if (combo === 't' && el.classList.contains('dropdown-input-field')) { + return true; + } else if (combo === 'command+p' || combo === 'ctrl+p') { + return false; + } + + return originalStopCallback(e, el, combo); + }, + }, + }; </script> <template> <div class="ide-view" > + <find-file + v-show="fileFindVisible" + /> <ide-sidebar /> <div class="multi-file-edit-pane" diff --git a/app/assets/javascripts/ide/constants.js b/app/assets/javascripts/ide/constants.js index b60d042e0be..b06da9f95d1 100644 --- a/app/assets/javascripts/ide/constants.js +++ b/app/assets/javascripts/ide/constants.js @@ -1,3 +1,8 @@ // Fuzzy file finder +export const MAX_FILE_FINDER_RESULTS = 40; +export const FILE_FINDER_ROW_HEIGHT = 55; +export const FILE_FINDER_EMPTY_ROW_HEIGHT = 33; + +// Commit message textarea export const MAX_TITLE_LENGTH = 50; export const MAX_BODY_LENGTH = 72; diff --git a/app/assets/javascripts/ide/lib/editor.js b/app/assets/javascripts/ide/lib/editor.js index 2d3ee7d4f48..b65d9c68a0b 100644 --- a/app/assets/javascripts/ide/lib/editor.js +++ b/app/assets/javascripts/ide/lib/editor.js @@ -1,10 +1,12 @@ import _ from 'underscore'; +import store from '../stores'; import DecorationsController from './decorations/controller'; import DirtyDiffController from './diff/controller'; import Disposable from './common/disposable'; import ModelManager from './common/model_manager'; import editorOptions, { defaultEditorOptions } from './editor_options'; import gitlabTheme from './themes/gl_theme'; +import keymap from './keymap.json'; export const clearDomElement = el => { if (!el || !el.firstChild) return; @@ -53,6 +55,8 @@ export default class Editor { )), ); + this.addCommands(); + window.addEventListener('resize', this.debouncedUpdate, false); } } @@ -73,6 +77,8 @@ export default class Editor { })), ); + this.addCommands(); + window.addEventListener('resize', this.debouncedUpdate, false); } } @@ -189,4 +195,31 @@ export default class Editor { static renderSideBySide(domElement) { return domElement.offsetWidth >= 700; } + + addCommands() { + const getKeyCode = key => { + const monacoKeyMod = key.indexOf('KEY_') === 0; + + return monacoKeyMod ? this.monaco.KeyCode[key] : this.monaco.KeyMod[key]; + }; + + keymap.forEach(command => { + const keybindings = command.bindings.map(binding => { + const keys = binding.split('+'); + + // eslint-disable-next-line no-bitwise + return keys.length > 1 ? getKeyCode(keys[0]) | getKeyCode(keys[1]) : getKeyCode(keys[0]); + }); + + this.instance.addAction({ + id: command.id, + label: command.label, + keybindings, + run() { + store.dispatch(command.action.name, command.action.params); + return null; + }, + }); + }); + } } diff --git a/app/assets/javascripts/ide/lib/keymap.json b/app/assets/javascripts/ide/lib/keymap.json new file mode 100644 index 00000000000..131abfebbed --- /dev/null +++ b/app/assets/javascripts/ide/lib/keymap.json @@ -0,0 +1,11 @@ +[ + { + "id": "file-finder", + "label": "File finder", + "bindings": ["CtrlCmd+KEY_P"], + "action": { + "name": "toggleFileFinder", + "params": true + } + } +] diff --git a/app/assets/javascripts/ide/stores/actions.js b/app/assets/javascripts/ide/stores/actions.js index 99e3c8f6794..4c8c997e376 100644 --- a/app/assets/javascripts/ide/stores/actions.js +++ b/app/assets/javascripts/ide/stores/actions.js @@ -146,7 +146,13 @@ export const updateTempFlagForEntry = ({ commit, dispatch, state }, { file, temp } }; +export const toggleFileFinder = ({ commit }, fileFindVisible) => + commit(types.TOGGLE_FILE_FINDER, fileFindVisible); + export * from './actions/tree'; export * from './actions/file'; export * from './actions/project'; export * from './actions/merge_request'; + +// prevent babel-plugin-rewire from generating an invalid default during karma tests +export default () => {}; diff --git a/app/assets/javascripts/ide/stores/getters.js b/app/assets/javascripts/ide/stores/getters.js index 8518d2f6f06..ec1ea155aee 100644 --- a/app/assets/javascripts/ide/stores/getters.js +++ b/app/assets/javascripts/ide/stores/getters.js @@ -42,4 +42,20 @@ export const collapseButtonTooltip = state => export const hasMergeRequest = state => !!state.currentMergeRequestId; +export const allBlobs = state => + Object.keys(state.entries) + .reduce((acc, key) => { + const entry = state.entries[key]; + + if (entry.type === 'blob') { + acc.push(entry); + } + + return acc; + }, []) + .sort((a, b) => b.lastOpenedAt - a.lastOpenedAt); + export const getStagedFile = state => path => state.stagedFiles.find(f => f.path === path); + +// prevent babel-plugin-rewire from generating an invalid default during karma tests +export default () => {}; diff --git a/app/assets/javascripts/ide/stores/modules/commit/actions.js b/app/assets/javascripts/ide/stores/modules/commit/actions.js index e7ac60d01cf..349ff68f1e3 100644 --- a/app/assets/javascripts/ide/stores/modules/commit/actions.js +++ b/app/assets/javascripts/ide/stores/modules/commit/actions.js @@ -196,3 +196,6 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState }) = commit(types.UPDATE_LOADING, false); }); }; + +// prevent babel-plugin-rewire from generating an invalid default during karma tests +export default () => {}; diff --git a/app/assets/javascripts/ide/stores/modules/commit/getters.js b/app/assets/javascripts/ide/stores/modules/commit/getters.js index 9c3905a0b0d..d01060201f2 100644 --- a/app/assets/javascripts/ide/stores/modules/commit/getters.js +++ b/app/assets/javascripts/ide/stores/modules/commit/getters.js @@ -27,3 +27,6 @@ export const branchName = (state, getters, rootState) => { return rootState.currentBranchId; }; + +// prevent babel-plugin-rewire from generating an invalid default during karma tests +export default () => {}; diff --git a/app/assets/javascripts/ide/stores/mutation_types.js b/app/assets/javascripts/ide/stores/mutation_types.js index 40cd3aaf010..f5c12db6db0 100644 --- a/app/assets/javascripts/ide/stores/mutation_types.js +++ b/app/assets/javascripts/ide/stores/mutation_types.js @@ -60,3 +60,4 @@ export const ADD_PENDING_TAB = 'ADD_PENDING_TAB'; export const REMOVE_PENDING_TAB = 'REMOVE_PENDING_TAB'; export const UPDATE_TEMP_FLAG = 'UPDATE_TEMP_FLAG'; +export const TOGGLE_FILE_FINDER = 'TOGGLE_FILE_FINDER'; diff --git a/app/assets/javascripts/ide/stores/mutations.js b/app/assets/javascripts/ide/stores/mutations.js index 7ea113f03bf..0c1d720df09 100644 --- a/app/assets/javascripts/ide/stores/mutations.js +++ b/app/assets/javascripts/ide/stores/mutations.js @@ -107,6 +107,11 @@ export default { delayViewerUpdated, }); }, + [types.TOGGLE_FILE_FINDER](state, fileFindVisible) { + Object.assign(state, { + fileFindVisible, + }); + }, [types.UPDATE_FILE_AFTER_COMMIT](state, { file, lastCommit }) { const changedFile = state.changedFiles.find(f => f.path === file.path); diff --git a/app/assets/javascripts/ide/stores/mutations/file.js b/app/assets/javascripts/ide/stores/mutations/file.js index dd7dcba8ac7..c3041c77199 100644 --- a/app/assets/javascripts/ide/stores/mutations/file.js +++ b/app/assets/javascripts/ide/stores/mutations/file.js @@ -4,6 +4,7 @@ export default { [types.SET_FILE_ACTIVE](state, { path, active }) { Object.assign(state.entries[path], { active, + lastOpenedAt: new Date().getTime(), }); if (active && !state.entries[path].pending) { diff --git a/app/assets/javascripts/ide/stores/state.js b/app/assets/javascripts/ide/stores/state.js index 34975ac3144..3470bb9aec0 100644 --- a/app/assets/javascripts/ide/stores/state.js +++ b/app/assets/javascripts/ide/stores/state.js @@ -18,4 +18,5 @@ export default () => ({ entries: {}, viewer: 'editor', delayViewerUpdated: false, + fileFindVisible: false, }); diff --git a/app/assets/javascripts/ide/stores/utils.js b/app/assets/javascripts/ide/stores/utils.js index 1fd0d18ff51..59185f8f0ad 100644 --- a/app/assets/javascripts/ide/stores/utils.js +++ b/app/assets/javascripts/ide/stores/utils.js @@ -43,6 +43,7 @@ export const dataStructure = () => ({ viewMode: 'edit', previewMode: null, size: 0, + lastOpenedAt: 0, }); export const decorateData = entity => { diff --git a/app/assets/javascripts/jobs/components/sidebar_details_block.vue b/app/assets/javascripts/jobs/components/sidebar_details_block.vue index 4cd44bf7a76..db19dc9b238 100644 --- a/app/assets/javascripts/jobs/components/sidebar_details_block.vue +++ b/app/assets/javascripts/jobs/components/sidebar_details_block.vue @@ -45,7 +45,7 @@ export default { return timeIntervalInWords(this.job.queued); }, runnerId() { - return `#${this.job.runner.id}`; + return `${this.job.runner.description} (#${this.job.runner.id})`; }, retryButtonClass() { let className = 'js-retry-button pull-right btn btn-retry visible-md-block visible-lg-block'; diff --git a/app/assets/javascripts/lib/utils/keycodes.js b/app/assets/javascripts/lib/utils/keycodes.js new file mode 100644 index 00000000000..5e0f9b612a2 --- /dev/null +++ b/app/assets/javascripts/lib/utils/keycodes.js @@ -0,0 +1,4 @@ +export const UP_KEY_CODE = 38; +export const DOWN_KEY_CODE = 40; +export const ENTER_KEY_CODE = 13; +export const ESC_KEY_CODE = 27; diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js index 244a6980b5a..98ce070288e 100644 --- a/app/assets/javascripts/notes/stores/actions.js +++ b/app/assets/javascripts/notes/stores/actions.js @@ -315,3 +315,6 @@ export const scrollToNoteIfNeeded = (context, el) => { scrollToElement(el); } }; + +// 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 f89591a54d6..787be6f4c99 100644 --- a/app/assets/javascripts/notes/stores/getters.js +++ b/app/assets/javascripts/notes/stores/getters.js @@ -68,3 +68,6 @@ export const resolvedDiscussionCount = (state, getters) => { return Object.keys(resolvedMap).length; }; + +// prevent babel-plugin-rewire from generating an invalid default during karma tests +export default () => {}; diff --git a/app/assets/javascripts/pipelines/components/graph/action_component.vue b/app/assets/javascripts/pipelines/components/graph/action_component.vue index e99d949801f..29ee73a2a6f 100644 --- a/app/assets/javascripts/pipelines/components/graph/action_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/action_component.vue @@ -32,26 +32,38 @@ export default { required: true, }, - buttonDisabled: { + requestFinishedFor: { type: String, required: false, - default: null, + default: '', }, }, + data() { + return { + isDisabled: false, + linkRequested: '', + }; + }, + computed: { cssClass() { const actionIconDash = dasherize(this.actionIcon); return `${actionIconDash} js-icon-${actionIconDash}`; }, - isDisabled() { - return this.buttonDisabled === this.link; + }, + watch: { + requestFinishedFor() { + if (this.requestFinishedFor === this.linkRequested) { + this.isDisabled = false; + } }, }, - methods: { onClickAction() { $(this.$el).tooltip('hide'); eventHub.$emit('graphAction', this.link); + this.linkRequested = this.link; + this.isDisabled = true; }, }, }; @@ -62,7 +74,8 @@ export default { @click="onClickAction" v-tooltip :title="tooltipText" - class="btn btn-blank btn-transparent ci-action-icon-container ci-action-icon-wrapper" + class="js-ci-action btn btn-blank +btn-transparent ci-action-icon-container ci-action-icon-wrapper" :class="cssClass" data-container="body" :disabled="isDisabled" diff --git a/app/assets/javascripts/pipelines/components/graph/dropdown_action_component.vue b/app/assets/javascripts/pipelines/components/graph/dropdown_action_component.vue deleted file mode 100644 index 7c4fd65e36f..00000000000 --- a/app/assets/javascripts/pipelines/components/graph/dropdown_action_component.vue +++ /dev/null @@ -1,53 +0,0 @@ -<script> - import icon from '../../../vue_shared/components/icon.vue'; - import tooltip from '../../../vue_shared/directives/tooltip'; - - /** - * Renders either a cancel, retry or play icon pointing to the given path. - * TODO: Remove UJS from here and use an async request instead. - */ - export default { - components: { - icon, - }, - - directives: { - tooltip, - }, - props: { - tooltipText: { - type: String, - required: true, - }, - - link: { - type: String, - required: true, - }, - - actionMethod: { - type: String, - required: true, - }, - - actionIcon: { - type: String, - required: true, - }, - }, - }; -</script> -<template> - <a - v-tooltip - :data-method="actionMethod" - :title="tooltipText" - :href="link" - rel="nofollow" - class="ci-action-icon-wrapper js-ci-status-icon" - data-container="body" - aria-label="Job's action" - > - <icon :name="actionIcon" /> - </a> -</template> diff --git a/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue b/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue index be213c2ee78..43121dd38f3 100644 --- a/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue @@ -1,77 +1,83 @@ <script> - import $ from 'jquery'; - import jobNameComponent from './job_name_component.vue'; - import jobComponent from './job_component.vue'; - import tooltip from '../../../vue_shared/directives/tooltip'; +import $ from 'jquery'; +import JobNameComponent from './job_name_component.vue'; +import JobComponent from './job_component.vue'; +import tooltip from '../../../vue_shared/directives/tooltip'; - /** - * Renders the dropdown for the pipeline graph. - * - * The following object should be provided as `job`: - * - * { - * "id": 4256, - * "name": "test", - * "status": { - * "icon": "icon_status_success", - * "text": "passed", - * "label": "passed", - * "group": "success", - * "details_path": "/root/ci-mock/builds/4256", - * "action": { - * "icon": "retry", - * "title": "Retry", - * "path": "/root/ci-mock/builds/4256/retry", - * "method": "post" - * } - * } - * } - */ - export default { - directives: { - tooltip, - }, +/** + * Renders the dropdown for the pipeline graph. + * + * The following object should be provided as `job`: + * + * { + * "id": 4256, + * "name": "test", + * "status": { + * "icon": "icon_status_success", + * "text": "passed", + * "label": "passed", + * "group": "success", + * "details_path": "/root/ci-mock/builds/4256", + * "action": { + * "icon": "retry", + * "title": "Retry", + * "path": "/root/ci-mock/builds/4256/retry", + * "method": "post" + * } + * } + * } + */ +export default { + directives: { + tooltip, + }, - components: { - jobComponent, - jobNameComponent, - }, + components: { + JobComponent, + JobNameComponent, + }, - props: { - job: { - type: Object, - required: true, - }, + props: { + job: { + type: Object, + required: true, }, - - computed: { - tooltipText() { - return `${this.job.name} - ${this.job.status.label}`; - }, + requestFinishedFor: { + type: String, + required: false, + default: '', }, + }, - mounted() { - this.stopDropdownClickPropagation(); + computed: { + tooltipText() { + return `${this.job.name} - ${this.job.status.label}`; }, + }, + + mounted() { + this.stopDropdownClickPropagation(); + }, - methods: { - /** - * When the user right clicks or cmd/ctrl + click in the job name - * the dropdown should not be closed and the link should open in another tab, - * so we stop propagation of the click event inside the dropdown. + methods: { + /** + * When the user right clicks or cmd/ctrl + click in the job name or the action icon + * the dropdown should not be closed so we stop propagation + * of the click event inside the dropdown. * * Since this component is rendered multiple times per page we need to guarantee we only * target the click event of this component. */ - stopDropdownClickPropagation() { - $(this.$el - .querySelectorAll('.js-grouped-pipeline-dropdown a.mini-pipeline-graph-dropdown-item')) - .on('click', (e) => { - e.stopPropagation(); - }); - }, + stopDropdownClickPropagation() { + $( + '.js-grouped-pipeline-dropdown button, .js-grouped-pipeline-dropdown a.mini-pipeline-graph-dropdown-item', + this.$el, + ).on('click', e => { + e.stopPropagation(); + }); }, - }; + }, +}; </script> <template> <div class="ci-job-dropdown-container"> @@ -101,8 +107,8 @@ :key="i"> <job-component :job="item" - :is-dropdown="true" css-class-job-name="mini-pipeline-graph-dropdown-item" + :request-finished-for="requestFinishedFor" /> </li> </ul> diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component.vue b/app/assets/javascripts/pipelines/components/graph/graph_component.vue index ac9ce7e47d6..7b8a5edcbff 100644 --- a/app/assets/javascripts/pipelines/components/graph/graph_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/graph_component.vue @@ -7,7 +7,6 @@ export default { StageColumnComponent, LoadingIcon, }, - props: { isLoading: { type: Boolean, @@ -17,10 +16,10 @@ export default { type: Object, required: true, }, - actionDisabled: { + requestFinishedFor: { type: String, required: false, - default: null, + default: '', }, }, @@ -75,7 +74,7 @@ export default { :key="stage.name" :stage-connector-class="stageConnectorClass(index, stage)" :is-first-column="isFirstColumn(index)" - :action-disabled="actionDisabled" + :request-finished-for="requestFinishedFor" /> </ul> </div> diff --git a/app/assets/javascripts/pipelines/components/graph/job_component.vue b/app/assets/javascripts/pipelines/components/graph/job_component.vue index c6e5ae6df41..4fcd4b79f4a 100644 --- a/app/assets/javascripts/pipelines/components/graph/job_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/job_component.vue @@ -1,6 +1,5 @@ <script> import ActionComponent from './action_component.vue'; -import DropdownActionComponent from './dropdown_action_component.vue'; import JobNameComponent from './job_name_component.vue'; import tooltip from '../../../vue_shared/directives/tooltip'; @@ -32,10 +31,8 @@ import tooltip from '../../../vue_shared/directives/tooltip'; export default { components: { ActionComponent, - DropdownActionComponent, JobNameComponent, }, - directives: { tooltip, }, @@ -44,26 +41,17 @@ export default { type: Object, required: true, }, - cssClassJobName: { type: String, required: false, default: '', }, - - isDropdown: { - type: Boolean, - required: false, - default: false, - }, - - actionDisabled: { + requestFinishedFor: { type: String, required: false, - default: null, + default: '', }, }, - computed: { status() { return this.job && this.job.status ? this.job.status : {}; @@ -134,19 +122,11 @@ export default { </div> <action-component - v-if="hasAction && !isDropdown" - :tooltip-text="status.action.title" - :link="status.action.path" - :action-icon="status.action.icon" - :button-disabled="actionDisabled" - /> - - <dropdown-action-component - v-if="hasAction && isDropdown" + v-if="hasAction" :tooltip-text="status.action.title" :link="status.action.path" :action-icon="status.action.icon" - :action-method="status.action.method" + :request-finished-for="requestFinishedFor" /> </div> </template> diff --git a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue index f6e6569e15b..5461fdbbadd 100644 --- a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue @@ -29,10 +29,11 @@ export default { required: false, default: '', }, - actionDisabled: { + + requestFinishedFor: { type: String, required: false, - default: null, + default: '', }, }, @@ -74,12 +75,12 @@ export default { v-if="job.size === 1" :job="job" css-class-job-name="build-content" - :action-disabled="actionDisabled" /> <dropdown-job-component v-if="job.size > 1" :job="job" + :request-finished-for="requestFinishedFor" /> </li> diff --git a/app/assets/javascripts/pipelines/pipeline_details_bundle.js b/app/assets/javascripts/pipelines/pipeline_details_bundle.js index 900eb7855f4..6584f96130b 100644 --- a/app/assets/javascripts/pipelines/pipeline_details_bundle.js +++ b/app/assets/javascripts/pipelines/pipeline_details_bundle.js @@ -25,7 +25,7 @@ export default () => { data() { return { mediator, - actionDisabled: null, + requestFinishedFor: null, }; }, created() { @@ -36,15 +36,17 @@ export default () => { }, methods: { postAction(action) { - this.actionDisabled = action; + // Click was made, reset this variable + this.requestFinishedFor = null; - this.mediator.service.postAction(action) + this.mediator.service + .postAction(action) .then(() => { this.mediator.refreshPipeline(); - this.actionDisabled = null; + this.requestFinishedFor = action; }) .catch(() => { - this.actionDisabled = null; + this.requestFinishedFor = action; Flash(__('An error occurred while making the request.')); }); }, @@ -54,7 +56,7 @@ export default () => { props: { isLoading: this.mediator.state.isLoading, pipeline: this.mediator.store.state.pipeline, - actionDisabled: this.actionDisabled, + requestFinishedFor: this.requestFinishedFor, }, }); }, @@ -79,7 +81,8 @@ export default () => { }, methods: { postAction(action) { - this.mediator.service.postAction(action.path) + this.mediator.service + .postAction(action.path) .then(() => this.mediator.refreshPipeline()) .catch(() => Flash(__('An error occurred while making the request.'))); }, diff --git a/app/assets/javascripts/registry/stores/actions.js b/app/assets/javascripts/registry/stores/actions.js index 795b39bb3dc..593a43c7cc1 100644 --- a/app/assets/javascripts/registry/stores/actions.js +++ b/app/assets/javascripts/registry/stores/actions.js @@ -35,3 +35,6 @@ export const deleteRegistry = ({ commit }, image) => Vue.http.delete(image.destr export const setMainEndpoint = ({ commit }, data) => commit(types.SET_MAIN_ENDPOINT, data); export const toggleLoading = ({ commit }) => commit(types.TOGGLE_MAIN_LOADING); + +// prevent babel-plugin-rewire from generating an invalid default during karma tests +export default () => {}; diff --git a/app/assets/javascripts/registry/stores/getters.js b/app/assets/javascripts/registry/stores/getters.js index 588f479c492..f4923512578 100644 --- a/app/assets/javascripts/registry/stores/getters.js +++ b/app/assets/javascripts/registry/stores/getters.js @@ -1,2 +1,5 @@ export const isLoading = state => state.isLoading; export const repos = state => state.repos; + +// prevent babel-plugin-rewire from generating an invalid default during karma tests +export default () => {}; diff --git a/app/assets/javascripts/vue_shared/components/ci_badge_link.vue b/app/assets/javascripts/vue_shared/components/ci_badge_link.vue index 5324d5dc797..0d64efcbf68 100644 --- a/app/assets/javascripts/vue_shared/components/ci_badge_link.vue +++ b/app/assets/javascripts/vue_shared/components/ci_badge_link.vue @@ -1,52 +1,52 @@ <script> - import ciIcon from './ci_icon.vue'; - import tooltip from '../directives/tooltip'; - /** - * Renders CI Badge link with CI icon and status text based on - * API response shared between all places where it is used. - * - * Receives status object containing: - * status: { - * details_path: "/gitlab-org/gitlab-ce/pipelines/8150156" // url - * group:"running" // used for CSS class - * icon: "icon_status_running" // used to render the icon - * label:"running" // used for potential tooltip - * text:"running" // text rendered - * } - * - * Used in: - * - Pipelines table - first column - * - Jobs table - first column - * - Pipeline show view - header - * - Job show view - header - * - MR widget - */ +import CiIcon from './ci_icon.vue'; +import tooltip from '../directives/tooltip'; +/** + * Renders CI Badge link with CI icon and status text based on + * API response shared between all places where it is used. + * + * Receives status object containing: + * status: { + * details_path: "/gitlab-org/gitlab-ce/pipelines/8150156" // url + * group:"running" // used for CSS class + * icon: "icon_status_running" // used to render the icon + * label:"running" // used for potential tooltip + * text:"running" // text rendered + * } + * + * Used in: + * - Pipelines table - first column + * - Jobs table - first column + * - Pipeline show view - header + * - Job show view - header + * - MR widget + */ - export default { - components: { - ciIcon, +export default { + components: { + CiIcon, + }, + directives: { + tooltip, + }, + props: { + status: { + type: Object, + required: true, }, - directives: { - tooltip, + showText: { + type: Boolean, + required: false, + default: true, }, - props: { - status: { - type: Object, - required: true, - }, - showText: { - type: Boolean, - required: false, - default: true, - }, + }, + computed: { + cssClass() { + const className = this.status.group; + return className ? `ci-status ci-${className}` : 'ci-status'; }, - computed: { - cssClass() { - const className = this.status.group; - return className ? `ci-status ci-${className}` : 'ci-status'; - }, - }, - }; + }, +}; </script> <template> <a diff --git a/app/assets/javascripts/vue_shared/components/ci_icon.vue b/app/assets/javascripts/vue_shared/components/ci_icon.vue index 8fea746f4de..fcab8f571dd 100644 --- a/app/assets/javascripts/vue_shared/components/ci_icon.vue +++ b/app/assets/javascripts/vue_shared/components/ci_icon.vue @@ -1,45 +1,44 @@ <script> - import icon from '../../vue_shared/components/icon.vue'; +import Icon from '../../vue_shared/components/icon.vue'; - /** - * Renders CI icon based on API response shared between all places where it is used. - * - * Receives status object containing: - * status: { - * details_path: "/gitlab-org/gitlab-ce/pipelines/8150156" // url - * group:"running" // used for CSS class - * icon: "icon_status_running" // used to render the icon - * label:"running" // used for potential tooltip - * text:"running" // text rendered - * } - * - * Used in: - * - Pipelines table Badge - * - Pipelines table mini graph - * - Pipeline graph - * - Pipeline show view badge - * - Jobs table - * - Jobs show view header - * - Jobs show view sidebar - */ - export default { - components: { - icon, +/** + * Renders CI icon based on API response shared between all places where it is used. + * + * Receives status object containing: + * status: { + * details_path: "/gitlab-org/gitlab-ce/pipelines/8150156" // url + * group:"running" // used for CSS class + * icon: "icon_status_running" // used to render the icon + * label:"running" // used for potential tooltip + * text:"running" // text rendered + * } + * + * Used in: + * - Pipelines table Badge + * - Pipelines table mini graph + * - Pipeline graph + * - Pipeline show view badge + * - Jobs table + * - Jobs show view header + * - Jobs show view sidebar + */ +export default { + components: { + Icon, + }, + props: { + status: { + type: Object, + required: true, }, - props: { - status: { - type: Object, - required: true, - }, + }, + computed: { + cssClass() { + const status = this.status.group; + return `ci-status-icon ci-status-icon-${status} js-ci-status-icon-${status}`; }, - - computed: { - cssClass() { - const status = this.status.group; - return `ci-status-icon ci-status-icon-${status} js-ci-status-icon-${status}`; - }, - }, - }; + }, +}; </script> <template> <span :class="cssClass"> diff --git a/app/assets/javascripts/vue_shared/components/clipboard_button.vue b/app/assets/javascripts/vue_shared/components/clipboard_button.vue index cab126a7eca..cb2cc3901ad 100644 --- a/app/assets/javascripts/vue_shared/components/clipboard_button.vue +++ b/app/assets/javascripts/vue_shared/components/clipboard_button.vue @@ -1,40 +1,50 @@ <script> - /** - * Falls back to the code used in `copy_to_clipboard.js` - */ - import tooltip from '../directives/tooltip'; +/** + * Falls back to the code used in `copy_to_clipboard.js` + * + * Renders a button with a clipboard icon that copies the content of `data-clipboard-text` + * when clicked. + * + * @example + * <clipboard-button + * title="Copy to clipbard" + * text="Content to be copied" + * css-class="btn-transparent" + * /> + */ +import tooltip from '../directives/tooltip'; - export default { - name: 'ClipboardButton', - directives: { - tooltip, +export default { + name: 'ClipboardButton', + directives: { + tooltip, + }, + props: { + text: { + type: String, + required: true, }, - props: { - text: { - type: String, - required: true, - }, - title: { - type: String, - required: true, - }, - tooltipPlacement: { - type: String, - required: false, - default: 'top', - }, - tooltipContainer: { - type: [String, Boolean], - required: false, - default: false, - }, - cssClass: { - type: String, - required: false, - default: 'btn-default', - }, + title: { + type: String, + required: true, }, - }; + tooltipPlacement: { + type: String, + required: false, + default: 'top', + }, + tooltipContainer: { + type: [String, Boolean], + required: false, + default: false, + }, + cssClass: { + type: String, + required: false, + default: 'btn-default', + }, + }, +}; </script> <template> diff --git a/app/assets/javascripts/vue_shared/components/commit.vue b/app/assets/javascripts/vue_shared/components/commit.vue index b8875d04488..8f250a6c989 100644 --- a/app/assets/javascripts/vue_shared/components/commit.vue +++ b/app/assets/javascripts/vue_shared/components/commit.vue @@ -1,119 +1,111 @@ <script> - import commitIconSvg from 'icons/_icon_commit.svg'; - import userAvatarLink from './user_avatar/user_avatar_link.vue'; - import tooltip from '../directives/tooltip'; - import icon from '../../vue_shared/components/icon.vue'; +import UserAvatarLink from './user_avatar/user_avatar_link.vue'; +import tooltip from '../directives/tooltip'; +import Icon from '../../vue_shared/components/icon.vue'; - export default { - directives: { - tooltip, +export default { + directives: { + tooltip, + }, + components: { + UserAvatarLink, + Icon, + }, + props: { + /** + * Indicates the existance of a tag. + * Used to render the correct icon, if true will render `fa-tag` icon, + * if false will render a svg sprite fork icon + */ + tag: { + type: Boolean, + required: false, + default: false, }, - components: { - userAvatarLink, - icon, + /** + * If provided is used to render the branch name and url. + * Should contain the following properties: + * name + * ref_url + */ + commitRef: { + type: Object, + required: false, + default: () => ({}), + }, + /** + * Used to link to the commit sha. + */ + commitUrl: { + type: String, + required: false, + default: '', }, - props: { - /** - * Indicates the existance of a tag. - * Used to render the correct icon, if true will render `fa-tag` icon, - * if false will render a svg sprite fork icon - */ - tag: { - type: Boolean, - required: false, - default: false, - }, - /** - * If provided is used to render the branch name and url. - * Should contain the following properties: - * name - * ref_url - */ - commitRef: { - type: Object, - required: false, - default: () => ({}), - }, - /** - * Used to link to the commit sha. - */ - commitUrl: { - type: String, - required: false, - default: '', - }, - /** - * Used to show the commit short sha that links to the commit url. - */ - shortSha: { - type: String, - required: false, - default: '', - }, - /** - * If provided shows the commit tile. - */ - title: { - type: String, - required: false, - default: '', - }, - /** - * If provided renders information about the author of the commit. - * When provided should include: - * `avatar_url` to render the avatar icon - * `web_url` to link to user profile - * `username` to render alt and title tags - */ - author: { - type: Object, - required: false, - default: () => ({}), - }, - showBranch: { - type: Boolean, - required: false, - default: true, - }, + /** + * Used to show the commit short sha that links to the commit url. + */ + shortSha: { + type: String, + required: false, + default: '', + }, + /** + * If provided shows the commit tile. + */ + title: { + type: String, + required: false, + default: '', + }, + /** + * If provided renders information about the author of the commit. + * When provided should include: + * `avatar_url` to render the avatar icon + * `web_url` to link to user profile + * `username` to render alt and title tags + */ + author: { + type: Object, + required: false, + default: () => ({}), + }, + showBranch: { + type: Boolean, + required: false, + default: true, }, - computed: { - /** - * Used to verify if all the properties needed to render the commit - * ref section were provided. - * - * @returns {Boolean} - */ - hasCommitRef() { - return this.commitRef && this.commitRef.name && this.commitRef.ref_url; - }, - /** - * Used to verify if all the properties needed to render the commit - * author section were provided. - * - * @returns {Boolean} - */ - hasAuthor() { - return this.author && - this.author.avatar_url && - this.author.path && - this.author.username; - }, - /** - * If information about the author is provided will return a string - * to be rendered as the alt attribute of the img tag. - * - * @returns {String} - */ - userImageAltDescription() { - return this.author && - this.author.username ? `${this.author.username}'s avatar` : null; - }, + }, + computed: { + /** + * Used to verify if all the properties needed to render the commit + * ref section were provided. + * + * @returns {Boolean} + */ + hasCommitRef() { + return this.commitRef && this.commitRef.name && this.commitRef.ref_url; }, - created() { - this.commitIconSvg = commitIconSvg; + /** + * Used to verify if all the properties needed to render the commit + * author section were provided. + * + * @returns {Boolean} + */ + hasAuthor() { + return this.author && this.author.avatar_url && this.author.path && this.author.username; }, - }; + /** + * If information about the author is provided will return a string + * to be rendered as the alt attribute of the img tag. + * + * @returns {String} + */ + userImageAltDescription() { + return this.author && this.author.username ? `${this.author.username}'s avatar` : null; + }, + }, +}; </script> <template> <div class="branch-commit"> @@ -141,11 +133,10 @@ {{ commitRef.name }} </a> </template> - <div - v-html="commitIconSvg" + <icon + name="commit" class="commit-icon js-commit-icon" - > - </div> + /> <a class="commit-sha" diff --git a/app/assets/javascripts/vue_shared/components/expand_button.vue b/app/assets/javascripts/vue_shared/components/expand_button.vue index c943c8d98a4..9295be3e2b2 100644 --- a/app/assets/javascripts/vue_shared/components/expand_button.vue +++ b/app/assets/javascripts/vue_shared/components/expand_button.vue @@ -1,33 +1,33 @@ <script> - import { __ } from '~/locale'; - /** - * Port of detail_behavior expand button. - * - * @example - * <expand-button> - * <template slot="expanded"> - * Text goes here. - * </template> - * </expand-button> - */ - export default { - name: 'ExpandButton', - data() { - return { - isCollapsed: true, - }; +import { __ } from '~/locale'; +/** + * Port of detail_behavior expand button. + * + * @example + * <expand-button> + * <template slot="expanded"> + * Text goes here. + * </template> + * </expand-button> + */ +export default { + name: 'ExpandButton', + data() { + return { + isCollapsed: true, + }; + }, + computed: { + ariaLabel() { + return __('Click to expand text'); }, - computed: { - ariaLabel() { - return __('Click to expand text'); - }, + }, + methods: { + onClick() { + this.isCollapsed = !this.isCollapsed; }, - methods: { - onClick() { - this.isCollapsed = !this.isCollapsed; - }, - }, - }; + }, +}; </script> <template> <span> diff --git a/app/assets/javascripts/vue_shared/components/header_ci_component.vue b/app/assets/javascripts/vue_shared/components/header_ci_component.vue index a0cd0cbd200..088187ed348 100644 --- a/app/assets/javascripts/vue_shared/components/header_ci_component.vue +++ b/app/assets/javascripts/vue_shared/components/header_ci_component.vue @@ -1,78 +1,78 @@ <script> - import ciIconBadge from './ci_badge_link.vue'; - import loadingIcon from './loading_icon.vue'; - import timeagoTooltip from './time_ago_tooltip.vue'; - import tooltip from '../directives/tooltip'; - import userAvatarImage from './user_avatar/user_avatar_image.vue'; +import CiIconBadge from './ci_badge_link.vue'; +import LoadingIcon from './loading_icon.vue'; +import TimeagoTooltip from './time_ago_tooltip.vue'; +import tooltip from '../directives/tooltip'; +import UserAvatarImage from './user_avatar/user_avatar_image.vue'; - /** - * Renders header component for job and pipeline page based on UI mockups - * - * Used in: - * - job show page - * - pipeline show page - */ - export default { - components: { - ciIconBadge, - loadingIcon, - timeagoTooltip, - userAvatarImage, +/** + * Renders header component for job and pipeline page based on UI mockups + * + * Used in: + * - job show page + * - pipeline show page + */ +export default { + components: { + CiIconBadge, + LoadingIcon, + TimeagoTooltip, + UserAvatarImage, + }, + directives: { + tooltip, + }, + props: { + status: { + type: Object, + required: true, }, - directives: { - tooltip, + itemName: { + type: String, + required: true, }, - props: { - status: { - type: Object, - required: true, - }, - itemName: { - type: String, - required: true, - }, - itemId: { - type: Number, - required: true, - }, - time: { - type: String, - required: true, - }, - user: { - type: Object, - required: false, - default: () => ({}), - }, - actions: { - type: Array, - required: false, - default: () => [], - }, - hasSidebarButton: { - type: Boolean, - required: false, - default: false, - }, - shouldRenderTriggeredLabel: { - type: Boolean, - required: false, - default: true, - }, + itemId: { + type: Number, + required: true, }, + time: { + type: String, + required: true, + }, + user: { + type: Object, + required: false, + default: () => ({}), + }, + actions: { + type: Array, + required: false, + default: () => [], + }, + hasSidebarButton: { + type: Boolean, + required: false, + default: false, + }, + shouldRenderTriggeredLabel: { + type: Boolean, + required: false, + default: true, + }, + }, - computed: { - userAvatarAltText() { - return `${this.user.name}'s avatar`; - }, + computed: { + userAvatarAltText() { + return `${this.user.name}'s avatar`; }, + }, - methods: { - onClickAction(action) { - this.$emit('actionClicked', action); - }, + methods: { + onClickAction(action) { + this.$emit('actionClicked', action); }, - }; + }, +}; </script> <template> diff --git a/app/assets/javascripts/vue_shared/components/icon.vue b/app/assets/javascripts/vue_shared/components/icon.vue index 6a2e05000e1..1a0df49bc29 100644 --- a/app/assets/javascripts/vue_shared/components/icon.vue +++ b/app/assets/javascripts/vue_shared/components/icon.vue @@ -1,76 +1,75 @@ <script> +/* This is a re-usable vue component for rendering a svg sprite + icon - /* This is a re-usable vue component for rendering a svg sprite - icon + Sample configuration: - Sample configuration: + <icon + name="retry" + :size="32" + css-classes="top" + /> - <icon - name="retry" - :size="32" - css-classes="top" - /> +*/ +// only allow classes in images.scss e.g. s12 +const validSizes = [8, 12, 16, 18, 24, 32, 48, 72]; - */ - // only allow classes in images.scss e.g. s12 - const validSizes = [8, 12, 16, 18, 24, 32, 48, 72]; - - export default { - props: { - name: { - type: String, - required: true, - }, +export default { + props: { + name: { + type: String, + required: true, + }, - size: { - type: Number, - required: false, - default: 16, - validator(value) { - return validSizes.includes(value); - }, + size: { + type: Number, + required: false, + default: 16, + validator(value) { + return validSizes.includes(value); }, + }, - cssClasses: { - type: String, - required: false, - default: '', - }, + cssClasses: { + type: String, + required: false, + default: '', + }, - width: { - type: Number, - required: false, - default: null, - }, + width: { + type: Number, + required: false, + default: null, + }, - height: { - type: Number, - required: false, - default: null, - }, + height: { + type: Number, + required: false, + default: null, + }, - y: { - type: Number, - required: false, - default: null, - }, + y: { + type: Number, + required: false, + default: null, + }, - x: { - type: Number, - required: false, - default: null, - }, + x: { + type: Number, + required: false, + default: null, }, + }, - computed: { - spriteHref() { - return `${gon.sprite_icons}#${this.name}`; - }, - iconSizeClass() { - return this.size ? `s${this.size}` : ''; - }, + computed: { + spriteHref() { + return `${gon.sprite_icons}#${this.name}`; + }, + iconSizeClass() { + return this.size ? `s${this.size}` : ''; }, - }; + }, +}; </script> <template> @@ -79,7 +78,8 @@ :width="width" :height="height" :x="x" - :y="y"> + :y="y" + > <use v-bind="{ 'xlink:href':spriteHref }" /> </svg> </template> diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue index 5ede53d8d01..70b46a9c2bb 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue @@ -1,4 +1,5 @@ <script> +import $ from 'jquery'; import { __ } from '~/locale'; import LabelsSelect from '~/labels_select'; import LoadingIcon from '../../loading_icon.vue'; @@ -98,11 +99,18 @@ export default { this.labelsDropdown = new LabelsSelect(this.$refs.dropdownButton, { handleClick: this.handleClick, }); + $(this.$refs.dropdown).on('hidden.gl.dropdown', this.handleDropdownHidden); }, methods: { handleClick(label) { this.$emit('onLabelClick', label); }, + handleCollapsedValueClick() { + this.$emit('toggleCollapse'); + }, + handleDropdownHidden() { + this.$emit('onDropdownClose'); + }, }, }; </script> @@ -112,6 +120,7 @@ export default { <dropdown-value-collapsed v-if="showCreate" :labels="context.labels" + @onValueClick="handleCollapsedValueClick" /> <dropdown-title :can-edit="canEdit" @@ -133,7 +142,10 @@ export default { :name="hiddenInputName" :label="label" /> - <div class="dropdown"> + <div + class="dropdown" + ref="dropdown" + > <dropdown-button :ability-name="abilityName" :field-name="hiddenInputName" diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue index 5cf728fe050..68fa2ab8d01 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue @@ -26,6 +26,11 @@ export default { return labelsString; }, }, + methods: { + handleClick() { + this.$emit('onValueClick'); + }, + }, }; </script> @@ -36,6 +41,7 @@ export default { data-placement="left" data-container="body" :title="labelsList" + @click="handleClick" > <i aria-hidden="true" diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index d0dda50a835..e058a0b35b7 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -472,6 +472,7 @@ img.emoji { .append-right-20 { margin-right: 20px; } .append-bottom-0 { margin-bottom: 0; } .append-bottom-5 { margin-bottom: 5px; } +.append-bottom-8 { margin-bottom: $grid-size; } .append-bottom-10 { margin-bottom: 10px; } .append-bottom-15 { margin-bottom: 15px; } .append-bottom-20 { margin-bottom: 20px; } diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index cc5fac6816d..664aade7375 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -43,7 +43,7 @@ border-color: $gray-darkest; } - [data-toggle="dropdown"] { + [data-toggle='dropdown'] { outline: 0; } } @@ -172,7 +172,11 @@ color: $brand-danger; } - &:hover, + &.disable-hover { + text-decoration: none; + } + + &:not(.disable-hover):hover, &:active, &:focus, &.is-focused { @@ -508,17 +512,16 @@ } &.is-indeterminate::before { - content: "\f068"; + content: '\f068'; } &.is-active::before { - content: "\f00c"; + content: '\f00c'; } } } } - .dropdown-title { position: relative; padding: 2px 25px 10px; @@ -724,7 +727,6 @@ } } - .dropdown-menu-due-date { .dropdown-content { max-height: 230px; @@ -854,9 +856,13 @@ header.header-content .dropdown-menu.projects-dropdown-menu { } .projects-list-frequent-container, - .projects-list-search-container, { + .projects-list-search-container { padding: 8px 0; overflow-y: auto; + + li.section-empty.section-failure { + color: $callout-danger-color; + } } .section-header, @@ -867,13 +873,6 @@ header.header-content .dropdown-menu.projects-dropdown-menu { font-size: $gl-font-size; } - .projects-list-frequent-container, - .projects-list-search-container { - li.section-empty.section-failure { - color: $callout-danger-color; - } - } - .search-input-container { position: relative; padding: 4px $gl-padding; @@ -905,8 +904,7 @@ header.header-content .dropdown-menu.projects-dropdown-menu { } .projects-list-item-container { - .project-item-avatar-container - .project-item-metadata-container { + .project-item-avatar-container .project-item-metadata-container { float: left; } diff --git a/app/assets/stylesheets/framework/mobile.scss b/app/assets/stylesheets/framework/mobile.scss index 8604e753c18..9e03bb98b8e 100644 --- a/app/assets/stylesheets/framework/mobile.scss +++ b/app/assets/stylesheets/framework/mobile.scss @@ -40,10 +40,6 @@ .project-home-panel { padding-left: 0 !important; - .project-avatar { - display: block; - } - .project-repo-buttons, .git-clone-holder { display: none; diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index 855ebf7d86d..3a8ec779c14 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -468,6 +468,14 @@ margin-bottom: 10px; white-space: normal; + .ci-job-dropdown-container { + // override dropdown.scss + .dropdown-menu li button { + padding: 0; + text-align: center; + } + } + // ensure .build-content has hover style when action-icon is hovered .ci-job-dropdown-container:hover .build-content { @extend .build-content:hover; diff --git a/app/assets/stylesheets/pages/repo.scss b/app/assets/stylesheets/pages/repo.scss index a94edc7d34c..6342042374f 100644 --- a/app/assets/stylesheets/pages/repo.scss +++ b/app/assets/stylesheets/pages/repo.scss @@ -17,6 +17,7 @@ } .ide-view { + position: relative; display: flex; height: calc(100vh - #{$header-height}); margin-top: 0; @@ -880,6 +881,26 @@ font-weight: $gl-font-weight-bold; } +.ide-file-finder-overlay { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 100; +} + +.ide-file-finder { + top: 10px; + left: 50%; + transform: translateX(-50%); + + .highlighted { + color: $blue-500; + font-weight: $gl-font-weight-bold; + } +} + .ide-commit-message-field { height: 200px; background-color: $white-light; diff --git a/app/controllers/concerns/authenticates_with_two_factor.rb b/app/controllers/concerns/authenticates_with_two_factor.rb index 2fdf346ef44..69a053d4246 100644 --- a/app/controllers/concerns/authenticates_with_two_factor.rb +++ b/app/controllers/concerns/authenticates_with_two_factor.rb @@ -23,6 +23,9 @@ module AuthenticatesWithTwoFactor # # Returns nil def prompt_for_two_factor(user) + # Set @user for Devise views + @user = user # rubocop:disable Gitlab/ModuleWithInstanceVariables + return locked_user_redirect(user) unless user.can?(:log_in) session[:otp_user_id] = user.id diff --git a/app/controllers/concerns/issuable_collections.rb b/app/controllers/concerns/issuable_collections.rb index 4114ca6bf7c..34228cf0b82 100644 --- a/app/controllers/concerns/issuable_collections.rb +++ b/app/controllers/concerns/issuable_collections.rb @@ -165,8 +165,8 @@ module IssuableCollections [:project, :author, :assignees, :labels, :milestone, project: :namespace] when 'MergeRequest' [ - :source_project, :target_project, :author, :assignee, :labels, :milestone, - head_pipeline: :project, target_project: :namespace, latest_merge_request_diff: :merge_request_diff_commits + :target_project, :author, :assignee, :labels, :milestone, + source_project: :route, head_pipeline: :project, target_project: :namespace, latest_merge_request_diff: :merge_request_diff_commits ] end end diff --git a/app/controllers/ldap/omniauth_callbacks_controller.rb b/app/controllers/ldap/omniauth_callbacks_controller.rb new file mode 100644 index 00000000000..fb24edb8602 --- /dev/null +++ b/app/controllers/ldap/omniauth_callbacks_controller.rb @@ -0,0 +1,31 @@ +class Ldap::OmniauthCallbacksController < OmniauthCallbacksController + extend ::Gitlab::Utils::Override + + def self.define_providers! + return unless Gitlab::Auth::LDAP::Config.enabled? + + Gitlab::Auth::LDAP::Config.available_servers.each do |server| + alias_method server['provider_name'], :ldap + end + end + + # We only find ourselves here + # if the authentication to LDAP was successful. + def ldap + sign_in_user_flow(Gitlab::Auth::LDAP::User) + end + + define_providers! + + override :set_remember_me + def set_remember_me(user) + user.remember_me = params[:remember_me] if user.persisted? + end + + override :fail_login + def fail_login(user) + flash[:alert] = 'Access denied for your LDAP account.' + + redirect_to new_user_session_path + end +end diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb index 5e6676ea513..9137bc92810 100644 --- a/app/controllers/omniauth_callbacks_controller.rb +++ b/app/controllers/omniauth_callbacks_controller.rb @@ -4,18 +4,12 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController protect_from_forgery except: [:kerberos, :saml, :cas3] - Gitlab.config.omniauth.providers.each do |provider| - define_method provider['name'] do - handle_omniauth - end + def handle_omniauth + omniauth_flow(Gitlab::Auth::OAuth) end - if Gitlab::Auth::LDAP::Config.enabled? - Gitlab::Auth::LDAP::Config.available_servers.each do |server| - define_method server['provider_name'] do - ldap - end - end + Gitlab.config.omniauth.providers.each do |provider| + alias_method provider['name'], :handle_omniauth end # Extend the standard implementation to also increment @@ -37,51 +31,12 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController error ||= exception.error if exception.respond_to?(:error) error ||= exception.message if exception.respond_to?(:message) error ||= env["omniauth.error.type"].to_s - error.to_s.humanize if error - end - - # We only find ourselves here - # if the authentication to LDAP was successful. - def ldap - ldap_user = Gitlab::Auth::LDAP::User.new(oauth) - ldap_user.save if ldap_user.changed? # will also save new users - - @user = ldap_user.gl_user - @user.remember_me = params[:remember_me] if ldap_user.persisted? - # Do additional LDAP checks for the user filter and EE features - if ldap_user.allowed? - if @user.two_factor_enabled? - prompt_for_two_factor(@user) - else - log_audit_event(@user, with: oauth['provider']) - sign_in_and_redirect(@user) - end - else - fail_ldap_login - end + error.to_s.humanize if error end def saml - if current_user - log_audit_event(current_user, with: :saml) - # Update SAML identity if data has changed. - identity = current_user.identities.with_extern_uid(:saml, oauth['uid']).take - if identity.nil? - current_user.identities.create(extern_uid: oauth['uid'], provider: :saml) - redirect_to profile_account_path, notice: 'Authentication method updated' - else - redirect_to after_sign_in_path_for(current_user) - end - else - saml_user = Gitlab::Auth::Saml::User.new(oauth) - saml_user.save if saml_user.changed? - @user = saml_user.gl_user - - continue_login_process - end - rescue Gitlab::Auth::OAuth::User::SignupDisabledError - handle_signup_error + omniauth_flow(Gitlab::Auth::Saml) end def omniauth_error @@ -117,25 +72,36 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController private - def handle_omniauth + def omniauth_flow(auth_module, identity_linker: nil) if current_user - # Add new authentication method - current_user.identities - .with_extern_uid(oauth['provider'], oauth['uid']) - .first_or_create(extern_uid: oauth['uid']) log_audit_event(current_user, with: oauth['provider']) - redirect_to profile_account_path, notice: 'Authentication method updated' - else - oauth_user = Gitlab::Auth::OAuth::User.new(oauth) - oauth_user.save - @user = oauth_user.gl_user - continue_login_process + identity_linker ||= auth_module::IdentityLinker.new(current_user, oauth) + + identity_linker.link + + if identity_linker.changed? + redirect_identity_linked + elsif identity_linker.error_message.present? + redirect_identity_link_failed(identity_linker.error_message) + else + redirect_identity_exists + end + else + sign_in_user_flow(auth_module::User) end - rescue Gitlab::Auth::OAuth::User::SigninDisabledForProviderError - handle_disabled_provider - rescue Gitlab::Auth::OAuth::User::SignupDisabledError - handle_signup_error + end + + def redirect_identity_exists + redirect_to after_sign_in_path_for(current_user) + end + + def redirect_identity_link_failed(error_message) + redirect_to profile_account_path, notice: "Authentication failed: #{error_message}" + end + + def redirect_identity_linked + redirect_to profile_account_path, notice: 'Authentication method updated' end def handle_service_ticket(provider, ticket) @@ -144,21 +110,27 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController session[:service_tickets][provider] = ticket end - def continue_login_process - # Only allow properly saved users to login. - if @user.persisted? && @user.valid? - log_audit_event(@user, with: oauth['provider']) + def sign_in_user_flow(auth_user_class) + auth_user = auth_user_class.new(oauth) + user = auth_user.find_and_update! + + if auth_user.valid_sign_in? + log_audit_event(user, with: oauth['provider']) - if @user.two_factor_enabled? - params[:remember_me] = '1' if remember_me? - prompt_for_two_factor(@user) + set_remember_me(user) + + if user.two_factor_enabled? + prompt_for_two_factor(user) else - remember_me(@user) if remember_me? - sign_in_and_redirect(@user) + sign_in_and_redirect(user) end else - fail_login + fail_login(user) end + rescue Gitlab::Auth::OAuth::User::SigninDisabledForProviderError + handle_disabled_provider + rescue Gitlab::Auth::OAuth::User::SignupDisabledError + handle_signup_error end def handle_signup_error @@ -178,18 +150,12 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController @oauth ||= request.env['omniauth.auth'] end - def fail_login - error_message = @user.errors.full_messages.to_sentence + def fail_login(user) + error_message = user.errors.full_messages.to_sentence return redirect_to omniauth_error_path(oauth['provider'], error: error_message) end - def fail_ldap_login - flash[:alert] = 'Access denied for your LDAP account.' - - redirect_to new_user_session_path - end - def fail_auth0_login flash[:alert] = 'Wrong extern UID provided. Make sure Auth0 is configured correctly.' @@ -208,6 +174,16 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController .for_authentication.security_event end + def set_remember_me(user) + return unless remember_me? + + if user.two_factor_enabled? + params[:remember_me] = '1' + else + remember_me(user) + end + end + def remember_me? request_params = request.env['omniauth.params'] (request_params['remember_me'] == '1') if request_params.present? diff --git a/app/finders/users_finder.rb b/app/finders/users_finder.rb index edde8022ec9..65824a51919 100644 --- a/app/finders/users_finder.rb +++ b/app/finders/users_finder.rb @@ -32,6 +32,7 @@ class UsersFinder users = by_active(users) users = by_external_identity(users) users = by_external(users) + users = by_2fa(users) users = by_created_at(users) users = by_custom_attributes(users) @@ -76,4 +77,15 @@ class UsersFinder users.external end + + def by_2fa(users) + case params[:two_factor] + when 'enabled' + users.with_two_factor + when 'disabled' + users.without_two_factor + else + users + end + end end diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb index 7f3c118c7ab..40073f714ee 100644 --- a/app/helpers/gitlab_routing_helper.rb +++ b/app/helpers/gitlab_routing_helper.rb @@ -81,6 +81,14 @@ module GitlabRoutingHelper end end + def edit_milestone_path(entity, *args) + if entity.parent.is_a?(Group) + edit_group_milestone_path(entity.parent, entity, *args) + else + edit_project_milestone_path(entity.parent, entity, *args) + end + end + def toggle_subscription_path(entity, *args) if entity.is_a?(Issue) toggle_subscription_project_issue_path(entity.project, entity) diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index b0c02cdeec7..9000ad860e9 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -27,6 +27,7 @@ module Ci has_one :metadata, class_name: 'Ci::BuildMetadata' delegate :timeout, to: :metadata, prefix: true, allow_nil: true + delegate :gitlab_deploy_token, to: :project ## # The "environment" field for builds is a String, and is the unexpanded name! @@ -604,6 +605,7 @@ module Ci .append(key: 'CI_REGISTRY_USER', value: CI_REGISTRY_USER) .append(key: 'CI_REGISTRY_PASSWORD', value: token, public: false) .append(key: 'CI_REPOSITORY_URL', value: repo_url, public: false) + .concat(deploy_token_variables) end end @@ -654,6 +656,15 @@ module Ci end end + def deploy_token_variables + Gitlab::Ci::Variables::Collection.new.tap do |variables| + break variables unless gitlab_deploy_token + + variables.append(key: 'CI_DEPLOY_USER', value: gitlab_deploy_token.name) + variables.append(key: 'CI_DEPLOY_PASSWORD', value: gitlab_deploy_token.token, public: false) + end + end + def environment_url options&.dig(:environment, :url) || persisted_environment&.external_url end diff --git a/app/models/concerns/avatarable.rb b/app/models/concerns/avatarable.rb index 7677891b9ce..13246a774e3 100644 --- a/app/models/concerns/avatarable.rb +++ b/app/models/concerns/avatarable.rb @@ -31,12 +31,13 @@ module Avatarable asset_host = ActionController::Base.asset_host use_asset_host = asset_host.present? + use_authentication = respond_to?(:public?) && !public? # Avatars for private and internal groups and projects require authentication to be viewed, # which means they can only be served by Rails, on the regular GitLab host. # If an asset host is configured, we need to return the fully qualified URL # instead of only the avatar path, so that Rails doesn't prefix it with the asset host. - if use_asset_host && respond_to?(:public?) && !public? + if use_asset_host && use_authentication use_asset_host = false only_path = false end @@ -49,6 +50,6 @@ module Avatarable url_base << gitlab_config.relative_url_root end - url_base + avatar.url + url_base + avatar.local_url end end diff --git a/app/models/concerns/protected_ref.rb b/app/models/concerns/protected_ref.rb index 454374121f3..94eef4ff7cd 100644 --- a/app/models/concerns/protected_ref.rb +++ b/app/models/concerns/protected_ref.rb @@ -31,7 +31,7 @@ module ProtectedRef end end - def protected_ref_accessible_to?(ref, user, action:, protected_refs: nil) + def protected_ref_accessible_to?(ref, user, project:, action:, protected_refs: nil) access_levels_for_ref(ref, action: action, protected_refs: protected_refs).any? do |access_level| access_level.check_access(user) end diff --git a/app/models/concerns/routable.rb b/app/models/concerns/routable.rb index dfd7d94450b..915ad6959be 100644 --- a/app/models/concerns/routable.rb +++ b/app/models/concerns/routable.rb @@ -102,7 +102,7 @@ module Routable # the route. Caching this per request ensures that even if we have multiple instances, # we will not have to duplicate work, avoiding N+1 queries in some cases. def full_path - return uncached_full_path unless RequestStore.active? + return uncached_full_path unless RequestStore.active? && persisted? RequestStore[full_path_key] ||= uncached_full_path end @@ -124,6 +124,11 @@ module Routable end end + # Group would override this to check from association + def owned_by?(user) + owner == user + end + private def set_path_errors diff --git a/app/models/deploy_token.rb b/app/models/deploy_token.rb index 979e9232fda..5082dc45368 100644 --- a/app/models/deploy_token.rb +++ b/app/models/deploy_token.rb @@ -4,6 +4,7 @@ class DeployToken < ActiveRecord::Base add_authentication_token_field :token AVAILABLE_SCOPES = %i(read_repository read_registry).freeze + GITLAB_DEPLOY_TOKEN_NAME = 'gitlab-deploy-token'.freeze default_value_for(:expires_at) { Forever.date } @@ -17,6 +18,10 @@ class DeployToken < ActiveRecord::Base scope :active, -> { where("revoked = false AND expires_at >= NOW()") } + def self.gitlab_deploy_token + active.find_by(name: GITLAB_DEPLOY_TOKEN_NAME) + end + def revoke! update!(revoked: true) end diff --git a/app/models/group.rb b/app/models/group.rb index 8ff781059cc..9b42bbf99be 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -125,6 +125,10 @@ class Group < Namespace self[:lfs_enabled] end + def owned_by?(user) + owners.include?(user) + end + def add_users(users, access_level, current_user: nil, expires_at: nil) GroupMember.add_users( self, diff --git a/app/models/project.rb b/app/models/project.rb index cec1e705aa8..172980b0ad4 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -68,6 +68,11 @@ class Project < ActiveRecord::Base after_save :update_project_statistics, if: :namespace_id_changed? after_create :create_project_feature, unless: :project_feature + + after_create :create_ci_cd_settings, + unless: :ci_cd_settings, + if: proc { ProjectCiCdSetting.available? } + after_create :set_last_activity_at after_create :set_last_repository_updated_at after_update :update_forks_visibility_level @@ -231,6 +236,7 @@ class Project < ActiveRecord::Base has_many :custom_attributes, class_name: 'ProjectCustomAttribute' has_many :project_badges, class_name: 'ProjectBadge' + has_one :ci_cd_settings, class_name: 'ProjectCiCdSetting' accepts_nested_attributes_for :variables, allow_destroy: true accepts_nested_attributes_for :project_feature, update_only: true @@ -1041,13 +1047,6 @@ class Project < ActiveRecord::Base "#{web_url}.git" end - def user_can_push_to_empty_repo?(user) - return false unless empty_repo? - return false unless Ability.allowed?(user, :push_code, self) - - !ProtectedBranch.default_branch_protected? || team.max_member_access(user.id) > Gitlab::Access::DEVELOPER - end - def forked? return true if fork_network && fork_network.root_project != self @@ -1879,6 +1878,10 @@ class Project < ActiveRecord::Base [] end + def gitlab_deploy_token + @gitlab_deploy_token ||= deploy_tokens.gitlab_deploy_token + end + private def storage @@ -2004,10 +2007,11 @@ class Project < ActiveRecord::Base def fetch_branch_allows_maintainer_push?(user, branch_name) check_access = -> do + next false if empty_repo? + merge_request = source_of_merge_requests.opened .where(allow_maintainer_to_push: true) .find_by(source_branch: branch_name) - merge_request&.can_be_merged_by?(user) end diff --git a/app/models/project_ci_cd_setting.rb b/app/models/project_ci_cd_setting.rb new file mode 100644 index 00000000000..9f10a93148c --- /dev/null +++ b/app/models/project_ci_cd_setting.rb @@ -0,0 +1,16 @@ +class ProjectCiCdSetting < ActiveRecord::Base + belongs_to :project + + # The version of the schema that first introduced this model/table. + MINIMUM_SCHEMA_VERSION = 20180403035759 + + def self.available? + @available ||= + ActiveRecord::Migrator.current_version >= MINIMUM_SCHEMA_VERSION + end + + def self.reset_column_information + @available = nil + super + end +end diff --git a/app/models/project_services/flowdock_service.rb b/app/models/project_services/flowdock_service.rb index 4d23a17a545..da01ac1b7cf 100644 --- a/app/models/project_services/flowdock_service.rb +++ b/app/models/project_services/flowdock_service.rb @@ -1,5 +1,51 @@ require "flowdock-git-hook" +# Flow dock depends on Grit to compute the number of commits between two given +# commits. To make this depend on Gitaly, a monkey patch is applied +module Flowdock + class Git + # pass down a Repository all the way down + def repo + @options[:repo] + end + + def config + {} + end + + def messages + Git::Builder.new(repo: repo, + ref: @ref, + before: @from, + after: @to, + commit_url: @commit_url, + branch_url: @branch_url, + diff_url: @diff_url, + repo_url: @repo_url, + repo_name: @repo_name, + permanent_refs: @permanent_refs, + tags: tags + ).to_hashes + end + + class Builder + def commits + @repo.commits_between(@before, @after).map do |commit| + { + url: @opts[:commit_url] ? @opts[:commit_url] % [commit.sha] : nil, + id: commit.sha, + message: commit.message, + author: { + name: commit.author_name, + email: commit.author_email + } + } + end + end + end + end +end + class FlowdockService < Service prop_accessor :token validates :token, presence: true, if: :activated? @@ -34,7 +80,7 @@ class FlowdockService < Service data[:before], data[:after], token: token, - repo: project.repository.path_to_repo, + repo: project.repository, repo_url: "#{Gitlab.config.gitlab.url}/#{project.full_path}", commit_url: "#{Gitlab.config.gitlab.url}/#{project.full_path}/commit/%s", diff_url: "#{Gitlab.config.gitlab.url}/#{project.full_path}/compare/%s...%s" diff --git a/app/models/protected_branch.rb b/app/models/protected_branch.rb index 609780c5587..cb361a66591 100644 --- a/app/models/protected_branch.rb +++ b/app/models/protected_branch.rb @@ -4,6 +4,15 @@ class ProtectedBranch < ActiveRecord::Base protected_ref_access_levels :merge, :push + def self.protected_ref_accessible_to?(ref, user, project:, action:, protected_refs: nil) + # Masters, owners and admins are allowed to create the default branch + if default_branch_protected? && project.empty_repo? + return true if user.admin? || project.team.max_member_access(user.id) > Gitlab::Access::DEVELOPER + end + + super + end + # Check if branch name is marked as protected in the system def self.protected?(project, ref_name) return true if project.empty_repo? && default_branch_protected? diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb index c9cb730c4e9..520710b757d 100644 --- a/app/policies/group_policy.rb +++ b/app/policies/group_policy.rb @@ -22,7 +22,7 @@ class GroupPolicy < BasePolicy condition(:can_change_parent_share_with_group_lock) { can?(:change_share_with_group_lock, @subject.parent) } condition(:has_projects) do - GroupProjectsFinder.new(group: @subject, current_user: @user).execute.any? + GroupProjectsFinder.new(group: @subject, current_user: @user, options: { include_subgroups: true }).execute.any? end with_options scope: :subject, score: 0 @@ -43,7 +43,11 @@ class GroupPolicy < BasePolicy end rule { admin } .enable :read_group - rule { has_projects } .enable :read_group + + rule { has_projects }.policy do + enable :read_group + enable :read_label + end rule { has_access }.enable :read_namespace diff --git a/app/presenters/project_presenter.rb b/app/presenters/project_presenter.rb index 484ac64580d..ad655a7b3f4 100644 --- a/app/presenters/project_presenter.rb +++ b/app/presenters/project_presenter.rb @@ -4,6 +4,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated include GitlabRoutingHelper include StorageHelper include TreeHelper + include ChecksCollaboration include Gitlab::Utils::StrongMemoize presents :project @@ -170,9 +171,11 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated end def can_current_user_push_to_branch?(branch) - return false unless repository.branch_exists?(branch) + user_access(project).can_push_to_branch?(branch) + end - ::Gitlab::UserAccess.new(current_user, project: project).can_push_to_branch?(branch) + def can_current_user_push_to_default_branch? + can_current_user_push_to_branch?(default_branch) end def files_anchor_data @@ -200,7 +203,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated end def new_file_anchor_data - if current_user && can_current_user_push_code? + if current_user && can_current_user_push_to_default_branch? OpenStruct.new(enabled: false, label: _('New file'), link: project_new_blob_path(project, default_branch || 'master'), @@ -209,7 +212,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated end def readme_anchor_data - if current_user && can_current_user_push_code? && repository.readme.blank? + if current_user && can_current_user_push_to_default_branch? && repository.readme.blank? OpenStruct.new(enabled: false, label: _('Add Readme'), link: add_readme_path) @@ -221,7 +224,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated end def changelog_anchor_data - if current_user && can_current_user_push_code? && repository.changelog.blank? + if current_user && can_current_user_push_to_default_branch? && repository.changelog.blank? OpenStruct.new(enabled: false, label: _('Add Changelog'), link: add_changelog_path) @@ -233,7 +236,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated end def license_anchor_data - if current_user && can_current_user_push_code? && repository.license_blob.blank? + if current_user && can_current_user_push_to_default_branch? && repository.license_blob.blank? OpenStruct.new(enabled: false, label: _('Add License'), link: add_license_path) @@ -245,7 +248,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated end def contribution_guide_anchor_data - if current_user && can_current_user_push_code? && repository.contribution_guide.blank? + if current_user && can_current_user_push_to_default_branch? && repository.contribution_guide.blank? OpenStruct.new(enabled: false, label: _('Add Contribution guide'), link: add_contribution_guide_path) @@ -260,7 +263,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated if current_user && can?(current_user, :admin_pipeline, project) && repository.gitlab_ci_yml.blank? && !show_auto_devops_callout OpenStruct.new(enabled: auto_devops_enabled?, label: auto_devops_enabled? ? _('Auto DevOps enabled') : _('Enable Auto DevOps'), - link: project_settings_ci_cd_path(project, anchor: 'js-general-pipeline-settings')) + link: project_settings_ci_cd_path(project, anchor: 'autodevops-settings')) elsif auto_devops_enabled? OpenStruct.new(enabled: true, label: _('Auto DevOps enabled'), diff --git a/app/services/quick_actions/interpret_service.rb b/app/services/quick_actions/interpret_service.rb index 6cc51b6ee1b..0215994b1a7 100644 --- a/app/services/quick_actions/interpret_service.rb +++ b/app/services/quick_actions/interpret_service.rb @@ -138,8 +138,10 @@ module QuickActions 'Remove assignee' end end - explanation do - "Removes #{'assignee'.pluralize(issuable.assignees.size)} #{issuable.assignees.map(&:to_reference).to_sentence}." + explanation do |users = nil| + assignees = issuable.assignees + assignees &= users if users.present? && issuable.allows_multiple_assignees? + "Removes #{'assignee'.pluralize(assignees.size)} #{assignees.map(&:to_reference).to_sentence}." end params do issuable.allows_multiple_assignees? ? '@user1 @user2' : '' @@ -268,6 +270,26 @@ module QuickActions end end + desc 'Copy labels and milestone from other issue or merge request' + explanation do |source_issuable| + "Copy labels and milestone from #{source_issuable.to_reference}." + end + params '#issue | !merge_request' + condition do + issuable.persisted? && + current_user.can?(:"update_#{issuable.to_ability_name}", issuable) + end + parse_params do |issuable_param| + extract_references(issuable_param, :issue).first || + extract_references(issuable_param, :merge_request).first + end + command :copy_metadata do |source_issuable| + if source_issuable.present? && source_issuable.project.id == issuable.project.id + @updates[:add_label_ids] = source_issuable.labels.map(&:id) + @updates[:milestone_id] = source_issuable.milestone.id if source_issuable.milestone + end + end + desc 'Add a todo' explanation 'Adds a todo.' condition do diff --git a/app/uploaders/gitlab_uploader.rb b/app/uploaders/gitlab_uploader.rb index f12f0466a1d..f8a237178d9 100644 --- a/app/uploaders/gitlab_uploader.rb +++ b/app/uploaders/gitlab_uploader.rb @@ -65,6 +65,10 @@ class GitlabUploader < CarrierWave::Uploader::Base !!model end + def local_url + File.join('/', self.class.base_dir, dynamic_segment, filename) + end + private # Designed to be overridden by child uploaders that have a dynamic path diff --git a/app/views/admin/application_settings/_repository_storage.html.haml b/app/views/admin/application_settings/_repository_storage.html.haml index ac31977e1a9..4eebb59110a 100644 --- a/app/views/admin/application_settings/_repository_storage.html.haml +++ b/app/views/admin/application_settings/_repository_storage.html.haml @@ -21,7 +21,7 @@ .help-block Manage repository storage paths. Learn more in the = succeed "." do - = link_to "repository storages documentation", help_page_path("administration/repository_storages") + = link_to "repository storages documentation", help_page_path("administration/repository_storage_paths") .sub-section %h4 Circuit breaker .form-group diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml index b15fe514a08..a066f9f4cca 100644 --- a/app/views/projects/empty.html.haml +++ b/app/views/projects/empty.html.haml @@ -22,7 +22,7 @@ %hr %p - - link_to_auto_devops_settings = link_to(s_('AutoDevOps|enable Auto DevOps (Beta)'), project_settings_ci_cd_path(@project, anchor: 'js-general-pipeline-settings')) + - link_to_auto_devops_settings = link_to(s_('AutoDevOps|enable Auto DevOps (Beta)'), project_settings_ci_cd_path(@project, anchor: 'autodevops-settings')) - link_to_add_kubernetes_cluster = link_to(s_('AutoDevOps|add a Kubernetes cluster'), new_project_cluster_path(@project)) = s_('AutoDevOps|You can automatically build and test your application if you %{link_to_auto_devops_settings} for this project. You can automatically deploy it as well, if you %{link_to_add_kubernetes_cluster}.').html_safe % { link_to_auto_devops_settings: link_to_auto_devops_settings, link_to_add_kubernetes_cluster: link_to_add_kubernetes_cluster } @@ -58,7 +58,9 @@ touch README.md git add README.md git commit -m "add README" - git push -u origin master + - if @project.can_current_user_push_to_default_branch? + %span>< + git push -u origin master %fieldset %h5 Existing folder @@ -69,7 +71,9 @@ git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'clone')} git add . git commit -m "Initial commit" - git push -u origin master + - if @project.can_current_user_push_to_default_branch? + %span>< + git push -u origin master %fieldset %h5 Existing Git repository @@ -78,8 +82,10 @@ cd existing_repo git remote rename origin old-origin git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'clone')} - git push -u origin --all - git push -u origin --tags + - if @project.can_current_user_push_to_default_branch? + %span>< + git push -u origin --all + git push -u origin --tags - if can? current_user, :remove_project, @project .prepend-top-20 diff --git a/app/views/projects/jobs/index.html.haml b/app/views/projects/jobs/index.html.haml index 9963cc93633..fe1c338b634 100644 --- a/app/views/projects/jobs/index.html.haml +++ b/app/views/projects/jobs/index.html.haml @@ -15,7 +15,7 @@ - unless @repository.gitlab_ci_yml = link_to 'Get started with Pipelines', help_page_path('ci/quick_start/README'), class: 'btn btn-info' - = link_to ci_lint_path, class: 'btn btn-default' do + = link_to project_ci_lint_path(@project), class: 'btn btn-default' do %span CI lint .content-list.builds-content-list diff --git a/app/views/projects/pipelines/_with_tabs.html.haml b/app/views/projects/pipelines/_with_tabs.html.haml index 852143ecb2a..218e7338c83 100644 --- a/app/views/projects/pipelines/_with_tabs.html.haml +++ b/app/views/projects/pipelines/_with_tabs.html.haml @@ -26,7 +26,7 @@ %ul - pipeline.yaml_errors.split(",").each do |error| %li= error - You can also test your .gitlab-ci.yml in the #{link_to "Lint", ci_lint_path} + You can also test your .gitlab-ci.yml in the #{link_to "Lint", project_ci_lint_path(@project)} - if pipeline.project.builds_enabled? && !pipeline.ci_yaml_file .bs-callout.bs-callout-warning diff --git a/app/views/projects/registry/repositories/index.html.haml b/app/views/projects/registry/repositories/index.html.haml index 2c80f7c3fa3..76f57320f99 100644 --- a/app/views/projects/registry/repositories/index.html.haml +++ b/app/views/projects/registry/repositories/index.html.haml @@ -29,7 +29,7 @@ docker login #{Gitlab.config.registry.host_port} %br %p - - deploy_token = link_to(_('deploy token'), help_page_path('user/projects/deploy_tokens/index', anchor: 'read-container-registry-images'), target: '_blank') + - deploy_token = link_to(_('deploy token'), help_page_path('user/project/deploy_tokens/index', anchor: 'read-container-registry-images'), target: '_blank') = s_('ContainerRegistry|You can also %{deploy_token} for read-only access to the registry images.').html_safe % { deploy_token: deploy_token } %br %p diff --git a/app/views/projects/settings/ci_cd/_autodevops_form.html.haml b/app/views/projects/settings/ci_cd/_autodevops_form.html.haml new file mode 100644 index 00000000000..7b410101c05 --- /dev/null +++ b/app/views/projects/settings/ci_cd/_autodevops_form.html.haml @@ -0,0 +1,40 @@ +.row.prepend-top-default + .col-lg-12 + = form_for @project, url: project_settings_ci_cd_path(@project) do |f| + = form_errors(@project) + %fieldset.builds-feature + .form-group + - message = auto_devops_warning_message(@project) + - ci_file_formatted = '<code>.gitlab-ci.yml</code>'.html_safe + - if message + %p.settings-message.text-center + = message.html_safe + = f.fields_for :auto_devops_attributes, @auto_devops do |form| + .radio + = form.label :enabled_true do + = form.radio_button :enabled, 'true' + %strong= s_('CICD|Enable Auto DevOps') + %br + = s_('CICD|The Auto DevOps pipeline configuration will be used when there is no %{ci_file} in the project.').html_safe % { ci_file: ci_file_formatted } + + .radio + = form.label :enabled_false do + = form.radio_button :enabled, 'false' + %strong= s_('CICD|Disable Auto DevOps') + %br + = s_('CICD|An explicit %{ci_file} needs to be specified before you can begin using Continuous Integration and Delivery.').html_safe % { ci_file: ci_file_formatted } + + .radio + = form.label :enabled_ do + = form.radio_button :enabled, '' + %strong= s_('CICD|Instance default (%{state})') % { state: "#{Gitlab::CurrentSettings.auto_devops_enabled? ? _('enabled') : _('disabled')}" } + %br + = s_('CICD|Follow the instance default to either have Auto DevOps enabled or disabled when there is no project specific %{ci_file}.').html_safe % { ci_file: ci_file_formatted } + + = form.label :domain, class:"prepend-top-10" do + = _('Domain') + = form.text_field :domain, class: 'form-control', placeholder: 'domain.com' + .help-block + = s_('CICD|You need to specify a domain if you want to use Auto Review Apps and Auto Deploy stages.') + + = f.submit 'Save changes', class: "btn btn-success prepend-top-15" diff --git a/app/views/projects/settings/ci_cd/_form.html.haml b/app/views/projects/settings/ci_cd/_form.html.haml index 20868f9ba5d..80c226ad273 100644 --- a/app/views/projects/settings/ci_cd/_form.html.haml +++ b/app/views/projects/settings/ci_cd/_form.html.haml @@ -3,44 +3,6 @@ = form_for @project, url: project_settings_ci_cd_path(@project) do |f| = form_errors(@project) %fieldset.builds-feature - .form-group - %h5 Auto DevOps (Beta) - %p - Auto DevOps will automatically build, test, and deploy your application based on a predefined Continuous Integration and Delivery configuration. - = link_to 'Learn more about Auto DevOps', help_page_path('topics/autodevops/index.md') - - message = auto_devops_warning_message(@project) - - if message - %p.settings-message.text-center - = message.html_safe - = f.fields_for :auto_devops_attributes, @auto_devops do |form| - .radio - = form.label :enabled_true do - = form.radio_button :enabled, 'true' - %strong Enable Auto DevOps - %br - %span.descr - The Auto DevOps pipeline configuration will be used when there is no <code>.gitlab-ci.yml</code> in the project. - - .radio - = form.label :enabled_false do - = form.radio_button :enabled, 'false' - %strong Disable Auto DevOps - %br - %span.descr - An explicit <code>.gitlab-ci.yml</code> needs to be specified before you can begin using Continuous Integration and Delivery. - - .radio - = form.label :enabled_ do - = form.radio_button :enabled, '' - %strong Instance default (#{Gitlab::CurrentSettings.auto_devops_enabled? ? 'enabled' : 'disabled'}) - %br - %span.descr - Follow the instance default to either have Auto DevOps enabled or disabled when there is no project specific <code>.gitlab-ci.yml</code>. - %p - You need to specify a domain if you want to use Auto Review Apps and Auto Deploy stages. - = form.text_field :domain, class: 'form-control', placeholder: 'domain.com' - - %hr .form-group.append-bottom-default.js-secret-runner-token = f.label :runners_token, "Runner token", class: 'label-light' .form-control.js-secret-value-placeholder diff --git a/app/views/projects/settings/ci_cd/show.html.haml b/app/views/projects/settings/ci_cd/show.html.haml index 09268c9943b..5f596a019f7 100644 --- a/app/views/projects/settings/ci_cd/show.html.haml +++ b/app/views/projects/settings/ci_cd/show.html.haml @@ -12,10 +12,22 @@ %button.btn.js-settings-toggle{ type: 'button' } = expanded ? 'Collapse' : 'Expand' %p - Update your CI/CD configuration, like job timeout or Auto DevOps. + Access your runner token, customize your pipeline configuration, and view your pipeline status and coverage report. .settings-content = render 'form' +%section.settings#autodevops-settings.no-animate{ class: ('expanded' if expanded) } + .settings-header + %h4 + = s_('CICD|Auto DevOps (Beta)') + %button.btn.btn-default.js-settings-toggle{ type: 'button' } + = expanded ? _('Collapse') : _('Expand') + %p + = s_('CICD|Auto DevOps will automatically build, test, and deploy your application based on a predefined Continuous Integration and Delivery configuration.') + = link_to s_('CICD|Learn more about Auto DevOps'), help_page_path('topics/autodevops/index.md') + .settings-content + = render 'autodevops_form' + %section.settings.no-animate{ class: ('expanded' if expanded) } .settings-header %h4 diff --git a/app/views/shared/_auto_devops_callout.html.haml b/app/views/shared/_auto_devops_callout.html.haml index e9ac192f5f7..d3fa324e460 100644 --- a/app/views/shared/_auto_devops_callout.html.haml +++ b/app/views/shared/_auto_devops_callout.html.haml @@ -9,7 +9,7 @@ - link = link_to(s_('AutoDevOps|Auto DevOps documentation'), help_page_path('topics/autodevops/index.md'), target: '_blank', rel: 'noopener noreferrer') = s_('AutoDevOps|Learn more in the %{link_to_documentation}').html_safe % { link_to_documentation: link } .banner-buttons - = link_to s_('AutoDevOps|Enable in settings'), project_settings_ci_cd_path(@project, anchor: 'js-general-pipeline-settings'), class: 'btn js-close-callout' + = link_to s_('AutoDevOps|Enable in settings'), project_settings_ci_cd_path(@project, anchor: 'autodevops-settings'), class: 'btn js-close-callout' %button.btn-transparent.banner-close.close.js-close-callout{ type: 'button', 'aria-label' => 'Dismiss Auto DevOps box' } diff --git a/changelogs/unreleased/10244-add-project-ci-cd-settings.yml b/changelogs/unreleased/10244-add-project-ci-cd-settings.yml new file mode 100644 index 00000000000..89f9a0fe03c --- /dev/null +++ b/changelogs/unreleased/10244-add-project-ci-cd-settings.yml @@ -0,0 +1,5 @@ +--- +title: Introduce new ProjectCiCdSetting model with group_runners_enabled +merge_request: 18144 +author: +type: performance diff --git a/changelogs/unreleased/33697-remove-ujs-action-big-graph.yml b/changelogs/unreleased/33697-remove-ujs-action-big-graph.yml new file mode 100644 index 00000000000..43ce52c1209 --- /dev/null +++ b/changelogs/unreleased/33697-remove-ujs-action-big-graph.yml @@ -0,0 +1,5 @@ +--- +title: Prevent pipeline actions in dropdown to redirct to a new page +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/39345-get-raw-archive.yml b/changelogs/unreleased/39345-get-raw-archive.yml deleted file mode 100644 index fa894432d4c..00000000000 --- a/changelogs/unreleased/39345-get-raw-archive.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Add an API endpoint to download git repository snapshots -merge_request: 18173 -author: -type: added diff --git a/changelogs/unreleased/42936-improve-ns-factory-avoid-duplicates.yml b/changelogs/unreleased/42936-improve-ns-factory-avoid-duplicates.yml new file mode 100644 index 00000000000..54b523b65a1 --- /dev/null +++ b/changelogs/unreleased/42936-improve-ns-factory-avoid-duplicates.yml @@ -0,0 +1,6 @@ +--- +title: Fix discussions API setting created_at for notable in a group or notable in + a project in a group with owners +merge_request: 18464 +author: +type: fixed diff --git a/changelogs/unreleased/43111-controller-projects-mergerequestscontroller-index-executes-more-than-100-sql-queries.yml b/changelogs/unreleased/43111-controller-projects-mergerequestscontroller-index-executes-more-than-100-sql-queries.yml new file mode 100644 index 00000000000..120e319acfb --- /dev/null +++ b/changelogs/unreleased/43111-controller-projects-mergerequestscontroller-index-executes-more-than-100-sql-queries.yml @@ -0,0 +1,5 @@ +--- +title: Reduce queries on merge requests list page for merge requests from forks +merge_request: 18561 +author: +type: performance diff --git a/changelogs/unreleased/43466-make-auto-devops-settings-first-class.yml b/changelogs/unreleased/43466-make-auto-devops-settings-first-class.yml new file mode 100644 index 00000000000..f5c5415159c --- /dev/null +++ b/changelogs/unreleased/43466-make-auto-devops-settings-first-class.yml @@ -0,0 +1,5 @@ +--- +title: Create settings section for autodevops +merge_request: 18321 +author: +type: changed diff --git a/changelogs/unreleased/44447-expose-deploy-token-to-ci-cd.yml b/changelogs/unreleased/44447-expose-deploy-token-to-ci-cd.yml new file mode 100644 index 00000000000..d01b797b1ff --- /dev/null +++ b/changelogs/unreleased/44447-expose-deploy-token-to-ci-cd.yml @@ -0,0 +1,5 @@ +--- +title: Expose Deploy Token data as environment varialbes on CI/CD jobs +merge_request: 18414 +author: +type: added diff --git a/changelogs/unreleased/44775-avatar-on-os-fails-with-cdn.yml b/changelogs/unreleased/44775-avatar-on-os-fails-with-cdn.yml new file mode 100644 index 00000000000..80b5b4a8abe --- /dev/null +++ b/changelogs/unreleased/44775-avatar-on-os-fails-with-cdn.yml @@ -0,0 +1,5 @@ +--- +title: Fixed wrong avatar URL when the avatar is on object storage. +merge_request: 18092 +author: +type: fixed diff --git a/changelogs/unreleased/44981-http-io-trace-with-multi-byte-char.yml b/changelogs/unreleased/44981-http-io-trace-with-multi-byte-char.yml deleted file mode 100644 index 64a17990ebc..00000000000 --- a/changelogs/unreleased/44981-http-io-trace-with-multi-byte-char.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix `Trace::HttpIO` can not render multi-byte chars -merge_request: 18417 -author: -type: fixed diff --git a/changelogs/unreleased/45287-align-icons.yml b/changelogs/unreleased/45287-align-icons.yml deleted file mode 100644 index 0a1cccf9ca6..00000000000 --- a/changelogs/unreleased/45287-align-icons.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Align action icons in pipeline graph -merge_request: -author: -type: fixed diff --git a/changelogs/unreleased/45363-optional-params-on-api-endpoint-produce-invalid-pagination-header-links.yml b/changelogs/unreleased/45363-optional-params-on-api-endpoint-produce-invalid-pagination-header-links.yml deleted file mode 100644 index 963ec893963..00000000000 --- a/changelogs/unreleased/45363-optional-params-on-api-endpoint-produce-invalid-pagination-header-links.yml +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: '[API] Fix URLs in the `Link` header for `GET /projects/:id/repository/contributors` - when no value is passed for `order_by` or `sort`' -merge_request: 18393 -author: -type: fixed diff --git a/changelogs/unreleased/45476-geo-statement-timeout-counting-local-job-artifacts.yml b/changelogs/unreleased/45476-geo-statement-timeout-counting-local-job-artifacts.yml deleted file mode 100644 index 763d3a28200..00000000000 --- a/changelogs/unreleased/45476-geo-statement-timeout-counting-local-job-artifacts.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Add index to file_store on ci_job_artifacts -merge_request: 18444 -author: -type: performance diff --git a/changelogs/unreleased/45507-fix-repository-archive-url.yml b/changelogs/unreleased/45507-fix-repository-archive-url.yml deleted file mode 100644 index 548c9c38689..00000000000 --- a/changelogs/unreleased/45507-fix-repository-archive-url.yml +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: Fix specifying a non-default ref when requesting an archive using the legacy - URL -merge_request: 18468 -author: -type: fixed diff --git a/changelogs/unreleased/45576-fix-create-project-for-user-endpoint.yml b/changelogs/unreleased/45576-fix-create-project-for-user-endpoint.yml new file mode 100644 index 00000000000..12631c75b44 --- /dev/null +++ b/changelogs/unreleased/45576-fix-create-project-for-user-endpoint.yml @@ -0,0 +1,5 @@ +--- +title: Fix project creation for user endpoint when jobs_enabled parameter supplied +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/45625-job-log-shows-no-job-log.yml b/changelogs/unreleased/45625-job-log-shows-no-job-log.yml deleted file mode 100644 index 5f60e818cc1..00000000000 --- a/changelogs/unreleased/45625-job-log-shows-no-job-log.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Removes 'No Job log' message from build trace -merge_request: 18523 -author: -type: fixed diff --git a/changelogs/unreleased/45666-project-ci-lint-links.yml b/changelogs/unreleased/45666-project-ci-lint-links.yml new file mode 100644 index 00000000000..dbf803c0921 --- /dev/null +++ b/changelogs/unreleased/45666-project-ci-lint-links.yml @@ -0,0 +1,5 @@ +--- +title: Update links to /ci/lint with ones to project ci/lint +merge_request: 18539 +author: Takuya Noguchi +type: fixed diff --git a/changelogs/unreleased/4950-unassign-slash-command-preview-fix.yml b/changelogs/unreleased/4950-unassign-slash-command-preview-fix.yml new file mode 100644 index 00000000000..0b8c14ae699 --- /dev/null +++ b/changelogs/unreleased/4950-unassign-slash-command-preview-fix.yml @@ -0,0 +1,5 @@ +--- +title: Fix unassign slash command preview +merge_request: 18447 +author: +type: fixed diff --git a/changelogs/unreleased/ab-45247-project-lookups-validation.yml b/changelogs/unreleased/ab-45247-project-lookups-validation.yml deleted file mode 100644 index cd5ebdebc58..00000000000 --- a/changelogs/unreleased/ab-45247-project-lookups-validation.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Validate project path prior to hitting the database. -merge_request: 18322 -author: -type: performance diff --git a/changelogs/unreleased/add-copy-metadata-command.yml b/changelogs/unreleased/add-copy-metadata-command.yml new file mode 100644 index 00000000000..3bf25ae6ce0 --- /dev/null +++ b/changelogs/unreleased/add-copy-metadata-command.yml @@ -0,0 +1,5 @@ +--- +title: Add Copy metadata quick action +merge_request: 16473 +author: Mateusz Bajorski +type: added diff --git a/changelogs/unreleased/align-project-avatar-on-small-viewports.yml b/changelogs/unreleased/align-project-avatar-on-small-viewports.yml new file mode 100644 index 00000000000..320e7fbc294 --- /dev/null +++ b/changelogs/unreleased/align-project-avatar-on-small-viewports.yml @@ -0,0 +1,5 @@ +--- +title: Align project avatar on small viewports +merge_request: 18513 +author: George Tsiolis +type: changed diff --git a/changelogs/unreleased/blackst0ne-add-missing-changelog-type-to-docs.yml b/changelogs/unreleased/blackst0ne-add-missing-changelog-type-to-docs.yml new file mode 100644 index 00000000000..f8790fa45aa --- /dev/null +++ b/changelogs/unreleased/blackst0ne-add-missing-changelog-type-to-docs.yml @@ -0,0 +1,5 @@ +--- +title: Add missing changelog type to docs +merge_request: 18526 +author: "@blackst0ne" +type: other diff --git a/changelogs/unreleased/bvl-fix-maintainer-push-error.yml b/changelogs/unreleased/bvl-fix-maintainer-push-error.yml new file mode 100644 index 00000000000..66ab8fbf884 --- /dev/null +++ b/changelogs/unreleased/bvl-fix-maintainer-push-error.yml @@ -0,0 +1,5 @@ +--- +title: Fix errors on pushing to an empty repository +merge_request: 18462 +author: +type: fixed diff --git a/changelogs/unreleased/dz-add-2fa-filter-admin-api.yml b/changelogs/unreleased/dz-add-2fa-filter-admin-api.yml new file mode 100644 index 00000000000..df479e69380 --- /dev/null +++ b/changelogs/unreleased/dz-add-2fa-filter-admin-api.yml @@ -0,0 +1,5 @@ +--- +title: Add 2FA filter to users API for admins only +merge_request: 18503 +author: +type: changed diff --git a/changelogs/unreleased/fix-direct-upload-for-old-records.yml b/changelogs/unreleased/fix-direct-upload-for-old-records.yml deleted file mode 100644 index a062b9e73e9..00000000000 --- a/changelogs/unreleased/fix-direct-upload-for-old-records.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix direct_upload when records with null file_store are used -merge_request: -author: -type: fixed diff --git a/changelogs/unreleased/fix-gb-fix-empty-secret-variables.yml b/changelogs/unreleased/fix-gb-fix-empty-secret-variables.yml deleted file mode 100644 index 94010c06a07..00000000000 --- a/changelogs/unreleased/fix-gb-fix-empty-secret-variables.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix a case with secret variables being empty sometimes -merge_request: 18400 -author: -type: fixed diff --git a/changelogs/unreleased/fj-42601-respect-visibility-options.yml b/changelogs/unreleased/fj-42601-respect-visibility-options.yml deleted file mode 100644 index eabb337234c..00000000000 --- a/changelogs/unreleased/fj-42601-respect-visibility-options.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Respect visibility options and description when importing project from template -merge_request: 18473 -author: -type: fixed diff --git a/changelogs/unreleased/fj-45057-improve-ssrf-documentation.yml b/changelogs/unreleased/fj-45057-improve-ssrf-documentation.yml new file mode 100644 index 00000000000..b923f442b26 --- /dev/null +++ b/changelogs/unreleased/fj-45057-improve-ssrf-documentation.yml @@ -0,0 +1,5 @@ +--- +title: Added Webhook SSRF prevention to documentation +merge_request: 18532 +author: +type: other diff --git a/changelogs/unreleased/ide-file-finder.yml b/changelogs/unreleased/ide-file-finder.yml new file mode 100644 index 00000000000..252dd3a30c4 --- /dev/null +++ b/changelogs/unreleased/ide-file-finder.yml @@ -0,0 +1,5 @@ +--- +title: Added fuzzy file finder to web IDE +merge_request: +author: +type: added diff --git a/changelogs/unreleased/ide-mr-changes-alert-box.yml b/changelogs/unreleased/ide-mr-changes-alert-box.yml deleted file mode 100644 index fec2719c2b1..00000000000 --- a/changelogs/unreleased/ide-mr-changes-alert-box.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Removed alert box in IDE when redirecting to new merge request -merge_request: -author: -type: fixed diff --git a/changelogs/unreleased/ide-subgroup-fix.yml b/changelogs/unreleased/ide-subgroup-fix.yml deleted file mode 100644 index 2234c42b4bd..00000000000 --- a/changelogs/unreleased/ide-subgroup-fix.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fixed IDE not loading for sub groups -merge_request: -author: -type: fixed diff --git a/changelogs/unreleased/ide-tree-loading-fix.yml b/changelogs/unreleased/ide-tree-loading-fix.yml deleted file mode 100644 index 2fb43380a48..00000000000 --- a/changelogs/unreleased/ide-tree-loading-fix.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fixed IDE not showing loading state when tree is loading -merge_request: -author: -type: fixed diff --git a/changelogs/unreleased/issue_45463.yml b/changelogs/unreleased/issue_45463.yml new file mode 100644 index 00000000000..a350568d04b --- /dev/null +++ b/changelogs/unreleased/issue_45463.yml @@ -0,0 +1,5 @@ +--- +title: Fix users not seeing labels from private groups when being a member of a child project +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/rd-44635-error-500-when-clicking-ghost-user-or-gitlab-support-bot-in-ui.yml b/changelogs/unreleased/rd-44635-error-500-when-clicking-ghost-user-or-gitlab-support-bot-in-ui.yml new file mode 100644 index 00000000000..a08a75ceda6 --- /dev/null +++ b/changelogs/unreleased/rd-44635-error-500-when-clicking-ghost-user-or-gitlab-support-bot-in-ui.yml @@ -0,0 +1,5 @@ +--- +title: Fix missing namespace for some internal users +merge_request: 18357 +author: +type: fixed diff --git a/changelogs/unreleased/rendering-markdown-multiple-projects.yml b/changelogs/unreleased/rendering-markdown-multiple-projects.yml deleted file mode 100644 index 8685772c089..00000000000 --- a/changelogs/unreleased/rendering-markdown-multiple-projects.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Support Markdown rendering using multiple projects -merge_request: -author: -type: performance diff --git a/changelogs/unreleased/sh-bump-lograge.yml b/changelogs/unreleased/sh-bump-lograge.yml new file mode 100644 index 00000000000..65b15153a35 --- /dev/null +++ b/changelogs/unreleased/sh-bump-lograge.yml @@ -0,0 +1,5 @@ +--- +title: Bump lograge to 0.10.0 and remove monkey patch +merge_request: +author: +type: other diff --git a/changelogs/unreleased/sh-fix-award-emoji-nplus-one-participants.yml b/changelogs/unreleased/sh-fix-award-emoji-nplus-one-participants.yml deleted file mode 100644 index aee26f9824a..00000000000 --- a/changelogs/unreleased/sh-fix-award-emoji-nplus-one-participants.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix N+1 queries when loading participants for a commit note -merge_request: -author: -type: performance diff --git a/changelogs/unreleased/show-runners-description-on-jobs-page.yml b/changelogs/unreleased/show-runners-description-on-jobs-page.yml new file mode 100644 index 00000000000..d9414a3d021 --- /dev/null +++ b/changelogs/unreleased/show-runners-description-on-jobs-page.yml @@ -0,0 +1,5 @@ +--- +title: Show Runner's description on job's page +merge_request: 17321 +author: +type: added diff --git a/changelogs/unreleased/update-doorkeeper-changelog.yml b/changelogs/unreleased/update-doorkeeper-changelog.yml new file mode 100644 index 00000000000..b47bdf4a28d --- /dev/null +++ b/changelogs/unreleased/update-doorkeeper-changelog.yml @@ -0,0 +1,5 @@ +--- +title: Update doorkeeper to 4.3.2 to fix GitLab OAuth authentication +merge_request: 18543 +author: +type: fixed diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index dc7999ac556..575f27d1ea9 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -1,131 +1,4 @@ -# rubocop:disable GitlabSecurity/PublicSend - -require_dependency Rails.root.join('lib/gitlab') # Load Gitlab as soon as possible - -class Settings < Settingslogic - source ENV.fetch('GITLAB_CONFIG') { "#{Rails.root}/config/gitlab.yml" } - namespace Rails.env - - class << self - def gitlab_on_standard_port? - on_standard_port?(gitlab) - end - - def host_without_www(url) - host(url).sub('www.', '') - end - - def build_gitlab_ci_url - custom_port = - if on_standard_port?(gitlab) - nil - else - ":#{gitlab.port}" - end - - [ - gitlab.protocol, - "://", - gitlab.host, - custom_port, - gitlab.relative_url_root - ].join('') - end - - def build_pages_url - base_url(pages).join('') - end - - def build_gitlab_shell_ssh_path_prefix - user_host = "#{gitlab_shell.ssh_user}@#{gitlab_shell.ssh_host}" - - if gitlab_shell.ssh_port != 22 - "ssh://#{user_host}:#{gitlab_shell.ssh_port}/" - else - if gitlab_shell.ssh_host.include? ':' - "[#{user_host}]:" - else - "#{user_host}:" - end - end - end - - def build_base_gitlab_url - base_url(gitlab).join('') - end - - def build_gitlab_url - (base_url(gitlab) + [gitlab.relative_url_root]).join('') - end - - # check that values in `current` (string or integer) is a contant in `modul`. - def verify_constant_array(modul, current, default) - values = default || [] - unless current.nil? - values = [] - current.each do |constant| - values.push(verify_constant(modul, constant, nil)) - end - values.delete_if { |value| value.nil? } - end - - values - end - - # check that `current` (string or integer) is a contant in `modul`. - def verify_constant(modul, current, default) - constant = modul.constants.find { |name| modul.const_get(name) == current } - value = constant.nil? ? default : modul.const_get(constant) - if current.is_a? String - value = modul.const_get(current.upcase) rescue default - end - - value - end - - def absolute(path) - File.expand_path(path, Rails.root) - end - - private - - def base_url(config) - custom_port = on_standard_port?(config) ? nil : ":#{config.port}" - - [ - config.protocol, - "://", - config.host, - custom_port - ] - end - - def on_standard_port?(config) - config.port.to_i == (config.https ? 443 : 80) - end - - # Extract the host part of the given +url+. - def host(url) - url = url.downcase - url = "http://#{url}" unless url.start_with?('http') - - # Get rid of the path so that we don't even have to encode it - url_without_path = url.sub(%r{(https?://[^/]+)/?.*}, '\1') - - URI.parse(url_without_path).host - end - - # Runs every minute in a random ten-minute period on Sundays, to balance the - # load on the server receiving these pings. The usage ping is safe to run - # multiple times because of a 24 hour exclusive lock. - def cron_for_usage_ping - hour = rand(24) - minute = rand(6) - - "#{minute}0-#{minute}9 #{hour} * * 0" - end - end -end +require_relative '../settings' # Default settings Settings['ldap'] ||= Settingslogic.new({}) diff --git a/config/initializers/2_app.rb b/config/initializers/2_app.rb deleted file mode 100644 index bd74f90e7d2..00000000000 --- a/config/initializers/2_app.rb +++ /dev/null @@ -1,8 +0,0 @@ -module Gitlab - def self.config - Settings - end - - VERSION = File.read(Rails.root.join("VERSION")).strip.freeze - REVISION = Gitlab::Popen.popen(%W(#{config.git.bin_path} log --pretty=format:%h -n 1)).first.chomp.freeze -end diff --git a/config/initializers/2_gitlab.rb b/config/initializers/2_gitlab.rb new file mode 100644 index 00000000000..1d2ab606a63 --- /dev/null +++ b/config/initializers/2_gitlab.rb @@ -0,0 +1 @@ +require_relative '../../lib/gitlab' diff --git a/config/initializers/deprecations.rb b/config/initializers/deprecations.rb index f3f47b2ccf0..2476ea9e38a 100644 --- a/config/initializers/deprecations.rb +++ b/config/initializers/deprecations.rb @@ -1,5 +1,5 @@ deprecator = ActiveSupport::Deprecation.new('11.0', 'GitLab') -if Gitlab.com? || Rails.env.development? +if Gitlab.dev_env_or_com? ActiveSupport::Deprecation.deprecate_methods(Gitlab::GitalyClient::StorageSettings, :legacy_disk_path, deprecator: deprecator) end diff --git a/config/initializers/lograge.rb b/config/initializers/lograge.rb index 49fdd23064c..114c1cb512f 100644 --- a/config/initializers/lograge.rb +++ b/config/initializers/lograge.rb @@ -1,21 +1,3 @@ -# Monkey patch lograge until https://github.com/roidrage/lograge/pull/241 is released -module Lograge - class RequestLogSubscriber < ActiveSupport::LogSubscriber - def strip_query_string(path) - index = path.index('?') - index ? path[0, index] : path - end - - def extract_location - location = Thread.current[:lograge_location] - return {} unless location - - Thread.current[:lograge_location] = nil - { location: strip_query_string(location) } - end - end -end - # Only use Lograge for Rails unless Sidekiq.server? filename = File.join(Rails.root, 'log', "#{Rails.env}_json.log") diff --git a/config/karma.config.js b/config/karma.config.js index 61f02294157..691cda98861 100644 --- a/config/karma.config.js +++ b/config/karma.config.js @@ -33,7 +33,7 @@ webpackConfig.plugins.push( }) ); -webpackConfig.devtool = 'cheap-inline-source-map'; +webpackConfig.devtool = process.env.BABEL_ENV !== 'coverage' && 'cheap-inline-source-map'; // Karma configuration module.exports = function(config) { diff --git a/config/routes/user.rb b/config/routes/user.rb index 57fb37530bb..f8677693fab 100644 --- a/config/routes/user.rb +++ b/config/routes/user.rb @@ -1,3 +1,21 @@ +# Allows individual providers to be directed to a chosen controller +# Call from inside devise_scope +def override_omniauth(provider, controller, path_prefix = '/users/auth') + match "#{path_prefix}/#{provider}/callback", + to: "#{controller}##{provider}", + as: "#{provider}_omniauth_callback", + via: [:get, :post] +end + +# Use custom controller for LDAP omniauth callback +if Gitlab::Auth::LDAP::Config.enabled? + devise_scope :user do + Gitlab::Auth::LDAP::Config.available_servers.each do |server| + override_omniauth(server['provider_name'], 'ldap/omniauth_callbacks') + end + end +end + devise_for :users, controllers: { omniauth_callbacks: :omniauth_callbacks, registrations: :registrations, passwords: :passwords, diff --git a/config/settings.rb b/config/settings.rb new file mode 100644 index 00000000000..69d637761ea --- /dev/null +++ b/config/settings.rb @@ -0,0 +1,126 @@ +require 'settingslogic' + +class Settings < Settingslogic + source ENV.fetch('GITLAB_CONFIG') { Pathname.new(File.expand_path('..', __dir__)).join('config/gitlab.yml') } + namespace ENV.fetch('GITLAB_ENV') { Rails.env } + + class << self + def gitlab_on_standard_port? + on_standard_port?(gitlab) + end + + def host_without_www(url) + host(url).sub('www.', '') + end + + def build_gitlab_ci_url + custom_port = + if on_standard_port?(gitlab) + nil + else + ":#{gitlab.port}" + end + + [ + gitlab.protocol, + "://", + gitlab.host, + custom_port, + gitlab.relative_url_root + ].join('') + end + + def build_pages_url + base_url(pages).join('') + end + + def build_gitlab_shell_ssh_path_prefix + user_host = "#{gitlab_shell.ssh_user}@#{gitlab_shell.ssh_host}" + + if gitlab_shell.ssh_port != 22 + "ssh://#{user_host}:#{gitlab_shell.ssh_port}/" + else + if gitlab_shell.ssh_host.include? ':' + "[#{user_host}]:" + else + "#{user_host}:" + end + end + end + + def build_base_gitlab_url + base_url(gitlab).join('') + end + + def build_gitlab_url + (base_url(gitlab) + [gitlab.relative_url_root]).join('') + end + + # check that values in `current` (string or integer) is a contant in `modul`. + def verify_constant_array(modul, current, default) + values = default || [] + unless current.nil? + values = [] + current.each do |constant| + values.push(verify_constant(modul, constant, nil)) + end + values.delete_if { |value| value.nil? } + end + + values + end + + # check that `current` (string or integer) is a contant in `modul`. + def verify_constant(modul, current, default) + constant = modul.constants.find { |name| modul.const_get(name) == current } + value = constant.nil? ? default : modul.const_get(constant) + if current.is_a? String + value = modul.const_get(current.upcase) rescue default + end + + value + end + + def absolute(path) + File.expand_path(path, Rails.root) + end + + private + + def base_url(config) + custom_port = on_standard_port?(config) ? nil : ":#{config.port}" + + [ + config.protocol, + "://", + config.host, + custom_port + ] + end + + def on_standard_port?(config) + config.port.to_i == (config.https ? 443 : 80) + end + + # Extract the host part of the given +url+. + def host(url) + url = url.downcase + url = "http://#{url}" unless url.start_with?('http') + + # Get rid of the path so that we don't even have to encode it + url_without_path = url.sub(%r{(https?://[^/]+)/?.*}, '\1') + + URI.parse(url_without_path).host + end + + # Runs every minute in a random ten-minute period on Sundays, to balance the + # load on the server receiving these pings. The usage ping is safe to run + # multiple times because of a 24 hour exclusive lock. + def cron_for_usage_ping + hour = rand(24) + minute = rand(6) + + "#{minute}0-#{minute}9 #{hour} * * 0" + end + end +end diff --git a/db/fixtures/development/17_cycle_analytics.rb b/db/fixtures/development/17_cycle_analytics.rb index d7be6f5950f..7b9a4bad449 100644 --- a/db/fixtures/development/17_cycle_analytics.rb +++ b/db/fixtures/development/17_cycle_analytics.rb @@ -1,5 +1,5 @@ require './spec/support/sidekiq' -require './spec/support/test_env' +require './spec/support/helpers/test_env' class Gitlab::Seeder::CycleAnalytics def initialize(project, perf: false) diff --git a/db/migrate/20180403035759_create_project_ci_cd_settings.rb b/db/migrate/20180403035759_create_project_ci_cd_settings.rb new file mode 100644 index 00000000000..06856af6204 --- /dev/null +++ b/db/migrate/20180403035759_create_project_ci_cd_settings.rb @@ -0,0 +1,68 @@ +class CreateProjectCiCdSettings < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + unless table_exists?(:project_ci_cd_settings) + create_table(:project_ci_cd_settings) do |t| + t.integer(:project_id, null: false) + t.boolean(:group_runners_enabled, default: true, null: false) + end + end + + disable_statement_timeout + + # This particular INSERT will take between 10 and 20 seconds. + execute 'INSERT INTO project_ci_cd_settings (project_id) SELECT id FROM projects' + + # We add the index and foreign key separately so the above INSERT statement + # takes as little time as possible. + add_concurrent_index(:project_ci_cd_settings, :project_id, unique: true) + + add_foreign_key_with_retry + end + + def down + drop_table :project_ci_cd_settings + end + + def add_foreign_key_with_retry + if Gitlab::Database.mysql? + # When using MySQL we don't support online upgrades, thus projects can't + # be deleted while we are running this migration. + return add_project_id_foreign_key + end + + # Between the initial INSERT and the addition of the foreign key some + # projects may have been removed, leaving orphaned rows in our new settings + # table. + loop do + remove_orphaned_settings + + begin + add_project_id_foreign_key + break + rescue ActiveRecord::InvalidForeignKey + say 'project_ci_cd_settings contains some orphaned rows, retrying...' + end + end + end + + def add_project_id_foreign_key + add_concurrent_foreign_key(:project_ci_cd_settings, :projects, column: :project_id) + end + + def remove_orphaned_settings + execute <<~SQL + DELETE FROM project_ci_cd_settings + WHERE NOT EXISTS ( + SELECT 1 + FROM projects + WHERE projects.id = project_ci_cd_settings.project_id + ) + SQL + end +end diff --git a/db/migrate/20180413022611_create_missing_namespace_for_internal_users.rb b/db/migrate/20180413022611_create_missing_namespace_for_internal_users.rb new file mode 100644 index 00000000000..8fc558be733 --- /dev/null +++ b/db/migrate/20180413022611_create_missing_namespace_for_internal_users.rb @@ -0,0 +1,66 @@ +class CreateMissingNamespaceForInternalUsers < ActiveRecord::Migration + DOWNTIME = false + + def up + connection.exec_query(users_query.to_sql).rows.each do |id, username| + create_namespace(id, username) + # When testing locally I've noticed that these internal users are missing + # the notification email, for more details visit the below link: + # https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/18357#note_68327560 + set_notification_email(id) + end + end + + def down + # no-op + end + + private + + def users + @users ||= Arel::Table.new(:users) + end + + def namespaces + @namespaces ||= Arel::Table.new(:namespaces) + end + + def users_query + condition = users[:ghost].eq(true) + + if column_exists?(:users, :support_bot) + condition = condition.or(users[:support_bot].eq(true)) + end + + users.join(namespaces, Arel::Nodes::OuterJoin) + .on(namespaces[:type].eq(nil).and(namespaces[:owner_id].eq(users[:id]))) + .where(namespaces[:owner_id].eq(nil)) + .where(condition) + .project(users[:id], users[:username]) + end + + def create_namespace(user_id, username) + path = Uniquify.new.string(username) do |str| + query = "SELECT id FROM namespaces WHERE parent_id IS NULL AND path='#{str}' LIMIT 1" + connection.exec_query(query).present? + end + + insert_query = "INSERT INTO namespaces(owner_id, path, name) VALUES(#{user_id}, '#{path}', '#{path}')" + namespace_id = connection.insert_sql(insert_query) + + create_route(namespace_id) + end + + def create_route(namespace_id) + return unless namespace_id + + row = connection.exec_query("SELECT id, path FROM namespaces WHERE id=#{namespace_id}").first + id, path = row.values_at('id', 'path') + + execute("INSERT INTO routes(source_id, source_type, path, name) VALUES(#{id}, 'Namespace', '#{path}', '#{path}')") + end + + def set_notification_email(user_id) + execute "UPDATE users SET notification_email = email WHERE notification_email IS NULL AND id = #{user_id}" + end +end diff --git a/db/post_migrate/20180409170809_populate_missing_project_ci_cd_settings.rb b/db/post_migrate/20180409170809_populate_missing_project_ci_cd_settings.rb new file mode 100644 index 00000000000..3b0fdb3aeea --- /dev/null +++ b/db/post_migrate/20180409170809_populate_missing_project_ci_cd_settings.rb @@ -0,0 +1,34 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class PopulateMissingProjectCiCdSettings < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + # MySQL does not support online upgrades, thus there can't be any missing + # rows. + return if Gitlab::Database.mysql? + + # Projects created after the initial migration but before the code started + # using ProjectCiCdSetting won't have a corresponding row in + # project_ci_cd_settings, so let's fix that. + execute <<~SQL + INSERT INTO project_ci_cd_settings (project_id) + SELECT id + FROM projects + WHERE NOT EXISTS ( + SELECT 1 + FROM project_ci_cd_settings + WHERE project_ci_cd_settings.project_id = projects.id + ) + SQL + end + + def down + # There's nothing to revert for this migration. + end +end diff --git a/db/schema.rb b/db/schema.rb index df621956c80..dc4607d0876 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -1436,6 +1436,13 @@ ActiveRecord::Schema.define(version: 20180418053107) do add_index "project_auto_devops", ["project_id"], name: "index_project_auto_devops_on_project_id", unique: true, using: :btree + create_table "project_ci_cd_settings", force: :cascade do |t| + t.integer "project_id", null: false + t.boolean "group_runners_enabled", default: true, null: false + end + + add_index "project_ci_cd_settings", ["project_id"], name: "index_project_ci_cd_settings_on_project_id", unique: true, using: :btree + create_table "project_custom_attributes", force: :cascade do |t| t.datetime "created_at", null: false t.datetime "updated_at", null: false @@ -2162,6 +2169,7 @@ ActiveRecord::Schema.define(version: 20180418053107) do add_foreign_key "project_authorizations", "projects", on_delete: :cascade add_foreign_key "project_authorizations", "users", on_delete: :cascade add_foreign_key "project_auto_devops", "projects", on_delete: :cascade + add_foreign_key "project_ci_cd_settings", "projects", name: "fk_24c15d2f2e", on_delete: :cascade add_foreign_key "project_custom_attributes", "projects", on_delete: :cascade add_foreign_key "project_deploy_tokens", "deploy_tokens", on_delete: :cascade add_foreign_key "project_deploy_tokens", "projects", on_delete: :cascade diff --git a/doc/administration/high_availability/nfs.md b/doc/administration/high_availability/nfs.md index d8928a7fe4c..ad8ffc46559 100644 --- a/doc/administration/high_availability/nfs.md +++ b/doc/administration/high_availability/nfs.md @@ -75,43 +75,33 @@ Notice several options that you should consider using: | `nobootwait` | Don't halt boot process waiting for this mount to become available | `lookupcache=positive` | Tells the NFS client to honor `positive` cache results but invalidates any `negative` cache results. Negative cache results cause problems with Git. Specifically, a `git push` can fail to register uniformly across all NFS clients. The negative cache causes the clients to 'remember' that the files did not exist previously. -## Mount locations +## A single NFS mount -When using default Omnibus configuration you will need to share 5 data locations -between all GitLab cluster nodes. No other locations should be shared. The -following are the 5 locations you need to mount: - -| Location | Description | Default configuration | -| -------- | ----------- | --------------------- | -| `/var/opt/gitlab/git-data` | Git repository data. This will account for a large portion of your data | `git_data_dirs({"default" => "/var/opt/gitlab/git-data"})` -| `/var/opt/gitlab/.ssh` | SSH `authorized_keys` file and keys used to import repositories from some other Git services | `user['home'] = '/var/opt/gitlab/'` -| `/var/opt/gitlab/gitlab-rails/uploads` | User uploaded attachments | `gitlab_rails['uploads_directory'] = '/var/opt/gitlab/gitlab-rails/uploads'` -| `/var/opt/gitlab/gitlab-rails/shared` | Build artifacts, GitLab Pages, LFS objects, temp files, etc. If you're using LFS this may also account for a large portion of your data | `gitlab_rails['shared_path'] = '/var/opt/gitlab/gitlab-rails/shared'` -| `/var/opt/gitlab/gitlab-ci/builds` | GitLab CI build traces | `gitlab_ci['builds_directory'] = '/var/opt/gitlab/gitlab-ci/builds'` +It's recommended to nest all gitlab data dirs within a mount, that allows automatic +restore of backups without manually moving existing data. -Other GitLab directories should not be shared between nodes. They contain -node-specific files and GitLab code that does not need to be shared. To ship -logs to a central location consider using remote syslog. GitLab Omnibus packages -provide configuration for [UDP log shipping][udp-log-shipping]. - -### Consolidating mount points - -If you don't want to configure 5-6 different NFS mount points, you have a few -alternative options. +``` +mountpoint +└── gitlab-data + ├── builds + ├── git-data + ├── home-git + ├── shared + └── uploads +``` -#### Change default file locations +To do so, we'll need to configure Omnibus with the paths to each directory nested +in the mount point as follows: -Omnibus allows you to configure the file locations. With custom configuration -you can specify just one main mountpoint and have all of these locations -as subdirectories. Mount `/gitlab-data` then use the following Omnibus +Mount `/gitlab-nfs` then use the following Omnibus configuration to move each data location to a subdirectory: ```ruby -git_data_dirs({"default" => "/gitlab-data/git-data"}) -user['home'] = '/gitlab-data/home' -gitlab_rails['uploads_directory'] = '/gitlab-data/uploads' -gitlab_rails['shared_path'] = '/gitlab-data/shared' -gitlab_ci['builds_directory'] = '/gitlab-data/builds' +git_data_dirs({"default" => "/gitlab-nfs/gitlab-data/git-data"}) +user['home'] = '/gitlab-nfs/gitlab-data/home' +gitlab_rails['uploads_directory'] = '/gitlab-nfs/gitlab-data/uploads' +gitlab_rails['shared_path'] = '/gitlab-nfs/gitlab-data/shared' +gitlab_ci['builds_directory'] = '/gitlab-nfs/gitlab-data/builds' ``` To move the `git` home directory, all GitLab services must be stopped. Run @@ -122,22 +112,52 @@ Run `sudo gitlab-ctl reconfigure` to start using the central location. Please be aware that if you had existing data you will need to manually copy/rsync it to these new locations and then restart GitLab. -#### Bind mounts +## Bind mounts + +Alternatively to changing the configuration in Omnibus, bind mounts can be used +to store the data on an NFS mount. Bind mounts provide a way to specify just one NFS mount and then bind the default GitLab data locations to the NFS mount. Start by defining your single NFS mount point as you normally would in `/etc/fstab`. Let's assume your -NFS mount point is `/gitlab-data`. Then, add the following bind mounts in +NFS mount point is `/gitlab-nfs`. Then, add the following bind mounts in `/etc/fstab`: ```bash -/gitlab-data/git-data /var/opt/gitlab/git-data none bind 0 0 -/gitlab-data/.ssh /var/opt/gitlab/.ssh none bind 0 0 -/gitlab-data/uploads /var/opt/gitlab/gitlab-rails/uploads none bind 0 0 -/gitlab-data/shared /var/opt/gitlab/gitlab-rails/shared none bind 0 0 -/gitlab-data/builds /var/opt/gitlab/gitlab-ci/builds none bind 0 0 +/gitlab-nfs/gitlab-data/git-data /var/opt/gitlab/git-data none bind 0 0 +/gitlab-nfs/gitlab-data/.ssh /var/opt/gitlab/.ssh none bind 0 0 +/gitlab-nfs/gitlab-data/uploads /var/opt/gitlab/gitlab-rails/uploads none bind 0 0 +/gitlab-nfs/gitlab-data/shared /var/opt/gitlab/gitlab-rails/shared none bind 0 0 +/gitlab-nfs/gitlab-data/builds /var/opt/gitlab/gitlab-ci/builds none bind 0 0 ``` +Using bind mounts will require manually making sure the data directories +are empty before attempting a restore. Read more about the +[restore prerequisites](../../raketasks/backup_restore.md). + +## Multiple NFS mounts + +When using default Omnibus configuration you will need to share 5 data locations +between all GitLab cluster nodes. No other locations should be shared. The +following are the 5 locations need to be shared: + +| Location | Description | Default configuration | +| -------- | ----------- | --------------------- | +| `/var/opt/gitlab/git-data` | Git repository data. This will account for a large portion of your data | `git_data_dirs({"default" => "/var/opt/gitlab/git-data"})` +| `/var/opt/gitlab/.ssh` | SSH `authorized_keys` file and keys used to import repositories from some other Git services | `user['home'] = '/var/opt/gitlab/'` +| `/var/opt/gitlab/gitlab-rails/uploads` | User uploaded attachments | `gitlab_rails['uploads_directory'] = '/var/opt/gitlab/gitlab-rails/uploads'` +| `/var/opt/gitlab/gitlab-rails/shared` | Build artifacts, GitLab Pages, LFS objects, temp files, etc. If you're using LFS this may also account for a large portion of your data | `gitlab_rails['shared_path'] = '/var/opt/gitlab/gitlab-rails/shared'` +| `/var/opt/gitlab/gitlab-ci/builds` | GitLab CI build traces | `gitlab_ci['builds_directory'] = '/var/opt/gitlab/gitlab-ci/builds'` + +Other GitLab directories should not be shared between nodes. They contain +node-specific files and GitLab code that does not need to be shared. To ship +logs to a central location consider using remote syslog. GitLab Omnibus packages +provide configuration for [UDP log shipping][udp-log-shipping]. + +Having multiple NFS mounts will require manually making sure the data directories +are empty before attempting a restore. Read more about the +[restore prerequisites](../../raketasks/backup_restore.md). + --- Read more on high-availability configuration: diff --git a/doc/api/project_import_export.md b/doc/api/project_import_export.md index d8f61852b11..085437c801a 100644 --- a/doc/api/project_import_export.md +++ b/doc/api/project_import_export.md @@ -112,13 +112,13 @@ POST /projects/import | `file` | string | yes | The file to be uploaded | | `path` | string | yes | Name and path for new project | | `overwrite` | boolean | no | If there is a project with the same path the import will overwrite it. Default to false | -| `override_params` | Hash | no | Supports all fields defined in the [Project API](projects.md)] | +| `override_params` | Hash | no | Supports all fields defined in the [Project API](projects.md) | -The override params passed will take precendence over all values defined inside the export file. +The override params passed will take precedence over all values defined inside the export file. -To upload a file from your filesystem, use the `--form` argument. This causes +To upload a file from your file system, use the `--form` argument. This causes cURL to post data using the header `Content-Type: multipart/form-data`. -The `file=` parameter must point to a file on your filesystem and be preceded +The `file=` parameter must point to a file on your file system and be preceded by `@`. For example: ```console diff --git a/doc/api/users.md b/doc/api/users.md index a4447e32908..ca5afa04687 100644 --- a/doc/api/users.md +++ b/doc/api/users.md @@ -55,6 +55,7 @@ GET /users | --------- | ---- | -------- | ----------- | | `order_by` | string | no | Return projects ordered by `id`, `name`, `username`, `created_at`, or `updated_at` fields. Default is `id` | | `sort` | string | no | Return projects sorted in `asc` or `desc` order. Default is `desc` | +| `two_factor` | string | no | Filter users by Two-factor authentication. Filter values are `enabled` or `disabled`. By default it returns all users | ```json [ diff --git a/doc/ci/environments.md b/doc/ci/environments.md index b3d9f0bc96c..517e25f00f7 100644 --- a/doc/ci/environments.md +++ b/doc/ci/environments.md @@ -260,6 +260,8 @@ are unsupported in environment name context: - `CI_REGISTRY_PASSWORD` - `CI_REPOSITORY_URL` - `CI_ENVIRONMENT_URL` +- `CI_DEPLOY_USER` +- `CI_DEPLOY_PASSWORD` GitLab Runner exposes various [environment variables][variables] when a job runs, and as such, you can use them as environment names. Let's add another job in diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md index 4a504a98902..f0df8b96cab 100644 --- a/doc/ci/variables/README.md +++ b/doc/ci/variables/README.md @@ -87,6 +87,8 @@ future GitLab releases.** | **GITLAB_USER_LOGIN** | 10.0 | all | The login username of the user who started the job | | **GITLAB_USER_NAME** | 10.0 | all | The real name of the user who started the job | | **RESTORE_CACHE_ATTEMPTS** | 8.15 | 1.9 | Number of attempts to restore the cache running a job | +| **CI_DEPLOY_USER** | 10.8 | all | Authentication username of the [GitLab Deploy Token][gitlab-deploy-token], only present if the Project has one related.| +| **CI_DEPLOY_PASSWORD** | 10.8 | all | Authentication password of the [GitLab Deploy Token][gitlab-deploy-token], only present if the Project has one related.| ## 9.0 Renaming @@ -546,6 +548,8 @@ You can find a full list of unsupported variables below: - `CI_REGISTRY_PASSWORD` - `CI_REPOSITORY_URL` - `CI_ENVIRONMENT_URL` +- `CI_DEPLOY_USER` +- `CI_DEPLOY_PASSWORD` These variables are also not supported in a contex of a [dynamic environment name][dynamic-environments]. @@ -562,3 +566,4 @@ These variables are also not supported in a contex of a [subgroups]: ../../user/group/subgroups/index.md [builds-policies]: ../yaml/README.md#only-and-except-complex [dynamic-environments]: ../environments.md#dynamic-environments +[gitlab-deploy-token]: ../../user/project/deploy_tokens/index.md#gitlab-deploy-token diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index 623e7d662a3..fb6d9826d08 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -1573,7 +1573,7 @@ capitalization, the commit will be created but the pipeline will be skipped. Each instance of GitLab CI has an embedded debug tool called Lint, which validates the content of your `.gitlab-ci.yml` files. You can find the Lint under the page `ci/lint` of your -project namespace (e.g, `http://gitlab-example.com/gitlab-org/project-123/ci/lint`) +project namespace (e.g, `http://gitlab-example.com/gitlab-org/project-123/-/ci/lint`) ## Using reserved keywords diff --git a/doc/development/changelog.md b/doc/development/changelog.md index d5a4acff481..a9fa5ae834f 100644 --- a/doc/development/changelog.md +++ b/doc/development/changelog.md @@ -22,7 +22,7 @@ The `merge_request` value is a reference to a merge request that adds this entry, and the `author` key is used to give attribution to community contributors. **Both are optional**. The `type` field maps the category of the change, -valid options are: added, fixed, changed, deprecated, removed, security, other. **Type field is mandatory**. +valid options are: added, fixed, changed, deprecated, removed, security, performance, other. **Type field is mandatory**. Community contributors and core team members are encouraged to add their name to the `author` field. GitLab team members **should not**. diff --git a/doc/development/testing_guide/best_practices.md b/doc/development/testing_guide/best_practices.md index df80cd9f584..7b32e0a47e1 100644 --- a/doc/development/testing_guide/best_practices.md +++ b/doc/development/testing_guide/best_practices.md @@ -63,6 +63,8 @@ writing one](testing_levels.md#consider-not-writing-a-system-test)! Sometimes you may need to debug Capybara tests by observing browser behavior. +#### Live debug + You can pause Capybara and view the website on the browser by using the `live_debug` method in your spec. The current page will be automatically opened in your default browser. @@ -90,6 +92,54 @@ Finished in 34.51 seconds (files took 0.76702 seconds to load) Note: `live_debug` only works on javascript enabled specs. +#### Run `:js` spec in a visible browser + +Run the spec with `CHROME_HEADLESS=0`, e.g.: + +``` +CHROME_HEADLESS=0 bundle exec rspec some_spec.rb +``` + +The test will go by quickly, but this will give you an idea of what's happening. + +You can also add `byebug` or `binding.pry` to pause execution and step through +the test. + +#### Screenshots + +We use the `capybara-screenshot` gem to automatically take a screenshot on +failure. In CI you can download these files as job artifacts. + +Also, you can manually take screenshots at any point in a test by adding the +methods below. Be sure to remove them when they are no longer needed! See +https://github.com/mattheworiordan/capybara-screenshot#manual-screenshots for +more. + +Add `screenshot_and_save_page` in a `:js` spec to screenshot what Capybara +"sees", and save the page source. + +Add `screenshot_and_open_image` in a `:js` spec to screenshot what Capybara +"sees", and automatically open the image. + +### Fast unit tests + +Some classes are well-isolated from Rails and you should be able to test them +without the overhead added by the Rails environment and Bundler's `:default` +group's gem loading. In these cases, you can `require 'fast_spec_helper'` +instead of `require 'spec_helper'` in your test file, and your test should run +really fast since: + +- Gems loading is skipped +- Rails app boot is skipped +- gitlab-shell and Gitaly setup are skipped +- Test repositories setup are skipped + +Note that in some cases, you might have to add some `require_dependency 'foo'` +in your file under test since Rails autoloading is not available in these cases. + +This shouldn't be a problem since explicitely listing dependencies should be +considered a good practice anyway. + ### `let` variables GitLab's RSpec suite has made extensive use of `let` variables to reduce @@ -281,14 +331,13 @@ All fixtures should be be placed under `spec/fixtures/`. RSpec config files are files that change the RSpec config (i.e. `RSpec.configure do |config|` blocks). They should be placed under -`spec/support/config/`. +`spec/support/`. Each file should be related to a specific domain, e.g. -`spec/support/config/capybara.rb`, `spec/support/config/carrierwave.rb`, etc. +`spec/support/capybara.rb`, `spec/support/carrierwave.rb`, etc. -Helpers can be included in the `spec/support/config/rspec.rb` file. If a -helpers module applies only to a certain kind of specs, it should add modifiers -to the `config.include` call. For instance if +If a helpers module applies only to a certain kind of specs, it should add +modifiers to the `config.include` call. For instance if `spec/support/helpers/cycle_analytics_helpers.rb` applies to `:lib` and `type: :model` specs only, you would write the following: @@ -299,6 +348,14 @@ RSpec.configure do |config| end ``` +If a config file only consists of `config.include`, you can add these +`config.include` directly in `spec/spec_helper.rb`. + +For very generic helpers, consider including them in the `spec/support/rspec.rb` +file which is used by the `spec/fast_spec_helper.rb` file. See +[Fast unit tests](#fast-unit-tests) for more details about the +`spec/fast_spec_helper.rb` file. + --- [Return to Testing documentation](index.md) diff --git a/doc/development/testing_guide/frontend_testing.md b/doc/development/testing_guide/frontend_testing.md index 0a6f402d5d2..af477f5ab99 100644 --- a/doc/development/testing_guide/frontend_testing.md +++ b/doc/development/testing_guide/frontend_testing.md @@ -126,13 +126,51 @@ it('tests a promise rejection', (done) => { }); ``` -#### Stubbing +#### Stubbing and Mocking -For unit tests, you should stub methods that are unrelated to the current unit you are testing. -If you need to use a prototype method, instantiate an instance of the class and call it there instead of mocking the instance completely. +Jasmine provides useful helpers `spyOn`, `spyOnProperty`, `jasmine.createSpy`, +and `jasmine.createSpyObject` to facilitate replacing methods with dummy +placeholders, and recalling when they are called and the arguments that are +passed to them. These tools should be used liberally, to test for expected +behavior, to mock responses, and to block unwanted side effects (such as a +method that would generate a network request or alter `window.location`). The +documentation for these methods can be found in the [jasmine introduction page](https://jasmine.github.io/2.0/introduction.html#section-Spies). -For integration tests, you should stub methods that will effect the stability of the test if they -execute their original behaviour. i.e. Network requests. +Sometimes you may need to spy on a method that is directly imported by another +module. GitLab has a custom `spyOnDependency` method which utilizes +[babel-plugin-rewire](https://github.com/speedskater/babel-plugin-rewire) to +achieve this. It can be used like so: + +```javascript +// my_module.js +import { visitUrl } from '~/lib/utils/url_utility'; + +export default function doSomething() { + visitUrl('/foo/bar'); +} + +// my_module_spec.js +import doSomething from '~/my_module'; + +describe('my_module', () => { + it('does something', () => { + const visitUrl = spyOnDependency(doSomething, 'visitUrl'); + + doSomething(); + expect(visitUrl).toHaveBeenCalledWith('/foo/bar'); + }); +}); +``` + +Unlike `spyOn`, `spyOnDependency` expects its first parameter to be the default +export of a module who's import you want to stub, rather than an object which +contains a method you wish to stub (if the module does not have a default +export, one is be generated by the babel plugin). The second parameter is the +name of the import you wish to change. The result of the function is a Spy +object which can be treated like any other jasmine spy object. + +Further documentation on the babel rewire pluign API can be found on +[its repository Readme doc](https://github.com/speedskater/babel-plugin-rewire#babel-plugin-rewire). ### Vue.js unit tests diff --git a/doc/development/testing_guide/testing_rake_tasks.md b/doc/development/testing_guide/testing_rake_tasks.md index 60163f1a230..db8ca87e9f8 100644 --- a/doc/development/testing_guide/testing_rake_tasks.md +++ b/doc/development/testing_guide/testing_rake_tasks.md @@ -9,7 +9,7 @@ At a minimum, requiring the Rake helper will redirect `stdout`, include the runtime task helpers, and include the `RakeHelpers` Spec support module. The `RakeHelpers` module exposes a `run_rake_task(<task>)` method to make -executing tasks simple. See `spec/support/rake_helpers.rb` for all available +executing tasks simple. See `spec/support/helpers/rake_helpers.rb` for all available methods. Example: diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md index bbd2d214fe4..785cc32d590 100644 --- a/doc/raketasks/backup_restore.md +++ b/doc/raketasks/backup_restore.md @@ -498,6 +498,13 @@ more of the following options: Read what the [backup timestamp is about](#backup-timestamp). - `force=yes` - Does not ask if the authorized_keys file should get regenerated and assumes 'yes' for warning that database tables will be removed. +If you are restoring into directories that are mountpoints you will need to make +sure these directories are empty before attempting a restore. Otherwise GitLab +will attempt to move these directories before restoring the new data and this +would cause an error. + +Read more on [configuring NFS mounts](../administration/high_availability/nfs.md) + ### Restore for installation from source ``` diff --git a/doc/security/img/outbound_requests_section.png b/doc/security/img/outbound_requests_section.png Binary files differnew file mode 100644 index 00000000000..95c9c6ee771 --- /dev/null +++ b/doc/security/img/outbound_requests_section.png diff --git a/doc/security/webhooks.md b/doc/security/webhooks.md index faabc53ce72..a573445ab5b 100644 --- a/doc/security/webhooks.md +++ b/doc/security/webhooks.md @@ -2,12 +2,19 @@ If you have non-GitLab web services running on your GitLab server or within its local network, these may be vulnerable to exploitation via Webhooks. -With [Webhooks](../user/project/integrations/webhooks.md), you and your project masters and owners can set up URLs to be triggered when specific things happen to projects. Normally, these requests are sent to external web services specifically set up for this purpose, that process the request and its attached data in some appropriate way. +With [Webhooks](../user/project/integrations/webhooks.md), you and your project masters and owners can set up URLs to be triggered when specific things happen to projects. Normally, these requests are sent to external web services specifically set up for this purpose, that process the request and its attached data in some appropriate way. Things get hairy, however, when a Webhook is set up with a URL that doesn't point to an external, but to an internal service, that may do something completely unintended when the webhook is triggered and the POST request is sent. Because Webhook requests are made by the GitLab server itself, these have complete access to everything running on the server (http://localhost:123) or within the server's local network (http://192.168.1.12:345), even if these services are otherwise protected and inaccessible from the outside world. -If a web service does not require authentication, Webhooks can be used to trigger destructive commands by getting the GitLab server to make POST requests to endpoints like "http://localhost:123/some-resource/delete". +If a web service does not require authentication, Webhooks can be used to trigger destructive commands by getting the GitLab server to make POST requests to endpoints like "http://localhost:123/some-resource/delete". -To prevent this type of exploitation from happening, make sure that you are aware of every web service GitLab could potentially have access to, and that all of these are set up to require authentication for every potentially destructive command. Enabling authentication but leaving a default password is not enough. +To prevent this type of exploitation from happening, starting with GitLab 10.6, all Webhook requests to the current GitLab instance server address and/or in a private network will be forbidden by default. That means that all requests made to 127.0.0.1, ::1 and 0.0.0.0, as well as IPv4 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 and IPv6 site-local (ffc0::/10) addresses won't be allowed. + +This behavior can be overridden by enabling the option *"Allow requests to the local network from hooks and services"* in the *"Outbound requests"* section inside the Admin area under **Settings** (`/admin/application_settings`): + +![Outbound requests admin settings](img/outbound_requests_section.png) + +>**Note:** +*System hooks* are exempt from this protection because they are set up by admins. diff --git a/doc/user/project/deploy_tokens/index.md b/doc/user/project/deploy_tokens/index.md index 86fc58020e8..7a8b3c75690 100644 --- a/doc/user/project/deploy_tokens/index.md +++ b/doc/user/project/deploy_tokens/index.md @@ -23,7 +23,7 @@ You can create as many deploy tokens as you like from the settings of your proje ![Personal access tokens page](img/deploy_tokens.png) -## Revoking a personal access token +## Revoking a deploy token At any time, you can revoke any deploy token by just clicking the respective **Revoke** button under the 'Active deploy tokens' area. @@ -71,6 +71,16 @@ docker login registry.example.com -u <username> -p <deploy_token> Just replace `<username>` and `<deploy_token>` with the proper values. Then you can simply pull images from your Container Registry. +### GitLab Deploy Token + +> [Introduced][ce-18414] in GitLab 10.8. + +There's a special case when it comes to Deploy Tokens, if a user creates one +named `gitlab-deploy-token`, the name and token of the Deploy Token will be +automatically exposed to the CI/CD jobs as environment variables: `CI_DEPLOY_USER` and +`CI_DEPLOY_PASSWORD`, respectively. + [ce-17894]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/17894 [ce-11845]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/11845 +[ce-18414]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/18414 [container registry]: ../container_registry.md diff --git a/doc/user/project/quick_actions.md b/doc/user/project/quick_actions.md index 442fc978284..2f4ed3493c2 100644 --- a/doc/user/project/quick_actions.md +++ b/doc/user/project/quick_actions.md @@ -38,6 +38,7 @@ do. | `/award :emoji:` | Toggle award for :emoji: | | `/board_move ~column` | Move issue to column on the board | | `/duplicate #issue` | Closes this issue and marks it as a duplicate of another issue | -| `/move path/to/project` | Moves issue to another project | -| `/tableflip` | Append the comment with `(╯°□°)╯︵ ┻━┻` | -| `/shrug` | Append the comment with `¯\_(ツ)_/¯` |
\ No newline at end of file +| `/move path/to/project` | Moves issue to another project | +| `/tableflip` | Append the comment with `(╯°□°)╯︵ ┻━┻` | +| `/shrug` | Append the comment with `¯\_(ツ)_/¯` | +| <code>/copy_metadata #issue | !merge_request</code> | Copy labels and milestone from other issue or merge request | diff --git a/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md b/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md index 377eee69c11..104ac0cf31b 100644 --- a/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md +++ b/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md @@ -244,3 +244,20 @@ GitLab checks files to detect LFS pointers on push. If LFS pointers are detected Verify that LFS in installed locally and consider a manual push with `git lfs push --all`. If you are storing LFS files outside of GitLab you can disable LFS on the project by settting `lfs_enabled: false` with the [projects api](../../api/projects.md#edit-project). + +### Hosting LFS objects externally + +It is possible to host LFS objects externally by setting a custom LFS url with `git config -f .lfsconfig lfs.url https://example.com/<project>.git/info/lfs`. + +Because GitLab verifies the existence of objects referenced by LFS pointers, push will fail when LFS is enabled for the project. + +LFS can be disabled for a project by Owners and Masters using the [Project API](../../api/projects.md#edit-project). + +```bash +curl --request PUT \ + --url https://example.com/api/v4/projects/<PROJECT_ID> \ + --header 'Private-Token: <YOUR_PRIVATE_TOKEN>' \ + --data 'lfs_enabled=false' +``` + +Note, `<PROJECT_ID>` can also be substituted with a [namespaced path](../../api/README.md#namespaced-path-encoding). diff --git a/ee/app/controllers/ee/ldap/omniauth_callbacks_controller.rb b/ee/app/controllers/ee/ldap/omniauth_callbacks_controller.rb new file mode 100644 index 00000000000..f1e851a210b --- /dev/null +++ b/ee/app/controllers/ee/ldap/omniauth_callbacks_controller.rb @@ -0,0 +1,22 @@ +module EE + module Ldap + module OmniauthCallbacksController + extend ::Gitlab::Utils::Override + + override :sign_in_and_redirect + def sign_in_and_redirect(user) + # The counter gets incremented in `sign_in_and_redirect` + show_ldap_sync_flash if user.sign_in_count == 0 + + super + end + + private + + def show_ldap_sync_flash + flash[:notice] = 'LDAP sync in progress. This could take a few minutes. '\ + 'Refresh the page to see the changes.' + end + end + end +end diff --git a/ee/spec/controllers/ldap/omniauth_callbacks_controller_spec.rb b/ee/spec/controllers/ldap/omniauth_callbacks_controller_spec.rb new file mode 100644 index 00000000000..0835ff35846 --- /dev/null +++ b/ee/spec/controllers/ldap/omniauth_callbacks_controller_spec.rb @@ -0,0 +1,29 @@ +require 'spec_helper' + +describe Ldap::OmniauthCallbacksController do + include_context 'Ldap::OmniauthCallbacksController' + + it "displays LDAP sync flash on first sign in" do + post provider + + expect(flash[:notice]).to match(/LDAP sync in progress*/) + end + + it "skips LDAP sync flash on subsequent sign ins" do + user.update!(sign_in_count: 1) + + post provider + + expect(flash[:notice]).to eq nil + end + + context 'access denied' do + let(:valid_login?) { false } + + it 'logs a failure event' do + stub_licensed_features(extended_audit_events: true) + + expect { post provider }.to change(SecurityEvent, :count).by(1) + end + end +end diff --git a/features/project/find_file.feature b/features/project/find_file.feature deleted file mode 100644 index ae8fa245923..00000000000 --- a/features/project/find_file.feature +++ /dev/null @@ -1,42 +0,0 @@ -@dashboard -Feature: Project Find File - Background: - Given I sign in as a user - And I own a project - And I visit my project's files page - - @javascript - Scenario: Navigate to find file by shortcut - Given I press "t" - Then I should see "find file" page - - Scenario: Navigate to find file - Given I click Find File button - Then I should see "find file" page - - @javascript - Scenario: I search file - Given I visit project find file page - And I fill in file find with "change" - Then I should not see ".gitignore" in files - And I should not see ".gitmodules" in files - And I should see "CHANGELOG" in files - And I should not see "VERSION" in files - - @javascript - Scenario: I search file that not exist - Given I visit project find file page - And I fill in file find with "asdfghjklqwertyuizxcvbnm" - Then I should not see ".gitignore" in files - And I should not see ".gitmodules" in files - And I should not see "CHANGELOG" in files - And I should not see "VERSION" in files - - @javascript - Scenario: I search file that partially matches - Given I visit project find file page - And I fill in file find with "git" - Then I should see ".gitignore" in files - And I should see ".gitmodules" in files - And I should not see "CHANGELOG" in files - And I should not see "VERSION" in files diff --git a/features/steps/project/project_find_file.rb b/features/steps/project/project_find_file.rb deleted file mode 100644 index 461160b8430..00000000000 --- a/features/steps/project/project_find_file.rb +++ /dev/null @@ -1,72 +0,0 @@ -class Spinach::Features::ProjectFindFile < Spinach::FeatureSteps - include SharedAuthentication - include SharedPaths - include SharedProject - include SharedProjectTab - - step 'I press "t"' do - find('body').native.send_key('t') - end - - step 'I click Find File button' do - click_link 'Find file' - end - - step 'I should see "find file" page' do - ensure_active_main_tab('Repository') - expect(page).to have_selector('.file-finder-holder', count: 1) - end - - step 'I fill in Find by path with "git"' do - ensure_active_main_tab('Repository') - expect(page).to have_selector('.file-finder-holder', count: 1) - end - - step 'I fill in file find with "git"' do - find_file "git" - end - - step 'I fill in file find with "change"' do - find_file "change" - end - - step 'I fill in file find with "asdfghjklqwertyuizxcvbnm"' do - find_file "asdfghjklqwertyuizxcvbnm" - end - - step 'I should see "VERSION" in files' do - expect(page).to have_content("VERSION") - end - - step 'I should not see "VERSION" in files' do - expect(page).not_to have_content("VERSION") - end - - step 'I should see "CHANGELOG" in files' do - expect(page).to have_content("CHANGELOG") - end - - step 'I should not see "CHANGELOG" in files' do - expect(page).not_to have_content("CHANGELOG") - end - - step 'I should see ".gitmodules" in files' do - expect(page).to have_content(".gitmodules") - end - - step 'I should not see ".gitmodules" in files' do - expect(page).not_to have_content(".gitmodules") - end - - step 'I should see ".gitignore" in files' do - expect(page).to have_content(".gitignore") - end - - step 'I should not see ".gitignore" in files' do - expect(page).not_to have_content(".gitignore") - end - - def find_file(text) - fill_in 'file_find', with: text - end -end diff --git a/features/steps/shared/paths.rb b/features/steps/shared/paths.rb index d16c127f6e6..014e6ad625b 100644 --- a/features/steps/shared/paths.rb +++ b/features/steps/shared/paths.rb @@ -200,10 +200,6 @@ module SharedPaths visit edit_project_path(@project) end - step "I visit my project's files page" do - visit project_tree_path(@project, root_ref) - end - step 'I visit a binary file in the repo' do visit project_blob_path(@project, File.join(root_ref, 'files/images/logo-black.png')) diff --git a/features/support/env.rb b/features/support/env.rb index 15211995918..8fa2fcb6e3e 100644 --- a/features/support/env.rb +++ b/features/support/env.rb @@ -12,7 +12,11 @@ end WebMock.enable! -%w(select2_helper test_env repo_helpers wait_for_requests sidekiq project_forks_helper webmock).each do |f| +%w(select2_helper test_env repo_helpers wait_for_requests project_forks_helper).each do |f| + require Rails.root.join('spec', 'support', 'helpers', f) +end + +%w(sidekiq webmock).each do |f| require Rails.root.join('spec', 'support', f) end diff --git a/lib/api/helpers/notes_helpers.rb b/lib/api/helpers/notes_helpers.rb index cd91df1ecd8..b74b8149834 100644 --- a/lib/api/helpers/notes_helpers.rb +++ b/lib/api/helpers/notes_helpers.rb @@ -64,8 +64,10 @@ module API authorize! :create_note, noteable parent = noteable_parent(noteable) + if opts[:created_at] - opts.delete(:created_at) unless current_user.admin? || parent.owner == current_user + opts.delete(:created_at) unless + current_user.admin? || parent.owned_by?(current_user) end project = parent if parent.is_a?(Project) diff --git a/lib/api/projects.rb b/lib/api/projects.rb index 51b3b0459f3..8871792060b 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -74,6 +74,11 @@ module API present options[:with].prepare_relation(projects, options), options end + + def translate_params_for_compatibility(params) + params[:builds_enabled] = params.delete(:jobs_enabled) if params.key?(:jobs_enabled) + params + end end resource :users, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do @@ -123,7 +128,7 @@ module API end post do attrs = declared_params(include_missing: false) - attrs[:builds_enabled] = attrs.delete(:jobs_enabled) if attrs.key?(:jobs_enabled) + attrs = translate_params_for_compatibility(attrs) project = ::Projects::CreateService.new(current_user, attrs).execute if project.saved? @@ -155,6 +160,7 @@ module API not_found!('User') unless user attrs = declared_params(include_missing: false) + attrs = translate_params_for_compatibility(attrs) project = ::Projects::CreateService.new(user, attrs).execute if project.saved? @@ -276,7 +282,7 @@ module API authorize! :rename_project, user_project if attrs[:name].present? authorize! :change_visibility_level, user_project if attrs[:visibility].present? - attrs[:builds_enabled] = attrs.delete(:jobs_enabled) if attrs.key?(:jobs_enabled) + attrs = translate_params_for_compatibility(attrs) result = ::Projects::UpdateService.new(user_project, current_user, attrs).execute diff --git a/lib/api/users.rb b/lib/api/users.rb index 3920171205f..14b8a796c8e 100644 --- a/lib/api/users.rb +++ b/lib/api/users.rb @@ -77,7 +77,7 @@ module API authenticated_as_admin! if params[:external].present? || (params[:extern_uid].present? && params[:provider].present?) unless current_user&.admin? - params.except!(:created_after, :created_before, :order_by, :sort) + params.except!(:created_after, :created_before, :order_by, :sort, :two_factor) end users = UsersFinder.new(current_user, params).execute diff --git a/lib/backup/files.rb b/lib/backup/files.rb index 88cb7e7b5a4..9895db9e451 100644 --- a/lib/backup/files.rb +++ b/lib/backup/files.rb @@ -53,6 +53,8 @@ module Backup FileUtils.mv(files, timestamped_files_path) rescue Errno::EACCES access_denied_error(app_files_dir) + rescue Errno::EBUSY + resource_busy_error(app_files_dir) end end end diff --git a/lib/backup/helper.rb b/lib/backup/helper.rb index a1ee0faefe9..54b9ce10b4d 100644 --- a/lib/backup/helper.rb +++ b/lib/backup/helper.rb @@ -13,5 +13,19 @@ module Backup EOS raise message end + + def resource_busy_error(path) + message = <<~EOS + + ### NOTICE ### + As part of restore, the task tried to rename `#{path}` before restoring. + This could not be completed, perhaps `#{path}` is a mountpoint? + + To complete the restore, please move the contents of `#{path}` to a + different location and run the restore task again. + + EOS + raise message + end end end diff --git a/lib/backup/repository.rb b/lib/backup/repository.rb index 89e3f1d9076..65e06fd78c0 100644 --- a/lib/backup/repository.rb +++ b/lib/backup/repository.rb @@ -81,6 +81,8 @@ module Backup FileUtils.mv(files, bk_repos_path) rescue Errno::EACCES access_denied_error(path) + rescue Errno::EBUSY + resource_busy_error(path) end end end diff --git a/lib/gitlab.rb b/lib/gitlab.rb index f6629982512..c5498d0da1a 100644 --- a/lib/gitlab.rb +++ b/lib/gitlab.rb @@ -1,9 +1,19 @@ -require_dependency 'gitlab/git' +require_dependency 'gitlab/popen' module Gitlab + def self.root + Pathname.new(File.expand_path('..', __dir__)) + end + + def self.config + Settings + end + COM_URL = 'https://gitlab.com'.freeze APP_DIRS_PATTERN = %r{^/?(app|config|ee|lib|spec|\(\w*\))} SUBDOMAIN_REGEX = %r{\Ahttps://[a-z0-9]+\.gitlab\.com\z} + VERSION = File.read(root.join("VERSION")).strip.freeze + REVISION = Gitlab::Popen.popen(%W(#{config.git.bin_path} log --pretty=format:%h -n 1)).first.chomp.freeze def self.com? # Check `gl_subdomain?` as well to keep parity with gitlab.com @@ -19,6 +29,6 @@ module Gitlab end def self.dev_env_or_com? - Rails.env.test? || Rails.env.development? || org? || com? + Rails.env.development? || org? || com? end end diff --git a/lib/gitlab/auth/ldap/user.rb b/lib/gitlab/auth/ldap/user.rb index 068212d9a21..922d0567d99 100644 --- a/lib/gitlab/auth/ldap/user.rb +++ b/lib/gitlab/auth/ldap/user.rb @@ -8,6 +8,8 @@ module Gitlab module Auth module LDAP class User < Gitlab::Auth::OAuth::User + extend ::Gitlab::Utils::Override + class << self def find_by_uid_and_provider(uid, provider) identity = ::Identity.with_extern_uid(provider, uid).take @@ -29,7 +31,8 @@ module Gitlab self.class.find_by_uid_and_provider(auth_hash.uid, auth_hash.provider) end - def changed? + override :should_save? + def should_save? gl_user.changed? || gl_user.identities.any?(&:changed?) end @@ -41,6 +44,10 @@ module Gitlab Gitlab::Auth::LDAP::Access.allowed?(gl_user) end + def valid_sign_in? + allowed? && super + end + def ldap_config Gitlab::Auth::LDAP::Config.new(auth_hash.provider) end diff --git a/lib/gitlab/auth/o_auth/identity_linker.rb b/lib/gitlab/auth/o_auth/identity_linker.rb new file mode 100644 index 00000000000..de92d7a214d --- /dev/null +++ b/lib/gitlab/auth/o_auth/identity_linker.rb @@ -0,0 +1,8 @@ +module Gitlab + module Auth + module OAuth + class IdentityLinker < OmniauthIdentityLinkerBase + end + end + end +end diff --git a/lib/gitlab/auth/o_auth/user.rb b/lib/gitlab/auth/o_auth/user.rb index d0c6b0386ba..6c5d0788a0a 100644 --- a/lib/gitlab/auth/o_auth/user.rb +++ b/lib/gitlab/auth/o_auth/user.rb @@ -30,6 +30,10 @@ module Gitlab gl_user.try(:valid?) end + def valid_sign_in? + valid? && persisted? + end + def save(provider = 'OAuth') raise SigninDisabledForProviderError if oauth_provider_disabled? raise SignupDisabledError unless gl_user @@ -64,8 +68,18 @@ module Gitlab user end + def find_and_update! + save if should_save? + + gl_user + end + protected + def should_save? + true + end + def add_or_update_user_identities return unless gl_user diff --git a/lib/gitlab/auth/omniauth_identity_linker_base.rb b/lib/gitlab/auth/omniauth_identity_linker_base.rb new file mode 100644 index 00000000000..ae365fcdfaa --- /dev/null +++ b/lib/gitlab/auth/omniauth_identity_linker_base.rb @@ -0,0 +1,47 @@ +module Gitlab + module Auth + class OmniauthIdentityLinkerBase + attr_reader :current_user, :oauth + + def initialize(current_user, oauth) + @current_user = current_user + @oauth = oauth + @changed = false + end + + def link + save if identity.new_record? + end + + def changed? + @changed + end + + def error_message + identity.validate + + identity.errors.full_messages.join(', ') + end + + private + + def save + @changed = identity.save + end + + def identity + @identity ||= current_user.identities + .with_extern_uid(provider, uid) + .first_or_initialize(extern_uid: uid) + end + + def provider + oauth['provider'] + end + + def uid + oauth['uid'] + end + end + end +end diff --git a/lib/gitlab/auth/saml/identity_linker.rb b/lib/gitlab/auth/saml/identity_linker.rb new file mode 100644 index 00000000000..7e4b191d512 --- /dev/null +++ b/lib/gitlab/auth/saml/identity_linker.rb @@ -0,0 +1,8 @@ +module Gitlab + module Auth + module Saml + class IdentityLinker < OmniauthIdentityLinkerBase + end + end + end +end diff --git a/lib/gitlab/auth/saml/user.rb b/lib/gitlab/auth/saml/user.rb index d4024e9ec39..cb01cd8004c 100644 --- a/lib/gitlab/auth/saml/user.rb +++ b/lib/gitlab/auth/saml/user.rb @@ -7,6 +7,8 @@ module Gitlab module Auth module Saml class User < Gitlab::Auth::OAuth::User + extend ::Gitlab::Utils::Override + def save super('SAML') end @@ -21,13 +23,14 @@ module Gitlab if external_users_enabled? && user # Check if there is overlap between the user's groups and the external groups # setting then set user as external or internal. - user.external = !(auth_hash.groups & Gitlab::Auth::Saml::Config.external_groups).empty? + user.external = !(auth_hash.groups & saml_config.external_groups).empty? end user end - def changed? + override :should_save? + def should_save? return true unless gl_user gl_user.changed? || gl_user.identities.any?(&:changed?) @@ -35,12 +38,16 @@ module Gitlab protected + def saml_config + Gitlab::Auth::Saml::Config + end + def auto_link_saml_user? Gitlab.config.omniauth.auto_link_saml_user end def external_users_enabled? - !Gitlab::Auth::Saml::Config.external_groups.nil? + !saml_config.external_groups.nil? end def auth_hash=(auth_hash) diff --git a/lib/gitlab/git.rb b/lib/gitlab/git.rb index c9abea90d21..e85e87a54af 100644 --- a/lib/gitlab/git.rb +++ b/lib/gitlab/git.rb @@ -1,3 +1,5 @@ +require_dependency 'gitlab/encoding_helper' + module Gitlab module Git # The ID of empty tree. diff --git a/lib/gitlab/wiki/committer_with_hooks.rb b/lib/gitlab/git/committer_with_hooks.rb index 19f0b3814fd..a8a59f998cd 100644 --- a/lib/gitlab/wiki/committer_with_hooks.rb +++ b/lib/gitlab/git/committer_with_hooks.rb @@ -1,5 +1,5 @@ module Gitlab - module Wiki + module Git class CommitterWithHooks < Gollum::Committer attr_reader :gl_wiki @@ -9,6 +9,9 @@ module Gitlab end def commit + # TODO: Remove after 10.8 + return super unless allowed_to_run_hooks? + result = Gitlab::Git::OperationService.new(git_user, gl_wiki.repository).with_branch( @wiki.ref, start_branch_name: @wiki.ref @@ -24,6 +27,11 @@ module Gitlab private + # TODO: Remove after 10.8 + def allowed_to_run_hooks? + @options[:user_id] != 0 && @options[:username].present? + end + def git_user @git_user ||= Gitlab::Git::User.new(@options[:username], @options[:name], diff --git a/lib/gitlab/git/wiki.rb b/lib/gitlab/git/wiki.rb index 821436911ab..84a26fe4a6f 100644 --- a/lib/gitlab/git/wiki.rb +++ b/lib/gitlab/git/wiki.rb @@ -290,7 +290,7 @@ module Gitlab end def committer_with_hooks(commit_details) - Gitlab::Wiki::CommitterWithHooks.new(self, commit_details.to_h) + Gitlab::Git::CommitterWithHooks.new(self, commit_details.to_h) end def with_committer_with_hooks(commit_details, &block) diff --git a/lib/gitlab/import_export/import_export.yml b/lib/gitlab/import_export/import_export.yml index ec91c02dbe7..0d1c4f73c6e 100644 --- a/lib/gitlab/import_export/import_export.yml +++ b/lib/gitlab/import_export/import_export.yml @@ -64,6 +64,7 @@ project_tree: - :project_feature - :custom_attributes - :project_badges + - :ci_cd_settings # Only include the following attributes for the models specified. included_attributes: @@ -73,6 +74,8 @@ included_attributes: - :username author: - :name + ci_cd_settings: + - :group_runners_enabled # Do not include the following attributes for the models specified. excluded_attributes: diff --git a/lib/gitlab/import_export/relation_factory.rb b/lib/gitlab/import_export/relation_factory.rb index 598832fb2df..e3e9f156fb4 100644 --- a/lib/gitlab/import_export/relation_factory.rb +++ b/lib/gitlab/import_export/relation_factory.rb @@ -17,7 +17,8 @@ module Gitlab auto_devops: :project_auto_devops, label: :project_label, custom_attributes: 'ProjectCustomAttribute', - project_badges: 'Badge' }.freeze + project_badges: 'Badge', + ci_cd_settings: 'ProjectCiCdSetting' }.freeze USER_REFERENCES = %w[author_id assignee_id updated_by_id user_id created_by_id last_edited_by_id merge_user_id resolved_by_id closed_by_id].freeze diff --git a/lib/gitlab/user_access.rb b/lib/gitlab/user_access.rb index 69952cbb47c..8cf5d636743 100644 --- a/lib/gitlab/user_access.rb +++ b/lib/gitlab/user_access.rb @@ -63,10 +63,12 @@ module Gitlab request_cache def can_push_to_branch?(ref) return false unless can_access_git? - return false unless user.can?(:push_code, project) || project.branch_allows_maintainer_push?(user, ref) + return false unless project + + return false if !user.can?(:push_code, project) && !project.branch_allows_maintainer_push?(user, ref) if protected?(ProtectedBranch, project, ref) - project.user_can_push_to_empty_repo?(user) || protected_branch_accessible_to?(ref, action: :push) + protected_branch_accessible_to?(ref, action: :push) else true end @@ -101,6 +103,7 @@ module Gitlab def protected_branch_accessible_to?(ref, action:) ProtectedBranch.protected_ref_accessible_to?( ref, user, + project: project, action: action, protected_refs: project.protected_branches) end @@ -108,6 +111,7 @@ module Gitlab def protected_tag_accessible_to?(ref, action:) ProtectedTag.protected_ref_accessible_to?( ref, user, + project: project, action: action, protected_refs: project.protected_tags) end diff --git a/package.json b/package.json index 45bea12fd9b..96dee321548 100644 --- a/package.json +++ b/package.json @@ -5,9 +5,9 @@ "eslint": "eslint --max-warnings 0 --ext .js,.vue .", "eslint-fix": "eslint --max-warnings 0 --ext .js,.vue --fix .", "eslint-report": "eslint --max-warnings 0 --ext .js,.vue --format html --output-file ./eslint-report.html .", - "karma": "karma start --single-run true config/karma.config.js", + "karma": "BABEL_ENV=${BABEL_ENV:=karma} karma start --single-run true config/karma.config.js", "karma-coverage": "BABEL_ENV=coverage karma start --single-run true config/karma.config.js", - "karma-start": "karma start config/karma.config.js", + "karma-start": "BABEL_ENV=karma karma start config/karma.config.js", "prettier-staged": "node ./scripts/frontend/prettier.js", "prettier-staged-save": "node ./scripts/frontend/prettier.js save", "prettier-all": "node ./scripts/frontend/prettier.js check-all", @@ -88,6 +88,7 @@ "vue-resource": "^1.3.5", "vue-router": "^3.0.1", "vue-template-compiler": "^2.5.13", + "vue-virtual-scroll-list": "^1.2.5", "vuex": "^3.0.1", "webpack": "^3.11.0", "webpack-bundle-analyzer": "^2.10.0", @@ -98,6 +99,9 @@ "axios-mock-adapter": "^1.10.0", "babel-eslint": "^8.0.2", "babel-plugin-istanbul": "^4.1.5", + "babel-plugin-rewire": "^1.1.0", + "babel-template": "^6.26.0", + "babel-types": "^6.26.0", "commander": "^2.15.1", "eslint": "^3.18.0", "eslint-config-airbnb-base": "^10.0.1", diff --git a/rubocop/spec_helpers.rb b/rubocop/spec_helpers.rb index 6c0f0193b1a..9bf5f1e3b18 100644 --- a/rubocop/spec_helpers.rb +++ b/rubocop/spec_helpers.rb @@ -1,6 +1,6 @@ module RuboCop module SpecHelpers - SPEC_HELPERS = %w[spec_helper.rb rails_helper.rb].freeze + SPEC_HELPERS = %w[fast_spec_helper.rb rails_helper.rb spec_helper.rb].freeze # Returns true if the given node originated from the spec directory. def in_spec?(node) diff --git a/spec/controllers/import/bitbucket_controller_spec.rb b/spec/controllers/import/bitbucket_controller_spec.rb index 2be46049aab..be49b92d23f 100644 --- a/spec/controllers/import/bitbucket_controller_spec.rb +++ b/spec/controllers/import/bitbucket_controller_spec.rb @@ -223,11 +223,12 @@ describe Import::BitbucketController do end context 'user has chosen an existing nested namespace and name for the project', :postgresql do - let(:parent_namespace) { create(:group, name: 'foo', owner: user) } + let(:parent_namespace) { create(:group, name: 'foo') } let(:nested_namespace) { create(:group, name: 'bar', parent: parent_namespace) } let(:test_name) { 'test_name' } before do + parent_namespace.add_owner(user) nested_namespace.add_owner(user) end @@ -273,7 +274,7 @@ describe Import::BitbucketController do context 'user has chosen existent and non-existent nested namespaces and name for the project', :postgresql do let(:test_name) { 'test_name' } - let!(:parent_namespace) { create(:group, name: 'foo', owner: user) } + let!(:parent_namespace) { create(:group, name: 'foo') } before do parent_namespace.add_owner(user) diff --git a/spec/controllers/import/gitlab_controller_spec.rb b/spec/controllers/import/gitlab_controller_spec.rb index e958be077c2..742f4787126 100644 --- a/spec/controllers/import/gitlab_controller_spec.rb +++ b/spec/controllers/import/gitlab_controller_spec.rb @@ -196,10 +196,11 @@ describe Import::GitlabController do end context 'user has chosen an existing nested namespace for the project', :postgresql do - let(:parent_namespace) { create(:group, name: 'foo', owner: user) } + let(:parent_namespace) { create(:group, name: 'foo') } let(:nested_namespace) { create(:group, name: 'bar', parent: parent_namespace) } before do + parent_namespace.add_owner(user) nested_namespace.add_owner(user) end @@ -245,7 +246,7 @@ describe Import::GitlabController do context 'user has chosen existent and non-existent nested namespaces and name for the project', :postgresql do let(:test_name) { 'test_name' } - let!(:parent_namespace) { create(:group, name: 'foo', owner: user) } + let!(:parent_namespace) { create(:group, name: 'foo') } before do parent_namespace.add_owner(user) diff --git a/spec/controllers/ldap/omniauth_callbacks_controller_spec.rb b/spec/controllers/ldap/omniauth_callbacks_controller_spec.rb new file mode 100644 index 00000000000..87c10a86cdd --- /dev/null +++ b/spec/controllers/ldap/omniauth_callbacks_controller_spec.rb @@ -0,0 +1,58 @@ +require 'spec_helper' + +describe Ldap::OmniauthCallbacksController do + include_context 'Ldap::OmniauthCallbacksController' + + it 'allows sign in' do + post provider + + expect(request.env['warden']).to be_authenticated + end + + it 'respects remember me checkbox' do + expect do + post provider, remember_me: '1' + end.to change { user.reload.remember_created_at }.from(nil) + end + + context 'with 2FA' do + let(:user) { create(:omniauth_user, :two_factor_via_otp, extern_uid: uid, provider: provider) } + + it 'passes remember_me to the Devise view' do + post provider, remember_me: '1' + + expect(assigns[:user].remember_me).to eq '1' + end + end + + context 'access denied' do + let(:valid_login?) { false } + + it 'warns the user' do + post provider + + expect(flash[:alert]).to match(/Access denied for your LDAP account*/) + end + + it "doesn't authenticate user" do + post provider + + expect(request.env['warden']).not_to be_authenticated + expect(response).to redirect_to(new_user_session_path) + end + end + + context 'sign up' do + let(:user) { double(email: 'new@example.com') } + + before do + stub_omniauth_setting(block_auto_created_users: false) + end + + it 'is allowed' do + post provider + + expect(request.env['warden']).to be_authenticated + end + end +end diff --git a/spec/controllers/projects/forks_controller_spec.rb b/spec/controllers/projects/forks_controller_spec.rb index c4b32dc3a09..e20623c0ac1 100644 --- a/spec/controllers/projects/forks_controller_spec.rb +++ b/spec/controllers/projects/forks_controller_spec.rb @@ -4,7 +4,11 @@ describe Projects::ForksController do let(:user) { create(:user) } let(:project) { create(:project, :public, :repository) } let(:forked_project) { Projects::ForkService.new(project, user).execute } - let(:group) { create(:group, owner: forked_project.creator) } + let(:group) { create(:group) } + + before do + group.add_owner(user) + end describe 'GET index' do def get_forks diff --git a/spec/factories/commits.rb b/spec/factories/commits.rb index d5d819d862a..818f7b046f6 100644 --- a/spec/factories/commits.rb +++ b/spec/factories/commits.rb @@ -1,4 +1,4 @@ -require_relative '../support/repo_helpers' +require_relative '../support/helpers/repo_helpers' FactoryBot.define do factory :commit do diff --git a/spec/factories/deploy_tokens.rb b/spec/factories/deploy_tokens.rb index 5fea4a9d5a6..017e866e69c 100644 --- a/spec/factories/deploy_tokens.rb +++ b/spec/factories/deploy_tokens.rb @@ -10,5 +10,13 @@ FactoryBot.define do trait :revoked do revoked true end + + trait :gitlab_deploy_token do + name DeployToken::GITLAB_DEPLOY_TOKEN_NAME + end + + trait :expired do + expires_at { Date.today - 1.month } + end end end diff --git a/spec/factories/gpg_key_subkeys.rb b/spec/factories/gpg_key_subkeys.rb index 57eaaee345f..6c7db5379a9 100644 --- a/spec/factories/gpg_key_subkeys.rb +++ b/spec/factories/gpg_key_subkeys.rb @@ -1,5 +1,3 @@ -require_relative '../support/gpg_helpers' - FactoryBot.define do factory :gpg_key_subkey do gpg_key diff --git a/spec/factories/gpg_keys.rb b/spec/factories/gpg_keys.rb index b8aabf74221..51b8ddc9934 100644 --- a/spec/factories/gpg_keys.rb +++ b/spec/factories/gpg_keys.rb @@ -1,4 +1,4 @@ -require_relative '../support/gpg_helpers' +require_relative '../support/helpers/gpg_helpers' FactoryBot.define do factory :gpg_key do diff --git a/spec/factories/gpg_signature.rb b/spec/factories/gpg_signature.rb index 4620caff823..b89e6ffc4b3 100644 --- a/spec/factories/gpg_signature.rb +++ b/spec/factories/gpg_signature.rb @@ -1,5 +1,3 @@ -require_relative '../support/gpg_helpers' - FactoryBot.define do factory :gpg_signature do commit_sha { Digest::SHA1.hexdigest(SecureRandom.hex) } diff --git a/spec/factories/groups.rb b/spec/factories/groups.rb index 8c531cf5909..3b354c0d96b 100644 --- a/spec/factories/groups.rb +++ b/spec/factories/groups.rb @@ -5,6 +5,14 @@ FactoryBot.define do type 'Group' owner nil + after(:create) do |group| + if group.owner + # We could remove this after we have proper constraint: + # https://gitlab.com/gitlab-org/gitlab-ce/issues/43292 + raise "Don't set owner for groups, use `group.add_owner(user)` instead" + end + end + trait :public do visibility_level Gitlab::VisibilityLevel::PUBLIC end diff --git a/spec/factories/namespaces.rb b/spec/factories/namespaces.rb index f94b09cff15..6feafa5ece9 100644 --- a/spec/factories/namespaces.rb +++ b/spec/factories/namespaces.rb @@ -2,6 +2,22 @@ FactoryBot.define do factory :namespace do sequence(:name) { |n| "namespace#{n}" } path { name.downcase.gsub(/\s/, '_') } - owner + + # This is a workaround to avoid the user creating another namespace via + # User#ensure_namespace_correct. We should try to remove it and then + # we could remove this workaround + association :owner, factory: :user, strategy: :build + before(:create) do |namespace| + owner = namespace.owner + + if owner + # We're changing the username here because we want to keep our path, + # and User#ensure_namespace_correct would change the path based on + # username, so we're forced to do this otherwise we'll need to change + # a lot of existing tests. + owner.username = namespace.path + owner.namespace = namespace + end + end end end diff --git a/spec/factories/notes.rb b/spec/factories/notes.rb index 857333f222d..40f3fa7d69b 100644 --- a/spec/factories/notes.rb +++ b/spec/factories/notes.rb @@ -1,4 +1,4 @@ -require_relative '../support/repo_helpers' +require_relative '../support/helpers/repo_helpers' include ActionDispatch::TestProcess diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb index 1761b6e2a3b..803498d3b19 100644 --- a/spec/factories/projects.rb +++ b/spec/factories/projects.rb @@ -1,4 +1,4 @@ -require_relative '../support/test_env' +require_relative '../support/helpers/test_env' FactoryBot.define do # Project without repository @@ -151,6 +151,13 @@ FactoryBot.define do end end + trait :stubbed_repository do + after(:build) do |project| + allow(project).to receive(:empty_repo?).and_return(false) + allow(project.repository).to receive(:empty?).and_return(false) + end + end + trait :wiki_repo do after(:create) do |project| raise 'Failed to create wiki repository!' unless project.create_wiki diff --git a/spec/fast_spec_helper.rb b/spec/fast_spec_helper.rb new file mode 100644 index 00000000000..978113a08a4 --- /dev/null +++ b/spec/fast_spec_helper.rb @@ -0,0 +1,16 @@ +require 'bundler/setup' + +ENV['GITLAB_ENV'] = 'test' +ENV['IN_MEMORY_APPLICATION_SETTINGS'] = 'true' + +unless Object.respond_to?(:require_dependency) + class Object + alias_method :require_dependency, :require + end +end + +# Defines Settings and Gitlab.config which are at the center of the app +require_relative '../config/settings' +require_relative '../lib/gitlab' unless defined?(Gitlab.config) + +require_relative 'support/rspec' diff --git a/spec/features/labels_hierarchy_spec.rb b/spec/features/labels_hierarchy_spec.rb index 3e05e7b7f38..ae41f611ddc 100644 --- a/spec/features/labels_hierarchy_spec.rb +++ b/spec/features/labels_hierarchy_spec.rb @@ -170,6 +170,8 @@ feature 'Labels Hierarchy', :js, :nested_groups do context 'on issue sidebar' do before do + project_1.add_developer(user) + visit project_issue_path(project_1, issue) end @@ -180,6 +182,8 @@ feature 'Labels Hierarchy', :js, :nested_groups do let(:board) { create(:board, project: project_1) } before do + project_1.add_developer(user) + visit project_board_path(project_1, board) wait_for_requests @@ -194,6 +198,8 @@ feature 'Labels Hierarchy', :js, :nested_groups do let(:board) { create(:board, group: parent) } before do + parent.add_developer(user) + visit group_board_path(parent, board) wait_for_requests @@ -211,6 +217,8 @@ feature 'Labels Hierarchy', :js, :nested_groups do context 'on project issuable list' do before do + project_1.add_developer(user) + visit project_issues_path(project_1) end @@ -237,6 +245,8 @@ feature 'Labels Hierarchy', :js, :nested_groups do let(:board) { create(:board, project: project_1) } before do + project_1.add_developer(user) + visit project_board_path(project_1, board) end @@ -247,6 +257,8 @@ feature 'Labels Hierarchy', :js, :nested_groups do let(:board) { create(:board, group: parent) } before do + parent.add_developer(user) + visit group_board_path(parent, board) end @@ -259,6 +271,7 @@ feature 'Labels Hierarchy', :js, :nested_groups do let(:board) { create(:board, project: project_1) } before do + project_1.add_developer(user) visit project_board_path(project_1, board) find('.js-new-board-list').click wait_for_requests @@ -281,6 +294,7 @@ feature 'Labels Hierarchy', :js, :nested_groups do let(:board) { create(:board, group: parent) } before do + parent.add_developer(user) visit group_board_path(parent, board) find('.js-new-board-list').click wait_for_requests diff --git a/spec/features/oauth_login_spec.rb b/spec/features/oauth_login_spec.rb index a5e325ee2e3..013cdaa6479 100644 --- a/spec/features/oauth_login_spec.rb +++ b/spec/features/oauth_login_spec.rb @@ -28,35 +28,46 @@ feature 'OAuth Login', :js, :allow_forgery_protection do OmniAuth.config.full_host = @omniauth_config_full_host end + def login_with_provider(provider, enter_two_factor: false) + login_via(provider.to_s, user, uid, remember_me: remember_me) + enter_code(user.current_otp) if enter_two_factor + end + providers.each do |provider| context "when the user logs in using the #{provider} provider" do + let(:uid) { 'my-uid' } + let(:remember_me) { false } + let(:user) { create(:omniauth_user, extern_uid: uid, provider: provider.to_s) } + let(:two_factor_user) { create(:omniauth_user, :two_factor, extern_uid: uid, provider: provider.to_s) } + + before do + stub_omniauth_config(provider) + end + context 'when two-factor authentication is disabled' do it 'logs the user in' do - stub_omniauth_config(provider) - user = create(:omniauth_user, extern_uid: 'my-uid', provider: provider.to_s) - login_via(provider.to_s, user, 'my-uid') + login_with_provider(provider) expect(current_path).to eq root_path end end context 'when two-factor authentication is enabled' do + let(:user) { two_factor_user } + it 'logs the user in' do - stub_omniauth_config(provider) - user = create(:omniauth_user, :two_factor, extern_uid: 'my-uid', provider: provider.to_s) - login_via(provider.to_s, user, 'my-uid') + login_with_provider(provider, enter_two_factor: true) - enter_code(user.current_otp) expect(current_path).to eq root_path end end context 'when "remember me" is checked' do + let(:remember_me) { true } + context 'when two-factor authentication is disabled' do it 'remembers the user after a browser restart' do - stub_omniauth_config(provider) - user = create(:omniauth_user, extern_uid: 'my-uid', provider: provider.to_s) - login_via(provider.to_s, user, 'my-uid', remember_me: true) + login_with_provider(provider) clear_browser_session @@ -66,11 +77,10 @@ feature 'OAuth Login', :js, :allow_forgery_protection do end context 'when two-factor authentication is enabled' do + let(:user) { two_factor_user } + it 'remembers the user after a browser restart' do - stub_omniauth_config(provider) - user = create(:omniauth_user, :two_factor, extern_uid: 'my-uid', provider: provider.to_s) - login_via(provider.to_s, user, 'my-uid', remember_me: true) - enter_code(user.current_otp) + login_with_provider(provider, enter_two_factor: true) clear_browser_session @@ -83,9 +93,7 @@ feature 'OAuth Login', :js, :allow_forgery_protection do context 'when "remember me" is not checked' do context 'when two-factor authentication is disabled' do it 'does not remember the user after a browser restart' do - stub_omniauth_config(provider) - user = create(:omniauth_user, extern_uid: 'my-uid', provider: provider.to_s) - login_via(provider.to_s, user, 'my-uid', remember_me: false) + login_with_provider(provider) clear_browser_session @@ -95,11 +103,10 @@ feature 'OAuth Login', :js, :allow_forgery_protection do end context 'when two-factor authentication is enabled' do + let(:user) { two_factor_user } + it 'does not remember the user after a browser restart' do - stub_omniauth_config(provider) - user = create(:omniauth_user, :two_factor, extern_uid: 'my-uid', provider: provider.to_s) - login_via(provider.to_s, user, 'my-uid', remember_me: false) - enter_code(user.current_otp) + login_with_provider(provider, enter_two_factor: true) clear_browser_session diff --git a/spec/features/projects/blobs/user_creates_new_blob_in_new_project_spec.rb b/spec/features/projects/blobs/user_creates_new_blob_in_new_project_spec.rb new file mode 100644 index 00000000000..b7d063596c1 --- /dev/null +++ b/spec/features/projects/blobs/user_creates_new_blob_in_new_project_spec.rb @@ -0,0 +1,52 @@ +require 'spec_helper' + +feature 'User creates blob in new project', :js do + let(:user) { create(:user) } + let(:project) { create(:project, :empty_repo) } + + shared_examples 'creating a file' do + before do + sign_in(user) + visit project_path(project) + end + + it 'allows the user to add a new file' do + click_link 'New file' + + find('#editor') + execute_script('ace.edit("editor").setValue("Hello world")') + + fill_in(:file_name, with: 'dummy-file') + + click_button('Commit changes') + + expect(page).to have_content('The file has been successfully created') + end + end + + describe 'as a master' do + before do + project.add_master(user) + end + + it_behaves_like 'creating a file' + end + + describe 'as an admin' do + let(:user) { create(:user, :admin) } + + it_behaves_like 'creating a file' + end + + describe 'as a developer' do + before do + project.add_developer(user) + sign_in(user) + visit project_path(project) + end + + it 'does not allow pushing to the default branch' do + expect(page).not_to have_content('New file') + end + end +end diff --git a/spec/features/projects/files/user_find_file_spec.rb b/spec/features/projects/files/user_find_file_spec.rb new file mode 100644 index 00000000000..df405e70dd4 --- /dev/null +++ b/spec/features/projects/files/user_find_file_spec.rb @@ -0,0 +1,66 @@ +require 'spec_helper' + +describe 'User find project file' do + let(:user) { create :user } + let(:project) { create :project, :repository } + + before do + sign_in(user) + project.add_master(user) + + visit project_tree_path(project, project.repository.root_ref) + end + + def active_main_tab + find('.sidebar-top-level-items > li.active') + end + + def find_file(text) + fill_in 'file_find', with: text + end + + it 'navigates to find file by shortcut', :js do + find('body').native.send_key('t') + + expect(active_main_tab).to have_content('Repository') + expect(page).to have_selector('.file-finder-holder', count: 1) + end + + it 'navigates to find file' do + click_link 'Find file' + + expect(active_main_tab).to have_content('Repository') + expect(page).to have_selector('.file-finder-holder', count: 1) + end + + it 'searches CHANGELOG file', :js do + click_link 'Find file' + + find_file 'change' + + expect(page).to have_content('CHANGELOG') + expect(page).not_to have_content('.gitignore') + expect(page).not_to have_content('VERSION') + end + + it 'does not find file when search not exist file', :js do + click_link 'Find file' + + find_file 'asdfghjklqwertyuizxcvbnm' + + expect(page).not_to have_content('CHANGELOG') + expect(page).not_to have_content('.gitignore') + expect(page).not_to have_content('VERSION') + end + + it 'searches file by partially matches', :js do + click_link 'Find file' + + find_file 'git' + + expect(page).to have_content('.gitignore') + expect(page).to have_content('.gitmodules') + expect(page).not_to have_content('CHANGELOG') + expect(page).not_to have_content('VERSION') + end +end diff --git a/spec/features/projects/jobs/user_browses_jobs_spec.rb b/spec/features/projects/jobs/user_browses_jobs_spec.rb index 36ebbeadd4a..786ec327b92 100644 --- a/spec/features/projects/jobs/user_browses_jobs_spec.rb +++ b/spec/features/projects/jobs/user_browses_jobs_spec.rb @@ -26,7 +26,7 @@ describe 'User browses jobs' do page.within('.nav-controls') do ci_lint_tool_link = page.find_link('CI lint') - expect(ci_lint_tool_link[:href]).to end_with(ci_lint_path) + expect(ci_lint_tool_link[:href]).to end_with(project_ci_lint_path(project)) end end diff --git a/spec/features/projects/new_project_spec.rb b/spec/features/projects/new_project_spec.rb index a5954fec54b..fee6287558e 100644 --- a/spec/features/projects/new_project_spec.rb +++ b/spec/features/projects/new_project_spec.rb @@ -64,7 +64,7 @@ feature 'New project' do end context 'with group namespace' do - let(:group) { create(:group, :private, owner: user) } + let(:group) { create(:group, :private) } before do group.add_owner(user) @@ -81,7 +81,7 @@ feature 'New project' do end context 'with subgroup namespace' do - let(:group) { create(:group, owner: user) } + let(:group) { create(:group) } let(:subgroup) { create(:group, parent: group) } before do diff --git a/spec/features/projects/settings/pipelines_settings_spec.rb b/spec/features/projects/settings/pipelines_settings_spec.rb index d9020333f28..e875a88a52b 100644 --- a/spec/features/projects/settings/pipelines_settings_spec.rb +++ b/spec/features/projects/settings/pipelines_settings_spec.rb @@ -8,6 +8,7 @@ describe "Projects > Settings > Pipelines settings" do before do sign_in(user) project.add_role(user, role) + create(:project_auto_devops, project: project) end context 'for developer' do @@ -27,10 +28,17 @@ describe "Projects > Settings > Pipelines settings" do visit project_settings_ci_cd_path(project) fill_in('Test coverage parsing', with: 'coverage_regex') - click_on 'Save changes' + + page.within '#js-general-pipeline-settings' do + click_on 'Save changes' + end expect(page.status_code).to eq(200) - expect(page).to have_button('Save changes', disabled: false) + + page.within '#js-general-pipeline-settings' do + expect(page).to have_button('Save changes', disabled: false) + end + expect(page).to have_field('Test coverage parsing', with: 'coverage_regex') end @@ -38,10 +46,15 @@ describe "Projects > Settings > Pipelines settings" do visit project_settings_ci_cd_path(project) page.check('Auto-cancel redundant, pending pipelines') - click_on 'Save changes' + page.within '#js-general-pipeline-settings' do + click_on 'Save changes' + end expect(page.status_code).to eq(200) - expect(page).to have_button('Save changes', disabled: false) + + page.within '#js-general-pipeline-settings' do + expect(page).to have_button('Save changes', disabled: false) + end checkbox = find_field('project_auto_cancel_pending_pipelines') expect(checkbox).to be_checked @@ -51,13 +64,16 @@ describe "Projects > Settings > Pipelines settings" do it 'update auto devops settings' do visit project_settings_ci_cd_path(project) - fill_in('project_auto_devops_attributes_domain', with: 'test.com') - page.choose('project_auto_devops_attributes_enabled_false') - click_on 'Save changes' + page.within '#autodevops-settings' do + fill_in('project_auto_devops_attributes_domain', with: 'test.com') + page.choose('project_auto_devops_attributes_enabled_false') + click_on 'Save changes' + end expect(page.status_code).to eq(200) expect(project.auto_devops).to be_present expect(project.auto_devops).not_to be_enabled + expect(project.auto_devops.domain).to eq('test.com') end end end diff --git a/spec/features/projects/show/user_sees_setup_shortcut_buttons_spec.rb b/spec/features/projects/show/user_sees_setup_shortcut_buttons_spec.rb index a906fa20233..e44361fbe26 100644 --- a/spec/features/projects/show/user_sees_setup_shortcut_buttons_spec.rb +++ b/spec/features/projects/show/user_sees_setup_shortcut_buttons_spec.rb @@ -65,7 +65,7 @@ describe 'Projects > Show > User sees setup shortcut buttons' do describe 'Auto DevOps button' do it '"Enable Auto DevOps" button linked to settings page' do page.within('.project-stats') do - expect(page).to have_link('Enable Auto DevOps', href: project_settings_ci_cd_path(project, anchor: 'js-general-pipeline-settings')) + expect(page).to have_link('Enable Auto DevOps', href: project_settings_ci_cd_path(project, anchor: 'autodevops-settings')) end end @@ -75,7 +75,7 @@ describe 'Projects > Show > User sees setup shortcut buttons' do visit project_path(project) page.within('.project-stats') do - expect(page).to have_link('Auto DevOps enabled', href: project_settings_ci_cd_path(project, anchor: 'js-general-pipeline-settings')) + expect(page).to have_link('Auto DevOps enabled', href: project_settings_ci_cd_path(project, anchor: 'autodevops-settings')) end end end @@ -212,7 +212,7 @@ describe 'Projects > Show > User sees setup shortcut buttons' do describe 'Auto DevOps button' do it '"Enable Auto DevOps" button linked to settings page' do page.within('.project-stats') do - expect(page).to have_link('Enable Auto DevOps', href: project_settings_ci_cd_path(project, anchor: 'js-general-pipeline-settings')) + expect(page).to have_link('Enable Auto DevOps', href: project_settings_ci_cd_path(project, anchor: 'autodevops-settings')) end end @@ -222,7 +222,7 @@ describe 'Projects > Show > User sees setup shortcut buttons' do visit project_path(project) page.within('.project-stats') do - expect(page).to have_link('Auto DevOps enabled', href: project_settings_ci_cd_path(project, anchor: 'js-general-pipeline-settings')) + expect(page).to have_link('Auto DevOps enabled', href: project_settings_ci_cd_path(project, anchor: 'autodevops-settings')) end end diff --git a/spec/features/projects/user_views_empty_project_spec.rb b/spec/features/projects/user_views_empty_project_spec.rb new file mode 100644 index 00000000000..7b982301ffc --- /dev/null +++ b/spec/features/projects/user_views_empty_project_spec.rb @@ -0,0 +1,43 @@ +require 'spec_helper' + +describe 'User views an empty project' do + let(:project) { create(:project, :empty_repo) } + let(:user) { create(:user) } + + shared_examples 'allowing push to default branch' do + before do + sign_in(user) + visit project_path(project) + end + + it 'shows push-to-master instructions' do + expect(page).to have_content('git push -u origin master') + end + end + + describe 'as a master' do + before do + project.add_master(user) + end + + it_behaves_like 'allowing push to default branch' + end + + describe 'as an admin' do + let(:user) { create(:user, :admin) } + + it_behaves_like 'allowing push to default branch' + end + + describe 'as a developer' do + before do + project.add_developer(user) + sign_in(user) + visit project_path(project) + end + + it 'does not show push-to-master instructions' do + expect(page).not_to have_content('git push -u origin master') + end + end +end diff --git a/spec/helpers/gitlab_routing_helper_spec.rb b/spec/helpers/gitlab_routing_helper_spec.rb index 6c4f7050ee0..143b28728a3 100644 --- a/spec/helpers/gitlab_routing_helper_spec.rb +++ b/spec/helpers/gitlab_routing_helper_spec.rb @@ -89,4 +89,19 @@ describe GitlabRoutingHelper do expect(preview_markdown_path(project)).to eq("/#{project.full_path}/preview_markdown") end end + + describe '#edit_milestone_path' do + it 'returns group milestone edit path when given entity parent is a Group' do + group = create(:group) + milestone = create(:milestone, group: group) + + expect(edit_milestone_path(milestone)).to eq("/groups/#{group.path}/-/milestones/#{milestone.iid}/edit") + end + + it 'returns project milestone edit path when given entity parent is not a Group' do + milestone = create(:milestone, group: nil) + + expect(edit_milestone_path(milestone)).to eq("/#{milestone.project.full_path}/milestones/#{milestone.iid}/edit") + end + end end diff --git a/spec/javascripts/.eslintrc b/spec/javascripts/.eslintrc index 3d922021978..9eb0e732572 100644 --- a/spec/javascripts/.eslintrc +++ b/spec/javascripts/.eslintrc @@ -18,6 +18,7 @@ "sandbox": false, "setFixtures": false, "setStyleFixtures": false, + "spyOnDependency": false, "spyOnEvent": false, "ClassSpecHelper": false }, diff --git a/spec/javascripts/activities_spec.js b/spec/javascripts/activities_spec.js index 909a1bf76bc..5dbdcd24296 100644 --- a/spec/javascripts/activities_spec.js +++ b/spec/javascripts/activities_spec.js @@ -3,24 +3,30 @@ import $ from 'jquery'; import 'vendor/jquery.endless-scroll'; import Activities from '~/activities'; +import Pager from '~/pager'; -(() => { +describe('Activities', () => { window.gon || (window.gon = {}); const fixtureTemplate = 'static/event_filter.html.raw'; const filters = [ { id: 'all', - }, { + }, + { id: 'push', name: 'push events', - }, { + }, + { id: 'merged', name: 'merge events', - }, { + }, + { id: 'comments', - }, { + }, + { id: 'team', - }]; + }, + ]; function getEventName(index) { const filter = filters[index]; @@ -32,31 +38,34 @@ import Activities from '~/activities'; return `#${filter.id}_event_filter`; } - describe('Activities', () => { - beforeEach(() => { - loadFixtures(fixtureTemplate); - new Activities(); - }); - - for (let i = 0; i < filters.length; i += 1) { - ((i) => { - describe(`when selecting ${getEventName(i)}`, () => { - beforeEach(() => { - $(getSelector(i)).click(); - }); - - for (let x = 0; x < filters.length; x += 1) { - ((x) => { - const shouldHighlight = i === x; - const testName = shouldHighlight ? 'should highlight' : 'should not highlight'; - - it(`${testName} ${getEventName(x)}`, () => { - expect($(getSelector(x)).parent().hasClass('active')).toEqual(shouldHighlight); - }); - })(x); - } - }); - })(i); - } + beforeEach(() => { + loadFixtures(fixtureTemplate); + spyOn(Pager, 'init').and.stub(); + new Activities(); }); -})(); + + for (let i = 0; i < filters.length; i += 1) { + (i => { + describe(`when selecting ${getEventName(i)}`, () => { + beforeEach(() => { + $(getSelector(i)).click(); + }); + + for (let x = 0; x < filters.length; x += 1) { + (x => { + const shouldHighlight = i === x; + const testName = shouldHighlight ? 'should highlight' : 'should not highlight'; + + it(`${testName} ${getEventName(x)}`, () => { + expect( + $(getSelector(x)) + .parent() + .hasClass('active'), + ).toEqual(shouldHighlight); + }); + })(x); + } + }); + })(i); + } +}); diff --git a/spec/javascripts/behaviors/quick_submit_spec.js b/spec/javascripts/behaviors/quick_submit_spec.js index c37c62c63dd..d03836d10f9 100644 --- a/spec/javascripts/behaviors/quick_submit_spec.js +++ b/spec/javascripts/behaviors/quick_submit_spec.js @@ -1,7 +1,7 @@ import $ from 'jquery'; import '~/behaviors/quick_submit'; -describe('Quick Submit behavior', () => { +describe('Quick Submit behavior', function () { const keydownEvent = (options = { keyCode: 13, metaKey: true }) => $.Event('keydown', options); preloadFixtures('merge_requests/merge_request_with_task_list.html.raw'); diff --git a/spec/javascripts/blob/blob_file_dropzone_spec.js b/spec/javascripts/blob/blob_file_dropzone_spec.js index 0b1de504435..346f795c3f5 100644 --- a/spec/javascripts/blob/blob_file_dropzone_spec.js +++ b/spec/javascripts/blob/blob_file_dropzone_spec.js @@ -1,7 +1,7 @@ import $ from 'jquery'; import BlobFileDropzone from '~/blob/blob_file_dropzone'; -describe('BlobFileDropzone', () => { +describe('BlobFileDropzone', function () { preloadFixtures('blob/show.html.raw'); beforeEach(() => { diff --git a/spec/javascripts/comment_type_toggle_spec.js b/spec/javascripts/comment_type_toggle_spec.js index dfd0810d52e..0ba709298c5 100644 --- a/spec/javascripts/comment_type_toggle_spec.js +++ b/spec/javascripts/comment_type_toggle_spec.js @@ -1,5 +1,4 @@ import CommentTypeToggle from '~/comment_type_toggle'; -import * as dropLabSrc from '~/droplab/drop_lab'; import InputSetter from '~/droplab/plugins/input_setter'; describe('CommentTypeToggle', function () { @@ -59,14 +58,14 @@ describe('CommentTypeToggle', function () { this.droplab = jasmine.createSpyObj('droplab', ['init']); - spyOn(dropLabSrc, 'default').and.returnValue(this.droplab); + this.droplabConstructor = spyOnDependency(CommentTypeToggle, 'DropLab').and.returnValue(this.droplab); spyOn(this.commentTypeToggle, 'setConfig').and.returnValue(this.config); CommentTypeToggle.prototype.initDroplab.call(this.commentTypeToggle); }); it('should instantiate a DropLab instance', function () { - expect(dropLabSrc.default).toHaveBeenCalled(); + expect(this.droplabConstructor).toHaveBeenCalled(); }); it('should set .droplab', function () { diff --git a/spec/javascripts/commit/pipelines/pipelines_spec.js b/spec/javascripts/commit/pipelines/pipelines_spec.js index 53820770f3f..819ed7896ca 100644 --- a/spec/javascripts/commit/pipelines/pipelines_spec.js +++ b/spec/javascripts/commit/pipelines/pipelines_spec.js @@ -4,7 +4,7 @@ import axios from '~/lib/utils/axios_utils'; import pipelinesTable from '~/commit/pipelines/pipelines_table.vue'; import mountComponent from 'spec/helpers/vue_mount_component_helper'; -describe('Pipelines table in Commits and Merge requests', () => { +describe('Pipelines table in Commits and Merge requests', function () { const jsonFixtureName = 'pipelines/pipelines.json'; let pipeline; let PipelinesTable; diff --git a/spec/javascripts/commits_spec.js b/spec/javascripts/commits_spec.js index 977298b9221..60d100e8544 100644 --- a/spec/javascripts/commits_spec.js +++ b/spec/javascripts/commits_spec.js @@ -3,6 +3,7 @@ import 'vendor/jquery.endless-scroll'; import MockAdapter from 'axios-mock-adapter'; import axios from '~/lib/utils/axios_utils'; import CommitsList from '~/commits'; +import Pager from '~/pager'; describe('Commits List', () => { let commitsList; @@ -14,6 +15,7 @@ describe('Commits List', () => { </form> <ol id="commits-list"></ol> `); + spyOn(Pager, 'init').and.stub(); commitsList = new CommitsList(25); }); @@ -68,9 +70,10 @@ describe('Commits List', () => { mock.restore(); }); - it('should save the last search string', (done) => { + it('should save the last search string', done => { commitsList.searchField.val('GitLab'); - commitsList.filterResults() + commitsList + .filterResults() .then(() => { expect(ajaxSpy).toHaveBeenCalled(); expect(commitsList.lastSearch).toEqual('GitLab'); @@ -80,8 +83,9 @@ describe('Commits List', () => { .catch(done.fail); }); - it('should not make ajax call if the input does not change', (done) => { - commitsList.filterResults() + it('should not make ajax call if the input does not change', done => { + commitsList + .filterResults() .then(() => { expect(ajaxSpy).not.toHaveBeenCalled(); expect(commitsList.lastSearch).toEqual(''); diff --git a/spec/javascripts/droplab/hook_spec.js b/spec/javascripts/droplab/hook_spec.js index 3d39bd0812b..5eed1db2750 100644 --- a/spec/javascripts/droplab/hook_spec.js +++ b/spec/javascripts/droplab/hook_spec.js @@ -1,5 +1,4 @@ import Hook from '~/droplab/hook'; -import * as dropdownSrc from '~/droplab/drop_down'; describe('Hook', function () { describe('class constructor', function () { @@ -10,7 +9,7 @@ describe('Hook', function () { this.config = {}; this.dropdown = {}; - spyOn(dropdownSrc, 'default').and.returnValue(this.dropdown); + this.dropdownConstructor = spyOnDependency(Hook, 'DropDown').and.returnValue(this.dropdown); this.hook = new Hook(this.trigger, this.list, this.plugins, this.config); }); @@ -24,7 +23,7 @@ describe('Hook', function () { }); it('should call DropDown constructor', function () { - expect(dropdownSrc.default).toHaveBeenCalledWith(this.list, this.config); + expect(this.dropdownConstructor).toHaveBeenCalledWith(this.list, this.config); }); it('should set .type', function () { diff --git a/spec/javascripts/filtered_search/filtered_search_manager_spec.js b/spec/javascripts/filtered_search/filtered_search_manager_spec.js index 95d02974bdc..8fcee36beb8 100644 --- a/spec/javascripts/filtered_search/filtered_search_manager_spec.js +++ b/spec/javascripts/filtered_search/filtered_search_manager_spec.js @@ -1,5 +1,3 @@ -import * as urlUtils from '~/lib/utils/url_utility'; -import * as recentSearchesStoreSrc from '~/filtered_search/stores/recent_searches_store'; import RecentSearchesService from '~/filtered_search/services/recent_searches_service'; import RecentSearchesServiceError from '~/filtered_search/services/recent_searches_service_error'; import RecentSearchesRoot from '~/filtered_search/recent_searches_root'; @@ -11,7 +9,7 @@ import FilteredSearchDropdownManager from '~/filtered_search/filtered_search_dro import FilteredSearchManager from '~/filtered_search/filtered_search_manager'; import FilteredSearchSpecHelper from '../helpers/filtered_search_spec_helper'; -describe('Filtered Search Manager', () => { +describe('Filtered Search Manager', function () { let input; let manager; let tokensContainer; @@ -74,18 +72,19 @@ describe('Filtered Search Manager', () => { describe('class constructor', () => { const isLocalStorageAvailable = 'isLocalStorageAvailable'; + let RecentSearchesStoreSpy; beforeEach(() => { spyOn(RecentSearchesService, 'isAvailable').and.returnValue(isLocalStorageAvailable); - spyOn(recentSearchesStoreSrc, 'default'); spyOn(RecentSearchesRoot.prototype, 'render'); + RecentSearchesStoreSpy = spyOnDependency(FilteredSearchManager, 'RecentSearchesStore'); }); it('should instantiate RecentSearchesStore with isLocalStorageAvailable', () => { manager = new FilteredSearchManager({ page }); expect(RecentSearchesService.isAvailable).toHaveBeenCalled(); - expect(recentSearchesStoreSrc.default).toHaveBeenCalledWith({ + expect(RecentSearchesStoreSpy).toHaveBeenCalledWith({ isLocalStorageAvailable, allowedKeys: FilteredSearchTokenKeys.getKeys(), }); @@ -164,7 +163,7 @@ describe('Filtered Search Manager', () => { it('should search with a single word', (done) => { input.value = 'searchTerm'; - spyOn(urlUtils, 'visitUrl').and.callFake((url) => { + spyOnDependency(FilteredSearchManager, 'visitUrl').and.callFake((url) => { expect(url).toEqual(`${defaultParams}&search=searchTerm`); done(); }); @@ -175,7 +174,7 @@ describe('Filtered Search Manager', () => { it('should search with multiple words', (done) => { input.value = 'awesome search terms'; - spyOn(urlUtils, 'visitUrl').and.callFake((url) => { + spyOnDependency(FilteredSearchManager, 'visitUrl').and.callFake((url) => { expect(url).toEqual(`${defaultParams}&search=awesome+search+terms`); done(); }); @@ -186,7 +185,7 @@ describe('Filtered Search Manager', () => { it('should search with special characters', (done) => { input.value = '~!@#$%^&*()_+{}:<>,.?/'; - spyOn(urlUtils, 'visitUrl').and.callFake((url) => { + spyOnDependency(FilteredSearchManager, 'visitUrl').and.callFake((url) => { expect(url).toEqual(`${defaultParams}&search=~!%40%23%24%25%5E%26*()_%2B%7B%7D%3A%3C%3E%2C.%3F%2F`); done(); }); @@ -200,7 +199,7 @@ describe('Filtered Search Manager', () => { ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug')} `); - spyOn(urlUtils, 'visitUrl').and.callFake((url) => { + spyOnDependency(FilteredSearchManager, 'visitUrl').and.callFake((url) => { expect(url).toEqual(`${defaultParams}&label_name[]=bug`); done(); }); diff --git a/spec/javascripts/filtered_search/recent_searches_root_spec.js b/spec/javascripts/filtered_search/recent_searches_root_spec.js index d8ba6de5f45..1e6272bad0b 100644 --- a/spec/javascripts/filtered_search/recent_searches_root_spec.js +++ b/spec/javascripts/filtered_search/recent_searches_root_spec.js @@ -1,11 +1,11 @@ import RecentSearchesRoot from '~/filtered_search/recent_searches_root'; -import * as vueSrc from 'vue'; describe('RecentSearchesRoot', () => { describe('render', () => { let recentSearchesRoot; let data; let template; + let VueSpy; beforeEach(() => { recentSearchesRoot = { @@ -14,7 +14,7 @@ describe('RecentSearchesRoot', () => { }, }; - spyOn(vueSrc, 'default').and.callFake((options) => { + VueSpy = spyOnDependency(RecentSearchesRoot, 'Vue').and.callFake((options) => { data = options.data; template = options.template; }); @@ -23,7 +23,7 @@ describe('RecentSearchesRoot', () => { }); it('should instantiate Vue', () => { - expect(vueSrc.default).toHaveBeenCalled(); + expect(VueSpy).toHaveBeenCalled(); expect(data()).toBe(recentSearchesRoot.store.state); expect(template).toContain(':is-local-storage-available="isLocalStorageAvailable"'); }); diff --git a/spec/javascripts/gl_dropdown_spec.js b/spec/javascripts/gl_dropdown_spec.js index 5393502196e..7f9c4811fba 100644 --- a/spec/javascripts/gl_dropdown_spec.js +++ b/spec/javascripts/gl_dropdown_spec.js @@ -1,9 +1,8 @@ /* eslint-disable comma-dangle, no-param-reassign, no-unused-expressions, max-len */ import $ from 'jquery'; -import '~/gl_dropdown'; +import GLDropdown from '~/gl_dropdown'; import '~/lib/utils/common_utils'; -import * as urlUtils from '~/lib/utils/url_utility'; describe('glDropdown', function describeDropdown() { preloadFixtures('static/gl_dropdown.html.raw'); @@ -138,13 +137,13 @@ describe('glDropdown', function describeDropdown() { expect(this.dropdownContainerElement).toHaveClass('open'); const randomIndex = Math.floor(Math.random() * (this.projectsData.length - 1)) + 0; navigateWithKeys('down', randomIndex, () => { - spyOn(urlUtils, 'visitUrl').and.stub(); + const visitUrl = spyOnDependency(GLDropdown, 'visitUrl').and.stub(); navigateWithKeys('enter', null, () => { expect(this.dropdownContainerElement).not.toHaveClass('open'); const link = $(`${ITEM_SELECTOR}:eq(${randomIndex}) a`, this.$dropdownMenuElement); expect(link).toHaveClass('is-active'); const linkedLocation = link.attr('href'); - if (linkedLocation && linkedLocation !== '#') expect(urlUtils.visitUrl).toHaveBeenCalledWith(linkedLocation); + if (linkedLocation && linkedLocation !== '#') expect(visitUrl).toHaveBeenCalledWith(linkedLocation); }); }); }); diff --git a/spec/javascripts/groups/components/app_spec.js b/spec/javascripts/groups/components/app_spec.js index d8428bd0e08..2b92c485f41 100644 --- a/spec/javascripts/groups/components/app_spec.js +++ b/spec/javascripts/groups/components/app_spec.js @@ -1,7 +1,6 @@ import $ from 'jquery'; import Vue from 'vue'; -import * as utils from '~/lib/utils/url_utility'; import appComponent from '~/groups/components/app.vue'; import groupFolderComponent from '~/groups/components/group_folder.vue'; import groupItemComponent from '~/groups/components/group_item.vue'; @@ -177,7 +176,7 @@ describe('AppComponent', () => { it('should fetch groups for provided page details and update window state', (done) => { spyOn(vm, 'fetchGroups').and.returnValue(returnServicePromise(mockGroups)); spyOn(vm, 'updateGroups').and.callThrough(); - spyOn(utils, 'mergeUrlParams').and.callThrough(); + const mergeUrlParams = spyOnDependency(appComponent, 'mergeUrlParams').and.callThrough(); spyOn(window.history, 'replaceState'); spyOn($, 'scrollTo'); @@ -193,7 +192,7 @@ describe('AppComponent', () => { setTimeout(() => { expect(vm.isLoading).toBe(false); expect($.scrollTo).toHaveBeenCalledWith(0); - expect(utils.mergeUrlParams).toHaveBeenCalledWith({ page: 2 }, jasmine.any(String)); + expect(mergeUrlParams).toHaveBeenCalledWith({ page: 2 }, jasmine.any(String)); expect(window.history.replaceState).toHaveBeenCalledWith({ page: jasmine.any(String), }, jasmine.any(String), jasmine.any(String)); diff --git a/spec/javascripts/groups/components/group_item_spec.js b/spec/javascripts/groups/components/group_item_spec.js index e3c942597a3..49a139855c8 100644 --- a/spec/javascripts/groups/components/group_item_spec.js +++ b/spec/javascripts/groups/components/group_item_spec.js @@ -1,5 +1,4 @@ import Vue from 'vue'; -import * as urlUtils from '~/lib/utils/url_utility'; import groupItemComponent from '~/groups/components/group_item.vue'; import groupFolderComponent from '~/groups/components/group_folder.vue'; import eventHub from '~/groups/event_hub'; @@ -135,13 +134,13 @@ describe('GroupItemComponent', () => { const group = Object.assign({}, mockParentGroupItem); group.childrenCount = 0; const newVm = createComponent(group); - spyOn(urlUtils, 'visitUrl').and.stub(); + const visitUrl = spyOnDependency(groupItemComponent, 'visitUrl').and.stub(); spyOn(eventHub, '$emit'); newVm.onClickRowGroup(event); setTimeout(() => { expect(eventHub.$emit).not.toHaveBeenCalled(); - expect(urlUtils.visitUrl).toHaveBeenCalledWith(newVm.group.relativePath); + expect(visitUrl).toHaveBeenCalledWith(newVm.group.relativePath); done(); }, 0); }); diff --git a/spec/javascripts/helpers/class_spec_helper_spec.js b/spec/javascripts/helpers/class_spec_helper_spec.js index 1415ffb7eb3..fa104ae5bcd 100644 --- a/spec/javascripts/helpers/class_spec_helper_spec.js +++ b/spec/javascripts/helpers/class_spec_helper_spec.js @@ -2,7 +2,7 @@ import './class_spec_helper'; -describe('ClassSpecHelper', () => { +describe('ClassSpecHelper', function () { describe('itShouldBeAStaticMethod', () => { beforeEach(() => { class TestClass { diff --git a/spec/javascripts/ide/components/file_finder/index_spec.js b/spec/javascripts/ide/components/file_finder/index_spec.js new file mode 100644 index 00000000000..4f208e946d2 --- /dev/null +++ b/spec/javascripts/ide/components/file_finder/index_spec.js @@ -0,0 +1,308 @@ +import Vue from 'vue'; +import store from '~/ide/stores'; +import FindFileComponent from '~/ide/components/file_finder/index.vue'; +import { UP_KEY_CODE, DOWN_KEY_CODE, ENTER_KEY_CODE, ESC_KEY_CODE } from '~/lib/utils/keycodes'; +import router from '~/ide/ide_router'; +import { file, resetStore } from '../../helpers'; +import { mountComponentWithStore } from '../../../helpers/vue_mount_component_helper'; + +describe('IDE File finder item spec', () => { + const Component = Vue.extend(FindFileComponent); + let vm; + + beforeEach(done => { + setFixtures('<div id="app"></div>'); + + vm = mountComponentWithStore(Component, { + store, + el: '#app', + props: { + index: 0, + }, + }); + + setTimeout(done); + }); + + afterEach(() => { + vm.$destroy(); + + resetStore(vm.$store); + }); + + describe('with entries', () => { + beforeEach(done => { + Vue.set(vm.$store.state.entries, 'folder', { + ...file('folder'), + path: 'folder', + type: 'folder', + }); + + Vue.set(vm.$store.state.entries, 'index.js', { + ...file('index.js'), + path: 'index.js', + type: 'blob', + url: '/index.jsurl', + }); + + Vue.set(vm.$store.state.entries, 'component.js', { + ...file('component.js'), + path: 'component.js', + type: 'blob', + }); + + setTimeout(done); + }); + + it('renders list of blobs', () => { + expect(vm.$el.textContent).toContain('index.js'); + expect(vm.$el.textContent).toContain('component.js'); + expect(vm.$el.textContent).not.toContain('folder'); + }); + + it('filters entries', done => { + vm.searchText = 'index'; + + vm.$nextTick(() => { + expect(vm.$el.textContent).toContain('index.js'); + expect(vm.$el.textContent).not.toContain('component.js'); + + done(); + }); + }); + + it('shows clear button when searchText is not empty', done => { + vm.searchText = 'index'; + + vm.$nextTick(() => { + expect(vm.$el.querySelector('.dropdown-input-clear').classList).toContain('show'); + expect(vm.$el.querySelector('.dropdown-input-search').classList).toContain('hidden'); + + done(); + }); + }); + + it('clear button resets searchText', done => { + vm.searchText = 'index'; + + vm + .$nextTick() + .then(() => { + vm.$el.querySelector('.dropdown-input-clear').click(); + }) + .then(vm.$nextTick) + .then(() => { + expect(vm.searchText).toBe(''); + }) + .then(done) + .catch(done.fail); + }); + + it('clear button focues search input', done => { + spyOn(vm.$refs.searchInput, 'focus'); + vm.searchText = 'index'; + + vm + .$nextTick() + .then(() => { + vm.$el.querySelector('.dropdown-input-clear').click(); + }) + .then(vm.$nextTick) + .then(() => { + expect(vm.$refs.searchInput.focus).toHaveBeenCalled(); + }) + .then(done) + .catch(done.fail); + }); + + describe('listShowCount', () => { + it('returns 1 when no filtered entries exist', done => { + vm.searchText = 'testing 123'; + + vm.$nextTick(() => { + expect(vm.listShowCount).toBe(1); + + done(); + }); + }); + + it('returns entries length when not filtered', () => { + expect(vm.listShowCount).toBe(2); + }); + }); + + describe('listHeight', () => { + it('returns 55 when entries exist', () => { + expect(vm.listHeight).toBe(55); + }); + + it('returns 33 when entries dont exist', done => { + vm.searchText = 'testing 123'; + + vm.$nextTick(() => { + expect(vm.listHeight).toBe(33); + + done(); + }); + }); + }); + + describe('filteredBlobsLength', () => { + it('returns length of filtered blobs', done => { + vm.searchText = 'index'; + + vm.$nextTick(() => { + expect(vm.filteredBlobsLength).toBe(1); + + done(); + }); + }); + }); + + describe('watches', () => { + describe('searchText', () => { + it('resets focusedIndex when updated', done => { + vm.focusedIndex = 1; + vm.searchText = 'test'; + + vm.$nextTick(() => { + expect(vm.focusedIndex).toBe(0); + + done(); + }); + }); + }); + + describe('fileFindVisible', () => { + it('returns searchText when false', done => { + vm.searchText = 'test'; + vm.$store.state.fileFindVisible = true; + + vm + .$nextTick() + .then(() => { + vm.$store.state.fileFindVisible = false; + }) + .then(vm.$nextTick) + .then(() => { + expect(vm.searchText).toBe(''); + }) + .then(done) + .catch(done.fail); + }); + }); + }); + + describe('openFile', () => { + beforeEach(() => { + spyOn(router, 'push'); + spyOn(vm, 'toggleFileFinder'); + }); + + it('closes file finder', () => { + vm.openFile(vm.$store.state.entries['index.js']); + + expect(vm.toggleFileFinder).toHaveBeenCalled(); + }); + + it('pushes to router', () => { + vm.openFile(vm.$store.state.entries['index.js']); + + expect(router.push).toHaveBeenCalledWith('/project/index.jsurl'); + }); + }); + + describe('onKeyup', () => { + it('opens file on enter key', done => { + const event = new CustomEvent('keyup'); + event.keyCode = ENTER_KEY_CODE; + + spyOn(vm, 'openFile'); + + vm.$refs.searchInput.dispatchEvent(event); + + vm.$nextTick(() => { + expect(vm.openFile).toHaveBeenCalledWith(vm.$store.state.entries['index.js']); + + done(); + }); + }); + + it('closes file finder on esc key', done => { + const event = new CustomEvent('keyup'); + event.keyCode = ESC_KEY_CODE; + + spyOn(vm, 'toggleFileFinder'); + + vm.$refs.searchInput.dispatchEvent(event); + + vm.$nextTick(() => { + expect(vm.toggleFileFinder).toHaveBeenCalled(); + + done(); + }); + }); + }); + + describe('onKeyDown', () => { + let el; + + beforeEach(() => { + el = vm.$refs.searchInput; + }); + + describe('up key', () => { + const event = new CustomEvent('keydown'); + event.keyCode = UP_KEY_CODE; + + it('resets to last index when at top', () => { + el.dispatchEvent(event); + + expect(vm.focusedIndex).toBe(1); + }); + + it('minus 1 from focusedIndex', () => { + vm.focusedIndex = 1; + + el.dispatchEvent(event); + + expect(vm.focusedIndex).toBe(0); + }); + }); + + describe('down key', () => { + const event = new CustomEvent('keydown'); + event.keyCode = DOWN_KEY_CODE; + + it('resets to first index when at bottom', () => { + vm.focusedIndex = 1; + el.dispatchEvent(event); + + expect(vm.focusedIndex).toBe(0); + }); + + it('adds 1 to focusedIndex', () => { + el.dispatchEvent(event); + + expect(vm.focusedIndex).toBe(1); + }); + }); + }); + }); + + describe('without entries', () => { + it('renders loading text when loading', done => { + store.state.loading = true; + + vm.$nextTick(() => { + expect(vm.$el.textContent).toContain('Loading...'); + + done(); + }); + }); + + it('renders no files text', () => { + expect(vm.$el.textContent).toContain('No files found.'); + }); + }); +}); diff --git a/spec/javascripts/ide/components/file_finder/item_spec.js b/spec/javascripts/ide/components/file_finder/item_spec.js new file mode 100644 index 00000000000..0f1116c6912 --- /dev/null +++ b/spec/javascripts/ide/components/file_finder/item_spec.js @@ -0,0 +1,140 @@ +import Vue from 'vue'; +import ItemComponent from '~/ide/components/file_finder/item.vue'; +import { file } from '../../helpers'; +import createComponent from '../../../helpers/vue_mount_component_helper'; + +describe('IDE File finder item spec', () => { + const Component = Vue.extend(ItemComponent); + let vm; + let localFile; + + beforeEach(() => { + localFile = { + ...file(), + name: 'test file', + path: 'test/file', + }; + + vm = createComponent(Component, { + file: localFile, + focused: true, + searchText: '', + index: 0, + }); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('renders file name & path', () => { + expect(vm.$el.textContent).toContain('test file'); + expect(vm.$el.textContent).toContain('test/file'); + }); + + describe('focused', () => { + it('adds is-focused class', () => { + expect(vm.$el.classList).toContain('is-focused'); + }); + + it('does not have is-focused class when not focused', done => { + vm.focused = false; + + vm.$nextTick(() => { + expect(vm.$el.classList).not.toContain('is-focused'); + + done(); + }); + }); + }); + + describe('changed file icon', () => { + it('does not render when not a changed or temp file', () => { + expect(vm.$el.querySelector('.diff-changed-stats')).toBe(null); + }); + + it('renders when a changed file', done => { + vm.file.changed = true; + + vm.$nextTick(() => { + expect(vm.$el.querySelector('.diff-changed-stats')).not.toBe(null); + + done(); + }); + }); + + it('renders when a temp file', done => { + vm.file.tempFile = true; + + vm.$nextTick(() => { + expect(vm.$el.querySelector('.diff-changed-stats')).not.toBe(null); + + done(); + }); + }); + }); + + it('emits event when clicked', () => { + spyOn(vm, '$emit'); + + vm.$el.click(); + + expect(vm.$emit).toHaveBeenCalledWith('click', vm.file); + }); + + describe('path', () => { + let el; + + beforeEach(done => { + vm.searchText = 'file'; + + el = vm.$el.querySelector('.diff-changed-file-path'); + + vm.$nextTick(done); + }); + + it('highlights text', () => { + expect(el.querySelectorAll('.highlighted').length).toBe(4); + }); + + it('adds ellipsis to long text', done => { + vm.file.path = new Array(70) + .fill() + .map((_, i) => `${i}-`) + .join(''); + + vm.$nextTick(() => { + expect(el.textContent).toBe(`...${vm.file.path.substr(vm.file.path.length - 60)}`); + done(); + }); + }); + }); + + describe('name', () => { + let el; + + beforeEach(done => { + vm.searchText = 'file'; + + el = vm.$el.querySelector('.diff-changed-file-name'); + + vm.$nextTick(done); + }); + + it('highlights text', () => { + expect(el.querySelectorAll('.highlighted').length).toBe(4); + }); + + it('does not add ellipsis to long text', done => { + vm.file.name = new Array(70) + .fill() + .map((_, i) => `${i}-`) + .join(''); + + vm.$nextTick(() => { + expect(el.textContent).not.toBe(`...${vm.file.name.substr(vm.file.name.length - 60)}`); + done(); + }); + }); + }); +}); diff --git a/spec/javascripts/ide/components/ide_spec.js b/spec/javascripts/ide/components/ide_spec.js index 5bd890094cc..7bfcfc90572 100644 --- a/spec/javascripts/ide/components/ide_spec.js +++ b/spec/javascripts/ide/components/ide_spec.js @@ -1,4 +1,5 @@ import Vue from 'vue'; +import Mousetrap from 'mousetrap'; import store from '~/ide/stores'; import ide from '~/ide/components/ide.vue'; import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; @@ -38,4 +39,68 @@ describe('ide component', () => { done(); }); }); + + describe('file finder', () => { + beforeEach(done => { + spyOn(vm, 'toggleFileFinder'); + + vm.$store.state.fileFindVisible = true; + + vm.$nextTick(done); + }); + + it('calls toggleFileFinder on `t` key press', done => { + Mousetrap.trigger('t'); + + vm + .$nextTick() + .then(() => { + expect(vm.toggleFileFinder).toHaveBeenCalled(); + }) + .then(done) + .catch(done.fail); + }); + + it('calls toggleFileFinder on `command+p` key press', done => { + Mousetrap.trigger('command+p'); + + vm + .$nextTick() + .then(() => { + expect(vm.toggleFileFinder).toHaveBeenCalled(); + }) + .then(done) + .catch(done.fail); + }); + + it('calls toggleFileFinder on `ctrl+p` key press', done => { + Mousetrap.trigger('ctrl+p'); + + vm + .$nextTick() + .then(() => { + expect(vm.toggleFileFinder).toHaveBeenCalled(); + }) + .then(done) + .catch(done.fail); + }); + + it('always allows `command+p` to trigger toggleFileFinder', () => { + expect( + vm.mousetrapStopCallback(null, vm.$el.querySelector('.dropdown-input-field'), 'command+p'), + ).toBe(false); + }); + + it('always allows `ctrl+p` to trigger toggleFileFinder', () => { + expect( + vm.mousetrapStopCallback(null, vm.$el.querySelector('.dropdown-input-field'), 'ctrl+p'), + ).toBe(false); + }); + + it('onlys handles `t` when focused in input-field', () => { + expect( + vm.mousetrapStopCallback(null, vm.$el.querySelector('.dropdown-input-field'), 't'), + ).toBe(true); + }); + }); }); diff --git a/spec/javascripts/ide/stores/actions_spec.js b/spec/javascripts/ide/stores/actions_spec.js index f307e6c4d22..a64af5b941b 100644 --- a/spec/javascripts/ide/stores/actions_spec.js +++ b/spec/javascripts/ide/stores/actions_spec.js @@ -1,5 +1,9 @@ -import * as urlUtils from '~/lib/utils/url_utility'; -import * as actions from '~/ide/stores/actions'; +import actions, { + stageAllChanges, + unstageAllChanges, + toggleFileFinder, + updateTempFlagForEntry, +} from '~/ide/stores/actions'; import store from '~/ide/stores'; import * as types from '~/ide/stores/mutation_types'; import router from '~/ide/ide_router'; @@ -17,12 +21,12 @@ describe('Multi-file store actions', () => { describe('redirectToUrl', () => { it('calls visitUrl', done => { - spyOn(urlUtils, 'visitUrl'); + const visitUrl = spyOnDependency(actions, 'visitUrl'); store .dispatch('redirectToUrl', 'test') .then(() => { - expect(urlUtils.visitUrl).toHaveBeenCalledWith('test'); + expect(visitUrl).toHaveBeenCalledWith('test'); done(); }) @@ -298,7 +302,7 @@ describe('Multi-file store actions', () => { store.state.changedFiles.push(file(), file('new')); testAction( - actions.stageAllChanges, + stageAllChanges, null, store.state, [ @@ -316,7 +320,7 @@ describe('Multi-file store actions', () => { store.state.stagedFiles.push(file(), file('new')); testAction( - actions.unstageAllChanges, + unstageAllChanges, null, store.state, [ @@ -351,7 +355,7 @@ describe('Multi-file store actions', () => { store.state.entries[f.path] = f; testAction( - actions.updateTempFlagForEntry, + updateTempFlagForEntry, { file: f, tempFile: false }, store.state, [{ type: 'UPDATE_TEMP_FLAG', payload: { path: f.path, tempFile: false } }], @@ -374,7 +378,7 @@ describe('Multi-file store actions', () => { store.state.entries[f.path] = f; testAction( - actions.updateTempFlagForEntry, + updateTempFlagForEntry, { file: f, tempFile: false }, store.state, [{ type: 'UPDATE_TEMP_FLAG', payload: { path: f.path, tempFile: false } }], @@ -383,4 +387,17 @@ describe('Multi-file store actions', () => { ); }); }); + + describe('toggleFileFinder', () => { + it('commits TOGGLE_FILE_FINDER', done => { + testAction( + toggleFileFinder, + true, + null, + [{ type: 'TOGGLE_FILE_FINDER', payload: true }], + [], + done, + ); + }); + }); }); diff --git a/spec/javascripts/ide/stores/getters_spec.js b/spec/javascripts/ide/stores/getters_spec.js index 8d04b83928c..b6b4dd28729 100644 --- a/spec/javascripts/ide/stores/getters_spec.js +++ b/spec/javascripts/ide/stores/getters_spec.js @@ -64,4 +64,24 @@ describe('IDE store getters', () => { expect(getters.currentMergeRequest(localState)).toBeNull(); }); }); + + describe('allBlobs', () => { + beforeEach(() => { + Object.assign(localState.entries, { + index: { type: 'blob', name: 'index', lastOpenedAt: 0 }, + app: { type: 'blob', name: 'blob', lastOpenedAt: 0 }, + folder: { type: 'folder', name: 'folder', lastOpenedAt: 0 }, + }); + }); + + it('returns only blobs', () => { + expect(getters.allBlobs(localState).length).toBe(2); + }); + + it('returns list sorted by lastOpenedAt', () => { + localState.entries.app.lastOpenedAt = new Date().getTime(); + + expect(getters.allBlobs(localState)[0].name).toBe('blob'); + }); + }); }); diff --git a/spec/javascripts/ide/stores/modules/commit/actions_spec.js b/spec/javascripts/ide/stores/modules/commit/actions_spec.js index 116967208e0..b2b4b85ca42 100644 --- a/spec/javascripts/ide/stores/modules/commit/actions_spec.js +++ b/spec/javascripts/ide/stores/modules/commit/actions_spec.js @@ -1,7 +1,7 @@ +import actions from '~/ide/stores/actions'; import store from '~/ide/stores'; import service from '~/ide/services'; import router from '~/ide/ide_router'; -import * as urlUtils from '~/lib/utils/url_utility'; import eventHub from '~/ide/eventhub'; import * as consts from '~/ide/stores/modules/commit/constants'; import { resetStore, file } from 'spec/ide/helpers'; @@ -307,8 +307,10 @@ describe('IDE commit module actions', () => { }); describe('commitChanges', () => { + let visitUrl; + beforeEach(() => { - spyOn(urlUtils, 'visitUrl'); + visitUrl = spyOnDependency(actions, 'visitUrl'); document.body.innerHTML += '<div class="flash-container"></div>'; @@ -461,7 +463,7 @@ describe('IDE commit module actions', () => { store .dispatch('commit/commitChanges') .then(() => { - expect(urlUtils.visitUrl).toHaveBeenCalledWith( + expect(visitUrl).toHaveBeenCalledWith( `webUrl/merge_requests/new?merge_request[source_branch]=${ store.getters['commit/newBranchName'] }&merge_request[target_branch]=master`, diff --git a/spec/javascripts/ide/stores/mutations_spec.js b/spec/javascripts/ide/stores/mutations_spec.js index 3e300d69ffb..997711d1e19 100644 --- a/spec/javascripts/ide/stores/mutations_spec.js +++ b/spec/javascripts/ide/stores/mutations_spec.js @@ -108,4 +108,12 @@ describe('Multi-file store mutations', () => { expect(localState.entries.test.changed).toBe(false); }); }); + + describe('TOGGLE_FILE_FINDER', () => { + it('updates fileFindVisible', () => { + mutations.TOGGLE_FILE_FINDER(localState, true); + + expect(localState.fileFindVisible).toBe(true); + }); + }); }); diff --git a/spec/javascripts/issue_show/components/app_spec.js b/spec/javascripts/issue_show/components/app_spec.js index d5a87b5ce20..bf1f0c822fe 100644 --- a/spec/javascripts/issue_show/components/app_spec.js +++ b/spec/javascripts/issue_show/components/app_spec.js @@ -2,7 +2,6 @@ import Vue from 'vue'; import MockAdapter from 'axios-mock-adapter'; import axios from '~/lib/utils/axios_utils'; import '~/behaviors/markdown/render_gfm'; -import * as urlUtils from '~/lib/utils/url_utility'; import issuableApp from '~/issue_show/components/app.vue'; import eventHub from '~/issue_show/event_hub'; import setTimeoutPromise from 'spec/helpers/set_timeout_promise_helper'; @@ -174,7 +173,7 @@ describe('Issuable output', () => { }); it('does not redirect if issue has not moved', (done) => { - spyOn(urlUtils, 'visitUrl'); + const visitUrl = spyOnDependency(issuableApp, 'visitUrl'); spyOn(vm.service, 'updateIssuable').and.callFake(() => new Promise((resolve) => { resolve({ data: { @@ -187,16 +186,13 @@ describe('Issuable output', () => { vm.updateIssuable(); setTimeout(() => { - expect( - urlUtils.visitUrl, - ).not.toHaveBeenCalled(); - + expect(visitUrl).not.toHaveBeenCalled(); done(); }); }); it('redirects if returned web_url has changed', (done) => { - spyOn(urlUtils, 'visitUrl'); + const visitUrl = spyOnDependency(issuableApp, 'visitUrl'); spyOn(vm.service, 'updateIssuable').and.callFake(() => new Promise((resolve) => { resolve({ data: { @@ -209,10 +205,7 @@ describe('Issuable output', () => { vm.updateIssuable(); setTimeout(() => { - expect( - urlUtils.visitUrl, - ).toHaveBeenCalledWith('/testing-issue-move'); - + expect(visitUrl).toHaveBeenCalledWith('/testing-issue-move'); done(); }); }); @@ -340,7 +333,7 @@ describe('Issuable output', () => { describe('deleteIssuable', () => { it('changes URL when deleted', (done) => { - spyOn(urlUtils, 'visitUrl'); + const visitUrl = spyOnDependency(issuableApp, 'visitUrl'); spyOn(vm.service, 'deleteIssuable').and.callFake(() => new Promise((resolve) => { resolve({ data: { @@ -352,16 +345,13 @@ describe('Issuable output', () => { vm.deleteIssuable(); setTimeout(() => { - expect( - urlUtils.visitUrl, - ).toHaveBeenCalledWith('/test'); - + expect(visitUrl).toHaveBeenCalledWith('/test'); done(); }); }); it('stops polling when deleting', (done) => { - spyOn(urlUtils, 'visitUrl'); + spyOnDependency(issuableApp, 'visitUrl'); spyOn(vm.poll, 'stop').and.callThrough(); spyOn(vm.service, 'deleteIssuable').and.callFake(() => new Promise((resolve) => { resolve({ @@ -377,7 +367,6 @@ describe('Issuable output', () => { expect( vm.poll.stop, ).toHaveBeenCalledWith(); - done(); }); }); diff --git a/spec/javascripts/issue_show/components/description_spec.js b/spec/javascripts/issue_show/components/description_spec.js index d96151a8a3a..889c8545faa 100644 --- a/spec/javascripts/issue_show/components/description_spec.js +++ b/spec/javascripts/issue_show/components/description_spec.js @@ -1,7 +1,6 @@ import $ from 'jquery'; import Vue from 'vue'; -import descriptionComponent from '~/issue_show/components/description.vue'; -import * as taskList from '~/task_list'; +import Description from '~/issue_show/components/description.vue'; import mountComponent from 'spec/helpers/vue_mount_component_helper'; describe('Description component', () => { @@ -17,7 +16,7 @@ describe('Description component', () => { }; beforeEach(() => { - DescriptionComponent = Vue.extend(descriptionComponent); + DescriptionComponent = Vue.extend(Description); if (!document.querySelector('.issuable-meta')) { const metaData = document.createElement('div'); @@ -82,18 +81,20 @@ describe('Description component', () => { }); describe('TaskList', () => { + let TaskList; + beforeEach(() => { vm = mountComponent(DescriptionComponent, Object.assign({}, props, { issuableType: 'issuableType', })); - spyOn(taskList, 'default'); + TaskList = spyOnDependency(Description, 'TaskList'); }); it('re-inits the TaskList when description changed', (done) => { vm.descriptionHtml = 'changed'; setTimeout(() => { - expect(taskList.default).toHaveBeenCalled(); + expect(TaskList).toHaveBeenCalled(); done(); }); }); @@ -103,7 +104,7 @@ describe('Description component', () => { vm.descriptionHtml = 'changed'; setTimeout(() => { - expect(taskList.default).not.toHaveBeenCalled(); + expect(TaskList).not.toHaveBeenCalled(); done(); }); }); @@ -112,7 +113,7 @@ describe('Description component', () => { vm.descriptionHtml = 'changed'; setTimeout(() => { - expect(taskList.default).toHaveBeenCalledWith({ + expect(TaskList).toHaveBeenCalledWith({ dataType: 'issuableType', fieldName: 'description', selector: '.detail-page-description', diff --git a/spec/javascripts/job_spec.js b/spec/javascripts/job_spec.js index c6bbacf237a..da00b615c9b 100644 --- a/spec/javascripts/job_spec.js +++ b/spec/javascripts/job_spec.js @@ -2,7 +2,6 @@ import $ from 'jquery'; import MockAdapter from 'axios-mock-adapter'; import axios from '~/lib/utils/axios_utils'; import { numberToHumanSize } from '~/lib/utils/number_utils'; -import * as urlUtils from '~/lib/utils/url_utility'; import '~/lib/utils/datetime_utility'; import Job from '~/job'; import '~/breakpoints'; @@ -22,7 +21,7 @@ describe('Job', () => { beforeEach(() => { loadFixtures('builds/build-with-artifacts.html.raw'); - spyOn(urlUtils, 'visitUrl'); + spyOnDependency(Job, 'visitUrl'); response = {}; diff --git a/spec/javascripts/jobs/sidebar_details_block_spec.js b/spec/javascripts/jobs/sidebar_details_block_spec.js index 6b397c22fb9..9c4454252ce 100644 --- a/spec/javascripts/jobs/sidebar_details_block_spec.js +++ b/spec/javascripts/jobs/sidebar_details_block_spec.js @@ -102,7 +102,7 @@ describe('Sidebar details block', () => { }); it('should render runner ID', () => { - expect(trimWhitespace(vm.$el.querySelector('.js-job-runner'))).toEqual('Runner: #1'); + expect(trimWhitespace(vm.$el.querySelector('.js-job-runner'))).toEqual('Runner: local ci runner (#1)'); }); it('should render timeout information', () => { diff --git a/spec/javascripts/lib/utils/csrf_token_spec.js b/spec/javascripts/lib/utils/csrf_token_spec.js index c484213df8e..81a39a97a84 100644 --- a/spec/javascripts/lib/utils/csrf_token_spec.js +++ b/spec/javascripts/lib/utils/csrf_token_spec.js @@ -1,6 +1,6 @@ import csrf from '~/lib/utils/csrf'; -describe('csrf', () => { +describe('csrf', function () { beforeEach(() => { this.tokenKey = 'X-CSRF-Token'; this.token = 'pH1cvjnP9grx2oKlhWEDvUZnJ8x2eXsIs1qzyHkF3DugSG5yTxR76CWeEZRhML2D1IeVB7NEW0t5l/axE4iJpQ=='; diff --git a/spec/javascripts/lib/utils/image_utility_spec.js b/spec/javascripts/lib/utils/image_utility_spec.js index 75addfcc833..a7eff419fba 100644 --- a/spec/javascripts/lib/utils/image_utility_spec.js +++ b/spec/javascripts/lib/utils/image_utility_spec.js @@ -1,4 +1,4 @@ -import * as imageUtility from '~/lib/utils/image_utility'; +import { isImageLoaded } from '~/lib/utils/image_utility'; describe('imageUtility', () => { describe('isImageLoaded', () => { @@ -8,7 +8,7 @@ describe('imageUtility', () => { naturalHeight: 100, }; - expect(imageUtility.isImageLoaded(element)).toEqual(false); + expect(isImageLoaded(element)).toEqual(false); }); it('should return false when naturalHeight = 0', () => { @@ -17,7 +17,7 @@ describe('imageUtility', () => { naturalHeight: 0, }; - expect(imageUtility.isImageLoaded(element)).toEqual(false); + expect(isImageLoaded(element)).toEqual(false); }); it('should return true when image.complete and naturalHeight != 0', () => { @@ -26,7 +26,7 @@ describe('imageUtility', () => { naturalHeight: 100, }; - expect(imageUtility.isImageLoaded(element)).toEqual(true); + expect(isImageLoaded(element)).toEqual(true); }); }); }); diff --git a/spec/javascripts/merge_request_tabs_spec.js b/spec/javascripts/merge_request_tabs_spec.js index 79c8cf0ba32..3dbd9756cd2 100644 --- a/spec/javascripts/merge_request_tabs_spec.js +++ b/spec/javascripts/merge_request_tabs_spec.js @@ -3,7 +3,6 @@ import $ from 'jquery'; import MockAdapter from 'axios-mock-adapter'; import axios from '~/lib/utils/axios_utils'; -import * as urlUtils from '~/lib/utils/url_utility'; import MergeRequestTabs from '~/merge_request_tabs'; import '~/commit/pipelines/pipelines_bundle'; import '~/breakpoints'; @@ -356,7 +355,7 @@ import 'vendor/jquery.scrollTo'; describe('with note fragment hash', () => { it('should expand and scroll to linked fragment hash #note_xxx', function (done) { - spyOn(urlUtils, 'getLocationHash').and.returnValue(noteId); + spyOnDependency(MergeRequestTabs, 'getLocationHash').and.returnValue(noteId); this.class.loadDiff('/foo/bar/merge_requests/1/diffs'); setTimeout(() => { @@ -372,7 +371,7 @@ import 'vendor/jquery.scrollTo'; }); it('should gracefully ignore non-existant fragment hash', function (done) { - spyOn(urlUtils, 'getLocationHash').and.returnValue('note_something-that-does-not-exist'); + spyOnDependency(MergeRequestTabs, 'getLocationHash').and.returnValue('note_something-that-does-not-exist'); this.class.loadDiff('/foo/bar/merge_requests/1/diffs'); setTimeout(() => { @@ -385,7 +384,7 @@ import 'vendor/jquery.scrollTo'; describe('with line number fragment hash', () => { it('should gracefully ignore line number fragment hash', function () { - spyOn(urlUtils, 'getLocationHash').and.returnValue(noteLineNumId); + spyOnDependency(MergeRequestTabs, 'getLocationHash').and.returnValue(noteLineNumId); this.class.loadDiff('/foo/bar/merge_requests/1/diffs'); expect(noteLineNumId.length).toBeGreaterThan(0); @@ -422,7 +421,7 @@ import 'vendor/jquery.scrollTo'; describe('with note fragment hash', () => { it('should expand and scroll to linked fragment hash #note_xxx', function (done) { - spyOn(urlUtils, 'getLocationHash').and.returnValue(noteId); + spyOnDependency(MergeRequestTabs, 'getLocationHash').and.returnValue(noteId); this.class.loadDiff('/foo/bar/merge_requests/1/diffs'); @@ -439,7 +438,7 @@ import 'vendor/jquery.scrollTo'; }); it('should gracefully ignore non-existant fragment hash', function (done) { - spyOn(urlUtils, 'getLocationHash').and.returnValue('note_something-that-does-not-exist'); + spyOnDependency(MergeRequestTabs, 'getLocationHash').and.returnValue('note_something-that-does-not-exist'); this.class.loadDiff('/foo/bar/merge_requests/1/diffs'); setTimeout(() => { @@ -451,7 +450,7 @@ import 'vendor/jquery.scrollTo'; describe('with line number fragment hash', () => { it('should gracefully ignore line number fragment hash', function () { - spyOn(urlUtils, 'getLocationHash').and.returnValue(noteLineNumId); + spyOnDependency(MergeRequestTabs, 'getLocationHash').and.returnValue(noteLineNumId); this.class.loadDiff('/foo/bar/merge_requests/1/diffs'); expect(noteLineNumId.length).toBeGreaterThan(0); diff --git a/spec/javascripts/monitoring/monitoring_store_spec.js b/spec/javascripts/monitoring/monitoring_store_spec.js index 88aa7659275..08d54946787 100644 --- a/spec/javascripts/monitoring/monitoring_store_spec.js +++ b/spec/javascripts/monitoring/monitoring_store_spec.js @@ -1,7 +1,7 @@ import MonitoringStore from '~/monitoring/stores/monitoring_store'; import MonitoringMock, { deploymentData } from './mock_data'; -describe('MonitoringStore', () => { +describe('MonitoringStore', function () { this.store = new MonitoringStore(); this.store.storeMetrics(MonitoringMock.data); diff --git a/spec/javascripts/notes_spec.js b/spec/javascripts/notes_spec.js index ec56ab0e2f0..0952356c2f4 100644 --- a/spec/javascripts/notes_spec.js +++ b/spec/javascripts/notes_spec.js @@ -3,7 +3,6 @@ import $ from 'jquery'; import _ from 'underscore'; import MockAdapter from 'axios-mock-adapter'; import axios from '~/lib/utils/axios_utils'; -import * as urlUtils from '~/lib/utils/url_utility'; import 'autosize'; import '~/gl_form'; import '~/lib/utils/text_utility'; @@ -222,7 +221,7 @@ import timeoutPromise from './helpers/set_timeout_promise_helper'; }); it('sets target when hash matches', () => { - spyOn(urlUtils, 'getLocationHash').and.returnValue(hash); + spyOnDependency(Notes, 'getLocationHash').and.returnValue(hash); Notes.updateNoteTargetSelector($note); @@ -231,7 +230,7 @@ import timeoutPromise from './helpers/set_timeout_promise_helper'; }); it('unsets target when hash does not match', () => { - spyOn(urlUtils, 'getLocationHash').and.returnValue('note_doesnotexist'); + spyOnDependency(Notes, 'getLocationHash').and.returnValue('note_doesnotexist'); Notes.updateNoteTargetSelector($note); @@ -239,7 +238,7 @@ import timeoutPromise from './helpers/set_timeout_promise_helper'; }); it('unsets target when there is not a hash fragment anymore', () => { - spyOn(urlUtils, 'getLocationHash').and.returnValue(null); + spyOnDependency(Notes, 'getLocationHash').and.returnValue(null); Notes.updateNoteTargetSelector($note); diff --git a/spec/javascripts/pager_spec.js b/spec/javascripts/pager_spec.js index b09494f0b77..04f2e7ef4f9 100644 --- a/spec/javascripts/pager_spec.js +++ b/spec/javascripts/pager_spec.js @@ -1,15 +1,25 @@ -/* global fixture */ +import $ from 'jquery'; import MockAdapter from 'axios-mock-adapter'; import axios from '~/lib/utils/axios_utils'; -import * as utils from '~/lib/utils/url_utility'; import Pager from '~/pager'; describe('pager', () => { + let axiosMock; + + beforeEach(() => { + axiosMock = new MockAdapter(axios); + }); + + afterEach(() => { + axiosMock.restore(); + }); + describe('init', () => { const originalHref = window.location.href; beforeEach(() => { setFixtures('<div class="content_list"></div><div class="loading"></div>'); + spyOn($.fn, 'endlessScroll').and.stub(); }); afterEach(() => { @@ -25,7 +35,7 @@ describe('pager', () => { it('should use current url if data-href attribute not provided', () => { const href = `${gl.TEST_HOST}/some_list`; - spyOn(utils, 'removeParams').and.returnValue(href); + spyOnDependency(Pager, 'removeParams').and.returnValue(href); Pager.init(); expect(Pager.url).toBe(href); }); @@ -39,42 +49,37 @@ describe('pager', () => { it('keeps extra query parameters from url', () => { window.history.replaceState({}, null, '?filter=test&offset=100'); const href = `${gl.TEST_HOST}/some_list?filter=test`; - spyOn(utils, 'removeParams').and.returnValue(href); + const removeParams = spyOnDependency(Pager, 'removeParams').and.returnValue(href); Pager.init(); - expect(utils.removeParams).toHaveBeenCalledWith(['limit', 'offset']); + expect(removeParams).toHaveBeenCalledWith(['limit', 'offset']); expect(Pager.url).toEqual(href); }); }); describe('getOld', () => { const urlRegex = /(.*)some_list(.*)$/; - let mock; function mockSuccess() { - mock.onGet(urlRegex).reply(200, { + axiosMock.onGet(urlRegex).reply(200, { count: 0, html: '', }); } function mockError() { - mock.onGet(urlRegex).networkError(); + axiosMock.onGet(urlRegex).networkError(); } beforeEach(() => { - setFixtures('<div class="content_list" data-href="/some_list"></div><div class="loading"></div>'); + setFixtures( + '<div class="content_list" data-href="/some_list"></div><div class="loading"></div>', + ); spyOn(axios, 'get').and.callThrough(); - mock = new MockAdapter(axios); - Pager.init(); }); - afterEach(() => { - mock.restore(); - }); - - it('shows loader while loading next page', (done) => { + it('shows loader while loading next page', done => { mockSuccess(); spyOn(Pager.loading, 'show'); @@ -87,7 +92,7 @@ describe('pager', () => { }); }); - it('hides loader on success', (done) => { + it('hides loader on success', done => { mockSuccess(); spyOn(Pager.loading, 'hide'); @@ -100,7 +105,7 @@ describe('pager', () => { }); }); - it('hides loader on error', (done) => { + it('hides loader on error', done => { mockError(); spyOn(Pager.loading, 'hide'); @@ -113,7 +118,7 @@ describe('pager', () => { }); }); - it('sends request to url with offset and limit params', (done) => { + it('sends request to url with offset and limit params', done => { Pager.offset = 100; Pager.limit = 20; Pager.getOld(); diff --git a/spec/javascripts/pages/admin/jobs/index/components/stop_jobs_modal_spec.js b/spec/javascripts/pages/admin/jobs/index/components/stop_jobs_modal_spec.js index a6fe9fb65e9..b69e5f9a3a0 100644 --- a/spec/javascripts/pages/admin/jobs/index/components/stop_jobs_modal_spec.js +++ b/spec/javascripts/pages/admin/jobs/index/components/stop_jobs_modal_spec.js @@ -2,7 +2,6 @@ import Vue from 'vue'; import axios from '~/lib/utils/axios_utils'; import stopJobsModal from '~/pages/admin/jobs/index/components/stop_jobs_modal.vue'; -import * as urlUtility from '~/lib/utils/url_utility'; import mountComponent from 'spec/helpers/vue_mount_component_helper'; @@ -24,7 +23,7 @@ describe('stop_jobs_modal.vue', () => { describe('onSubmit', () => { it('stops jobs and redirects to overview page', (done) => { const responseURL = `${gl.TEST_HOST}/stop_jobs_modal.vue/jobs`; - const redirectSpy = spyOn(urlUtility, 'redirectTo'); + const redirectSpy = spyOnDependency(stopJobsModal, 'redirectTo'); spyOn(axios, 'post').and.callFake((url) => { expect(url).toBe(props.url); return Promise.resolve({ @@ -44,7 +43,7 @@ describe('stop_jobs_modal.vue', () => { it('displays error if stopping jobs failed', (done) => { const dummyError = new Error('stopping jobs failed'); - const redirectSpy = spyOn(urlUtility, 'redirectTo'); + const redirectSpy = spyOnDependency(stopJobsModal, 'redirectTo'); spyOn(axios, 'post').and.callFake((url) => { expect(url).toBe(props.url); return Promise.reject(dummyError); diff --git a/spec/javascripts/pages/milestones/shared/components/delete_milestone_modal_spec.js b/spec/javascripts/pages/milestones/shared/components/delete_milestone_modal_spec.js index 6074e06fcec..94401beb5c9 100644 --- a/spec/javascripts/pages/milestones/shared/components/delete_milestone_modal_spec.js +++ b/spec/javascripts/pages/milestones/shared/components/delete_milestone_modal_spec.js @@ -3,7 +3,6 @@ import Vue from 'vue'; import axios from '~/lib/utils/axios_utils'; import deleteMilestoneModal from '~/pages/milestones/shared/components/delete_milestone_modal.vue'; import eventHub from '~/pages/milestones/shared/event_hub'; -import * as urlUtility from '~/lib/utils/url_utility'; import mountComponent from 'spec/helpers/vue_mount_component_helper'; @@ -40,7 +39,7 @@ describe('delete_milestone_modal.vue', () => { }, }); }); - const redirectSpy = spyOn(urlUtility, 'redirectTo'); + const redirectSpy = spyOnDependency(deleteMilestoneModal, 'redirectTo'); vm.onSubmit() .then(() => { @@ -60,7 +59,7 @@ describe('delete_milestone_modal.vue', () => { eventHub.$emit.calls.reset(); return Promise.reject(dummyError); }); - const redirectSpy = spyOn(urlUtility, 'redirectTo'); + const redirectSpy = spyOnDependency(deleteMilestoneModal, 'redirectTo'); vm.onSubmit() .catch((error) => { diff --git a/spec/javascripts/pages/projects/pipeline_schedules/shared/components/pipeline_schedule_callout_spec.js b/spec/javascripts/pages/projects/pipeline_schedules/shared/components/pipeline_schedule_callout_spec.js index f95a7cef18a..fb7d2763b49 100644 --- a/spec/javascripts/pages/projects/pipeline_schedules/shared/components/pipeline_schedule_callout_spec.js +++ b/spec/javascripts/pages/projects/pipeline_schedules/shared/components/pipeline_schedule_callout_spec.js @@ -6,7 +6,7 @@ const PipelineSchedulesCalloutComponent = Vue.extend(PipelineSchedulesCallout); const cookieKey = 'pipeline_schedules_callout_dismissed'; const docsUrl = 'help/ci/scheduled_pipelines'; -describe('Pipeline Schedule Callout', () => { +describe('Pipeline Schedule Callout', function () { beforeEach(() => { setFixtures(` <div id='pipeline-schedules-callout' data-docs-url=${docsUrl}></div> diff --git a/spec/javascripts/pipelines/graph/action_component_spec.js b/spec/javascripts/pipelines/graph/action_component_spec.js index 581209f215d..3de10392472 100644 --- a/spec/javascripts/pipelines/graph/action_component_spec.js +++ b/spec/javascripts/pipelines/graph/action_component_spec.js @@ -6,7 +6,7 @@ import mountComponent from '../../helpers/vue_mount_component_helper'; describe('pipeline graph action component', () => { let component; - beforeEach((done) => { + beforeEach(done => { const ActionComponent = Vue.extend(actionComponent); component = mountComponent(ActionComponent, { tooltipText: 'bar', @@ -22,7 +22,7 @@ describe('pipeline graph action component', () => { }); it('should emit an event with the provided link', () => { - eventHub.$on('graphAction', (link) => { + eventHub.$on('graphAction', link => { expect(link).toEqual('foo'); }); }); @@ -31,7 +31,7 @@ describe('pipeline graph action component', () => { expect(component.$el.getAttribute('data-original-title')).toEqual('bar'); }); - it('should update bootstrap tooltip when title changes', (done) => { + it('should update bootstrap tooltip when title changes', done => { component.tooltipText = 'changed'; setTimeout(() => { @@ -44,4 +44,45 @@ describe('pipeline graph action component', () => { expect(component.$el.querySelector('.ci-action-icon-wrapper')).toBeDefined(); expect(component.$el.querySelector('svg')).toBeDefined(); }); + + it('disables the button when clicked', done => { + component.$el.click(); + + component.$nextTick(() => { + expect(component.$el.getAttribute('disabled')).toEqual('disabled'); + done(); + }); + }); + + it('re-enabled the button when `requestFinishedFor` matches `linkRequested`', done => { + component.$el.click(); + + component + .$nextTick() + .then(() => { + expect(component.$el.getAttribute('disabled')).toEqual('disabled'); + component.requestFinishedFor = 'foo'; + }) + .then(() => { + expect(component.$el.getAttribute('disabled')).toBeNull(); + }) + .then(done) + .catch(done.fail); + }); + + it('does not re-enable the button when `requestFinishedFor` does not matches `linkRequested`', done => { + component.$el.click(); + + component + .$nextTick() + .then(() => { + expect(component.$el.getAttribute('disabled')).toEqual('disabled'); + component.requestFinishedFor = 'bar'; + }) + .then(() => { + expect(component.$el.getAttribute('disabled')).toEqual('disabled'); + }) + .then(done) + .catch(done.fail); + }); }); diff --git a/spec/javascripts/pipelines/graph/dropdown_action_component_spec.js b/spec/javascripts/pipelines/graph/dropdown_action_component_spec.js deleted file mode 100644 index ba721bc53c6..00000000000 --- a/spec/javascripts/pipelines/graph/dropdown_action_component_spec.js +++ /dev/null @@ -1,32 +0,0 @@ -import Vue from 'vue'; -import dropdownActionComponent from '~/pipelines/components/graph/dropdown_action_component.vue'; - -describe('action component', () => { - let component; - - beforeEach((done) => { - const DropdownActionComponent = Vue.extend(dropdownActionComponent); - component = new DropdownActionComponent({ - propsData: { - tooltipText: 'bar', - link: 'foo', - actionMethod: 'post', - actionIcon: 'cancel', - }, - }).$mount(); - - Vue.nextTick(done); - }); - - it('should render a link', () => { - expect(component.$el.getAttribute('href')).toEqual('foo'); - }); - - it('should render the provided title as a bootstrap tooltip', () => { - expect(component.$el.getAttribute('data-original-title')).toEqual('bar'); - }); - - it('should render an svg', () => { - expect(component.$el.querySelector('svg')).toBeDefined(); - }); -}); diff --git a/spec/javascripts/pipelines/graph/job_component_spec.js b/spec/javascripts/pipelines/graph/job_component_spec.js index c9677ae209a..073dae56c25 100644 --- a/spec/javascripts/pipelines/graph/job_component_spec.js +++ b/spec/javascripts/pipelines/graph/job_component_spec.js @@ -93,17 +93,6 @@ describe('pipeline graph job component', () => { }); }); - describe('dropdown', () => { - it('should render the dropdown action icon', () => { - component = mountComponent(JobComponent, { - job: mockJob, - isDropdown: true, - }); - - expect(component.$el.querySelector('a.ci-action-icon-wrapper')).toBeDefined(); - }); - }); - it('should render provided class name', () => { component = mountComponent(JobComponent, { job: mockJob, diff --git a/spec/javascripts/right_sidebar_spec.js b/spec/javascripts/right_sidebar_spec.js index 80770a61011..e264b16335f 100644 --- a/spec/javascripts/right_sidebar_spec.js +++ b/spec/javascripts/right_sidebar_spec.js @@ -9,8 +9,6 @@ import Sidebar from '~/right_sidebar'; (function() { var $aside, $icon, $labelsIcon, $page, $toggle, assertSidebarState; - this.sidebar = null; - $aside = null; $toggle = null; @@ -43,7 +41,7 @@ import Sidebar from '~/right_sidebar'; beforeEach(function() { loadFixtures(fixtureName); mock = new MockAdapter(axios); - this.sidebar = new Sidebar(); + new Sidebar(); // eslint-disable-line no-new $aside = $('.right-sidebar'); $page = $('.layout-page'); $icon = $aside.find('i'); diff --git a/spec/javascripts/search_autocomplete_spec.js b/spec/javascripts/search_autocomplete_spec.js index 1a27955983d..4f515f98a7e 100644 --- a/spec/javascripts/search_autocomplete_spec.js +++ b/spec/javascripts/search_autocomplete_spec.js @@ -4,7 +4,6 @@ import $ from 'jquery'; import '~/gl_dropdown'; import SearchAutocomplete from '~/search_autocomplete'; import '~/lib/utils/common_utils'; -import * as urlUtils from '~/lib/utils/url_utility'; describe('Search autocomplete dropdown', () => { var assertLinks, @@ -129,9 +128,6 @@ describe('Search autocomplete dropdown', () => { beforeEach(function() { loadFixtures('static/search_autocomplete.html.raw'); - // Prevent turbolinks from triggering within gl_dropdown - spyOn(urlUtils, 'visitUrl').and.returnValue(true); - window.gon = {}; window.gon.current_user_id = userId; window.gon.current_username = userName; diff --git a/spec/javascripts/settings_panels_spec.js b/spec/javascripts/settings_panels_spec.js index d433f8c3e07..4fba36bd4de 100644 --- a/spec/javascripts/settings_panels_spec.js +++ b/spec/javascripts/settings_panels_spec.js @@ -13,9 +13,9 @@ describe('Settings Panels', () => { }); it('should expand linked hash fragment panel', () => { - location.hash = '#js-general-pipeline-settings'; + location.hash = '#autodevops-settings'; - const pipelineSettingsPanel = document.querySelector('#js-general-pipeline-settings'); + const pipelineSettingsPanel = document.querySelector('#autodevops-settings'); // Our test environment automatically expands everything so we need to clear that out first pipelineSettingsPanel.classList.remove('expanded'); diff --git a/spec/javascripts/shortcuts_dashboard_navigation_spec.js b/spec/javascripts/shortcuts_dashboard_navigation_spec.js index 888b49004bf..7cb201e01d8 100644 --- a/spec/javascripts/shortcuts_dashboard_navigation_spec.js +++ b/spec/javascripts/shortcuts_dashboard_navigation_spec.js @@ -1,24 +1,23 @@ import findAndFollowLink from '~/shortcuts_dashboard_navigation'; -import * as urlUtility from '~/lib/utils/url_utility'; describe('findAndFollowLink', () => { it('visits a link when the selector exists', () => { const href = '/some/path'; - const locationSpy = spyOn(urlUtility, 'visitUrl'); + const visitUrl = spyOnDependency(findAndFollowLink, 'visitUrl'); setFixtures(`<a class="my-shortcut" href="${href}">link</a>`); findAndFollowLink('.my-shortcut'); - expect(locationSpy).toHaveBeenCalledWith(href); + expect(visitUrl).toHaveBeenCalledWith(href); }); it('does not throw an exception when the selector does not exist', () => { - const locationSpy = spyOn(urlUtility, 'visitUrl'); + const visitUrl = spyOnDependency(findAndFollowLink, 'visitUrl'); // this should not throw an exception findAndFollowLink('.this-selector-does-not-exist'); - expect(locationSpy).not.toHaveBeenCalled(); + expect(visitUrl).not.toHaveBeenCalled(); }); }); diff --git a/spec/javascripts/shortcuts_issuable_spec.js b/spec/javascripts/shortcuts_issuable_spec.js index b0d714cbefb..d73608ed0ed 100644 --- a/spec/javascripts/shortcuts_issuable_spec.js +++ b/spec/javascripts/shortcuts_issuable_spec.js @@ -4,7 +4,7 @@ import ShortcutsIssuable from '~/shortcuts_issuable'; initCopyAsGFM(); -describe('ShortcutsIssuable', () => { +describe('ShortcutsIssuable', function () { const fixtureName = 'merge_requests/diff_comment.html.raw'; preloadFixtures(fixtureName); beforeEach(() => { diff --git a/spec/javascripts/sidebar/sidebar_mediator_spec.js b/spec/javascripts/sidebar/sidebar_mediator_spec.js index afa18cc127e..da950258a94 100644 --- a/spec/javascripts/sidebar/sidebar_mediator_spec.js +++ b/spec/javascripts/sidebar/sidebar_mediator_spec.js @@ -1,12 +1,11 @@ import _ from 'underscore'; import Vue from 'vue'; -import * as urlUtils from '~/lib/utils/url_utility'; import SidebarMediator from '~/sidebar/sidebar_mediator'; import SidebarStore from '~/sidebar/stores/sidebar_store'; import SidebarService from '~/sidebar/services/sidebar_service'; import Mock from './mock_data'; -describe('Sidebar mediator', () => { +describe('Sidebar mediator', function() { beforeEach(() => { Vue.http.interceptors.push(Mock.sidebarMockInterceptor); this.mediator = new SidebarMediator(Mock.mediator); @@ -87,12 +86,12 @@ describe('Sidebar mediator', () => { const moveToProjectId = 7; this.mediator.store.setMoveToProjectId(moveToProjectId); spyOn(this.mediator.service, 'moveIssue').and.callThrough(); - spyOn(urlUtils, 'visitUrl'); + const visitUrl = spyOnDependency(SidebarMediator, 'visitUrl'); this.mediator.moveIssue() .then(() => { expect(this.mediator.service.moveIssue).toHaveBeenCalledWith(moveToProjectId); - expect(urlUtils.visitUrl).toHaveBeenCalledWith('/root/some-project/issues/5'); + expect(visitUrl).toHaveBeenCalledWith('/root/some-project/issues/5'); }) .then(done) .catch(done.fail); diff --git a/spec/javascripts/sidebar/sidebar_move_issue_spec.js b/spec/javascripts/sidebar/sidebar_move_issue_spec.js index d8e636cbdf0..a3fb965fbab 100644 --- a/spec/javascripts/sidebar/sidebar_move_issue_spec.js +++ b/spec/javascripts/sidebar/sidebar_move_issue_spec.js @@ -7,7 +7,7 @@ import SidebarService from '~/sidebar/services/sidebar_service'; import SidebarMoveIssue from '~/sidebar/lib/sidebar_move_issue'; import Mock from './mock_data'; -describe('SidebarMoveIssue', () => { +describe('SidebarMoveIssue', function () { beforeEach(() => { Vue.http.interceptors.push(Mock.sidebarMockInterceptor); this.mediator = new SidebarMediator(Mock.mediator); diff --git a/spec/javascripts/sidebar/sidebar_store_spec.js b/spec/javascripts/sidebar/sidebar_store_spec.js index 3591f96ff87..08b112a54ba 100644 --- a/spec/javascripts/sidebar/sidebar_store_spec.js +++ b/spec/javascripts/sidebar/sidebar_store_spec.js @@ -31,7 +31,7 @@ const PARTICIPANT_LIST = [ { ...PARTICIPANT, id: 3 }, ]; -describe('Sidebar store', () => { +describe('Sidebar store', function () { beforeEach(() => { this.store = new SidebarStore({ currentUser: { diff --git a/spec/javascripts/test_bundle.js b/spec/javascripts/test_bundle.js index 14bff05e537..bcd15f5eae2 100644 --- a/spec/javascripts/test_bundle.js +++ b/spec/javascripts/test_bundle.js @@ -1,4 +1,5 @@ -/* eslint-disable jasmine/no-global-setup */ +/* eslint-disable jasmine/no-global-setup, jasmine/no-unsafe-spy, no-underscore-dangle */ + import $ from 'jquery'; import 'vendor/jasmine-jquery'; import '~/commons'; @@ -55,6 +56,17 @@ window.addEventListener('unhandledrejection', event => { console.error(event.reason.stack || event.reason); }); +// Add global function to spy on a module's dependencies via rewire +window.spyOnDependency = (module, name) => { + const dependency = module.__GetDependency__(name); + const spy = jasmine.createSpy(name, dependency); + module.__Rewire__(name, spy); + return spy; +}; + +// Reset any rewired modules after each test (see babel-plugin-rewire) +afterEach(__rewire_reset_all__); // eslint-disable-line + // HACK: Chrome 59 disconnects if there are too many synchronous tests in a row // because it appears to lock up the thread that communicates to Karma's socket // This async beforeEach gets called on every spec and releases the JS thread long diff --git a/spec/javascripts/todos_spec.js b/spec/javascripts/todos_spec.js index 898bbb3819b..e74f4bdef7e 100644 --- a/spec/javascripts/todos_spec.js +++ b/spec/javascripts/todos_spec.js @@ -1,5 +1,4 @@ import $ from 'jquery'; -import * as urlUtils from '~/lib/utils/url_utility'; import Todos from '~/pages/dashboard/todos/index/todos'; import '~/lib/utils/common_utils'; @@ -18,7 +17,7 @@ describe('Todos', () => { it('opens the todo url', (done) => { const todoLink = todoItem.dataset.url; - spyOn(urlUtils, 'visitUrl').and.callFake((url) => { + spyOnDependency(Todos, 'visitUrl').and.callFake((url) => { expect(url).toEqual(todoLink); done(); }); @@ -33,7 +32,7 @@ describe('Todos', () => { beforeEach(() => { metakeyEvent = $.Event('click', { keyCode: 91, ctrlKey: true }); - visitUrlSpy = spyOn(urlUtils, 'visitUrl').and.callFake(() => {}); + visitUrlSpy = spyOnDependency(Todos, 'visitUrl').and.callFake(() => {}); windowOpenSpy = spyOn(window, 'open').and.callFake(() => {}); }); diff --git a/spec/javascripts/u2f/authenticate_spec.js b/spec/javascripts/u2f/authenticate_spec.js index 39c47a5c06d..d84b13b07c4 100644 --- a/spec/javascripts/u2f/authenticate_spec.js +++ b/spec/javascripts/u2f/authenticate_spec.js @@ -3,7 +3,7 @@ import U2FAuthenticate from '~/u2f/authenticate'; import 'vendor/u2f'; import MockU2FDevice from './mock_u2f_device'; -describe('U2FAuthenticate', () => { +describe('U2FAuthenticate', function () { preloadFixtures('u2f/authenticate.html.raw'); beforeEach((done) => { diff --git a/spec/javascripts/u2f/register_spec.js b/spec/javascripts/u2f/register_spec.js index 136b4cad737..d9383314891 100644 --- a/spec/javascripts/u2f/register_spec.js +++ b/spec/javascripts/u2f/register_spec.js @@ -3,7 +3,7 @@ import U2FRegister from '~/u2f/register'; import 'vendor/u2f'; import MockU2FDevice from './mock_u2f_device'; -describe('U2FRegister', () => { +describe('U2FRegister', function () { preloadFixtures('u2f/register.html.raw'); beforeEach((done) => { diff --git a/spec/javascripts/vue_mr_widget/components/deployment_spec.js b/spec/javascripts/vue_mr_widget/components/deployment_spec.js index ff8d54c029f..c82ba61a5b1 100644 --- a/spec/javascripts/vue_mr_widget/components/deployment_spec.js +++ b/spec/javascripts/vue_mr_widget/components/deployment_spec.js @@ -1,5 +1,4 @@ import Vue from 'vue'; -import * as urlUtils from '~/lib/utils/url_utility'; import deploymentComponent from '~/vue_merge_request_widget/components/deployment.vue'; import MRWidgetService from '~/vue_merge_request_widget/services/mr_widget_service'; import { getTimeago } from '~/lib/utils/datetime_utility'; @@ -117,13 +116,13 @@ describe('Deployment component', () => { it('should show a confirm dialog and call service.stopEnvironment when confirmed', (done) => { spyOn(window, 'confirm').and.returnValue(true); spyOn(MRWidgetService, 'stopEnvironment').and.returnValue(returnPromise(true)); - spyOn(urlUtils, 'visitUrl').and.returnValue(true); + const visitUrl = spyOnDependency(deploymentComponent, 'visitUrl').and.returnValue(true); vm = mockStopEnvironment(); expect(window.confirm).toHaveBeenCalled(); expect(MRWidgetService.stopEnvironment).toHaveBeenCalledWith(deploymentMockData.stop_url); setTimeout(() => { - expect(urlUtils.visitUrl).toHaveBeenCalledWith(url); + expect(visitUrl).toHaveBeenCalledWith(url); done(); }, 333); }); diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js index 300b7882d03..81c16593eb4 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js @@ -1,7 +1,6 @@ import Vue from 'vue'; import ReadyToMerge from '~/vue_merge_request_widget/components/states/ready_to_merge.vue'; import eventHub from '~/vue_merge_request_widget/event_hub'; -import * as simplePoll from '~/lib/utils/simple_poll'; const commitMessage = 'This is the commit message'; const commitMessageWithDescription = 'This is the commit message description'; @@ -355,9 +354,9 @@ describe('ReadyToMerge', () => { describe('initiateMergePolling', () => { it('should call simplePoll', () => { - spyOn(simplePoll, 'default'); + const simplePoll = spyOnDependency(ReadyToMerge, 'simplePoll'); vm.initiateMergePolling(); - expect(simplePoll.default).toHaveBeenCalled(); + expect(simplePoll).toHaveBeenCalled(); }); }); @@ -457,11 +456,11 @@ describe('ReadyToMerge', () => { describe('initiateRemoveSourceBranchPolling', () => { it('should emit event and call simplePoll', () => { spyOn(eventHub, '$emit'); - spyOn(simplePoll, 'default'); + const simplePoll = spyOnDependency(ReadyToMerge, 'simplePoll'); vm.initiateRemoveSourceBranchPolling(); expect(eventHub.$emit).toHaveBeenCalledWith('SetBranchRemoveFlag', [true]); - expect(simplePoll.default).toHaveBeenCalled(); + expect(simplePoll).toHaveBeenCalled(); }); }); @@ -524,18 +523,20 @@ describe('ReadyToMerge', () => { }); describe('when user can merge and can delete branch', () => { + let customVm; + beforeEach(() => { - this.customVm = createComponent({ + customVm = createComponent({ mr: { canRemoveSourceBranch: true }, }); }); it('isRemoveSourceBranchButtonDisabled should be false', () => { - expect(this.customVm.isRemoveSourceBranchButtonDisabled).toBe(false); + expect(customVm.isRemoveSourceBranchButtonDisabled).toBe(false); }); it('should be enabled in rendered output', () => { - const checkboxElement = this.customVm.$el.querySelector('#remove-source-branch-input'); + const checkboxElement = customVm.$el.querySelector('#remove-source-branch-input'); expect(checkboxElement).not.toBeNull(); }); }); diff --git a/spec/javascripts/vue_shared/components/ci_icon_spec.js b/spec/javascripts/vue_shared/components/ci_icon_spec.js index d8664408595..423bc746a22 100644 --- a/spec/javascripts/vue_shared/components/ci_icon_spec.js +++ b/spec/javascripts/vue_shared/components/ci_icon_spec.js @@ -1,139 +1,122 @@ import Vue from 'vue'; import ciIcon from '~/vue_shared/components/ci_icon.vue'; +import mountComponent from 'spec/helpers/vue_mount_component_helper'; describe('CI Icon component', () => { - let CiIcon; - beforeEach(() => { - CiIcon = Vue.extend(ciIcon); + const Component = Vue.extend(ciIcon); + let vm; + + afterEach(() => { + vm.$destroy(); }); it('should render a span element with an svg', () => { - const component = new CiIcon({ - propsData: { - status: { - icon: 'icon_status_success', - }, + vm = mountComponent(Component, { + status: { + icon: 'icon_status_success', }, - }).$mount(); + }); - expect(component.$el.tagName).toEqual('SPAN'); - expect(component.$el.querySelector('span > svg')).toBeDefined(); + expect(vm.$el.tagName).toEqual('SPAN'); + expect(vm.$el.querySelector('span > svg')).toBeDefined(); }); it('should render a success status', () => { - const component = new CiIcon({ - propsData: { - status: { - icon: 'icon_status_success', - group: 'success', - }, + vm = mountComponent(Component, { + status: { + icon: 'icon_status_success', + group: 'success', }, - }).$mount(); + }); - expect(component.$el.classList.contains('ci-status-icon-success')).toEqual(true); + expect(vm.$el.classList.contains('ci-status-icon-success')).toEqual(true); }); it('should render a failed status', () => { - const component = new CiIcon({ - propsData: { - status: { - icon: 'icon_status_failed', - group: 'failed', - }, + vm = mountComponent(Component, { + status: { + icon: 'icon_status_failed', + group: 'failed', }, - }).$mount(); + }); - expect(component.$el.classList.contains('ci-status-icon-failed')).toEqual(true); + expect(vm.$el.classList.contains('ci-status-icon-failed')).toEqual(true); }); it('should render success with warnings status', () => { - const component = new CiIcon({ - propsData: { - status: { - icon: 'icon_status_warning', - group: 'warning', - }, + vm = mountComponent(Component, { + status: { + icon: 'icon_status_warning', + group: 'warning', }, - }).$mount(); + }); - expect(component.$el.classList.contains('ci-status-icon-warning')).toEqual(true); + expect(vm.$el.classList.contains('ci-status-icon-warning')).toEqual(true); }); it('should render pending status', () => { - const component = new CiIcon({ - propsData: { - status: { - icon: 'icon_status_pending', - group: 'pending', - }, + vm = mountComponent(Component, { + status: { + icon: 'icon_status_pending', + group: 'pending', }, - }).$mount(); + }); - expect(component.$el.classList.contains('ci-status-icon-pending')).toEqual(true); + expect(vm.$el.classList.contains('ci-status-icon-pending')).toEqual(true); }); it('should render running status', () => { - const component = new CiIcon({ - propsData: { - status: { - icon: 'icon_status_running', - group: 'running', - }, + vm = mountComponent(Component, { + status: { + icon: 'icon_status_running', + group: 'running', }, - }).$mount(); + }); - expect(component.$el.classList.contains('ci-status-icon-running')).toEqual(true); + expect(vm.$el.classList.contains('ci-status-icon-running')).toEqual(true); }); it('should render created status', () => { - const component = new CiIcon({ - propsData: { - status: { - icon: 'icon_status_created', - group: 'created', - }, + vm = mountComponent(Component, { + status: { + icon: 'icon_status_created', + group: 'created', }, - }).$mount(); + }); - expect(component.$el.classList.contains('ci-status-icon-created')).toEqual(true); + expect(vm.$el.classList.contains('ci-status-icon-created')).toEqual(true); }); it('should render skipped status', () => { - const component = new CiIcon({ - propsData: { - status: { - icon: 'icon_status_skipped', - group: 'skipped', - }, + vm = mountComponent(Component, { + status: { + icon: 'icon_status_skipped', + group: 'skipped', }, - }).$mount(); + }); - expect(component.$el.classList.contains('ci-status-icon-skipped')).toEqual(true); + expect(vm.$el.classList.contains('ci-status-icon-skipped')).toEqual(true); }); it('should render canceled status', () => { - const component = new CiIcon({ - propsData: { - status: { - icon: 'icon_status_canceled', - group: 'canceled', - }, + vm = mountComponent(Component, { + status: { + icon: 'icon_status_canceled', + group: 'canceled', }, - }).$mount(); + }); - expect(component.$el.classList.contains('ci-status-icon-canceled')).toEqual(true); + expect(vm.$el.classList.contains('ci-status-icon-canceled')).toEqual(true); }); it('should render status for manual action', () => { - const component = new CiIcon({ - propsData: { - status: { - icon: 'icon_status_manual', - group: 'manual', - }, + vm = mountComponent(Component, { + status: { + icon: 'icon_status_manual', + group: 'manual', }, - }).$mount(); + }); - expect(component.$el.classList.contains('ci-status-icon-manual')).toEqual(true); + expect(vm.$el.classList.contains('ci-status-icon-manual')).toEqual(true); }); }); diff --git a/spec/javascripts/vue_shared/components/clipboard_button_spec.js b/spec/javascripts/vue_shared/components/clipboard_button_spec.js index f598b1afa74..97f0fbb04db 100644 --- a/spec/javascripts/vue_shared/components/clipboard_button_spec.js +++ b/spec/javascripts/vue_shared/components/clipboard_button_spec.js @@ -3,10 +3,10 @@ import clipboardButton from '~/vue_shared/components/clipboard_button.vue'; import mountComponent from 'spec/helpers/vue_mount_component_helper'; describe('clipboard button', () => { + const Component = Vue.extend(clipboardButton); let vm; beforeEach(() => { - const Component = Vue.extend(clipboardButton); vm = mountComponent(Component, { text: 'copy me', title: 'Copy this value into Clipboard!', diff --git a/spec/javascripts/vue_shared/components/commit_spec.js b/spec/javascripts/vue_shared/components/commit_spec.js index ed66361bfc3..7189e8cfcfa 100644 --- a/spec/javascripts/vue_shared/components/commit_spec.js +++ b/spec/javascripts/vue_shared/components/commit_spec.js @@ -55,7 +55,6 @@ describe('Commit component', () => { path: '/jschatz1', username: 'jschatz1', }, - commitIconSvg: '<svg></svg>', }; component = mountComponent(CommitComponent, props); @@ -82,8 +81,10 @@ describe('Commit component', () => { expect(component.$el.querySelector('.commit-sha').textContent).toContain(props.shortSha); }); - it('should render the given commitIconSvg', () => { - expect(component.$el.querySelector('.js-commit-icon').children).toContain('svg'); + it('should render icon for commit', () => { + expect( + component.$el.querySelector('.js-commit-icon use').getAttribute('xlink:href'), + ).toContain('commit'); }); describe('Given commit title and author props', () => { diff --git a/spec/javascripts/vue_shared/components/expand_button_spec.js b/spec/javascripts/vue_shared/components/expand_button_spec.js index f19589d3b75..af9693c48fd 100644 --- a/spec/javascripts/vue_shared/components/expand_button_spec.js +++ b/spec/javascripts/vue_shared/components/expand_button_spec.js @@ -3,10 +3,10 @@ import expandButton from '~/vue_shared/components/expand_button.vue'; import mountComponent from 'spec/helpers/vue_mount_component_helper'; describe('expand button', () => { + const Component = Vue.extend(expandButton); let vm; beforeEach(() => { - const Component = Vue.extend(expandButton); vm = mountComponent(Component, { slots: { expanded: '<p>Expanded!</p>', @@ -22,7 +22,7 @@ describe('expand button', () => { expect(vm.$el.textContent.trim()).toEqual('...'); }); - it('hides expander on click', (done) => { + it('hides expander on click', done => { vm.$el.querySelector('button').click(); vm.$nextTick(() => { expect(vm.$el.querySelector('button').getAttribute('style')).toEqual('display: none;'); diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/base_spec.js b/spec/javascripts/vue_shared/components/sidebar/labels_select/base_spec.js index 6fe95153204..e8685ab48be 100644 --- a/spec/javascripts/vue_shared/components/sidebar/labels_select/base_spec.js +++ b/spec/javascripts/vue_shared/components/sidebar/labels_select/base_spec.js @@ -73,6 +73,22 @@ describe('BaseComponent', () => { expect(vm.$emit).toHaveBeenCalledWith('onLabelClick', mockLabels[0]); }); }); + + describe('handleCollapsedValueClick', () => { + it('emits toggleCollapse event on component', () => { + spyOn(vm, '$emit'); + vm.handleCollapsedValueClick(); + expect(vm.$emit).toHaveBeenCalledWith('toggleCollapse'); + }); + }); + + describe('handleDropdownHidden', () => { + it('emits onDropdownClose event on component', () => { + spyOn(vm, '$emit'); + vm.handleDropdownHidden(); + expect(vm.$emit).toHaveBeenCalledWith('onDropdownClose'); + }); + }); }); describe('mounted', () => { diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js index 39040670a87..da74595bcdc 100644 --- a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js +++ b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js @@ -56,6 +56,16 @@ describe('DropdownValueCollapsedComponent', () => { }); }); + describe('methods', () => { + describe('handleClick', () => { + it('emits onValueClick event on component', () => { + spyOn(vm, '$emit'); + vm.handleClick(); + expect(vm.$emit).toHaveBeenCalledWith('onValueClick'); + }); + }); + }); + describe('template', () => { it('renders component container element with tooltip`', () => { expect(vm.$el.dataset.placement).toBe('left'); diff --git a/spec/lib/backup/files_spec.rb b/spec/lib/backup/files_spec.rb index 14d055cbcc1..99872211a4e 100644 --- a/spec/lib/backup/files_spec.rb +++ b/spec/lib/backup/files_spec.rb @@ -62,5 +62,19 @@ describe Backup::Files do subject.restore end end + + describe 'folders that are a mountpoint' do + before do + allow(FileUtils).to receive(:mv).and_raise(Errno::EBUSY) + allow(subject).to receive(:run_pipeline!).and_return(true) + end + + it 'shows error message' do + expect(subject).to receive(:resource_busy_error).with("/var/gitlab-registry") + .and_call_original + + expect { subject.restore }.to raise_error(/is a mountpoint/) + end + end end end diff --git a/spec/lib/backup/repository_spec.rb b/spec/lib/backup/repository_spec.rb index e4c1c9bafc0..b3777be312b 100644 --- a/spec/lib/backup/repository_spec.rb +++ b/spec/lib/backup/repository_spec.rb @@ -81,6 +81,18 @@ describe Backup::Repository do subject.restore end end + + describe 'folder that is a mountpoint' do + before do + allow(FileUtils).to receive(:mv).and_raise(Errno::EBUSY) + end + + it 'shows error message' do + expect(subject).to receive(:resource_busy_error).and_call_original + + expect { subject.restore }.to raise_error(/is a mountpoint/) + end + end end describe '#empty_repo?' do diff --git a/spec/lib/gitlab/auth/ldap/user_spec.rb b/spec/lib/gitlab/auth/ldap/user_spec.rb index cab2169593a..653c19942ea 100644 --- a/spec/lib/gitlab/auth/ldap/user_spec.rb +++ b/spec/lib/gitlab/auth/ldap/user_spec.rb @@ -25,20 +25,20 @@ describe Gitlab::Auth::LDAP::User do OmniAuth::AuthHash.new(uid: 'uid=John Smith,ou=People,dc=example,dc=com', provider: 'ldapmain', info: info_upper_case) end - describe '#changed?' do + describe '#should_save?' do it "marks existing ldap user as changed" do create(:omniauth_user, extern_uid: 'uid=John Smith,ou=People,dc=example,dc=com', provider: 'ldapmain') - expect(ldap_user.changed?).to be_truthy + expect(ldap_user.should_save?).to be_truthy end it "marks existing non-ldap user if the email matches as changed" do create(:user, email: 'john@example.com') - expect(ldap_user.changed?).to be_truthy + expect(ldap_user.should_save?).to be_truthy end it "does not mark existing ldap user as changed" do create(:omniauth_user, email: 'john@example.com', extern_uid: 'uid=john smith,ou=people,dc=example,dc=com', provider: 'ldapmain') - expect(ldap_user.changed?).to be_falsey + expect(ldap_user.should_save?).to be_falsey end end diff --git a/spec/lib/gitlab/auth/o_auth/identity_linker_spec.rb b/spec/lib/gitlab/auth/o_auth/identity_linker_spec.rb new file mode 100644 index 00000000000..528f1b4ec57 --- /dev/null +++ b/spec/lib/gitlab/auth/o_auth/identity_linker_spec.rb @@ -0,0 +1,62 @@ +require 'spec_helper' + +describe Gitlab::Auth::OAuth::IdentityLinker do + let(:user) { create(:user) } + let(:provider) { 'twitter' } + let(:uid) { user.email } + let(:oauth) { { 'provider' => provider, 'uid' => uid } } + + subject { described_class.new(user, oauth) } + + context 'linked identity exists' do + let!(:identity) { user.identities.create!(provider: provider, extern_uid: uid) } + + it "doesn't create new identity" do + expect { subject.link }.not_to change { Identity.count } + end + + it "sets #changed? to false" do + subject.link + + expect(subject).not_to be_changed + end + end + + context 'identity already linked to different user' do + let!(:identity) { create(:identity, provider: provider, extern_uid: uid) } + + it "#changed? returns false" do + subject.link + + expect(subject).not_to be_changed + end + + it 'exposes error message' do + expect(subject.error_message).to eq 'Extern uid has already been taken' + end + end + + context 'identity needs to be created' do + it 'creates linked identity' do + expect { subject.link }.to change { user.identities.count } + end + + it 'sets identity provider' do + subject.link + + expect(user.identities.last.provider).to eq provider + end + + it 'sets identity extern_uid' do + subject.link + + expect(user.identities.last.extern_uid).to eq uid + end + + it 'sets #changed? to true' do + subject.link + + expect(subject).to be_changed + end + end +end diff --git a/spec/lib/gitlab/auth/saml/identity_linker_spec.rb b/spec/lib/gitlab/auth/saml/identity_linker_spec.rb new file mode 100644 index 00000000000..f3305d574cc --- /dev/null +++ b/spec/lib/gitlab/auth/saml/identity_linker_spec.rb @@ -0,0 +1,48 @@ +require 'spec_helper' + +describe Gitlab::Auth::Saml::IdentityLinker do + let(:user) { create(:user) } + let(:provider) { 'saml' } + let(:uid) { user.email } + let(:oauth) { { 'provider' => provider, 'uid' => uid } } + + subject { described_class.new(user, oauth) } + + context 'linked identity exists' do + let!(:identity) { user.identities.create!(provider: provider, extern_uid: uid) } + + it "doesn't create new identity" do + expect { subject.link }.not_to change { Identity.count } + end + + it "sets #changed? to false" do + subject.link + + expect(subject).not_to be_changed + end + end + + context 'identity needs to be created' do + it 'creates linked identity' do + expect { subject.link }.to change { user.identities.count } + end + + it 'sets identity provider' do + subject.link + + expect(user.identities.last.provider).to eq provider + end + + it 'sets identity extern_uid' do + subject.link + + expect(user.identities.last.extern_uid).to eq uid + end + + it 'sets #changed? to true' do + subject.link + + expect(subject).to be_changed + end + end +end diff --git a/spec/lib/gitlab/bitbucket_import/project_creator_spec.rb b/spec/lib/gitlab/bitbucket_import/project_creator_spec.rb index ae5b31dc12d..c3f528dd6fc 100644 --- a/spec/lib/gitlab/bitbucket_import/project_creator_spec.rb +++ b/spec/lib/gitlab/bitbucket_import/project_creator_spec.rb @@ -15,7 +15,7 @@ describe Gitlab::BitbucketImport::ProjectCreator do has_wiki?: false) end - let(:namespace) { create(:group, owner: user) } + let(:namespace) { create(:group) } let(:token) { "asdasd12345" } let(:secret) { "sekrettt" } let(:access_params) { { bitbucket_access_token: token, bitbucket_access_token_secret: secret } } diff --git a/spec/lib/gitlab/ci/status/build/play_spec.rb b/spec/lib/gitlab/ci/status/build/play_spec.rb index f128c1d4ca4..e2bb378f663 100644 --- a/spec/lib/gitlab/ci/status/build/play_spec.rb +++ b/spec/lib/gitlab/ci/status/build/play_spec.rb @@ -2,8 +2,8 @@ require 'spec_helper' describe Gitlab::Ci::Status::Build::Play do let(:user) { create(:user) } - let(:project) { build.project } - let(:build) { create(:ci_build, :manual) } + let(:project) { create(:project, :stubbed_repository) } + let(:build) { create(:ci_build, :manual, project: project) } let(:status) { Gitlab::Ci::Status::Core.new(build, user) } subject { described_class.new(status) } @@ -46,6 +46,8 @@ describe Gitlab::Ci::Status::Build::Play do context 'when user can not push to the branch' do before do build.project.add_developer(user) + create(:protected_branch, :masters_can_push, + name: build.ref, project: project) end it { is_expected.not_to have_action } diff --git a/spec/lib/gitlab/wiki/committer_with_hooks_spec.rb b/spec/lib/gitlab/git/committer_with_hooks_spec.rb index 830fb8a8598..267056b96e6 100644 --- a/spec/lib/gitlab/wiki/committer_with_hooks_spec.rb +++ b/spec/lib/gitlab/git/committer_with_hooks_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Gitlab::Wiki::CommitterWithHooks, seed_helper: true do +describe Gitlab::Git::CommitterWithHooks, seed_helper: true do shared_examples 'calling wiki hooks' do let(:project) { create(:project) } let(:user) { project.owner } diff --git a/spec/lib/gitlab/gitlab_import/project_creator_spec.rb b/spec/lib/gitlab/gitlab_import/project_creator_spec.rb index 82548c7fd31..5ea086e4abd 100644 --- a/spec/lib/gitlab/gitlab_import/project_creator_spec.rb +++ b/spec/lib/gitlab/gitlab_import/project_creator_spec.rb @@ -12,7 +12,7 @@ describe Gitlab::GitlabImport::ProjectCreator do owner: { name: "john" } }.with_indifferent_access end - let(:namespace) { create(:group, owner: user) } + let(:namespace) { create(:group) } let(:token) { "asdffg" } let(:access_params) { { gitlab_access_token: token } } diff --git a/spec/lib/gitlab/google_code_import/project_creator_spec.rb b/spec/lib/gitlab/google_code_import/project_creator_spec.rb index 8d5b60d50de..24cd518c77b 100644 --- a/spec/lib/gitlab/google_code_import/project_creator_spec.rb +++ b/spec/lib/gitlab/google_code_import/project_creator_spec.rb @@ -9,7 +9,7 @@ describe Gitlab::GoogleCodeImport::ProjectCreator do "repositoryUrls" => ["https://vim.googlecode.com/git/"] ) end - let(:namespace) { create(:group, owner: user) } + let(:namespace) { create(:group) } before do namespace.add_owner(user) diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index 897a5984782..e7f20f81fe0 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -286,6 +286,7 @@ project: - internal_ids - project_deploy_tokens - deploy_tokens +- ci_cd_settings award_emoji: - awardable - user diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml index 05790bb5fe1..31141807cb2 100644 --- a/spec/lib/gitlab/import_export/safe_model_attributes.yml +++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml @@ -546,3 +546,5 @@ Badge: - created_at - updated_at - type +ProjectCiCdSetting: +- group_runners_enabled diff --git a/spec/lib/gitlab/legacy_github_import/project_creator_spec.rb b/spec/lib/gitlab/legacy_github_import/project_creator_spec.rb index 737c9a624e0..eb1b13704ea 100644 --- a/spec/lib/gitlab/legacy_github_import/project_creator_spec.rb +++ b/spec/lib/gitlab/legacy_github_import/project_creator_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' describe Gitlab::LegacyGithubImport::ProjectCreator do let(:user) { create(:user) } - let(:namespace) { create(:group, owner: user) } + let(:namespace) { create(:group) } let(:repo) do OpenStruct.new( diff --git a/spec/lib/gitlab/user_access_spec.rb b/spec/lib/gitlab/user_access_spec.rb index 40c8286b1b9..97b6069f64d 100644 --- a/spec/lib/gitlab/user_access_spec.rb +++ b/spec/lib/gitlab/user_access_spec.rb @@ -32,6 +32,12 @@ describe Gitlab::UserAccess do let(:empty_project) { create(:project_empty_repo) } let(:project_access) { described_class.new(user, project: empty_project) } + it 'returns true for admins' do + user.update!(admin: true) + + expect(access.can_push_to_branch?('master')).to be_truthy + end + it 'returns true if user is master' do empty_project.add_master(user) @@ -71,6 +77,12 @@ describe Gitlab::UserAccess do let(:branch) { create :protected_branch, project: project, name: "test" } let(:not_existing_branch) { create :protected_branch, :developers_can_merge, project: project } + it 'returns true for admins' do + user.update!(admin: true) + + expect(access.can_push_to_branch?(branch.name)).to be_truthy + end + it 'returns true if user is a master' do project.add_master(user) diff --git a/spec/lib/gitlab_spec.rb b/spec/lib/gitlab_spec.rb index bd443a5d9e7..da146e24893 100644 --- a/spec/lib/gitlab_spec.rb +++ b/spec/lib/gitlab_spec.rb @@ -1,6 +1,14 @@ -require 'rails_helper' +require 'fast_spec_helper' + +require_dependency 'gitlab' describe Gitlab do + describe '.root' do + it 'returns the root path of the app' do + expect(described_class.root).to eq(Pathname.new(File.expand_path('../..', __dir__))) + end + end + describe '.com?' do it 'is true when on GitLab.com' do stub_config_setting(url: 'https://gitlab.com') diff --git a/spec/migrations/create_missing_namespace_for_internal_users_spec.rb b/spec/migrations/create_missing_namespace_for_internal_users_spec.rb new file mode 100644 index 00000000000..ac3a4b1f68f --- /dev/null +++ b/spec/migrations/create_missing_namespace_for_internal_users_spec.rb @@ -0,0 +1,42 @@ +require 'spec_helper' +require Rails.root.join('db', 'migrate', '20180413022611_create_missing_namespace_for_internal_users.rb') + +describe CreateMissingNamespaceForInternalUsers, :migration do + let(:users) { table(:users) } + let(:namespaces) { table(:namespaces) } + let(:routes) { table(:routes) } + + internal_user_types = [:ghost] + internal_user_types << :support_bot if ActiveRecord::Base.connection.column_exists?(:users, :support_bot) + + internal_user_types.each do |attr| + context "for #{attr} user" do + let(:internal_user) do + users.create!(email: 'test@example.com', projects_limit: 100, username: 'test', attr => true) + end + + it 'creates the missing namespace' do + expect(namespaces.find_by(owner_id: internal_user.id)).to be_nil + + migrate! + + namespace = Namespace.find_by(type: nil, owner_id: internal_user.id) + route = namespace.route + + expect(namespace.path).to eq(route.path) + expect(namespace.name).to eq(route.name) + end + + it 'sets notification email' do + users.update(internal_user.id, notification_email: nil) + + expect(users.find(internal_user.id).notification_email).to be_nil + + migrate! + + user = users.find(internal_user.id) + expect(user.notification_email).to eq(user.email) + end + end + end +end diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index fcdc31c8984..3158e006720 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -2035,6 +2035,34 @@ describe Ci::Build do expect(build).not_to be_persisted end end + + context 'for deploy tokens' do + let(:deploy_token) { create(:deploy_token, :gitlab_deploy_token) } + + let(:deploy_token_variables) do + [ + { key: 'CI_DEPLOY_USER', value: deploy_token.name, public: true }, + { key: 'CI_DEPLOY_PASSWORD', value: deploy_token.token, public: false } + ] + end + + context 'when gitlab-deploy-token exists' do + before do + project.deploy_tokens << deploy_token + end + + it 'should include deploy token variables' do + is_expected.to include(*deploy_token_variables) + end + end + + context 'when gitlab-deploy-token does not exist' do + it 'should not include deploy token variables' do + expect(subject.find { |v| v[:key] == 'CI_DEPLOY_USER'}).to be_nil + expect(subject.find { |v| v[:key] == 'CI_DEPLOY_PASSWORD'}).to be_nil + end + end + end end describe '#scoped_variables' do @@ -2083,7 +2111,9 @@ describe Ci::Build do CI_REGISTRY_USER CI_REGISTRY_PASSWORD CI_REPOSITORY_URL - CI_ENVIRONMENT_URL] + CI_ENVIRONMENT_URL + CI_DEPLOY_USER + CI_DEPLOY_PASSWORD] build.scoped_variables.map { |env| env[:key] }.tap do |names| expect(names).not_to include(*keys) diff --git a/spec/models/concerns/avatarable_spec.rb b/spec/models/concerns/avatarable_spec.rb index 3696e6f62fd..9faf21bfbbd 100644 --- a/spec/models/concerns/avatarable_spec.rb +++ b/spec/models/concerns/avatarable_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe Avatarable do - set(:project) { create(:project, avatar: fixture_file_upload(File.join(Rails.root, 'spec/fixtures/dk.png'))) } + let(:project) { create(:project, :with_avatar) } let(:gitlab_host) { "https://gitlab.example.com" } let(:relative_url_root) { "/gitlab" } @@ -37,11 +37,23 @@ describe Avatarable do project.visibility_level = visibility_level end - let(:avatar_path) { (avatar_path_prefix + [project.avatar.url]).join } + let(:avatar_path) { (avatar_path_prefix + [project.avatar.local_url]).join } it 'returns the expected avatar path' do expect(project.avatar_path(only_path: only_path)).to eq(avatar_path) end + + context "when avatar is stored remotely" do + before do + stub_uploads_object_storage(AvatarUploader) + + project.avatar.migrate!(ObjectStorage::Store::REMOTE) + end + + it 'returns the expected avatar path' do + expect(project.avatar_url(only_path: only_path)).to eq(avatar_path) + end + end end end end diff --git a/spec/models/deploy_token_spec.rb b/spec/models/deploy_token_spec.rb index 780b200e837..f8d51a95833 100644 --- a/spec/models/deploy_token_spec.rb +++ b/spec/models/deploy_token_spec.rb @@ -142,4 +142,23 @@ describe DeployToken do end end end + + describe '.gitlab_deploy_token' do + let(:project) { create(:project ) } + + subject { project.deploy_tokens.gitlab_deploy_token } + + context 'with a gitlab deploy token associated' do + it 'should return the gitlab deploy token' do + deploy_token = create(:deploy_token, :gitlab_deploy_token, projects: [project]) + is_expected.to eq(deploy_token) + end + end + + context 'with no gitlab deploy token associated' do + it 'should return nil' do + is_expected.to be_nil + end + end + end end diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb index 56161bfcc28..25d6597084c 100644 --- a/spec/models/environment_spec.rb +++ b/spec/models/environment_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe Environment do - let(:project) { create(:project) } + let(:project) { create(:project, :stubbed_repository) } subject(:environment) { create(:environment, project: project) } it { is_expected.to belong_to(:project) } @@ -201,7 +201,7 @@ describe Environment do end describe '#stop_with_action!' do - let(:user) { create(:admin) } + let(:user) { create(:user) } subject { environment.stop_with_action!(user) } diff --git a/spec/models/project_ci_cd_setting_spec.rb b/spec/models/project_ci_cd_setting_spec.rb new file mode 100644 index 00000000000..4aa62028169 --- /dev/null +++ b/spec/models/project_ci_cd_setting_spec.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe ProjectCiCdSetting do + describe '.available?' do + before do + described_class.reset_column_information + end + + it 'returns true' do + expect(described_class).to be_available + end + + it 'memoizes the schema version' do + expect(ActiveRecord::Migrator) + .to receive(:current_version) + .and_call_original + .once + + 2.times { described_class.available? } + end + end +end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 2675c2f52c1..127eb998abe 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -93,6 +93,15 @@ describe Project do end end + context 'when creating a new project' do + it 'automatically creates a CI/CD settings row' do + project = create(:project) + + expect(project.ci_cd_settings).to be_an_instance_of(ProjectCiCdSetting) + expect(project.ci_cd_settings).to be_persisted + end + end + describe '#members & #requesters' do let(:project) { create(:project, :public, :access_requestable) } let(:requester) { create(:user) } @@ -325,7 +334,7 @@ describe Project do let(:owner) { create(:user, name: 'Gitlab') } let(:namespace) { create(:namespace, path: 'sample-namespace', owner: owner) } let(:project) { create(:project, path: 'sample-project', namespace: namespace) } - let(:group) { create(:group, name: 'Group', path: 'sample-group', owner: owner) } + let(:group) { create(:group, name: 'Group', path: 'sample-group') } context 'when nil argument' do it 'returns nil' do @@ -1483,52 +1492,6 @@ describe Project do end end - describe '#user_can_push_to_empty_repo?' do - let(:project) { create(:project) } - let(:user) { create(:user) } - - it 'returns false when default_branch_protection is in full protection and user is developer' do - project.add_developer(user) - stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_FULL) - - expect(project.user_can_push_to_empty_repo?(user)).to be_falsey - end - - it 'returns false when default_branch_protection only lets devs merge and user is dev' do - project.add_developer(user) - stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_DEV_CAN_MERGE) - - expect(project.user_can_push_to_empty_repo?(user)).to be_falsey - end - - it 'returns true when default_branch_protection lets devs push and user is developer' do - project.add_developer(user) - stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_DEV_CAN_PUSH) - - expect(project.user_can_push_to_empty_repo?(user)).to be_truthy - end - - it 'returns true when default_branch_protection is unprotected and user is developer' do - project.add_developer(user) - stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_NONE) - - expect(project.user_can_push_to_empty_repo?(user)).to be_truthy - end - - it 'returns true when user is master' do - project.add_master(user) - - expect(project.user_can_push_to_empty_repo?(user)).to be_truthy - end - - it 'returns false when the repo is not empty' do - project.add_master(user) - expect(project).to receive(:empty_repo?).and_return(false) - - expect(project.user_can_push_to_empty_repo?(user)).to be_falsey - end - end - describe '#container_registry_url' do let(:project) { create(:project) } @@ -3585,4 +3548,44 @@ describe Project do it { is_expected.not_to be_valid } end end + + describe '#gitlab_deploy_token' do + let(:project) { create(:project) } + + subject { project.gitlab_deploy_token } + + context 'when there is a gitlab deploy token associated' do + let!(:deploy_token) { create(:deploy_token, :gitlab_deploy_token, projects: [project]) } + + it { is_expected.to eq(deploy_token) } + end + + context 'when there is no a gitlab deploy token associated' do + it { is_expected.to be_nil } + end + + context 'when there is a gitlab deploy token associated but is has been revoked' do + let!(:deploy_token) { create(:deploy_token, :gitlab_deploy_token, :revoked, projects: [project]) } + it { is_expected.to be_nil } + end + + context 'when there is a gitlab deploy token associated but it is expired' do + let!(:deploy_token) { create(:deploy_token, :gitlab_deploy_token, :expired, projects: [project]) } + + it { is_expected.to be_nil } + end + + context 'when there is a deploy token associated with a different name' do + let!(:deploy_token) { create(:deploy_token, projects: [project]) } + + it { is_expected.to be_nil } + end + + context 'when there is a deploy token associated to a different project' do + let(:project_2) { create(:project) } + let!(:deploy_token) { create(:deploy_token, projects: [project_2]) } + + it { is_expected.to be_nil } + end + end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 35db7616efb..3f2eb58f009 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -1164,8 +1164,12 @@ describe User do end context 'with a group route matching the given path' do + let!(:group) { create(:group, path: 'group_path') } + context 'when the group namespace has an owner_id (legacy data)' do - let!(:group) { create(:group, path: 'group_path', owner: user) } + before do + group.update!(owner_id: user.id) + end it 'returns nil' do expect(described_class.find_by_full_path('group_path')).to eq(nil) @@ -1173,8 +1177,6 @@ describe User do end context 'when the group namespace does not have an owner_id' do - let!(:group) { create(:group, path: 'group_path') } - it 'returns nil' do expect(described_class.find_by_full_path('group_path')).to eq(nil) end diff --git a/spec/policies/group_policy_spec.rb b/spec/policies/group_policy_spec.rb index b4d25e06d9a..9b5c290b9f9 100644 --- a/spec/policies/group_policy_spec.rb +++ b/spec/policies/group_policy_spec.rb @@ -7,9 +7,9 @@ describe GroupPolicy do let(:master) { create(:user) } let(:owner) { create(:user) } let(:admin) { create(:admin) } - let(:group) { create(:group) } + let(:group) { create(:group, :private) } - let(:guest_permissions) { [:read_group, :upload_file, :read_namespace] } + let(:guest_permissions) { [:read_label, :read_group, :upload_file, :read_namespace] } let(:reporter_permissions) { [:admin_label] } @@ -50,6 +50,7 @@ describe GroupPolicy do end context 'with no user' do + let(:group) { create(:group, :public) } let(:current_user) { nil } it do @@ -63,6 +64,28 @@ describe GroupPolicy do end end + context 'has projects' do + let(:current_user) { create(:user) } + let(:project) { create(:project, namespace: group) } + + before do + project.add_developer(current_user) + end + + it do + expect_allowed(:read_group, :read_label) + end + + context 'in subgroups', :nested_groups do + let(:subgroup) { create(:group, :private, parent: group) } + let(:project) { create(:project, namespace: subgroup) } + + it do + expect_allowed(:read_group, :read_label) + end + end + end + context 'guests' do let(:current_user) { guest } diff --git a/spec/presenters/project_presenter_spec.rb b/spec/presenters/project_presenter_spec.rb index 55962f345d4..830d2ee3b20 100644 --- a/spec/presenters/project_presenter_spec.rb +++ b/spec/presenters/project_presenter_spec.rb @@ -208,6 +208,17 @@ describe ProjectPresenter do it 'returns nil if user cannot push' do expect(presenter.new_file_anchor_data).to be_nil end + + context 'when the project is empty' do + let(:project) { create(:project, :empty_repo) } + + # Since we protect the default branch for empty repos + it 'is empty for a developer' do + project.add_developer(user) + + expect(presenter.new_file_anchor_data).to be_nil + end + end end describe '#readme_anchor_data' do @@ -321,7 +332,7 @@ describe ProjectPresenter do expect(presenter.autodevops_anchor_data).to eq(OpenStruct.new(enabled: false, label: 'Enable Auto DevOps', - link: presenter.project_settings_ci_cd_path(project, anchor: 'js-general-pipeline-settings'))) + link: presenter.project_settings_ci_cd_path(project, anchor: 'autodevops-settings'))) end end end diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index 17272cb00e5..85a571b8f0e 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -685,7 +685,8 @@ describe API::Projects do issues_enabled: false, merge_requests_enabled: false, wiki_enabled: false, - request_access_enabled: true + request_access_enabled: true, + jobs_enabled: true }) post api("/projects/user/#{user.id}", admin), project diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index f406d2ffb22..e8196980a8c 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -212,6 +212,18 @@ describe API::Users do expect(json_response.last['id']).to eq(user.id) end + it 'returns users with 2fa enabled' do + admin + user + user_with_2fa = create(:user, :two_factor_via_otp) + + get api('/users', admin), { two_factor: 'enabled' } + + expect(response).to match_response_schema('public_api/v4/user/admins') + expect(json_response.size).to eq(1) + expect(json_response.first['id']).to eq(user_with_2fa.id) + end + it 'returns 400 when provided incorrect sort params' do get api('/users', admin), { order_by: 'magic', sort: 'asc' } diff --git a/spec/services/ci/retry_pipeline_service_spec.rb b/spec/services/ci/retry_pipeline_service_spec.rb index 6ce75c65c8c..f1acfc48468 100644 --- a/spec/services/ci/retry_pipeline_service_spec.rb +++ b/spec/services/ci/retry_pipeline_service_spec.rb @@ -235,6 +235,8 @@ describe Ci::RetryPipelineService, '#execute' do context 'when user is not allowed to trigger manual action' do before do project.add_developer(user) + create(:protected_branch, :masters_can_push, + name: pipeline.ref, project: project) end context 'when there is a failed manual action present' do diff --git a/spec/services/groups/nested_create_service_spec.rb b/spec/services/groups/nested_create_service_spec.rb index 6491fb34777..86fdd43c1e5 100644 --- a/spec/services/groups/nested_create_service_spec.rb +++ b/spec/services/groups/nested_create_service_spec.rb @@ -59,8 +59,11 @@ describe Groups::NestedCreateService do describe "#execute" do it 'returns the group if it already existed' do - parent = create(:group, path: 'a-group', owner: user) - child = create(:group, path: 'a-sub-group', parent: parent, owner: user) + parent = create(:group, path: 'a-group') + child = create(:group, path: 'a-sub-group', parent: parent) + + parent.add_owner(user) + child.add_owner(user) expect(service.execute).to eq(child) end diff --git a/spec/services/quick_actions/interpret_service_spec.rb b/spec/services/quick_actions/interpret_service_spec.rb index f793f55e51b..bd835a1fca6 100644 --- a/spec/services/quick_actions/interpret_service_spec.rb +++ b/spec/services/quick_actions/interpret_service_spec.rb @@ -306,6 +306,23 @@ describe QuickActions::InterpretService do end end + shared_examples 'copy_metadata command' do + it 'fetches issue or merge request and copies labels and milestone if content contains /copy_metadata reference' do + source_issuable # populate the issue + todo_label # populate this label + inreview_label # populate this label + _, updates = service.execute(content, issuable) + + expect(updates[:add_label_ids]).to match_array([inreview_label.id, todo_label.id]) + + if source_issuable.milestone + expect(updates[:milestone_id]).to eq(source_issuable.milestone.id) + else + expect(updates).not_to have_key(:milestone_id) + end + end + end + shared_examples 'shrug command' do it 'appends ¯\_(ツ)_/¯ to the comment' do new_content, _ = service.execute(content, issuable) @@ -757,6 +774,65 @@ describe QuickActions::InterpretService do let(:issuable) { issue } end + context '/copy_metadata command' do + let(:todo_label) { create(:label, project: project, title: 'To Do') } + let(:inreview_label) { create(:label, project: project, title: 'In Review') } + + it_behaves_like 'empty command' do + let(:content) { '/copy_metadata' } + let(:issuable) { issue } + end + + it_behaves_like 'copy_metadata command' do + let(:source_issuable) { create(:labeled_issue, project: project, labels: [inreview_label, todo_label]) } + + let(:content) { "/copy_metadata #{source_issuable.to_reference}" } + let(:issuable) { issue } + end + + context 'when the parent issuable has a milestone' do + it_behaves_like 'copy_metadata command' do + let(:source_issuable) { create(:labeled_issue, project: project, labels: [todo_label, inreview_label], milestone: milestone) } + + let(:content) { "/copy_metadata #{source_issuable.to_reference(project)}" } + let(:issuable) { issue } + end + end + + context 'when more than one issuable is passed' do + it_behaves_like 'copy_metadata command' do + let(:source_issuable) { create(:labeled_issue, project: project, labels: [inreview_label, todo_label]) } + let(:other_label) { create(:label, project: project, title: 'Other') } + let(:other_source_issuable) { create(:labeled_issue, project: project, labels: [other_label]) } + + let(:content) { "/copy_metadata #{source_issuable.to_reference} #{other_source_issuable.to_reference}" } + let(:issuable) { issue } + end + end + + context 'cross project references' do + it_behaves_like 'empty command' do + let(:other_project) { create(:project, :public) } + let(:source_issuable) { create(:labeled_issue, project: other_project, labels: [todo_label, inreview_label]) } + let(:content) { "/copy_metadata #{source_issuable.to_reference(project)}" } + let(:issuable) { issue } + end + + it_behaves_like 'empty command' do + let(:content) { "/copy_metadata imaginary#1234" } + let(:issuable) { issue } + end + + it_behaves_like 'empty command' do + let(:other_project) { create(:project, :private) } + let(:source_issuable) { create(:issue, project: other_project) } + + let(:content) { "/copy_metadata #{source_issuable.to_reference(project)}" } + let(:issuable) { issue } + end + end + end + context '/duplicate command' do it_behaves_like 'duplicate command' do let(:issue_duplicate) { create(:issue, project: project) } diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 83664bae046..53045815a6a 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -32,42 +32,19 @@ require 'rainbow/ext/string' # Requires supporting ruby files with custom matchers and macros, etc, # in spec/support/ and its subdirectories. +# Requires helpers, and shared contexts/examples first since they're used in other support files +Dir[Rails.root.join("spec/support/helpers/*.rb")].each { |f| require f } +Dir[Rails.root.join("spec/support/shared_contexts/*.rb")].each { |f| require f } +Dir[Rails.root.join("spec/support/shared_examples/*.rb")].each { |f| require f } Dir[Rails.root.join("spec/support/**/*.rb")].each { |f| require f } RSpec.configure do |config| config.use_transactional_fixtures = false config.use_instantiated_fixtures = false - config.mock_with :rspec config.verbose_retry = true config.display_try_failure_messages = true - config.include Devise::Test::ControllerHelpers, type: :controller - config.include Devise::Test::ControllerHelpers, type: :view - config.include Devise::Test::IntegrationHelpers, type: :feature - config.include Warden::Test::Helpers, type: :request - config.include LoginHelpers, type: :feature - config.include SearchHelpers, type: :feature - config.include CookieHelper, :js - config.include InputHelper, :js - config.include SelectionHelper, :js - config.include InspectRequests, :js - config.include WaitForRequests, :js - config.include LiveDebugger, :js - config.include StubConfiguration - config.include EmailHelpers, :mailer, type: :mailer - config.include TestEnv - config.include ActiveJob::TestHelper - config.include ActiveSupport::Testing::TimeHelpers - config.include StubGitlabCalls - config.include StubGitlabData - config.include ApiHelpers, :api - config.include Gitlab::Routing, type: :routing - config.include MigrationsHelpers, :migration - config.include StubFeatureFlags - config.include StubENV - config.include ExpectOffense - config.infer_spec_type_from_file_location! config.define_derived_metadata(file_path: %r{/spec/}) do |metadata| @@ -82,7 +59,33 @@ RSpec.configure do |config| metadata[:type] = match[1].singularize.to_sym if match end - config.raise_errors_for_deprecations! + config.include ActiveJob::TestHelper + config.include ActiveSupport::Testing::TimeHelpers + config.include CycleAnalyticsHelpers + config.include ExpectOffense + config.include FactoryBot::Syntax::Methods + config.include FixtureHelpers + config.include GitlabRoutingHelper + config.include StubFeatureFlags + config.include StubGitlabCalls + config.include StubGitlabData + config.include TestEnv + config.include Devise::Test::ControllerHelpers, type: :controller + config.include Devise::Test::IntegrationHelpers, type: :feature + config.include LoginHelpers, type: :feature + config.include SearchHelpers, type: :feature + config.include EmailHelpers, :mailer, type: :mailer + config.include Warden::Test::Helpers, type: :request + config.include Gitlab::Routing, type: :routing + config.include Devise::Test::ControllerHelpers, type: :view + config.include ApiHelpers, :api + config.include CookieHelper, :js + config.include InputHelper, :js + config.include SelectionHelper, :js + config.include InspectRequests, :js + config.include WaitForRequests, :js + config.include LiveDebugger, :js + config.include MigrationsHelpers, :migration if ENV['CI'] # This includes the first try, i.e. tests will be run 4 times before failing. diff --git a/spec/support/capybara.rb b/spec/support/capybara.rb index 9ddcc5f2fbf..c0ceb0f6605 100644 --- a/spec/support/capybara.rb +++ b/spec/support/capybara.rb @@ -60,6 +60,8 @@ Capybara::Screenshot.register_driver(:chrome) do |driver, path| end RSpec.configure do |config| + config.include CapybaraHelpers, type: :feature + config.before(:context, :js) do next if $capybara_server_already_started diff --git a/spec/support/controllers/githubish_import_controller_shared_examples.rb b/spec/support/controllers/githubish_import_controller_shared_examples.rb index 3321f920666..368439aa5b0 100644 --- a/spec/support/controllers/githubish_import_controller_shared_examples.rb +++ b/spec/support/controllers/githubish_import_controller_shared_examples.rb @@ -56,7 +56,7 @@ shared_examples 'a GitHub-ish import controller: GET status' do end it "assigns variables" do - project = create(:project, import_type: provider, creator_id: user.id) + project = create(:project, import_type: provider, namespace: user.namespace) stub_client(repos: [repo, org_repo], orgs: [org], org_repos: [org_repo]) get :status @@ -69,7 +69,7 @@ shared_examples 'a GitHub-ish import controller: GET status' do end it "does not show already added project" do - project = create(:project, import_type: provider, creator_id: user.id, import_source: 'asd/vim') + project = create(:project, import_type: provider, namespace: user.namespace, import_source: 'asd/vim') stub_client(repos: [repo], orgs: []) get :status @@ -257,11 +257,12 @@ shared_examples 'a GitHub-ish import controller: POST create' do end context 'user has chosen an existing nested namespace and name for the project', :postgresql do - let(:parent_namespace) { create(:group, name: 'foo', owner: user) } + let(:parent_namespace) { create(:group, name: 'foo') } let(:nested_namespace) { create(:group, name: 'bar', parent: parent_namespace) } let(:test_name) { 'test_name' } before do + parent_namespace.add_owner(user) nested_namespace.add_owner(user) end @@ -307,7 +308,7 @@ shared_examples 'a GitHub-ish import controller: POST create' do context 'user has chosen existent and non-existent nested namespaces and name for the project', :postgresql do let(:test_name) { 'test_name' } - let!(:parent_namespace) { create(:group, name: 'foo', owner: user) } + let!(:parent_namespace) { create(:group, name: 'foo') } before do parent_namespace.add_owner(user) diff --git a/spec/support/controllers/ldap_omniauth_callbacks_controller_shared_context.rb b/spec/support/controllers/ldap_omniauth_callbacks_controller_shared_context.rb new file mode 100644 index 00000000000..72912ffb89d --- /dev/null +++ b/spec/support/controllers/ldap_omniauth_callbacks_controller_shared_context.rb @@ -0,0 +1,33 @@ +require 'spec_helper' + +shared_context 'Ldap::OmniauthCallbacksController' do + include LoginHelpers + include LdapHelpers + + let(:uid) { 'my-uid' } + let(:provider) { 'ldapmain' } + let(:valid_login?) { true } + let(:user) { create(:omniauth_user, extern_uid: uid, provider: provider) } + let(:ldap_server_config) do + { main: ldap_config_defaults(:main) } + end + + def ldap_config_defaults(key, hash = {}) + { + provider_name: "ldap#{key}", + attributes: {}, + encryption: 'plain' + }.merge(hash) + end + + before do + stub_ldap_setting(enabled: true, servers: ldap_server_config) + described_class.define_providers! + Rails.application.reload_routes! + + mock_auth_hash(provider.to_s, uid, user.email) + stub_omniauth_provider(provider, context: request) + + allow(Gitlab::Auth::LDAP::Access).to receive(:allowed?).and_return(valid_login?) + end +end diff --git a/spec/support/factory_bot.rb b/spec/support/factory_bot.rb deleted file mode 100644 index c7890e49c66..00000000000 --- a/spec/support/factory_bot.rb +++ /dev/null @@ -1,3 +0,0 @@ -RSpec.configure do |config| - config.include FactoryBot::Syntax::Methods -end diff --git a/spec/support/generate-seed-repo-rb b/spec/support/generate-seed-repo-rb index 876b3b8242d..44b3de23b99 100755 --- a/spec/support/generate-seed-repo-rb +++ b/spec/support/generate-seed-repo-rb @@ -8,7 +8,7 @@ # # Usage: # -# ./spec/support/generate-seed-repo-rb > spec/support/seed_repo.rb +# ./spec/support/generate-seed-repo-rb > spec/support/helpers/seed_repo.rb # # diff --git a/spec/support/gitlab-git-test.git/README.md b/spec/support/gitlab-git-test.git/README.md index f072cd421be..f757e613ee6 100644 --- a/spec/support/gitlab-git-test.git/README.md +++ b/spec/support/gitlab-git-test.git/README.md @@ -12,5 +12,5 @@ inflate the size of the gitlab-ce repository. - make changes in your local clone of gitlab-git-test - run `git push` which will push to your local source `gitlab-ce/spec/support/gitlab-git-test.git` - in gitlab-ce: run `spec/support/prepare-gitlab-git-test-for-commit` -- in gitlab-ce: `git add spec/support/seed_repo.rb spec/support/gitlab-git-test.git` +- in gitlab-ce: `git add spec/support/helpers/seed_repo.rb spec/support/gitlab-git-test.git` - commit your changes in gitlab-ce diff --git a/spec/support/api_helpers.rb b/spec/support/helpers/api_helpers.rb index ac0c7a9b493..ac0c7a9b493 100644 --- a/spec/support/api_helpers.rb +++ b/spec/support/helpers/api_helpers.rb diff --git a/spec/support/bare_repo_operations.rb b/spec/support/helpers/bare_repo_operations.rb index 3f4a4243cb6..3f4a4243cb6 100644 --- a/spec/support/bare_repo_operations.rb +++ b/spec/support/helpers/bare_repo_operations.rb diff --git a/spec/support/board_helpers.rb b/spec/support/helpers/board_helpers.rb index 507d0432d7f..507d0432d7f 100644 --- a/spec/support/board_helpers.rb +++ b/spec/support/helpers/board_helpers.rb diff --git a/spec/support/capybara_helpers.rb b/spec/support/helpers/capybara_helpers.rb index 868233416bf..bcc2df44708 100644 --- a/spec/support/capybara_helpers.rb +++ b/spec/support/helpers/capybara_helpers.rb @@ -41,7 +41,3 @@ module CapybaraHelpers page.driver.browser.manage.delete_cookie('_gitlab_session') end end - -RSpec.configure do |config| - config.include CapybaraHelpers, type: :feature -end diff --git a/spec/support/cookie_helper.rb b/spec/support/helpers/cookie_helper.rb index 5ff7b0b68c9..5ff7b0b68c9 100644 --- a/spec/support/cookie_helper.rb +++ b/spec/support/helpers/cookie_helper.rb diff --git a/spec/support/cycle_analytics_helpers.rb b/spec/support/helpers/cycle_analytics_helpers.rb index 73cc64c0b74..55359d36597 100644 --- a/spec/support/cycle_analytics_helpers.rb +++ b/spec/support/helpers/cycle_analytics_helpers.rb @@ -135,7 +135,3 @@ module CycleAnalyticsHelpers end end end - -RSpec.configure do |config| - config.include CycleAnalyticsHelpers -end diff --git a/spec/support/database_connection_helpers.rb b/spec/support/helpers/database_connection_helpers.rb index 763329499f0..763329499f0 100644 --- a/spec/support/database_connection_helpers.rb +++ b/spec/support/helpers/database_connection_helpers.rb diff --git a/spec/support/devise_helpers.rb b/spec/support/helpers/devise_helpers.rb index 66874e10f38..66874e10f38 100644 --- a/spec/support/devise_helpers.rb +++ b/spec/support/helpers/devise_helpers.rb diff --git a/spec/support/drag_to_helper.rb b/spec/support/helpers/drag_to_helper.rb index ae149631ed9..ae149631ed9 100644 --- a/spec/support/drag_to_helper.rb +++ b/spec/support/helpers/drag_to_helper.rb diff --git a/spec/support/dropzone_helper.rb b/spec/support/helpers/dropzone_helper.rb index fe72d320fcf..fe72d320fcf 100644 --- a/spec/support/dropzone_helper.rb +++ b/spec/support/helpers/dropzone_helper.rb diff --git a/spec/support/email_helpers.rb b/spec/support/helpers/email_helpers.rb index 1fb8252459f..1fb8252459f 100644 --- a/spec/support/email_helpers.rb +++ b/spec/support/helpers/email_helpers.rb diff --git a/spec/support/fake_migration_classes.rb b/spec/support/helpers/fake_migration_classes.rb index b0fc8422857..b0fc8422857 100644 --- a/spec/support/fake_migration_classes.rb +++ b/spec/support/helpers/fake_migration_classes.rb diff --git a/spec/support/fake_u2f_device.rb b/spec/support/helpers/fake_u2f_device.rb index a7605cd483a..a7605cd483a 100644 --- a/spec/support/fake_u2f_device.rb +++ b/spec/support/helpers/fake_u2f_device.rb diff --git a/spec/support/filter_item_select_helper.rb b/spec/support/helpers/filter_item_select_helper.rb index 519e84d359e..519e84d359e 100644 --- a/spec/support/filter_item_select_helper.rb +++ b/spec/support/helpers/filter_item_select_helper.rb diff --git a/spec/support/filter_spec_helper.rb b/spec/support/helpers/filter_spec_helper.rb index 721d359c2ee..721d359c2ee 100644 --- a/spec/support/filter_spec_helper.rb +++ b/spec/support/helpers/filter_spec_helper.rb diff --git a/spec/support/filtered_search_helpers.rb b/spec/support/helpers/filtered_search_helpers.rb index 5f42ff77fb2..5f42ff77fb2 100644 --- a/spec/support/filtered_search_helpers.rb +++ b/spec/support/helpers/filtered_search_helpers.rb diff --git a/spec/support/fixture_helpers.rb b/spec/support/helpers/fixture_helpers.rb index 8854382dc6b..611d19f36a0 100644 --- a/spec/support/fixture_helpers.rb +++ b/spec/support/helpers/fixture_helpers.rb @@ -9,7 +9,3 @@ module FixtureHelpers File.expand_path(Rails.root.join(dir, 'spec', 'fixtures', filename)) end end - -RSpec.configure do |config| - config.include FixtureHelpers -end diff --git a/spec/support/git_http_helpers.rb b/spec/support/helpers/git_http_helpers.rb index b8289e6c5f1..b8289e6c5f1 100644 --- a/spec/support/git_http_helpers.rb +++ b/spec/support/helpers/git_http_helpers.rb diff --git a/spec/support/helpers/gitlab_verify_helpers.rb b/spec/support/helpers/gitlab_verify_helpers.rb new file mode 100644 index 00000000000..5df4bf24ec2 --- /dev/null +++ b/spec/support/helpers/gitlab_verify_helpers.rb @@ -0,0 +1,25 @@ +module GitlabVerifyHelpers + def collect_ranges(args = {}) + verifier = described_class.new(args.merge(batch_size: 1)) + + collect_results(verifier).map { |range, _| range } + end + + def collect_failures + verifier = described_class.new(batch_size: 1) + + out = {} + + collect_results(verifier).map { |_, failures| out.merge!(failures) } + + out + end + + def collect_results(verifier) + out = [] + + verifier.run_batches { |*args| out << args } + + out + end +end diff --git a/spec/support/gpg_helpers.rb b/spec/support/helpers/gpg_helpers.rb index 3f7279a50e0..3f7279a50e0 100644 --- a/spec/support/gpg_helpers.rb +++ b/spec/support/helpers/gpg_helpers.rb diff --git a/spec/support/import_spec_helper.rb b/spec/support/helpers/import_spec_helper.rb index d4eced724fa..d4eced724fa 100644 --- a/spec/support/import_spec_helper.rb +++ b/spec/support/helpers/import_spec_helper.rb diff --git a/spec/support/input_helper.rb b/spec/support/helpers/input_helper.rb index acbb42274ec..acbb42274ec 100644 --- a/spec/support/input_helper.rb +++ b/spec/support/helpers/input_helper.rb diff --git a/spec/support/inspect_requests.rb b/spec/support/helpers/inspect_requests.rb index 88ddc5c7f6c..88ddc5c7f6c 100644 --- a/spec/support/inspect_requests.rb +++ b/spec/support/helpers/inspect_requests.rb diff --git a/spec/support/issue_helpers.rb b/spec/support/helpers/issue_helpers.rb index ffd72515f37..ffd72515f37 100644 --- a/spec/support/issue_helpers.rb +++ b/spec/support/helpers/issue_helpers.rb diff --git a/spec/support/javascript_fixtures_helpers.rb b/spec/support/helpers/javascript_fixtures_helpers.rb index 2197bc9d853..2197bc9d853 100644 --- a/spec/support/javascript_fixtures_helpers.rb +++ b/spec/support/helpers/javascript_fixtures_helpers.rb diff --git a/spec/support/jira_service_helper.rb b/spec/support/helpers/jira_service_helper.rb index 88a7aeba461..88a7aeba461 100644 --- a/spec/support/jira_service_helper.rb +++ b/spec/support/helpers/jira_service_helper.rb diff --git a/spec/support/kubernetes_helpers.rb b/spec/support/helpers/kubernetes_helpers.rb index e46b61b6461..e46b61b6461 100644 --- a/spec/support/kubernetes_helpers.rb +++ b/spec/support/helpers/kubernetes_helpers.rb diff --git a/spec/support/ldap_helpers.rb b/spec/support/helpers/ldap_helpers.rb index 0e87b3d359d..b90bbc4b106 100644 --- a/spec/support/ldap_helpers.rb +++ b/spec/support/helpers/ldap_helpers.rb @@ -18,6 +18,10 @@ module LdapHelpers allow_any_instance_of(::Gitlab::Auth::LDAP::Config).to receive_messages(messages) end + def stub_ldap_setting(messages) + allow(Gitlab.config.ldap).to receive_messages(to_settings(messages)) + end + # Stub an LDAP person search and provide the return entry. Specify `nil` for # `entry` to simulate when an LDAP person is not found # diff --git a/spec/support/live_debugger.rb b/spec/support/helpers/live_debugger.rb index 911eb48a8ca..911eb48a8ca 100644 --- a/spec/support/live_debugger.rb +++ b/spec/support/helpers/live_debugger.rb diff --git a/spec/support/login_helpers.rb b/spec/support/helpers/login_helpers.rb index db34090e971..72e5c2d66dd 100644 --- a/spec/support/login_helpers.rb +++ b/spec/support/helpers/login_helpers.rb @@ -112,7 +112,7 @@ module LoginHelpers } } }) - Rails.application.env_config['omniauth.auth'] = OmniAuth.config.mock_auth[:saml] + Rails.application.env_config['omniauth.auth'] = OmniAuth.config.mock_auth[provider.to_sym] end def mock_saml_config @@ -129,7 +129,7 @@ module LoginHelpers env = env_from_context(context) set_devise_mapping(context: context) - env['omniauth.auth'] = OmniAuth.config.mock_auth[provider] + env['omniauth.auth'] = OmniAuth.config.mock_auth[provider.to_sym] end def stub_omniauth_saml_config(messages) diff --git a/spec/support/markdown_feature.rb b/spec/support/helpers/markdown_feature.rb index 39e94ad53de..39e94ad53de 100644 --- a/spec/support/markdown_feature.rb +++ b/spec/support/helpers/markdown_feature.rb diff --git a/spec/support/merge_request_helpers.rb b/spec/support/helpers/merge_request_helpers.rb index 772adff4626..772adff4626 100644 --- a/spec/support/merge_request_helpers.rb +++ b/spec/support/helpers/merge_request_helpers.rb diff --git a/spec/support/migrations_helpers.rb b/spec/support/helpers/migrations_helpers.rb index 5d6f662e8fe..5d6f662e8fe 100644 --- a/spec/support/migrations_helpers.rb +++ b/spec/support/helpers/migrations_helpers.rb diff --git a/spec/support/mobile_helpers.rb b/spec/support/helpers/mobile_helpers.rb index 3b9eb84e824..3b9eb84e824 100644 --- a/spec/support/mobile_helpers.rb +++ b/spec/support/helpers/mobile_helpers.rb diff --git a/spec/support/project_forks_helper.rb b/spec/support/helpers/project_forks_helper.rb index 2c501a2a27c..2c501a2a27c 100644 --- a/spec/support/project_forks_helper.rb +++ b/spec/support/helpers/project_forks_helper.rb diff --git a/spec/support/prometheus_helpers.rb b/spec/support/helpers/prometheus_helpers.rb index 4212be2cc88..4212be2cc88 100644 --- a/spec/support/prometheus_helpers.rb +++ b/spec/support/helpers/prometheus_helpers.rb diff --git a/spec/support/helpers/query_recorder.rb b/spec/support/helpers/query_recorder.rb new file mode 100644 index 00000000000..28536bbef5e --- /dev/null +++ b/spec/support/helpers/query_recorder.rb @@ -0,0 +1,38 @@ +module ActiveRecord + class QueryRecorder + attr_reader :log, :cached + + def initialize(&block) + @log = [] + @cached = [] + ActiveSupport::Notifications.subscribed(method(:callback), 'sql.active_record', &block) + end + + def show_backtrace(values) + Rails.logger.debug("QueryRecorder SQL: #{values[:sql]}") + caller.each { |line| Rails.logger.debug(" --> #{line}") } + end + + def callback(name, start, finish, message_id, values) + show_backtrace(values) if ENV['QUERY_RECORDER_DEBUG'] + + if values[:name]&.include?("CACHE") + @cached << values[:sql] + elsif !values[:name]&.include?("SCHEMA") + @log << values[:sql] + end + end + + def count + @log.count + end + + def cached_count + @cached.count + end + + def log_message + @log.join("\n\n") + end + end +end diff --git a/spec/support/helpers/quick_actions_helpers.rb b/spec/support/helpers/quick_actions_helpers.rb new file mode 100644 index 00000000000..361190aa352 --- /dev/null +++ b/spec/support/helpers/quick_actions_helpers.rb @@ -0,0 +1,10 @@ +module QuickActionsHelpers + def write_note(text) + Sidekiq::Testing.fake! do + page.within('.js-main-target-form') do + fill_in 'note[note]', with: text + find('.js-comment-submit-button').click + end + end + end +end diff --git a/spec/support/rake_helpers.rb b/spec/support/helpers/rake_helpers.rb index 86bfeed107c..86bfeed107c 100644 --- a/spec/support/rake_helpers.rb +++ b/spec/support/helpers/rake_helpers.rb diff --git a/spec/support/reactive_caching_helpers.rb b/spec/support/helpers/reactive_caching_helpers.rb index e22dd974c6a..e22dd974c6a 100644 --- a/spec/support/reactive_caching_helpers.rb +++ b/spec/support/helpers/reactive_caching_helpers.rb diff --git a/spec/support/redis_without_keys.rb b/spec/support/helpers/redis_without_keys.rb index 6220167dee6..6220167dee6 100644 --- a/spec/support/redis_without_keys.rb +++ b/spec/support/helpers/redis_without_keys.rb diff --git a/spec/support/reference_parser_helpers.rb b/spec/support/helpers/reference_parser_helpers.rb index c01897ed1a1..c01897ed1a1 100644 --- a/spec/support/reference_parser_helpers.rb +++ b/spec/support/helpers/reference_parser_helpers.rb diff --git a/spec/support/repo_helpers.rb b/spec/support/helpers/repo_helpers.rb index 3c6956cf5e0..3c6956cf5e0 100644 --- a/spec/support/repo_helpers.rb +++ b/spec/support/helpers/repo_helpers.rb diff --git a/spec/support/search_helpers.rb b/spec/support/helpers/search_helpers.rb index abbbb636d66..abbbb636d66 100644 --- a/spec/support/search_helpers.rb +++ b/spec/support/helpers/search_helpers.rb diff --git a/spec/support/seed_helper.rb b/spec/support/helpers/seed_helper.rb index 11ef1fc477f..8fd107260cc 100644 --- a/spec/support/seed_helper.rb +++ b/spec/support/helpers/seed_helper.rb @@ -9,7 +9,7 @@ TEST_MUTABLE_REPO_PATH = 'mutable-repo.git'.freeze TEST_BROKEN_REPO_PATH = 'broken-repo.git'.freeze module SeedHelper - GITLAB_GIT_TEST_REPO_URL = File.expand_path('../gitlab-git-test.git', __FILE__).freeze + GITLAB_GIT_TEST_REPO_URL = File.expand_path('../gitlab-git-test.git', __dir__).freeze def ensure_seeds if File.exist?(SEED_STORAGE_PATH) @@ -108,11 +108,3 @@ bla/bla.txt { 'GIT_TEMPLATE_DIR' => '' } end end - -RSpec.configure do |config| - config.include SeedHelper, :seed_helper - - config.before(:all, :seed_helper) do - ensure_seeds - end -end diff --git a/spec/support/seed_repo.rb b/spec/support/helpers/seed_repo.rb index b4868e82cd7..b4868e82cd7 100644 --- a/spec/support/seed_repo.rb +++ b/spec/support/helpers/seed_repo.rb diff --git a/spec/support/select2_helper.rb b/spec/support/helpers/select2_helper.rb index 90618ba5b19..90618ba5b19 100644 --- a/spec/support/select2_helper.rb +++ b/spec/support/helpers/select2_helper.rb diff --git a/spec/support/selection_helper.rb b/spec/support/helpers/selection_helper.rb index b4725b137b2..b4725b137b2 100644 --- a/spec/support/selection_helper.rb +++ b/spec/support/helpers/selection_helper.rb diff --git a/spec/support/helpers/sorting_helper.rb b/spec/support/helpers/sorting_helper.rb new file mode 100644 index 00000000000..577518d726c --- /dev/null +++ b/spec/support/helpers/sorting_helper.rb @@ -0,0 +1,18 @@ +# Helper allows you to sort items +# +# Params +# value - value for sorting +# +# Usage: +# include SortingHelper +# +# sorting_by('Oldest updated') +# +module SortingHelper + def sorting_by(value) + find('button.dropdown-toggle').click + page.within('.content ul.dropdown-menu.dropdown-menu-align-right li') do + click_link value + end + end +end diff --git a/spec/support/stub_configuration.rb b/spec/support/helpers/stub_configuration.rb index a75a3eaefcb..1823099dd9c 100644 --- a/spec/support/stub_configuration.rb +++ b/spec/support/helpers/stub_configuration.rb @@ -1,3 +1,6 @@ +require 'active_support/core_ext/hash/transform_values' +require 'active_support/hash_with_indifferent_access' + module StubConfiguration def stub_application_setting(messages) add_predicates(messages) diff --git a/spec/support/stub_env.rb b/spec/support/helpers/stub_env.rb index 36b90fc68d6..36b90fc68d6 100644 --- a/spec/support/stub_env.rb +++ b/spec/support/helpers/stub_env.rb diff --git a/spec/support/stub_feature_flags.rb b/spec/support/helpers/stub_feature_flags.rb index b96338bf548..b96338bf548 100644 --- a/spec/support/stub_feature_flags.rb +++ b/spec/support/helpers/stub_feature_flags.rb diff --git a/spec/support/stub_gitlab_calls.rb b/spec/support/helpers/stub_gitlab_calls.rb index c1618f5086c..c1618f5086c 100644 --- a/spec/support/stub_gitlab_calls.rb +++ b/spec/support/helpers/stub_gitlab_calls.rb diff --git a/spec/support/stub_gitlab_data.rb b/spec/support/helpers/stub_gitlab_data.rb index fa402f35b95..fa402f35b95 100644 --- a/spec/support/stub_gitlab_data.rb +++ b/spec/support/helpers/stub_gitlab_data.rb diff --git a/spec/support/stub_object_storage.rb b/spec/support/helpers/stub_object_storage.rb index 6e88641da42..19d744b959a 100644 --- a/spec/support/stub_object_storage.rb +++ b/spec/support/helpers/stub_object_storage.rb @@ -1,4 +1,4 @@ -module StubConfiguration +module StubObjectStorage def stub_object_storage_uploader( config:, uploader:, diff --git a/spec/support/test_env.rb b/spec/support/helpers/test_env.rb index d87f265cdf0..d87f265cdf0 100644 --- a/spec/support/test_env.rb +++ b/spec/support/helpers/test_env.rb diff --git a/spec/support/upload_helpers.rb b/spec/support/helpers/upload_helpers.rb index 5eead80c935..5eead80c935 100644 --- a/spec/support/upload_helpers.rb +++ b/spec/support/helpers/upload_helpers.rb diff --git a/spec/support/user_activities_helpers.rb b/spec/support/helpers/user_activities_helpers.rb index 44feb104644..44feb104644 100644 --- a/spec/support/user_activities_helpers.rb +++ b/spec/support/helpers/user_activities_helpers.rb diff --git a/spec/support/wait_for_requests.rb b/spec/support/helpers/wait_for_requests.rb index fda0e29f983..fda0e29f983 100644 --- a/spec/support/wait_for_requests.rb +++ b/spec/support/helpers/wait_for_requests.rb diff --git a/spec/support/workhorse_helpers.rb b/spec/support/helpers/workhorse_helpers.rb index ef1f9f68671..ef1f9f68671 100644 --- a/spec/support/workhorse_helpers.rb +++ b/spec/support/helpers/workhorse_helpers.rb diff --git a/spec/support/issuables_list_metadata_shared_examples.rb b/spec/support/issuables_list_metadata_shared_examples.rb deleted file mode 100644 index e61983c60b4..00000000000 --- a/spec/support/issuables_list_metadata_shared_examples.rb +++ /dev/null @@ -1,46 +0,0 @@ -shared_examples 'issuables list meta-data' do |issuable_type, action = nil| - before do - @issuable_ids = [] - - %w[fix improve/awesome].each do |source_branch| - issuable = - if issuable_type == :issue - create(issuable_type, project: project, author: project.creator) - else - create(issuable_type, source_project: project, source_branch: source_branch, author: project.creator) - end - - @issuable_ids << issuable.id - end - end - - it "creates indexed meta-data object for issuable notes and votes count" do - if action - get action, author_id: project.creator.id - else - get :index, namespace_id: project.namespace, project_id: project - end - - meta_data = assigns(:issuable_meta_data) - - aggregate_failures do - expect(meta_data.keys).to match_array(@issuable_ids) - expect(meta_data.values).to all(be_kind_of(Issuable::IssuableMeta)) - end - end - - describe "when given empty collection" do - let(:project2) { create(:project, :public) } - - it "doesn't execute any queries with false conditions" do - get_action = - if action - proc { get action, author_id: project.creator.id } - else - proc { get :index, namespace_id: project2.namespace, project_id: project2 } - end - - expect(&get_action).not_to make_queries_matching(/WHERE (?:1=0|0=1)/) - end - end -end diff --git a/spec/support/json_response_helpers.rb b/spec/support/json_response.rb index aa235529c56..210b0e6d867 100644 --- a/spec/support/json_response_helpers.rb +++ b/spec/support/json_response.rb @@ -1,7 +1,3 @@ -shared_context 'JSON response' do - let(:json_response) { JSON.parse(response.body) } -end - RSpec.configure do |config| config.include_context 'JSON response' config.include_context 'JSON response', type: :request diff --git a/spec/support/background_migrations_matchers.rb b/spec/support/matchers/background_migrations_matchers.rb index f4127efc6ae..f4127efc6ae 100644 --- a/spec/support/background_migrations_matchers.rb +++ b/spec/support/matchers/background_migrations_matchers.rb diff --git a/spec/support/query_recorder.rb b/spec/support/matchers/exceed_query_limit.rb index 8cf8f45a8b2..88d22a3ddd9 100644 --- a/spec/support/query_recorder.rb +++ b/spec/support/matchers/exceed_query_limit.rb @@ -1,42 +1,3 @@ -module ActiveRecord - class QueryRecorder - attr_reader :log, :cached - - def initialize(&block) - @log = [] - @cached = [] - ActiveSupport::Notifications.subscribed(method(:callback), 'sql.active_record', &block) - end - - def show_backtrace(values) - Rails.logger.debug("QueryRecorder SQL: #{values[:sql]}") - caller.each { |line| Rails.logger.debug(" --> #{line}") } - end - - def callback(name, start, finish, message_id, values) - show_backtrace(values) if ENV['QUERY_RECORDER_DEBUG'] - - if values[:name]&.include?("CACHE") - @cached << values[:sql] - elsif !values[:name]&.include?("SCHEMA") - @log << values[:sql] - end - end - - def count - @log.count - end - - def cached_count - @cached.count - end - - def log_message - @log.join("\n\n") - end - end -end - RSpec::Matchers.define :exceed_query_limit do |expected| supports_block_expectations diff --git a/spec/support/prepare-gitlab-git-test-for-commit b/spec/support/prepare-gitlab-git-test-for-commit index 3047786a599..d08e3ba5481 100755 --- a/spec/support/prepare-gitlab-git-test-for-commit +++ b/spec/support/prepare-gitlab-git-test-for-commit @@ -1,7 +1,7 @@ #!/usr/bin/env ruby abort unless [ - system('spec/support/generate-seed-repo-rb', out: 'spec/support/seed_repo.rb'), + system('spec/support/generate-seed-repo-rb', out: 'spec/support/helpers/seed_repo.rb'), system('spec/support/unpack-gitlab-git-test') ].all? diff --git a/spec/support/routing_helpers.rb b/spec/support/routing_helpers.rb deleted file mode 100644 index af1f4760804..00000000000 --- a/spec/support/routing_helpers.rb +++ /dev/null @@ -1,3 +0,0 @@ -RSpec.configure do |config| - config.include GitlabRoutingHelper -end diff --git a/spec/support/rspec.rb b/spec/support/rspec.rb new file mode 100644 index 00000000000..dffab22d8b5 --- /dev/null +++ b/spec/support/rspec.rb @@ -0,0 +1,12 @@ +require_relative "helpers/stub_configuration" +require_relative "helpers/stub_object_storage" +require_relative "helpers/stub_env" + +RSpec.configure do |config| + config.mock_with :rspec + config.raise_errors_for_deprecations! + + config.include StubConfiguration + config.include StubObjectStorage + config.include StubENV +end diff --git a/spec/support/seed.rb b/spec/support/seed.rb new file mode 100644 index 00000000000..bea2e9c3044 --- /dev/null +++ b/spec/support/seed.rb @@ -0,0 +1,7 @@ +RSpec.configure do |config| + config.include SeedHelper, :seed_helper + + config.before(:all, :seed_helper) do + ensure_seeds + end +end diff --git a/spec/support/shared_contexts/json_response_shared_context.rb b/spec/support/shared_contexts/json_response_shared_context.rb new file mode 100644 index 00000000000..df5fc288089 --- /dev/null +++ b/spec/support/shared_contexts/json_response_shared_context.rb @@ -0,0 +1,3 @@ +shared_context 'JSON response' do + let(:json_response) { JSON.parse(response.body) } +end diff --git a/spec/support/services_shared_context.rb b/spec/support/shared_contexts/services_shared_context.rb index 23f9b46ae0c..23f9b46ae0c 100644 --- a/spec/support/services_shared_context.rb +++ b/spec/support/shared_contexts/services_shared_context.rb diff --git a/spec/support/chat_slash_commands_shared_examples.rb b/spec/support/shared_examples/chat_slash_commands_shared_examples.rb index dc97a39f051..dc97a39f051 100644 --- a/spec/support/chat_slash_commands_shared_examples.rb +++ b/spec/support/shared_examples/chat_slash_commands_shared_examples.rb diff --git a/spec/support/email_format_shared_examples.rb b/spec/support/shared_examples/email_format_shared_examples.rb index b924a208e71..b924a208e71 100644 --- a/spec/support/email_format_shared_examples.rb +++ b/spec/support/shared_examples/email_format_shared_examples.rb diff --git a/spec/support/gitlab_verify.rb b/spec/support/shared_examples/gitlab_verify.rb index 13e2e37624d..560913ca92f 100644 --- a/spec/support/gitlab_verify.rb +++ b/spec/support/shared_examples/gitlab_verify.rb @@ -17,29 +17,3 @@ RSpec.shared_examples 'Gitlab::Verify::BatchVerifier subclass' do end end end - -module GitlabVerifyHelpers - def collect_ranges(args = {}) - verifier = described_class.new(args.merge(batch_size: 1)) - - collect_results(verifier).map { |range, _| range } - end - - def collect_failures - verifier = described_class.new(batch_size: 1) - - out = {} - - collect_results(verifier).map { |_, failures| out.merge!(failures) } - - out - end - - def collect_results(verifier) - out = [] - - verifier.run_batches { |*args| out << args } - - out - end -end diff --git a/spec/support/group_members_shared_example.rb b/spec/support/shared_examples/group_members_shared_example.rb index 547c83c7955..547c83c7955 100644 --- a/spec/support/group_members_shared_example.rb +++ b/spec/support/shared_examples/group_members_shared_example.rb diff --git a/spec/support/issuable_shared_examples.rb b/spec/support/shared_examples/issuable_shared_examples.rb index 42f3b4db23c..42f3b4db23c 100644 --- a/spec/support/issuable_shared_examples.rb +++ b/spec/support/shared_examples/issuable_shared_examples.rb diff --git a/spec/support/shared_examples/issuables_list_metadata_shared_examples.rb b/spec/support/shared_examples/issuables_list_metadata_shared_examples.rb new file mode 100644 index 00000000000..f4bc6f8efa5 --- /dev/null +++ b/spec/support/shared_examples/issuables_list_metadata_shared_examples.rb @@ -0,0 +1,62 @@ +shared_examples 'issuables list meta-data' do |issuable_type, action = nil| + include ProjectForksHelper + + def get_action(action, project) + if action + get action, author_id: project.creator.id + else + get :index, namespace_id: project.namespace, project_id: project + end + end + + def create_issuable(issuable_type, project, source_branch:) + if issuable_type == :issue + create(issuable_type, project: project, author: project.creator) + else + create(issuable_type, source_project: project, source_branch: source_branch, author: project.creator) + end + end + + before do + @issuable_ids = %w[fix improve/awesome].map do |source_branch| + create_issuable(issuable_type, project, source_branch: source_branch).id + end + end + + it "creates indexed meta-data object for issuable notes and votes count" do + get_action(action, project) + + meta_data = assigns(:issuable_meta_data) + + aggregate_failures do + expect(meta_data.keys).to match_array(@issuable_ids) + expect(meta_data.values).to all(be_kind_of(Issuable::IssuableMeta)) + end + end + + it "avoids N+1 queries" do + control = ActiveRecord::QueryRecorder.new { get_action(action, project) } + issuable = create_issuable(issuable_type, project, source_branch: 'csv') + + if issuable_type == :merge_request + issuable.update!(source_project: fork_project(project)) + end + + expect { get_action(action, project) }.not_to exceed_query_limit(control.count) + end + + describe "when given empty collection" do + let(:project2) { create(:project, :public) } + + it "doesn't execute any queries with false conditions" do + get_empty = + if action + proc { get action, author_id: project.creator.id } + else + proc { get :index, namespace_id: project2.namespace, project_id: project2 } + end + + expect(&get_empty).not_to make_queries_matching(/WHERE (?:1=0|0=1)/) + end + end +end diff --git a/spec/support/issue_tracker_service_shared_example.rb b/spec/support/shared_examples/issue_tracker_service_shared_example.rb index a6ab03cb808..a6ab03cb808 100644 --- a/spec/support/issue_tracker_service_shared_example.rb +++ b/spec/support/shared_examples/issue_tracker_service_shared_example.rb diff --git a/spec/support/ldap_shared_examples.rb b/spec/support/shared_examples/ldap_shared_examples.rb index 52c34e78965..52c34e78965 100644 --- a/spec/support/ldap_shared_examples.rb +++ b/spec/support/shared_examples/ldap_shared_examples.rb diff --git a/spec/support/legacy_path_redirect_shared_examples.rb b/spec/support/shared_examples/legacy_path_redirect_shared_examples.rb index f300bdd48b1..f300bdd48b1 100644 --- a/spec/support/legacy_path_redirect_shared_examples.rb +++ b/spec/support/shared_examples/legacy_path_redirect_shared_examples.rb diff --git a/spec/support/malicious_regexp_shared_examples.rb b/spec/support/shared_examples/malicious_regexp_shared_examples.rb index ac5d22298bb..ac5d22298bb 100644 --- a/spec/support/malicious_regexp_shared_examples.rb +++ b/spec/support/shared_examples/malicious_regexp_shared_examples.rb diff --git a/spec/support/mentionable_shared_examples.rb b/spec/support/shared_examples/mentionable_shared_examples.rb index 1685decbe94..1685decbe94 100644 --- a/spec/support/mentionable_shared_examples.rb +++ b/spec/support/shared_examples/mentionable_shared_examples.rb diff --git a/spec/support/milestone_tabs_examples.rb b/spec/support/shared_examples/milestone_tabs_examples.rb index 70b499198bf..70b499198bf 100644 --- a/spec/support/milestone_tabs_examples.rb +++ b/spec/support/shared_examples/milestone_tabs_examples.rb diff --git a/spec/support/notify_shared_examples.rb b/spec/support/shared_examples/notify_shared_examples.rb index e2c23607406..e2c23607406 100644 --- a/spec/support/notify_shared_examples.rb +++ b/spec/support/shared_examples/notify_shared_examples.rb diff --git a/spec/support/reference_parser_shared_examples.rb b/spec/support/shared_examples/reference_parser_shared_examples.rb index baf8bcc04b8..baf8bcc04b8 100644 --- a/spec/support/reference_parser_shared_examples.rb +++ b/spec/support/shared_examples/reference_parser_shared_examples.rb diff --git a/spec/support/slack_mattermost_notifications_shared_examples.rb b/spec/support/shared_examples/slack_mattermost_notifications_shared_examples.rb index 07bc3a51fd8..07bc3a51fd8 100644 --- a/spec/support/slack_mattermost_notifications_shared_examples.rb +++ b/spec/support/shared_examples/slack_mattermost_notifications_shared_examples.rb diff --git a/spec/support/snippet_visibility.rb b/spec/support/shared_examples/snippet_visibility.rb index 3a7c69b7877..3a7c69b7877 100644 --- a/spec/support/snippet_visibility.rb +++ b/spec/support/shared_examples/snippet_visibility.rb diff --git a/spec/support/snippets_shared_examples.rb b/spec/support/shared_examples/snippets_shared_examples.rb index 85f0facd5c3..85f0facd5c3 100644 --- a/spec/support/snippets_shared_examples.rb +++ b/spec/support/shared_examples/snippets_shared_examples.rb diff --git a/spec/support/taskable_shared_examples.rb b/spec/support/shared_examples/taskable_shared_examples.rb index 4056ff06b84..4056ff06b84 100644 --- a/spec/support/taskable_shared_examples.rb +++ b/spec/support/shared_examples/taskable_shared_examples.rb diff --git a/spec/support/time_tracking_shared_examples.rb b/spec/support/shared_examples/time_tracking_shared_examples.rb index 909d4e2ee8d..909d4e2ee8d 100644 --- a/spec/support/time_tracking_shared_examples.rb +++ b/spec/support/shared_examples/time_tracking_shared_examples.rb diff --git a/spec/support/unique_ip_check_shared_examples.rb b/spec/support/shared_examples/unique_ip_check_shared_examples.rb index e5c8ac6a004..e5c8ac6a004 100644 --- a/spec/support/unique_ip_check_shared_examples.rb +++ b/spec/support/shared_examples/unique_ip_check_shared_examples.rb diff --git a/spec/support/update_invalid_issuable.rb b/spec/support/shared_examples/update_invalid_issuable.rb index 1490287681b..1490287681b 100644 --- a/spec/support/update_invalid_issuable.rb +++ b/spec/support/shared_examples/update_invalid_issuable.rb diff --git a/spec/support/updating_mentions_shared_examples.rb b/spec/support/shared_examples/updating_mentions_shared_examples.rb index 5e3f19ba19e..5e3f19ba19e 100644 --- a/spec/support/updating_mentions_shared_examples.rb +++ b/spec/support/shared_examples/updating_mentions_shared_examples.rb diff --git a/spec/views/projects/settings/ci_cd/_form.html.haml_spec.rb b/spec/views/projects/settings/ci_cd/_autodevops_form.html.haml_spec.rb index be9a4d9c57c..d15391911c1 100644 --- a/spec/views/projects/settings/ci_cd/_form.html.haml_spec.rb +++ b/spec/views/projects/settings/ci_cd/_autodevops_form.html.haml_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe 'projects/settings/ci_cd/_form' do +describe 'projects/settings/ci_cd/_autodevops_form' do let(:project) { create(:project, :repository) } before do diff --git a/spec/workers/issue_due_scheduler_worker_spec.rb b/spec/workers/issue_due_scheduler_worker_spec.rb index 7b60835fd26..2710267d384 100644 --- a/spec/workers/issue_due_scheduler_worker_spec.rb +++ b/spec/workers/issue_due_scheduler_worker_spec.rb @@ -14,7 +14,9 @@ describe IssueDueSchedulerWorker do create(:issue, :closed, project: project_closed_issue, due_date: Date.tomorrow) create(:issue, :opened, project: project_issue_due_another_day, due_date: Date.today) - expect(MailScheduler::IssueDueWorker).to receive(:bulk_perform_async).with([[project1.id], [project2.id]]) + expect(MailScheduler::IssueDueWorker).to receive(:bulk_perform_async) do |args| + expect(args).to match_array([[project1.id], [project2.id]]) + end described_class.new.perform end diff --git a/yarn.lock b/yarn.lock index f05278cfde5..7aca6b0d427 100644 --- a/yarn.lock +++ b/yarn.lock @@ -670,6 +670,10 @@ babel-plugin-istanbul@^4.1.5: istanbul-lib-instrument "^1.7.5" test-exclude "^4.1.1" +babel-plugin-rewire@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/babel-plugin-rewire/-/babel-plugin-rewire-1.1.0.tgz#a6b966d9d8c06c03d95dcda2eec4e2521519549b" + babel-plugin-syntax-async-functions@^6.8.0: version "6.13.0" resolved "https://registry.yarnpkg.com/babel-plugin-syntax-async-functions/-/babel-plugin-syntax-async-functions-6.13.0.tgz#cad9cad1191b5ad634bf30ae0872391e0647be95" @@ -8474,6 +8478,10 @@ vue-template-es2015-compiler@^1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.6.0.tgz#dc42697133302ce3017524356a6c61b7b69b4a18" +vue-virtual-scroll-list@^1.2.5: + version "1.2.5" + resolved "https://registry.yarnpkg.com/vue-virtual-scroll-list/-/vue-virtual-scroll-list-1.2.5.tgz#bcbd010f7cdb035eba8958ebf807c6214d9a167a" + vue@^2.5.13: version "2.5.13" resolved "https://registry.yarnpkg.com/vue/-/vue-2.5.13.tgz#95bd31e20efcf7a7f39239c9aa6787ce8cf578e1" |