diff options
245 files changed, 2802 insertions, 1409 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 797a20ef16e..23d71675ae4 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -131,7 +131,7 @@ stages: .single-script-job: &single-script-job image: ruby:2.4-alpine before_script: [] - stage: build + stage: test cache: {} dependencies: [] variables: &single-script-job-variables diff --git a/CHANGELOG.md b/CHANGELOG.md index 256dd913435..c1d5a638cd0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,21 @@ documentation](doc/development/changelog.md) for instructions on adding your own entry. +## 11.2.3 (2018-08-28) + +### Fixed (1 change) + +- Fixed cache invalidation issue with diff lines from 11.2.2. + +## 11.2.2 (2018-08-27) + +### Security (3 changes) + +- Fixed persistent XSS rendering/escaping of diff location lines. +- Adding CSRF protection to Hooks resend action. +- Block link-local addresses in URLBlocker. + + ## 11.2.1 (2018-08-22) ### Fixed (2 changes) @@ -254,6 +269,25 @@ entry. - Moves help_popover component to a common location. +## 11.1.6 (2018-08-28) + +### Fixed (1 change) + +- Fixed cache invalidation issue with diff lines from 11.2.2. + +## 11.1.5 (2018-08-27) + +### Security (3 changes) + +- Fixed persistent XSS rendering/escaping of diff location lines. +- Adding CSRF protection to Hooks resend action. +- Block link-local addresses in URLBlocker. + +### Fixed (1 change, 1 of them is from the community) + +- Sanitize git URL in import errors. (Jamie Schembri) + + ## 11.1.4 (2018-07-30) ### Fixed (4 changes, 1 of them is from the community) @@ -536,6 +570,19 @@ entry. - Use monospaced font for MR diff commit link ref on GFM. +## 11.0.6 (2018-08-27) + +### Security (3 changes) + +- Fixed persistent XSS rendering/escaping of diff location lines. +- Adding CSRF protection to Hooks resend action. +- Block link-local addresses in URLBlocker. + +### Fixed (1 change, 1 of them is from the community) + +- Sanitize git URL in import errors. (Jamie Schembri) + + ## 11.0.5 (2018-07-26) ### Security (4 changes) @@ -195,6 +195,9 @@ gem 're2', '~> 1.1.1' gem 'version_sorter', '~> 2.1.0' +# Export Ruby Regex to Javascript +gem 'js_regex', '~> 2.2.1' + # User agent parsing gem 'device_detector' @@ -365,7 +368,7 @@ group :development, :test do gem 'benchmark-ips', '~> 2.3.0', require: false - gem 'license_finder', '~> 3.1', require: false + gem 'license_finder', '~> 5.4', require: false gem 'knapsack', '~> 1.16' gem 'activerecord_sane_schema_dumper', gem_versions['activerecord_sane_schema_dumper'] diff --git a/Gemfile.lock b/Gemfile.lock index 77effb63d2e..11921a64900 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -86,7 +86,6 @@ GEM bindata (2.4.3) binding_of_caller (0.7.2) debug_inspector (>= 0.0.1) - blankslate (2.1.2.4) bootsnap (1.3.1) msgpack (~> 1.0) bootstrap_form (2.7.0) @@ -428,6 +427,8 @@ GEM multipart-post oauth (~> 0.5, >= 0.5.0) jquery-atwho-rails (1.3.2) + js_regex (2.2.1) + regexp_parser (>= 0.4.11, <= 0.5.0) json (1.8.6) json-jwt (1.9.4) activesupport @@ -463,13 +464,12 @@ GEM actionmailer (>= 3.2) letter_opener (~> 1.0) railties (>= 3.2) - license_finder (3.1.1) + license_finder (5.4.0) bundler - httparty rubyzip thor - toml (= 0.1.2) - with_env (> 1.0) + toml (= 0.2.0) + with_env (= 1.1.0) xml-simple licensee (8.9.2) rugged (~> 0.24) @@ -587,8 +587,7 @@ GEM parallel (1.12.1) parser (2.5.1.0) ast (~> 2.4.0) - parslet (1.5.0) - blankslate (~> 2.0) + parslet (1.8.2) path_expander (1.0.2) peek (1.0.1) concurrent-ruby (>= 0.9.0) @@ -726,6 +725,7 @@ GEM redis-store (>= 1.2, < 2) redis-store (1.4.1) redis (>= 2.2, < 5) + regexp_parser (0.5.0) representable (3.0.4) declarative (< 0.1.0) declarative-option (< 0.2.0) @@ -907,8 +907,8 @@ GEM tilt (2.0.8) timecop (0.8.1) timfel-krb5-auth (0.8.3) - toml (0.1.2) - parslet (~> 1.5.0) + toml (0.2.0) + parslet (~> 1.8.0) toml-rb (1.0.0) citrus (~> 3.0, > 3.0) trollop (2.1.3) @@ -1074,13 +1074,14 @@ DEPENDENCIES influxdb (~> 0.2) jira-ruby (~> 1.4) jquery-atwho-rails (~> 1.3.2) + js_regex (~> 2.2.1) json-schema (~> 2.8.0) jwt (~> 1.5.6) kaminari (~> 1.0) knapsack (~> 1.16) kubeclient (~> 3.1.0) letter_opener_web (~> 1.3.0) - license_finder (~> 3.1) + license_finder (~> 5.4) licensee (~> 8.9) lograge (~> 0.5) loofah (~> 2.2) @@ -1201,4 +1202,4 @@ DEPENDENCIES wikicloth (= 0.8.1) BUNDLED WITH - 1.16.3 + 1.16.4 diff --git a/Gemfile.rails5.lock b/Gemfile.rails5.lock index 63b450d3f62..02f9e112300 100644 --- a/Gemfile.rails5.lock +++ b/Gemfile.rails5.lock @@ -89,7 +89,6 @@ GEM bindata (2.4.3) binding_of_caller (0.7.2) debug_inspector (>= 0.0.1) - blankslate (2.1.2.4) bootsnap (1.3.1) msgpack (~> 1.0) bootstrap_form (2.7.0) @@ -431,6 +430,8 @@ GEM multipart-post oauth (~> 0.5, >= 0.5.0) jquery-atwho-rails (1.3.2) + js_regex (2.2.1) + regexp_parser (>= 0.4.11, <= 0.5.0) json (1.8.6) json-jwt (1.9.4) activesupport @@ -466,13 +467,12 @@ GEM actionmailer (>= 3.2) letter_opener (~> 1.0) railties (>= 3.2) - license_finder (3.1.1) + license_finder (5.4.0) bundler - httparty rubyzip thor - toml (= 0.1.2) - with_env (> 1.0) + toml (= 0.2.0) + with_env (= 1.1.0) xml-simple licensee (8.9.2) rugged (~> 0.24) @@ -591,8 +591,7 @@ GEM parallel (1.12.1) parser (2.5.1.0) ast (~> 2.4.0) - parslet (1.5.0) - blankslate (~> 2.0) + parslet (1.8.2) path_expander (1.0.2) peek (1.0.1) concurrent-ruby (>= 0.9.0) @@ -735,6 +734,7 @@ GEM redis-store (>= 1.2, < 2) redis-store (1.4.1) redis (>= 2.2, < 5) + regexp_parser (0.5.0) representable (3.0.4) declarative (< 0.1.0) declarative-option (< 0.2.0) @@ -914,8 +914,8 @@ GEM tilt (2.0.8) timecop (0.8.1) timfel-krb5-auth (0.8.3) - toml (0.1.2) - parslet (~> 1.5.0) + toml (0.2.0) + parslet (~> 1.8.0) toml-rb (1.0.0) citrus (~> 3.0, > 3.0) trollop (2.1.3) @@ -1084,13 +1084,14 @@ DEPENDENCIES influxdb (~> 0.2) jira-ruby (~> 1.4) jquery-atwho-rails (~> 1.3.2) + js_regex (~> 2.2.1) json-schema (~> 2.8.0) jwt (~> 1.5.6) kaminari (~> 1.0) knapsack (~> 1.16) kubeclient (~> 3.1.0) letter_opener_web (~> 1.3.0) - license_finder (~> 3.1) + license_finder (~> 5.4) licensee (~> 8.9) lograge (~> 0.5) loofah (~> 2.2) @@ -1211,4 +1212,4 @@ DEPENDENCIES wikicloth (= 0.8.1) BUNDLED WITH - 1.16.3 + 1.16.4 diff --git a/app/assets/javascripts/flash.js b/app/assets/javascripts/flash.js index a0af2875ab5..a29de9ae899 100644 --- a/app/assets/javascripts/flash.js +++ b/app/assets/javascripts/flash.js @@ -10,6 +10,7 @@ const hideFlash = (flashEl, fadeTransition = true) => { flashEl.addEventListener('transitionend', () => { flashEl.remove(); + window.dispatchEvent(new Event('resize')); if (document.body.classList.contains('flash-shown')) document.body.classList.remove('flash-shown'); }, { once: true, diff --git a/app/assets/javascripts/ide/components/ide.vue b/app/assets/javascripts/ide/components/ide.vue index 2c8305aa0cc..6a5ab35a16a 100644 --- a/app/assets/javascripts/ide/components/ide.vue +++ b/app/assets/javascripts/ide/components/ide.vue @@ -78,13 +78,13 @@ export default { </script> <template> - <article class="ide"> + <article class="ide position-relative d-flex flex-column align-items-stretch"> <error-message v-if="errorMessage" :message="errorMessage" /> <div - class="ide-view" + class="ide-view flex-grow d-flex" > <find-file v-show="fileFindVisible" diff --git a/app/assets/javascripts/ide/components/new_dropdown/upload.vue b/app/assets/javascripts/ide/components/new_dropdown/upload.vue index 5b1743bb30e..e2be805ed22 100644 --- a/app/assets/javascripts/ide/components/new_dropdown/upload.vue +++ b/app/assets/javascripts/ide/components/new_dropdown/upload.vue @@ -24,12 +24,6 @@ export default { default: null, }, }, - mounted() { - this.$refs.fileUpload.addEventListener('change', this.openFile); - }, - beforeDestroy() { - this.$refs.fileUpload.removeEventListener('change', this.openFile); - }, methods: { createFile(target, file, isText) { const { name } = file; @@ -85,6 +79,8 @@ export default { ref="fileUpload" type="file" class="hidden" + multiple + @change="openFile" /> </div> </template> diff --git a/app/assets/javascripts/ide/components/repo_file.vue b/app/assets/javascripts/ide/components/repo_file.vue index dbdf0be2809..110eda83bb4 100644 --- a/app/assets/javascripts/ide/components/repo_file.vue +++ b/app/assets/javascripts/ide/components/repo_file.vue @@ -95,16 +95,18 @@ export default { return this.file.changed || this.file.tempFile || this.file.staged; }, }, + watch: { + 'file.active': function fileActiveWatch(active) { + if (this.file.type === 'blob' && active) { + this.scrollIntoView(); + } + }, + }, mounted() { if (this.hasPathAtCurrentRoute()) { this.scrollIntoView(true); } }, - updated() { - if (this.file.type === 'blob' && this.file.active) { - this.scrollIntoView(); - } - }, methods: { ...mapActions(['toggleTreeOpen']), clickFile() { diff --git a/app/assets/javascripts/pages/admin/application_settings/account_and_limits.js b/app/assets/javascripts/pages/admin/application_settings/account_and_limits.js new file mode 100644 index 00000000000..7281f907ec7 --- /dev/null +++ b/app/assets/javascripts/pages/admin/application_settings/account_and_limits.js @@ -0,0 +1,25 @@ +import { __ } from '~/locale'; + +export const PLACEHOLDER_USER_EXTERNAL_DEFAULT_TRUE = __('Regex pattern'); +export const PLACEHOLDER_USER_EXTERNAL_DEFAULT_FALSE = __('To define internal users, first enable new users set to external'); + +function setUserInternalRegexPlaceholder(checkbox) { + const userInternalRegex = document.getElementById('application_setting_user_default_internal_regex'); + if (checkbox && userInternalRegex) { + if (checkbox.checked) { + userInternalRegex.readOnly = false; + userInternalRegex.placeholder = PLACEHOLDER_USER_EXTERNAL_DEFAULT_TRUE; + } else { + userInternalRegex.readOnly = true; + userInternalRegex.placeholder = PLACEHOLDER_USER_EXTERNAL_DEFAULT_FALSE; + } + } +} + +export default function initUserInternalRegexPlaceholder() { + const checkbox = document.getElementById('application_setting_user_default_external'); + setUserInternalRegexPlaceholder(checkbox); + checkbox.addEventListener('change', () => { + setUserInternalRegexPlaceholder(checkbox); + }); +} diff --git a/app/assets/javascripts/pages/admin/index.js b/app/assets/javascripts/pages/admin/index.js index e50b61f09e2..3aa793e47b9 100644 --- a/app/assets/javascripts/pages/admin/index.js +++ b/app/assets/javascripts/pages/admin/index.js @@ -1,3 +1,7 @@ import initAdmin from './admin'; +import initUserInternalRegexPlaceholder from './application_settings/account_and_limits'; -document.addEventListener('DOMContentLoaded', initAdmin); +document.addEventListener('DOMContentLoaded', () => { + initAdmin(); + initUserInternalRegexPlaceholder(); +}); diff --git a/app/assets/javascripts/pages/admin/users/new/index.js b/app/assets/javascripts/pages/admin/users/new/index.js new file mode 100644 index 00000000000..58bfa8d64e7 --- /dev/null +++ b/app/assets/javascripts/pages/admin/users/new/index.js @@ -0,0 +1,49 @@ +import $ from 'jquery'; + +export default class UserInternalRegexHandler { + constructor() { + this.regexPattern = $('[data-user-internal-regex-pattern]').data('user-internal-regex-pattern'); + if (this.regexPattern && this.regexPattern !== '') { + this.regexOptions = $('[data-user-internal-regex-options]').data('user-internal-regex-options'); + this.external = $('#user_external'); + this.warningMessage = $('#warning_external_automatically_set'); + this.addListenerToEmailField(); + this.addListenerToUserExternalCheckbox(); + } + } + + addListenerToEmailField() { + $('#user_email').on('input', (event) => { + this.setExternalCheckbox(event.currentTarget.value); + }); + } + + addListenerToUserExternalCheckbox() { + this.external.on('click', () => { + this.warningMessage.addClass('hidden'); + }); + } + + isEmailInternal(email) { + const regex = new RegExp(this.regexPattern, this.regexOptions); + return regex.test(email); + } + + setExternalCheckbox(email) { + const isChecked = this.external.prop('checked'); + if (this.isEmailInternal(email)) { + if (isChecked) { + this.external.prop('checked', false); + this.warningMessage.removeClass('hidden'); + } + } else if (!isChecked) { + this.external.prop('checked', true); + this.warningMessage.addClass('hidden'); + } + } +} + +document.addEventListener('DOMContentLoaded', () => { + // eslint-disable-next-line + new UserInternalRegexHandler(); +}); diff --git a/app/assets/javascripts/performance_bar/index.js b/app/assets/javascripts/performance_bar/index.js index 41880d27516..6e5ef0ac0b2 100644 --- a/app/assets/javascripts/performance_bar/index.js +++ b/app/assets/javascripts/performance_bar/index.js @@ -1,5 +1,4 @@ import Vue from 'vue'; -import Flash from '../flash'; import PerformanceBarService from './services/performance_bar_service'; import PerformanceBarStore from './stores/performance_bar_store'; @@ -46,7 +45,8 @@ export default ({ container }) => this.store.addRequestDetails(requestId, res.data.data); }) .catch(() => - Flash(`Error getting performance bar results for ${requestId}`), + // eslint-disable-next-line no-console + console.warn(`Error getting performance bar results for ${requestId}`), ); }, }, diff --git a/app/assets/javascripts/performance_bar/services/performance_bar_service.js b/app/assets/javascripts/performance_bar/services/performance_bar_service.js index bc71911ae35..60d9ba62570 100644 --- a/app/assets/javascripts/performance_bar/services/performance_bar_service.js +++ b/app/assets/javascripts/performance_bar/services/performance_bar_service.js @@ -11,13 +11,10 @@ export default class PerformanceBarService { static registerInterceptor(peekUrl, callback) { const interceptor = response => { - const requestId = response.headers['x-request-id']; - // Get the request URL from response.config for Axios, and response for - // Vue Resource. - const requestUrl = (response.config || response).url; - const cachedResponse = response.headers['x-gitlab-from-cache'] === 'true'; + const [fireCallback, requestId, requestUrl] = + PerformanceBarService.callbackParams(response, peekUrl); - if (requestUrl !== peekUrl && requestId && !cachedResponse) { + if (fireCallback) { callback(requestId, requestUrl); } @@ -38,4 +35,16 @@ export default class PerformanceBarService { vueResourceInterceptor, ); } + + static callbackParams(response, peekUrl) { + const requestId = response.headers && response.headers['x-request-id']; + // Get the request URL from response.config for Axios, and response for + // Vue Resource. + const requestUrl = (response.config || response).url; + const apiRequest = requestUrl && requestUrl.match(/^\/api\//); + const cachedResponse = response.headers && response.headers['x-gitlab-from-cache'] === 'true'; + const fireCallback = requestUrl !== peekUrl && requestId && !apiRequest && !cachedResponse; + + return [fireCallback, requestId, requestUrl]; + } } 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 8487c8036ee..2ad66f4fe86 100644 --- a/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue @@ -1,6 +1,5 @@ <script> import $ from 'jquery'; -import _ from 'underscore'; import JobNameComponent from './job_name_component.vue'; import JobComponent from './job_component.vue'; import tooltip from '../../../vue_shared/directives/tooltip'; @@ -47,7 +46,7 @@ export default { computed: { tooltipText() { - return _.escape(`${this.job.name} - ${this.job.status.label}`); + return `${this.job.name} - ${this.job.status.label}`; }, }, diff --git a/app/assets/javascripts/pipelines/components/graph/job_component.vue b/app/assets/javascripts/pipelines/components/graph/job_component.vue index 66f95147193..9ac16b7e541 100644 --- a/app/assets/javascripts/pipelines/components/graph/job_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/job_component.vue @@ -1,5 +1,4 @@ <script> -import _ from 'underscore'; import ActionComponent from './action_component.vue'; import JobNameComponent from './job_name_component.vue'; import tooltip from '../../../vue_shared/directives/tooltip'; @@ -62,7 +61,7 @@ export default { const textBuilder = []; if (this.job.name) { - textBuilder.push(_.escape(this.job.name)); + textBuilder.push(this.job.name); } if (this.job.name && this.status.tooltip) { @@ -106,7 +105,6 @@ export default { :class="cssClassJobName" :data-boundary="tooltipBoundary" data-container="body" - data-html="true" class="js-pipeline-graph-job-link" > @@ -122,7 +120,6 @@ export default { :title="tooltipText" :class="cssClassJobName" class="js-job-component-tooltip non-details-job-component" - data-html="true" data-container="body" > diff --git a/app/assets/stylesheets/framework/emojis.scss b/app/assets/stylesheets/framework/emojis.scss index a8ec1e1145a..6c50ea719d3 100644 --- a/app/assets/stylesheets/framework/emojis.scss +++ b/app/assets/stylesheets/framework/emojis.scss @@ -3,6 +3,6 @@ gl-emoji { display: inline-flex; vertical-align: middle; font-family: "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; - font-size: 1.5em; - line-height: 0.9; + font-size: 1.4em; + line-height: 1em; } diff --git a/app/assets/stylesheets/framework/layout.scss b/app/assets/stylesheets/framework/layout.scss index 52b5f059f20..d4bae4cb137 100644 --- a/app/assets/stylesheets/framework/layout.scss +++ b/app/assets/stylesheets/framework/layout.scss @@ -111,3 +111,42 @@ body { .with-performance-bar .layout-page { margin-top: $header-height + $performance-bar-height; } + +.fullscreen-layout { + padding-top: 0; + height: 100vh; + width: 100%; + display: flex; + flex-direction: column; + align-items: stretch; + overflow: hidden; + + > #js-peek, + > .navbar-gitlab { + position: static; + top: auto; + } + + .flash-container { + margin-top: 0; + margin-bottom: 0; + } + + .alert-wrapper .flash-container .flash-alert:last-child, + .alert-wrapper .flash-container .flash-notice:last-child { + margin-bottom: 0; + } + + .content-wrapper { + margin-top: 0; + padding-bottom: 0; + flex: 1; + min-height: 0; + } + + &.flash-shown { + .content-wrapper { + margin-top: 0; + } + } +} diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss index 5c6110737a4..9929f1bdebf 100644 --- a/app/assets/stylesheets/framework/typography.scss +++ b/app/assets/stylesheets/framework/typography.scss @@ -327,7 +327,7 @@ h6 { pre { font-family: $monospace-font; display: block; - padding: $gl-padding-8; + padding: $gl-padding-8 $input-horizontal-padding; margin: 0 0 $gl-padding-8; font-size: 13px; word-break: break-all; diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 2781d910b8d..d76f5cbd9ff 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -236,6 +236,7 @@ $gl-vert-padding: 6px; $gl-padding-top: 10px; $gl-sidebar-padding: 22px; $gl-bar-padding: 3px; +$input-horizontal-padding: 12px; /* * Misc diff --git a/app/assets/stylesheets/page_bundles/ide.scss b/app/assets/stylesheets/page_bundles/ide.scss index eac1345742d..5ff4e487d04 100644 --- a/app/assets/stylesheets/page_bundles/ide.scss +++ b/app/assets/stylesheets/page_bundles/ide.scss @@ -28,11 +28,10 @@ $ide-tree-text-start: $ide-activity-bar-width + $ide-tree-padding; .ide-view { position: relative; - display: flex; - height: calc(100vh - #{$header-height}); margin-top: 0; padding-bottom: $ide-statusbar-height; color: $gl-text-color; + min-height: 0; // firefox fix &.is-collapsed { .ide-file-list { @@ -50,7 +49,7 @@ $ide-tree-text-start: $ide-activity-bar-width + $ide-tree-padding; display: flex; flex-direction: column; flex: 1; - min-height: 0; + min-height: 0; // firefox fix .file { height: 32px; @@ -357,7 +356,7 @@ $ide-tree-text-start: $ide-activity-bar-width + $ide-tree-padding; .multi-file-editor-holder { height: 100%; - min-height: 0; + min-height: 0; // firefox fix &.is-readonly, .editor.original { @@ -546,7 +545,7 @@ $ide-tree-text-start: $ide-activity-bar-width + $ide-tree-padding; border-left: 1px solid $white-dark; border-top: 1px solid $white-dark; border-top-left-radius: $border-radius-small; - min-height: 0; + min-height: 0; // firefox fix } } @@ -758,7 +757,7 @@ $ide-tree-text-start: $ide-activity-bar-width + $ide-tree-padding; .ide-loading { display: flex; - height: 100vh; + height: 100%; align-items: center; justify-content: center; } @@ -772,60 +771,7 @@ $ide-tree-text-start: $ide-activity-bar-width + $ide-tree-padding; .ide { overflow: hidden; - - &.nav-only { - padding-top: $header-height; - - .with-performance-bar & { - padding-top: $header-height + $performance-bar-height; - } - - .flash-container { - margin-top: 0; - margin-bottom: 0; - } - - .alert-wrapper .flash-container .flash-alert:last-child, - .alert-wrapper .flash-container .flash-notice:last-child { - margin-bottom: 0; - } - - .content-wrapper { - margin-top: 0; - padding-bottom: 0; - } - - &.flash-shown { - .content-wrapper { - margin-top: 0; - } - - .ide-view { - height: calc(100vh - #{$header-height + $flash-height}); - } - } - } -} - -.with-performance-bar .ide.nav-only { - .flash-container { - margin-top: 0; - } - - .content-wrapper { - margin-top: 0; - padding-bottom: 0; - } - - .ide-view { - height: calc(100vh - #{$header-height + $performance-bar-height}); - } - - &.flash-shown { - .ide-view { - height: calc(100vh - #{$header-height + $performance-bar-height + $flash-height}); - } - } + flex: 1; } .drag-handle { @@ -1199,7 +1145,7 @@ $ide-tree-text-start: $ide-activity-bar-width + $ide-tree-padding; } .avatar-container { - flex: initial; + flex: 0 0 auto; margin-right: 0; } @@ -1209,7 +1155,7 @@ $ide-tree-text-start: $ide-activity-bar-width + $ide-tree-padding; } .ide-context-body { - min-height: 0; + min-height: 0; // firefox fix } .ide-sidebar-project-title { diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index a999a70693e..7d7143631f2 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -25,10 +25,6 @@ color: $gl-text-color; border-radius: 0 0 3px 3px; - .code { - padding: 0; - } - .unfold { cursor: pointer; } diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index fce04c58c24..dbe9f0c03fb 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -141,6 +141,9 @@ ul.notes { } .note-body { + overflow-x: auto; + overflow-y: hidden; + .note-text { @include md-typography; // Reset ul style types since we're nested inside a ul already diff --git a/app/controllers/concerns/sends_blob.rb b/app/controllers/concerns/sends_blob.rb new file mode 100644 index 00000000000..971390d9118 --- /dev/null +++ b/app/controllers/concerns/sends_blob.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true + +module SendsBlob + extend ActiveSupport::Concern + + included do + include BlobHelper + include SendFileUpload + end + + def send_blob(blob, params = {}) + if blob + headers['X-Content-Type-Options'] = 'nosniff' + + return if cached_blob?(blob) + + if blob.stored_externally? + send_lfs_object(blob) + else + send_git_blob(repository, blob, params) + end + else + render_404 + end + end + + private + + def cached_blob?(blob) + stale = stale?(etag: blob.id) # The #stale? method sets cache headers. + + # Because we are opinionated we set the cache headers ourselves. + response.cache_control[:public] = project.public? + + response.cache_control[:max_age] = + if @ref && @commit && @ref == @commit.id # rubocop:disable Gitlab/ModuleWithInstanceVariables + # This is a link to a commit by its commit SHA. That means that the blob + # is immutable. The only reason to invalidate the cache is if the commit + # was deleted or if the user lost access to the repository. + Blob::CACHE_TIME_IMMUTABLE + else + # A branch or tag points at this blob. That means that the expected blob + # value may change over time. + Blob::CACHE_TIME + end + + response.etag = blob.id + !stale + end + + def send_lfs_object(blob) + lfs_object = find_lfs_object(blob) + + if lfs_object && lfs_object.project_allowed_access?(project) + send_upload(lfs_object.file, attachment: blob.name) + else + render_404 + end + end + + def find_lfs_object(blob) + lfs_object = LfsObject.find_by_oid(blob.lfs_oid) + if lfs_object && lfs_object.file.exists? + lfs_object + else + nil + end + end +end diff --git a/app/controllers/ide_controller.rb b/app/controllers/ide_controller.rb index 1ff25a45398..96bb2237d90 100644 --- a/app/controllers/ide_controller.rb +++ b/app/controllers/ide_controller.rb @@ -1,5 +1,5 @@ class IdeController < ApplicationController - layout 'nav_only' + layout 'fullscreen' def index end diff --git a/app/controllers/projects/avatars_controller.rb b/app/controllers/projects/avatars_controller.rb index 53fdc5843b5..878c82cd183 100644 --- a/app/controllers/projects/avatars_controller.rb +++ b/app/controllers/projects/avatars_controller.rb @@ -1,24 +1,16 @@ class Projects::AvatarsController < Projects::ApplicationController - include BlobHelper + include SendsBlob before_action :authorize_admin_project!, only: [:destroy] def show @blob = @repository.blob_at_branch(@repository.root_ref, @project.avatar_in_git) - if @blob - headers['X-Content-Type-Options'] = 'nosniff' - return if cached_blob? - - send_git_blob @repository, @blob - else - render_404 - end + send_blob(@blob) end def destroy @project.remove_avatar! - @project.save redirect_to edit_project_path(@project, anchor: 'js-general-project-settings'), status: :found diff --git a/app/controllers/projects/raw_controller.rb b/app/controllers/projects/raw_controller.rb index 1cba0011304..91cf35bc70b 100644 --- a/app/controllers/projects/raw_controller.rb +++ b/app/controllers/projects/raw_controller.rb @@ -1,8 +1,7 @@ # Controller for viewing a file's raw class Projects::RawController < Projects::ApplicationController include ExtractsPath - include BlobHelper - include SendFileUpload + include SendsBlob before_action :require_non_empty_project before_action :assign_ref_vars @@ -10,39 +9,7 @@ class Projects::RawController < Projects::ApplicationController def show @blob = @repository.blob_at(@commit.id, @path) - if @blob - headers['X-Content-Type-Options'] = 'nosniff' - return if cached_blob? - - if @blob.stored_externally? - send_lfs_object - else - send_git_blob @repository, @blob, inline: (params[:inline] != 'false') - end - else - render_404 - end - end - - private - - def send_lfs_object - lfs_object = find_lfs_object - - if lfs_object && lfs_object.project_allowed_access?(@project) - send_upload(lfs_object.file, attachment: @blob.name) - else - render_404 - end - end - - def find_lfs_object - lfs_object = LfsObject.find_by_oid(@blob.lfs_oid) - if lfs_object && lfs_object.file.exists? - lfs_object - else - nil - end + send_blob(@blob, inline: (params[:inline] != 'false')) end end diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb index 1e05f07e676..684c84c3006 100644 --- a/app/helpers/application_settings_helper.rb +++ b/app/helpers/application_settings_helper.rb @@ -255,6 +255,7 @@ module ApplicationSettingsHelper :instance_statistics_visibility_private, :user_default_external, :user_show_add_ssh_key_message, + :user_default_internal_regex, :user_oauth_applications, :version_check_enabled, :web_ide_clientside_preview_enabled diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb index b61cbd5418a..00ebafd177b 100644 --- a/app/helpers/blob_helper.rb +++ b/app/helpers/blob_helper.rb @@ -157,28 +157,6 @@ module BlobHelper end end - def cached_blob? - stale = stale?(etag: @blob.id) # The #stale? method sets cache headers. - - # Because we are opionated we set the cache headers ourselves. - response.cache_control[:public] = @project.public? - - response.cache_control[:max_age] = - if @ref && @commit && @ref == @commit.id - # This is a link to a commit by its commit SHA. That means that the blob - # is immutable. The only reason to invalidate the cache is if the commit - # was deleted or if the user lost access to the repository. - Blob::CACHE_TIME_IMMUTABLE - else - # A branch or tag points at this blob. That means that the expected blob - # value may change over time. - Blob::CACHE_TIME - end - - response.etag = @blob.id - !stale - end - def licenses_for_select return @licenses_for_select if defined?(@licenses_for_select) diff --git a/app/helpers/submodule_helper.rb b/app/helpers/submodule_helper.rb index ebfde993456..ec2cf2b16c0 100644 --- a/app/helpers/submodule_helper.rb +++ b/app/helpers/submodule_helper.rb @@ -64,8 +64,7 @@ module SubmoduleHelper end def relative_self_url?(url) - # (./)?(../repo.git) || (./)?(../../project/repo.git) ) - url =~ %r{\A((\./)?(\.\./))(?!(\.\.)|(.*/)).*(\.git)?\z} || url =~ %r{\A((\./)?(\.\./){2})(?!(\.\.))([^/]*)/(?!(\.\.)|(.*/)).*(\.git)?\z} + url.start_with?('../', './') end def standard_links(host, namespace, project, commit) @@ -73,25 +72,29 @@ module SubmoduleHelper [base, [base, '/tree/', commit].join('')] end - def relative_self_links(url, commit, project) - url.rstrip! - # Map relative links to a namespace and project - # For example: - # ../bar.git -> same namespace, repo bar - # ../foo/bar.git -> namespace foo, repo bar - # ../../foo/bar/baz.git -> namespace bar, repo baz - components = url.split('/') - base = components.pop.gsub(/.git$/, '') - namespace = components.pop.gsub(/^\.\.$/, '') - - if namespace.empty? - namespace = project.namespace.full_path + def relative_self_links(relative_path, commit, project) + relative_path.rstrip! + absolute_project_path = "/" + project.full_path + + # Resolve `relative_path` to target path + # Assuming `absolute_project_path` is `/g1/p1`: + # ../p2.git -> /g1/p2 + # ../g2/p3.git -> /g1/g2/p3 + # ../../g3/g4/p4.git -> /g3/g4/p4 + submodule_project_path = File.absolute_path(relative_path, absolute_project_path) + target_namespace_path = File.dirname(submodule_project_path) + + if target_namespace_path == '/' || target_namespace_path.start_with?(absolute_project_path) + return [nil, nil] end + target_namespace_path.sub!(%r{^/}, '') + submodule_base = File.basename(submodule_project_path, '.git') + begin [ - namespace_project_path(namespace, base), - namespace_project_tree_path(namespace, base, commit) + namespace_project_path(target_namespace_path, submodule_base), + namespace_project_tree_path(target_namespace_path, submodule_base, commit) ] rescue ActionController::UrlGenerationError [nil, nil] diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb index ceea4384f91..2c0c4254a0c 100644 --- a/app/helpers/users_helper.rb +++ b/app/helpers/users_helper.rb @@ -23,6 +23,17 @@ module UsersHelper profile_tabs.include?(tab) end + def user_internal_regex_data + settings = Gitlab::CurrentSettings.current_application_settings + + pattern, options = if settings.user_default_internal_regex_enabled? + regex = settings.user_default_internal_regex_instance + JsRegex.new(regex).to_h.slice(:source, :options).values + end + + { user_internal_regex_pattern: pattern, user_internal_regex_options: options } + end + def current_user_menu_items @current_user_menu_items ||= get_current_user_menu_items end diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index c77faa4b71d..03bd7fa016e 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -192,6 +192,8 @@ class ApplicationSetting < ActiveRecord::Base numericality: { less_than_or_equal_to: :gitaly_timeout_default }, if: :gitaly_timeout_default + validates :user_default_internal_regex, js_regex: true, allow_nil: true + SUPPORTED_KEY_TYPES.each do |type| validates :"#{type}_key_restriction", presence: true, key_restriction: { type: type } end @@ -299,6 +301,7 @@ class ApplicationSetting < ActiveRecord::Base usage_ping_enabled: Settings.gitlab['usage_ping_enabled'], instance_statistics_visibility_private: false, user_default_external: false, + user_default_internal_regex: nil, user_show_add_ssh_key_message: true } end @@ -435,6 +438,14 @@ class ApplicationSetting < ActiveRecord::Base password_authentication_enabled_for_web? || password_authentication_enabled_for_git? end + def user_default_internal_regex_enabled? + user_default_external? && user_default_internal_regex.present? + end + + def user_default_internal_regex_instance + Regexp.new(user_default_internal_regex, Regexp::IGNORECASE) + end + delegate :terms, to: :latest_terms, allow_nil: true def latest_terms @latest_terms ||= Term.latest diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index e4aed76f611..526bf7af99b 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -649,8 +649,7 @@ module Ci def keep_around_commits return unless project - project.repository.keep_around(self.sha) - project.repository.keep_around(self.before_sha) + project.repository.keep_around(self.sha, self.before_sha) end def valid_source diff --git a/app/models/concerns/atomic_internal_id.rb b/app/models/concerns/atomic_internal_id.rb index 7f6d48d972c..4e15b60ccd1 100644 --- a/app/models/concerns/atomic_internal_id.rb +++ b/app/models/concerns/atomic_internal_id.rb @@ -26,7 +26,7 @@ module AtomicInternalId extend ActiveSupport::Concern - module ClassMethods + class_methods do def has_internal_id(column, scope:, init:, presence: true) # rubocop:disable Naming/PredicateName # We require init here to retain the ability to recalculate in the absence of a # InternaLId record (we may delete records in `internal_ids` for example). diff --git a/app/models/concerns/awardable.rb b/app/models/concerns/awardable.rb index 4200253053a..6f29c92d176 100644 --- a/app/models/concerns/awardable.rb +++ b/app/models/concerns/awardable.rb @@ -12,7 +12,7 @@ module Awardable end end - module ClassMethods + class_methods do def awarded(user, name) sql = <<~EOL EXISTS ( diff --git a/app/models/concerns/case_sensitivity.rb b/app/models/concerns/case_sensitivity.rb index 0ba542b75ab..6e80365ee5b 100644 --- a/app/models/concerns/case_sensitivity.rb +++ b/app/models/concerns/case_sensitivity.rb @@ -4,7 +4,7 @@ module CaseSensitivity extend ActiveSupport::Concern - module ClassMethods + class_methods do # Queries the given columns regardless of the casing used. # # Unlike other ActiveRecord methods this method only operates on a Hash. diff --git a/app/models/concerns/each_batch.rb b/app/models/concerns/each_batch.rb index a9e14cb55eb..8cf0b8b154d 100644 --- a/app/models/concerns/each_batch.rb +++ b/app/models/concerns/each_batch.rb @@ -3,7 +3,7 @@ module EachBatch extend ActiveSupport::Concern - module ClassMethods + class_methods do # Iterates over the rows in a relation in batches, similar to Rails' # `in_batches` but in a more efficient way. # diff --git a/app/models/concerns/ignorable_column.rb b/app/models/concerns/ignorable_column.rb index 2b074c1921c..5c1f7dfcd2a 100644 --- a/app/models/concerns/ignorable_column.rb +++ b/app/models/concerns/ignorable_column.rb @@ -14,7 +14,7 @@ module IgnorableColumn extend ActiveSupport::Concern - module ClassMethods + class_methods do def columns super.reject { |column| ignored_columns.include?(column.name) } end diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index e8072145551..f881ce2321c 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -118,7 +118,7 @@ module Issuable end end - module ClassMethods + class_methods do # Searches for records with a matching title. # # This method uses ILIKE on PostgreSQL and LIKE on MySQL. diff --git a/app/models/concerns/loaded_in_group_list.rb b/app/models/concerns/loaded_in_group_list.rb index a2233eb2997..fc15c6d55ed 100644 --- a/app/models/concerns/loaded_in_group_list.rb +++ b/app/models/concerns/loaded_in_group_list.rb @@ -3,7 +3,7 @@ module LoadedInGroupList extend ActiveSupport::Concern - module ClassMethods + class_methods do def with_counts(archived:) selects_including_counts = [ 'namespaces.*', diff --git a/app/models/concerns/manual_inverse_association.rb b/app/models/concerns/manual_inverse_association.rb index d0d781dc15f..e18edd33ba7 100644 --- a/app/models/concerns/manual_inverse_association.rb +++ b/app/models/concerns/manual_inverse_association.rb @@ -3,7 +3,7 @@ module ManualInverseAssociation extend ActiveSupport::Concern - module ClassMethods + class_methods do def manual_inverse_association(association, inverse) define_method(association) do |*args| super(*args).tap do |value| diff --git a/app/models/concerns/mentionable.rb b/app/models/concerns/mentionable.rb index 7e7eccb1c27..393607e82c4 100644 --- a/app/models/concerns/mentionable.rb +++ b/app/models/concerns/mentionable.rb @@ -10,7 +10,7 @@ module Mentionable extend ActiveSupport::Concern - module ClassMethods + class_methods do # Indicate which attributes of the Mentionable to search for GFM references. def attr_mentionable(attr, options = {}) attr = attr.to_s diff --git a/app/models/concerns/optionally_search.rb b/app/models/concerns/optionally_search.rb index dec97b7dee8..4093429e372 100644 --- a/app/models/concerns/optionally_search.rb +++ b/app/models/concerns/optionally_search.rb @@ -3,7 +3,7 @@ module OptionallySearch extend ActiveSupport::Concern - module ClassMethods + class_methods do def search(*) raise( NotImplementedError, diff --git a/app/models/concerns/participable.rb b/app/models/concerns/participable.rb index 1f6c42f3b3a..614c3242874 100644 --- a/app/models/concerns/participable.rb +++ b/app/models/concerns/participable.rb @@ -26,7 +26,7 @@ module Participable extend ActiveSupport::Concern - module ClassMethods + class_methods do # Adds a list of participant attributes. Attributes can either be symbols or # Procs. # diff --git a/app/models/concerns/referable.rb b/app/models/concerns/referable.rb index 468eaf68883..58143a32fdc 100644 --- a/app/models/concerns/referable.rb +++ b/app/models/concerns/referable.rb @@ -40,7 +40,7 @@ module Referable end end - module ClassMethods + class_methods do # The character that prefixes the actual reference identifier # # This should be overridden by the including class. diff --git a/app/models/concerns/resolvable_note.rb b/app/models/concerns/resolvable_note.rb index f47e20229f1..16ea330701d 100644 --- a/app/models/concerns/resolvable_note.rb +++ b/app/models/concerns/resolvable_note.rb @@ -20,7 +20,7 @@ module ResolvableNote scope :unresolved, -> { resolvable.where(resolved_at: nil) } end - module ClassMethods + class_methods do # This method must be kept in sync with `#resolve!` def resolve!(current_user) unresolved.update_all(resolved_at: Time.now, resolved_by_id: current_user.id) diff --git a/app/models/concerns/select_for_project_authorization.rb b/app/models/concerns/select_for_project_authorization.rb index 39306179eb8..333c9118aa5 100644 --- a/app/models/concerns/select_for_project_authorization.rb +++ b/app/models/concerns/select_for_project_authorization.rb @@ -3,7 +3,7 @@ module SelectForProjectAuthorization extend ActiveSupport::Concern - module ClassMethods + class_methods do def select_for_project_authorization select("projects.id AS project_id, members.access_level") end diff --git a/app/models/concerns/sha_attribute.rb b/app/models/concerns/sha_attribute.rb index c322c356db2..e51b4e22c96 100644 --- a/app/models/concerns/sha_attribute.rb +++ b/app/models/concerns/sha_attribute.rb @@ -3,7 +3,7 @@ module ShaAttribute extend ActiveSupport::Concern - module ClassMethods + class_methods do def sha_attribute(name) return if ENV['STATIC_VERIFICATION'] diff --git a/app/models/concerns/sortable.rb b/app/models/concerns/sortable.rb index 501bd1bb83c..29e48f0c5f7 100644 --- a/app/models/concerns/sortable.rb +++ b/app/models/concerns/sortable.rb @@ -19,7 +19,7 @@ module Sortable scope :order_name_desc, -> { reorder(Arel::Nodes::Descending.new(arel_table[:name].lower)) } end - module ClassMethods + class_methods do def order_by(method) case method.to_s when 'created_asc' then order_created_asc diff --git a/app/models/concerns/spammable.rb b/app/models/concerns/spammable.rb index c6e3dc385fe..3ff4b4046d3 100644 --- a/app/models/concerns/spammable.rb +++ b/app/models/concerns/spammable.rb @@ -3,7 +3,7 @@ module Spammable extend ActiveSupport::Concern - module ClassMethods + class_methods do def attr_spammable(attr, options = {}) spammable_attrs << [attr.to_s, options] end diff --git a/app/models/concerns/strip_attribute.rb b/app/models/concerns/strip_attribute.rb index 344f677a3f3..c9f5ba7793d 100644 --- a/app/models/concerns/strip_attribute.rb +++ b/app/models/concerns/strip_attribute.rb @@ -14,7 +14,7 @@ module StripAttribute extend ActiveSupport::Concern - module ClassMethods + class_methods do def strip_attributes(*attrs) strip_attrs.concat(attrs) end diff --git a/app/models/diff_note.rb b/app/models/diff_note.rb index 58d949315e0..716cf6574d3 100644 --- a/app/models/diff_note.rb +++ b/app/models/diff_note.rb @@ -191,14 +191,18 @@ class DiffNote < Note end def keep_around_commits - project.repository.keep_around(self.original_position.base_sha) - project.repository.keep_around(self.original_position.start_sha) - project.repository.keep_around(self.original_position.head_sha) + shas = [ + self.original_position.base_sha, + self.original_position.start_sha, + self.original_position.head_sha + ] if self.position != self.original_position - project.repository.keep_around(self.position.base_sha) - project.repository.keep_around(self.position.start_sha) - project.repository.keep_around(self.position.head_sha) + shas << self.position.base_sha + shas << self.position.start_sha + shas << self.position.head_sha end + + project.repository.keep_around(*shas) end end diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb index d9393b4e545..bbe4f6f7969 100644 --- a/app/models/merge_request_diff.rb +++ b/app/models/merge_request_diff.rb @@ -314,9 +314,7 @@ class MergeRequestDiff < ActiveRecord::Base def keep_around_commits [repository, merge_request.source_project.repository].uniq.each do |repo| - repo.keep_around(start_commit_sha) - repo.keep_around(head_commit_sha) - repo.keep_around(base_commit_sha) + repo.keep_around(start_commit_sha, head_commit_sha, base_commit_sha) end end end diff --git a/app/models/repository.rb b/app/models/repository.rb index 69f375dc6f3..cf255c8951f 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -247,15 +247,22 @@ class Repository # Git GC will delete commits from the repository that are no longer in any # branches or tags, but we want to keep some of these commits around, for # example if they have comments or CI builds. - def keep_around(sha) - return unless sha.present? && commit_by(oid: sha) + # + # For Geo's sake, pass in multiple shas rather than calling it multiple times, + # to avoid unnecessary syncing. + def keep_around(*shas) + shas.each do |sha| + begin + next unless sha.present? && commit_by(oid: sha) - return if kept_around?(sha) + next if kept_around?(sha) - # This will still fail if the file is corrupted (e.g. 0 bytes) - raw_repository.write_ref(keep_around_ref_name(sha), sha, shell: false) - rescue Gitlab::Git::CommandError => ex - Rails.logger.error "Unable to create keep-around reference for repository #{disk_path}: #{ex}" + # This will still fail if the file is corrupted (e.g. 0 bytes) + raw_repository.write_ref(keep_around_ref_name(sha), sha, shell: false) + rescue Gitlab::Git::CommandError => ex + Rails.logger.error "Unable to create keep-around reference for repository #{disk_path}: #{ex}" + end + end end def kept_around?(sha) diff --git a/app/serializers/diff_file_entity.rb b/app/serializers/diff_file_entity.rb index 79844c9210a..d49d4895d89 100644 --- a/app/serializers/diff_file_entity.rb +++ b/app/serializers/diff_file_entity.rb @@ -2,7 +2,6 @@ class DiffFileEntity < Grape::Entity include RequestAwareEntity - include BlobHelper include CommitsHelper include DiffHelper include SubmoduleHelper diff --git a/app/services/projects/transfer_service.rb b/app/services/projects/transfer_service.rb index c2a0c5fa7f3..3746cfef702 100644 --- a/app/services/projects/transfer_service.rb +++ b/app/services/projects/transfer_service.rb @@ -43,8 +43,8 @@ module Projects @new_path = File.join(@new_namespace.try(:full_path) || '', project.path) @old_namespace = project.namespace - if Project.where(path: project.path, namespace_id: @new_namespace.try(:id)).exists? - raise TransferError.new("Project with same path in target namespace already exists") + if Project.where(namespace_id: @new_namespace.try(:id)).where('path = ? or name = ?', project.path, project.name).exists? + raise TransferError.new("Project with same name or path in target namespace already exists") end if project.has_container_registry_tags? @@ -118,6 +118,7 @@ module Projects def rollback_side_effects rollback_folder_move + project.reload update_namespace_and_visibility(@old_namespace) write_repository_config(@old_path) end diff --git a/app/services/users/build_service.rb b/app/services/users/build_service.rb index acc2fa153ae..9417c63c43a 100644 --- a/app/services/users/build_service.rb +++ b/app/services/users/build_service.rb @@ -2,6 +2,10 @@ module Users class BuildService < BaseService + delegate :user_default_internal_regex_enabled?, + :user_default_internal_regex_instance, + to: :'Gitlab::CurrentSettings.current_application_settings' + def initialize(current_user, params = {}) @current_user = current_user @params = params.dup @@ -89,6 +93,10 @@ module Users if params[:reset_password] user_params.merge!(force_random_password: true, password_expires_at: nil) end + + if user_default_internal_regex_enabled? && !user_params.key?(:external) + user_params[:external] = user_external? + end else allowed_signup_params = signup_params allowed_signup_params << :skip_confirmation if skip_authorization @@ -105,5 +113,9 @@ module Users def skip_user_confirmation_email_from_setting !Gitlab::CurrentSettings.send_user_confirmation_email end + + def user_external? + user_default_internal_regex_instance.match(params[:email]).nil? + end end end diff --git a/app/validators/js_regex_validator.rb b/app/validators/js_regex_validator.rb new file mode 100644 index 00000000000..a515af7b919 --- /dev/null +++ b/app/validators/js_regex_validator.rb @@ -0,0 +1,15 @@ +class JsRegexValidator < ActiveModel::EachValidator + def validate_each(record, attribute, value) + return true if value.blank? + + parsed_regex = JsRegex.new(Regexp.new(value, Regexp::IGNORECASE)) + + if parsed_regex.source.empty? + record.errors.add(attribute, "Regex Pattern #{value} can not be expressed in Javascript") + else + parsed_regex.warnings.each { |warning| record.errors.add(attribute, warning) } + end + rescue RegexpError => regex_error + record.errors.add(attribute, regex_error.to_s) + end +end diff --git a/app/views/admin/application_settings/_account_and_limit.html.haml b/app/views/admin/application_settings/_account_and_limit.html.haml index 622cb11010e..9121e44d31b 100644 --- a/app/views/admin/application_settings/_account_and_limit.html.haml +++ b/app/views/admin/application_settings/_account_and_limit.html.haml @@ -29,6 +29,13 @@ = f.check_box :user_default_external, class: 'form-check-input' = f.label :user_default_external, class: 'form-check-label' do Newly registered users will by default be external + .prepend-top-10 + = _('Internal users') + = f.text_field :user_default_internal_regex, placeholder: _('Regex pattern'), class: 'form-control prepend-top-5' + .help-block + = _('Specify an e-mail address regex pattern to identify default internal users.') + = link_to _('More information'), help_page_path('user/permissions', anchor: 'external-users-permissions'), + target: '_blank' .form-group = f.label :user_show_add_ssh_key_message, 'Prompt users to upload SSH keys', class: 'label-bold' .form-check diff --git a/app/views/admin/hook_logs/show.html.haml b/app/views/admin/hook_logs/show.html.haml index 2eb3ac85722..86729dbe7bc 100644 --- a/app/views/admin/hook_logs/show.html.haml +++ b/app/views/admin/hook_logs/show.html.haml @@ -4,7 +4,6 @@ %hr -= link_to 'Resend Request', retry_admin_hook_hook_log_path(@hook, @hook_log), class: "btn btn-default float-right prepend-left-10" += link_to 'Resend Request', retry_admin_hook_hook_log_path(@hook, @hook_log), method: :post, class: "btn btn-default float-right prepend-left-10" = render partial: 'shared/hook_logs/content', locals: { hook_log: @hook_log } - diff --git a/app/views/admin/users/_access_levels.html.haml b/app/views/admin/users/_access_levels.html.haml index 5f68163163e..12e24ddef02 100644 --- a/app/views/admin/users/_access_levels.html.haml +++ b/app/views/admin/users/_access_levels.html.haml @@ -34,8 +34,12 @@ .form-group.row .col-sm-2.text-right = f.label :external, class: 'col-form-label' + .hidden{ data: user_internal_regex_data } .col-sm-10 = f.check_box :external do External %p.light External users cannot see internal or private projects unless access is explicitly granted. Also, external users cannot create projects or groups. + %row.hidden#warning_external_automatically_set.hidden + .badge.badge-warning.text-white + = _('Automatically marked as default internal user') diff --git a/app/views/ci/status/_dropdown_graph_badge.html.haml b/app/views/ci/status/_dropdown_graph_badge.html.haml index 8b0463db000..9de9143e8b1 100644 --- a/app/views/ci/status/_dropdown_graph_badge.html.haml +++ b/app/views/ci/status/_dropdown_graph_badge.html.haml @@ -6,12 +6,12 @@ - tooltip = "#{subject.name} - #{status.status_tooltip}" - if status.has_details? - = link_to status.details_path, class: 'mini-pipeline-graph-dropdown-item', data: { toggle: 'tooltip', title: tooltip, html: 'true', container: 'body' } do + = link_to status.details_path, class: 'mini-pipeline-graph-dropdown-item', data: { toggle: 'tooltip', title: tooltip, container: 'body' } do %span{ class: klass }= sprite_icon(status.icon) %span.ci-build-text= subject.name - else - .menu-item.mini-pipeline-graph-dropdown-item{ data: { toggle: 'tooltip', html: 'true', title: tooltip, container: 'body' } } + .menu-item.mini-pipeline-graph-dropdown-item{ data: { toggle: 'tooltip', title: tooltip, container: 'body' } } %span{ class: klass }= sprite_icon(status.icon) %span.ci-build-text= subject.name diff --git a/app/views/ide/index.html.haml b/app/views/ide/index.html.haml index 4cae9c51acc..d8bd37fe986 100644 --- a/app/views/ide/index.html.haml +++ b/app/views/ide/index.html.haml @@ -1,4 +1,4 @@ -- @body_class = 'ide' +- @body_class = 'ide-layout' - page_title 'IDE' - content_for :page_specific_javascripts do diff --git a/app/views/layouts/nav_only.html.haml b/app/views/layouts/fullscreen.html.haml index 0811211f7b2..95db8313821 100644 --- a/app/views/layouts/nav_only.html.haml +++ b/app/views/layouts/fullscreen.html.haml @@ -1,7 +1,7 @@ !!! 5 %html{ lang: I18n.locale, class: page_class } = render "layouts/head" - %body{ class: "#{user_application_theme} #{@body_class} nav-only", data: { page: body_data_page } } + %body{ class: "#{user_application_theme} #{@body_class} fullscreen-layout", data: { page: body_data_page } } = render 'peek/bar' = render "layouts/header/default" = render 'shared/outdated_browser' @@ -10,5 +10,5 @@ = render "layouts/broadcast" = yield :flash_message = render "layouts/flash" - .content{ id: "content-body" } + .content-wrapper{ id: "content-body", class: "d-flex flex-column align-items-stretch" } = yield diff --git a/app/views/projects/diffs/_line.html.haml b/app/views/projects/diffs/_line.html.haml index cd0fb21f8a7..ffdca500abe 100644 --- a/app/views/projects/diffs/_line.html.haml +++ b/app/views/projects/diffs/_line.html.haml @@ -32,9 +32,9 @@ %a{ href: "##{line_code}", data: { linenumber: link_text } } %td.line_content.noteable_line{ class: type }< - if email - %pre= line.text + %pre= line.rich_text - else - = diff_line_content(line.text) + = diff_line_content(line.rich_text) - if line_discussions&.any? - discussion_expanded = local_assigns.fetch(:discussion_expanded, line_discussions.any?(&:expanded?)) diff --git a/app/views/projects/diffs/_parallel_view.html.haml b/app/views/projects/diffs/_parallel_view.html.haml index 1f0ca211074..e47361354f3 100644 --- a/app/views/projects/diffs/_parallel_view.html.haml +++ b/app/views/projects/diffs/_parallel_view.html.haml @@ -24,7 +24,7 @@ - discussion_left = discussions_left.try(:first) - if discussion_left && discussion_left.resolvable? %diff-note-avatars{ "discussion-id" => discussion_left.id } - %td.line_content.parallel.noteable_line.left-side{ id: left_line_code, class: left.type }= diff_line_content(left.text) + %td.line_content.parallel.noteable_line.left-side{ id: left_line_code, class: left.type }= diff_line_content(left.rich_text) - else %td.old_line.diff-line-num.empty-cell %td.line_content.parallel.left-side @@ -45,7 +45,7 @@ - discussion_right = discussions_right.try(:first) - if discussion_right && discussion_right.resolvable? %diff-note-avatars{ "discussion-id" => discussion_right.id } - %td.line_content.parallel.noteable_line.right-side{ id: right_line_code, class: right.type }= diff_line_content(right.text) + %td.line_content.parallel.noteable_line.right-side{ id: right_line_code, class: right.type }= diff_line_content(right.rich_text) - else %td.old_line.diff-line-num.empty-cell %td.line_content.parallel.right-side diff --git a/app/views/projects/hook_logs/show.html.haml b/app/views/projects/hook_logs/show.html.haml index e51efa85df0..bd8ca5e7d70 100644 --- a/app/views/projects/hook_logs/show.html.haml +++ b/app/views/projects/hook_logs/show.html.haml @@ -4,6 +4,6 @@ Request details .col-lg-9 - = link_to 'Resend Request', retry_project_hook_hook_log_path(@project, @hook, @hook_log), class: "btn btn-default float-right prepend-left-10" + = link_to 'Resend Request', retry_project_hook_hook_log_path(@project, @hook, @hook_log), method: :post, class: "btn btn-default float-right prepend-left-10" = render partial: 'shared/hook_logs/content', locals: { hook_log: @hook_log } diff --git a/app/views/projects/jobs/_sidebar.html.haml b/app/views/projects/jobs/_sidebar.html.haml index 86b2b8bf2f7..acc1e17b811 100644 --- a/app/views/projects/jobs/_sidebar.html.haml +++ b/app/views/projects/jobs/_sidebar.html.haml @@ -82,7 +82,7 @@ - builds.select{|build| build.status == build_status}.each do |build| .build-job{ class: sidebar_build_class(build, @build), data: { stage: build.stage } } - tooltip = sanitize(build.tooltip_message.dup) - = link_to(project_job_path(@project, build), data: { toggle: 'tooltip', html: 'true', title: tooltip, container: 'body' }) do + = link_to(project_job_path(@project, build), data: { toggle: 'tooltip', title: tooltip, container: 'body' }) do = sprite_icon('arrow-right', size:16, css_class: 'icon-arrow-right') %span{ class: "ci-status-icon-#{build.status}" } = ci_icon_for_status(build.status) diff --git a/app/views/projects/merge_requests/_how_to_merge.html.haml b/app/views/projects/merge_requests/_how_to_merge.html.haml index d3871453b9f..15499c89ffb 100644 --- a/app/views/projects/merge_requests/_how_to_merge.html.haml +++ b/app/views/projects/merge_requests/_how_to_merge.html.haml @@ -30,11 +30,13 @@ %pre.dark#merge-info-3 - if @merge_request.for_fork? :preserve - git checkout #{h @merge_request.target_branch} + git fetch origin + git checkout origin/#{h @merge_request.target_branch} git merge --no-ff #{h @merge_request.source_project_path}-#{h @merge_request.source_branch} - else :preserve - git checkout #{h @merge_request.target_branch} + git fetch origin + git checkout origin/#{h @merge_request.target_branch} git merge --no-ff #{h @merge_request.source_branch} %p %strong Step 4. diff --git a/app/views/projects/milestones/_deprecation_message.html.haml b/app/views/projects/milestones/_deprecation_message.html.haml new file mode 100644 index 00000000000..b2cca3690d6 --- /dev/null +++ b/app/views/projects/milestones/_deprecation_message.html.haml @@ -0,0 +1,7 @@ +.banner-callout.compact.milestone-deprecation-message.prepend-top-20 + .banner-graphic= image_tag 'illustrations/milestone_removing-page.svg' + .banner-body.prepend-left-10.append-right-10 + %h5.banner-title.prepend-top-0 + = _('The tabs below will be removed in a future version') + %p.milestone-banner-text + = _('Learn more about %{issue_boards_url}, to keep track of issues in multiple lists, using labels, assignees, and milestones. If you’re missing something from issue boards, please create an issue on %{gitlab_issues_url}.').html_safe % { issue_boards_url: link_to(_('issue boards'), help_page_url('user/project/issue_board'), target: '_blank', rel: 'noopener noreferrer'), gitlab_issues_url: link_to(_('GitLab’s issue tracker'), 'https://gitlab.com/gitlab-org/gitlab-ce/issues', target: '_blank', rel: 'noopener noreferrer') } diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml index 0a684f9016a..5859de61d71 100644 --- a/app/views/projects/milestones/show.html.haml +++ b/app/views/projects/milestones/show.html.haml @@ -67,5 +67,6 @@ .alert.alert-success.prepend-top-default %span All issues for this milestone are closed. You may close this milestone now. + = render 'deprecation_message' = render 'shared/milestones/tabs', milestone: @milestone = render 'shared/milestones/sidebar', milestone: @milestone, project: @project, affix_offset: 153 diff --git a/app/workers/concerns/application_worker.rb b/app/workers/concerns/application_worker.rb index bb06e31641d..d64c2f82a09 100644 --- a/app/workers/concerns/application_worker.rb +++ b/app/workers/concerns/application_worker.rb @@ -11,7 +11,7 @@ module ApplicationWorker set_queue end - module ClassMethods + class_methods do def inherited(subclass) subclass.set_queue end diff --git a/app/workers/concerns/waitable_worker.rb b/app/workers/concerns/waitable_worker.rb index d85bc7d1660..27b94a82444 100644 --- a/app/workers/concerns/waitable_worker.rb +++ b/app/workers/concerns/waitable_worker.rb @@ -3,7 +3,7 @@ module WaitableWorker extend ActiveSupport::Concern - module ClassMethods + class_methods do # Schedules multiple jobs and waits for them to be completed. def bulk_perform_and_wait(args_list, timeout: 10) # Short-circuit: it's more efficient to do small numbers of jobs inline diff --git a/changelogs/unreleased/37356-relative-submodule-link.yml b/changelogs/unreleased/37356-relative-submodule-link.yml new file mode 100644 index 00000000000..99d1577609d --- /dev/null +++ b/changelogs/unreleased/37356-relative-submodule-link.yml @@ -0,0 +1,5 @@ +--- +title: Fix git submodule link for subgroup projects with relative path +merge_request: 21154 +author: +type: fixed diff --git a/changelogs/unreleased/41292-users-stuck-on-a-redirect-loop-after-transferring-project.yml b/changelogs/unreleased/41292-users-stuck-on-a-redirect-loop-after-transferring-project.yml new file mode 100644 index 00000000000..830c02510f2 --- /dev/null +++ b/changelogs/unreleased/41292-users-stuck-on-a-redirect-loop-after-transferring-project.yml @@ -0,0 +1,5 @@ +--- +title: Fix project transfer name validation issues causing a redirect loop +merge_request: 21408 +author: +type: fixed diff --git a/changelogs/unreleased/46591-fix-ide-height-issues.yml b/changelogs/unreleased/46591-fix-ide-height-issues.yml new file mode 100644 index 00000000000..d161bda6ab1 --- /dev/null +++ b/changelogs/unreleased/46591-fix-ide-height-issues.yml @@ -0,0 +1,5 @@ +--- +title: Fix IDE issues with persistent banners +merge_request: 21283 +author: +type: fixed diff --git a/changelogs/unreleased/47943-project-milestone-page-deprecation-message.yml b/changelogs/unreleased/47943-project-milestone-page-deprecation-message.yml new file mode 100644 index 00000000000..b9f68e1c46c --- /dev/null +++ b/changelogs/unreleased/47943-project-milestone-page-deprecation-message.yml @@ -0,0 +1,5 @@ +--- +title: Show deprecation message on project milestone page for category tabs +merge_request: 21236 +author: +type: changed diff --git a/changelogs/unreleased/50414-rubocop-rule-to-enforce-class-methods-over-module.yml b/changelogs/unreleased/50414-rubocop-rule-to-enforce-class-methods-over-module.yml new file mode 100644 index 00000000000..1694fb2376d --- /dev/null +++ b/changelogs/unreleased/50414-rubocop-rule-to-enforce-class-methods-over-module.yml @@ -0,0 +1,5 @@ +--- +title: Adds Rubocop rule to enforce class_methods over module ClassMethods +merge_request: 21379 +author: Jacopo Beschi @jacopo-beschi +type: added diff --git a/changelogs/unreleased/50801-error-getting-performance-bar-results-for-uuid.yml b/changelogs/unreleased/50801-error-getting-performance-bar-results-for-uuid.yml new file mode 100644 index 00000000000..6e57a215367 --- /dev/null +++ b/changelogs/unreleased/50801-error-getting-performance-bar-results-for-uuid.yml @@ -0,0 +1,5 @@ +--- +title: Don't show flash messages for performance bar errors +merge_request: 21411 +author: +type: other diff --git a/changelogs/unreleased/feature-whitelist-new-users-as-internal.yml b/changelogs/unreleased/feature-whitelist-new-users-as-internal.yml new file mode 100644 index 00000000000..7a3bd11c119 --- /dev/null +++ b/changelogs/unreleased/feature-whitelist-new-users-as-internal.yml @@ -0,0 +1,5 @@ +--- +title: Add an option to whitelist users based on email address as internal when the "New user set to external" setting is enabled. +merge_request: 17711 +author: Roger Rüttimann +type: added diff --git a/changelogs/unreleased/fix_emojis_cutting_and_regressions.yml b/changelogs/unreleased/fix_emojis_cutting_and_regressions.yml new file mode 100644 index 00000000000..a9c1b88a61c --- /dev/null +++ b/changelogs/unreleased/fix_emojis_cutting_and_regressions.yml @@ -0,0 +1,5 @@ +--- +title: Fix Emojis cutting in the right way +merge_request: +author: Alexander Popov +type: fixed diff --git a/changelogs/unreleased/fj-47229-fix-logo-lfs-tracked.yml b/changelogs/unreleased/fj-47229-fix-logo-lfs-tracked.yml new file mode 100644 index 00000000000..ed2af81f779 --- /dev/null +++ b/changelogs/unreleased/fj-47229-fix-logo-lfs-tracked.yml @@ -0,0 +1,5 @@ +--- +title: Fixed bug when the project logo file is stored in LFS +merge_request: 20948 +author: +type: fixed diff --git a/changelogs/unreleased/ide-multiple-file-uploads.yml b/changelogs/unreleased/ide-multiple-file-uploads.yml new file mode 100644 index 00000000000..6bb73739864 --- /dev/null +++ b/changelogs/unreleased/ide-multiple-file-uploads.yml @@ -0,0 +1,5 @@ +--- +title: Enabled multiple file uploads in the Web IDE +merge_request: +author: +type: added diff --git a/changelogs/unreleased/ide-row-hover-scroll.yml b/changelogs/unreleased/ide-row-hover-scroll.yml new file mode 100644 index 00000000000..24c273b4f25 --- /dev/null +++ b/changelogs/unreleased/ide-row-hover-scroll.yml @@ -0,0 +1,5 @@ +--- +title: Fixed IDE file row scrolling into view when hovering +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/rails5-silence-stream.yml b/changelogs/unreleased/rails5-silence-stream.yml new file mode 100644 index 00000000000..df4fd14a077 --- /dev/null +++ b/changelogs/unreleased/rails5-silence-stream.yml @@ -0,0 +1,5 @@ +--- +title: 'Rails 5: replace removed silence_stream' +merge_request: 21387 +author: Jasper Maes +type: other diff --git a/changelogs/unreleased/schema-changed-ee-backport.yml b/changelogs/unreleased/schema-changed-ee-backport.yml new file mode 100644 index 00000000000..f3b16fc0c27 --- /dev/null +++ b/changelogs/unreleased/schema-changed-ee-backport.yml @@ -0,0 +1,5 @@ +--- +title: Backport schema_changed.sh from EE which prints the diff if the schema is different +merge_request: 21422 +author: Jasper Maes +type: other diff --git a/changelogs/unreleased/security-49085-persistent-xss-rendering.yml b/changelogs/unreleased/security-49085-persistent-xss-rendering.yml new file mode 100644 index 00000000000..dc15d356c1c --- /dev/null +++ b/changelogs/unreleased/security-49085-persistent-xss-rendering.yml @@ -0,0 +1,5 @@ +--- +title: Fixed persistent XSS rendering/escaping of diff location lines +merge_request: +author: +type: security diff --git a/changelogs/unreleased/sh-block-link-local-master.yml b/changelogs/unreleased/sh-block-link-local-master.yml new file mode 100644 index 00000000000..0a6017479af --- /dev/null +++ b/changelogs/unreleased/sh-block-link-local-master.yml @@ -0,0 +1,5 @@ +--- +title: Block link-local addresses in URLBlocker +merge_request: +author: +type: security diff --git a/changelogs/unreleased/sh-fix-error-500-updating-wikis.yml b/changelogs/unreleased/sh-fix-error-500-updating-wikis.yml new file mode 100644 index 00000000000..d80d4952ba5 --- /dev/null +++ b/changelogs/unreleased/sh-fix-error-500-updating-wikis.yml @@ -0,0 +1,5 @@ +--- +title: Fix Error 500s due to encoding issues when Wiki hooks fire +merge_request: 21414 +author: +type: fixed diff --git a/changelogs/unreleased/update-padding-markdown.yml b/changelogs/unreleased/update-padding-markdown.yml new file mode 100644 index 00000000000..51037200bd1 --- /dev/null +++ b/changelogs/unreleased/update-padding-markdown.yml @@ -0,0 +1,5 @@ +--- +title: Increase padding in code blocks +merge_request: +author: +type: fixed diff --git a/config/routes/admin.rb b/config/routes/admin.rb index 7ee960970f8..fa1f79a90be 100644 --- a/config/routes/admin.rb +++ b/config/routes/admin.rb @@ -59,7 +59,7 @@ namespace :admin do resources :hook_logs, only: [:show] do member do - get :retry + post :retry end end end diff --git a/config/routes/project.rb b/config/routes/project.rb index 0220e88c819..34f49546983 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -307,7 +307,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do resources :hook_logs, only: [:show] do member do - get :retry + post :retry end end end diff --git a/db/migrate/20180308125206_add_user_internal_regex_to_application_setting.rb b/db/migrate/20180308125206_add_user_internal_regex_to_application_setting.rb new file mode 100644 index 00000000000..fe50e909563 --- /dev/null +++ b/db/migrate/20180308125206_add_user_internal_regex_to_application_setting.rb @@ -0,0 +1,13 @@ +class AddUserInternalRegexToApplicationSetting < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def up + add_column :application_settings, :user_default_internal_regex, :string, null: true + end + + def down + remove_column :application_settings, :user_default_internal_regex + end +end diff --git a/db/schema.rb b/db/schema.rb index cb8f90efded..02e545bec7d 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -164,6 +164,7 @@ ActiveRecord::Schema.define(version: 20180826111825) do t.boolean "authorized_keys_enabled", default: true, null: false t.string "auto_devops_domain" t.boolean "pages_domain_verification_enabled", default: true, null: false + t.string "user_default_internal_regex" t.boolean "allow_local_requests_from_hooks_and_services", default: false, null: false t.boolean "enforce_terms", default: false t.boolean "mirror_available", default: true, null: false diff --git a/doc/administration/compliance.md b/doc/administration/compliance.md new file mode 100644 index 00000000000..0414b3ec12e --- /dev/null +++ b/doc/administration/compliance.md @@ -0,0 +1,18 @@ +# Compliance features + +You can configure the following GitLab features to help ensure that your GitLab instance meets common compliance standards. Click a feature name for further documentation. + +GitLab’s [security features](../security/README.md) may also help you meet relevant compliance standards. + +|Feature |GitLab tier |GitLab.com | +| ---------| :--------: | :-------: | +|**[Restrict SSH Keys](../README.html#administrator-documentation)**<br>Control the technology and key length of SSH keys used to access GitLab|Core+|| +|**[Granular user roles and flexible permissions](../user/permissions.html)**<br>Manage access and permissions with five different user roles and settings for external users. Set permissions according to people's role, rather than either read or write access to a repository. Don't share the source code with people that only need access to the issue tracker.|Core+|✓| +|**[Enforce TOS acceptance](../user/admin_area/settings/terms.html)**<br>Enforce your users accepting new terms of service by blocking GitLab traffic.|Core+|| +|**[Email all users of a project, group, or entire server](../user/admin_area/settings/terms.html)**<br>An admin can email groups of users based on project or group membership, or email everyone using the GitLab instance. This is great for scheduled maintenance or upgrades.|Starter+|| +|**[Omnibus package supports log forwarding](https://docs.gitlab.com/omnibus/settings/logs.html#udp-log-forwarding)**<br>Forward your logs to a central system.|Starter+|| +|**[Lock project membership to group](../workflow/groups.html#lock-project-membership-to-members-of-this-group)**<br>Group owners can prevent new members from being added to projects within a group.|Starter+|✓| +|**[LDAP group sync](https://docs.gitlab.com/ee/administration/auth/ldap-ee.html#group-sync)**<br>GitLab Enterprise Edition gives admins the ability to automatically sync groups and manage SSH keys, permissions, and authentication, so you can focus on building your product, not configuring your tools.|Starter+|| +|**[LDAP group sync filters](https://docs.gitlab.com/ee/administration/auth/ldap-ee.html#group-sync)**<br>GitLab Enterprise Edition Premium gives more flexibility to synchronize with LDAP based on filters, meaning you can leverage LDAP attributes to map GitLab permissions.|Premium+|| +|**[Audit logs](https://docs.gitlab.com/ee/administration/audit_events.html)**<br>To maintain the integrity of your code, GitLab Enterprise Edition Premium gives admins the ability to view any modifications made within the GitLab server in an advanced audit log system, so you can control, analyze and track every change.|Premium+|| +|**[Auditor users](https://docs.gitlab.com/ee/administration/auditor_users.html)**<br>Auditor users are users who are given read-only access to all projects, groups, and other resources on the GitLab instance.|Premium+||
\ No newline at end of file diff --git a/doc/administration/index.md b/doc/administration/index.md index 030a2f95e23..837a04f3e88 100644 --- a/doc/administration/index.md +++ b/doc/administration/index.md @@ -46,6 +46,7 @@ Learn how to install, configure, update, and maintain your GitLab instance. - [Plugins](plugins.md): With custom plugins, GitLab administrators can introduce custom integrations without modifying GitLab's source code. - [Enforcing Terms of Service](../user/admin_area/settings/terms.md) - [Third party offers](../user/admin_area/settings/third_party_offers.md) +- [Compliance](compliance.md): A collection of features from across the application that you may configure to help ensure that your GitLab instance and DevOps workflow meet compliance standards. #### Customizing GitLab's appearance diff --git a/doc/install/kubernetes/gitlab_chart.md b/doc/install/kubernetes/gitlab_chart.md index 4e636ae3399..5a6f26319c7 100644 --- a/doc/install/kubernetes/gitlab_chart.md +++ b/doc/install/kubernetes/gitlab_chart.md @@ -30,7 +30,7 @@ The `gitlab` chart includes all required dependencies, and takes a few minutes to deploy. TIP: **Tip:** -For large scale deployments, we strongly recommend using the +For production deployments, we strongly recommend using the [detailed installation instructions](https://gitlab.com/charts/gitlab/blob/master/doc/installation/README.md) utilizing [external Postgres, Redis, and object storage](https://gitlab.com/charts/gitlab/tree/master/doc/advanced) services. diff --git a/doc/user/group/img/groups.png b/doc/user/group/img/groups.png Binary files differindex efdfd5f82cd..2e27d46b370 100644 --- a/doc/user/group/img/groups.png +++ b/doc/user/group/img/groups.png diff --git a/doc/user/permissions.md b/doc/user/permissions.md index b6438397db8..10ac6301aa1 100644 --- a/doc/user/permissions.md +++ b/doc/user/permissions.md @@ -197,7 +197,7 @@ They will, like usual users, receive a role in the project or group with all the abilities that are mentioned in the table above. They cannot however create groups or projects, and they have the same access as logged out users in all other cases. - + An administrator can flag a user as external [through the API](../api/users.md) or by checking the checkbox on the admin panel. As an administrator, navigate to **Admin > Users** to create a new user or edit an existing one. There, you @@ -206,6 +206,21 @@ will find the option to flag the user as external. By default new users are not set as external users. This behavior can be changed by an administrator under **Admin > Application Settings**. +### Default internal users + +The "Internal users" field allows specifying an e-mail address regex pattern to identify default internal users. + +New users whose email address matches the regex pattern will be set to internal by default rather than an external collaborator. + +The regex pattern format is Ruby, but it needs to be convertible to JavaScript, and the ignore case flag will be set, e.g. "/regex pattern/i". + +Here are some examples: + +- Use `\.internal@domain\.com` to mark email addresses containing ".internal@domain.com" internal. +- Use `^(?:(?!\.ext@domain\.com).)*$\r?` to mark users with email addresses NOT including .ext@domain.com internal. + +Please be aware that this regex could lead to a DOS attack, [see](https://en.wikipedia.org/wiki/ReDoS?) ReDos on Wikipedia. + ## Auditor users **[PREMIUM ONLY]** >[Introduced][ee-998] in [GitLab Premium][eep] 8.17. diff --git a/doc/user/project/integrations/microsoft_teams.md b/doc/user/project/integrations/microsoft_teams.md index 5cf80a298ad..140c6738a49 100644 --- a/doc/user/project/integrations/microsoft_teams.md +++ b/doc/user/project/integrations/microsoft_teams.md @@ -2,7 +2,7 @@ ## On Microsoft Teams -To enable Microsoft Teams integration you must create an incoming webhook integration on Microsoft Teams by following the steps described in this [document](https://docs.microsoft.com/en-us/microsoftteams/platform/concepts/connectors#setting-up-a-custom-incoming-webhook). +To enable Microsoft Teams integration you must create an incoming webhook integration on Microsoft Teams by following the steps described in this [document](https://docs.microsoft.com/en-us/microsoftteams/platform/concepts/connectors/connectors-using#setting-up-a-custom-incoming-webhook). ## On GitLab diff --git a/lib/api/api_guard.rb b/lib/api/api_guard.rb index c17089759de..8ee7987cfff 100644 --- a/lib/api/api_guard.rb +++ b/lib/api/api_guard.rb @@ -84,7 +84,7 @@ module API end end - module ClassMethods + class_methods do private def install_error_responders(base) diff --git a/lib/api/projects_relation_builder.rb b/lib/api/projects_relation_builder.rb index 6482fd94ab8..9fd79c491c2 100644 --- a/lib/api/projects_relation_builder.rb +++ b/lib/api/projects_relation_builder.rb @@ -2,7 +2,7 @@ module API module ProjectsRelationBuilder extend ActiveSupport::Concern - module ClassMethods + class_methods do def prepare_relation(projects_relation, options = {}) projects_relation = preload_relation(projects_relation, options) execute_batch_counting(projects_relation) diff --git a/lib/gitlab/ci/status/build/failed.rb b/lib/gitlab/ci/status/build/failed.rb index 703f0b9217b..508b4814631 100644 --- a/lib/gitlab/ci/status/build/failed.rb +++ b/lib/gitlab/ci/status/build/failed.rb @@ -32,7 +32,7 @@ module Gitlab end def description - "<br> (#{failure_reason_message})" + "- (#{failure_reason_message})" end def failure_reason_message diff --git a/lib/gitlab/diff/highlight.rb b/lib/gitlab/diff/highlight.rb index 5c1baa19b66..1f012043e56 100644 --- a/lib/gitlab/diff/highlight.rb +++ b/lib/gitlab/diff/highlight.rb @@ -37,7 +37,7 @@ module Gitlab end end - diff_line.text = rich_line + diff_line.rich_text = rich_line diff_line end diff --git a/lib/gitlab/diff/line.rb b/lib/gitlab/diff/line.rb index 1faf7770634..1ab6df0b6ae 100644 --- a/lib/gitlab/diff/line.rb +++ b/lib/gitlab/diff/line.rb @@ -1,16 +1,17 @@ module Gitlab module Diff class Line - SERIALIZE_KEYS = %i(line_code text type index old_pos new_pos).freeze + SERIALIZE_KEYS = %i(line_code rich_text text type index old_pos new_pos).freeze attr_reader :line_code, :type, :index, :old_pos, :new_pos attr_writer :rich_text attr_accessor :text - def initialize(text, type, index, old_pos, new_pos, parent_file: nil, line_code: nil) + def initialize(text, type, index, old_pos, new_pos, parent_file: nil, line_code: nil, rich_text: nil) @text, @type, @index = text, type, index @old_pos, @new_pos = old_pos, new_pos @parent_file = parent_file + @rich_text = rich_text # When line code is not provided from cache store we build it # using the parent_file(Diff::File or Conflict::File). @@ -18,7 +19,7 @@ module Gitlab end def self.init_from_hash(hash) - new(hash[:text], hash[:type], hash[:index], hash[:old_pos], hash[:new_pos], line_code: hash[:line_code]) + new(hash[:text], hash[:type], hash[:index], hash[:old_pos], hash[:new_pos], line_code: hash[:line_code], rich_text: hash[:rich_text]) end def to_hash @@ -85,7 +86,7 @@ module Gitlab old_line: old_line, new_line: new_line, text: text, - rich_text: rich_text || text, + rich_text: rich_text || CGI.escapeHTML(text), meta_data: meta_positions } end diff --git a/lib/gitlab/encoding_helper.rb b/lib/gitlab/encoding_helper.rb index d1fd5dfe0cb..0f336fbaa10 100644 --- a/lib/gitlab/encoding_helper.rb +++ b/lib/gitlab/encoding_helper.rb @@ -75,7 +75,7 @@ module Gitlab end def binary_stringio(str) - StringIO.new(str || '').tap { |io| io.set_encoding(Encoding::ASCII_8BIT) } + StringIO.new(str.freeze || '').tap { |io| io.set_encoding(Encoding::ASCII_8BIT) } end private diff --git a/lib/gitlab/github_import/representation/expose_attribute.rb b/lib/gitlab/github_import/representation/expose_attribute.rb index c3405759631..d2438ee8094 100644 --- a/lib/gitlab/github_import/representation/expose_attribute.rb +++ b/lib/gitlab/github_import/representation/expose_attribute.rb @@ -6,7 +6,7 @@ module Gitlab module ExposeAttribute extend ActiveSupport::Concern - module ClassMethods + class_methods do # Defines getter methods for the given attribute names. # # Example: diff --git a/lib/gitlab/graphql/mount_mutation.rb b/lib/gitlab/graphql/mount_mutation.rb index 8cab84d7a5f..9048967d4e1 100644 --- a/lib/gitlab/graphql/mount_mutation.rb +++ b/lib/gitlab/graphql/mount_mutation.rb @@ -5,7 +5,7 @@ module Gitlab module MountMutation extend ActiveSupport::Concern - module ClassMethods + class_methods do def mount_mutation(mutation_class) # Using an underscored field name symbol will make `graphql-ruby` # standardize the field name diff --git a/lib/gitlab/import_export/uploads_manager.rb b/lib/gitlab/import_export/uploads_manager.rb index 07875ebb56a..e0d4235e65b 100644 --- a/lib/gitlab/import_export/uploads_manager.rb +++ b/lib/gitlab/import_export/uploads_manager.rb @@ -13,13 +13,11 @@ module Gitlab end def save - copy_files(@from, uploads_export_path) if File.directory?(@from) - if File.file?(@from) && @relative_export_path == 'avatar' copy_files(@from, File.join(uploads_export_path, @project.avatar.filename)) end - copy_from_object_storage + copy_project_uploads true rescue => e @@ -48,14 +46,19 @@ module Gitlab UploadService.new(@project, File.open(upload, 'r'), FileUploader, uploader_context).execute end - def copy_from_object_storage - return unless Gitlab::ImportExport.object_storage? - + def copy_project_uploads each_uploader do |uploader| next unless uploader.file - next if uploader.upload.local? # Already copied, using the old method - download_and_copy(uploader) + if uploader.upload.local? + next unless uploader.upload.exist? + + copy_files(uploader.absolute_path, File.join(uploads_export_path, uploader.upload.path)) + else + next unless Gitlab::ImportExport.object_storage? + + download_and_copy(uploader) + end end end diff --git a/lib/gitlab/url_blocker.rb b/lib/gitlab/url_blocker.rb index 38be75b7482..3b483f27e70 100644 --- a/lib/gitlab/url_blocker.rb +++ b/lib/gitlab/url_blocker.rb @@ -31,6 +31,7 @@ module Gitlab validate_localhost!(addrs_info) unless allow_localhost validate_local_network!(addrs_info) unless allow_local_network + validate_link_local!(addrs_info) unless allow_local_network true end @@ -89,6 +90,13 @@ module Gitlab raise BlockedUrlError, "Requests to the local network are not allowed" end + def validate_link_local!(addrs_info) + netmask = IPAddr.new('169.254.0.0/16') + return unless addrs_info.any? { |addr| addr.ipv6_linklocal? || netmask.include?(addr.ip_address) } + + raise BlockedUrlError, "Requests to the link local network are not allowed" + end + def internal?(uri) internal_web?(uri) || internal_shell?(uri) end diff --git a/lib/static_model.rb b/lib/static_model.rb index 60e2dd82e4e..44673c2b5f6 100644 --- a/lib/static_model.rb +++ b/lib/static_model.rb @@ -2,7 +2,7 @@ module StaticModel extend ActiveSupport::Concern - module ClassMethods + class_methods do # Used by ActiveRecord's polymorphic association to set object_id def primary_key 'id' diff --git a/lib/tasks/gettext.rake b/lib/tasks/gettext.rake index f431352b61e..a497d26312e 100644 --- a/lib/tasks/gettext.rake +++ b/lib/tasks/gettext.rake @@ -82,7 +82,7 @@ namespace :gettext do # `gettext:find` writes touches to temp files to `stderr` which would cause # `static-analysis` to report failures. We can ignore these. - silence_stream($stderr) do + silence_sdterr do Rake::Task['gettext:find'].invoke end @@ -118,4 +118,15 @@ namespace :gettext do end end end + + def silence_sdterr(&block) + old_stderr = $stderr.dup + $stderr.reopen(File::NULL) + $stderr.sync = true + + yield + ensure + $stderr.reopen(old_stderr) + old_stderr.close + end end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index ce5d82d479b..936b85146d4 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -727,6 +727,9 @@ msgstr "" msgid "AutoDevOps|enable Auto DevOps" msgstr "" +msgid "Automatically marked as default internal user" +msgstr "" + msgid "Available" msgstr "" @@ -2801,6 +2804,9 @@ msgstr "" msgid "GitLab.com import" msgstr "" +msgid "GitLab’s issue tracker" +msgstr "" + msgid "Gitaly" msgstr "" @@ -3187,6 +3193,9 @@ msgstr "" msgid "Internal - The project can be accessed by any logged in user." msgstr "" +msgid "Internal users" +msgstr "" + msgid "Interval Pattern" msgstr "" @@ -3387,6 +3396,9 @@ msgstr "" msgid "Learn more" msgstr "" +msgid "Learn more about %{issue_boards_url}, to keep track of issues in multiple lists, using labels, assignees, and milestones. If you’re missing something from issue boards, please create an issue on %{gitlab_issues_url}." +msgstr "" + msgid "Learn more about Kubernetes" msgstr "" @@ -4670,6 +4682,9 @@ msgid_plural "Refreshing in %d seconds to show the updated status..." msgstr[0] "" msgstr[1] "" +msgid "Regex pattern" +msgstr "" + msgid "Register / Sign In" msgstr "" @@ -5277,6 +5292,9 @@ msgstr "" msgid "Specific Runners" msgstr "" +msgid "Specify an e-mail address regex pattern to identify default internal users." +msgstr "" + msgid "Specify the following URL during the Runner setup:" msgstr "" @@ -5555,6 +5573,9 @@ msgstr "" msgid "The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time." msgstr "" +msgid "The tabs below will be removed in a future version" +msgstr "" + msgid "The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running." msgstr "" @@ -5934,6 +5955,9 @@ msgstr "" msgid "To add an SSH key you need to %{generate_link_start}generate one%{link_end} or use an %{existing_link_start}existing key%{link_end}." msgstr "" +msgid "To define internal users, first enable new users set to external" +msgstr "" + msgid "To get started you enter your FogBugz URL and login information below. In the next steps, you'll be able to map users and select the projects you want to import." msgstr "" @@ -6590,6 +6614,9 @@ msgstr "" msgid "importing" msgstr "" +msgid "issue boards" +msgstr "" + msgid "latest version" msgstr "" diff --git a/qa/qa/page/view.rb b/qa/qa/page/view.rb index b2a2da4dbf3..c59fad2e223 100644 --- a/qa/qa/page/view.rb +++ b/qa/qa/page/view.rb @@ -1,3 +1,5 @@ +require 'pathname' + module QA module Page class View @@ -9,7 +11,7 @@ module QA end def pathname - @pathname ||= Pathname.new(::File.join(__dir__, '../../../', @path)) + @pathname ||= ::Pathname.new(::File.join(__dir__, '../../../', @path)) .cleanpath.expand_path end diff --git a/qa/qa/scenario/template.rb b/qa/qa/scenario/template.rb index 66eb86f25c8..a87d925ce32 100644 --- a/qa/qa/scenario/template.rb +++ b/qa/qa/scenario/template.rb @@ -21,14 +21,18 @@ module QA def perform(address, *rspec_options) Runtime::Scenario.define(:gitlab_address, address) + ## + # Perform before hooks, which are different for CE and EE + # + Runtime::Release.perform_before_hooks + Specs::Runner.perform do |specs| specs.tty = true - specs.tags = self.class.focus specs.options = if rspec_options.any? rspec_options else - ::File.expand_path('../specs/features', __dir__) + ['--tag', self.class.focus.join(','), '--', ::File.expand_path('../specs/features', __dir__)] end end end diff --git a/qa/qa/scenario/test/instance.rb b/qa/qa/scenario/test/instance.rb index 2117a610efb..a2d503cc015 100644 --- a/qa/qa/scenario/test/instance.rb +++ b/qa/qa/scenario/test/instance.rb @@ -20,13 +20,18 @@ module QA def self.do_perform(address, *rspec_options) Runtime::Scenario.define(:gitlab_address, address) + ## + # Perform before hooks, which are different for CE and EE + # + Runtime::Release.perform_before_hooks + Specs::Runner.perform do |specs| specs.tty = true specs.options = if rspec_options.any? rspec_options else - ::File.expand_path('../../specs/features', __dir__) + ['--', ::File.expand_path('../../specs/features', __dir__)] end end end diff --git a/qa/qa/specs/features/api/1_manage/.gitkeep b/qa/qa/specs/features/api/1_manage/.gitkeep new file mode 100644 index 00000000000..e69de29bb2d --- /dev/null +++ b/qa/qa/specs/features/api/1_manage/.gitkeep diff --git a/qa/qa/specs/features/api/users_spec.rb b/qa/qa/specs/features/api/1_manage/users_spec.rb index 3d25cca1e59..3e3c9e859aa 100644 --- a/qa/qa/specs/features/api/users_spec.rb +++ b/qa/qa/specs/features/api/1_manage/users_spec.rb @@ -1,19 +1,21 @@ +# frozen_string_literal: true + module QA - describe 'API users' do - before(:context) do - @api_client = Runtime::API::Client.new(:gitlab) - end + context :manage do + describe 'Users API' do + before(:context) do + @api_client = Runtime::API::Client.new(:gitlab) + end - context 'when authenticated' do let(:request) { Runtime::API::Request.new(@api_client, '/users') } - it 'get list of users' do + it 'GET /users' do get request.url expect_status(200) end - it 'submit request with a valid user name' do + it 'GET /users/:username with a valid username' do get request.url, { params: { username: Runtime::User.username } } expect_status(200) @@ -22,20 +24,12 @@ module QA ) end - it 'submit request with an invalid user name' do + it 'GET /users/:username with an invalid username' do get request.url, { params: { username: SecureRandom.hex(10) } } expect_status(200) expect(json_body).to eq([]) end end - - it 'submit request with an invalid token' do - request = Runtime::API::Request.new(@api_client, '/users', private_token: 'invalid') - - get request.url - - expect_status(401) - end end end diff --git a/qa/qa/specs/features/api/2_plan/.gitkeep b/qa/qa/specs/features/api/2_plan/.gitkeep new file mode 100644 index 00000000000..e69de29bb2d --- /dev/null +++ b/qa/qa/specs/features/api/2_plan/.gitkeep diff --git a/qa/qa/specs/features/api/basics_spec.rb b/qa/qa/specs/features/api/3_create/repository/files_spec.rb index bc0b5ebfe10..bc0b5ebfe10 100644 --- a/qa/qa/specs/features/api/basics_spec.rb +++ b/qa/qa/specs/features/api/3_create/repository/files_spec.rb diff --git a/qa/qa/specs/features/api/4_verify/.gitkeep b/qa/qa/specs/features/api/4_verify/.gitkeep new file mode 100644 index 00000000000..e69de29bb2d --- /dev/null +++ b/qa/qa/specs/features/api/4_verify/.gitkeep diff --git a/qa/qa/specs/features/api/5_package/.gitkeep b/qa/qa/specs/features/api/5_package/.gitkeep new file mode 100644 index 00000000000..e69de29bb2d --- /dev/null +++ b/qa/qa/specs/features/api/5_package/.gitkeep diff --git a/qa/qa/specs/features/api/6_release/.gitkeep b/qa/qa/specs/features/api/6_release/.gitkeep new file mode 100644 index 00000000000..e69de29bb2d --- /dev/null +++ b/qa/qa/specs/features/api/6_release/.gitkeep diff --git a/qa/qa/specs/features/api/7_configure/.gitkeep b/qa/qa/specs/features/api/7_configure/.gitkeep new file mode 100644 index 00000000000..e69de29bb2d --- /dev/null +++ b/qa/qa/specs/features/api/7_configure/.gitkeep diff --git a/qa/qa/specs/features/browser_ui/1_manage/login/log_in_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/login/log_in_spec.rb new file mode 100644 index 00000000000..1c7da930567 --- /dev/null +++ b/qa/qa/specs/features/browser_ui/1_manage/login/log_in_spec.rb @@ -0,0 +1,17 @@ +module QA + context :manage, :smoke do + describe 'basic user login' do + it 'user logs in using basic credentials' do + Runtime::Browser.visit(:gitlab, Page::Main::Login) + Page::Main::Login.act { sign_in_using_credentials } + + # TODO, since `Signed in successfully` message was removed + # this is the only way to tell if user is signed in correctly. + # + Page::Menu::Main.perform do |menu| + expect(menu).to have_personal_area + end + end + end + end +end diff --git a/qa/qa/specs/features/browser_ui/1_manage/login/log_into_gitlab_via_ldap_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/login/log_into_gitlab_via_ldap_spec.rb new file mode 100644 index 00000000000..c9958917be9 --- /dev/null +++ b/qa/qa/specs/features/browser_ui/1_manage/login/log_into_gitlab_via_ldap_spec.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module QA + context :manage, :orchestrated, :ldap do + describe 'LDAP login' do + before do + Runtime::Env.user_type = 'ldap' + end + + it 'user logs into GitLab using LDAP credentials' do + Runtime::Browser.visit(:gitlab, Page::Main::Login) + Page::Main::Login.act { sign_in_using_credentials } + + # TODO, since `Signed in successfully` message was removed + # this is the only way to tell if user is signed in correctly. + # + Page::Menu::Main.perform do |menu| + expect(menu).to have_personal_area + end + end + end + end +end diff --git a/qa/qa/specs/features/browser_ui/1_manage/login/log_into_mattermost_via_gitlab_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/login/log_into_mattermost_via_gitlab_spec.rb new file mode 100644 index 00000000000..6eda2c750d4 --- /dev/null +++ b/qa/qa/specs/features/browser_ui/1_manage/login/log_into_mattermost_via_gitlab_spec.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module QA + context :manage, :orchestrated, :mattermost do + describe 'Mattermost login' do + it 'user logs into Mattermost using GitLab OAuth' do + Runtime::Browser.visit(:gitlab, Page::Main::Login) do + Page::Main::Login.act { sign_in_using_credentials } + + Runtime::Browser.visit(:mattermost, Page::Mattermost::Login) do + Page::Mattermost::Login.act { sign_in_using_oauth } + + Page::Mattermost::Main.perform do |page| + expect(page).to have_content(/(Welcome to: Mattermost|Logout GitLab Mattermost)/) + end + end + end + end + end + end +end diff --git a/qa/qa/specs/features/browser_ui/1_manage/project/create_project_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/project/create_project_spec.rb new file mode 100644 index 00000000000..bb1f3ab26d1 --- /dev/null +++ b/qa/qa/specs/features/browser_ui/1_manage/project/create_project_spec.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +module QA + context :manage, :smoke do + describe 'Project creation' do + it 'user creates a new project' do + Runtime::Browser.visit(:gitlab, Page::Main::Login) + Page::Main::Login.act { sign_in_using_credentials } + + created_project = Factory::Resource::Project.fabricate! do |project| + project.name = 'awesome-project' + project.description = 'create awesome project test' + end + + expect(created_project.name).to match /^awesome-project-\h{16}$/ + + expect(page).to have_content( + /Project \S?awesome-project\S+ was successfully created/ + ) + + expect(page).to have_content('create awesome project test') + expect(page).to have_content('The repository for this project is empty') + end + end + end +end diff --git a/qa/qa/specs/features/browser_ui/1_manage/project/import_github_repo_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/project/import_github_repo_spec.rb new file mode 100644 index 00000000000..2ef8de61441 --- /dev/null +++ b/qa/qa/specs/features/browser_ui/1_manage/project/import_github_repo_spec.rb @@ -0,0 +1,110 @@ +# frozen_string_literal: true + +module QA + context :manage, :orchestrated, :github do + describe 'Project import from GitHub' do + let(:imported_project) do + Factory::Resource::ProjectImportedFromGithub.fabricate! do |project| + project.name = 'imported-project' + project.personal_access_token = Runtime::Env.github_access_token + project.github_repository_path = 'gitlab-qa/test-project' + end + end + + after do + # We need to delete the imported project because it's impossible to import + # the same GitHub project twice for a given user. + api_client = Runtime::API::Client.new(:gitlab) + delete_project_request = Runtime::API::Request.new(api_client, "/projects/#{CGI.escape("#{Runtime::Namespace.path}/#{imported_project.name}")}") + delete delete_project_request.url + + expect_status(202) + end + + it 'user imports a GitHub repo' do + Runtime::Browser.visit(:gitlab, Page::Main::Login) + Page::Main::Login.act { sign_in_using_credentials } + + imported_project # import the project + + Page::Menu::Main.act { go_to_projects } + Page::Dashboard::Projects.perform do |dashboard| + dashboard.go_to_project(imported_project.name) + end + + Page::Project::Show.act { wait_for_import } + + verify_repository_import + verify_issues_import + verify_merge_requests_import + verify_labels_import + verify_milestones_import + verify_wiki_import + end + + def verify_repository_import + expect(page).to have_content('This test project is used for automated GitHub import by GitLab QA.') + expect(page).to have_content(imported_project.name) + end + + def verify_issues_import + Page::Menu::Side.act { click_issues } + expect(page).to have_content('This is a sample issue') + + click_link 'This is a sample issue' + + expect(page).to have_content('We should populate this project with issues, pull requests and wiki pages.') + + # Comments + expect(page).to have_content('This is a comment from @rymai.') + + Page::Issuable::Sidebar.perform do |issuable| + expect(issuable).to have_label('enhancement') + expect(issuable).to have_label('help wanted') + expect(issuable).to have_label('good first issue') + end + end + + def verify_merge_requests_import + Page::Menu::Side.act { click_merge_requests } + expect(page).to have_content('Improve README.md') + + click_link 'Improve README.md' + + expect(page).to have_content('This improves the README file a bit.') + + # Review comment are not supported yet + expect(page).not_to have_content('Really nice change.') + + # Comments + expect(page).to have_content('Nice work! This is a comment from @rymai.') + + # Diff comments + expect(page).to have_content('[Review comment] I like that!') + expect(page).to have_content('[Review comment] Nice blank line.') + expect(page).to have_content('[Single diff comment] Much better without this line!') + + Page::Issuable::Sidebar.perform do |issuable| + expect(issuable).to have_label('bug') + expect(issuable).to have_label('enhancement') + end + end + + def verify_labels_import + # TODO: Waiting on https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/19228 + # to build upon it. + end + + def verify_milestones_import + # TODO: Waiting on https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/18727 + # to build upon it. + end + + def verify_wiki_import + Page::Menu::Side.act { click_wiki } + + expect(page).to have_content('Welcome to the test-project wiki!') + end + end + end +end diff --git a/qa/qa/specs/features/browser_ui/1_manage/project/view_project_activity_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/project/view_project_activity_spec.rb new file mode 100644 index 00000000000..34bb6f1c197 --- /dev/null +++ b/qa/qa/specs/features/browser_ui/1_manage/project/view_project_activity_spec.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module QA + context :manage do + describe 'Project activity' do + it 'user creates an event in the activity page upon Git push' do + Runtime::Browser.visit(:gitlab, Page::Main::Login) + Page::Main::Login.act { sign_in_using_credentials } + + Factory::Repository::ProjectPush.fabricate! do |push| + push.file_name = 'README.md' + push.file_content = '# This is a test project' + push.commit_message = 'Add README.md' + end + + Page::Menu::Side.act { go_to_activity } + + Page::Project::Activity.act { go_to_push_events } + + expect(page).to have_content('pushed new branch master') + end + end + end +end diff --git a/qa/qa/specs/features/browser_ui/2_plan/issue/create_issue_spec.rb b/qa/qa/specs/features/browser_ui/2_plan/issue/create_issue_spec.rb new file mode 100644 index 00000000000..dd1be935220 --- /dev/null +++ b/qa/qa/specs/features/browser_ui/2_plan/issue/create_issue_spec.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module QA + context :plan, :smoke do + describe 'Issue creation' do + let(:issue_title) { 'issue title' } + + it 'user creates an issue' do + Runtime::Browser.visit(:gitlab, Page::Main::Login) + Page::Main::Login.act { sign_in_using_credentials } + + Factory::Resource::Issue.fabricate! do |issue| + issue.title = issue_title + end + + Page::Menu::Side.act { click_issues } + + expect(page).to have_content(issue_title) + end + end + end +end diff --git a/qa/qa/specs/features/browser_ui/3_create/merge_request/create_merge_request_spec.rb b/qa/qa/specs/features/browser_ui/3_create/merge_request/create_merge_request_spec.rb new file mode 100644 index 00000000000..bcf55a02a61 --- /dev/null +++ b/qa/qa/specs/features/browser_ui/3_create/merge_request/create_merge_request_spec.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +module QA + context :create do + describe 'Merge request creation' do + it 'user creates a new merge request' do + Runtime::Browser.visit(:gitlab, Page::Main::Login) + Page::Main::Login.act { sign_in_using_credentials } + + current_project = Factory::Resource::Project.fabricate! do |project| + project.name = 'project-with-merge-request-and-milestone' + end + + current_milestone = Factory::Resource::ProjectMilestone.fabricate! do |milestone| + milestone.title = 'unique-milestone' + milestone.project = current_project + end + + Factory::Resource::MergeRequest.fabricate! do |merge_request| + merge_request.title = 'This is a merge request with a milestone' + merge_request.description = 'Great feature with milestone' + merge_request.project = current_project + merge_request.milestone = current_milestone + end + + expect(page).to have_content('This is a merge request with a milestone') + expect(page).to have_content('Great feature with milestone') + expect(page).to have_content(/Opened [\w\s]+ ago/) + + Page::Issuable::Sidebar.perform do |sidebar| + expect(sidebar).to have_milestone(current_milestone.title) + end + end + end + end + + describe 'creates a merge request', :smoke do + it 'user creates a new merge request' do + Runtime::Browser.visit(:gitlab, Page::Main::Login) + Page::Main::Login.act { sign_in_using_credentials } + + current_project = Factory::Resource::Project.fabricate! do |project| + project.name = 'project-with-merge-request' + end + + Factory::Resource::MergeRequest.fabricate! do |merge_request| + merge_request.title = 'This is a merge request' + merge_request.description = 'Great feature' + merge_request.project = current_project + end + + expect(page).to have_content('This is a merge request') + expect(page).to have_content('Great feature') + expect(page).to have_content(/Opened [\w\s]+ ago/) + end + end +end diff --git a/qa/qa/specs/features/browser_ui/3_create/merge_request/merge_merge_request_from_fork_spec.rb b/qa/qa/specs/features/browser_ui/3_create/merge_request/merge_merge_request_from_fork_spec.rb new file mode 100644 index 00000000000..407a15800ab --- /dev/null +++ b/qa/qa/specs/features/browser_ui/3_create/merge_request/merge_merge_request_from_fork_spec.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module QA + context :create do + describe 'Merge request creation from fork' do + it 'user forks a project, submits a merge request and maintainer merges it' do + Runtime::Browser.visit(:gitlab, Page::Main::Login) + Page::Main::Login.act { sign_in_using_credentials } + + merge_request = Factory::Resource::MergeRequestFromFork.fabricate! do |merge_request| + merge_request.fork_branch = 'feature-branch' + end + + Page::Menu::Main.perform { |main| main.sign_out } + Page::Main::Login.perform { |login| login.sign_in_using_credentials } + + merge_request.visit! + + Page::MergeRequest::Show.perform { |show| show.merge! } + + expect(page).to have_content('The changes were merged') + end + end + end +end diff --git a/qa/qa/specs/features/browser_ui/3_create/merge_request/rebase_merge_request_spec.rb b/qa/qa/specs/features/browser_ui/3_create/merge_request/rebase_merge_request_spec.rb new file mode 100644 index 00000000000..ddcbc94b1b1 --- /dev/null +++ b/qa/qa/specs/features/browser_ui/3_create/merge_request/rebase_merge_request_spec.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +module QA + context :create do + describe 'Merge request rebasing' do + it 'user rebases source branch of merge request' do + Runtime::Browser.visit(:gitlab, Page::Main::Login) + Page::Main::Login.act { sign_in_using_credentials } + + project = Factory::Resource::Project.fabricate! do |project| + project.name = "only-fast-forward" + end + + Page::Menu::Side.act { go_to_settings } + Page::Project::Settings::MergeRequest.act { enable_ff_only } + + merge_request = Factory::Resource::MergeRequest.fabricate! do |merge_request| + merge_request.project = project + merge_request.title = 'Needs rebasing' + end + + Factory::Repository::ProjectPush.fabricate! do |push| + push.project = project + push.file_name = "other.txt" + push.file_content = "New file added!" + push.branch_name = "master" + push.new_branch = false + end + + merge_request.visit! + + Page::MergeRequest::Show.perform do |merge_request| + expect(merge_request).to have_content('Needs rebasing') + expect(merge_request).not_to be_fast_forward_possible + expect(merge_request).not_to have_merge_button + + merge_request.rebase! + + expect(merge_request).to have_merge_button + expect(merge_request.fast_forward_possible?).to be_truthy + end + end + end + end +end diff --git a/qa/qa/specs/features/browser_ui/3_create/merge_request/squash_merge_request_spec.rb b/qa/qa/specs/features/browser_ui/3_create/merge_request/squash_merge_request_spec.rb new file mode 100644 index 00000000000..b5b8855a35d --- /dev/null +++ b/qa/qa/specs/features/browser_ui/3_create/merge_request/squash_merge_request_spec.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +module QA + context :create do + describe 'Merge request squashing' do + it 'user squashes commits while merging' do + Runtime::Browser.visit(:gitlab, Page::Main::Login) + Page::Main::Login.act { sign_in_using_credentials } + + project = Factory::Resource::Project.fabricate! do |project| + project.name = "squash-before-merge" + end + + merge_request = Factory::Resource::MergeRequest.fabricate! do |merge_request| + merge_request.project = project + merge_request.title = 'Squashing commits' + end + + Factory::Repository::ProjectPush.fabricate! do |push| + push.project = project + push.commit_message = 'to be squashed' + push.branch_name = merge_request.source_branch + push.new_branch = false + push.file_name = 'other.txt' + push.file_content = "Test with unicode characters ❤✓€❄" + end + + merge_request.visit! + + expect(page).to have_text('to be squashed') + + Page::MergeRequest::Show.perform do |merge_request_page| + merge_request_page.mark_to_squash + merge_request_page.merge! + + merge_request.project.visit! + + Git::Repository.perform do |repository| + repository.uri = Page::Project::Show.act do + choose_repository_clone_http + repository_location.uri + end + + repository.use_default_credentials + + repository.act { clone } + + expect(repository.commits.size).to eq 3 + end + end + end + end + end +end diff --git a/qa/qa/specs/features/repository/clone_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/clone_spec.rb index 8b0613c5f78..b19bdd950fa 100644 --- a/qa/qa/specs/features/repository/clone_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/repository/clone_spec.rb @@ -1,6 +1,8 @@ +# frozen_string_literal: true + module QA - describe 'clone code from the repository' do - context 'with regular account over http' do + context :create do + describe 'Git clone over HTTP' do let(:location) do Page::Project::Show.act do choose_repository_clone_http diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/create_edit_delete_file_via_web_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/create_edit_delete_file_via_web_spec.rb new file mode 100644 index 00000000000..f18655442c1 --- /dev/null +++ b/qa/qa/specs/features/browser_ui/3_create/repository/create_edit_delete_file_via_web_spec.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +module QA + context :create, :core do + describe 'Files management' do + it 'user creates, edits and deletes a file via the Web' do + Runtime::Browser.visit(:gitlab, Page::Main::Login) + Page::Main::Login.act { sign_in_using_credentials } + + # Create + file_name = 'QA Test - File name' + file_content = 'QA Test - File content' + commit_message_for_create = 'QA Test - Create new file' + + Factory::Resource::File.fabricate! do |file| + file.name = file_name + file.content = file_content + file.commit_message = commit_message_for_create + end + + expect(page).to have_content('The file has been successfully created.') + expect(page).to have_content(file_name) + expect(page).to have_content(file_content) + expect(page).to have_content(commit_message_for_create) + + # Edit + updated_file_content = 'QA Test - Updated file content' + commit_message_for_update = 'QA Test - Update file' + + Page::File::Show.act { click_edit } + + Page::File::Form.act do + remove_content + add_content(updated_file_content) + add_commit_message(commit_message_for_update) + commit_changes + end + + expect(page).to have_content('Your changes have been successfully committed.') + expect(page).to have_content(updated_file_content) + expect(page).to have_content(commit_message_for_update) + + # Delete + commit_message_for_delete = 'QA Test - Delete file' + + Page::File::Show.act do + click_delete + add_commit_message(commit_message_for_delete) + click_delete_file + end + + expect(page).to have_content('The file has been successfully deleted.') + expect(page).to have_content(commit_message_for_delete) + expect(page).to have_no_content(file_name) + end + end + end +end diff --git a/qa/qa/specs/features/repository/push_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_spec.rb index 1e89942e932..40dfd138a1b 100644 --- a/qa/qa/specs/features/repository/push_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_spec.rb @@ -1,6 +1,8 @@ +# frozen_string_literal: true + module QA - describe 'push code to repository' do - context 'with regular account over http' do + context :create do + describe 'Git push over HTTP' do it 'user pushes code to the repository' do Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/push_protected_branch_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/push_protected_branch_spec.rb new file mode 100644 index 00000000000..1d9cc33080d --- /dev/null +++ b/qa/qa/specs/features/browser_ui/3_create/repository/push_protected_branch_spec.rb @@ -0,0 +1,70 @@ +# frozen_string_literal: true + +module QA + context :create do + describe 'Protected branch support' do + let(:branch_name) { 'protected-branch' } + let(:commit_message) { 'Protected push commit message' } + let(:project) do + Factory::Resource::Project.fabricate! do |resource| + resource.name = 'protected-branch-project' + end + end + + before do + Runtime::Browser.visit(:gitlab, Page::Main::Login) + Page::Main::Login.act { sign_in_using_credentials } + end + + after do + # We need to clear localStorage because we're using it for the dropdown, + # and capybara doesn't do this for us. + # https://github.com/teamcapybara/capybara/issues/1702 + Capybara.execute_script 'localStorage.clear()' + end + + context 'when developers and maintainers are allowed to push to a protected branch' do + it 'user with push rights successfully pushes to the protected branch' do + create_protected_branch(allow_to_push: true) + + push = push_new_file(branch_name) + + expect(push.output).to match(/remote: To create a merge request for protected-branch, visit/) + end + end + + context 'when developers and maintainers are not allowed to push to a protected branch' do + it 'user without push rights fails to push to the protected branch' do + create_protected_branch(allow_to_push: false) + + push = push_new_file(branch_name) + + expect(push.output) + .to match(/remote\: GitLab\: You are not allowed to push code to protected branches on this project/) + expect(push.output) + .to match(/\[remote rejected\] #{branch_name} -> #{branch_name} \(pre-receive hook declined\)/) + end + end + + def create_protected_branch(allow_to_push:) + Factory::Resource::Branch.fabricate! do |resource| + resource.branch_name = branch_name + resource.project = project + resource.allow_to_push = allow_to_push + resource.protected = true + end + end + + def push_new_file(branch) + Factory::Repository::ProjectPush.fabricate! do |resource| + resource.project = project + resource.file_name = 'new_file.md' + resource.file_content = '# This is a new file' + resource.commit_message = 'Add new_file.md' + resource.branch_name = branch_name + resource.new_branch = false + end + end + end + end +end diff --git a/qa/qa/specs/features/browser_ui/3_create/wiki/create_edit_clone_push_wiki_spec.rb b/qa/qa/specs/features/browser_ui/3_create/wiki/create_edit_clone_push_wiki_spec.rb new file mode 100644 index 00000000000..8009b9e8609 --- /dev/null +++ b/qa/qa/specs/features/browser_ui/3_create/wiki/create_edit_clone_push_wiki_spec.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +module QA + context :create do + describe 'Wiki management' do + def login + Runtime::Browser.visit(:gitlab, Page::Main::Login) + Page::Main::Login.act { sign_in_using_credentials } + end + + def validate_content(content) + expect(page).to have_content('Wiki was successfully updated') + expect(page).to have_content(/#{content}/) + end + + before do + login + end + + it 'user creates, edits, clones, and pushes to the wiki' do + wiki = Factory::Resource::Wiki.fabricate! do |resource| + resource.title = 'Home' + resource.content = '# My First Wiki Content' + resource.message = 'Update home' + end + + validate_content('My First Wiki Content') + + Page::Project::Wiki::Edit.act { go_to_edit_page } + Page::Project::Wiki::New.perform do |page| + page.set_content("My Second Wiki Content") + page.save_changes + end + + validate_content('My Second Wiki Content') + + Factory::Repository::WikiPush.fabricate! do |push| + push.wiki = wiki + push.file_name = 'Home.md' + push.file_content = '# My Third Wiki Content' + push.commit_message = 'Update Home.md' + end + Page::Menu::Side.act { click_wiki } + + expect(page).to have_content('My Third Wiki Content') + end + end + end +end diff --git a/qa/qa/specs/features/browser_ui/4_verify/pipeline/create_and_process_pipeline_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/pipeline/create_and_process_pipeline_spec.rb new file mode 100644 index 00000000000..cdfe9b90e15 --- /dev/null +++ b/qa/qa/specs/features/browser_ui/4_verify/pipeline/create_and_process_pipeline_spec.rb @@ -0,0 +1,87 @@ +# frozen_string_literal: true + +module QA + context :verify, :orchestrated, :docker do + describe 'Pipeline creation and processing' do + let(:executor) { "qa-runner-#{Time.now.to_i}" } + + after do + Service::Runner.new(executor).remove! + end + + it 'users creates a pipeline which gets processed' do + Runtime::Browser.visit(:gitlab, Page::Main::Login) + Page::Main::Login.act { sign_in_using_credentials } + + project = Factory::Resource::Project.fabricate! do |project| + project.name = 'project-with-pipelines' + project.description = 'Project with CI/CD Pipelines.' + end + + Factory::Resource::Runner.fabricate! do |runner| + runner.project = project + runner.name = executor + runner.tags = %w[qa test] + end + + Factory::Repository::ProjectPush.fabricate! do |push| + push.project = project + push.file_name = '.gitlab-ci.yml' + push.commit_message = 'Add .gitlab-ci.yml' + push.file_content = <<~EOF + test-success: + tags: + - qa + - test + script: echo 'OK' + + test-failure: + tags: + - qa + - test + script: + - echo 'FAILURE' + - exit 1 + + test-tags: + tags: + - qa + - docker + script: echo 'NOOP' + + test-artifacts: + tags: + - qa + - test + script: mkdir my-artifacts; echo "CONTENTS" > my-artifacts/artifact.txt + artifacts: + paths: + - my-artifacts/ + EOF + end + + Page::Project::Show.act { wait_for_push } + + expect(page).to have_content('Add .gitlab-ci.yml') + + Page::Menu::Side.act { click_ci_cd_pipelines } + + expect(page).to have_content('All 1') + expect(page).to have_content('Add .gitlab-ci.yml') + + puts 'Waiting for the runner to process the pipeline' + sleep 15 # Runner should process all jobs within 15 seconds. + + Page::Project::Pipeline::Index.act { go_to_latest_pipeline } + + Page::Project::Pipeline::Show.perform do |pipeline| + expect(pipeline).to be_running + expect(pipeline).to have_build('test-success', status: :success) + expect(pipeline).to have_build('test-failure', status: :failed) + expect(pipeline).to have_build('test-tags', status: :pending) + expect(pipeline).to have_build('test-artifacts', status: :success) + end + end + end + end +end diff --git a/qa/qa/specs/features/browser_ui/4_verify/runner/register_runner_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/runner/register_runner_spec.rb new file mode 100644 index 00000000000..8d83a20f5bf --- /dev/null +++ b/qa/qa/specs/features/browser_ui/4_verify/runner/register_runner_spec.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +module QA + context :verify, :docker do + describe 'Runner registration' do + let(:executor) { "qa-runner-#{Time.now.to_i}" } + + after do + Service::Runner.new(executor).remove! + end + + it 'user registers a new specific runner' do + Runtime::Browser.visit(:gitlab, Page::Main::Login) + Page::Main::Login.act { sign_in_using_credentials } + + Factory::Resource::Runner.fabricate! do |runner| + runner.name = executor + end + + Page::Project::Settings::CICD.perform do |settings| + sleep 5 # Runner should register within 5 seconds + settings.refresh + + settings.expand_runners_settings do |page| + expect(page).to have_content(executor) + expect(page).to have_online_runner + end + end + end + end + end +end diff --git a/qa/qa/specs/features/browser_ui/4_verify/secret_variable/add_secret_variable_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/secret_variable/add_secret_variable_spec.rb new file mode 100644 index 00000000000..08a87df5837 --- /dev/null +++ b/qa/qa/specs/features/browser_ui/4_verify/secret_variable/add_secret_variable_spec.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +module QA + context :verify do + describe 'Secret variable support' do + it 'user adds a secret variable' do + Runtime::Browser.visit(:gitlab, Page::Main::Login) + Page::Main::Login.act { sign_in_using_credentials } + + Factory::Resource::SecretVariable.fabricate! do |resource| + resource.key = 'VARIABLE_KEY' + resource.value = 'some secret variable' + end + + Page::Project::Settings::CICD.perform do |settings| + settings.expand_secret_variables do |page| + expect(page).to have_field(with: 'VARIABLE_KEY') + expect(page).not_to have_field(with: 'some secret variable') + + page.reveal_variables + + expect(page).to have_field(with: 'some secret variable') + end + end + end + end + end +end diff --git a/qa/qa/specs/features/browser_ui/5_package/.gitkeep b/qa/qa/specs/features/browser_ui/5_package/.gitkeep new file mode 100644 index 00000000000..e69de29bb2d --- /dev/null +++ b/qa/qa/specs/features/browser_ui/5_package/.gitkeep diff --git a/qa/qa/specs/features/browser_ui/6_release/deploy_key/add_deploy_key_spec.rb b/qa/qa/specs/features/browser_ui/6_release/deploy_key/add_deploy_key_spec.rb new file mode 100644 index 00000000000..17dfa887434 --- /dev/null +++ b/qa/qa/specs/features/browser_ui/6_release/deploy_key/add_deploy_key_spec.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module QA + context :release do + describe 'Deploy key creation' do + it 'user adds a deploy key' do + Runtime::Browser.visit(:gitlab, Page::Main::Login) + Page::Main::Login.act { sign_in_using_credentials } + + key = Runtime::Key::RSA.new + deploy_key_title = 'deploy key title' + deploy_key_value = key.public_key + + deploy_key = Factory::Resource::DeployKey.fabricate! do |resource| + resource.title = deploy_key_title + resource.key = deploy_key_value + end + + expect(deploy_key.fingerprint).to eq(key.fingerprint) + end + end + end +end diff --git a/qa/qa/specs/features/browser_ui/6_release/deploy_key/clone_using_deploy_key_spec.rb b/qa/qa/specs/features/browser_ui/6_release/deploy_key/clone_using_deploy_key_spec.rb new file mode 100644 index 00000000000..8352d13b06d --- /dev/null +++ b/qa/qa/specs/features/browser_ui/6_release/deploy_key/clone_using_deploy_key_spec.rb @@ -0,0 +1,109 @@ +# frozen_string_literal: true + +require 'digest/sha1' + +module QA + context :release, :docker do + describe 'Git clone using a deploy key' do + def login + Runtime::Browser.visit(:gitlab, Page::Main::Login) + Page::Main::Login.act { sign_in_using_credentials } + end + + before(:all) do + login + + @runner_name = "qa-runner-#{Time.now.to_i}" + + @project = Factory::Resource::Project.fabricate! do |resource| + resource.name = 'deploy-key-clone-project' + end + + @repository_location = @project.repository_ssh_location + + Factory::Resource::Runner.fabricate! do |resource| + resource.project = @project + resource.name = @runner_name + resource.tags = %w[qa docker] + resource.image = 'gitlab/gitlab-runner:ubuntu' + end + + Page::Menu::Main.act { sign_out } + end + + after(:all) do + Service::Runner.new(@runner_name).remove! + end + + keys = [ + [Runtime::Key::RSA, 8192], + [Runtime::Key::ECDSA, 521], + [Runtime::Key::ED25519] + ] + + keys.each do |(key_class, bits)| + it "user sets up a deploy key with #{key_class}(#{bits}) to clone code using pipelines" do + key = key_class.new(*bits) + + login + + Factory::Resource::DeployKey.fabricate! do |resource| + resource.project = @project + resource.title = "deploy key #{key.name}(#{key.bits})" + resource.key = key.public_key + end + + deploy_key_name = "DEPLOY_KEY_#{key.name}_#{key.bits}" + + Factory::Resource::SecretVariable.fabricate! do |resource| + resource.project = @project + resource.key = deploy_key_name + resource.value = key.private_key + end + + gitlab_ci = <<~YAML + cat-config: + script: + - mkdir -p ~/.ssh + - ssh-keyscan -p #{@repository_location.port} #{@repository_location.host} >> ~/.ssh/known_hosts + - eval $(ssh-agent -s) + - ssh-add -D + - echo "$#{deploy_key_name}" | ssh-add - + - git clone #{@repository_location.git_uri} + - cd #{@project.name} + - git checkout #{deploy_key_name} + - sha1sum .gitlab-ci.yml + tags: + - qa + - docker + YAML + + Factory::Repository::ProjectPush.fabricate! do |resource| + resource.project = @project + resource.file_name = '.gitlab-ci.yml' + resource.commit_message = 'Add .gitlab-ci.yml' + resource.file_content = gitlab_ci + resource.branch_name = deploy_key_name + resource.new_branch = true + end + + sha1sum = Digest::SHA1.hexdigest(gitlab_ci) + + Page::Project::Show.act { wait_for_push } + Page::Menu::Side.act { click_ci_cd_pipelines } + Page::Project::Pipeline::Index.act { go_to_latest_pipeline } + Page::Project::Pipeline::Show.act { go_to_first_job } + + Page::Project::Job::Show.perform do |job| + job.wait(reload: false) do + job.completed? && !job.trace_loading? + end + + expect(job.passed?).to be_truthy, "Job status did not become \"passed\"." + expect(job.output).to include(sha1sum) + end + end + end + end + end +end diff --git a/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb b/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb new file mode 100644 index 00000000000..dd24e8ffba5 --- /dev/null +++ b/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb @@ -0,0 +1,68 @@ +# frozen_string_literal: true + +require 'pathname' + +module QA + context :configure, :orchestrated, :kubernetes do + describe 'Auto DevOps support' do + after do + @cluster&.remove! + end + + it 'user creates a new project and runs auto devops' do + Runtime::Browser.visit(:gitlab, Page::Main::Login) + Page::Main::Login.act { sign_in_using_credentials } + + project = Factory::Resource::Project.fabricate! do |p| + p.name = 'project-with-autodevops' + p.description = 'Project with Auto Devops' + end + + # Disable code_quality check in Auto DevOps pipeline as it takes + # too long and times out the test + Factory::Resource::SecretVariable.fabricate! do |resource| + resource.key = 'CODE_QUALITY_DISABLED' + resource.value = '1' + end + + # Create Auto Devops compatible repo + Factory::Repository::ProjectPush.fabricate! do |push| + push.project = project + push.directory = Pathname + .new(__dir__) + .join('../../../fixtures/auto_devops_rack') + push.commit_message = 'Create Auto DevOps compatible rack application' + end + + Page::Project::Show.act { wait_for_push } + + # Create and connect K8s cluster + @cluster = Service::KubernetesCluster.new.create! + kubernetes_cluster = Factory::Resource::KubernetesCluster.fabricate! do |cluster| + cluster.project = project + cluster.cluster = @cluster + cluster.install_helm_tiller = true + cluster.install_ingress = true + cluster.install_prometheus = true + cluster.install_runner = true + end + + project.visit! + Page::Menu::Side.act { click_ci_cd_settings } + Page::Project::Settings::CICD.perform do |p| + p.enable_auto_devops_with_domain("#{kubernetes_cluster.ingress_ip}.nip.io") + end + + project.visit! + Page::Menu::Side.act { click_ci_cd_pipelines } + Page::Project::Pipeline::Index.act { go_to_latest_pipeline } + + Page::Project::Pipeline::Show.perform do |pipeline| + expect(pipeline).to have_build('build', status: :success, wait: 600) + expect(pipeline).to have_build('test', status: :success, wait: 600) + expect(pipeline).to have_build('production', status: :success, wait: 1200) + end + end + end + end +end diff --git a/qa/qa/specs/features/browser_ui/7_configure/mattermost/create_group_with_mattermost_team_spec.rb b/qa/qa/specs/features/browser_ui/7_configure/mattermost/create_group_with_mattermost_team_spec.rb new file mode 100644 index 00000000000..6ffdc55538a --- /dev/null +++ b/qa/qa/specs/features/browser_ui/7_configure/mattermost/create_group_with_mattermost_team_spec.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module QA + context :configure, :orchestrated, :mattermost do + describe 'Mattermost support' do + it 'user creates a group with a mattermost team' do + Runtime::Browser.visit(:gitlab, Page::Main::Login) + Page::Main::Login.act { sign_in_using_credentials } + Page::Menu::Main.act { go_to_groups } + + Page::Dashboard::Groups.perform do |page| + page.go_to_new_group + + expect(page).to have_content( + /Create a Mattermost team for this group/ + ) + end + end + end + end +end diff --git a/qa/qa/specs/features/login/basic_spec.rb b/qa/qa/specs/features/login/basic_spec.rb deleted file mode 100644 index f866466c7bf..00000000000 --- a/qa/qa/specs/features/login/basic_spec.rb +++ /dev/null @@ -1,15 +0,0 @@ -module QA - describe 'basic user login', :smoke do - it 'user logs in using basic credentials' do - Runtime::Browser.visit(:gitlab, Page::Main::Login) - Page::Main::Login.act { sign_in_using_credentials } - - # TODO, since `Signed in successfully` message was removed - # this is the only way to tell if user is signed in correctly. - # - Page::Menu::Main.perform do |menu| - expect(menu).to have_personal_area - end - end - end -end diff --git a/qa/qa/specs/features/login/ldap_spec.rb b/qa/qa/specs/features/login/ldap_spec.rb deleted file mode 100644 index de6111eea64..00000000000 --- a/qa/qa/specs/features/login/ldap_spec.rb +++ /dev/null @@ -1,19 +0,0 @@ -module QA - describe 'LDAP user login', :orchestrated, :ldap do - before do - Runtime::Env.user_type = 'ldap' - end - - it 'user logs in using LDAP credentials' do - Runtime::Browser.visit(:gitlab, Page::Main::Login) - Page::Main::Login.act { sign_in_using_credentials } - - # TODO, since `Signed in successfully` message was removed - # this is the only way to tell if user is signed in correctly. - # - Page::Menu::Main.perform do |menu| - expect(menu).to have_personal_area - end - end - end -end diff --git a/qa/qa/specs/features/mattermost/group_create_spec.rb b/qa/qa/specs/features/mattermost/group_create_spec.rb deleted file mode 100644 index 097e1713aef..00000000000 --- a/qa/qa/specs/features/mattermost/group_create_spec.rb +++ /dev/null @@ -1,17 +0,0 @@ -module QA - describe 'create a new group', :orchestrated, :mattermost do - it 'creating a group with a mattermost team' do - Runtime::Browser.visit(:gitlab, Page::Main::Login) - Page::Main::Login.act { sign_in_using_credentials } - Page::Menu::Main.act { go_to_groups } - - Page::Dashboard::Groups.perform do |page| - page.go_to_new_group - - expect(page).to have_content( - /Create a Mattermost team for this group/ - ) - end - end - end -end diff --git a/qa/qa/specs/features/mattermost/login_spec.rb b/qa/qa/specs/features/mattermost/login_spec.rb deleted file mode 100644 index 27f7d4c245f..00000000000 --- a/qa/qa/specs/features/mattermost/login_spec.rb +++ /dev/null @@ -1,17 +0,0 @@ -module QA - describe 'logging in to Mattermost', :orchestrated, :mattermost do - it 'can use gitlab oauth' do - Runtime::Browser.visit(:gitlab, Page::Main::Login) do - Page::Main::Login.act { sign_in_using_credentials } - - Runtime::Browser.visit(:mattermost, Page::Mattermost::Login) do - Page::Mattermost::Login.act { sign_in_using_oauth } - - Page::Mattermost::Main.perform do |page| - expect(page).to have_content(/(Welcome to: Mattermost|Logout GitLab Mattermost)/) - end - end - end - end - end -end diff --git a/qa/qa/specs/features/merge_request/create_spec.rb b/qa/qa/specs/features/merge_request/create_spec.rb deleted file mode 100644 index 71e79956b85..00000000000 --- a/qa/qa/specs/features/merge_request/create_spec.rb +++ /dev/null @@ -1,53 +0,0 @@ -module QA - describe 'creates a merge request with milestone' do - it 'user creates a new merge request' do - Runtime::Browser.visit(:gitlab, Page::Main::Login) - Page::Main::Login.act { sign_in_using_credentials } - - current_project = Factory::Resource::Project.fabricate! do |project| - project.name = 'project-with-merge-request-and-milestone' - end - - current_milestone = Factory::Resource::ProjectMilestone.fabricate! do |milestone| - milestone.title = 'unique-milestone' - milestone.project = current_project - end - - Factory::Resource::MergeRequest.fabricate! do |merge_request| - merge_request.title = 'This is a merge request with a milestone' - merge_request.description = 'Great feature with milestone' - merge_request.project = current_project - merge_request.milestone = current_milestone - end - - expect(page).to have_content('This is a merge request with a milestone') - expect(page).to have_content('Great feature with milestone') - expect(page).to have_content(/Opened [\w\s]+ ago/) - - Page::Issuable::Sidebar.perform do |sidebar| - expect(sidebar).to have_milestone(current_milestone.title) - end - end - end - - describe 'creates a merge request', :smoke do - it 'user creates a new merge request' do - Runtime::Browser.visit(:gitlab, Page::Main::Login) - Page::Main::Login.act { sign_in_using_credentials } - - current_project = Factory::Resource::Project.fabricate! do |project| - project.name = 'project-with-merge-request' - end - - Factory::Resource::MergeRequest.fabricate! do |merge_request| - merge_request.title = 'This is a merge request' - merge_request.description = 'Great feature' - merge_request.project = current_project - end - - expect(page).to have_content('This is a merge request') - expect(page).to have_content('Great feature') - expect(page).to have_content(/Opened [\w\s]+ ago/) - end - end -end diff --git a/qa/qa/specs/features/merge_request/rebase_spec.rb b/qa/qa/specs/features/merge_request/rebase_spec.rb deleted file mode 100644 index c36d28e4237..00000000000 --- a/qa/qa/specs/features/merge_request/rebase_spec.rb +++ /dev/null @@ -1,39 +0,0 @@ -module QA - describe 'merge request rebase' do - it 'rebases source branch of merge request' do - Runtime::Browser.visit(:gitlab, Page::Main::Login) - Page::Main::Login.act { sign_in_using_credentials } - - project = Factory::Resource::Project.fabricate! do |project| - project.name = "only-fast-forward" - end - - Page::Menu::Side.act { go_to_settings } - Page::Project::Settings::MergeRequest.act { enable_ff_only } - - merge_request = Factory::Resource::MergeRequest.fabricate! do |merge_request| - merge_request.project = project - merge_request.title = 'Needs rebasing' - end - - Factory::Repository::ProjectPush.fabricate! do |push| - push.project = project - push.file_name = "other.txt" - push.file_content = "New file added!" - end - - merge_request.visit! - - Page::MergeRequest::Show.perform do |merge_request| - expect(merge_request).to have_content('Needs rebasing') - expect(merge_request).not_to be_fast_forward_possible - expect(merge_request).not_to have_merge_button - - merge_request.rebase! - - expect(merge_request).to have_merge_button - expect(merge_request.fast_forward_possible?).to be_truthy - end - end - end -end diff --git a/qa/qa/specs/features/merge_request/squash_spec.rb b/qa/qa/specs/features/merge_request/squash_spec.rb deleted file mode 100644 index 3ecc36a5ae1..00000000000 --- a/qa/qa/specs/features/merge_request/squash_spec.rb +++ /dev/null @@ -1,50 +0,0 @@ -module QA - describe 'merge request squash commits' do - it 'when squash commits is marked before merge' do - Runtime::Browser.visit(:gitlab, Page::Main::Login) - Page::Main::Login.act { sign_in_using_credentials } - - project = Factory::Resource::Project.fabricate! do |project| - project.name = "squash-before-merge" - end - - merge_request = Factory::Resource::MergeRequest.fabricate! do |merge_request| - merge_request.project = project - merge_request.title = 'Squashing commits' - end - - Factory::Repository::ProjectPush.fabricate! do |push| - push.project = project - push.commit_message = 'to be squashed' - push.branch_name = merge_request.source_branch - push.new_branch = false - push.file_name = 'other.txt' - push.file_content = "Test with unicode characters ❤✓€❄" - end - - merge_request.visit! - - expect(page).to have_text('to be squashed') - - Page::MergeRequest::Show.perform do |merge_request_page| - merge_request_page.mark_to_squash - merge_request_page.merge! - - merge_request.project.visit! - - Git::Repository.perform do |repository| - repository.uri = Page::Project::Show.act do - choose_repository_clone_http - repository_location.uri - end - - repository.use_default_credentials - - repository.act { clone } - - expect(repository.commits.size).to eq 3 - end - end - end - end -end diff --git a/qa/qa/specs/features/project/activity_spec.rb b/qa/qa/specs/features/project/activity_spec.rb deleted file mode 100644 index c7ce8dfdcc6..00000000000 --- a/qa/qa/specs/features/project/activity_spec.rb +++ /dev/null @@ -1,20 +0,0 @@ -module QA - describe 'activity page' do - it 'push creates an event in the activity page' do - Runtime::Browser.visit(:gitlab, Page::Main::Login) - Page::Main::Login.act { sign_in_using_credentials } - - Factory::Repository::ProjectPush.fabricate! do |push| - push.file_name = 'README.md' - push.file_content = '# This is a test project' - push.commit_message = 'Add README.md' - end - - Page::Menu::Side.act { go_to_activity } - - Page::Project::Activity.act { go_to_push_events } - - expect(page).to have_content('pushed new branch master') - end - end -end diff --git a/qa/qa/specs/features/project/add_deploy_key_spec.rb b/qa/qa/specs/features/project/add_deploy_key_spec.rb deleted file mode 100644 index 24f9f4c77f8..00000000000 --- a/qa/qa/specs/features/project/add_deploy_key_spec.rb +++ /dev/null @@ -1,19 +0,0 @@ -module QA - describe 'deploy keys support' do - it 'user adds a deploy key' do - Runtime::Browser.visit(:gitlab, Page::Main::Login) - Page::Main::Login.act { sign_in_using_credentials } - - key = Runtime::Key::RSA.new - deploy_key_title = 'deploy key title' - deploy_key_value = key.public_key - - deploy_key = Factory::Resource::DeployKey.fabricate! do |resource| - resource.title = deploy_key_title - resource.key = deploy_key_value - end - - expect(deploy_key.fingerprint).to eq(key.fingerprint) - end - end -end diff --git a/qa/qa/specs/features/project/add_secret_variable_spec.rb b/qa/qa/specs/features/project/add_secret_variable_spec.rb deleted file mode 100644 index 04d9fe488e2..00000000000 --- a/qa/qa/specs/features/project/add_secret_variable_spec.rb +++ /dev/null @@ -1,24 +0,0 @@ -module QA - describe 'secret variables support' do - it 'user adds a secret variable' do - Runtime::Browser.visit(:gitlab, Page::Main::Login) - Page::Main::Login.act { sign_in_using_credentials } - - Factory::Resource::SecretVariable.fabricate! do |resource| - resource.key = 'VARIABLE_KEY' - resource.value = 'some secret variable' - end - - Page::Project::Settings::CICD.perform do |settings| - settings.expand_secret_variables do |page| - expect(page).to have_field(with: 'VARIABLE_KEY') - expect(page).not_to have_field(with: 'some secret variable') - - page.reveal_variables - - expect(page).to have_field(with: 'some secret variable') - end - end - end - end -end diff --git a/qa/qa/specs/features/project/auto_devops_spec.rb b/qa/qa/specs/features/project/auto_devops_spec.rb deleted file mode 100644 index 248669b6046..00000000000 --- a/qa/qa/specs/features/project/auto_devops_spec.rb +++ /dev/null @@ -1,64 +0,0 @@ -require 'pathname' - -module QA - describe 'Auto Devops', :orchestrated, :kubernetes do - after do - @cluster&.remove! - end - - it 'user creates a new project and runs auto devops' do - Runtime::Browser.visit(:gitlab, Page::Main::Login) - Page::Main::Login.act { sign_in_using_credentials } - - project = Factory::Resource::Project.fabricate! do |p| - p.name = 'project-with-autodevops' - p.description = 'Project with Auto Devops' - end - - # Disable code_quality check in Auto DevOps pipeline as it takes - # too long and times out the test - Factory::Resource::SecretVariable.fabricate! do |resource| - resource.key = 'CODE_QUALITY_DISABLED' - resource.value = '1' - end - - # Create Auto Devops compatible repo - Factory::Repository::ProjectPush.fabricate! do |push| - push.project = project - push.directory = Pathname - .new(__dir__) - .join('../../../fixtures/auto_devops_rack') - push.commit_message = 'Create Auto DevOps compatible rack application' - end - - Page::Project::Show.act { wait_for_push } - - # Create and connect K8s cluster - @cluster = Service::KubernetesCluster.new.create! - kubernetes_cluster = Factory::Resource::KubernetesCluster.fabricate! do |cluster| - cluster.project = project - cluster.cluster = @cluster - cluster.install_helm_tiller = true - cluster.install_ingress = true - cluster.install_prometheus = true - cluster.install_runner = true - end - - project.visit! - Page::Menu::Side.act { click_ci_cd_settings } - Page::Project::Settings::CICD.perform do |p| - p.enable_auto_devops_with_domain("#{kubernetes_cluster.ingress_ip}.nip.io") - end - - project.visit! - Page::Menu::Side.act { click_ci_cd_pipelines } - Page::Project::Pipeline::Index.act { go_to_latest_pipeline } - - Page::Project::Pipeline::Show.perform do |pipeline| - expect(pipeline).to have_build('build', status: :success, wait: 600) - expect(pipeline).to have_build('test', status: :success, wait: 600) - expect(pipeline).to have_build('production', status: :success, wait: 1200) - end - end - end -end diff --git a/qa/qa/specs/features/project/create_issue_spec.rb b/qa/qa/specs/features/project/create_issue_spec.rb deleted file mode 100644 index 793e7db87cb..00000000000 --- a/qa/qa/specs/features/project/create_issue_spec.rb +++ /dev/null @@ -1,18 +0,0 @@ -module QA - describe 'creates issue', :smoke do - let(:issue_title) { 'issue title' } - - it 'user creates issue' do - Runtime::Browser.visit(:gitlab, Page::Main::Login) - Page::Main::Login.act { sign_in_using_credentials } - - Factory::Resource::Issue.fabricate! do |issue| - issue.title = issue_title - end - - Page::Menu::Side.act { click_issues } - - expect(page).to have_content(issue_title) - end - end -end diff --git a/qa/qa/specs/features/project/create_spec.rb b/qa/qa/specs/features/project/create_spec.rb deleted file mode 100644 index 5e19e490778..00000000000 --- a/qa/qa/specs/features/project/create_spec.rb +++ /dev/null @@ -1,22 +0,0 @@ -module QA - describe 'create a new project', :smoke do - it 'user creates a new project' do - Runtime::Browser.visit(:gitlab, Page::Main::Login) - Page::Main::Login.act { sign_in_using_credentials } - - created_project = Factory::Resource::Project.fabricate! do |project| - project.name = 'awesome-project' - project.description = 'create awesome project test' - end - - expect(created_project.name).to match /^awesome-project-\h{16}$/ - - expect(page).to have_content( - /Project \S?awesome-project\S+ was successfully created/ - ) - - expect(page).to have_content('create awesome project test') - expect(page).to have_content('The repository for this project is empty') - end - end -end diff --git a/qa/qa/specs/features/project/deploy_key_clone_spec.rb b/qa/qa/specs/features/project/deploy_key_clone_spec.rb deleted file mode 100644 index 1d099508c24..00000000000 --- a/qa/qa/specs/features/project/deploy_key_clone_spec.rb +++ /dev/null @@ -1,105 +0,0 @@ -require 'digest/sha1' - -module QA - describe 'cloning code using a deploy key', :docker do - def login - Runtime::Browser.visit(:gitlab, Page::Main::Login) - Page::Main::Login.act { sign_in_using_credentials } - end - - before(:all) do - login - - @runner_name = "qa-runner-#{Time.now.to_i}" - - @project = Factory::Resource::Project.fabricate! do |resource| - resource.name = 'deploy-key-clone-project' - end - - @repository_location = @project.repository_ssh_location - - Factory::Resource::Runner.fabricate! do |resource| - resource.project = @project - resource.name = @runner_name - resource.tags = %w[qa docker] - resource.image = 'gitlab/gitlab-runner:ubuntu' - end - - Page::Menu::Main.act { sign_out } - end - - after(:all) do - Service::Runner.new(@runner_name).remove! - end - - keys = [ - [Runtime::Key::RSA, 8192], - [Runtime::Key::ECDSA, 521], - [Runtime::Key::ED25519] - ] - - keys.each do |(key_class, bits)| - it "user sets up a deploy key with #{key_class}(#{bits}) to clone code using pipelines" do - key = key_class.new(*bits) - - login - - Factory::Resource::DeployKey.fabricate! do |resource| - resource.project = @project - resource.title = "deploy key #{key.name}(#{key.bits})" - resource.key = key.public_key - end - - deploy_key_name = "DEPLOY_KEY_#{key.name}_#{key.bits}" - - Factory::Resource::SecretVariable.fabricate! do |resource| - resource.project = @project - resource.key = deploy_key_name - resource.value = key.private_key - end - - gitlab_ci = <<~YAML - cat-config: - script: - - mkdir -p ~/.ssh - - ssh-keyscan -p #{@repository_location.port} #{@repository_location.host} >> ~/.ssh/known_hosts - - eval $(ssh-agent -s) - - ssh-add -D - - echo "$#{deploy_key_name}" | ssh-add - - - git clone #{@repository_location.git_uri} - - cd #{@project.name} - - git checkout #{deploy_key_name} - - sha1sum .gitlab-ci.yml - tags: - - qa - - docker - YAML - - Factory::Repository::ProjectPush.fabricate! do |resource| - resource.project = @project - resource.file_name = '.gitlab-ci.yml' - resource.commit_message = 'Add .gitlab-ci.yml' - resource.file_content = gitlab_ci - resource.branch_name = deploy_key_name - resource.new_branch = true - end - - sha1sum = Digest::SHA1.hexdigest(gitlab_ci) - - Page::Project::Show.act { wait_for_push } - Page::Menu::Side.act { click_ci_cd_pipelines } - Page::Project::Pipeline::Index.act { go_to_latest_pipeline } - Page::Project::Pipeline::Show.act { go_to_first_job } - - Page::Project::Job::Show.perform do |job| - job.wait(reload: false) do - job.completed? && !job.trace_loading? - end - - expect(job.passed?).to be_truthy, "Job status did not become \"passed\"." - expect(job.output).to include(sha1sum) - end - end - end - end -end diff --git a/qa/qa/specs/features/project/file_spec.rb b/qa/qa/specs/features/project/file_spec.rb deleted file mode 100644 index 5659784cd5c..00000000000 --- a/qa/qa/specs/features/project/file_spec.rb +++ /dev/null @@ -1,54 +0,0 @@ -module QA - describe 'File Functionality', :core do - it 'lets a user create, edit and delete a file via WebUI' do - Runtime::Browser.visit(:gitlab, Page::Main::Login) - Page::Main::Login.act { sign_in_using_credentials } - - # Create - file_name = 'QA Test - File name' - file_content = 'QA Test - File content' - commit_message_for_create = 'QA Test - Create new file' - - Factory::Resource::File.fabricate! do |file| - file.name = file_name - file.content = file_content - file.commit_message = commit_message_for_create - end - - expect(page).to have_content('The file has been successfully created.') - expect(page).to have_content(file_name) - expect(page).to have_content(file_content) - expect(page).to have_content(commit_message_for_create) - - # Edit - updated_file_content = 'QA Test - Updated file content' - commit_message_for_update = 'QA Test - Update file' - - Page::File::Show.act { click_edit } - - Page::File::Form.act do - remove_content - add_content(updated_file_content) - add_commit_message(commit_message_for_update) - commit_changes - end - - expect(page).to have_content('Your changes have been successfully committed.') - expect(page).to have_content(updated_file_content) - expect(page).to have_content(commit_message_for_update) - - # Delete - commit_message_for_delete = 'QA Test - Delete file' - - Page::File::Show.act do - click_delete - add_commit_message(commit_message_for_delete) - click_delete_file - end - - expect(page).to have_content('The file has been successfully deleted.') - expect(page).to have_content(commit_message_for_delete) - expect(page).to have_no_content(file_name) - end - end -end diff --git a/qa/qa/specs/features/project/fork_project_spec.rb b/qa/qa/specs/features/project/fork_project_spec.rb deleted file mode 100644 index 280978bb950..00000000000 --- a/qa/qa/specs/features/project/fork_project_spec.rb +++ /dev/null @@ -1,21 +0,0 @@ -module QA - describe 'Project fork' do - it 'can submit merge requests to upstream master' do - Runtime::Browser.visit(:gitlab, Page::Main::Login) - Page::Main::Login.act { sign_in_using_credentials } - - merge_request = Factory::Resource::MergeRequestFromFork.fabricate! do |merge_request| - merge_request.fork_branch = 'feature-branch' - end - - Page::Menu::Main.perform { |main| main.sign_out } - Page::Main::Login.perform { |login| login.sign_in_using_credentials } - - merge_request.visit! - - Page::MergeRequest::Show.perform { |show| show.merge! } - - expect(page).to have_content('The changes were merged') - end - end -end diff --git a/qa/qa/specs/features/project/import_from_github_spec.rb b/qa/qa/specs/features/project/import_from_github_spec.rb deleted file mode 100644 index 57695d2c726..00000000000 --- a/qa/qa/specs/features/project/import_from_github_spec.rb +++ /dev/null @@ -1,106 +0,0 @@ -module QA - describe 'user imports a GitHub repo', :orchestrated, :github do - let(:imported_project) do - Factory::Resource::ProjectImportedFromGithub.fabricate! do |project| - project.name = 'imported-project' - project.personal_access_token = Runtime::Env.github_access_token - project.github_repository_path = 'gitlab-qa/test-project' - end - end - - after do - # We need to delete the imported project because it's impossible to import - # the same GitHub project twice for a given user. - api_client = Runtime::API::Client.new(:gitlab) - delete_project_request = Runtime::API::Request.new(api_client, "/projects/#{CGI.escape("#{Runtime::Namespace.path}/#{imported_project.name}")}") - delete delete_project_request.url - - expect_status(202) - end - - it 'user imports a GitHub repo' do - Runtime::Browser.visit(:gitlab, Page::Main::Login) - Page::Main::Login.act { sign_in_using_credentials } - - imported_project # import the project - - Page::Menu::Main.act { go_to_projects } - Page::Dashboard::Projects.perform do |dashboard| - dashboard.go_to_project(imported_project.name) - end - - Page::Project::Show.act { wait_for_import } - - verify_repository_import - verify_issues_import - verify_merge_requests_import - verify_labels_import - verify_milestones_import - verify_wiki_import - end - - def verify_repository_import - expect(page).to have_content('This test project is used for automated GitHub import by GitLab QA.') - expect(page).to have_content(imported_project.name) - end - - def verify_issues_import - Page::Menu::Side.act { click_issues } - expect(page).to have_content('This is a sample issue') - - click_link 'This is a sample issue' - - expect(page).to have_content('We should populate this project with issues, pull requests and wiki pages.') - - # Comments - expect(page).to have_content('This is a comment from @rymai.') - - Page::Issuable::Sidebar.perform do |issuable| - expect(issuable).to have_label('enhancement') - expect(issuable).to have_label('help wanted') - expect(issuable).to have_label('good first issue') - end - end - - def verify_merge_requests_import - Page::Menu::Side.act { click_merge_requests } - expect(page).to have_content('Improve README.md') - - click_link 'Improve README.md' - - expect(page).to have_content('This improves the README file a bit.') - - # Review comment are not supported yet - expect(page).not_to have_content('Really nice change.') - - # Comments - expect(page).to have_content('Nice work! This is a comment from @rymai.') - - # Diff comments - expect(page).to have_content('[Review comment] I like that!') - expect(page).to have_content('[Review comment] Nice blank line.') - expect(page).to have_content('[Single diff comment] Much better without this line!') - - Page::Issuable::Sidebar.perform do |issuable| - expect(issuable).to have_label('bug') - expect(issuable).to have_label('enhancement') - end - end - - def verify_labels_import - # TODO: Waiting on https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/19228 - # to build upon it. - end - - def verify_milestones_import - # TODO: Waiting on https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/18727 - # to build upon it. - end - - def verify_wiki_import - Page::Menu::Side.act { click_wiki } - - expect(page).to have_content('Welcome to the test-project wiki!') - end - end -end diff --git a/qa/qa/specs/features/project/pipelines_spec.rb b/qa/qa/specs/features/project/pipelines_spec.rb deleted file mode 100644 index 6c6b4e80626..00000000000 --- a/qa/qa/specs/features/project/pipelines_spec.rb +++ /dev/null @@ -1,102 +0,0 @@ -module QA - describe 'CI/CD Pipelines', :orchestrated, :docker do - let(:executor) { "qa-runner-#{Time.now.to_i}" } - - after do - Service::Runner.new(executor).remove! - end - - it 'user registers a new specific runner' do - Runtime::Browser.visit(:gitlab, Page::Main::Login) - Page::Main::Login.act { sign_in_using_credentials } - - Factory::Resource::Runner.fabricate! do |runner| - runner.name = executor - end - - Page::Project::Settings::CICD.perform do |settings| - sleep 5 # Runner should register within 5 seconds - settings.refresh - - settings.expand_runners_settings do |page| - expect(page).to have_content(executor) - expect(page).to have_online_runner - end - end - end - - it 'users creates a new pipeline' do - Runtime::Browser.visit(:gitlab, Page::Main::Login) - Page::Main::Login.act { sign_in_using_credentials } - - project = Factory::Resource::Project.fabricate! do |project| - project.name = 'project-with-pipelines' - project.description = 'Project with CI/CD Pipelines.' - end - - Factory::Resource::Runner.fabricate! do |runner| - runner.project = project - runner.name = executor - runner.tags = %w[qa test] - end - - Factory::Repository::ProjectPush.fabricate! do |push| - push.project = project - push.file_name = '.gitlab-ci.yml' - push.commit_message = 'Add .gitlab-ci.yml' - push.file_content = <<~EOF - test-success: - tags: - - qa - - test - script: echo 'OK' - - test-failure: - tags: - - qa - - test - script: - - echo 'FAILURE' - - exit 1 - - test-tags: - tags: - - qa - - docker - script: echo 'NOOP' - - test-artifacts: - tags: - - qa - - test - script: mkdir my-artifacts; echo "CONTENTS" > my-artifacts/artifact.txt - artifacts: - paths: - - my-artifacts/ - EOF - end - - Page::Project::Show.act { wait_for_push } - - expect(page).to have_content('Add .gitlab-ci.yml') - - Page::Menu::Side.act { click_ci_cd_pipelines } - - expect(page).to have_content('All 1') - expect(page).to have_content('Add .gitlab-ci.yml') - - puts 'Waiting for the runner to process the pipeline' - sleep 15 # Runner should process all jobs within 15 seconds. - - Page::Project::Pipeline::Index.act { go_to_latest_pipeline } - - Page::Project::Pipeline::Show.perform do |pipeline| - expect(pipeline).to be_running - expect(pipeline).to have_build('test-success', status: :success) - expect(pipeline).to have_build('test-failure', status: :failed) - expect(pipeline).to have_build('test-tags', status: :pending) - expect(pipeline).to have_build('test-artifacts', status: :success) - end - end - end -end diff --git a/qa/qa/specs/features/project/wikis_spec.rb b/qa/qa/specs/features/project/wikis_spec.rb deleted file mode 100644 index 9af2dbd1264..00000000000 --- a/qa/qa/specs/features/project/wikis_spec.rb +++ /dev/null @@ -1,45 +0,0 @@ -module QA - describe 'Wiki Functionality' do - def login - Runtime::Browser.visit(:gitlab, Page::Main::Login) - Page::Main::Login.act { sign_in_using_credentials } - end - - def validate_content(content) - expect(page).to have_content('Wiki was successfully updated') - expect(page).to have_content(/#{content}/) - end - - before do - login - end - - it 'User creates, edits, clones, and pushes to the wiki' do - wiki = Factory::Resource::Wiki.fabricate! do |resource| - resource.title = 'Home' - resource.content = '# My First Wiki Content' - resource.message = 'Update home' - end - - validate_content('My First Wiki Content') - - Page::Project::Wiki::Edit.act { go_to_edit_page } - Page::Project::Wiki::New.perform do |page| - page.set_content("My Second Wiki Content") - page.save_changes - end - - validate_content('My Second Wiki Content') - - Factory::Repository::WikiPush.fabricate! do |push| - push.wiki = wiki - push.file_name = 'Home.md' - push.file_content = '# My Third Wiki Content' - push.commit_message = 'Update Home.md' - end - Page::Menu::Side.act { click_wiki } - - expect(page).to have_content('My Third Wiki Content') - end - end -end diff --git a/qa/qa/specs/features/repository/protected_branches_spec.rb b/qa/qa/specs/features/repository/protected_branches_spec.rb deleted file mode 100644 index aa23145478d..00000000000 --- a/qa/qa/specs/features/repository/protected_branches_spec.rb +++ /dev/null @@ -1,66 +0,0 @@ -module QA - describe 'branch protection support' do - let(:branch_name) { 'protected-branch' } - let(:commit_message) { 'Protected push commit message' } - let(:project) do - Factory::Resource::Project.fabricate! do |resource| - resource.name = 'protected-branch-project' - end - end - - before do - Runtime::Browser.visit(:gitlab, Page::Main::Login) - Page::Main::Login.act { sign_in_using_credentials } - end - - after do - # We need to clear localStorage because we're using it for the dropdown, - # and capybara doesn't do this for us. - # https://github.com/teamcapybara/capybara/issues/1702 - Capybara.execute_script 'localStorage.clear()' - end - - context 'when developers and maintainers are allowed to push to a protected branch' do - it 'user with push rights successfully pushes to the protected branch' do - create_protected_branch(allow_to_push: true) - - push = push_new_file(branch_name) - - expect(push.output).to match(/remote: To create a merge request for protected-branch, visit/) - end - end - - context 'when developers and maintainers are not allowed to push to a protected branch' do - it 'user without push rights fails to push to the protected branch' do - create_protected_branch(allow_to_push: false) - - push = push_new_file(branch_name) - - expect(push.output) - .to match(/remote\: GitLab\: You are not allowed to push code to protected branches on this project/) - expect(push.output) - .to match(/\[remote rejected\] #{branch_name} -> #{branch_name} \(pre-receive hook declined\)/) - end - end - - def create_protected_branch(allow_to_push:) - Factory::Resource::Branch.fabricate! do |resource| - resource.branch_name = branch_name - resource.project = project - resource.allow_to_push = allow_to_push - resource.protected = true - end - end - - def push_new_file(branch) - Factory::Repository::ProjectPush.fabricate! do |resource| - resource.project = project - resource.file_name = 'new_file.md' - resource.file_content = '# This is a new file' - resource.commit_message = 'Add new_file.md' - resource.branch_name = branch_name - resource.new_branch = false - end - end - end -end diff --git a/qa/spec/runtime/env_spec.rb b/qa/spec/runtime/env_spec.rb index ccc0b906845..5493a33cd2a 100644 --- a/qa/spec/runtime/env_spec.rb +++ b/qa/spec/runtime/env_spec.rb @@ -104,6 +104,8 @@ describe QA::Runtime::Env do describe '.github_access_token' do it 'returns "" if GITHUB_ACCESS_TOKEN is not defined' do + stub_env('GITHUB_ACCESS_TOKEN', nil) + expect(described_class.github_access_token).to eq('') end @@ -115,6 +117,8 @@ describe QA::Runtime::Env do describe '.require_github_access_token!' do it 'raises ArgumentError if GITHUB_ACCESS_TOKEN is not defined' do + stub_env('GITHUB_ACCESS_TOKEN', nil) + expect { described_class.require_github_access_token! }.to raise_error(ArgumentError) end diff --git a/qa/spec/scenario/test/instance/all_spec.rb b/qa/spec/scenario/test/instance/all_spec.rb index 423527e938e..1d96352550b 100644 --- a/qa/spec/scenario/test/instance/all_spec.rb +++ b/qa/spec/scenario/test/instance/all_spec.rb @@ -1,4 +1,10 @@ describe QA::Scenario::Test::Instance::All do + subject do + Class.new(described_class) do + tags :rspec, :foo + end + end + context '#perform' do let(:arguments) { spy('Runtime::Scenario') } let(:release) { spy('Runtime::Release') } @@ -24,7 +30,7 @@ describe QA::Scenario::Test::Instance::All do subject.perform("test") expect(runner).to have_received(:options=) - .with(::File.expand_path('../../../../qa/specs/features', __dir__)) + .with(['--tag', 'rspec,foo', '--', ::File.expand_path('../../../../qa/specs/features', __dir__)]) end end diff --git a/qa/spec/scenario/test/instance/smoke_spec.rb b/qa/spec/scenario/test/instance/smoke_spec.rb index e79d19e8212..386eefae930 100644 --- a/qa/spec/scenario/test/instance/smoke_spec.rb +++ b/qa/spec/scenario/test/instance/smoke_spec.rb @@ -30,7 +30,7 @@ describe QA::Scenario::Test::Instance::Smoke do subject.perform("test") expect(runner).to have_received(:options=) - .with(::File.expand_path('../../../../qa/specs/features', __dir__)) + .with(['--tag', 'smoke', '--', ::File.expand_path('../../../../qa/specs/features', __dir__)]) end end diff --git a/rubocop/cop/prefer_class_methods_over_module.rb b/rubocop/cop/prefer_class_methods_over_module.rb new file mode 100644 index 00000000000..0dfa80ccfab --- /dev/null +++ b/rubocop/cop/prefer_class_methods_over_module.rb @@ -0,0 +1,73 @@ +# frozen_string_literal: true + +module RuboCop + module Cop + # Enforces the use of 'class_methods' instead of 'module ClassMethods' for activesupport concerns. + # For more information see: https://gitlab.com/gitlab-org/gitlab-ce/issues/50414 + # + # @example + # # bad + # module Foo + # extend ActiveSupport::Concern + # + # module ClassMethods + # def a_class_method + # end + # end + # end + # + # # good + # module Foo + # extend ActiveSupport::Concern + # + # class_methods do + # def a_class_method + # end + # end + # end + # + class PreferClassMethodsOverModule < RuboCop::Cop::Cop + include RangeHelp + + MSG = 'Do not use module ClassMethods, use class_methods block instead.' + + def_node_matcher :extend_activesupport_concern?, <<~PATTERN + (:send nil? :extend (:const (:const nil? :ActiveSupport) :Concern)) + PATTERN + + def on_module(node) + add_offense(node) if node.defined_module_name == 'ClassMethods' && module_extends_activesupport_concern?(node) + end + + def autocorrect(node) + lambda do |corrector| + corrector.replace(module_range(node), 'class_methods do') + end + end + + private + + def module_extends_activesupport_concern?(node) + container_module = container_module_of(node) + return false unless container_module + + container_module.descendants.any? do |descendant| + extend_activesupport_concern?(descendant) + end + end + + def container_module_of(node) + while node = node.parent + break if node.type == :module + end + + node + end + + def module_range(node) + module_node, _ = *node + range_between(node.loc.keyword.begin_pos, module_node.source_range.end_pos) + end + end + end +end diff --git a/rubocop/rubocop.rb b/rubocop/rubocop.rb index eaf421a7235..d823fa4edb1 100644 --- a/rubocop/rubocop.rb +++ b/rubocop/rubocop.rb @@ -7,6 +7,7 @@ require_relative 'cop/include_sidekiq_worker' require_relative 'cop/avoid_return_from_blocks' require_relative 'cop/avoid_break_from_strong_memoize' require_relative 'cop/line_break_around_conditional_block' +require_relative 'cop/prefer_class_methods_over_module' require_relative 'cop/migration/add_column' require_relative 'cop/migration/add_concurrent_foreign_key' require_relative 'cop/migration/add_concurrent_index' diff --git a/scripts/schema_changed.sh b/scripts/schema_changed.sh index 5de2b35571d..b5e510c2367 100644 --- a/scripts/schema_changed.sh +++ b/scripts/schema_changed.sh @@ -1,9 +1,14 @@ -function schema_changed() { - if [[ ! -z `git diff --name-only -- db/schema.rb` ]]; then - echo "db/schema.rb after rake db:migrate:reset is different from one in the repository" +#!/bin/sh + +schema_changed() { + if [ ! -z "$(git diff --name-only -- db/schema.rb)" ]; then + printf "db/schema.rb after rake db:migrate:reset is different from one in the repository" + printf "The diff is as follows:\n" + diff=$(git diff -p --binary -- db/schema.rb) + printf "%s" "$diff" exit 1 else - echo "db/schema.rb after rake db:migrate:reset matches one in the repository" + printf "db/schema.rb after rake db:migrate:reset matches one in the repository" fi } diff --git a/spec/controllers/projects/avatars_controller_spec.rb b/spec/controllers/projects/avatars_controller_spec.rb index 17c9a61f339..14059cff74c 100644 --- a/spec/controllers/projects/avatars_controller_spec.rb +++ b/spec/controllers/projects/avatars_controller_spec.rb @@ -1,24 +1,55 @@ require 'spec_helper' describe Projects::AvatarsController do - let(:project) { create(:project, :repository, avatar: fixture_file_upload("spec/fixtures/dk.png", "image/png")) } - let(:user) { create(:user) } + let(:project) { create(:project, :repository) } before do - sign_in(user) - project.add_maintainer(user) controller.instance_variable_set(:@project, project) end - it 'GET #show' do - get :show, namespace_id: project.namespace.id, project_id: project.id + describe 'GET #show' do + subject { get :show, namespace_id: project.namespace, project_id: project } - expect(response).to have_gitlab_http_status(404) + context 'when repository has no avatar' do + it 'shows 404' do + subject + + expect(response).to have_gitlab_http_status(404) + end + end + + context 'when repository has an avatar' do + before do + allow(project).to receive(:avatar_in_git).and_return(filepath) + end + + context 'when the avatar is stored in the repository' do + let(:filepath) { 'files/images/logo-white.png' } + + it 'sends the avatar' do + subject + + expect(response).to have_gitlab_http_status(200) + expect(response.header['Content-Type']).to eq('image/png') + expect(response.header[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with('git-blob:') + end + end + + context 'when the avatar is stored in lfs' do + it_behaves_like 'repository lfs file load' do + let(:filename) { 'lfs_object.iso' } + let(:filepath) { "files/lfs/#{filename}" } + end + end + end end - it 'removes avatar from DB by calling destroy' do - delete :destroy, namespace_id: project.namespace.id, project_id: project.id - expect(project.avatar.present?).to be_falsey - expect(project).to be_valid + describe 'DELETE #destroy' do + it 'removes avatar from DB by calling destroy' do + delete :destroy, namespace_id: project.namespace.id, project_id: project.id + + expect(project.avatar.present?).to be_falsey + expect(project).to be_valid + end end end diff --git a/spec/controllers/projects/raw_controller_spec.rb b/spec/controllers/projects/raw_controller_spec.rb index c3468536ae1..6b658bf5295 100644 --- a/spec/controllers/projects/raw_controller_spec.rb +++ b/spec/controllers/projects/raw_controller_spec.rb @@ -1,14 +1,21 @@ require 'spec_helper' describe Projects::RawController do - let(:public_project) { create(:project, :public, :repository) } + let(:project) { create(:project, :public, :repository) } + + describe 'GET #show' do + subject do + get(:show, + namespace_id: project.namespace, + project_id: project, + id: filepath) + end - describe '#show' do context 'regular filename' do - let(:id) { 'master/README.md' } + let(:filepath) { 'master/README.md' } it 'delivers ASCII file' do - get_show(public_project, id) + subject expect(response).to have_gitlab_http_status(200) expect(response.header['Content-Type']).to eq('text/plain; charset=utf-8') @@ -19,10 +26,10 @@ describe Projects::RawController do end context 'image header' do - let(:id) { 'master/files/images/6049019_460s.jpg' } + let(:filepath) { 'master/files/images/6049019_460s.jpg' } it 'sets image content type header' do - get_show(public_project, id) + subject expect(response).to have_gitlab_http_status(200) expect(response.header['Content-Type']).to eq('image/jpeg') @@ -30,85 +37,9 @@ describe Projects::RawController do end end - context 'lfs object' do - let(:id) { 'be93687/files/lfs/lfs_object.iso' } - let!(:lfs_object) { create(:lfs_object, oid: '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897', size: '1575078') } - - context 'when lfs is enabled' do - before do - allow_any_instance_of(Project).to receive(:lfs_enabled?).and_return(true) - end - - context 'when project has access' do - before do - public_project.lfs_objects << lfs_object - allow_any_instance_of(LfsObjectUploader).to receive(:exists?).and_return(true) - allow(controller).to receive(:send_file) { controller.head :ok } - end - - it 'serves the file' do - expect(controller).to receive(:send_file).with("#{LfsObjectUploader.root}/91/ef/f75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897", filename: 'lfs_object.iso', disposition: 'attachment') - get_show(public_project, id) - - expect(response).to have_gitlab_http_status(200) - end - - context 'and lfs uses object storage' do - let(:lfs_object) { create(:lfs_object, :with_file, oid: '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897', size: '1575078') } - - before do - stub_lfs_object_storage - lfs_object.file.migrate!(LfsObjectUploader::Store::REMOTE) - end - - it 'responds with redirect to file' do - get_show(public_project, id) - - expect(response).to have_gitlab_http_status(302) - expect(response.location).to include(lfs_object.reload.file.path) - end - - it 'sets content disposition' do - get_show(public_project, id) - - file_uri = URI.parse(response.location) - params = CGI.parse(file_uri.query) - - expect(params["response-content-disposition"].first).to eq 'attachment;filename="lfs_object.iso"' - end - end - end - - context 'when project does not have access' do - it 'does not serve the file' do - get_show(public_project, id) - - expect(response).to have_gitlab_http_status(404) - end - end - end - - context 'when lfs is not enabled' do - before do - allow_any_instance_of(Project).to receive(:lfs_enabled?).and_return(false) - end - - it 'delivers ASCII file' do - get_show(public_project, id) - - expect(response).to have_gitlab_http_status(200) - expect(response.header['Content-Type']).to eq('text/plain; charset=utf-8') - expect(response.header['Content-Disposition']) - .to eq('inline') - expect(response.header[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with('git-blob:') - end - end + it_behaves_like 'repository lfs file load' do + let(:filename) { 'lfs_object.iso' } + let(:filepath) { "be93687/files/lfs/#{filename}" } end end - - def get_show(project, id) - get(:show, namespace_id: project.namespace.to_param, - project_id: project, - id: id) - end end diff --git a/spec/features/admin/admin_settings_spec.rb b/spec/features/admin/admin_settings_spec.rb index af1c153dec8..a3229fe1741 100644 --- a/spec/features/admin/admin_settings_spec.rb +++ b/spec/features/admin/admin_settings_spec.rb @@ -78,6 +78,18 @@ describe 'Admin updates settings' do expect(page).to have_content "Application settings saved successfully" end + it 'Change New users set to external', :js do + user_internal_regex = find('#application_setting_user_default_internal_regex', visible: :all) + + expect(user_internal_regex).to be_readonly + expect(user_internal_regex['placeholder']).to eq 'To define internal users, first enable new users set to external' + + check 'application_setting_user_default_external' + + expect(user_internal_regex).not_to be_readonly + expect(user_internal_regex['placeholder']).to eq 'Regex pattern' + end + it 'Change Sign-in restrictions' do page.within('.as-signin') do fill_in 'Home page URL', with: 'https://about.gitlab.com/' diff --git a/spec/features/admin/admin_users_spec.rb b/spec/features/admin/admin_users_spec.rb index b2eaeb1c487..d32f33ca1e2 100644 --- a/spec/features/admin/admin_users_spec.rb +++ b/spec/features/admin/admin_users_spec.rb @@ -125,6 +125,52 @@ describe "Admin::Users" do expect(page).to have_content('Username can contain only letters, digits') end end + + context 'with new users set to external enabled' do + context 'with regex to match internal user email address set', :js do + before do + stub_application_setting(user_default_external: true) + stub_application_setting(user_default_internal_regex: '.internal@') + + visit new_admin_user_path + end + + def expects_external_to_be_checked + expect(find('#user_external')).to be_checked + end + + def expects_external_to_be_unchecked + expect(find('#user_external')).not_to be_checked + end + + def expects_warning_to_be_hidden + expect(find('#warning_external_automatically_set', visible: :all)[:class]).to include 'hidden' + end + + def expects_warning_to_be_shown + expect(find('#warning_external_automatically_set')[:class]).not_to include 'hidden' + end + + it 'automatically unchecks external for matching email' do + expects_external_to_be_checked + expects_warning_to_be_hidden + + fill_in 'user_email', with: 'test.internal@domain.ch' + + expects_external_to_be_unchecked + expects_warning_to_be_shown + + fill_in 'user_email', with: 'test@domain.ch' + + expects_external_to_be_checked + expects_warning_to_be_hidden + + uncheck 'user_external' + + expects_warning_to_be_hidden + end + end + end end describe "GET /admin/users/:id" do diff --git a/spec/features/projects/issues/rss_spec.rb b/spec/features/issues/rss_spec.rb index 0e1383cd607..0e1383cd607 100644 --- a/spec/features/projects/issues/rss_spec.rb +++ b/spec/features/issues/rss_spec.rb diff --git a/spec/features/projects/issues/user_comments_on_issue_spec.rb b/spec/features/issues/user_comments_on_issue_spec.rb index ba5b80ed04b..ba5b80ed04b 100644 --- a/spec/features/projects/issues/user_comments_on_issue_spec.rb +++ b/spec/features/issues/user_comments_on_issue_spec.rb diff --git a/spec/features/projects/issues/user_creates_issue_spec.rb b/spec/features/issues/user_creates_issue_spec.rb index 5e8662100c5..5e8662100c5 100644 --- a/spec/features/projects/issues/user_creates_issue_spec.rb +++ b/spec/features/issues/user_creates_issue_spec.rb diff --git a/spec/features/projects/issues/user_edits_issue_spec.rb b/spec/features/issues/user_edits_issue_spec.rb index 1d9c3abc20f..1d9c3abc20f 100644 --- a/spec/features/projects/issues/user_edits_issue_spec.rb +++ b/spec/features/issues/user_edits_issue_spec.rb diff --git a/spec/features/projects/issues/user_sorts_issues_spec.rb b/spec/features/issues/user_sorts_issues_spec.rb index 7d261ec7dae..7d261ec7dae 100644 --- a/spec/features/projects/issues/user_sorts_issues_spec.rb +++ b/spec/features/issues/user_sorts_issues_spec.rb diff --git a/spec/features/projects/issues/user_toggles_subscription_spec.rb b/spec/features/issues/user_toggles_subscription_spec.rb index c2b2a193682..c2b2a193682 100644 --- a/spec/features/projects/issues/user_toggles_subscription_spec.rb +++ b/spec/features/issues/user_toggles_subscription_spec.rb diff --git a/spec/features/projects/issues/user_views_issue_spec.rb b/spec/features/issues/user_views_issue_spec.rb index 117e5986f29..117e5986f29 100644 --- a/spec/features/projects/issues/user_views_issue_spec.rb +++ b/spec/features/issues/user_views_issue_spec.rb diff --git a/spec/features/projects/issues/user_views_issues_spec.rb b/spec/features/issues/user_views_issues_spec.rb index 58afb4efb86..58afb4efb86 100644 --- a/spec/features/projects/issues/user_views_issues_spec.rb +++ b/spec/features/issues/user_views_issues_spec.rb diff --git a/spec/features/projects/merge_requests/user_accepts_merge_request_spec.rb b/spec/features/merge_request/user_accepts_merge_request_spec.rb index 01aeed93947..01aeed93947 100644 --- a/spec/features/projects/merge_requests/user_accepts_merge_request_spec.rb +++ b/spec/features/merge_request/user_accepts_merge_request_spec.rb diff --git a/spec/features/projects/merge_requests/user_closes_merge_request_spec.rb b/spec/features/merge_request/user_closes_merge_request_spec.rb index 2d12d690151..2d12d690151 100644 --- a/spec/features/projects/merge_requests/user_closes_merge_request_spec.rb +++ b/spec/features/merge_request/user_closes_merge_request_spec.rb diff --git a/spec/features/projects/merge_requests/user_comments_on_commit_spec.rb b/spec/features/merge_request/user_comments_on_commit_spec.rb index 8ea358bcc70..8ea358bcc70 100644 --- a/spec/features/projects/merge_requests/user_comments_on_commit_spec.rb +++ b/spec/features/merge_request/user_comments_on_commit_spec.rb diff --git a/spec/features/projects/merge_requests/user_comments_on_diff_spec.rb b/spec/features/merge_request/user_comments_on_diff_spec.rb index 441b080bee5..441b080bee5 100644 --- a/spec/features/projects/merge_requests/user_comments_on_diff_spec.rb +++ b/spec/features/merge_request/user_comments_on_diff_spec.rb diff --git a/spec/features/projects/merge_requests/user_comments_on_merge_request_spec.rb b/spec/features/merge_request/user_comments_on_merge_request_spec.rb index 69bdab85d81..69bdab85d81 100644 --- a/spec/features/projects/merge_requests/user_comments_on_merge_request_spec.rb +++ b/spec/features/merge_request/user_comments_on_merge_request_spec.rb diff --git a/spec/features/projects/merge_requests/user_creates_merge_request_spec.rb b/spec/features/merge_request/user_creates_merge_request_spec.rb index 38b4e4a6d1b..38b4e4a6d1b 100644 --- a/spec/features/projects/merge_requests/user_creates_merge_request_spec.rb +++ b/spec/features/merge_request/user_creates_merge_request_spec.rb diff --git a/spec/features/projects/merge_requests/user_edits_merge_request_spec.rb b/spec/features/merge_request/user_edits_merge_request_spec.rb index 7de0f9daac6..7de0f9daac6 100644 --- a/spec/features/projects/merge_requests/user_edits_merge_request_spec.rb +++ b/spec/features/merge_request/user_edits_merge_request_spec.rb diff --git a/spec/features/projects/merge_requests/user_manages_subscription_spec.rb b/spec/features/merge_request/user_manages_subscription_spec.rb index 68a835e7f77..68a835e7f77 100644 --- a/spec/features/projects/merge_requests/user_manages_subscription_spec.rb +++ b/spec/features/merge_request/user_manages_subscription_spec.rb diff --git a/spec/features/projects/merge_requests/user_merges_merge_request_spec.rb b/spec/features/merge_request/user_merges_merge_request_spec.rb index 6539e6e9208..6539e6e9208 100644 --- a/spec/features/projects/merge_requests/user_merges_merge_request_spec.rb +++ b/spec/features/merge_request/user_merges_merge_request_spec.rb diff --git a/spec/features/projects/merge_requests/user_rebases_merge_request_spec.rb b/spec/features/merge_request/user_rebases_merge_request_spec.rb index 92e1c9942b1..92e1c9942b1 100644 --- a/spec/features/projects/merge_requests/user_rebases_merge_request_spec.rb +++ b/spec/features/merge_request/user_rebases_merge_request_spec.rb diff --git a/spec/features/projects/merge_requests/user_reopens_merge_request_spec.rb b/spec/features/merge_request/user_reopens_merge_request_spec.rb index 745b4537e72..745b4537e72 100644 --- a/spec/features/projects/merge_requests/user_reopens_merge_request_spec.rb +++ b/spec/features/merge_request/user_reopens_merge_request_spec.rb diff --git a/spec/features/projects/merge_requests/user_reverts_merge_request_spec.rb b/spec/features/merge_request/user_reverts_merge_request_spec.rb index 67b6aefb2d8..67b6aefb2d8 100644 --- a/spec/features/projects/merge_requests/user_reverts_merge_request_spec.rb +++ b/spec/features/merge_request/user_reverts_merge_request_spec.rb diff --git a/spec/features/merge_request/user_sees_diff_spec.rb b/spec/features/merge_request/user_sees_diff_spec.rb index d6e7ff33d5d..0c15febe8df 100644 --- a/spec/features/merge_request/user_sees_diff_spec.rb +++ b/spec/features/merge_request/user_sees_diff_spec.rb @@ -2,6 +2,7 @@ require 'rails_helper' describe 'Merge request > User sees diff', :js do include ProjectForksHelper + include RepoHelpers let(:project) { create(:project, :public, :repository) } let(:merge_request) { create(:merge_request, source_project: project) } @@ -81,5 +82,58 @@ describe 'Merge request > User sees diff', :js do expect(page).to have_selector('.js-cancel-fork-suggestion-button', count: 1) end end + + context 'when file contains html' do + let(:current_user) { project.owner } + let(:branch_name) {"test_branch"} + + def create_file(branch_name, file_name, content) + Files::CreateService.new( + project, + current_user, + start_branch: branch_name, + branch_name: branch_name, + commit_message: "Create file", + file_path: file_name, + file_content: content + ).execute + + project.commit(branch_name) + end + + it 'escapes any HTML special characters in the diff chunk header' do + file_content = + <<~CONTENT + function foo<input> { + let a = 1; + let b = 2; + let c = 3; + let d = 3; + } + CONTENT + + new_file_content = + <<~CONTENT + function foo<input> { + let a = 1; + let b = 2; + let c = 3; + let x = 3; + } + CONTENT + + file_name = 'xss_file.txt' + + create_file('master', file_name, file_content) + merge_request = create(:merge_request, source_project: project) + create_file(merge_request.source_branch, file_name, new_file_content) + + project.commit(merge_request.source_branch) + + visit diffs_project_merge_request_path(project, merge_request) + + expect(page).to have_text("function foo<input> {") + end + end end end diff --git a/spec/features/projects/merge_requests/user_views_diffs_spec.rb b/spec/features/merge_request/user_views_diffs_spec.rb index b1bfe9e5de3..b1bfe9e5de3 100644 --- a/spec/features/projects/merge_requests/user_views_diffs_spec.rb +++ b/spec/features/merge_request/user_views_diffs_spec.rb diff --git a/spec/features/projects/merge_requests/user_views_open_merge_request_spec.rb b/spec/features/merge_request/user_views_open_merge_request_spec.rb index 6ac495aa03d..6ac495aa03d 100644 --- a/spec/features/projects/merge_requests/user_views_open_merge_request_spec.rb +++ b/spec/features/merge_request/user_views_open_merge_request_spec.rb diff --git a/spec/features/projects/merge_requests/user_views_user_status_on_merge_request_spec.rb b/spec/features/merge_request/user_views_user_status_on_merge_request_spec.rb index 78d9c6c6db1..78d9c6c6db1 100644 --- a/spec/features/projects/merge_requests/user_views_user_status_on_merge_request_spec.rb +++ b/spec/features/merge_request/user_views_user_status_on_merge_request_spec.rb diff --git a/spec/features/projects/merge_requests/user_sorts_merge_requests_spec.rb b/spec/features/merge_requests/user_sorts_merge_requests_spec.rb index 82cfe600d52..82cfe600d52 100644 --- a/spec/features/projects/merge_requests/user_sorts_merge_requests_spec.rb +++ b/spec/features/merge_requests/user_sorts_merge_requests_spec.rb diff --git a/spec/features/projects/merge_requests/user_views_all_merge_requests_spec.rb b/spec/features/merge_requests/user_views_all_merge_requests_spec.rb index 6c695bd7aa9..6c695bd7aa9 100644 --- a/spec/features/projects/merge_requests/user_views_all_merge_requests_spec.rb +++ b/spec/features/merge_requests/user_views_all_merge_requests_spec.rb diff --git a/spec/features/projects/merge_requests/user_views_closed_merge_requests_spec.rb b/spec/features/merge_requests/user_views_closed_merge_requests_spec.rb index 853809fe87a..853809fe87a 100644 --- a/spec/features/projects/merge_requests/user_views_closed_merge_requests_spec.rb +++ b/spec/features/merge_requests/user_views_closed_merge_requests_spec.rb diff --git a/spec/features/projects/merge_requests/user_views_merged_merge_requests_spec.rb b/spec/features/merge_requests/user_views_merged_merge_requests_spec.rb index eb012694f1e..eb012694f1e 100644 --- a/spec/features/projects/merge_requests/user_views_merged_merge_requests_spec.rb +++ b/spec/features/merge_requests/user_views_merged_merge_requests_spec.rb diff --git a/spec/features/projects/merge_requests/user_views_open_merge_requests_spec.rb b/spec/features/merge_requests/user_views_open_merge_requests_spec.rb index 115e548b691..115e548b691 100644 --- a/spec/features/projects/merge_requests/user_views_open_merge_requests_spec.rb +++ b/spec/features/merge_requests/user_views_open_merge_requests_spec.rb diff --git a/spec/features/projects/jobs/user_browses_job_spec.rb b/spec/features/projects/jobs/user_browses_job_spec.rb index 50e957bf12b..2d791947ee9 100644 --- a/spec/features/projects/jobs/user_browses_job_spec.rb +++ b/spec/features/projects/jobs/user_browses_job_spec.rb @@ -40,7 +40,7 @@ describe 'User browses a job', :js do it 'displays the failure reason' do within('.builds-container') do build_link = first('.build-job > a') - expect(build_link['data-title']).to eq('test - failed <br> (unknown failure)') + expect(build_link['data-title']).to eq('test - failed - (unknown failure)') end end end @@ -51,7 +51,7 @@ describe 'User browses a job', :js do it 'displays the failure reason and retried label' do within('.builds-container') do build_link = first('.build-job > a') - expect(build_link['data-title']).to eq('test - failed <br> (unknown failure) (retried)') + expect(build_link['data-title']).to eq('test - failed - (unknown failure) (retried)') end 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 08786fe1630..ebc20d15d67 100644 --- a/spec/features/projects/jobs/user_browses_jobs_spec.rb +++ b/spec/features/projects/jobs/user_browses_jobs_spec.rb @@ -36,7 +36,7 @@ describe 'User browses jobs' do it 'displays a tooltip with the failure reason' do page.within('.ci-table') do failed_job_link = page.find('.ci-failed') - expect(failed_job_link[:title]).to eq('Failed <br> (unknown failure)') + expect(failed_job_link[:title]).to eq('Failed - (unknown failure)') end end end diff --git a/spec/features/projects/pipelines/pipeline_spec.rb b/spec/features/projects/pipelines/pipeline_spec.rb index a84492ea5f1..603503a531c 100644 --- a/spec/features/projects/pipelines/pipeline_spec.rb +++ b/spec/features/projects/pipelines/pipeline_spec.rb @@ -126,7 +126,7 @@ describe 'Pipeline', :js do it 'should include the failure reason' do page.within('#ci-badge-test') do build_link = page.find('.js-pipeline-graph-job-link') - expect(build_link['data-original-title']).to eq('test - failed <br> (unknown failure)') + expect(build_link['data-original-title']).to eq('test - failed - (unknown failure)') end end end @@ -319,7 +319,7 @@ describe 'Pipeline', :js do it 'displays a tooltip with the failure reason' do page.within('.ci-table') do failed_job_link = page.find('.ci-failed') - expect(failed_job_link[:title]).to eq('Failed <br> (unknown failure)') + expect(failed_job_link[:title]).to eq('Failed - (unknown failure)') end end end diff --git a/spec/features/projects/pipelines/pipelines_spec.rb b/spec/features/projects/pipelines/pipelines_spec.rb index 4a83bcc3efb..26a92f14787 100644 --- a/spec/features/projects/pipelines/pipelines_spec.rb +++ b/spec/features/projects/pipelines/pipelines_spec.rb @@ -406,7 +406,7 @@ describe 'Pipelines', :js do within('.js-builds-dropdown-list') do build_element = page.find('.mini-pipeline-graph-dropdown-item') - expect(build_element['data-original-title']).to eq('build - failed <br> (unknown failure)') + expect(build_element['data-original-title']).to eq('build - failed - (unknown failure)') end end end diff --git a/spec/helpers/submodule_helper_spec.rb b/spec/helpers/submodule_helper_spec.rb index a64f8a11ef2..8662cadc7a0 100644 --- a/spec/helpers/submodule_helper_spec.rb +++ b/spec/helpers/submodule_helper_spec.rb @@ -162,42 +162,77 @@ describe SubmoduleHelper do end context 'submodules with relative links' do - let(:group) { create(:group, name: "Master Project", path: "master-project") } + let(:group) { create(:group, name: "top group", path: "top-group") } let(:project) { create(:project, group: group) } - let(:commit_id) { sample_commit[:id] } + let(:repo) { double(:repo, project: project) } + + def expect_relative_link_to_resolve_to(relative_path, expected_path) + allow(repo).to receive(:submodule_url_for).and_return(relative_path) + + result = submodule_links(submodule_item) + + expect(result).to eq([expected_path, "#{expected_path}/tree/#{submodule_item.id}"]) + end - it 'one level down' do - result = relative_self_links('../test.git', commit_id, project) - expect(result).to eq(["/#{group.path}/test", "/#{group.path}/test/tree/#{commit_id}"]) + it 'handles project under same group' do + expect_relative_link_to_resolve_to('../test.git', "/#{group.path}/test") end - it 'with trailing whitespace' do - result = relative_self_links('../test.git ', commit_id, project) - expect(result).to eq(["/#{group.path}/test", "/#{group.path}/test/tree/#{commit_id}"]) + it 'handles trailing whitespace' do + expect_relative_link_to_resolve_to('../test.git ', "/#{group.path}/test") end - it 'two levels down' do - result = relative_self_links('../../test.git', commit_id, project) - expect(result).to eq(["/#{group.path}/test", "/#{group.path}/test/tree/#{commit_id}"]) + it 'handles project under another top group' do + expect_relative_link_to_resolve_to('../../baz/test.git ', "/baz/test") + end + + context 'repo path resolves to be located at root (namespace absent)' do + it 'returns nil' do + allow(repo).to receive(:submodule_url_for).and_return('../../test.git') + + result = submodule_links(submodule_item) + + expect(result).to eq([nil, nil]) + end end - it 'one level down with namespace and repo' do - result = relative_self_links('../foobar/test.git', commit_id, project) - expect(result).to eq(["/foobar/test", "/foobar/test/tree/#{commit_id}"]) + context 'repo path resolves to be located underneath current project path' do + it 'returns nil because it is not possible to have repo nested under another repo' do + allow(repo).to receive(:submodule_url_for).and_return('./test.git') + + result = submodule_links(submodule_item) + + expect(result).to eq([nil, nil]) + end end - it 'two levels down with namespace and repo' do - result = relative_self_links('../foobar/baz/test.git', commit_id, project) - expect(result).to eq(["/baz/test", "/baz/test/tree/#{commit_id}"]) + context 'subgroup' do + let(:sub_group) { create(:group, parent: group, name: "sub group", path: "sub-group") } + let(:sub_project) { create(:project, group: sub_group) } + + context 'project in sub group' do + let(:project) { sub_project } + + it "handles referencing ancestor group's project" do + expect_relative_link_to_resolve_to('../../../top-group/test.git', "/#{group.path}/test") + end + end + + it "handles referencing descendent group's project" do + expect_relative_link_to_resolve_to('../sub-group/test.git', "/top-group/sub-group/test") + end + + it "handles referencing another top group's project" do + expect_relative_link_to_resolve_to('../../frontend/css/test.git', "/frontend/css/test") + end end context 'personal project' do let(:user) { create(:user) } let(:project) { create(:project, namespace: user.namespace) } - it 'one level down with personal project' do - result = relative_self_links('../test.git', commit_id, project) - expect(result).to eq(["/#{user.username}/test", "/#{user.username}/test/tree/#{commit_id}"]) + it 'handles referencing another personal project' do + expect_relative_link_to_resolve_to('../test.git', "/#{user.username}/test") end end end diff --git a/spec/helpers/users_helper_spec.rb b/spec/helpers/users_helper_spec.rb index b079802cb81..34d9115a1f6 100644 --- a/spec/helpers/users_helper_spec.rb +++ b/spec/helpers/users_helper_spec.rb @@ -42,6 +42,30 @@ describe UsersHelper do end end + describe '#user_internal_regex_data' do + using RSpec::Parameterized::TableSyntax + + where(:user_default_external, :user_default_internal_regex, :result) do + false | nil | { user_internal_regex_pattern: nil, user_internal_regex_options: nil } + false | '' | { user_internal_regex_pattern: nil, user_internal_regex_options: nil } + false | 'mockRegexPattern' | { user_internal_regex_pattern: nil, user_internal_regex_options: nil } + true | nil | { user_internal_regex_pattern: nil, user_internal_regex_options: nil } + true | '' | { user_internal_regex_pattern: nil, user_internal_regex_options: nil } + true | 'mockRegexPattern' | { user_internal_regex_pattern: 'mockRegexPattern', user_internal_regex_options: 'gi' } + end + + with_them do + before do + stub_application_setting(user_default_external: user_default_external) + stub_application_setting(user_default_internal_regex: user_default_internal_regex) + end + + subject { helper.user_internal_regex_data } + + it { is_expected.to eq(result) } + end + end + describe '#current_user_menu_items' do subject(:items) { helper.current_user_menu_items } diff --git a/spec/javascripts/fixtures/admin_users.rb b/spec/javascripts/fixtures/admin_users.rb new file mode 100644 index 00000000000..9989ac4fff2 --- /dev/null +++ b/spec/javascripts/fixtures/admin_users.rb @@ -0,0 +1,29 @@ +require 'spec_helper' + +describe Admin::UsersController, '(JavaScript fixtures)', type: :controller do + include StubENV + include JavaScriptFixturesHelpers + + let(:admin) { create(:admin) } + + before do + stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false') + sign_in(admin) + end + + render_views + + before(:all) do + clean_frontend_fixtures('admin/users') + end + + it 'admin/users/new_with_internal_user_regex.html.raw' do |example| + stub_application_setting(user_default_external: true) + stub_application_setting(user_default_internal_regex: '^(?:(?!\.ext@).)*$\r?') + + get :new + + expect(response).to be_success + store_frontend_fixture(response, example.description) + end +end diff --git a/spec/javascripts/fixtures/application_settings.rb b/spec/javascripts/fixtures/application_settings.rb new file mode 100644 index 00000000000..a9d3043f73d --- /dev/null +++ b/spec/javascripts/fixtures/application_settings.rb @@ -0,0 +1,34 @@ +require 'spec_helper' + +describe Admin::ApplicationSettingsController, '(JavaScript fixtures)', type: :controller do + include StubENV + include JavaScriptFixturesHelpers + + let(:admin) { create(:admin) } + let(:namespace) { create(:namespace, name: 'frontend-fixtures' )} + let(:project) { create(:project_empty_repo, namespace: namespace, path: 'application-settings') } + + before do + stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false') + sign_in(admin) + end + + render_views + + before(:all) do + clean_frontend_fixtures('application_settings/') + end + + after do + remove_repository(project) + end + + it 'application_settings/accounts_and_limit.html.raw' do |example| + stub_application_setting(user_default_external: false) + + get :show + + expect(response).to be_success + store_frontend_fixture(response, example.description) + end +end diff --git a/spec/javascripts/ide/components/new_dropdown/upload_spec.js b/spec/javascripts/ide/components/new_dropdown/upload_spec.js index 9c76500cfe5..70b885ede26 100644 --- a/spec/javascripts/ide/components/new_dropdown/upload_spec.js +++ b/spec/javascripts/ide/components/new_dropdown/upload_spec.js @@ -21,6 +21,23 @@ describe('new dropdown upload', () => { vm.$destroy(); }); + describe('openFile', () => { + it('calls for each file', () => { + const files = ['test', 'test2', 'test3']; + + spyOn(vm, 'readFile'); + spyOnProperty(vm.$refs.fileUpload, 'files').and.returnValue(files); + + vm.openFile(); + + expect(vm.readFile.calls.count()).toBe(3); + + files.forEach((file, i) => { + expect(vm.readFile.calls.argsFor(i)).toEqual([file]); + }); + }); + }); + describe('readFile', () => { beforeEach(() => { spyOn(FileReader.prototype, 'readAsText'); diff --git a/spec/javascripts/ide/components/repo_file_spec.js b/spec/javascripts/ide/components/repo_file_spec.js index f99d1f9890a..fc639a672e2 100644 --- a/spec/javascripts/ide/components/repo_file_spec.js +++ b/spec/javascripts/ide/components/repo_file_spec.js @@ -121,4 +121,25 @@ describe('RepoFile', () => { ).toContain('Locked by testuser'); }); }); + + it('calls scrollIntoView if made active', done => { + createComponent({ + file: { + ...file(), + type: 'blob', + active: false, + }, + level: 0, + }); + + spyOn(vm, 'scrollIntoView'); + + vm.file.active = true; + + vm.$nextTick(() => { + expect(vm.scrollIntoView).toHaveBeenCalled(); + + done(); + }); + }); }); diff --git a/spec/javascripts/pages/admin/application_settings/account_and_limits_spec.js b/spec/javascripts/pages/admin/application_settings/account_and_limits_spec.js new file mode 100644 index 00000000000..4dbfd8f0eaa --- /dev/null +++ b/spec/javascripts/pages/admin/application_settings/account_and_limits_spec.js @@ -0,0 +1,33 @@ +import $ from 'jquery'; +import initUserInternalRegexPlaceholder, { PLACEHOLDER_USER_EXTERNAL_DEFAULT_FALSE, + PLACEHOLDER_USER_EXTERNAL_DEFAULT_TRUE } from '~/pages/admin/application_settings/account_and_limits'; + +describe('AccountAndLimits', () => { + const FIXTURE = 'application_settings/accounts_and_limit.html.raw'; + let $userDefaultExternal; + let $userInternalRegex; + preloadFixtures(FIXTURE); + + beforeEach(() => { + loadFixtures(FIXTURE); + initUserInternalRegexPlaceholder(); + $userDefaultExternal = $('#application_setting_user_default_external'); + $userInternalRegex = document.querySelector('#application_setting_user_default_internal_regex'); + }); + + describe('Changing of userInternalRegex when userDefaultExternal', () => { + it('is unchecked', () => { + expect($userDefaultExternal.prop('checked')).toBeFalsy(); + expect($userInternalRegex.placeholder).toEqual(PLACEHOLDER_USER_EXTERNAL_DEFAULT_FALSE); + expect($userInternalRegex.readOnly).toBeTruthy(); + }); + + it('is checked', (done) => { + if (!$userDefaultExternal.prop('checked')) $userDefaultExternal.click(); + expect($userDefaultExternal.prop('checked')).toBeTruthy(); + expect($userInternalRegex.placeholder).toEqual(PLACEHOLDER_USER_EXTERNAL_DEFAULT_TRUE); + expect($userInternalRegex.readOnly).toBeFalsy(); + done(); + }); + }); +}); diff --git a/spec/javascripts/pages/admin/users/new/index_spec.js b/spec/javascripts/pages/admin/users/new/index_spec.js new file mode 100644 index 00000000000..2bac3951c3a --- /dev/null +++ b/spec/javascripts/pages/admin/users/new/index_spec.js @@ -0,0 +1,43 @@ +import $ from 'jquery'; +import UserInternalRegexHandler from '~/pages/admin/users/new/index'; + +describe('UserInternalRegexHandler', () => { + const FIXTURE = 'admin/users/new_with_internal_user_regex.html.raw'; + let $userExternal; + let $userEmail; + let $warningMessage; + + preloadFixtures(FIXTURE); + + beforeEach(() => { + loadFixtures(FIXTURE); + // eslint-disable-next-line no-new + new UserInternalRegexHandler(); + $userExternal = $('#user_external'); + $userEmail = $('#user_email'); + $warningMessage = $('#warning_external_automatically_set'); + if (!$userExternal.prop('checked')) $userExternal.prop('checked', 'checked'); + }); + + describe('Behaviour of userExternal checkbox when', () => { + it('matches email as internal', (done) => { + expect($warningMessage.hasClass('hidden')).toBeTruthy(); + + $userEmail.val('test@').trigger('input'); + + expect($userExternal.prop('checked')).toBeFalsy(); + expect($warningMessage.hasClass('hidden')).toBeFalsy(); + done(); + }); + + it('matches email as external', (done) => { + expect($warningMessage.hasClass('hidden')).toBeTruthy(); + + $userEmail.val('test.ext@').trigger('input'); + + expect($userExternal.prop('checked')).toBeTruthy(); + expect($warningMessage.hasClass('hidden')).toBeTruthy(); + done(); + }); + }); +}); diff --git a/spec/javascripts/pdf/page_spec.js b/spec/javascripts/pdf/page_spec.js index ff1bfd7f650..a207f2afce6 100644 --- a/spec/javascripts/pdf/page_spec.js +++ b/spec/javascripts/pdf/page_spec.js @@ -3,53 +3,45 @@ import pdfjsLib from 'vendor/pdf'; import workerSrc from 'vendor/pdf.worker.min'; import PageComponent from '~/pdf/page/index.vue'; -import testPDF from '../fixtures/blob/pdf/test.pdf'; - -const Component = Vue.extend(PageComponent); +import mountComponent from 'spec/helpers/vue_mount_component_helper'; +import testPDF from 'spec/fixtures/blob/pdf/test.pdf'; describe('Page component', () => { + const Component = Vue.extend(PageComponent); let vm; let testPage; - pdfjsLib.PDFJS.workerSrc = workerSrc; - - const checkRendered = (done) => { - if (vm.rendering) { - setTimeout(() => { - checkRendered(done); - }, 100); - } else { - done(); - } - }; - beforeEach((done) => { - pdfjsLib.getDocument(testPDF) + beforeEach(done => { + pdfjsLib.PDFJS.workerSrc = workerSrc; + pdfjsLib + .getDocument(testPDF) .then(pdf => pdf.getPage(1)) - .then((page) => { + .then(page => { testPage = page; - done(); }) - .catch((error) => { - done.fail(error); - }); + .then(done) + .catch(done.fail); }); - describe('render', () => { - beforeEach((done) => { - vm = new Component({ - propsData: { - page: testPage, - number: 1, - }, - }); - - vm.$mount(); + afterEach(() => { + vm.$destroy(); + }); - checkRendered(done); + it('renders the page when mounting', done => { + const promise = Promise.resolve(); + spyOn(testPage, 'render').and.callFake(() => promise); + vm = mountComponent(Component, { + page: testPage, + number: 1, }); + expect(vm.rendering).toBe(true); - it('renders first page', () => { - expect(vm.$el.tagName).toBeDefined(); - }); + promise + .then(() => { + expect(testPage.render).toHaveBeenCalledWith(vm.renderContext); + expect(vm.rendering).toBe(false); + }) + .then(done) + .catch(done.fail); }); }); diff --git a/spec/javascripts/performance_bar/services/performance_bar_service_spec.js b/spec/javascripts/performance_bar/services/performance_bar_service_spec.js new file mode 100644 index 00000000000..bc6947dbe81 --- /dev/null +++ b/spec/javascripts/performance_bar/services/performance_bar_service_spec.js @@ -0,0 +1,63 @@ +import PerformanceBarService from '~/performance_bar/services/performance_bar_service'; + +describe('PerformanceBarService', () => { + describe('callbackParams', () => { + describe('fireCallback', () => { + function fireCallback(response, peekUrl) { + return PerformanceBarService.callbackParams(response, peekUrl)[0]; + } + + it('returns false when the request URL is the peek URL', () => { + expect(fireCallback({ headers: { 'x-request-id': '123' }, url: '/peek' }, '/peek')) + .toBeFalsy(); + }); + + it('returns false when there is no request ID', () => { + expect(fireCallback({ headers: {}, url: '/request' }, '/peek')) + .toBeFalsy(); + }); + + it('returns false when the request is an API request', () => { + expect(fireCallback({ headers: { 'x-request-id': '123' }, url: '/api/' }, '/peek')) + .toBeFalsy(); + }); + + it('returns false when the response is from the cache', () => { + expect(fireCallback({ headers: { 'x-request-id': '123', 'x-gitlab-from-cache': 'true' }, url: '/request' }, '/peek')) + .toBeFalsy(); + }); + + it('returns true when all conditions are met', () => { + expect(fireCallback({ headers: { 'x-request-id': '123' }, url: '/request' }, '/peek')) + .toBeTruthy(); + }); + }); + + describe('requestId', () => { + function requestId(response, peekUrl) { + return PerformanceBarService.callbackParams(response, peekUrl)[1]; + } + + it('gets the request ID from the headers', () => { + expect(requestId({ headers: { 'x-request-id': '123' } }, '/peek')) + .toEqual('123'); + }); + }); + + describe('requestUrl', () => { + function requestUrl(response, peekUrl) { + return PerformanceBarService.callbackParams(response, peekUrl)[2]; + } + + it('gets the request URL from the response object', () => { + expect(requestUrl({ headers: {}, url: '/request' }, '/peek')) + .toEqual('/request'); + }); + + it('gets the request URL from response.config if present', () => { + expect(requestUrl({ headers: {}, config: { url: '/config-url' }, url: '/request' }, '/peek')) + .toEqual('/config-url'); + }); + }); + }); +}); diff --git a/spec/javascripts/pipelines/graph/dropdown_job_component_spec.js b/spec/javascripts/pipelines/graph/dropdown_job_component_spec.js index ff584396d61..2b47ca236b2 100644 --- a/spec/javascripts/pipelines/graph/dropdown_job_component_spec.js +++ b/spec/javascripts/pipelines/graph/dropdown_job_component_spec.js @@ -82,12 +82,4 @@ describe('dropdown job component', () => { it('renders dropdown with jobs', () => { expect(vm.$el.querySelectorAll('.scrollable-menu>ul>li').length).toEqual(mock.jobs.length); }); - - it('escapes tooltip title', () => { - expect( - vm.$el.querySelector('.js-pipeline-graph-job-link').getAttribute('data-original-title'), - ).toEqual( - '<img src=x onerror=alert(document.domain)> - passed', - ); - }); }); diff --git a/spec/javascripts/pipelines/graph/job_component_spec.js b/spec/javascripts/pipelines/graph/job_component_spec.js index 215ce1e81b5..0ae448f2ea8 100644 --- a/spec/javascripts/pipelines/graph/job_component_spec.js +++ b/spec/javascripts/pipelines/graph/job_component_spec.js @@ -161,24 +161,4 @@ describe('pipeline graph job component', () => { expect(component.$el.querySelector(tooltipBoundary)).toBeNull(); }); }); - - describe('tooltipText', () => { - it('escapes job name', () => { - component = mountComponent(JobComponent, { - job: { - id: 4259, - name: '<img src=x onerror=alert(document.domain)>', - status: { - icon: 'status_success', - label: 'success', - tooltip: 'failed', - }, - }, - }); - - expect( - component.$el.querySelector('.js-job-component-tooltip').getAttribute('data-original-title'), - ).toEqual('<img src=x onerror=alert(document.domain)> - failed'); - }); - }); }); diff --git a/spec/lib/gitlab/ci/status/build/factory_spec.rb b/spec/lib/gitlab/ci/status/build/factory_spec.rb index d53a7d468e3..8b92088902b 100644 --- a/spec/lib/gitlab/ci/status/build/factory_spec.rb +++ b/spec/lib/gitlab/ci/status/build/factory_spec.rb @@ -88,7 +88,7 @@ describe Gitlab::Ci::Status::Build::Factory do expect(status.icon).to eq 'status_failed' expect(status.favicon).to eq 'favicon_status_failed' expect(status.label).to eq 'failed' - expect(status.status_tooltip).to eq 'failed <br> (unknown failure)' + expect(status.status_tooltip).to eq 'failed - (unknown failure)' expect(status).to have_details expect(status).to have_action end diff --git a/spec/lib/gitlab/ci/status/build/failed_allowed_spec.rb b/spec/lib/gitlab/ci/status/build/failed_allowed_spec.rb index bfaa508785e..af03d5a1308 100644 --- a/spec/lib/gitlab/ci/status/build/failed_allowed_spec.rb +++ b/spec/lib/gitlab/ci/status/build/failed_allowed_spec.rb @@ -76,7 +76,7 @@ describe Gitlab::Ci::Status::Build::FailedAllowed do let(:status) { described_class.new(build_status) } it 'does override badge_tooltip' do - expect(status.badge_tooltip).to eq('failed <br> (unknown failure)') + expect(status.badge_tooltip).to eq('failed - (unknown failure)') end end @@ -87,7 +87,7 @@ describe Gitlab::Ci::Status::Build::FailedAllowed do let(:status) { described_class.new(build_status) } it 'does override status_tooltip' do - expect(status.status_tooltip).to eq 'failed <br> (unknown failure) (allowed to fail)' + expect(status.status_tooltip).to eq 'failed - (unknown failure) (allowed to fail)' end end diff --git a/spec/lib/gitlab/ci/status/build/failed_spec.rb b/spec/lib/gitlab/ci/status/build/failed_spec.rb index b6676b40fd3..e424270f7c5 100644 --- a/spec/lib/gitlab/ci/status/build/failed_spec.rb +++ b/spec/lib/gitlab/ci/status/build/failed_spec.rb @@ -52,7 +52,7 @@ describe Gitlab::Ci::Status::Build::Failed do let(:status) { Gitlab::Ci::Status::Failed.new(build, user) } it 'does override badge_tooltip' do - expect(subject.badge_tooltip).to eq 'failed <br> (script failure)' + expect(subject.badge_tooltip).to eq 'failed - (script failure)' end end @@ -61,7 +61,7 @@ describe Gitlab::Ci::Status::Build::Failed do let(:status) { Gitlab::Ci::Status::Failed.new(build, user) } it 'does override status_tooltip' do - expect(subject.status_tooltip).to eq 'failed <br> (script failure)' + expect(subject.status_tooltip).to eq 'failed - (script failure)' end end diff --git a/spec/lib/gitlab/ci/status/build/retried_spec.rb b/spec/lib/gitlab/ci/status/build/retried_spec.rb index ee9acaf1c21..76c2fb01e3f 100644 --- a/spec/lib/gitlab/ci/status/build/retried_spec.rb +++ b/spec/lib/gitlab/ci/status/build/retried_spec.rb @@ -66,7 +66,7 @@ describe Gitlab::Ci::Status::Build::Retried do let(:status) { Gitlab::Ci::Status::Build::Failed.new(failed_status) } it 'does override status_tooltip' do - expect(subject.status_tooltip).to eq 'failed <br> (unknown failure) (retried)' + expect(subject.status_tooltip).to eq 'failed - (unknown failure) (retried)' end end diff --git a/spec/lib/gitlab/conflict/file_spec.rb b/spec/lib/gitlab/conflict/file_spec.rb index 5b343920429..9095ffbfd52 100644 --- a/spec/lib/gitlab/conflict/file_spec.rb +++ b/spec/lib/gitlab/conflict/file_spec.rb @@ -69,10 +69,6 @@ describe Gitlab::Conflict::File do CGI.unescapeHTML(ActionView::Base.full_sanitizer.sanitize(html)).delete("\n") end - it 'modifies the existing lines' do - expect { conflict_file.highlight_lines! }.to change { conflict_file.lines.map(&:instance_variables) } - end - it 'is called implicitly when rich_text is accessed on a line' do expect(conflict_file).to receive(:highlight_lines!).once.and_call_original diff --git a/spec/lib/gitlab/diff/highlight_spec.rb b/spec/lib/gitlab/diff/highlight_spec.rb index 7c9e8c8d04e..3c8cf9c56cc 100644 --- a/spec/lib/gitlab/diff/highlight_spec.rb +++ b/spec/lib/gitlab/diff/highlight_spec.rb @@ -24,19 +24,19 @@ describe Gitlab::Diff::Highlight do it 'highlights and marks unchanged lines' do code = %Q{ <span id="LC7" class="line" lang="ruby"> <span class="k">def</span> <span class="nf">popen</span><span class="p">(</span><span class="n">cmd</span><span class="p">,</span> <span class="n">path</span><span class="o">=</span><span class="kp">nil</span><span class="p">)</span></span>\n} - expect(subject[2].text).to eq(code) + expect(subject[2].rich_text).to eq(code) end it 'highlights and marks removed lines' do code = %Q{-<span id="LC9" class="line" lang="ruby"> <span class="k">raise</span> <span class="s2">"System commands must be given as an array of strings"</span></span>\n} - expect(subject[4].text).to eq(code) + expect(subject[4].rich_text).to eq(code) end it 'highlights and marks added lines' do code = %Q{+<span id="LC9" class="line" lang="ruby"> <span class="k">raise</span> <span class="no"><span class="idiff left">RuntimeError</span></span><span class="p"><span class="idiff">,</span></span><span class="idiff right"> </span><span class="s2">"System commands must be given as an array of strings"</span></span>\n} - expect(subject[5].text).to eq(code) + expect(subject[5].rich_text).to eq(code) end end @@ -69,8 +69,8 @@ describe Gitlab::Diff::Highlight do it 'marks added lines' do code = %q{+ raise <span class="idiff left right">RuntimeError, </span>"System commands must be given as an array of strings"} - expect(subject[5].text).to eq(code) - expect(subject[5].text).to be_html_safe + expect(subject[5].rich_text).to eq(code) + expect(subject[5].rich_text).to be_html_safe end context 'when the inline diff marker has an invalid range' do diff --git a/spec/lib/gitlab/diff/line_spec.rb b/spec/lib/gitlab/diff/line_spec.rb new file mode 100644 index 00000000000..76d411973c7 --- /dev/null +++ b/spec/lib/gitlab/diff/line_spec.rb @@ -0,0 +1,22 @@ +describe Gitlab::Diff::Line do + describe '.init_from_hash' do + it 'round-trips correctly with to_hash' do + line = described_class.new('<input>', 'match', 0, 0, 1, + parent_file: double(:file), + line_code: double(:line_code), + rich_text: '<input>') + + expect(described_class.init_from_hash(line.to_hash).to_hash) + .to eq(line.to_hash) + end + end + + context "when setting rich text" do + it 'escapes any HTML special characters in the diff chunk header' do + subject = described_class.new("<input>", "", 0, 0, 0) + line = subject.as_json + + expect(line[:rich_text]).to eq("<input>") + end + end +end diff --git a/spec/lib/gitlab/encoding_helper_spec.rb b/spec/lib/gitlab/encoding_helper_spec.rb index e68c9850f6b..a5bf2f2b3df 100644 --- a/spec/lib/gitlab/encoding_helper_spec.rb +++ b/spec/lib/gitlab/encoding_helper_spec.rb @@ -1,3 +1,4 @@ +# coding: utf-8 require "spec_helper" describe Gitlab::EncodingHelper do @@ -187,4 +188,15 @@ describe Gitlab::EncodingHelper do end end end + + describe '#binary_stringio' do + it 'does not mutate the original string encoding' do + test = 'my-test' + + io_stream = ext_class.binary_stringio(test) + + expect(io_stream.external_encoding.name).to eq('ASCII-8BIT') + expect(test.encoding.name).to eq('UTF-8') + end + end end diff --git a/spec/lib/gitlab/import_export/uploads_manager_spec.rb b/spec/lib/gitlab/import_export/uploads_manager_spec.rb index 9c3870a0af8..f799de18cd0 100644 --- a/spec/lib/gitlab/import_export/uploads_manager_spec.rb +++ b/spec/lib/gitlab/import_export/uploads_manager_spec.rb @@ -36,6 +36,38 @@ describe Gitlab::ImportExport::UploadsManager do expect(File).to exist(exported_file_path) end + + context 'with orphaned project upload files' do + let(:orphan_path) { File.join(FileUploader.absolute_base_dir(project), 'f93f088ddf492ffd950cf059002cbbb6', 'orphan.jpg') } + let(:exported_orphan_path) { "#{shared.export_path}/uploads/f93f088ddf492ffd950cf059002cbbb6/orphan.jpg" } + + before do + FileUtils.mkdir_p(File.dirname(orphan_path)) + FileUtils.touch(orphan_path) + end + + after do + File.delete(orphan_path) if File.exist?(orphan_path) + end + + it 'excludes orphaned upload files' do + manager.save + + expect(File).not_to exist(exported_orphan_path) + end + end + + context 'with an upload missing its file' do + before do + File.delete(upload.absolute_path) + end + + it 'does not cause errors' do + manager.save + + expect(shared.errors).to be_empty + end + end end context 'using object storage' do diff --git a/spec/lib/gitlab/url_blocker_spec.rb b/spec/lib/gitlab/url_blocker_spec.rb index 6f5f9938eca..624e2add860 100644 --- a/spec/lib/gitlab/url_blocker_spec.rb +++ b/spec/lib/gitlab/url_blocker_spec.rb @@ -1,3 +1,4 @@ +# coding: utf-8 require 'spec_helper' describe Gitlab::UrlBlocker do @@ -82,6 +83,17 @@ describe Gitlab::UrlBlocker do expect(described_class).not_to be_blocked_url("http://#{ip}") end end + + it 'allows IPv4 link-local endpoints' do + expect(described_class).not_to be_blocked_url('http://169.254.169.254') + expect(described_class).not_to be_blocked_url('http://169.254.168.100') + end + + # This is blocked due to the hostname check: https://gitlab.com/gitlab-org/gitlab-ce/issues/50227 + it 'blocks IPv6 link-local endpoints' do + expect(described_class).to be_blocked_url('http://[::ffff:169.254.169.254]') + expect(described_class).to be_blocked_url('http://[::ffff:169.254.168.100]') + end end context 'false' do @@ -96,10 +108,21 @@ describe Gitlab::UrlBlocker do expect(described_class).to be_blocked_url("http://#{ip}", allow_local_network: false) end end + + it 'blocks IPv4 link-local endpoints' do + expect(described_class).to be_blocked_url('http://169.254.169.254', allow_local_network: false) + expect(described_class).to be_blocked_url('http://169.254.168.100', allow_local_network: false) + end + + it 'blocks IPv6 link-local endpoints' do + expect(described_class).to be_blocked_url('http://[::ffff:169.254.169.254]', allow_local_network: false) + expect(described_class).to be_blocked_url('http://[::ffff:169.254.168.100]', allow_local_network: false) + expect(described_class).to be_blocked_url('http://[FE80::C800:EFF:FE74:8]', allow_local_network: false) + end end def stub_domain_resolv(domain, ip) - allow(Addrinfo).to receive(:getaddrinfo).with(domain, any_args).and_return([double(ip_address: ip, ipv4_private?: true)]) + allow(Addrinfo).to receive(:getaddrinfo).with(domain, any_args).and_return([double(ip_address: ip, ipv4_private?: true, ipv6_link_local?: false)]) end def unstub_domain_resolv diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb index 02f74e2ea54..483cc546423 100644 --- a/spec/models/application_setting_spec.rb +++ b/spec/models/application_setting_spec.rb @@ -538,4 +538,28 @@ describe ApplicationSetting do expect(setting.allow_signup?).to be_falsey end end + + describe '#user_default_internal_regex_enabled?' do + using RSpec::Parameterized::TableSyntax + + where(:user_default_external, :user_default_internal_regex, :result) do + false | nil | false + false | '' | false + false | '^(?:(?!\.ext@).)*$\r?\n?' | false + true | '' | false + true | nil | false + true | '^(?:(?!\.ext@).)*$\r?\n?' | true + end + + with_them do + before do + setting.update(user_default_external: user_default_external) + setting.update(user_default_internal_regex: user_default_internal_regex) + end + + subject { setting.user_default_internal_regex_enabled? } + + it { is_expected.to eq(result) } + end + end end diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 2859d5149ec..93898012d34 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -2005,6 +2005,24 @@ describe Repository do File.delete(path) end + + context 'for multiple SHAs' do + it 'skips non-existent SHAs' do + repository.keep_around('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', sample_commit.id) + + expect(repository.kept_around?(sample_commit.id)).to be_truthy + end + + it 'skips already-kept-around SHAs' do + repository.keep_around(sample_commit.id) + + expect(repository.raw_repository).to receive(:write_ref).exactly(1).and_call_original + + repository.keep_around(sample_commit.id, another_sample_commit.id) + + expect(repository.kept_around?(another_sample_commit.id)).to be_truthy + end + end end describe '#update_ref' do diff --git a/spec/presenters/ci/build_presenter_spec.rb b/spec/presenters/ci/build_presenter_spec.rb index 6dfaa3b72f7..547d95e0908 100644 --- a/spec/presenters/ci/build_presenter_spec.rb +++ b/spec/presenters/ci/build_presenter_spec.rb @@ -78,7 +78,7 @@ describe Ci::BuildPresenter do it 'returns the reason of failure' do status_title = presenter.status_title - expect(status_title).to eq('Failed <br> (unknown failure)') + expect(status_title).to eq('Failed - (unknown failure)') end end @@ -89,7 +89,7 @@ describe Ci::BuildPresenter do status_title = presenter.status_title expect(status_title).not_to include('(retried)') - expect(status_title).to eq('Failed <br> (unknown failure)') + expect(status_title).to eq('Failed - (unknown failure)') end end @@ -99,7 +99,7 @@ describe Ci::BuildPresenter do it 'returns the reason of failure' do status_title = presenter.status_title - expect(status_title).to eq('Failed <br> (unknown failure)') + expect(status_title).to eq('Failed - (unknown failure)') end end @@ -173,7 +173,7 @@ describe Ci::BuildPresenter do it 'returns the reason of failure' do tooltip = subject.tooltip_message - expect(tooltip).to eq("#{build.name} - failed <br> (script failure)") + expect(tooltip).to eq("#{build.name} - failed - (script failure)") end end @@ -183,7 +183,7 @@ describe Ci::BuildPresenter do it 'should include the reason of failure and the retried title' do tooltip = subject.tooltip_message - expect(tooltip).to eq("#{build.name} - failed <br> (script failure) (retried)") + expect(tooltip).to eq("#{build.name} - failed - (script failure) (retried)") end end @@ -193,7 +193,7 @@ describe Ci::BuildPresenter do it 'should include the reason of failure' do tooltip = subject.tooltip_message - expect(tooltip).to eq("#{build.name} - failed <br> (script failure) (allowed to fail)") + expect(tooltip).to eq("#{build.name} - failed - (script failure) (allowed to fail)") end end diff --git a/spec/routing/admin_routing_spec.rb b/spec/routing/admin_routing_spec.rb index 98df5f787f7..77baaef7afd 100644 --- a/spec/routing/admin_routing_spec.rb +++ b/spec/routing/admin_routing_spec.rb @@ -103,11 +103,11 @@ describe Admin::HooksController, "routing" do end end -# admin_hook_hook_log_retry GET /admin/hooks/:hook_id/hook_logs/:id/retry(.:format) admin/hook_logs#retry +# admin_hook_hook_log_retry POST /admin/hooks/:hook_id/hook_logs/:id/retry(.:format) admin/hook_logs#retry # admin_hook_hook_log GET /admin/hooks/:hook_id/hook_logs/:id(.:format) admin/hook_logs#show describe Admin::HookLogsController, 'routing' do it 'to #retry' do - expect(get('/admin/hooks/1/hook_logs/1/retry')).to route_to('admin/hook_logs#retry', hook_id: '1', id: '1') + expect(post('/admin/hooks/1/hook_logs/1/retry')).to route_to('admin/hook_logs#retry', hook_id: '1', id: '1') end it 'to #show' do diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb index 70a7707826e..5abc6d81958 100644 --- a/spec/routing/project_routing_spec.rb +++ b/spec/routing/project_routing_spec.rb @@ -381,7 +381,7 @@ describe 'project routing' do end end - # test_project_hook GET /:project_id/hooks/:id/test(.:format) hooks#test + # test_project_hook POST /:project_id/hooks/:id/test(.:format) hooks#test # project_hooks GET /:project_id/hooks(.:format) hooks#index # POST /:project_id/hooks(.:format) hooks#create # edit_project_hook GET /:project_id/hooks/:id/edit(.:format) hooks#edit @@ -398,11 +398,11 @@ describe 'project routing' do end end - # retry_namespace_project_hook_hook_log GET /:project_id/hooks/:hook_id/hook_logs/:id/retry(.:format) projects/hook_logs#retry + # retry_namespace_project_hook_hook_log POST /:project_id/hooks/:hook_id/hook_logs/:id/retry(.:format) projects/hook_logs#retry # namespace_project_hook_hook_log GET /:project_id/hooks/:hook_id/hook_logs/:id(.:format) projects/hook_logs#show describe Projects::HookLogsController, 'routing' do it 'to #retry' do - expect(get('/gitlab/gitlabhq/hooks/1/hook_logs/1/retry')).to route_to('projects/hook_logs#retry', namespace_id: 'gitlab', project_id: 'gitlabhq', hook_id: '1', id: '1') + expect(post('/gitlab/gitlabhq/hooks/1/hook_logs/1/retry')).to route_to('projects/hook_logs#retry', namespace_id: 'gitlab', project_id: 'gitlabhq', hook_id: '1', id: '1') end it 'to #show' do diff --git a/spec/rubocop/cop/prefer_class_methods_over_module_spec.rb b/spec/rubocop/cop/prefer_class_methods_over_module_spec.rb new file mode 100644 index 00000000000..4739f0e6c47 --- /dev/null +++ b/spec/rubocop/cop/prefer_class_methods_over_module_spec.rb @@ -0,0 +1,98 @@ +# frozen_string_literal: true + +require 'spec_helper' +require 'rubocop' +require 'rubocop/rspec/support' +require_relative '../../../rubocop/cop/prefer_class_methods_over_module' + +describe RuboCop::Cop::PreferClassMethodsOverModule do + include CopHelper + + subject(:cop) { described_class.new } + + it 'flags violation when using module ClassMethods' do + expect_offense(<<~RUBY) + module Foo + extend ActiveSupport::Concern + + module ClassMethods + ^^^^^^^^^^^^^^^^^^^ Do not use module ClassMethods, use class_methods block instead. + def a_class_method + end + end + end + RUBY + end + + it "doesn't flag violation when using class_methods" do + expect_no_offenses(<<~RUBY) + module Foo + extend ActiveSupport::Concern + + class_methods do + def a_class_method + end + end + end + RUBY + end + + it "doesn't flag violation when module is not extending ActiveSupport::Concern" do + expect_no_offenses(<<~RUBY) + module Foo + module ClassMethods + def a_class_method + end + end + end + RUBY + end + + it "doesn't flag violation when ClassMethods is used inside a class" do + expect_no_offenses(<<~RUBY) + class Foo + module ClassMethods + def a_class_method + end + end + end + RUBY + end + + it "doesn't flag violation when not using either class_methods or ClassMethods" do + expect_no_offenses(<<~RUBY) + module Foo + extend ActiveSupport::Concern + + def a_method + end + end + RUBY + end + + it 'autocorrects ClassMethods into class_methods' do + source = <<~RUBY + module Foo + extend ActiveSupport::Concern + + module ClassMethods + def a_class_method + end + end + end + RUBY + autocorrected = autocorrect_source(source) + + expected_source = <<~RUBY + module Foo + extend ActiveSupport::Concern + + class_methods do + def a_class_method + end + end + end + RUBY + expect(autocorrected).to eq(expected_source) + end +end diff --git a/spec/serializers/build_serializer_spec.rb b/spec/serializers/build_serializer_spec.rb index 52459cd369d..302ef147eb2 100644 --- a/spec/serializers/build_serializer_spec.rb +++ b/spec/serializers/build_serializer_spec.rb @@ -37,7 +37,7 @@ describe BuildSerializer do it 'serializes only status' do expect(subject[:text]).to eq(status.text) expect(subject[:label]).to eq('failed') - expect(subject[:tooltip]).to eq('failed <br> (unknown failure)') + expect(subject[:tooltip]).to eq('failed - (unknown failure)') expect(subject[:icon]).to eq(status.icon) expect(subject[:favicon]).to match_asset_path("/assets/ci_favicons/#{status.favicon}.png") end diff --git a/spec/serializers/job_entity_spec.rb b/spec/serializers/job_entity_spec.rb index a5581a34517..8e1ca3f308d 100644 --- a/spec/serializers/job_entity_spec.rb +++ b/spec/serializers/job_entity_spec.rb @@ -142,7 +142,7 @@ describe JobEntity do end it 'should indicate the failure reason on tooltip' do - expect(subject[:status][:tooltip]).to eq('failed <br> (API failure)') + expect(subject[:status][:tooltip]).to eq('failed - (API failure)') end it 'should include a callout message with a verbose output' do @@ -166,7 +166,7 @@ describe JobEntity do end it 'should indicate the failure reason on tooltip' do - expect(subject[:status][:tooltip]).to eq('failed <br> (API failure) (allowed to fail)') + expect(subject[:status][:tooltip]).to eq('failed - (API failure) (allowed to fail)') end it 'should include a callout message with a verbose output' do diff --git a/spec/services/projects/transfer_service_spec.rb b/spec/services/projects/transfer_service_spec.rb index 1a85c52fc97..92c5ac7354a 100644 --- a/spec/services/projects/transfer_service_spec.rb +++ b/spec/services/projects/transfer_service_spec.rb @@ -169,6 +169,35 @@ describe Projects::TransferService do it { expect(project.errors[:new_namespace]).to include('Cannot move project') } end + context 'target namespace containing the same project name' do + before do + group.add_owner(user) + project.update(name: 'new_name') + + create(:project, name: 'new_name', group: group, path: 'other') + + @result = transfer_project(project, user, group) + end + + it { expect(@result).to eq false } + it { expect(project.namespace).to eq(user.namespace) } + it { expect(project.errors[:new_namespace]).to include('Project with same name or path in target namespace already exists') } + end + + context 'target namespace containing the same project path' do + before do + group.add_owner(user) + + create(:project, name: 'other-name', path: project.path, group: group) + + @result = transfer_project(project, user, group) + end + + it { expect(@result).to eq false } + it { expect(project.namespace).to eq(user.namespace) } + it { expect(project.errors[:new_namespace]).to include('Project with same name or path in target namespace already exists') } + end + def transfer_project(project, user, new_namespace) service = Projects::TransferService.new(project, user) diff --git a/spec/services/users/build_service_spec.rb b/spec/services/users/build_service_spec.rb index 677d4a622e1..b987fe45138 100644 --- a/spec/services/users/build_service_spec.rb +++ b/spec/services/users/build_service_spec.rb @@ -13,6 +13,59 @@ describe Users::BuildService do it 'returns a valid user' do expect(service.execute).to be_valid end + + context 'with "user_default_external" application setting' do + using RSpec::Parameterized::TableSyntax + + where(:user_default_external, :external, :email, :user_default_internal_regex, :result) do + true | nil | 'fl@example.com' | nil | true + true | true | 'fl@example.com' | nil | true + true | false | 'fl@example.com' | nil | false + + true | nil | 'fl@example.com' | '' | true + true | true | 'fl@example.com' | '' | true + true | false | 'fl@example.com' | '' | false + + true | nil | 'fl@example.com' | '^(?:(?!\.ext@).)*$\r?' | false + true | true | 'fl@example.com' | '^(?:(?!\.ext@).)*$\r?' | true + true | false | 'fl@example.com' | '^(?:(?!\.ext@).)*$\r?' | false + + true | nil | 'tester.ext@domain.com' | '^(?:(?!\.ext@).)*$\r?' | true + true | true | 'tester.ext@domain.com' | '^(?:(?!\.ext@).)*$\r?' | true + true | false | 'tester.ext@domain.com' | '^(?:(?!\.ext@).)*$\r?' | false + + false | nil | 'fl@example.com' | nil | false + false | true | 'fl@example.com' | nil | true + false | false | 'fl@example.com' | nil | false + + false | nil | 'fl@example.com' | '' | false + false | true | 'fl@example.com' | '' | true + false | false | 'fl@example.com' | '' | false + + false | nil | 'fl@example.com' | '^(?:(?!\.ext@).)*$\r?' | false + false | true | 'fl@example.com' | '^(?:(?!\.ext@).)*$\r?' | true + false | false | 'fl@example.com' | '^(?:(?!\.ext@).)*$\r?' | false + + false | nil | 'tester.ext@domain.com' | '^(?:(?!\.ext@).)*$\r?' | false + false | true | 'tester.ext@domain.com' | '^(?:(?!\.ext@).)*$\r?' | true + false | false | 'tester.ext@domain.com' | '^(?:(?!\.ext@).)*$\r?' | false + end + + with_them do + before do + stub_application_setting(user_default_external: user_default_external) + stub_application_setting(user_default_internal_regex: user_default_internal_regex) + + params.merge!({ external: external, email: email }.compact) + end + + subject(:user) { service.execute } + + it 'correctly sets user.external' do + expect(user.external).to eq(result) + end + end + end end context 'with non admin user' do @@ -50,6 +103,59 @@ describe Users::BuildService do expect(service.execute).to be_confirmed end end + + context 'with "user_default_external" application setting' do + using RSpec::Parameterized::TableSyntax + + where(:user_default_external, :external, :email, :user_default_internal_regex, :result) do + true | nil | 'fl@example.com' | nil | true + true | true | 'fl@example.com' | nil | true + true | false | 'fl@example.com' | nil | true + + true | nil | 'fl@example.com' | '' | true + true | true | 'fl@example.com' | '' | true + true | false | 'fl@example.com' | '' | true + + true | nil | 'fl@example.com' | '^(?:(?!\.ext@).)*$\r?' | true + true | true | 'fl@example.com' | '^(?:(?!\.ext@).)*$\r?' | true + true | false | 'fl@example.com' | '^(?:(?!\.ext@).)*$\r?' | true + + true | nil | 'tester.ext@domain.com' | '^(?:(?!\.ext@).)*$\r?' | true + true | true | 'tester.ext@domain.com' | '^(?:(?!\.ext@).)*$\r?' | true + true | false | 'tester.ext@domain.com' | '^(?:(?!\.ext@).)*$\r?' | true + + false | nil | 'fl@example.com' | nil | false + false | true | 'fl@example.com' | nil | false + false | false | 'fl@example.com' | nil | false + + false | nil | 'fl@example.com' | '' | false + false | true | 'fl@example.com' | '' | false + false | false | 'fl@example.com' | '' | false + + false | nil | 'fl@example.com' | '^(?:(?!\.ext@).)*$\r?' | false + false | true | 'fl@example.com' | '^(?:(?!\.ext@).)*$\r?' | false + false | false | 'fl@example.com' | '^(?:(?!\.ext@).)*$\r?' | false + + false | nil | 'tester.ext@domain.com' | '^(?:(?!\.ext@).)*$\r?' | false + false | true | 'tester.ext@domain.com' | '^(?:(?!\.ext@).)*$\r?' | false + false | false | 'tester.ext@domain.com' | '^(?:(?!\.ext@).)*$\r?' | false + end + + with_them do + before do + stub_application_setting(user_default_external: user_default_external) + stub_application_setting(user_default_internal_regex: user_default_internal_regex) + + params.merge!({ external: external, email: email }.compact) + end + + subject(:user) { service.execute } + + it 'sets the value of Gitlab::CurrentSettings.user_default_external' do + expect(user.external).to eq(result) + end + end + end end end end diff --git a/spec/support/shared_examples/controllers/repository_lfs_file_load_examples.rb b/spec/support/shared_examples/controllers/repository_lfs_file_load_examples.rb new file mode 100644 index 00000000000..a3d31e26498 --- /dev/null +++ b/spec/support/shared_examples/controllers/repository_lfs_file_load_examples.rb @@ -0,0 +1,89 @@ +# frozen_string_literal: true + +# Shared examples for controllers that load and send files from the git repository +# (like Projects::RawController or Projects::AvatarsController) + +# These examples requires the following variables: +# - `project` +# - `filename`: filename of the file +# - `filepath`: path of the file (contains filename) +# - `subject`: the request to be made to the controller. Example: +# subject { get :show, namespace_id: project.namespace, project_id: project } +shared_examples 'repository lfs file load' do + context 'when file is stored in lfs' do + let(:lfs_oid) { '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897' } + let(:lfs_size) { '1575078' } + let!(:lfs_object) { create(:lfs_object, oid: lfs_oid, size: lfs_size) } + + context 'when lfs is enabled' do + before do + allow_any_instance_of(Project).to receive(:lfs_enabled?).and_return(true) + end + + context 'when project has access' do + before do + project.lfs_objects << lfs_object + allow_any_instance_of(LfsObjectUploader).to receive(:exists?).and_return(true) + allow(controller).to receive(:send_file) { controller.head :ok } + end + + it 'serves the file' do + expect(controller).to receive(:send_file).with("#{LfsObjectUploader.root}/91/ef/f75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897", filename: filename, disposition: 'attachment') + + subject + + expect(response).to have_gitlab_http_status(200) + end + + context 'and lfs uses object storage' do + let(:lfs_object) { create(:lfs_object, :with_file, oid: lfs_oid, size: lfs_size) } + + before do + stub_lfs_object_storage + lfs_object.file.migrate!(LfsObjectUploader::Store::REMOTE) + end + + it 'responds with redirect to file' do + subject + + expect(response).to have_gitlab_http_status(302) + expect(response.location).to include(lfs_object.reload.file.path) + end + + it 'sets content disposition' do + subject + + file_uri = URI.parse(response.location) + params = CGI.parse(file_uri.query) + + expect(params["response-content-disposition"].first).to eq "attachment;filename=\"#{filename}\"" + end + end + end + + context 'when project does not have access' do + it 'does not serve the file' do + subject + + expect(response).to have_gitlab_http_status(404) + end + end + end + + context 'when lfs is not enabled' do + before do + allow_any_instance_of(Project).to receive(:lfs_enabled?).and_return(false) + end + + it 'delivers ASCII file' do + subject + + expect(response).to have_gitlab_http_status(200) + expect(response.header['Content-Type']).to eq('text/plain; charset=utf-8') + expect(response.header['Content-Disposition']) + .to eq('inline') + expect(response.header[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with('git-blob:') + end + end + end +end diff --git a/spec/validators/js_regex_validator_spec.rb b/spec/validators/js_regex_validator_spec.rb new file mode 100644 index 00000000000..aeb55cdc0e5 --- /dev/null +++ b/spec/validators/js_regex_validator_spec.rb @@ -0,0 +1,27 @@ +require 'spec_helper' + +describe JsRegexValidator do + describe '#validates_each' do + using RSpec::Parameterized::TableSyntax + + let(:validator) { described_class.new(attributes: [:user_default_internal_regex]) } + let(:application_setting) { build(:application_setting, user_default_external: true) } + + where(:user_default_internal_regex, :result) do + nil | [] + '' | [] + '(?#comment)' | ['Regex Pattern (?#comment) can not be expressed in Javascript'] + '(?(a)b|c)' | ['invalid conditional pattern: /(?(a)b|c)/i'] + '[a-z&&[^uo]]' | ["Dropped unsupported set intersection '[a-z&&[^uo]]' at index 0", + "Dropped unsupported nested negative set data '[^uo]' at index 6"] + end + + with_them do + it 'generates correct errors' do + validator.validate_each(application_setting, :user_default_internal_regex, user_default_internal_regex) + + expect(application_setting.errors[:user_default_internal_regex]).to eq result + end + end + end +end |