diff options
author | Grzegorz Bizon <grzesiek.bizon@gmail.com> | 2018-05-15 12:01:07 +0200 |
---|---|---|
committer | Grzegorz Bizon <grzesiek.bizon@gmail.com> | 2018-05-15 12:01:07 +0200 |
commit | bbba6d7e62fb9d8bca635d57604fd503fb3c4645 (patch) | |
tree | 75e6d810370791ba991d007ab67b0ca265ab7ec6 | |
parent | f16f2b599412ed1514ba96d81758b9a2e6fd9c1f (diff) | |
parent | a78b1b27b86d34c00e1b0631e967d637f8a6714b (diff) | |
download | gitlab-ce-bbba6d7e62fb9d8bca635d57604fd503fb3c4645.tar.gz |
Merge branch 'master' into feature/gb/add-regexp-variables-expression
* master: (76 commits)
Conflicts:
lib/gitlab/untrusted_regexp.rb
163 files changed, 2120 insertions, 3833 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 05487134cb1..b1445feee58 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -10,6 +10,7 @@ image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.7-golang-1.9-git paths: - vendor/ruby - .yarn-cache/ + - vendor/gitaly-ruby .push-cache: &push-cache cache: @@ -30,7 +31,6 @@ variables: GIT_SUBMODULE_STRATEGY: "none" GET_SOURCES_ATTEMPTS: "3" KNAPSACK_RSPEC_SUITE_REPORT_PATH: knapsack/${CI_PROJECT_NAME}/rspec_report-master.json - KNAPSACK_SPINACH_SUITE_REPORT_PATH: knapsack/${CI_PROJECT_NAME}/spinach_report-master.json FLAKY_RSPEC_SUITE_REPORT_PATH: rspec_flaky/report-suite.json before_script: @@ -178,46 +178,6 @@ stages: <<: *rspec-metadata-mysql <<: *rails5 -.spinach-metadata: &spinach-metadata - <<: *dedicated-runner - <<: *except-docs-and-qa - <<: *pull-cache - <<: *rails5-variables - stage: test - script: - - JOB_NAME=( $CI_JOB_NAME ) - - export CI_NODE_INDEX=${JOB_NAME[-2]} - - export CI_NODE_TOTAL=${JOB_NAME[-1]} - - export KNAPSACK_REPORT_PATH=knapsack/${CI_PROJECT_NAME}/${JOB_NAME[0]}_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json - - export KNAPSACK_GENERATE_REPORT=true - - export CACHE_CLASSES=true - - cp ${KNAPSACK_SPINACH_SUITE_REPORT_PATH} ${KNAPSACK_REPORT_PATH} - - scripts/gitaly-test-spawn - - knapsack spinach "-r rerun" -b || retry '[[ -e tmp/spinach-rerun.txt ]] && bundle exec spinach -b -r rerun $(cat tmp/spinach-rerun.txt)' - artifacts: - expire_in: 31d - when: always - paths: - - coverage/ - - knapsack/ - - tmp/capybara/ - -.spinach-metadata-pg: &spinach-metadata-pg - <<: *spinach-metadata - <<: *use-pg - -.spinach-metadata-pg-rails5: &spinach-metadata-pg-rails5 - <<: *spinach-metadata-pg - <<: *rails5 - -.spinach-metadata-mysql: &spinach-metadata-mysql - <<: *spinach-metadata - <<: *use-mysql - -.spinach-metadata-mysql-rails5: &spinach-metadata-mysql-rails5 - <<: *spinach-metadata-mysql - <<: *rails5 - .only-canonical-masters: &only-canonical-masters only: - master@gitlab-org/gitlab-ce @@ -349,9 +309,7 @@ retrieve-tests-metadata: script: - mkdir -p knapsack/${CI_PROJECT_NAME}/ - wget -O $KNAPSACK_RSPEC_SUITE_REPORT_PATH http://${TESTS_METADATA_S3_BUCKET}.s3.amazonaws.com/$KNAPSACK_RSPEC_SUITE_REPORT_PATH || rm $KNAPSACK_RSPEC_SUITE_REPORT_PATH - - wget -O $KNAPSACK_SPINACH_SUITE_REPORT_PATH http://${TESTS_METADATA_S3_BUCKET}.s3.amazonaws.com/$KNAPSACK_SPINACH_SUITE_REPORT_PATH || rm $KNAPSACK_SPINACH_SUITE_REPORT_PATH - '[[ -f $KNAPSACK_RSPEC_SUITE_REPORT_PATH ]] || echo "{}" > ${KNAPSACK_RSPEC_SUITE_REPORT_PATH}' - - '[[ -f $KNAPSACK_SPINACH_SUITE_REPORT_PATH ]] || echo "{}" > ${KNAPSACK_SPINACH_SUITE_REPORT_PATH}' - mkdir -p rspec_flaky/ - wget -O $FLAKY_RSPEC_SUITE_REPORT_PATH http://${TESTS_METADATA_S3_BUCKET}.s3.amazonaws.com/$FLAKY_RSPEC_SUITE_REPORT_PATH || rm $FLAKY_RSPEC_SUITE_REPORT_PATH - '[[ -f $FLAKY_RSPEC_SUITE_REPORT_PATH ]] || echo "{}" > ${FLAKY_RSPEC_SUITE_REPORT_PATH}' @@ -369,10 +327,9 @@ update-tests-metadata: script: - retry gem install fog-aws mime-types activesupport - scripts/merge-reports ${KNAPSACK_RSPEC_SUITE_REPORT_PATH} knapsack/${CI_PROJECT_NAME}/rspec-pg_node_*.json - - scripts/merge-reports ${KNAPSACK_SPINACH_SUITE_REPORT_PATH} knapsack/${CI_PROJECT_NAME}/spinach-pg_node_*.json - scripts/merge-reports ${FLAKY_RSPEC_SUITE_REPORT_PATH} rspec_flaky/all_*_*.json - FLAKY_RSPEC_GENERATE_REPORT=1 scripts/prune-old-flaky-specs ${FLAKY_RSPEC_SUITE_REPORT_PATH} - - '[[ -z ${TESTS_METADATA_S3_BUCKET} ]] || scripts/sync-reports put $TESTS_METADATA_S3_BUCKET $KNAPSACK_RSPEC_SUITE_REPORT_PATH $KNAPSACK_SPINACH_SUITE_REPORT_PATH' + - '[[ -z ${TESTS_METADATA_S3_BUCKET} ]] || scripts/sync-reports put $TESTS_METADATA_S3_BUCKET $KNAPSACK_RSPEC_SUITE_REPORT_PATH' - '[[ -z ${TESTS_METADATA_S3_BUCKET} ]] || scripts/sync-reports put $TESTS_METADATA_S3_BUCKET $FLAKY_RSPEC_SUITE_REPORT_PATH' - rm -f knapsack/${CI_PROJECT_NAME}/*_node_*.json - rm -f rspec_flaky/all_*.json rspec_flaky/new_*.json @@ -438,134 +395,131 @@ setup-test-env: paths: - tmp/tests - config/secrets.yml - -rspec-pg 0 28: *rspec-metadata-pg -rspec-pg 1 28: *rspec-metadata-pg -rspec-pg 2 28: *rspec-metadata-pg -rspec-pg 3 28: *rspec-metadata-pg -rspec-pg 4 28: *rspec-metadata-pg -rspec-pg 5 28: *rspec-metadata-pg -rspec-pg 6 28: *rspec-metadata-pg -rspec-pg 7 28: *rspec-metadata-pg -rspec-pg 8 28: *rspec-metadata-pg -rspec-pg 9 28: *rspec-metadata-pg -rspec-pg 10 28: *rspec-metadata-pg -rspec-pg 11 28: *rspec-metadata-pg -rspec-pg 12 28: *rspec-metadata-pg -rspec-pg 13 28: *rspec-metadata-pg -rspec-pg 14 28: *rspec-metadata-pg -rspec-pg 15 28: *rspec-metadata-pg -rspec-pg 16 28: *rspec-metadata-pg -rspec-pg 17 28: *rspec-metadata-pg -rspec-pg 18 28: *rspec-metadata-pg -rspec-pg 19 28: *rspec-metadata-pg -rspec-pg 20 28: *rspec-metadata-pg -rspec-pg 21 28: *rspec-metadata-pg -rspec-pg 22 28: *rspec-metadata-pg -rspec-pg 23 28: *rspec-metadata-pg -rspec-pg 24 28: *rspec-metadata-pg -rspec-pg 25 28: *rspec-metadata-pg -rspec-pg 26 28: *rspec-metadata-pg -rspec-pg 27 28: *rspec-metadata-pg - -rspec-mysql 0 28: *rspec-metadata-mysql -rspec-mysql 1 28: *rspec-metadata-mysql -rspec-mysql 2 28: *rspec-metadata-mysql -rspec-mysql 3 28: *rspec-metadata-mysql -rspec-mysql 4 28: *rspec-metadata-mysql -rspec-mysql 5 28: *rspec-metadata-mysql -rspec-mysql 6 28: *rspec-metadata-mysql -rspec-mysql 7 28: *rspec-metadata-mysql -rspec-mysql 8 28: *rspec-metadata-mysql -rspec-mysql 9 28: *rspec-metadata-mysql -rspec-mysql 10 28: *rspec-metadata-mysql -rspec-mysql 11 28: *rspec-metadata-mysql -rspec-mysql 12 28: *rspec-metadata-mysql -rspec-mysql 13 28: *rspec-metadata-mysql -rspec-mysql 14 28: *rspec-metadata-mysql -rspec-mysql 15 28: *rspec-metadata-mysql -rspec-mysql 16 28: *rspec-metadata-mysql -rspec-mysql 17 28: *rspec-metadata-mysql -rspec-mysql 18 28: *rspec-metadata-mysql -rspec-mysql 19 28: *rspec-metadata-mysql -rspec-mysql 20 28: *rspec-metadata-mysql -rspec-mysql 21 28: *rspec-metadata-mysql -rspec-mysql 22 28: *rspec-metadata-mysql -rspec-mysql 23 28: *rspec-metadata-mysql -rspec-mysql 24 28: *rspec-metadata-mysql -rspec-mysql 25 28: *rspec-metadata-mysql -rspec-mysql 26 28: *rspec-metadata-mysql -rspec-mysql 27 28: *rspec-metadata-mysql - -spinach-pg 0 2: *spinach-metadata-pg -spinach-pg 1 2: *spinach-metadata-pg - -spinach-mysql 0 2: *spinach-metadata-mysql -spinach-mysql 1 2: *spinach-metadata-mysql - -rspec-pg-rails5 0 28: *rspec-metadata-pg-rails5 -rspec-pg-rails5 1 28: *rspec-metadata-pg-rails5 -rspec-pg-rails5 2 28: *rspec-metadata-pg-rails5 -rspec-pg-rails5 3 28: *rspec-metadata-pg-rails5 -rspec-pg-rails5 4 28: *rspec-metadata-pg-rails5 -rspec-pg-rails5 5 28: *rspec-metadata-pg-rails5 -rspec-pg-rails5 6 28: *rspec-metadata-pg-rails5 -rspec-pg-rails5 7 28: *rspec-metadata-pg-rails5 -rspec-pg-rails5 8 28: *rspec-metadata-pg-rails5 -rspec-pg-rails5 9 28: *rspec-metadata-pg-rails5 -rspec-pg-rails5 10 28: *rspec-metadata-pg-rails5 -rspec-pg-rails5 11 28: *rspec-metadata-pg-rails5 -rspec-pg-rails5 12 28: *rspec-metadata-pg-rails5 -rspec-pg-rails5 13 28: *rspec-metadata-pg-rails5 -rspec-pg-rails5 14 28: *rspec-metadata-pg-rails5 -rspec-pg-rails5 15 28: *rspec-metadata-pg-rails5 -rspec-pg-rails5 16 28: *rspec-metadata-pg-rails5 -rspec-pg-rails5 17 28: *rspec-metadata-pg-rails5 -rspec-pg-rails5 18 28: *rspec-metadata-pg-rails5 -rspec-pg-rails5 19 28: *rspec-metadata-pg-rails5 -rspec-pg-rails5 20 28: *rspec-metadata-pg-rails5 -rspec-pg-rails5 21 28: *rspec-metadata-pg-rails5 -rspec-pg-rails5 22 28: *rspec-metadata-pg-rails5 -rspec-pg-rails5 23 28: *rspec-metadata-pg-rails5 -rspec-pg-rails5 24 28: *rspec-metadata-pg-rails5 -rspec-pg-rails5 25 28: *rspec-metadata-pg-rails5 -rspec-pg-rails5 26 28: *rspec-metadata-pg-rails5 -rspec-pg-rails5 27 28: *rspec-metadata-pg-rails5 - -rspec-mysql-rails5 0 28: *rspec-metadata-mysql-rails5 -rspec-mysql-rails5 1 28: *rspec-metadata-mysql-rails5 -rspec-mysql-rails5 2 28: *rspec-metadata-mysql-rails5 -rspec-mysql-rails5 3 28: *rspec-metadata-mysql-rails5 -rspec-mysql-rails5 4 28: *rspec-metadata-mysql-rails5 -rspec-mysql-rails5 5 28: *rspec-metadata-mysql-rails5 -rspec-mysql-rails5 6 28: *rspec-metadata-mysql-rails5 -rspec-mysql-rails5 7 28: *rspec-metadata-mysql-rails5 -rspec-mysql-rails5 8 28: *rspec-metadata-mysql-rails5 -rspec-mysql-rails5 9 28: *rspec-metadata-mysql-rails5 -rspec-mysql-rails5 10 28: *rspec-metadata-mysql-rails5 -rspec-mysql-rails5 11 28: *rspec-metadata-mysql-rails5 -rspec-mysql-rails5 12 28: *rspec-metadata-mysql-rails5 -rspec-mysql-rails5 13 28: *rspec-metadata-mysql-rails5 -rspec-mysql-rails5 14 28: *rspec-metadata-mysql-rails5 -rspec-mysql-rails5 15 28: *rspec-metadata-mysql-rails5 -rspec-mysql-rails5 16 28: *rspec-metadata-mysql-rails5 -rspec-mysql-rails5 17 28: *rspec-metadata-mysql-rails5 -rspec-mysql-rails5 18 28: *rspec-metadata-mysql-rails5 -rspec-mysql-rails5 19 28: *rspec-metadata-mysql-rails5 -rspec-mysql-rails5 20 28: *rspec-metadata-mysql-rails5 -rspec-mysql-rails5 21 28: *rspec-metadata-mysql-rails5 -rspec-mysql-rails5 22 28: *rspec-metadata-mysql-rails5 -rspec-mysql-rails5 23 28: *rspec-metadata-mysql-rails5 -rspec-mysql-rails5 24 28: *rspec-metadata-mysql-rails5 -rspec-mysql-rails5 25 28: *rspec-metadata-mysql-rails5 -rspec-mysql-rails5 26 28: *rspec-metadata-mysql-rails5 -rspec-mysql-rails5 27 28: *rspec-metadata-mysql-rails5 - -spinach-pg-rails5 0 2: *spinach-metadata-pg-rails5 -spinach-pg-rails5 1 2: *spinach-metadata-pg-rails5 - -spinach-mysql-rails5 0 2: *spinach-metadata-mysql-rails5 -spinach-mysql-rails5 1 2: *spinach-metadata-mysql-rails5 + - vendor/gitaly-ruby + +rspec-pg 0 30: *rspec-metadata-pg +rspec-pg 1 30: *rspec-metadata-pg +rspec-pg 2 30: *rspec-metadata-pg +rspec-pg 3 30: *rspec-metadata-pg +rspec-pg 4 30: *rspec-metadata-pg +rspec-pg 5 30: *rspec-metadata-pg +rspec-pg 6 30: *rspec-metadata-pg +rspec-pg 7 30: *rspec-metadata-pg +rspec-pg 8 30: *rspec-metadata-pg +rspec-pg 9 30: *rspec-metadata-pg +rspec-pg 10 30: *rspec-metadata-pg +rspec-pg 11 30: *rspec-metadata-pg +rspec-pg 12 30: *rspec-metadata-pg +rspec-pg 13 30: *rspec-metadata-pg +rspec-pg 14 30: *rspec-metadata-pg +rspec-pg 15 30: *rspec-metadata-pg +rspec-pg 16 30: *rspec-metadata-pg +rspec-pg 17 30: *rspec-metadata-pg +rspec-pg 18 30: *rspec-metadata-pg +rspec-pg 19 30: *rspec-metadata-pg +rspec-pg 20 30: *rspec-metadata-pg +rspec-pg 21 30: *rspec-metadata-pg +rspec-pg 22 30: *rspec-metadata-pg +rspec-pg 23 30: *rspec-metadata-pg +rspec-pg 24 30: *rspec-metadata-pg +rspec-pg 25 30: *rspec-metadata-pg +rspec-pg 26 30: *rspec-metadata-pg +rspec-pg 27 30: *rspec-metadata-pg +rspec-pg 28 30: *rspec-metadata-pg +rspec-pg 29 30: *rspec-metadata-pg + +rspec-mysql 0 30: *rspec-metadata-mysql +rspec-mysql 1 30: *rspec-metadata-mysql +rspec-mysql 2 30: *rspec-metadata-mysql +rspec-mysql 3 30: *rspec-metadata-mysql +rspec-mysql 4 30: *rspec-metadata-mysql +rspec-mysql 5 30: *rspec-metadata-mysql +rspec-mysql 6 30: *rspec-metadata-mysql +rspec-mysql 7 30: *rspec-metadata-mysql +rspec-mysql 8 30: *rspec-metadata-mysql +rspec-mysql 9 30: *rspec-metadata-mysql +rspec-mysql 10 30: *rspec-metadata-mysql +rspec-mysql 11 30: *rspec-metadata-mysql +rspec-mysql 12 30: *rspec-metadata-mysql +rspec-mysql 13 30: *rspec-metadata-mysql +rspec-mysql 14 30: *rspec-metadata-mysql +rspec-mysql 15 30: *rspec-metadata-mysql +rspec-mysql 16 30: *rspec-metadata-mysql +rspec-mysql 17 30: *rspec-metadata-mysql +rspec-mysql 18 30: *rspec-metadata-mysql +rspec-mysql 19 30: *rspec-metadata-mysql +rspec-mysql 20 30: *rspec-metadata-mysql +rspec-mysql 21 30: *rspec-metadata-mysql +rspec-mysql 22 30: *rspec-metadata-mysql +rspec-mysql 23 30: *rspec-metadata-mysql +rspec-mysql 24 30: *rspec-metadata-mysql +rspec-mysql 25 30: *rspec-metadata-mysql +rspec-mysql 26 30: *rspec-metadata-mysql +rspec-mysql 27 30: *rspec-metadata-mysql +rspec-mysql 28 30: *rspec-metadata-mysql +rspec-mysql 29 30: *rspec-metadata-mysql + +rspec-pg-rails5 0 30: *rspec-metadata-pg-rails5 +rspec-pg-rails5 1 30: *rspec-metadata-pg-rails5 +rspec-pg-rails5 2 30: *rspec-metadata-pg-rails5 +rspec-pg-rails5 3 30: *rspec-metadata-pg-rails5 +rspec-pg-rails5 4 30: *rspec-metadata-pg-rails5 +rspec-pg-rails5 5 30: *rspec-metadata-pg-rails5 +rspec-pg-rails5 6 30: *rspec-metadata-pg-rails5 +rspec-pg-rails5 7 30: *rspec-metadata-pg-rails5 +rspec-pg-rails5 8 30: *rspec-metadata-pg-rails5 +rspec-pg-rails5 9 30: *rspec-metadata-pg-rails5 +rspec-pg-rails5 10 30: *rspec-metadata-pg-rails5 +rspec-pg-rails5 11 30: *rspec-metadata-pg-rails5 +rspec-pg-rails5 12 30: *rspec-metadata-pg-rails5 +rspec-pg-rails5 13 30: *rspec-metadata-pg-rails5 +rspec-pg-rails5 14 30: *rspec-metadata-pg-rails5 +rspec-pg-rails5 15 30: *rspec-metadata-pg-rails5 +rspec-pg-rails5 16 30: *rspec-metadata-pg-rails5 +rspec-pg-rails5 17 30: *rspec-metadata-pg-rails5 +rspec-pg-rails5 18 30: *rspec-metadata-pg-rails5 +rspec-pg-rails5 19 30: *rspec-metadata-pg-rails5 +rspec-pg-rails5 20 30: *rspec-metadata-pg-rails5 +rspec-pg-rails5 21 30: *rspec-metadata-pg-rails5 +rspec-pg-rails5 22 30: *rspec-metadata-pg-rails5 +rspec-pg-rails5 23 30: *rspec-metadata-pg-rails5 +rspec-pg-rails5 24 30: *rspec-metadata-pg-rails5 +rspec-pg-rails5 25 30: *rspec-metadata-pg-rails5 +rspec-pg-rails5 26 30: *rspec-metadata-pg-rails5 +rspec-pg-rails5 27 30: *rspec-metadata-pg-rails5 +rspec-pg-rails5 28 30: *rspec-metadata-pg-rails5 +rspec-pg-rails5 29 30: *rspec-metadata-pg-rails5 + +rspec-mysql-rails5 0 30: *rspec-metadata-mysql-rails5 +rspec-mysql-rails5 1 30: *rspec-metadata-mysql-rails5 +rspec-mysql-rails5 2 30: *rspec-metadata-mysql-rails5 +rspec-mysql-rails5 3 30: *rspec-metadata-mysql-rails5 +rspec-mysql-rails5 4 30: *rspec-metadata-mysql-rails5 +rspec-mysql-rails5 5 30: *rspec-metadata-mysql-rails5 +rspec-mysql-rails5 6 30: *rspec-metadata-mysql-rails5 +rspec-mysql-rails5 7 30: *rspec-metadata-mysql-rails5 +rspec-mysql-rails5 8 30: *rspec-metadata-mysql-rails5 +rspec-mysql-rails5 9 30: *rspec-metadata-mysql-rails5 +rspec-mysql-rails5 10 30: *rspec-metadata-mysql-rails5 +rspec-mysql-rails5 11 30: *rspec-metadata-mysql-rails5 +rspec-mysql-rails5 12 30: *rspec-metadata-mysql-rails5 +rspec-mysql-rails5 13 30: *rspec-metadata-mysql-rails5 +rspec-mysql-rails5 14 30: *rspec-metadata-mysql-rails5 +rspec-mysql-rails5 15 30: *rspec-metadata-mysql-rails5 +rspec-mysql-rails5 16 30: *rspec-metadata-mysql-rails5 +rspec-mysql-rails5 17 30: *rspec-metadata-mysql-rails5 +rspec-mysql-rails5 18 30: *rspec-metadata-mysql-rails5 +rspec-mysql-rails5 19 30: *rspec-metadata-mysql-rails5 +rspec-mysql-rails5 20 30: *rspec-metadata-mysql-rails5 +rspec-mysql-rails5 21 30: *rspec-metadata-mysql-rails5 +rspec-mysql-rails5 22 30: *rspec-metadata-mysql-rails5 +rspec-mysql-rails5 23 30: *rspec-metadata-mysql-rails5 +rspec-mysql-rails5 24 30: *rspec-metadata-mysql-rails5 +rspec-mysql-rails5 25 30: *rspec-metadata-mysql-rails5 +rspec-mysql-rails5 26 30: *rspec-metadata-mysql-rails5 +rspec-mysql-rails5 27 30: *rspec-metadata-mysql-rails5 +rspec-mysql-rails5 28 30: *rspec-metadata-mysql-rails5 +rspec-mysql-rails5 29 30: *rspec-metadata-mysql-rails5 static-analysis: <<: *dedicated-no-docs-no-db-pull-cache-job diff --git a/.gitlab/merge_request_templates/Database Changes.md b/.gitlab/merge_request_templates/Database Changes.md index 2bb1f374e98..b81900f67e1 100644 --- a/.gitlab/merge_request_templates/Database Changes.md +++ b/.gitlab/merge_request_templates/Database Changes.md @@ -37,9 +37,9 @@ When removing columns, tables, indexes or other structures: - [ ] [Documentation created/updated](https://docs.gitlab.com/ee/development/doc_styleguide.html) - [ ] API support added - [ ] Tests added for this feature/bug -- Review - - [ ] Has been reviewed by Backend - - [ ] Has been reviewed by Database +- Conform by the [code review guidelines](https://docs.gitlab.com/ee/development/code_review.html) + - [ ] Has been reviewed by a Backend maintainer + - [ ] Has been reviewed by a Database specialist - [ ] Conform by the [merge request performance guides](https://docs.gitlab.com/ee/development/merge_request_performance_guidelines.html) - [ ] Conform by the [style guides](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/CONTRIBUTING.md#style-guides) - [ ] [Squashed related commits together](https://git-scm.com/book/en/Git-Tools-Rewriting-History#Squashing-Commits) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4f5d19ce2ce..d82f21fe795 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -728,6 +728,3 @@ available at [http://contributor-covenant.org/version/1/1/0/](http://contributor [polling-etag]: https://docs.gitlab.com/ce/development/polling.html [testing]: doc/development/testing_guide/index.md [us-english]: https://en.wikipedia.org/wiki/American_English - -[^1]: Please note that specs other than JavaScript specs are considered backend - code. @@ -6,7 +6,7 @@ end gem_versions = {} gem_versions['activerecord_sane_schema_dumper'] = rails5? ? '1.0' : '0.2' gem_versions['default_value_for'] = rails5? ? '~> 3.0.5' : '~> 3.0.0' -gem_versions['rails'] = rails5? ? '5.0.6' : '4.2.10' +gem_versions['rails'] = rails5? ? '5.0.7' : '4.2.10' gem_versions['rails-i18n'] = rails5? ? '~> 5.1' : '~> 4.0.9' # --- The end of special code for migrating to Rails 5.0 --- @@ -160,7 +160,7 @@ gem 'state_machines-activerecord', '~> 0.5.1' gem 'acts-as-taggable-on', '~> 5.0' # Background jobs -gem 'sidekiq', '~> 5.0' +gem 'sidekiq', '~> 5.1' gem 'sidekiq-cron', '~> 0.6.0' gem 'redis-namespace', '~> 1.5.2' gem 'sidekiq-limit_fetch', '~> 3.4', require: false @@ -325,8 +325,6 @@ group :development, :test do gem 'factory_bot_rails', '~> 4.8.2' gem 'rspec-rails', '~> 3.6.0' gem 'rspec-retry', '~> 0.4.5' - gem 'spinach-rails', '~> 0.2.1' - gem 'spinach-rerun-reporter', '~> 0.0.2' gem 'rspec_profiling', '~> 0.0.5' gem 'rspec-set', '~> 0.1.3' gem 'rspec-parameterized', require: false @@ -343,7 +341,6 @@ group :development, :test do gem 'spring', '~> 2.0.0' gem 'spring-commands-rspec', '~> 1.0.4' - gem 'spring-commands-spinach', '~> 1.1.0' gem 'gitlab-styles', '~> 2.3', require: false # Pin these dependencies, otherwise a new rule could break the CI pipelines diff --git a/Gemfile.lock b/Gemfile.lock index 6028ce32d2f..18c25cc34b6 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -131,7 +131,6 @@ GEM coderay (1.1.1) coercible (1.0.0) descendants_tracker (~> 0.0.1) - colorize (0.7.7) commonmarker (0.17.8) ruby-enum (~> 0.5) concord (0.1.5) @@ -288,7 +287,6 @@ GEM gettext_i18n_rails (>= 0.7.1) po_to_json (>= 1.0.0) rails (>= 3.2.0) - gherkin-ruby (0.3.2) gitaly-proto (0.99.0) google-protobuf (~> 3.1) grpc (~> 1.10) @@ -846,11 +844,11 @@ GEM rack shoulda-matchers (3.1.2) activesupport (>= 4.0.0) - sidekiq (5.0.5) + sidekiq (5.1.3) concurrent-ruby (~> 1.0) connection_pool (~> 2.2, >= 2.2.0) rack-protection (>= 1.5.0) - redis (>= 3.3.4, < 5) + redis (>= 3.3.5, < 5) sidekiq-cron (0.6.0) rufus-scheduler (>= 3.3.0) sidekiq (>= 4.2.1) @@ -869,22 +867,10 @@ GEM simplecov-html (0.10.0) slack-notifier (1.5.1) slop (3.6.0) - spinach (0.8.10) - colorize - gherkin-ruby (>= 0.3.2) - json - spinach-rails (0.2.1) - capybara (>= 2.0.0) - railties (>= 3) - spinach (>= 0.4) - spinach-rerun-reporter (0.0.2) - spinach (~> 0.8) spring (2.0.1) activesupport (>= 4.2) spring-commands-rspec (1.0.4) spring (>= 0.9.1) - spring-commands-spinach (1.1.0) - spring (>= 0.9.1) sprockets (3.7.1) concurrent-ruby (~> 1.0) rack (> 1, < 3) @@ -1177,17 +1163,14 @@ DEPENDENCIES settingslogic (~> 2.0.9) sham_rack (~> 1.3.6) shoulda-matchers (~> 3.1.2) - sidekiq (~> 5.0) + sidekiq (~> 5.1) sidekiq-cron (~> 0.6.0) sidekiq-limit_fetch (~> 3.4) simple_po_parser (~> 1.1.2) simplecov (~> 0.14.0) slack-notifier (~> 1.5.1) - spinach-rails (~> 0.2.1) - spinach-rerun-reporter (~> 0.0.2) spring (~> 2.0.0) spring-commands-rspec (~> 1.0.4) - spring-commands-spinach (~> 1.1.0) sprockets (~> 3.7.0) sshkey (~> 1.9.0) stackprof (~> 0.2.10) diff --git a/Gemfile.rails5.lock b/Gemfile.rails5.lock index 3056b97ccd5..af7305619eb 100644 --- a/Gemfile.rails5.lock +++ b/Gemfile.rails5.lock @@ -4,43 +4,43 @@ GEM RedCloth (4.3.2) abstract_type (0.0.7) ace-rails-ap (4.1.4) - actioncable (5.0.6) - actionpack (= 5.0.6) + actioncable (5.0.7) + actionpack (= 5.0.7) nio4r (>= 1.2, < 3.0) websocket-driver (~> 0.6.1) - actionmailer (5.0.6) - actionpack (= 5.0.6) - actionview (= 5.0.6) - activejob (= 5.0.6) + actionmailer (5.0.7) + actionpack (= 5.0.7) + actionview (= 5.0.7) + activejob (= 5.0.7) mail (~> 2.5, >= 2.5.4) rails-dom-testing (~> 2.0) - actionpack (5.0.6) - actionview (= 5.0.6) - activesupport (= 5.0.6) + actionpack (5.0.7) + actionview (= 5.0.7) + activesupport (= 5.0.7) rack (~> 2.0) rack-test (~> 0.6.3) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.0.2) - actionview (5.0.6) - activesupport (= 5.0.6) + actionview (5.0.7) + activesupport (= 5.0.7) builder (~> 3.1) erubis (~> 2.7.0) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.0.3) - activejob (5.0.6) - activesupport (= 5.0.6) + activejob (5.0.7) + activesupport (= 5.0.7) globalid (>= 0.3.6) - activemodel (5.0.6) - activesupport (= 5.0.6) - activerecord (5.0.6) - activemodel (= 5.0.6) - activesupport (= 5.0.6) + activemodel (5.0.7) + activesupport (= 5.0.7) + activerecord (5.0.7) + activemodel (= 5.0.7) + activesupport (= 5.0.7) arel (~> 7.0) activerecord_sane_schema_dumper (1.0) rails (>= 5, < 6) - activesupport (5.0.6) + activesupport (5.0.7) concurrent-ruby (~> 1.0, >= 1.0.2) - i18n (~> 0.7) + i18n (>= 0.7, < 2) minitest (~> 5.1) tzinfo (~> 1.1) acts-as-taggable-on (5.0.0) @@ -62,13 +62,13 @@ GEM asciidoctor (1.5.6.1) asciidoctor-plantuml (0.0.8) asciidoctor (~> 1.5) - asset_sync (2.2.0) + asset_sync (2.4.0) activemodel (>= 4.1.0) fog-core mime-types (>= 2.99) unf ast (2.4.0) - atomic (1.1.100) + atomic (1.1.99) attr_encrypted (3.1.0) encryptor (~> 3.0.0) attr_required (1.0.1) @@ -132,7 +132,6 @@ GEM coderay (1.1.2) coercible (1.0.0) descendants_tracker (~> 0.0.1) - colorize (0.8.1) commonmarker (0.17.9) ruby-enum (~> 0.5) concord (0.1.5) @@ -144,12 +143,10 @@ GEM connection_pool (2.2.1) crack (0.4.3) safe_yaml (~> 1.0.0) - crass (1.0.3) + crass (1.0.4) creole (0.5.0) css_parser (1.6.0) addressable - d3_rails (3.5.17) - railties (>= 3.1.0) daemons (1.2.6) database_cleaner (1.5.3) debug_inspector (0.0.3) @@ -291,8 +288,7 @@ GEM gettext_i18n_rails (>= 0.7.1) po_to_json (>= 1.0.0) rails (>= 3.2.0) - gherkin-ruby (0.3.2) - gitaly-proto (0.97.0) + gitaly-proto (0.99.0) google-protobuf (~> 3.1) grpc (~> 1.10) github-linguist (5.3.3) @@ -335,9 +331,8 @@ GEM activesupport (>= 4.2.0) gollum-grit_adapter (1.0.1) gitlab-grit (~> 2.7, >= 2.7.1) - gon (6.1.0) + gon (6.2.0) actionpack (>= 3.0) - json multi_json request_store (>= 1.0) google-api-client (0.19.8) @@ -367,8 +362,8 @@ GEM rack (>= 1.3.0) rack-accept virtus (>= 1.0.0) - grape-entity (0.6.1) - activesupport (>= 5.0.0) + grape-entity (0.7.1) + activesupport (>= 4.0) multi_json (>= 1.3.2) grape-route-helpers (2.1.0) activesupport @@ -420,7 +415,7 @@ GEM json (~> 1.8) multi_xml (>= 0.5.2) httpclient (2.8.3) - i18n (0.9.5) + i18n (1.0.1) concurrent-ruby (~> 1.0) ice_nine (0.11.2) influxdb (0.5.3) @@ -515,7 +510,7 @@ GEM net-ldap (0.16.1) net-ssh (4.2.0) netrc (0.11.0) - nio4r (2.2.0) + nio4r (2.3.1) nokogiri (1.8.2) mini_portile2 (~> 2.3.0) numerizer (0.1.1) @@ -545,9 +540,9 @@ GEM omniauth (~> 1.2) omniauth-facebook (4.0.0) omniauth-oauth2 (~> 1.2) - omniauth-github (1.1.2) - omniauth (~> 1.0) - omniauth-oauth2 (~> 1.1) + omniauth-github (1.3.0) + omniauth (~> 1.5) + omniauth-oauth2 (>= 1.4.0, < 2.0) omniauth-gitlab (1.0.3) omniauth (~> 1.0) omniauth-oauth2 (~> 1.0) @@ -633,7 +628,7 @@ GEM parser unparser procto (0.0.3) - prometheus-client-mmap (0.9.1) + prometheus-client-mmap (0.9.2) pry (0.11.3) coderay (~> 1.1.0) method_source (~> 0.9.0) @@ -644,7 +639,7 @@ GEM pry (>= 0.10.4) public_suffix (3.0.2) pyu-ruby-sasl (0.0.3.3) - rack (2.0.4) + rack (2.0.5) rack-accept (0.4.5) rack (>= 0.4) rack-attack (4.4.1) @@ -662,17 +657,17 @@ GEM rack rack-test (0.6.3) rack (>= 1.0) - rails (5.0.6) - actioncable (= 5.0.6) - actionmailer (= 5.0.6) - actionpack (= 5.0.6) - actionview (= 5.0.6) - activejob (= 5.0.6) - activemodel (= 5.0.6) - activerecord (= 5.0.6) - activesupport (= 5.0.6) + rails (5.0.7) + actioncable (= 5.0.7) + actionmailer (= 5.0.7) + actionpack (= 5.0.7) + actionview (= 5.0.7) + activejob (= 5.0.7) + activemodel (= 5.0.7) + activerecord (= 5.0.7) + activesupport (= 5.0.7) bundler (>= 1.3.0) - railties (= 5.0.6) + railties (= 5.0.7) sprockets-rails (>= 2.0.0) rails-controller-testing (1.0.2) actionpack (~> 5.x, >= 5.0.1) @@ -683,21 +678,21 @@ GEM rails-dom-testing (2.0.3) activesupport (>= 4.2.0) nokogiri (>= 1.6) - rails-html-sanitizer (1.0.3) - loofah (~> 2.0) + rails-html-sanitizer (1.0.4) + loofah (~> 2.2, >= 2.2.2) rails-i18n (5.1.1) i18n (>= 0.7, < 2) railties (>= 5.0, < 6) - railties (5.0.6) - actionpack (= 5.0.6) - activesupport (= 5.0.6) + railties (5.0.7) + actionpack (= 5.0.7) + activesupport (= 5.0.7) method_source rake (>= 0.8.7) thor (>= 0.18.1, < 2.0) rainbow (2.2.2) rake raindrops (0.19.0) - rake (12.3.0) + rake (12.3.1) rb-fsevent (0.10.3) rb-inotify (0.9.10) ffi (>= 0.5.0, < 2) @@ -874,22 +869,10 @@ GEM simplecov-html (~> 0.10.0) simplecov-html (0.10.2) slack-notifier (1.5.1) - spinach (0.8.10) - colorize - gherkin-ruby (>= 0.3.2) - json - spinach-rails (0.2.1) - capybara (>= 2.0.0) - railties (>= 3) - spinach (>= 0.4) - spinach-rerun-reporter (0.0.2) - spinach (~> 0.8) spring (2.0.2) activesupport (>= 4.2) spring-commands-rspec (1.0.4) spring (>= 0.9.1) - spring-commands-spinach (1.1.0) - spring (>= 0.9.1) sprockets (3.7.1) concurrent-ruby (~> 1.0) rack (> 1, < 3) @@ -1001,7 +984,7 @@ DEPENDENCIES asana (~> 0.6.0) asciidoctor (~> 1.5.6) asciidoctor-plantuml (= 0.0.8) - asset_sync (~> 2.2.0) + asset_sync (~> 2.4) attr_encrypted (~> 3.1.0) awesome_print (~> 1.2.0) babosa (~> 1.0.2) @@ -1027,12 +1010,11 @@ DEPENDENCIES concurrent-ruby (~> 1.0.5) connection_pool (~> 2.0) creole (~> 0.5.0) - d3_rails (~> 3.5.0) database_cleaner (~> 1.5.0) deckar01-task_list (= 2.0.0) default_value_for (~> 3.0.5) device_detector - devise (~> 4.2) + devise (~> 4.4) devise-two-factor (~> 3.0.0) diffy (~> 3.1.0) doorkeeper (~> 4.3) @@ -1063,7 +1045,7 @@ DEPENDENCIES gettext (~> 3.2.2) gettext_i18n_rails (~> 1.8.0) gettext_i18n_rails_js (~> 1.3) - gitaly-proto (~> 0.97.0) + gitaly-proto (~> 0.99.0) github-linguist (~> 5.3.3) gitlab-flowdock-git-hook (~> 1.0.1) gitlab-gollum-lib (~> 4.2) @@ -1071,12 +1053,12 @@ DEPENDENCIES gitlab-markup (~> 1.6.2) gitlab-styles (~> 2.3) gitlab_omniauth-ldap (~> 2.0.4) - gon (~> 6.1.0) + gon (~> 6.2) google-api-client (~> 0.19.8) google-protobuf (= 3.5.1) gpgme grape (~> 1.0) - grape-entity (~> 0.6.0) + grape-entity (~> 0.7.1) grape-route-helpers (~> 2.1.0) grape_logging (~> 1.7) grpc (~> 1.11.0) @@ -1117,7 +1099,7 @@ DEPENDENCIES omniauth-azure-oauth2 (~> 0.0.9) omniauth-cas3 (~> 1.1.4) omniauth-facebook (~> 4.0.0) - omniauth-github (~> 1.1.1) + omniauth-github (~> 1.3) omniauth-gitlab (~> 1.0.2) omniauth-google-oauth2 (~> 0.5.3) omniauth-kerberos (~> 0.3.0) @@ -1136,14 +1118,14 @@ DEPENDENCIES peek-sidekiq (~> 1.0.3) pg (~> 0.18.2) premailer-rails (~> 1.9.7) - prometheus-client-mmap (~> 0.9.1) + prometheus-client-mmap (~> 0.9.2) pry-byebug (~> 3.4.1) pry-rails (~> 0.3.4) rack-attack (~> 4.4.1) rack-cors (~> 1.0.0) rack-oauth2 (~> 1.2.1) rack-proxy (~> 0.6.0) - rails (= 5.0.6) + rails (= 5.0.7) rails-controller-testing rails-deprecated_sanitizer (~> 1.0.3) rails-i18n (~> 5.1) @@ -1191,11 +1173,8 @@ DEPENDENCIES simple_po_parser (~> 1.1.2) simplecov (~> 0.14.0) slack-notifier (~> 1.5.1) - spinach-rails (~> 0.2.1) - spinach-rerun-reporter (~> 0.0.2) spring (~> 2.0.0) spring-commands-rspec (~> 1.0.4) - spring-commands-spinach (~> 1.1.0) sprockets (~> 3.7.0) sshkey (~> 1.9.0) stackprof (~> 0.2.10) diff --git a/app/assets/javascripts/pages/projects/wikis/components/delete_wiki_modal.vue b/app/assets/javascripts/pages/projects/wikis/components/delete_wiki_modal.vue new file mode 100644 index 00000000000..df21e2f8771 --- /dev/null +++ b/app/assets/javascripts/pages/projects/wikis/components/delete_wiki_modal.vue @@ -0,0 +1,77 @@ +<script> +import _ from 'underscore'; +import GlModal from '~/vue_shared/components/gl_modal.vue'; +import { s__, sprintf } from '~/locale'; + +export default { + components: { + GlModal, + }, + props: { + deleteWikiUrl: { + type: String, + required: true, + default: '', + }, + pageTitle: { + type: String, + required: true, + default: '', + }, + csrfToken: { + type: String, + required: true, + default: '', + }, + }, + computed: { + message() { + return s__('WikiPageConfirmDelete|Are you sure you want to delete this page?'); + }, + title() { + return sprintf( + s__('WikiPageConfirmDelete|Delete page %{pageTitle}?'), + { + pageTitle: _.escape(this.pageTitle), + }, + false, + ); + }, + }, + methods: { + onSubmit() { + this.$refs.form.submit(); + }, + }, +}; +</script> + +<template> + <gl-modal + id="delete-wiki-modal" + :header-title-text="title" + footer-primary-button-variant="danger" + :footer-primary-button-text="s__('WikiPageConfirmDelete|Delete page')" + @submit="onSubmit" + > + {{ message }} + <form + ref="form" + :action="deleteWikiUrl" + method="post" + class="form-horizontal js-requires-input" + > + <input + ref="method" + type="hidden" + name="_method" + value="delete" + /> + <input + type="hidden" + name="authenticity_token" + :value="csrfToken" + /> + </form> + </gl-modal> +</template> diff --git a/app/assets/javascripts/pages/projects/wikis/index.js b/app/assets/javascripts/pages/projects/wikis/index.js index ec01c66ffda..0295653cb29 100644 --- a/app/assets/javascripts/pages/projects/wikis/index.js +++ b/app/assets/javascripts/pages/projects/wikis/index.js @@ -1,12 +1,40 @@ import $ from 'jquery'; +import Vue from 'vue'; +import Translate from '~/vue_shared/translate'; +import csrf from '~/lib/utils/csrf'; import Wikis from './wikis'; import ShortcutsWiki from '../../../shortcuts_wiki'; import ZenMode from '../../../zen_mode'; import GLForm from '../../../gl_form'; +import deleteWikiModal from './components/delete_wiki_modal.vue'; document.addEventListener('DOMContentLoaded', () => { new Wikis(); // eslint-disable-line no-new new ShortcutsWiki(); // eslint-disable-line no-new new ZenMode(); // eslint-disable-line no-new new GLForm($('.wiki-form'), true); // eslint-disable-line no-new + + const deleteWikiButton = document.getElementById('delete-wiki-button'); + + if (deleteWikiButton) { + Vue.use(Translate); + + const { deleteWikiUrl, pageTitle } = deleteWikiButton.dataset; + const deleteWikiModalEl = document.getElementById('delete-wiki-modal'); + const deleteModal = new Vue({ // eslint-disable-line + el: deleteWikiModalEl, + data: { + deleteWikiUrl: '', + }, + render(createElement) { + return createElement(deleteWikiModal, { + props: { + pageTitle, + deleteWikiUrl, + csrfToken: csrf.token, + }, + }); + }, + }); + } }); diff --git a/app/assets/javascripts/pipelines/components/async_button.vue b/app/assets/javascripts/pipelines/components/async_button.vue deleted file mode 100644 index 0cdffbde05b..00000000000 --- a/app/assets/javascripts/pipelines/components/async_button.vue +++ /dev/null @@ -1,95 +0,0 @@ -<script> - /* eslint-disable no-alert */ - - import eventHub from '../event_hub'; - import loadingIcon from '../../vue_shared/components/loading_icon.vue'; - import icon from '../../vue_shared/components/icon.vue'; - import tooltip from '../../vue_shared/directives/tooltip'; - - export default { - directives: { - tooltip, - }, - components: { - loadingIcon, - icon, - }, - props: { - endpoint: { - type: String, - required: true, - }, - title: { - type: String, - required: true, - }, - icon: { - type: String, - required: true, - }, - cssClass: { - type: String, - required: true, - }, - pipelineId: { - type: Number, - required: true, - }, - type: { - type: String, - required: true, - }, - }, - data() { - return { - isLoading: false, - }; - }, - computed: { - buttonClass() { - return `btn ${this.cssClass}`; - }, - }, - created() { - // We're using eventHub to listen to the modal here instead of - // using props because it would would make the parent components - // much more complex to keep track of the loading state of each button - eventHub.$on('postAction', this.setLoading); - }, - beforeDestroy() { - eventHub.$off('postAction', this.setLoading); - }, - methods: { - onClick() { - eventHub.$emit('openConfirmationModal', { - pipelineId: this.pipelineId, - endpoint: this.endpoint, - type: this.type, - }); - }, - setLoading(endpoint) { - if (endpoint === this.endpoint) { - this.isLoading = true; - } - }, - }, - }; -</script> - -<template> - <button - v-tooltip - type="button" - @click="onClick" - :class="buttonClass" - :title="title" - :aria-label="title" - data-container="body" - data-placement="top" - :disabled="isLoading"> - <icon - :name="icon" - /> - <loading-icon v-if="isLoading" /> - </button> -</template> diff --git a/app/assets/javascripts/pipelines/components/pipelines_table.vue b/app/assets/javascripts/pipelines/components/pipelines_table.vue index 714aed1333e..41986b827cd 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_table.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_table.vue @@ -1,7 +1,7 @@ <script> - import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue'; + import Modal from '~/vue_shared/components/gl_modal.vue'; import { s__, sprintf } from '~/locale'; - import pipelinesTableRowComponent from './pipelines_table_row.vue'; + import PipelinesTableRowComponent from './pipelines_table_row.vue'; import eventHub from '../event_hub'; /** @@ -11,8 +11,8 @@ */ export default { components: { - pipelinesTableRowComponent, - DeprecatedModal, + PipelinesTableRowComponent, + Modal, }, props: { pipelines: { @@ -37,30 +37,18 @@ return { pipelineId: '', endpoint: '', - type: '', }; }, computed: { modalTitle() { - return this.type === 'stop' ? - sprintf(s__('Pipeline|Stop pipeline #%{pipelineId}?'), { - pipelineId: `'${this.pipelineId}'`, - }, false) : - sprintf(s__('Pipeline|Retry pipeline #%{pipelineId}?'), { - pipelineId: `'${this.pipelineId}'`, - }, false); + return sprintf(s__('Pipeline|Stop pipeline #%{pipelineId}?'), { + pipelineId: `${this.pipelineId}`, + }, false); }, modalText() { - return this.type === 'stop' ? - sprintf(s__('Pipeline|You’re about to stop pipeline %{pipelineId}.'), { - pipelineId: `<strong>#${this.pipelineId}</strong>`, - }, false) : - sprintf(s__('Pipeline|You’re about to retry pipeline %{pipelineId}.'), { - pipelineId: `<strong>#${this.pipelineId}</strong>`, - }, false); - }, - primaryButtonLabel() { - return this.type === 'stop' ? s__('Pipeline|Stop pipeline') : s__('Pipeline|Retry pipeline'); + return sprintf(s__('Pipeline|You’re about to stop pipeline %{pipelineId}.'), { + pipelineId: `<strong>#${this.pipelineId}</strong>`, + }, false); }, }, created() { @@ -73,7 +61,6 @@ setModalData(data) { this.pipelineId = data.pipelineId; this.endpoint = data.endpoint; - this.type = data.type; }, onSubmit() { eventHub.$emit('postAction', this.endpoint); @@ -120,20 +107,16 @@ :auto-devops-help-path="autoDevopsHelpPath" :view-type="viewType" /> - <deprecated-modal + + <modal id="confirmation-modal" - :title="modalTitle" - :text="modalText" - kind="danger" - :primary-button-label="primaryButtonLabel" + :header-title-text="modalTitle" + footer-primary-button-variant="danger" + :footer-primary-button-text="s__('Pipeline|Stop pipeline')" @submit="onSubmit" > - <template - slot="body" - slot-scope="props" - > - <p v-html="props.text"></p> - </template> - </deprecated-modal> + <span v-html="modalText"></span> + </modal> + </div> </template> diff --git a/app/assets/javascripts/pipelines/components/pipelines_table_row.vue b/app/assets/javascripts/pipelines/components/pipelines_table_row.vue index 4cbd67e0372..498a97851fa 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_table_row.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_table_row.vue @@ -1,13 +1,14 @@ <script> - /* eslint-disable no-param-reassign */ - import asyncButtonComponent from './async_button.vue'; - import pipelinesActionsComponent from './pipelines_actions.vue'; - import pipelinesArtifactsComponent from './pipelines_artifacts.vue'; - import ciBadge from '../../vue_shared/components/ci_badge_link.vue'; - import pipelineStage from './stage.vue'; - import pipelineUrl from './pipeline_url.vue'; - import pipelinesTimeago from './time_ago.vue'; - import commitComponent from '../../vue_shared/components/commit.vue'; + import eventHub from '../event_hub'; + import PipelinesActionsComponent from './pipelines_actions.vue'; + import PipelinesArtifactsComponent from './pipelines_artifacts.vue'; + import CiBadge from '../../vue_shared/components/ci_badge_link.vue'; + import PipelineStage from './stage.vue'; + import PipelineUrl from './pipeline_url.vue'; + import PipelinesTimeago from './time_ago.vue'; + import CommitComponent from '../../vue_shared/components/commit.vue'; + import LoadingButton from '../../vue_shared/components/loading_button.vue'; + import Icon from '../../vue_shared/components/icon.vue'; /** * Pipeline table row. @@ -16,14 +17,15 @@ */ export default { components: { - asyncButtonComponent, - pipelinesActionsComponent, - pipelinesArtifactsComponent, - commitComponent, - pipelineStage, - pipelineUrl, - ciBadge, - pipelinesTimeago, + PipelinesActionsComponent, + PipelinesArtifactsComponent, + CommitComponent, + PipelineStage, + PipelineUrl, + CiBadge, + PipelinesTimeago, + LoadingButton, + Icon, }, props: { pipeline: { @@ -44,6 +46,12 @@ required: true, }, }, + data() { + return { + isRetrying: false, + isCancelling: false, + }; + }, computed: { /** * If provided, returns the commit tag. @@ -119,8 +127,10 @@ if (this.pipeline.ref) { return Object.keys(this.pipeline.ref).reduce((accumulator, prop) => { if (prop === 'path') { + // eslint-disable-next-line no-param-reassign accumulator.ref_url = this.pipeline.ref[prop]; } else { + // eslint-disable-next-line no-param-reassign accumulator[prop] = this.pipeline.ref[prop]; } return accumulator; @@ -216,6 +226,21 @@ return this.viewType === 'child'; }, }, + + methods: { + handleCancelClick() { + this.isCancelling = true; + + eventHub.$emit('openConfirmationModal', { + pipelineId: this.pipeline.id, + endpoint: this.pipeline.cancel_path, + }); + }, + handleRetryClick() { + this.isRetrying = true; + eventHub.$emit('retryPipeline', this.pipeline.retry_path); + }, + }, }; </script> <template> @@ -287,7 +312,8 @@ <div v-if="displayPipelineActions" - class="table-section section-20 table-button-footer pipeline-actions"> + class="table-section section-20 table-button-footer pipeline-actions" + > <div class="btn-group table-action-buttons"> <pipelines-actions-component v-if="pipeline.details.manual_actions.length" @@ -300,29 +326,27 @@ :artifacts="pipeline.details.artifacts" /> - <async-button-component + <loading-button v-if="pipeline.flags.retryable" - :endpoint="pipeline.retry_path" - css-class="js-pipelines-retry-button btn-default btn-retry" - title="Retry" - icon="repeat" - :pipeline-id="pipeline.id" - data-toggle="modal" - data-target="#confirmation-modal" - type="retry" - /> + @click="handleRetryClick" + container-class="js-pipelines-retry-button btn btn-default btn-retry" + :loading="isRetrying" + :disabled="isRetrying" + > + <icon name="repeat" /> + </loading-button> - <async-button-component + <loading-button v-if="pipeline.flags.cancelable" - :endpoint="pipeline.cancel_path" - css-class="js-pipelines-cancel-button btn-remove" - title="Stop" - icon="close" - :pipeline-id="pipeline.id" + @click="handleCancelClick" data-toggle="modal" data-target="#confirmation-modal" - type="stop" - /> + container-class="js-pipelines-cancel-button btn btn-remove" + :loading="isCancelling" + :disabled="isCancelling" + > + <icon name="close" /> + </loading-button> </div> </div> </div> diff --git a/app/assets/javascripts/pipelines/mixins/pipelines.js b/app/assets/javascripts/pipelines/mixins/pipelines.js index 6d87f75ae8e..de0faf181e5 100644 --- a/app/assets/javascripts/pipelines/mixins/pipelines.js +++ b/app/assets/javascripts/pipelines/mixins/pipelines.js @@ -53,10 +53,12 @@ export default { }); eventHub.$on('postAction', this.postAction); + eventHub.$on('retryPipeline', this.postAction); eventHub.$on('clickedDropdown', this.updateTable); }, beforeDestroy() { eventHub.$off('postAction', this.postAction); + eventHub.$off('retryPipeline', this.postAction); eventHub.$off('clickedDropdown', this.updateTable); }, destroyed() { diff --git a/app/assets/javascripts/user_callout.js b/app/assets/javascripts/user_callout.js index 97d5cf96bcb..96dfff77859 100644 --- a/app/assets/javascripts/user_callout.js +++ b/app/assets/javascripts/user_callout.js @@ -15,7 +15,7 @@ export default class UserCallout { init() { if (!this.isCalloutDismissed || this.isCalloutDismissed === 'false') { - $('.js-close-callout').on('click', e => this.dismissCallout(e)); + this.userCalloutBody.find('.js-close-callout').on('click', e => this.dismissCallout(e)); } } @@ -23,12 +23,15 @@ export default class UserCallout { const $currentTarget = $(e.currentTarget); if (this.options.setCalloutPerProject) { - Cookies.set(this.cookieName, 'true', { expires: 365, path: this.userCalloutBody.data('projectPath') }); + Cookies.set(this.cookieName, 'true', { + expires: 365, + path: this.userCalloutBody.data('projectPath'), + }); } else { Cookies.set(this.cookieName, 'true', { expires: 365 }); } - if ($currentTarget.hasClass('close')) { + if ($currentTarget.hasClass('close') || $currentTarget.hasClass('js-close')) { this.userCalloutBody.remove(); } } diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_maintainer_edit.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_maintainer_edit.vue deleted file mode 100644 index f0298f732ea..00000000000 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_maintainer_edit.vue +++ /dev/null @@ -1,20 +0,0 @@ -<script> - export default { - name: 'MRWidgetMaintainerEdit', - props: { - maintainerEditAllowed: { - type: Boolean, - default: false, - required: false, - }, - }, - }; -</script> - -<template> - <section class="mr-info-list mr-links"> - <p v-if="maintainerEditAllowed"> - {{ s__("mrWidget|Allows edits from maintainers") }} - </p> - </section> -</template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_squash_before_merge.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_squash_before_merge.vue index bf8628d18a6..926a3172412 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_squash_before_merge.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_squash_before_merge.vue @@ -10,6 +10,6 @@ In EE, the configuration extends this object to add a functioning squash-before- button. */ -export default { - template: '', -}; +<script> + export default {}; +</script> diff --git a/app/assets/javascripts/vue_merge_request_widget/dependencies.js b/app/assets/javascripts/vue_merge_request_widget/dependencies.js index 7f5f28091da..15097fa2a3f 100644 --- a/app/assets/javascripts/vue_merge_request_widget/dependencies.js +++ b/app/assets/javascripts/vue_merge_request_widget/dependencies.js @@ -15,7 +15,6 @@ export { default as WidgetHeader } from './components/mr_widget_header.vue'; export { default as WidgetMergeHelp } from './components/mr_widget_merge_help.vue'; export { default as WidgetPipeline } from './components/mr_widget_pipeline.vue'; export { default as Deployment } from './components/deployment.vue'; -export { default as WidgetMaintainerEdit } from './components/mr_widget_maintainer_edit.vue'; export { default as WidgetRelatedLinks } from './components/mr_widget_related_links.vue'; export { default as MergedState } from './components/states/mr_widget_merged.vue'; export { default as FailedToMerge } from './components/states/mr_widget_failed_to_merge.vue'; @@ -41,8 +40,8 @@ export { default as MRWidgetService } from './services/mr_widget_service'; export { default as eventHub } from './event_hub'; export { default as getStateKey } from './stores/get_state_key'; export { default as stateMaps } from './stores/state_maps'; -export { default as SquashBeforeMerge } from './components/states/mr_widget_squash_before_merge'; +export { default as SquashBeforeMerge } from './components/states/mr_widget_squash_before_merge.vue'; export { default as notify } from '../lib/utils/notify'; export { default as SourceBranchRemovalStatus } from './components/source_branch_removal_status.vue'; -export { default as mrWidgetOptions } from './mr_widget_options'; +export { default as mrWidgetOptions } from './mr_widget_options.vue'; diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue index 345f9ac1b4b..f69fe03fcb3 100644 --- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js +++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue @@ -1,12 +1,13 @@ +<script> + import Project from '~/pages/projects/project'; import SmartInterval from '~/smart_interval'; -import Flash from '../flash'; +import createFlash from '../flash'; import { WidgetHeader, WidgetMergeHelp, WidgetPipeline, Deployment, - WidgetMaintainerEdit, WidgetRelatedLinks, MergedState, ClosedState, @@ -40,10 +41,39 @@ import { setFavicon } from '../lib/utils/common_utils'; export default { el: '#js-vue-mr-widget', name: 'MRWidget', + components: { + 'mr-widget-header': WidgetHeader, + 'mr-widget-merge-help': WidgetMergeHelp, + 'mr-widget-pipeline': WidgetPipeline, + Deployment, + 'mr-widget-related-links': WidgetRelatedLinks, + 'mr-widget-merged': MergedState, + 'mr-widget-closed': ClosedState, + 'mr-widget-merging': MergingState, + 'mr-widget-failed-to-merge': FailedToMerge, + 'mr-widget-wip': WorkInProgressState, + 'mr-widget-archived': ArchivedState, + 'mr-widget-conflicts': ConflictsState, + 'mr-widget-nothing-to-merge': NothingToMergeState, + 'mr-widget-not-allowed': NotAllowedState, + 'mr-widget-missing-branch': MissingBranchState, + 'mr-widget-ready-to-merge': ReadyToMergeState, + 'sha-mismatch': ShaMismatchState, + 'mr-widget-squash-before-merge': SquashBeforeMerge, + 'mr-widget-checking': CheckingState, + 'mr-widget-unresolved-discussions': UnresolvedDiscussionsState, + 'mr-widget-pipeline-blocked': PipelineBlockedState, + 'mr-widget-pipeline-failed': PipelineFailedState, + 'mr-widget-merge-when-pipeline-succeeds': MergeWhenPipelineSucceedsState, + 'mr-widget-auto-merge-failed': AutoMergeFailed, + 'mr-widget-rebase': RebaseState, + SourceBranchRemovalStatus, + }, props: { mrData: { type: Object, required: false, + default: null, }, }, data() { @@ -72,6 +102,13 @@ export default { (!this.mr.isNothingToMergeState && !this.mr.isMergedState); }, }, + created() { + this.initPolling(); + this.bindEventHubListeners(); + }, + mounted() { + this.handleMounted(); + }, methods: { createService(store) { const endpoints = { @@ -99,7 +136,7 @@ export default { cb.call(null, data); } }) - .catch(() => new Flash('Something went wrong. Please try again.')); + .catch(() => createFlash('Something went wrong. Please try again.')); }, initPolling() { this.pollingInterval = new SmartInterval({ @@ -134,7 +171,7 @@ export default { } }) .catch(() => { - new Flash('Something went wrong while fetching the environments for this merge request. Please try again.'); // eslint-disable-line + createFlash('Something went wrong while fetching the environments for this merge request. Please try again.'); // eslint-disable-line }); }, fetchActionsContent() { @@ -147,7 +184,7 @@ export default { Project.initRefSwitcher(); } }) - .catch(() => new Flash('Something went wrong. Please try again.')); + .catch(() => createFlash('Something went wrong. Please try again.')); }, handleNotification(data) { if (data.ci_status === this.mr.ciStatus) return; @@ -202,76 +239,53 @@ export default { this.initDeploymentsPolling(); }, }, - created() { - this.initPolling(); - this.bindEventHubListeners(); - }, - mounted() { - this.handleMounted(); - }, - components: { - 'mr-widget-header': WidgetHeader, - 'mr-widget-merge-help': WidgetMergeHelp, - 'mr-widget-pipeline': WidgetPipeline, - Deployment, - 'mr-widget-maintainer-edit': WidgetMaintainerEdit, - 'mr-widget-related-links': WidgetRelatedLinks, - 'mr-widget-merged': MergedState, - 'mr-widget-closed': ClosedState, - 'mr-widget-merging': MergingState, - 'mr-widget-failed-to-merge': FailedToMerge, - 'mr-widget-wip': WorkInProgressState, - 'mr-widget-archived': ArchivedState, - 'mr-widget-conflicts': ConflictsState, - 'mr-widget-nothing-to-merge': NothingToMergeState, - 'mr-widget-not-allowed': NotAllowedState, - 'mr-widget-missing-branch': MissingBranchState, - 'mr-widget-ready-to-merge': ReadyToMergeState, - 'mr-widget-sha-mismatch': ShaMismatchState, - 'mr-widget-squash-before-merge': SquashBeforeMerge, - 'mr-widget-checking': CheckingState, - 'mr-widget-unresolved-discussions': UnresolvedDiscussionsState, - 'mr-widget-pipeline-blocked': PipelineBlockedState, - 'mr-widget-pipeline-failed': PipelineFailedState, - 'mr-widget-merge-when-pipeline-succeeds': MergeWhenPipelineSucceedsState, - 'mr-widget-auto-merge-failed': AutoMergeFailed, - 'mr-widget-rebase': RebaseState, - SourceBranchRemovalStatus, - }, - template: ` - <div class="mr-state-widget prepend-top-default"> - <mr-widget-header :mr="mr" /> - <mr-widget-pipeline - v-if="shouldRenderPipelines" - :pipeline="mr.pipeline" - :ci-status="mr.ciStatus" - :has-ci="mr.hasCI" - /> - <deployment - v-for="deployment in mr.deployments" - :key="deployment.id" - :deployment="deployment" +}; +</script> +<template> + <div class="mr-state-widget prepend-top-default"> + <mr-widget-header + :mr="mr" + /> + <mr-widget-pipeline + v-if="shouldRenderPipelines" + :pipeline="mr.pipeline" + :ci-status="mr.ciStatus" + :has-ci="mr.hasCI" + /> + <deployment + v-for="deployment in mr.deployments" + :key="deployment.id" + :deployment="deployment" + /> + <div class="mr-widget-section"> + <component + :is="componentName" + :mr="mr" + :service="service" + /> + + <section + v-if="mr.maintainerEditAllowed" + class="mr-info-list mr-links" + > + {{ s__("mrWidget|Allows edits from maintainers") }} + </section> + + <mr-widget-related-links + v-if="shouldRenderRelatedLinks" + :state="mr.state" + :related-links="mr.relatedLinks" + /> + + <source-branch-removal-status + v-if="shouldRenderSourceBranchRemovalStatus" /> - <div class="mr-widget-section"> - <component - :is="componentName" - :mr="mr" - :service="service" /> - <mr-widget-maintainer-edit - :maintainerEditAllowed="mr.maintainerEditAllowed" /> - <mr-widget-related-links - v-if="shouldRenderRelatedLinks" - :state="mr.state" - :related-links="mr.relatedLinks" /> - <source-branch-removal-status - v-if="shouldRenderSourceBranchRemovalStatus" - /> - </div> - <div - class="mr-widget-footer" - v-if="shouldRenderMergeHelp"> - <mr-widget-merge-help /> - </div> </div> - `, -}; + <div + class="mr-widget-footer" + v-if="shouldRenderMergeHelp" + > + <mr-widget-merge-help /> + </div> + </div> +</template> diff --git a/app/assets/javascripts/vue_shared/components/loading_button.vue b/app/assets/javascripts/vue_shared/components/loading_button.vue index e832d94d32f..88c13a1f340 100644 --- a/app/assets/javascripts/vue_shared/components/loading_button.vue +++ b/app/assets/javascripts/vue_shared/components/loading_button.vue @@ -70,12 +70,14 @@ /> </transition> <transition name="fade"> - <span - v-if="label" - class="js-loading-button-label" - > - {{ label }} - </span> + <slot> + <span + v-if="label" + class="js-loading-button-label" + > + {{ label }} + </span> + </slot> </transition> </button> </template> diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 2caffec66ac..2843d70c645 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -13,8 +13,7 @@ class ApplicationController < ActionController::Base before_action :authenticate_sessionless_user! before_action :authenticate_user! - before_action :enforce_terms!, if: -> { Gitlab::CurrentSettings.current_application_settings.enforce_terms }, - unless: :peek_request? + before_action :enforce_terms!, if: :should_enforce_terms? before_action :validate_user_service_ticket! before_action :check_password_expiration before_action :ldap_security_check @@ -373,4 +372,10 @@ class ApplicationController < ActionController::Base def peek_request? request.path.start_with?('/-/peek') end + + def should_enforce_terms? + return false unless Gitlab::CurrentSettings.current_application_settings.enforce_terms + + !(peek_request? || devise_controller?) + end end diff --git a/app/controllers/concerns/send_file_upload.rb b/app/controllers/concerns/send_file_upload.rb index 55011c89886..237c93daee8 100644 --- a/app/controllers/concerns/send_file_upload.rb +++ b/app/controllers/concerns/send_file_upload.rb @@ -2,6 +2,10 @@ module SendFileUpload def send_upload(file_upload, send_params: {}, redirect_params: {}, attachment: nil, disposition: 'attachment') if attachment redirect_params[:query] = { "response-content-disposition" => "#{disposition};filename=#{attachment.inspect}" } + # By default, Rails will send uploads with an extension of .js with a + # content-type of text/javascript, which will trigger Rails' + # cross-origin JavaScript protection. + send_params[:content_type] = 'text/plain' if File.extname(attachment) == '.js' send_params.merge!(filename: attachment, disposition: disposition) end diff --git a/app/controllers/groups/group_members_controller.rb b/app/controllers/groups/group_members_controller.rb index 134b0dfc0db..ef3eba80154 100644 --- a/app/controllers/groups/group_members_controller.rb +++ b/app/controllers/groups/group_members_controller.rb @@ -11,13 +11,20 @@ class Groups::GroupMembersController < Groups::ApplicationController :override def index + can_manage_members = can?(current_user, :admin_group_member, @group) + @sort = params[:sort].presence || sort_value_name @project = @group.projects.find(params[:project_id]) if params[:project_id] @members = GroupMembersFinder.new(@group).execute - @members = @members.non_invite unless can?(current_user, :admin_group, @group) + @members = @members.non_invite unless can_manage_members @members = @members.search(params[:search]) if params[:search].present? @members = @members.sort_by_attribute(@sort) + + if can_manage_members && params[:two_factor].present? + @members = @members.filter_by_2fa(params[:two_factor]) + end + @members = @members.page(params[:page]).per(50) @members = present_members(@members.includes(:user)) diff --git a/app/controllers/projects/notes_controller.rb b/app/controllers/projects/notes_controller.rb index bc13b8ad7ba..4d4c2af2415 100644 --- a/app/controllers/projects/notes_controller.rb +++ b/app/controllers/projects/notes_controller.rb @@ -8,19 +8,6 @@ class Projects::NotesController < Projects::ApplicationController before_action :authorize_create_note!, only: [:create] before_action :authorize_resolve_note!, only: [:resolve, :unresolve] - # - # This is a fix to make spinach feature tests passing: - # Controller actions are returned from AbstractController::Base and methods of parent classes are - # excluded in order to return only specific controller related methods. - # That is ok for the app (no :create method in ancestors) - # but fails for tests because there is a :create method on FactoryBot (one of the ancestors) - # - # see https://github.com/rails/rails/blob/v4.2.7/actionpack/lib/abstract_controller/base.rb#L78 - # - def create - super - end - def delete_attachment note.remove_attachment! note.update_attribute(:attachment, nil) diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb index 0b1b46944aa..f7417a6a5aa 100644 --- a/app/controllers/projects/pipelines_controller.rb +++ b/app/controllers/projects/pipelines_controller.rb @@ -181,4 +181,8 @@ class Projects::PipelinesController < Projects::ApplicationController # Also see https://gitlab.com/gitlab-org/gitlab-ce/issues/42343 Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42339') end + + def authorize_update_pipeline! + return access_denied! unless can?(current_user, :update_pipeline, @pipeline) + end end diff --git a/app/controllers/users/terms_controller.rb b/app/controllers/users/terms_controller.rb index 95c5c3432d5..ab685b9106e 100644 --- a/app/controllers/users/terms_controller.rb +++ b/app/controllers/users/terms_controller.rb @@ -3,6 +3,10 @@ module Users include InternalRedirect skip_before_action :enforce_terms! + skip_before_action :check_password_expiration + skip_before_action :check_two_factor_requirement + skip_before_action :require_email + before_action :terms layout 'terms' diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb index 079b3cd3aa0..cb6f709c604 100644 --- a/app/helpers/events_helper.rb +++ b/app/helpers/events_helper.rb @@ -41,7 +41,7 @@ module EventsHelper key = key.to_s active = 'active' if @event_filter.active?(key) link_opts = { - class: "event-filter-link has-tooltip", + class: "event-filter-link", id: "#{key}_event_filter", title: tooltip } diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb index e803cd3a8d8..ce9373f5883 100644 --- a/app/helpers/users_helper.rb +++ b/app/helpers/users_helper.rb @@ -42,22 +42,11 @@ module UsersHelper items << :sign_out if current_user - # TODO: Remove these conditions when the permissions are prevented in - # https://gitlab.com/gitlab-org/gitlab-ce/issues/45849 - terms_not_enforced = !Gitlab::CurrentSettings - .current_application_settings - .enforce_terms? - required_terms_accepted = terms_not_enforced || current_user.terms_accepted? + return items if current_user&.required_terms_not_accepted? - items << :help if required_terms_accepted - - if can?(current_user, :read_user, current_user) && required_terms_accepted - items << :profile - end - - if can?(current_user, :update_user, current_user) && required_terms_accepted - items << :settings - end + items << :help + items << :profile if can?(current_user, :read_user, current_user) + items << :settings if can?(current_user, :update_user, current_user) items end diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 0b90834d415..1f49764e7cc 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -37,12 +37,16 @@ module Ci delegate :id, to: :project, prefix: true delegate :full_path, to: :project, prefix: true - validates :source, exclusion: { in: %w(unknown), unless: :importing? }, on: :create validates :sha, presence: { unless: :importing? } validates :ref, presence: { unless: :importing? } validates :status, presence: { unless: :importing? } validate :valid_commit_sha, unless: :importing? + # Replace validator below with + # `validates :source, presence: { unless: :importing? }, on: :create` + # when removing Gitlab.rails5? code. + validate :valid_source, unless: :importing?, on: :create + after_create :keep_around_commits, unless: :importing? enum source: { @@ -601,5 +605,11 @@ module Ci project.repository.keep_around(self.sha) project.repository.keep_around(self.before_sha) end + + def valid_source + if source.nil? || source == "unknown" + errors.add(:source, "invalid source") + end + end end end diff --git a/app/models/concerns/routable.rb b/app/models/concerns/routable.rb index 915ad6959be..0176a12a131 100644 --- a/app/models/concerns/routable.rb +++ b/app/models/concerns/routable.rb @@ -4,7 +4,9 @@ module Routable extend ActiveSupport::Concern included do - has_one :route, as: :source, autosave: true, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent + # Remove `inverse_of: source` when upgraded to rails 5.2 + # See https://github.com/rails/rails/pull/28808 + has_one :route, as: :source, autosave: true, dependent: :destroy, inverse_of: :source # rubocop:disable Cop/ActiveRecordDependent has_many :redirect_routes, as: :source, autosave: true, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent validates :route, presence: true diff --git a/app/models/member.rb b/app/models/member.rb index eac4a22a03f..68572f2e33a 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -96,6 +96,17 @@ class Member < ActiveRecord::Base joins(:user).merge(User.search(query)) end + def filter_by_2fa(value) + case value + when 'enabled' + left_join_users.merge(User.with_two_factor_indistinct) + when 'disabled' + left_join_users.merge(User.without_two_factor) + else + all + end + end + def sort_by_attribute(method) case method.to_s when 'access_level_asc' then reorder(access_level: :asc) diff --git a/app/models/user.rb b/app/models/user.rb index a9cfd39f604..dfef065f094 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -237,14 +237,18 @@ class User < ActiveRecord::Base scope :order_recent_sign_in, -> { reorder(Gitlab::Database.nulls_last_order('current_sign_in_at', 'DESC')) } scope :order_oldest_sign_in, -> { reorder(Gitlab::Database.nulls_last_order('current_sign_in_at', 'ASC')) } - def self.with_two_factor + def self.with_two_factor_indistinct joins("LEFT OUTER JOIN u2f_registrations AS u2f ON u2f.user_id = users.id") - .where("u2f.id IS NOT NULL OR otp_required_for_login = ?", true).distinct(arel_table[:id]) + .where("u2f.id IS NOT NULL OR users.otp_required_for_login = ?", true) + end + + def self.with_two_factor + with_two_factor_indistinct.distinct(arel_table[:id]) end def self.without_two_factor joins("LEFT OUTER JOIN u2f_registrations AS u2f ON u2f.user_id = users.id") - .where("u2f.id IS NULL AND otp_required_for_login = ?", false) + .where("u2f.id IS NULL AND users.otp_required_for_login = ?", false) end # @@ -1193,6 +1197,11 @@ class User < ActiveRecord::Base accepted_term_id.present? end + def required_terms_not_accepted? + Gitlab::CurrentSettings.current_application_settings.enforce_terms? && + !terms_accepted? + end + protected # override, from Devise::Validatable diff --git a/app/policies/ci/build_policy.rb b/app/policies/ci/build_policy.rb index 808a81cbbf9..8b65758f3e8 100644 --- a/app/policies/ci/build_policy.rb +++ b/app/policies/ci/build_policy.rb @@ -14,11 +14,20 @@ module Ci @subject.triggered_by?(@user) end + condition(:branch_allows_maintainer_push) do + @subject.project.branch_allows_maintainer_push?(@user, @subject.ref) + end + rule { protected_ref }.policy do prevent :update_build prevent :erase_build end rule { can?(:admin_build) | (can?(:update_build) & owner_of_job) }.enable :erase_build + + rule { can?(:public_access) & branch_allows_maintainer_push }.policy do + enable :update_build + enable :update_commit_status + end end end diff --git a/app/policies/ci/pipeline_policy.rb b/app/policies/ci/pipeline_policy.rb index 6363c382ff8..540e4235299 100644 --- a/app/policies/ci/pipeline_policy.rb +++ b/app/policies/ci/pipeline_policy.rb @@ -4,8 +4,16 @@ module Ci condition(:protected_ref) { ref_protected?(@user, @subject.project, @subject.tag?, @subject.ref) } + condition(:branch_allows_maintainer_push) do + @subject.project.branch_allows_maintainer_push?(@user, @subject.ref) + end + rule { protected_ref }.prevent :update_pipeline + rule { can?(:public_access) & branch_allows_maintainer_push }.policy do + enable :update_pipeline + end + def ref_protected?(user, project, tag, ref) access = ::Gitlab::UserAccess.new(user, project: project) diff --git a/app/policies/global_policy.rb b/app/policies/global_policy.rb index 64e550d19d0..1cf5515d9d7 100644 --- a/app/policies/global_policy.rb +++ b/app/policies/global_policy.rb @@ -1,22 +1,24 @@ class GlobalPolicy < BasePolicy desc "User is blocked" with_options scope: :user, score: 0 - condition(:blocked) { @user.blocked? } + condition(:blocked) { @user&.blocked? } desc "User is an internal user" with_options scope: :user, score: 0 - condition(:internal) { @user.internal? } + condition(:internal) { @user&.internal? } desc "User's access has been locked" with_options scope: :user, score: 0 - condition(:access_locked) { @user.access_locked? } + condition(:access_locked) { @user&.access_locked? } - condition(:can_create_fork, scope: :user) { @user.manageable_namespaces.any? { |namespace| @user.can?(:create_projects, namespace) } } + condition(:can_create_fork, scope: :user) { @user && @user.manageable_namespaces.any? { |namespace| @user.can?(:create_projects, namespace) } } + + condition(:required_terms_not_accepted, scope: :user, score: 0) do + @user&.required_terms_not_accepted? + end rule { anonymous }.policy do prevent :log_in - prevent :access_api - prevent :access_git prevent :receive_notifications prevent :use_quick_actions prevent :create_group @@ -38,6 +40,11 @@ class GlobalPolicy < BasePolicy prevent :use_quick_actions end + rule { required_terms_not_accepted }.policy do + prevent :access_api + prevent :access_git + end + rule { can_create_group }.policy do enable :create_group end diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb index 5759b1a376f..99a0d7118f2 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -76,7 +76,7 @@ class ProjectPolicy < BasePolicy condition(:request_access_enabled, scope: :subject, score: 0) { project.request_access_enabled } desc "Has merge requests allowing pushes to user" - condition(:has_merge_requests_allowing_pushes, scope: :subject) do + condition(:has_merge_requests_allowing_pushes) do project.merge_requests_allowing_push_to_user(user).any? end @@ -354,9 +354,7 @@ class ProjectPolicy < BasePolicy # to run pipelines for the branches they have access to. rule { can?(:public_access) & has_merge_requests_allowing_pushes }.policy do enable :create_build - enable :update_build enable :create_pipeline - enable :update_pipeline end rule do diff --git a/app/views/groups/group_members/index.html.haml b/app/views/groups/group_members/index.html.haml index ad9d5562ded..c8addc49117 100644 --- a/app/views/groups/group_members/index.html.haml +++ b/app/views/groups/group_members/index.html.haml @@ -1,10 +1,11 @@ - page_title "Members" +- can_manage_members = can?(current_user, :admin_group_member, @group) .project-members-page.prepend-top-default %h4 Members %hr - - if can?(current_user, :admin_group_member, @group) + - if can_manage_members .project-members-new.append-bottom-default %p.clearfix Add new member to @@ -13,20 +14,23 @@ = render 'shared/members/requests', membership_source: @group, requesters: @requesters - .append-bottom-default.clearfix + .clearfix %h5.member.existing-title Existing members - = form_tag group_group_members_path(@group), method: :get, class: 'form-inline member-search-form' do - .form-group - = search_field_tag :search, params[:search], { placeholder: 'Find existing members by name', class: 'form-control', spellcheck: false } - %button.member-search-btn{ type: "submit", "aria-label" => "Submit search" } - = icon("search") - = render 'shared/members/sort_dropdown' .panel.panel-default - .panel-heading - Members with access to - %strong= @group.name + .panel-heading.flex-project-members-panel + %span.flex-project-title + Members with access to + %strong= @group.name %span.badge= @members.total_count + = form_tag group_group_members_path(@group), method: :get, class: 'form-inline member-search-form flex-project-members-form' do + .form-group + = search_field_tag :search, params[:search], { placeholder: 'Find existing members by name', class: 'form-control', spellcheck: false } + %button.member-search-btn{ type: "submit", "aria-label" => "Submit search" } + = icon("search") + - if can_manage_members + = render 'shared/members/filter_2fa_dropdown' + = render 'shared/members/sort_dropdown' %ul.content-list.members-list = render partial: 'shared/members/member', collection: @members, as: :member = paginate @members, theme: 'gitlab' diff --git a/app/views/groups/issues.html.haml b/app/views/groups/issues.html.haml index bbfbea4ac7a..662db18cf86 100644 --- a/app/views/groups/issues.html.haml +++ b/app/views/groups/issues.html.haml @@ -8,7 +8,7 @@ .top-area = render 'shared/issuable/nav', type: :issues .nav-controls - = link_to params.merge(rss_url_options), class: 'btn' do + = link_to safe_params.merge(rss_url_options), class: 'btn' do = icon('rss') %span.icon-label Subscribe diff --git a/app/views/projects/wikis/edit.html.haml b/app/views/projects/wikis/edit.html.haml index 9d3d4072027..35c7dc2984a 100644 --- a/app/views/projects/wikis/edit.html.haml +++ b/app/views/projects/wikis/edit.html.haml @@ -28,9 +28,16 @@ = link_to project_wiki_history_path(@project, @page), class: "btn" do = s_("Wiki|Page history") - if can?(current_user, :admin_wiki, @project) - = link_to project_wiki_path(@project, @page), data: { confirm: s_("WikiPageConfirmDelete|Are you sure you want to delete this page?")}, method: :delete, class: "btn btn-danger" do - = _("Delete") + %button.btn.btn-danger{ data: { toggle: 'modal', + target: '#delete-wiki-modal', + delete_wiki_url: project_wiki_path(@project, @page), + page_title: @page.title.capitalize }, + id: 'delete-wiki-button', + type: 'button' } + = _('Delete') = render 'form' = render 'sidebar' + +#delete-wiki-modal.modal.fade diff --git a/app/views/shared/members/_filter_2fa_dropdown.html.haml b/app/views/shared/members/_filter_2fa_dropdown.html.haml new file mode 100644 index 00000000000..95c35c56b3c --- /dev/null +++ b/app/views/shared/members/_filter_2fa_dropdown.html.haml @@ -0,0 +1,11 @@ +- filter = params[:two_factor] || 'everyone' +- filter_options = { 'everyone' => 'Everyone', 'enabled' => 'Enabled', 'disabled' => 'Disabled' } +.dropdown.inline.member-filter-2fa-dropdown + = dropdown_toggle('2FA: ' + filter_options[filter], { toggle: 'dropdown' }) + %ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-selectable + %li.dropdown-header + Filter by two-factor authentication + - filter_options.each do |value, title| + %li + = link_to filter_group_project_member_path(two_factor: value), class: ("is-active" if filter == value) do + = title diff --git a/app/views/shared/members/_member.html.haml b/app/views/shared/members/_member.html.haml index 1c139827acf..1961ad6d616 100644 --- a/app/views/shared/members/_member.html.haml +++ b/app/views/shared/members/_member.html.haml @@ -20,6 +20,10 @@ %label.label.label-danger %strong Blocked + - if user.two_factor_enabled? + %label.label.label-info + 2FA + - if source.instance_of?(Group) && source != @group · = link_to source.full_name, source, class: "member-group-link" diff --git a/bin/secpick b/bin/secpick index 76ae231e913..5029fe57cfe 100755 --- a/bin/secpick +++ b/bin/secpick @@ -5,7 +5,6 @@ require 'rainbow/refinement' using Rainbow BRANCH_PREFIX = 'security'.freeze -STABLE_BRANCH_SUFFIX = 'stable'.freeze REMOTE = 'dev'.freeze options = { version: nil, branch: nil, sha: nil } @@ -37,9 +36,9 @@ abort("Missing options. Use #{$0} --help to see the list of options available".r abort("Wrong version format #{options[:version].bold}".red) unless options[:version] =~ /\A\d*\-\d*\Z/ branch = [BRANCH_PREFIX, options[:branch], options[:version]].join('-').freeze -stable_branch = "#{options[:version]}-#{STABLE_BRANCH_SUFFIX}".freeze +stable_branch = "#{BRANCH_PREFIX}-#{options[:version]}".freeze -command = "git checkout #{stable_branch} && git pull #{REMOTE} #{stable_branch} && git checkout -B #{branch} && git cherry-pick #{options[:sha]} && git push #{REMOTE} #{branch}" +command = "git fetch #{REMOTE} #{stable_branch} && git checkout #{stable_branch} && git pull #{REMOTE} #{stable_branch} && git checkout -B #{branch} && git cherry-pick #{options[:sha]} && git push #{REMOTE} #{branch}" _stdin, stdout, stderr = Open3.popen3(command) diff --git a/bin/spinach b/bin/spinach deleted file mode 100755 index eda81c9ed8a..00000000000 --- a/bin/spinach +++ /dev/null @@ -1,13 +0,0 @@ -#!/usr/bin/env ruby - -# Remove this block when removing rails5? code. -gemfile = %w[1 true].include?(ENV["RAILS5"]) ? "Gemfile.rails5" : "Gemfile" -ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../#{gemfile}", __dir__) - -begin - load File.expand_path('../spring', __FILE__) -rescue LoadError => e - raise unless e.message.include?('spring') -end -require 'bundler/setup' -load Gem.bin_path('spinach', 'spinach') diff --git a/changelogs/unreleased/45715-remove-modal-retry.yml b/changelogs/unreleased/45715-remove-modal-retry.yml new file mode 100644 index 00000000000..04f2ff5142e --- /dev/null +++ b/changelogs/unreleased/45715-remove-modal-retry.yml @@ -0,0 +1,5 @@ +--- +title: Remove modalbox confirmation when retrying a pipeline +merge_request: 18879 +author: +type: changed diff --git a/changelogs/unreleased/46286-fix-ingress-rbac-default-value.yml b/changelogs/unreleased/46286-fix-ingress-rbac-default-value.yml new file mode 100644 index 00000000000..e9cd8977394 --- /dev/null +++ b/changelogs/unreleased/46286-fix-ingress-rbac-default-value.yml @@ -0,0 +1,5 @@ +--- +title: Disables RBAC on nginx-ingress +merge_request: 18947 +author: +type: fixed diff --git a/changelogs/unreleased/46345-kubernetes-popover-illustration-skewed.yml b/changelogs/unreleased/46345-kubernetes-popover-illustration-skewed.yml new file mode 100644 index 00000000000..a0e6b39fef6 --- /dev/null +++ b/changelogs/unreleased/46345-kubernetes-popover-illustration-skewed.yml @@ -0,0 +1,5 @@ +--- +title: Correct skewed Kubernetes popover illustration +merge_request: 18949 +author: +type: fixed diff --git a/changelogs/unreleased/blackst0ne-remove-spinach.yml b/changelogs/unreleased/blackst0ne-remove-spinach.yml new file mode 100644 index 00000000000..104da257bad --- /dev/null +++ b/changelogs/unreleased/blackst0ne-remove-spinach.yml @@ -0,0 +1,5 @@ +--- +title: Remove Spinach +merge_request: 18869 +author: '@blackst0ne' +type: other diff --git a/changelogs/unreleased/blackst0ne-replace-spinach-project-forked-merge-requests-feature.yml b/changelogs/unreleased/blackst0ne-replace-spinach-project-forked-merge-requests-feature.yml new file mode 100644 index 00000000000..2ac43490c26 --- /dev/null +++ b/changelogs/unreleased/blackst0ne-replace-spinach-project-forked-merge-requests-feature.yml @@ -0,0 +1,5 @@ +--- +title: 'Replace the `project/forked_merge_requests.feature` spinach test with an rspec analog' +merge_request: 18867 +author: '@blackst0ne' +type: other diff --git a/changelogs/unreleased/blackst0ne-replace-spinach-project-merge-requests-references-feature.yml b/changelogs/unreleased/blackst0ne-replace-spinach-project-merge-requests-references-feature.yml new file mode 100644 index 00000000000..c0ba984bfdc --- /dev/null +++ b/changelogs/unreleased/blackst0ne-replace-spinach-project-merge-requests-references-feature.yml @@ -0,0 +1,5 @@ +--- +title: 'Replace the `project/merge_requests/references.feature` spinach test with an rspec analog' +merge_request: 18794 +author: '@blackst0ne' +type: other diff --git a/changelogs/unreleased/bvl-restrict-api-git-for-terms.yml b/changelogs/unreleased/bvl-restrict-api-git-for-terms.yml new file mode 100644 index 00000000000..49cd04b065b --- /dev/null +++ b/changelogs/unreleased/bvl-restrict-api-git-for-terms.yml @@ -0,0 +1,6 @@ +--- +title: Block access to the API & git for users that did not accept enforced Terms + of Service +merge_request: 18816 +author: +type: other diff --git a/changelogs/unreleased/dz-add-2fa-filter.yml b/changelogs/unreleased/dz-add-2fa-filter.yml new file mode 100644 index 00000000000..82d501d6604 --- /dev/null +++ b/changelogs/unreleased/dz-add-2fa-filter.yml @@ -0,0 +1,5 @@ +--- +title: Add 2FA filter to the group members page +merge_request: 18483 +author: +type: changed diff --git a/changelogs/unreleased/jprovazn-pipeline-policy.yml b/changelogs/unreleased/jprovazn-pipeline-policy.yml new file mode 100644 index 00000000000..2997c6c8667 --- /dev/null +++ b/changelogs/unreleased/jprovazn-pipeline-policy.yml @@ -0,0 +1,6 @@ +--- +title: Allow maintainers to retry pipelines on forked projects (if allowed in merge + request) +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/refactor-move-squash-before-merge-vue-component.yml b/changelogs/unreleased/refactor-move-squash-before-merge-vue-component.yml new file mode 100644 index 00000000000..b8b2762a21d --- /dev/null +++ b/changelogs/unreleased/refactor-move-squash-before-merge-vue-component.yml @@ -0,0 +1,5 @@ +--- +title: Move SquashBeforeMerge vue component +merge_request: 18813 +author: George Tsiolis +type: performance diff --git a/changelogs/unreleased/sh-fix-blocked-user-account-ldap.yml b/changelogs/unreleased/sh-fix-blocked-user-account-ldap.yml new file mode 100644 index 00000000000..f7abe763ea8 --- /dev/null +++ b/changelogs/unreleased/sh-fix-blocked-user-account-ldap.yml @@ -0,0 +1,5 @@ +--- +title: Fix system hook not firing for blocked users when LDAP sign-in is used +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/sh-fix-cross-site-origin-uploads-js.yml b/changelogs/unreleased/sh-fix-cross-site-origin-uploads-js.yml new file mode 100644 index 00000000000..3c51aaae896 --- /dev/null +++ b/changelogs/unreleased/sh-fix-cross-site-origin-uploads-js.yml @@ -0,0 +1,5 @@ +--- +title: Fix cross-origin errors when attempting to download JavaScript attachments +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/update-wiki-modal.yml b/changelogs/unreleased/update-wiki-modal.yml new file mode 100644 index 00000000000..00f2fc4f181 --- /dev/null +++ b/changelogs/unreleased/update-wiki-modal.yml @@ -0,0 +1,5 @@ +--- +title: New design for wiki page deletion confirmation +merge_request: 18712 +author: Constance Okoghenun +type: added diff --git a/changelogs/unreleased/zj-wiki-find-file-opt-out.yml b/changelogs/unreleased/zj-wiki-find-file-opt-out.yml new file mode 100644 index 00000000000..5af53c56017 --- /dev/null +++ b/changelogs/unreleased/zj-wiki-find-file-opt-out.yml @@ -0,0 +1,5 @@ +--- +title: Finding a wiki page is done by Gitaly by default +merge_request: +author: +type: other diff --git a/config/initializers/deprecations.rb b/config/initializers/deprecations.rb index 2476ea9e38a..c8d7f742bb1 100644 --- a/config/initializers/deprecations.rb +++ b/config/initializers/deprecations.rb @@ -1,5 +1,9 @@ -deprecator = ActiveSupport::Deprecation.new('11.0', 'GitLab') - if Gitlab.dev_env_or_com? + deprecator = ActiveSupport::Deprecation.new('11.0', 'GitLab') + + deprecator.behavior = -> (message, callstack) { + Rails.logger.warn("#{message}: #{callstack[1..20].join}") + } + ActiveSupport::Deprecation.deprecate_methods(Gitlab::GitalyClient::StorageSettings, :legacy_disk_path, deprecator: deprecator) end diff --git a/doc/README.md b/doc/README.md index c929ba7a59e..ff8dd3fab8a 100644 --- a/doc/README.md +++ b/doc/README.md @@ -241,7 +241,7 @@ GitLab.com is hosted, managed, and administered by GitLab, Inc., with and teams: Free, Bronze, Silver, and Gold. GitLab.com subscriptions grants access -to the same features available in GitLab self-hosted, **expect +to the same features available in GitLab self-hosted, **except [administration](administration/index.md) tools and settings**: - GitLab.com Free includes the same features available in Core diff --git a/doc/api/group_milestones.md b/doc/api/group_milestones.md index 21d3ac73000..152929b7614 100644 --- a/doc/api/group_milestones.md +++ b/doc/api/group_milestones.md @@ -22,7 +22,7 @@ Parameters: | --------- | ---- | -------- | ----------- | | `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user | | `iids[]` | Array[integer] | optional | Return only the milestones having the given `iid` | -| `state` | string | optional | Return only `active` or `closed` milestones` | +| `state` | string | optional | Return only `active` or `closed` milestones | | `search` | string | optional | Return only milestones with a title or description matching the provided string | ```bash diff --git a/doc/api/jobs.md b/doc/api/jobs.md index db4fe2f6880..e4e48edd9a7 100644 --- a/doc/api/jobs.md +++ b/doc/api/jobs.md @@ -82,7 +82,7 @@ Example of response "artifacts_file": null, "finished_at": "2015-12-24T17:54:24.921Z", "id": 6, - "name": "spinach:other", + "name": "rspec:other", "pipeline": { "id": 6, "ref": "master", @@ -196,7 +196,7 @@ Example of response "artifacts_file": null, "finished_at": "2015-12-24T17:54:24.921Z", "id": 6, - "name": "spinach:other", + "name": "rspec:other", "pipeline": { "id": 6, "ref": "master", diff --git a/doc/api/services.md b/doc/api/services.md index 92f12acbc73..ec632125325 100644 --- a/doc/api/services.md +++ b/doc/api/services.md @@ -968,7 +968,7 @@ Group Chat Software Set Microsoft Teams service for a project. ``` -PUT /projects/:id/services/microsoft_teams +PUT /projects/:id/services/microsoft-teams ``` Parameters: @@ -982,7 +982,7 @@ Parameters: Delete Microsoft Teams service for a project. ``` -DELETE /projects/:id/services/microsoft_teams +DELETE /projects/:id/services/microsoft-teams ``` ### Get Microsoft Teams service settings @@ -990,7 +990,7 @@ DELETE /projects/:id/services/microsoft_teams Get Microsoft Teams service settings for a project. ``` -GET /projects/:id/services/microsoft_teams +GET /projects/:id/services/microsoft-teams ``` ## Mattermost notifications diff --git a/doc/development/code_review.md b/doc/development/code_review.md index 7165b8062a7..d03b7fa23ca 100644 --- a/doc/development/code_review.md +++ b/doc/development/code_review.md @@ -29,6 +29,10 @@ There are a few rules to get your merge request accepted: to ask one of the [Merge request coaches][team]. 1. The reviewer will assign the merge request to a maintainer once the reviewer is satisfied with the state of the merge request. +1. Keep in mind that maintainers are also going to perform a final code review. + The ideal scenario is that the reviewer has already addressed any concerns + the maintainer would have found, and the maintainer only has to perform the + merge, but be prepared for further review comments. For more guidance, see [CONTRIBUTING.md](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md). @@ -207,3 +211,4 @@ Largely based on the [thoughtbot code review guide]. [projects]: https://about.gitlab.com/handbook/engineering/projects/ [team]: https://about.gitlab.com/team/ [build handbook]: https://about.gitlab.com/handbook/build/handbook/build#how-to-work-with-build +[^1]: Please note that specs other than JavaScript specs are considered backend code. diff --git a/doc/development/database_debugging.md b/doc/development/database_debugging.md index 32f392f1303..9c31265e417 100644 --- a/doc/development/database_debugging.md +++ b/doc/development/database_debugging.md @@ -11,7 +11,7 @@ Available `RAILS_ENV` - `production` (generally not for your main GDK db, but you may need this for e.g. omnibus) - `development` (this is your main GDK db) - - `test` (used for tests like rspec and spinach) + - `test` (used for tests like rspec) ## Nuke everything and start over diff --git a/doc/development/rake_tasks.md b/doc/development/rake_tasks.md index fdfa1f10402..31addcaf675 100644 --- a/doc/development/rake_tasks.md +++ b/doc/development/rake_tasks.md @@ -65,12 +65,11 @@ To make sure that indices still fit. You could find great details in: ## Run tests In order to run the test you can use the following commands: -- `rake spinach` to run the spinach suite - `rake spec` to run the rspec suite - `rake karma` to run the karma test suite - `rake gitlab:test` to run all the tests -Note: Both `rake spinach` and `rake spec` takes significant time to pass. +Note: `rake spec` takes significant time to pass. Instead of running full test suite locally you can save a lot of time by running a single test or directory related to your changes. After you submit merge request CI will run full test suite for you. Green CI status in the merge request means @@ -82,12 +81,10 @@ files it can find, also the ones in `/tmp` To run a single test file you can use: - `bin/rspec spec/controllers/commit_controller_spec.rb` for a rspec test -- `bin/spinach features/project/issues/milestones.feature` for a spinach test To run several tests inside one directory: - `bin/rspec spec/requests/api/` for the rspec tests if you want to test API only -- `bin/spinach features/profile/` for the spinach tests if you want to test only profile pages ### Speed-up tests, rake tasks, and migrations diff --git a/doc/development/testing_guide/best_practices.md b/doc/development/testing_guide/best_practices.md index 61fa5459b91..9d3f2935127 100644 --- a/doc/development/testing_guide/best_practices.md +++ b/doc/development/testing_guide/best_practices.md @@ -12,8 +12,7 @@ Here are some things to keep in mind regarding test performance: - `FactoryBot.build(...)` and `.build_stubbed` are faster than `.create`. - Don't `create` an object when `build`, `build_stubbed`, `attributes_for`, `spy`, or `double` will do. Database persistence is slow! -- Don't mark a feature as requiring JavaScript (through `@javascript` in - Spinach or `:js` in RSpec) unless it's _actually_ required for the test +- Don't mark a feature as requiring JavaScript (through `:js` in RSpec) unless it's _actually_ required for the test to be valid. Headless browser testing is slow! [parallelization]: ci.md#test-suite-parallelization-on-the-ci diff --git a/doc/development/testing_guide/ci.md b/doc/development/testing_guide/ci.md index e90de55068d..0d8e150e090 100644 --- a/doc/development/testing_guide/ci.md +++ b/doc/development/testing_guide/ci.md @@ -24,8 +24,7 @@ Our current CI parallelization setup is as follows: uploaded to S3. After that, the next pipeline will use the up-to-date -`knapsack/${CI_PROJECT_NAME}/rspec_report-master.json` file. The same strategy -is used for Spinach tests as well. +`knapsack/${CI_PROJECT_NAME}/rspec_report-master.json` file. ### Monitoring diff --git a/doc/development/testing_guide/frontend_testing.md b/doc/development/testing_guide/frontend_testing.md index 0d0d511582b..3b2b9c8c947 100644 --- a/doc/development/testing_guide/frontend_testing.md +++ b/doc/development/testing_guide/frontend_testing.md @@ -280,26 +280,6 @@ describe "Admin::AbuseReports", :js do end ``` -### Spinach errors due to missing JavaScript - -NOTE: **Note:** Since we are discouraging the use of Spinach when writing new -feature tests, you shouldn't ever need to use this. This information is kept -available for legacy purposes only. - -In Spinach, the JavaScript driver is enabled differently. In the `*.feature` -file for the failing spec, add the `@javascript` flag above the Scenario: - -``` -@javascript -Scenario: Developer can approve merge request - Given I am a "Shop" developer - And I visit project "Shop" merge requests page - And merge request 'Bug NS-04' must be approved - And I click link "Bug NS-04" - When I click link "Approve" - Then I should see approved merge request "Bug NS-04" -``` - [jasmine-focus]: https://jasmine.github.io/2.5/focused_specs.html [jasmine-jquery]: https://github.com/velesin/jasmine-jquery [karma]: http://karma-runner.github.io/ diff --git a/doc/development/testing_guide/index.md b/doc/development/testing_guide/index.md index 74d09eb91ff..0cd63a54b55 100644 --- a/doc/development/testing_guide/index.md +++ b/doc/development/testing_guide/index.md @@ -72,21 +72,6 @@ Everything you should know about how to run end-to-end tests using --- -## Spinach (feature) tests - -GitLab [moved from Cucumber to Spinach](https://github.com/gitlabhq/gitlabhq/pull/1426) -for its feature/integration tests in September 2012. - -As of March 2016, we are [trying to avoid adding new Spinach -tests](https://gitlab.com/gitlab-org/gitlab-ce/issues/14121) going forward, -opting for [RSpec feature](#features-integration) specs. - -Adding new Spinach scenarios is acceptable _only if_ the new scenario requires -no more than one new `step` definition. If more than that is required, the -test should be re-implemented using RSpec instead. - ---- - [Return to Development documentation](../README.md) [^1]: /ci/yaml/README.html#dependencies diff --git a/doc/development/testing_guide/testing_levels.md b/doc/development/testing_guide/testing_levels.md index 51794f7f4df..07ced36f0c1 100644 --- a/doc/development/testing_guide/testing_levels.md +++ b/doc/development/testing_guide/testing_levels.md @@ -81,7 +81,6 @@ possible). | Tests path | Testing engine | Notes | | ---------- | -------------- | ----- | | `spec/features/` | [Capybara] + [RSpec] | If your spec has the `:js` metadata, the browser driver will be [Poltergeist], otherwise it's using [RackTest]. | -| `features/` | Spinach | Spinach tests are deprecated, [you shouldn't add new Spinach tests](#spinach-feature-tests). | ### Consider **not** writing a system test! diff --git a/doc/install/kubernetes/gitlab_runner_chart.md b/doc/install/kubernetes/gitlab_runner_chart.md index 0a093c9ec32..2aab225fcdb 100644 --- a/doc/install/kubernetes/gitlab_runner_chart.md +++ b/doc/install/kubernetes/gitlab_runner_chart.md @@ -1,6 +1,6 @@ # GitLab Runner Helm Chart > **Note:** -These charts have been tested on Google Kubernetes Engine and Azure Container Service. Other Kubernetes installations may work as well, if not please [open an issue](https://gitlab.com/charts/charts.gitlab.io/issues). +These charts have been tested on Google Kubernetes Engine and Azure Container Service. Other Kubernetes installations may work as well, if not please [open an issue](https://gitlab.com/charts/gitlab-runner/issues). The `gitlab-runner` Helm chart deploys a GitLab Runner instance into your Kubernetes cluster. @@ -25,7 +25,7 @@ For more information on available GitLab Helm Charts, please see our [overview]( Create a `values.yaml` file for your GitLab Runner configuration. See [Helm docs](https://github.com/kubernetes/helm/blob/master/docs/chart_template_guide/values_files.md) for information on how your values file will override the defaults. -The default configuration can always be found in the [values.yaml](https://gitlab.com/charts/charts.gitlab.io/blob/master/charts/gitlab-runner/values.yaml) in the chart repository. +The default configuration can always be found in the [values.yaml](https://gitlab.com/charts/gitlab-runner/blob/master/values.yaml) in the chart repository. ### Required configuration @@ -39,7 +39,7 @@ Unless you need to specify additional configuration, you are [ready to install]( ### Other configuration -The rest of the configuration is [documented in the `values.yaml`](https://gitlab.com/charts/charts.gitlab.io/blob/master/charts/gitlab-runner/values.yaml) in the chart repository. +The rest of the configuration is [documented in the `values.yaml`](https://gitlab.com/charts/gitlab-runner/blob/master/values.yaml) in the chart repository. Here is a snippet of the important settings: diff --git a/doc/user/project/merge_requests/maintainer_access.md b/doc/user/project/merge_requests/maintainer_access.md index c9763a3fe02..89f71e16a50 100644 --- a/doc/user/project/merge_requests/maintainer_access.md +++ b/doc/user/project/merge_requests/maintainer_access.md @@ -16,3 +16,5 @@ source project, and only lasts while the merge request is open. Enable this functionality while creating a merge request: ![Enable maintainer edits](./img/allow_maintainer_push.png) + +[ce-17395]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/17395 diff --git a/features/project/commits/diff_comments.feature b/features/project/commits/diff_comments.feature deleted file mode 100644 index 35687aac9ea..00000000000 --- a/features/project/commits/diff_comments.feature +++ /dev/null @@ -1,96 +0,0 @@ -@project_commits -Feature: Project Commits Diff Comments - Background: - Given I sign in as a user - And I own project "Shop" - And I visit project commit page - - @javascript - Scenario: I can comment on a commit diff - Given I leave a diff comment like "Typo, please fix" - Then I should see a diff comment saying "Typo, please fix" - - @javascript - Scenario: I can add a diff comment with a single emoji - Given I open a diff comment form - And I write a diff comment like ":smile:" - Then I should see a diff comment with an emoji image - - @javascript - Scenario: I get a temporary form for the first comment on a diff line - Given I open a diff comment form - Then I should see a temporary diff comment form - - @javascript - Scenario: I have a cancel button on the diff form - Given I open a diff comment form - Then I should see the cancel comment button - - @javascript - Scenario: I can cancel a diff form - Given I open a diff comment form - And I cancel the diff comment - Then I should not see the diff comment form - - @javascript - Scenario: I can't open a second form for a diff line - Given I open a diff comment form - And I open a diff comment form - Then I should only see one diff form - - @javascript - Scenario: I can have multiple forms - Given I open a diff comment form - And I write a diff comment like ":-1: I don't like this" - And I open another diff comment form - Then I should see a diff comment form with ":-1: I don't like this" - And I should see an empty diff comment form - - @javascript - Scenario: I can preview multiple forms separately - Given I preview a diff comment text like "Should fix it :smile:" - And I preview another diff comment text like "DRY this up" - Then I should see two separate previews - - @javascript - Scenario: I have a reply button in discussions - Given I leave a diff comment like "Typo, please fix" - Then I should see a discussion reply button - - @javascript - Scenario: I can preview with text - Given I open a diff comment form - And I write a diff comment like ":-1: I don't like this" - Then The diff comment preview tab should display rendered Markdown - - @javascript - Scenario: I preview a diff comment - Given I preview a diff comment text like "Should fix it :smile:" - Then I should see the diff comment preview - And I should not see the diff comment text field - - @javascript - Scenario: I can edit after preview - Given I preview a diff comment text like "Should fix it :smile:" - Then I should see the diff comment write tab - - @javascript - Scenario: The form gets removed after posting - Given I preview a diff comment text like "Should fix it :smile:" - And I submit the diff comment - Then I should not see the diff comment form - And I should see a discussion reply button - - @javascript - Scenario: I can add a comment on a side-by-side commit diff (left side) - Given I open a diff comment form - And I click side-by-side diff button - When I leave a diff comment in a parallel view on the left side like "Old comment" - Then I should see a diff comment on the left side saying "Old comment" - - @javascript - Scenario: I can add a comment on a side-by-side commit diff (right side) - Given I open a diff comment form - And I click side-by-side diff button - When I leave a diff comment in a parallel view on the right side like "New comment" - Then I should see a diff comment on the right side saying "New comment" diff --git a/features/project/forked_merge_requests.feature b/features/project/forked_merge_requests.feature deleted file mode 100644 index 9809b0ea0fe..00000000000 --- a/features/project/forked_merge_requests.feature +++ /dev/null @@ -1,51 +0,0 @@ -Feature: Project Forked Merge Requests - Background: - Given I sign in as a user - And I am a member of project "Shop" - And I have a project forked off of "Shop" called "Forked Shop" - - @javascript - Scenario: I submit new unassigned merge request to a forked project - Given I visit project "Forked Shop" merge requests page - And I click link "New Merge Request" - And I fill out a "Merge Request On Forked Project" merge request - And I submit the merge request - Then I should see merge request "Merge Request On Forked Project" - - # TODO: Improve it so it does not fail randomly - # - #@javascript - #Scenario: I can edit a forked merge request - #Given I visit project "Forked Shop" merge requests page - #And I click link "New Merge Request" - #And I fill out a "Merge Request On Forked Project" merge request - #And I submit the merge request - #And I should see merge request "Merge Request On Forked Project" - #And I click link edit "Merge Request On Forked Project" - #Then I see the edit page prefilled for "Merge Request On Forked Project" - #And I update the merge request title - #And I save the merge request - #Then I should see the edited merge request - - Scenario: I cannot submit an invalid merge request - Given I visit project "Forked Shop" merge requests page - And I click link "New Merge Request" - And I fill out an invalid "Merge Request On Forked Project" merge request - Then I should see validation errors - - @javascript - Scenario: Merge request should target fork repository by default - Given I visit project "Forked Shop" merge requests page - And I click link "New Merge Request" - Then the target repository should be the original repository - - @javascript - Scenario: I see the users in the target project for a new merge request - Given I sign in as an admin - And I have a project forked off of "Shop" called "Forked Shop" - Then I visit project "Forked Shop" merge requests page - And I click link "New Merge Request" - And I fill out a "Merge Request On Forked Project" merge request - When I click "Assign to" dropdown" - Then I should see the target project ID in the input selector - And I should see the users from the target project ID diff --git a/features/project/merge_requests/references.feature b/features/project/merge_requests/references.feature deleted file mode 100644 index 571612261a9..00000000000 --- a/features/project/merge_requests/references.feature +++ /dev/null @@ -1,31 +0,0 @@ -@project_merge_requests -Feature: Project Merge Requests References - Background: - Given I sign in as "John Doe" - And public project "Community" - And "John Doe" owns public project "Community" - And project "Community" has "Community fix" open merge request - And I logout - And I sign in as "Mary Jane" - And private project "Enterprise" - And "Mary Jane" owns private project "Enterprise" - And project "Enterprise" has "Enterprise issue" open issue - And project "Enterprise" has "Enterprise fix" open merge request - And I visit issue page "Enterprise issue" - And I leave a comment referencing issue "Community fix" - And I visit merge request page "Enterprise fix" - And I leave a comment referencing issue "Community fix" - And I logout - - @javascript - Scenario: Viewing the public issue as a "John Doe" - Given I sign in as "John Doe" - When I visit issue page "Community fix" - Then I should see no notes at all - - @javascript - Scenario: Viewing the public issue as "Mary Jane" - Given I sign in as "Mary Jane" - When I visit issue page "Community fix" - And I should see a note linking to "Enterprise fix" merge request - And I should see a note linking to "Enterprise issue" issue diff --git a/features/steps/group/members.rb b/features/steps/group/members.rb deleted file mode 100644 index 97bcca7730b..00000000000 --- a/features/steps/group/members.rb +++ /dev/null @@ -1,68 +0,0 @@ -class Spinach::Features::GroupMembers < Spinach::FeatureSteps - include WaitForRequests - include SharedAuthentication - include SharedPaths - include SharedGroup - include SharedUser - - step 'I should see user "John Doe" in team list' do - expect(group_members_list).to have_content("John Doe") - end - - step 'I should not see user "Mary Jane" in team list' do - expect(group_members_list).not_to have_content("Mary Jane") - end - - step 'I click on the "Remove User From Group" button for "John Doe"' do - find(:css, '.project-members-page li', text: "John Doe").find(:css, 'a.btn-remove').click - # poltergeist always confirms popups. - end - - step 'I click on the "Remove User From Group" button for "Mary Jane"' do - find(:css, 'li', text: "Mary Jane").find(:css, 'a.btn-remove').click - # poltergeist always confirms popups. - end - - step 'I should not see the "Remove User From Group" button for "John Doe"' do - expect(find(:css, '.project-members-page li', text: "John Doe")).not_to have_selector(:css, 'a.btn-remove') - # poltergeist always confirms popups. - end - - step 'I should not see the "Remove User From Group" button for "Mary Jane"' do - expect(find(:css, 'li', text: "Mary Jane")).not_to have_selector(:css, 'a.btn-remove') - # poltergeist always confirms popups. - end - - step 'I change the "Mary Jane" role to "Developer"' do - member = mary_jane_member - - page.within "#group_member_#{member.id}" do - click_button member.human_access - - page.within '.dropdown-menu' do - click_link 'Developer' - end - - wait_for_requests - end - end - - step 'I should see "Mary Jane" as "Developer"' do - member = mary_jane_member - - page.within "#group_member_#{member.id}" do - expect(page).to have_content "Developer" - end - end - - private - - def mary_jane_member - user = User.find_by(name: "Mary Jane") - owned_group.members.find_by(user_id: user.id) - end - - def group_members_list - find(".panel .content-list") - end -end diff --git a/features/steps/profile/notifications.rb b/features/steps/profile/notifications.rb deleted file mode 100644 index f8eb0f01de8..00000000000 --- a/features/steps/profile/notifications.rb +++ /dev/null @@ -1,20 +0,0 @@ -class Spinach::Features::ProfileNotifications < Spinach::FeatureSteps - include SharedAuthentication - include SharedProject - - step 'I visit profile notifications page' do - visit profile_notifications_path - end - - step 'I should see global notifications settings' do - expect(page).to have_content "Notifications" - end - - step 'I select Mention setting from dropdown' do - first(:link, "On mention").click - end - - step 'I should see Notification saved message' do - expect(page).to have_content 'On mention' - end -end diff --git a/features/steps/project/commits/branches.rb b/features/steps/project/commits/branches.rb deleted file mode 100644 index 3ecd4c8b672..00000000000 --- a/features/steps/project/commits/branches.rb +++ /dev/null @@ -1,32 +0,0 @@ -class Spinach::Features::ProjectCommitsBranches < Spinach::FeatureSteps - include SharedAuthentication - include SharedProject - include SharedPaths - - step 'I click link "All"' do - click_link "All" - end - - step 'I click link "Protected"' do - click_link "Protected" - end - - step 'I click new branch link' do - click_link "New branch" - end - - step 'I submit new branch form with invalid name' do - fill_in 'branch_name', with: '1.0 stable' - page.find("body").click # defocus the branch_name input - select_branch('master') - click_button 'Create branch' - end - - def select_branch(branch_name) - find('.git-revision-dropdown-toggle').click - - page.within '#new-branch-form .dropdown-menu' do - click_link branch_name - end - end -end diff --git a/features/steps/project/commits/comments.rb b/features/steps/project/commits/comments.rb deleted file mode 100644 index 3d4d8ad6368..00000000000 --- a/features/steps/project/commits/comments.rb +++ /dev/null @@ -1,6 +0,0 @@ -class Spinach::Features::ProjectCommitsComments < Spinach::FeatureSteps - include SharedAuthentication - include SharedNote - include SharedPaths - include SharedProject -end diff --git a/features/steps/project/commits/diff_comments.rb b/features/steps/project/commits/diff_comments.rb deleted file mode 100644 index b9d8cf2c5a5..00000000000 --- a/features/steps/project/commits/diff_comments.rb +++ /dev/null @@ -1,6 +0,0 @@ -class Spinach::Features::ProjectCommitsDiffComments < Spinach::FeatureSteps - include SharedAuthentication - include SharedDiffNote - include SharedPaths - include SharedProject -end diff --git a/features/steps/project/create.rb b/features/steps/project/create.rb deleted file mode 100644 index 60fa232672e..00000000000 --- a/features/steps/project/create.rb +++ /dev/null @@ -1,23 +0,0 @@ -class Spinach::Features::ProjectCreate < Spinach::FeatureSteps - include SharedAuthentication - include SharedPaths - include SharedUser - - step 'fill project form with valid data' do - fill_in 'project_path', with: 'Empty' - page.within '#content-body' do - click_button "Create project" - end - end - - step 'I should see project page' do - expect(page).to have_content "Empty" - expect(current_path).to eq project_path(Project.last) - end - - step 'I should see empty project instructions' do - expect(page).to have_content "git init" - expect(page).to have_content "git remote" - expect(page).to have_content Project.last.url_to_repo - end -end diff --git a/features/steps/project/forked_merge_requests.rb b/features/steps/project/forked_merge_requests.rb deleted file mode 100644 index 82b931b2246..00000000000 --- a/features/steps/project/forked_merge_requests.rb +++ /dev/null @@ -1,139 +0,0 @@ -class Spinach::Features::ProjectForkedMergeRequests < Spinach::FeatureSteps - include SharedAuthentication - include SharedProject - include SharedNote - include SharedPaths - include Select2Helper - include WaitForRequests - include ProjectForksHelper - - step 'I am a member of project "Shop"' do - @project = ::Project.find_by(name: "Shop") - @project ||= create(:project, :repository, name: "Shop") - @project.add_reporter(@user) - end - - step 'I have a project forked off of "Shop" called "Forked Shop"' do - @forked_project = fork_project(@project, @user, - namespace: @user.namespace, - repository: true) - end - - step 'I click link "New Merge Request"' do - page.within '#content-body' do - page.has_link?('New Merge Request') ? click_link("New Merge Request") : click_link('New merge request') - end - end - - step 'I should see merge request "Merge Request On Forked Project"' do - expect(@project.merge_requests.size).to be >= 1 - @merge_request = @project.merge_requests.last - expect(current_path).to eq project_merge_request_path(@project, @merge_request) - expect(@merge_request.title).to eq "Merge Request On Forked Project" - expect(@merge_request.source_project).to eq @forked_project - expect(@merge_request.source_branch).to eq "fix" - expect(@merge_request.target_branch).to eq "master" - expect(page).to have_content @forked_project.full_path - expect(page).to have_content @project.full_path - expect(page).to have_content @merge_request.source_branch - expect(page).to have_content @merge_request.target_branch - - wait_for_requests - end - - step 'I fill out a "Merge Request On Forked Project" merge request' do - expect(page).to have_content('Source branch') - expect(page).to have_content('Target branch') - - first('.js-source-project').click - first('.dropdown-source-project a', text: @forked_project.full_path) - - first('.js-target-project').click - first('.dropdown-target-project a', text: @project.full_path) - - first('.js-source-branch').click - wait_for_requests - first('.js-source-branch-dropdown .dropdown-content a', text: 'fix').click - - click_button "Compare branches and continue" - - expect(page).to have_css("h3.page-title", text: "New Merge Request") - - page.within 'form#new_merge_request' do - fill_in "merge_request_title", with: "Merge Request On Forked Project" - end - end - - step 'I submit the merge request' do - click_button "Submit merge request" - end - - step 'I update the merge request title' do - fill_in "merge_request_title", with: "An Edited Forked Merge Request" - end - - step 'I save the merge request' do - click_button "Save changes" - end - - step 'I should see the edited merge request' do - expect(page).to have_content "An Edited Forked Merge Request" - expect(@project.merge_requests.size).to be >= 1 - @merge_request = @project.merge_requests.last - expect(current_path).to eq project_merge_request_path(@project, @merge_request) - expect(@merge_request.source_project).to eq @forked_project - expect(@merge_request.source_branch).to eq "fix" - expect(@merge_request.target_branch).to eq "master" - expect(page).to have_content @forked_project.full_path - expect(page).to have_content @project.full_path - expect(page).to have_content @merge_request.source_branch - expect(page).to have_content @merge_request.target_branch - end - - step 'I should see last push widget' do - expect(page).to have_content "You pushed to new_design" - expect(page).to have_link "Create Merge Request" - end - - step 'I click link edit "Merge Request On Forked Project"' do - find("#edit_merge_request").click - end - - step 'I see the edit page prefilled for "Merge Request On Forked Project"' do - expect(current_path).to eq edit_project_merge_request_path(@project, @merge_request) - expect(page).to have_content "Edit merge request #{@merge_request.to_reference}" - expect(find("#merge_request_title").value).to eq "Merge Request On Forked Project" - end - - step 'I fill out an invalid "Merge Request On Forked Project" merge request' do - expect(find_by_id("merge_request_source_project_id", visible: false).value).to eq @forked_project.id.to_s - expect(find_by_id("merge_request_target_project_id", visible: false).value).to eq @project.id.to_s - expect(find_by_id("merge_request_source_branch", visible: false).value).to eq nil - expect(find_by_id("merge_request_target_branch", visible: false).value).to eq "master" - click_button "Compare branches" - end - - step 'I should see validation errors' do - expect(page).to have_content "You must select source and target branch" - end - - step 'the target repository should be the original repository' do - expect(find_by_id("merge_request_target_project_id").value).to eq "#{@project.id}" - end - - step 'I click "Assign to" dropdown"' do - click_button 'Assignee' - end - - step 'I should see the target project ID in the input selector' do - expect(find('.js-assignee-search')["data-project-id"]).to eq "#{@project.id}" - end - - step 'I should see the users from the target project ID' do - page.within '.dropdown-menu-user' do - expect(page).to have_content 'Unassigned' - expect(page).to have_content current_user.name - expect(page).to have_content @project.users.first.name - end - end -end diff --git a/features/steps/project/issues/filter_labels.rb b/features/steps/project/issues/filter_labels.rb deleted file mode 100644 index b467af53c98..00000000000 --- a/features/steps/project/issues/filter_labels.rb +++ /dev/null @@ -1,61 +0,0 @@ -class Spinach::Features::ProjectIssuesFilterLabels < Spinach::FeatureSteps - include SharedAuthentication - include SharedProject - include SharedPaths - include Select2Helper - - step 'I should see "Bugfix1" in issues list' do - page.within ".issues-list" do - expect(page).to have_content "Bugfix1" - end - end - - step 'I should see "Bugfix2" in issues list' do - page.within ".issues-list" do - expect(page).to have_content "Bugfix2" - end - end - - step 'I should not see "Bugfix2" in issues list' do - page.within ".issues-list" do - expect(page).not_to have_content "Bugfix2" - end - end - - step 'I should not see "Feature1" in issues list' do - page.within ".issues-list" do - expect(page).not_to have_content "Feature1" - end - end - - step 'I click "dropdown close button"' do - page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click - sleep 2 - end - - step 'I click link "feature"' do - page.within ".labels-filter" do - click_link "feature" - end - end - - step 'project "Shop" has issue "Bugfix1" with labels: "bug", "feature"' do - project = Project.find_by(name: "Shop") - issue = create(:issue, title: "Bugfix1", project: project) - issue.labels << project.labels.find_by(title: 'bug') - issue.labels << project.labels.find_by(title: 'feature') - end - - step 'project "Shop" has issue "Bugfix2" with labels: "bug", "enhancement"' do - project = Project.find_by(name: "Shop") - issue = create(:issue, title: "Bugfix2", project: project) - issue.labels << project.labels.find_by(title: 'bug') - issue.labels << project.labels.find_by(title: 'enhancement') - end - - step 'project "Shop" has issue "Feature1" with labels: "feature"' do - project = Project.find_by(name: "Shop") - issue = create(:issue, title: "Feature1", project: project) - issue.labels << project.labels.find_by(title: 'feature') - end -end diff --git a/features/steps/project/issues/issues.rb b/features/steps/project/issues/issues.rb deleted file mode 100644 index baa78c23203..00000000000 --- a/features/steps/project/issues/issues.rb +++ /dev/null @@ -1,175 +0,0 @@ -class Spinach::Features::ProjectIssues < Spinach::FeatureSteps - include SharedAuthentication - include SharedIssuable - include SharedProject - include SharedNote - include SharedPaths - include SharedMarkdown - include SharedUser - - step 'I should not see "Release 0.3" in issues' do - expect(page).not_to have_content "Release 0.3" - end - - step 'I click link "Closed"' do - find('.issues-state-filters [data-state="closed"] span', text: 'Closed').click - end - - step 'I should see "Release 0.3" in issues' do - expect(page).to have_content "Release 0.3" - end - - step 'I should not see "Release 0.4" in issues' do - expect(page).not_to have_content "Release 0.4" - end - - step 'I click link "All"' do - find('.issues-state-filters [data-state="all"] span', text: 'All').click - # Waits for load - expect(find('.issues-state-filters > .active')).to have_content 'All' - end - - step 'I should see issue "Tweet control"' do - expect(page).to have_content "Tweet control" - end - - step 'I click "author" dropdown' do - page.find('.js-author-search').click - sleep 1 - end - - step 'I see current user as the first user' do - expect(page).to have_selector('.dropdown-content', visible: true) - users = page.all('.dropdown-menu-author .dropdown-content li a') - expect(users[0].text).to eq 'Any Author' - expect(users[1].text).to eq "#{current_user.name} #{current_user.to_reference}" - end - - step 'I click link "500 error on profile"' do - click_link "500 error on profile" - end - - step 'I should see label \'bug\' with issue' do - page.within '.issuable-show-labels' do - expect(page).to have_content 'bug' - end - end - - step 'I fill in issue search with "Re"' do - filter_issue "Re" - end - - step 'I fill in issue search with "Bu"' do - filter_issue "Bu" - end - - step 'I fill in issue search with ".3"' do - filter_issue ".3" - end - - step 'I fill in issue search with "Something"' do - filter_issue "Something" - end - - step 'I fill in issue search with ""' do - filter_issue "" - end - - step 'project "Shop" has milestone "v2.2"' do - milestone = create(:milestone, title: "v2.2", project: project) - - 3.times { create(:issue, project: project, milestone: milestone) } - end - - step 'project "Shop" has milestone "v3.0"' do - milestone = create(:milestone, title: "v3.0", project: project) - - 3.times { create(:issue, project: project, milestone: milestone) } - end - - When 'I select milestone "v3.0"' do - select "v3.0", from: "milestone_id" - end - - step 'I should see selected milestone with title "v3.0"' do - issues_milestone_selector = "#issue_milestone_id_chzn > a" - expect(find(issues_milestone_selector)).to have_content("v3.0") - end - - When 'I select first assignee from "Shop" project' do - first_assignee = project.users.first - select first_assignee.name, from: "assignee_id" - end - - step 'I should see first assignee from "Shop" as selected assignee' do - issues_assignee_selector = "#issue_assignee_id_chzn > a" - - assignee_name = project.users.first.name - expect(find(issues_assignee_selector)).to have_content(assignee_name) - end - - step 'The list should be sorted by "Least popular"' do - page.within '.issues-list' do - page.within 'li.issue:nth-child(1)' do - expect(page).to have_content 'Tweet control' - expect(page).to have_content '1 2' - end - - page.within 'li.issue:nth-child(2)' do - expect(page).to have_content 'Release 0.4' - expect(page).to have_content '2 1' - end - - page.within 'li.issue:nth-child(3)' do - expect(page).to have_content 'Bugfix' - expect(page).not_to have_content '0 0' - end - end - end - - When 'I visit empty project page' do - project = Project.find_by(name: 'Empty Project') - visit project_path(project) - end - - When "I visit project \"Community\" issues page" do - project = Project.find_by(name: 'Community') - visit project_issues_path(project) - end - - step 'project \'Shop\' has issue \'Bugfix1\' with description: \'Description for issue1\'' do - create(:issue, title: 'Bugfix1', description: 'Description for issue1', project: project) - end - - step 'project \'Shop\' has issue \'Feature1\' with description: \'Feature submitted for issue1\'' do - create(:issue, title: 'Feature1', description: 'Feature submitted for issue1', project: project) - end - - step 'I fill in issue search with \'Description for issue1\'' do - filter_issue 'Description for issue' - end - - step 'I fill in issue search with \'issue1\'' do - filter_issue 'issue1' - end - - step 'I fill in issue search with \'Rock and roll\'' do - filter_issue 'Rock and roll' - end - - step 'I should see \'Bugfix1\' in issues' do - expect(page).to have_content 'Bugfix1' - end - - step 'I should see \'Feature1\' in issues' do - expect(page).to have_content 'Feature1' - end - - step 'I should not see \'Bugfix1\' in issues' do - expect(page).not_to have_content 'Bugfix1' - end - - def filter_issue(text) - fill_in 'issuable_search', with: text - end -end diff --git a/features/steps/project/issues/milestones.rb b/features/steps/project/issues/milestones.rb deleted file mode 100644 index 30927306a4f..00000000000 --- a/features/steps/project/issues/milestones.rb +++ /dev/null @@ -1,20 +0,0 @@ -class Spinach::Features::ProjectIssuesMilestones < Spinach::FeatureSteps - include SharedAuthentication - include SharedProject - include SharedPaths - include SharedMarkdown - - step 'project "Shop" has milestone "v2.2"' do - project = Project.find_by(name: "Shop") - milestone = create(:milestone, - title: "v2.2", - project: project, - description: "# Description header" - ) - 3.times { create(:issue, project: project, milestone: milestone) } - end - - When 'I click link "All Issues"' do - click_link 'All Issues' - end -end diff --git a/features/steps/project/issues/references.rb b/features/steps/project/issues/references.rb deleted file mode 100644 index 69e8b5cbde5..00000000000 --- a/features/steps/project/issues/references.rb +++ /dev/null @@ -1,7 +0,0 @@ -class Spinach::Features::ProjectIssuesReferences < Spinach::FeatureSteps - include SharedAuthentication - include SharedIssuable - include SharedNote - include SharedProject - include SharedUser -end diff --git a/features/steps/project/merge_requests/references.rb b/features/steps/project/merge_requests/references.rb deleted file mode 100644 index ab2ae6847a2..00000000000 --- a/features/steps/project/merge_requests/references.rb +++ /dev/null @@ -1,7 +0,0 @@ -class Spinach::Features::ProjectMergeRequestsReferences < Spinach::FeatureSteps - include SharedAuthentication - include SharedIssuable - include SharedNote - include SharedProject - include SharedUser -end diff --git a/features/steps/project/source/browse_files.rb b/features/steps/project/source/browse_files.rb deleted file mode 100644 index afaad4b255e..00000000000 --- a/features/steps/project/source/browse_files.rb +++ /dev/null @@ -1,435 +0,0 @@ -# coding: utf-8 -class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps - include SharedAuthentication - include SharedProject - include SharedPaths - include RepoHelpers - include WaitForRequests - - step "I don't have write access" do - @project = create(:project, :repository, name: "Other Project", path: "other-project") - @project.add_reporter(@user) - visit project_tree_path(@project, root_ref) - end - - step 'I should see files from repository' do - expect(page).to have_content "VERSION" - expect(page).to have_content ".gitignore" - expect(page).to have_content "LICENSE" - end - - step 'I should see files from repository for "6d39438"' do - expect(current_path).to eq project_tree_path(@project, "6d39438") - expect(page).to have_content ".gitignore" - expect(page).to have_content "LICENSE" - end - - step 'I see the ".gitignore"' do - expect(page).to have_content '.gitignore' - end - - step 'I don\'t see the ".gitignore"' do - expect(page).not_to have_content '.gitignore' - end - - step 'I click on ".gitignore" file in repo' do - click_link ".gitignore" - end - - step 'I should see its content' do - wait_for_requests - expect(page).to have_content old_gitignore_content - end - - step 'I should see its new content' do - wait_for_requests - expect(page).to have_content new_gitignore_content - end - - step 'I click link "Raw"' do - click_link 'Open raw' - end - - step 'I should see raw file content' do - expect(source).to eq '' # Body is filled in by gitlab-workhorse - end - - step 'I click button "Edit"' do - find('.js-edit-blob').click - end - - step 'I cannot see the edit button' do - expect(page).not_to have_link 'edit' - end - - step 'I click button "Fork"' do - click_link 'Fork' - end - - step 'I edit code' do - expect(page).to have_selector('.file-editor') - set_new_content - end - - step 'I fill the new file name' do - fill_in :file_name, with: new_file_name - end - - step 'I fill the new branch name' do - fill_in :branch_name, with: 'new_branch_name', visible: true - end - - step 'I fill the new file name with a new directory' do - fill_in :file_name, with: new_file_name_with_directory - end - - step 'I fill the commit message' do - fill_in :commit_message, with: 'New commit message', visible: true - end - - step 'I click link "Diff"' do - click_link 'Preview changes' - end - - step 'I click on "Commit changes"' do - click_button 'Commit changes' - end - - step 'I click on "Changes" tab' do - click_link 'Changes' - end - - step 'I click on "Create directory"' do - click_button 'Create directory' - end - - step 'I click on "Delete"' do - click_on 'Delete' - end - - step 'I click on "Delete file"' do - click_button 'Delete file' - end - - step 'I click on "Replace"' do - click_on "Replace" - end - - step 'I click on "Replace file"' do - click_button 'Replace file' - end - - step 'I see diff' do - expect(page).to have_css '.line_holder.new' - end - - step 'I click on "New file" link in repo' do - find('.add-to-tree').click - click_link 'New file' - expect(page).to have_selector('.file-editor') - end - - step 'I click on "Upload file" link in repo' do - find('.add-to-tree').click - click_link 'Upload file' - end - - step 'I click on "New directory" link in repo' do - find('.add-to-tree').click - click_link 'New directory' - end - - step 'I fill the new directory name' do - fill_in :dir_name, with: new_dir_name - end - - step 'I fill an existing directory name' do - fill_in :dir_name, with: 'files' - end - - step 'I can see new file page' do - expect(page).to have_content "New File" - expect(page).to have_content "Commit message" - end - - step 'I click on "Upload file"' do - click_button 'Upload file' - end - - step 'I can see the new commit message' do - expect(page).to have_content "New commit message" - end - - step 'I upload a new text file' do - drop_in_dropzone test_text_file - end - - step 'I fill the upload file commit message' do - page.within('#modal-upload-blob') do - fill_in :commit_message, with: 'New commit message' - end - end - - step 'I replace it with a text file' do - drop_in_dropzone test_text_file - end - - step 'I fill the replace file commit message' do - page.within('#modal-upload-blob') do - fill_in :commit_message, with: 'Replacement file commit message' - end - end - - step 'I can see the replacement commit message' do - expect(page).to have_content "Replacement file commit message" - end - - step 'I can see the new text file' do - expect(page).to have_content "Lorem ipsum dolor sit amet" - expect(page).to have_content "Sed ut perspiciatis unde omnis" - end - - step 'I click on files directory' do - click_link 'files' - end - - step 'I click on History link' do - click_link 'History' - end - - step 'I see Browse dir link' do - expect(page).to have_link 'Browse Directory' - expect(page).not_to have_link 'Browse Code' - end - - step 'I click on readme file' do - page.within '.tree-table' do - click_link 'README.md' - end - end - - step 'I see Browse file link' do - expect(page).to have_link 'Browse File' - expect(page).not_to have_link 'Browse Files' - end - - step 'I see Browse code link' do - expect(page).to have_link 'Browse Files' - expect(page).not_to have_link 'Browse Directory' - end - - step 'I click on Permalink' do - click_link 'Permalink' - end - - step 'I am redirected to the files URL' do - expect(current_path).to eq project_tree_path(@project, 'master') - end - - step 'I am redirected to the ".gitignore"' do - expect(current_path).to eq(project_blob_path(@project, 'master/.gitignore')) - end - - step 'I am redirected to the permalink URL' do - expect(current_path).to( - eq(project_blob_path(@project, - @project.repository.commit.sha + - '/.gitignore')) - ) - end - - step 'I am redirected to the new file' do - expect(current_path).to eq( - project_blob_path(@project, 'master/' + new_file_name)) - end - - step 'I am redirected to the new file with directory' do - expect(current_path).to eq( - project_blob_path(@project, 'master/' + new_file_name_with_directory)) - end - - step 'I am redirected to the new merge request page' do - expect(current_path).to eq(project_new_merge_request_path(@project)) - end - - step "I am redirected to the fork's new merge request page" do - fork = @user.fork_of(@project) - expect(current_path).to eq(project_new_merge_request_path(fork)) - end - - step 'I am redirected to the root directory' do - expect(current_path).to eq( - project_tree_path(@project, 'master')) - end - - step "I don't see the permalink link" do - expect(page).not_to have_link('permalink') - end - - step 'I see "Unable to create directory"' do - expect(page).to have_content('A directory with this name already exists') - end - - step 'I see "Path can contain only..."' do - expect(page).to have_content('Path can contain only') - end - - step 'I see a commit error message' do - expect(page).to have_content('Your changes could not be committed') - end - - step "I switch ref to 'test'" do - first('.js-project-refs-dropdown').click - - page.within '.project-refs-form' do - click_link "'test'" - end - end - - step "I switch ref to fix" do - first('.js-project-refs-dropdown').click - - page.within '.project-refs-form' do - click_link 'fix' - end - end - - step "I see the ref 'test' has been selected" do - expect(page).to have_selector '.dropdown-toggle-text', text: "'test'" - end - - step "I visit the 'test' tree" do - visit project_tree_path(@project, "'test'") - end - - step "I visit the fix tree" do - visit project_tree_path(@project, "fix/.testdir") - end - - step 'I see the commit data' do - expect(page).to have_css('.tree-commit-link', visible: true) - expect(page).not_to have_content('Loading commit data...') - end - - step 'I see the commit data for a directory with a leading dot' do - expect(page).to have_css('.tree-commit-link', visible: true) - expect(page).not_to have_content('Loading commit data...') - end - - step 'I click on "files/lfs/lfs_object.iso" file in repo' do - allow_any_instance_of(Project).to receive(:lfs_enabled?).and_return(true) - visit project_tree_path(@project, "lfs") - click_link 'files' - click_link "lfs" - click_link "lfs_object.iso" - end - - step 'I should see download link and object size' do - expect(page).to have_content 'Download (1.5 MB)' - end - - step 'I should not see lfs pointer details' do - expect(page).not_to have_content 'version https://git-lfs.github.com/spec/v1' - expect(page).not_to have_content 'oid sha256:91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897' - expect(page).not_to have_content 'size 1575078' - end - - step 'I should see buttons for allowed commands' do - page.within '.content' do - expect(page).to have_link 'Download' - expect(page).to have_content 'History' - expect(page).to have_content 'Permalink' - expect(page).not_to have_content 'Edit' - expect(page).not_to have_content 'Blame' - expect(page).to have_content 'Delete' - expect(page).to have_content 'Replace' - end - end - - step 'I should see a Fork/Cancel combo' do - expect(page).to have_link 'Fork' - expect(page).to have_button 'Cancel' - end - - step 'I should see a notice about a new fork having been created' do - expect(page).to have_content "You're not allowed to make changes to this project directly. A fork of this project has been created that you can make changes in, so you can submit a merge request." - end - - # SVG files - step 'I upload a new SVG file' do - drop_in_dropzone test_svg_file - end - - step 'I visit the SVG file' do - visit project_blob_path(@project, 'new_branch_name/logo_sample.svg') - end - - step 'I can see the new rendered SVG image' do - expect(page).to have_css('.file-content img') - end - - private - - def set_new_content - find('#editor') - execute_script("ace.edit('editor').setValue('#{new_gitignore_content}')") - end - - # Content of the gitignore file on the seed repository. - def old_gitignore_content - '*.rbc' - end - - # Constant value that differs from the content - # of the gitignore of the seed repository. - def new_gitignore_content - old_gitignore_content + 'a' - end - - # Constant value that is a valid filename and - # not a filename present at root of the seed repository. - def new_file_name - 'not_a_file.md' - end - - # Constant value that is a valid filename with directory and - # not a filename present at root of the seed repository. - def new_file_name_with_directory - 'foo/bar/baz.txt' - end - - # Constant value that is a valid directory and - # not a directory present at root of the seed repository. - def new_dir_name - 'new_dir/subdir' - end - - def drop_in_dropzone(file_path) - # Generate a fake input selector - page.execute_script <<-JS - var fakeFileInput = window.$('<input/>').attr( - {id: 'fakeFileInput', type: 'file'} - ).appendTo('body'); - JS - # Attach the file to the fake input selector with Capybara - attach_file("fakeFileInput", file_path) - # Add the file to a fileList array and trigger the fake drop event - page.execute_script <<-JS - var fileList = [$('#fakeFileInput')[0].files[0]]; - var e = jQuery.Event('drop', { dataTransfer : { files : fileList } }); - $('.dropzone')[0].dropzone.listeners[0].events.drop(e); - JS - end - - def test_text_file - File.join(Rails.root, 'spec', 'fixtures', 'doc_sample.txt') - end - - def test_image_file - File.join(Rails.root, 'spec', 'fixtures', 'banana_sample.gif') - end - - def test_svg_file - File.join(Rails.root, 'spec', 'fixtures', 'logo_sample.svg') - end -end diff --git a/features/steps/shared/active_tab.rb b/features/steps/shared/active_tab.rb deleted file mode 100644 index 104d024fee2..00000000000 --- a/features/steps/shared/active_tab.rb +++ /dev/null @@ -1,32 +0,0 @@ -module SharedActiveTab - include Spinach::DSL - include WaitForRequests - - after do - wait_for_requests if javascript_test? - end - - def ensure_active_main_tab(content) - expect(find('.sidebar-top-level-items > li.active')).to have_content(content) - end - - def ensure_active_sub_tab(content) - expect(find('.sidebar-sub-level-items > li.active:not(.fly-out-top-item)')).to have_content(content) - end - - def ensure_active_sub_nav(content) - expect(find('.layout-nav .controls li.active')).to have_content(content) - end - - step 'no other main tabs should be active' do - expect(page).to have_selector('.sidebar-top-level-items > li.active', count: 1) - end - - step 'no other sub tabs should be active' do - expect(page).to have_selector('.sidebar-sub-level-items > li.active:not(.fly-out-top-item)', count: 1) - end - - step 'no other sub navs should be active' do - expect(page).to have_selector('.layout-nav .controls li.active', count: 1) - end -end diff --git a/features/steps/shared/admin.rb b/features/steps/shared/admin.rb deleted file mode 100644 index ac0a1764147..00000000000 --- a/features/steps/shared/admin.rb +++ /dev/null @@ -1,11 +0,0 @@ -module SharedAdmin - include Spinach::DSL - - step 'there are projects in system' do - 2.times { create(:project, :repository) } - end - - step 'system has users' do - 2.times { create(:user) } - end -end diff --git a/features/steps/shared/authentication.rb b/features/steps/shared/authentication.rb deleted file mode 100644 index 97fac595d8e..00000000000 --- a/features/steps/shared/authentication.rb +++ /dev/null @@ -1,75 +0,0 @@ -require Rails.root.join('features', 'support', 'login_helpers') - -module SharedAuthentication - include Spinach::DSL - include LoginHelpers - - step 'I sign in as a user' do - sign_out(@user) if @user - - @user = create(:user) - sign_in(@user) - end - - step 'I sign in via the UI' do - gitlab_sign_in(create(:user)) - end - - step 'I sign in as an admin' do - sign_out(@user) if @user - - @user = create(:admin) - sign_in(@user) - end - - step 'I sign in as "John Doe"' do - gitlab_sign_in(user_exists("John Doe")) - end - - step 'I sign in as "Mary Jane"' do - gitlab_sign_in(user_exists("Mary Jane")) - end - - step 'I should be redirected to sign in page' do - expect(current_path).to eq new_user_session_path - end - - step "I logout" do - gitlab_sign_out - end - - step "I logout directly" do - gitlab_sign_out - end - - def current_user - @user || User.reorder(nil).first - end - - private - - def gitlab_sign_in(user) - visit new_user_session_path - - fill_in "user_login", with: user.email - fill_in "user_password", with: "12345678" - check 'user_remember_me' - click_button "Sign in" - - @user = user - end - - def gitlab_sign_out - return unless @user - - if Capybara.current_driver == Capybara.javascript_driver - find('.header-user-dropdown-toggle').click - click_link 'Sign out' - expect(page).to have_button('Sign in') - else - sign_out(@user) - end - - @user = nil - end -end diff --git a/features/steps/shared/diff_note.rb b/features/steps/shared/diff_note.rb deleted file mode 100644 index aa32528a7ca..00000000000 --- a/features/steps/shared/diff_note.rb +++ /dev/null @@ -1,237 +0,0 @@ -module SharedDiffNote - include Spinach::DSL - include RepoHelpers - include WaitForRequests - - after do - wait_for_requests if javascript_test? - end - - step 'I cancel the diff comment' do - page.within(diff_file_selector) do - find(".js-close-discussion-note-form").click - end - end - - step 'I delete a diff comment' do - find('.note').hover - find(".js-note-delete").click - end - - step 'I haven\'t written any diff comment text' do - page.within(diff_file_selector) do - fill_in "note[note]", with: "" - end - end - - step 'I leave a diff comment like "Typo, please fix"' do - page.within(diff_file_selector) do - click_diff_line(sample_commit.line_code) - - page.within("form[data-line-code='#{sample_commit.line_code}']") do - fill_in "note[note]", with: "Typo, please fix" - find(".js-comment-button").click - end - end - end - - step 'I leave a diff comment in a parallel view on the left side like "Old comment"' do - click_parallel_diff_line(sample_commit.del_line_code, 'old') - page.within("#{diff_file_selector} form[data-line-code='#{sample_commit.del_line_code}']") do - fill_in "note[note]", with: "Old comment" - find(".js-comment-button").click - end - end - - step 'I leave a diff comment in a parallel view on the right side like "New comment"' do - click_parallel_diff_line(sample_commit.line_code, 'new') - page.within("#{diff_file_selector} form[data-line-code='#{sample_commit.line_code}']") do - fill_in "note[note]", with: "New comment" - find(".js-comment-button").click - end - end - - step 'I preview a diff comment text like "Should fix it :smile:"' do - page.within(diff_file_selector) do - click_diff_line(sample_commit.line_code) - - page.within("form[data-line-code='#{sample_commit.line_code}']") do - fill_in "note[note]", with: "Should fix it :smile:" - find('.js-md-preview-button').click - end - end - end - - step 'I preview another diff comment text like "DRY this up"' do - page.within(diff_file_selector) do - click_diff_line(sample_commit.del_line_code) - - page.within("form[data-line-code='#{sample_commit.del_line_code}']") do - fill_in "note[note]", with: "DRY this up" - find('.js-md-preview-button').click - end - end - end - - step 'I open a diff comment form' do - page.within(diff_file_selector) do - click_diff_line(sample_commit.line_code) - end - end - - step 'I open another diff comment form' do - page.within(diff_file_selector) do - click_diff_line(sample_commit.del_line_code) - end - end - - step 'I write a diff comment like ":-1: I don\'t like this"' do - page.within(diff_file_selector) do - fill_in "note[note]", with: ":-1: I don\'t like this" - end - end - - step 'I write a diff comment like ":smile:"' do - page.within(diff_file_selector) do - click_diff_line(sample_commit.line_code) - - page.within("form[data-line-code='#{sample_commit.line_code}']") do - fill_in 'note[note]', with: ':smile:' - click_button('Comment') - end - end - end - - step 'I submit the diff comment' do - page.within(diff_file_selector) do - click_button("Comment") - end - end - - step 'I should not see the diff comment form' do - page.within(diff_file_selector) do - expect(page).not_to have_css("form.new_note") - end - end - - step 'The diff comment preview tab should say there is nothing to do' do - page.within(diff_file_selector) do - find('.js-md-preview-button').click - expect(find('.js-md-preview')).to have_content('Nothing to preview.') - end - end - - step 'I should not see the diff comment text field' do - page.within(diff_file_selector) do - expect(find('.js-note-text')).not_to be_visible - end - end - - step 'I should only see one diff form' do - page.within(diff_file_selector) do - expect(page).to have_css("form.new-note", count: 1) - end - end - - step 'I should see a diff comment form with ":-1: I don\'t like this"' do - page.within(diff_file_selector) do - expect(page).to have_field("note[note]", with: ":-1: I don\'t like this") - end - end - - step 'I should see a diff comment saying "Typo, please fix"' do - page.within("#{diff_file_selector} .note") do - expect(page).to have_content("Typo, please fix") - end - end - - step 'I should see a diff comment on the left side saying "Old comment"' do - page.within("#{diff_file_selector} .notes_content.parallel.old") do - expect(page).to have_content("Old comment") - end - end - - step 'I should see a diff comment on the right side saying "New comment"' do - page.within("#{diff_file_selector} .notes_content.parallel.new") do - expect(page).to have_content("New comment") - end - end - - step 'I should see a discussion reply button' do - page.within(diff_file_selector) do - expect(page).to have_button('Reply...') - end - end - - step 'I should see a temporary diff comment form' do - page.within(diff_file_selector) do - expect(page).to have_css(".js-temp-notes-holder form.new-note") - end - end - - step 'I should see an empty diff comment form' do - page.within(diff_file_selector) do - expect(page).to have_field("note[note]", with: "") - end - end - - step 'I should see the cancel comment button' do - page.within("#{diff_file_selector} form") do - expect(page).to have_css(".js-close-discussion-note-form", text: "Cancel") - end - end - - step 'I should see the diff comment preview' do - page.within("#{diff_file_selector} form") do - expect(page).to have_css('.js-md-preview', visible: true) - end - end - - step 'I should see the diff comment write tab' do - page.within(diff_file_selector) do - expect(page).to have_css('.js-md-write-button', visible: true) - end - end - - step 'The diff comment preview tab should display rendered Markdown' do - page.within(diff_file_selector) do - find('.js-md-preview-button').click - expect(find('.js-md-preview')).to have_css('gl-emoji', visible: true) - end - end - - step 'I should see two separate previews' do - page.within(diff_file_selector) do - expect(page).to have_css('.js-md-preview', visible: true, count: 2) - expect(page).to have_content('Should fix it') - expect(page).to have_content('DRY this up') - end - end - - step 'I should see a diff comment with an emoji image' do - page.within("#{diff_file_selector} .note") do - expect(page).to have_xpath("//gl-emoji[@data-name='smile']") - end - end - - step 'I click side-by-side diff button' do - find('#parallel-diff-btn').click - end - - step 'I see side-by-side diff button' do - expect(page).to have_content "Side-by-side" - end - - def diff_file_selector - '.diff-file:nth-of-type(1)' - end - - def click_diff_line(code) - find(".line_holder[id='#{code}'] button").click - end - - def click_parallel_diff_line(code, line_type) - find(".line_holder.parallel td[id='#{code}']").find(:xpath, 'preceding-sibling::*[1][self::td]').hover - find(".line_holder.parallel button[data-line-code='#{code}']").click - end -end diff --git a/features/steps/shared/group.rb b/features/steps/shared/group.rb deleted file mode 100644 index 0126ce39c5a..00000000000 --- a/features/steps/shared/group.rb +++ /dev/null @@ -1,46 +0,0 @@ -module SharedGroup - include Spinach::DSL - - step 'current user is developer of group "Owned"' do - is_member_of(current_user.name, "Owned", Gitlab::Access::DEVELOPER) - end - - step '"John Doe" is guest of group "Guest"' do - is_member_of("John Doe", "Guest", Gitlab::Access::GUEST) - end - - step '"Mary Jane" is owner of group "Owned"' do - is_member_of("Mary Jane", "Owned", Gitlab::Access::OWNER) - end - - step '"Mary Jane" is guest of group "Owned"' do - is_member_of("Mary Jane", "Owned", Gitlab::Access::GUEST) - end - - step '"Mary Jane" is guest of group "Guest"' do - is_member_of("Mary Jane", "Guest", Gitlab::Access::GUEST) - end - - step 'I should see group "TestGroup"' do - expect(page).to have_content "TestGroup" - end - - step 'I should not see group "TestGroup"' do - expect(page).not_to have_content "TestGroup" - end - - protected - - def is_member_of(username, groupname, role) - user = User.find_by(name: username) || create(:user, name: username) - group = Group.find_by(name: groupname) || create(:group, name: groupname) - group.add_user(user, role) - project ||= create(:project, :repository, namespace: group) - create(:closed_issue_event, project: project) - project.add_master(user) - end - - def owned_group - @owned_group ||= Group.find_by(name: "Owned") - end -end diff --git a/features/steps/shared/issuable.rb b/features/steps/shared/issuable.rb deleted file mode 100644 index 8d9cd3db9d9..00000000000 --- a/features/steps/shared/issuable.rb +++ /dev/null @@ -1,139 +0,0 @@ -module SharedIssuable - include Spinach::DSL - - def edit_issuable - find('.js-issuable-edit', visible: true).click - end - - step 'project "Community" has "Community fix" open merge request' do - create_issuable_for_project( - project_name: 'Community', - type: :merge_request, - title: 'Community fix' - ) - end - - step 'project "Enterprise" has "Enterprise issue" open issue' do - create_issuable_for_project( - project_name: 'Enterprise', - title: 'Enterprise issue' - ) - end - - step 'project "Enterprise" has "Enterprise fix" open merge request' do - create_issuable_for_project( - project_name: 'Enterprise', - type: :merge_request, - title: 'Enterprise fix' - ) - end - - step 'I leave a comment referencing issue "Community issue"' do - leave_reference_comment( - issuable: Issue.find_by(title: 'Community issue'), - from_project_name: 'Enterprise' - ) - end - - step 'I leave a comment referencing issue "Community fix"' do - leave_reference_comment( - issuable: MergeRequest.find_by(title: 'Community fix'), - from_project_name: 'Enterprise' - ) - end - - step 'I visit issue page "Enterprise issue"' do - issue = Issue.find_by(title: 'Enterprise issue') - visit project_issue_path(issue.project, issue) - end - - step 'I visit merge request page "Enterprise fix"' do - mr = MergeRequest.find_by(title: 'Enterprise fix') - visit project_merge_request_path(mr.target_project, mr) - end - - step 'I visit issue page "Community fix"' do - mr = MergeRequest.find_by(title: 'Community fix') - visit project_merge_request_path(mr.target_project, mr) - end - - step 'I should see a note linking to "Enterprise fix" merge request' do - visible_note( - issuable: MergeRequest.find_by(title: 'Enterprise fix'), - from_project_name: 'Community', - user_name: 'Mary Jane' - ) - end - - step 'I should see a note linking to "Enterprise issue" issue' do - visible_note( - issuable: Issue.find_by(title: 'Enterprise issue'), - from_project_name: 'Community', - user_name: 'Mary Jane' - ) - end - - step 'I click link "Edit" for the merge request' do - edit_issuable - end - - step 'I sort the list by "Least popular"' do - find('button.dropdown-toggle').click - - page.within('.content ul.dropdown-menu.dropdown-menu-align-right li') do - click_link 'Least popular' - end - end - - step 'I click link "Next" in the sidebar' do - page.within '.issuable-sidebar' do - click_link 'Next' - end - end - - def create_issuable_for_project(project_name:, title:, type: :issue) - project = Project.find_by(name: project_name) - - attrs = { - title: title, - author: project.users.first, - description: '# Description header' - } - - case type - when :issue - attrs[:project] = project - when :merge_request - attrs.merge!( - source_project: project, - target_project: project, - source_branch: 'fix', - target_branch: 'master' - ) - end - - create(type, attrs) - end - - def leave_reference_comment(issuable:, from_project_name:) - project = Project.find_by(name: from_project_name) - - page.within('.js-main-target-form') do - fill_in 'note[note]', with: "##{issuable.to_reference(project)}" - click_button 'Comment' - end - end - - def visible_note(issuable:, from_project_name:, user_name:) - project = Project.find_by(name: from_project_name) - - expect(page).to have_content(user_name) - expect(page).to have_content("mentioned in #{issuable.class.to_s.titleize.downcase} #{issuable.to_reference(project)}") - end - - def expect_sidebar_content(content) - page.within '.issuable-sidebar' do - expect(page).to have_content content - end - end -end diff --git a/features/steps/shared/markdown.rb b/features/steps/shared/markdown.rb deleted file mode 100644 index 65118f07ca2..00000000000 --- a/features/steps/shared/markdown.rb +++ /dev/null @@ -1,11 +0,0 @@ -module SharedMarkdown - include Spinach::DSL - - step 'I should not see the Markdown preview' do - expect(find('.gfm-form .js-md-preview')).not_to be_visible - end - - step 'I haven\'t written any description text' do - find('.gfm-form').fill_in 'Description', with: '' - end -end diff --git a/features/steps/shared/note.rb b/features/steps/shared/note.rb deleted file mode 100644 index bf1b88c60d7..00000000000 --- a/features/steps/shared/note.rb +++ /dev/null @@ -1,25 +0,0 @@ -module SharedNote - include Spinach::DSL - include WaitForRequests - - after do - wait_for_requests if javascript_test? - end - - step 'I haven\'t written any comment text' do - page.within(".js-main-target-form") do - fill_in "note[note]", with: "" - end - end - - step 'The comment preview tab should say there is nothing to do' do - page.within(".js-main-target-form") do - find('.js-md-preview-button').click - expect(find('.js-md-preview')).to have_content('Nothing to preview.') - end - end - - step 'I should see no notes at all' do - expect(page).not_to have_css('.note') - end -end diff --git a/features/steps/shared/paths.rb b/features/steps/shared/paths.rb deleted file mode 100644 index a6bf7008955..00000000000 --- a/features/steps/shared/paths.rb +++ /dev/null @@ -1,421 +0,0 @@ -module SharedPaths - include Spinach::DSL - include RepoHelpers - include DashboardHelper - include WaitForRequests - - step 'I visit new project page' do - visit new_project_path - end - - step 'I visit login page' do - visit new_user_session_path - end - - # ---------------------------------------- - # User - # ---------------------------------------- - - step 'I visit user "John Doe" page' do - visit user_path("john_doe") - end - - # ---------------------------------------- - # Group - # ---------------------------------------- - - step 'I visit group "Owned" page' do - visit group_path(Group.find_by(name: "Owned")) - end - - step 'I visit group "Owned" activity page' do - visit activity_group_path(Group.find_by(name: "Owned")) - end - - step 'I visit group "Owned" issues page' do - visit issues_group_path(Group.find_by(name: "Owned")) - end - - step 'I visit group "Owned" merge requests page' do - visit merge_requests_group_path(Group.find_by(name: "Owned")) - end - - step 'I visit group "Owned" milestones page' do - visit group_milestones_path(Group.find_by(name: "Owned")) - end - - step 'I visit group "Owned" members page' do - visit group_group_members_path(Group.find_by(name: "Owned")) - end - - step 'I visit group "Owned" projects page' do - visit projects_group_path(Group.find_by(name: "Owned")) - end - - step 'I visit group "Guest" page' do - visit group_path(Group.find_by(name: "Guest")) - end - - step 'I visit group "Guest" issues page' do - visit issues_group_path(Group.find_by(name: "Guest")) - end - - step 'I visit group "Guest" merge requests page' do - visit merge_requests_group_path(Group.find_by(name: "Guest")) - end - - step 'I visit group "Guest" members page' do - visit group_group_members_path(Group.find_by(name: "Guest")) - end - - step 'I visit group "Guest" settings page' do - visit edit_group_path(Group.find_by(name: "Guest")) - end - - # ---------------------------------------- - # Dashboard - # ---------------------------------------- - - step 'I visit dashboard page' do - visit dashboard_projects_path - end - - step 'I visit dashboard activity page' do - visit activity_dashboard_path - end - - step 'I visit dashboard projects page' do - visit projects_dashboard_path - end - - step 'I visit dashboard issues page' do - visit assigned_issues_dashboard_path - end - - step 'I visit dashboard search page' do - visit search_path - end - - step 'I visit dashboard help page' do - visit help_path - end - - step 'I visit dashboard groups page' do - visit dashboard_groups_path - end - - step 'I should be redirected to the dashboard groups page' do - expect(current_path).to eq dashboard_groups_path - end - - step 'I visit dashboard starred projects page' do - visit starred_dashboard_projects_path - end - - # ---------------------------------------- - # Profile - # ---------------------------------------- - - step 'I visit profile page' do - visit profile_path - end - - step 'I visit profile applications page' do - visit applications_profile_path - end - - step 'I visit profile password page' do - visit edit_profile_password_path - end - - step 'I visit profile account page' do - visit profile_account_path - end - - step 'I visit profile SSH keys page' do - visit profile_keys_path - end - - step 'I visit profile preferences page' do - visit profile_preferences_path - end - - step 'I visit Authentication log page' do - visit audit_log_profile_path - end - - # ---------------------------------------- - # Admin - # ---------------------------------------- - - step 'I visit admin page' do - visit admin_root_path - end - - step 'I visit abuse reports page' do - visit admin_abuse_reports_path - end - - step 'I visit admin projects page' do - visit admin_projects_path - end - - step 'I visit admin users page' do - visit admin_users_path - end - - step 'I visit admin logs page' do - visit admin_logs_path - end - - step 'I visit admin messages page' do - visit admin_broadcast_messages_path - end - - step 'I visit admin hooks page' do - visit admin_hooks_path - end - - step 'I visit admin Resque page' do - visit admin_background_jobs_path - end - - step 'I visit admin teams page' do - visit admin_teams_path - end - - step 'I visit spam logs page' do - visit admin_spam_logs_path - end - - # ---------------------------------------- - # Generic Project - # ---------------------------------------- - - step "I visit my project's settings page" do - visit edit_project_path(@project) - end - - step 'I visit a binary file in the repo' do - visit project_blob_path(@project, - File.join(root_ref, 'files/images/logo-black.png')) - end - - step "I visit my project's commits page" do - visit project_commits_path(@project, root_ref, { limit: 5 }) - end - - step "I visit my project's commits page for a specific path" do - visit project_commits_path(@project, root_ref + "/files/ruby/regex.rb", { limit: 5 }) - end - - step 'I visit my project\'s commits stats page' do - visit stats_project_repository_path(@project) - end - - step "I visit my project's graph page" do - # Stub Graph max_size to speed up test (10 commits vs. 650) - Network::Graph.stub(max_count: 10) - - visit project_network_path(@project, root_ref) - end - - step "I visit my project's issues page" do - visit project_issues_path(@project) - end - - step "I visit my project's merge requests page" do - visit project_merge_requests_path(@project) - end - - step "I visit my project's members page" do - visit project_project_members_path(@project) - end - - step "I visit my project's wiki page" do - visit project_wiki_path(@project, :home) - end - - step 'I visit project hooks page' do - visit project_settings_integrations_path(@project) - end - - step 'I visit project find file page' do - visit project_find_file_path(@project, root_ref) - end - - # ---------------------------------------- - # "Shop" Project - # ---------------------------------------- - - step 'I visit project "Shop" page' do - visit project_path(project) - end - - step 'I visit project "Forked Shop" merge requests page' do - visit project_merge_requests_path(@forked_project) - end - - step 'I visit edit project "Shop" page' do - visit edit_project_path(project) - end - - step 'I visit compare refs page' do - visit project_compare_index_path(@project) - end - - step 'I visit project commits page' do - visit project_commits_path(@project, root_ref, { limit: 5 }) - end - - step 'I visit project commits page for stable branch' do - visit project_commits_path(@project, 'stable', { limit: 5 }) - end - - step 'I visit blob file from repo' do - visit project_blob_path(@project, File.join(sample_commit.id, sample_blob.path)) - end - - step 'I visit ".gitignore" file in repo' do - visit project_blob_path(@project, File.join(root_ref, '.gitignore')) - end - - step 'I am on the new file page' do - expect(current_path).to eq(project_create_blob_path(@project, root_ref)) - end - - step 'I am on the ".gitignore" edit file page' do - expect(current_path).to eq( - project_edit_blob_path(@project, File.join(root_ref, '.gitignore'))) - end - - step 'I visit project source page for "6d39438"' do - visit project_tree_path(@project, "6d39438") - end - - step 'I visit project source page for' \ - ' "6d394385cf567f80a8fd85055db1ab4c5295806f"' do - visit project_tree_path(@project, - '6d394385cf567f80a8fd85055db1ab4c5295806f') - end - - step 'I visit project tags page' do - visit project_tags_path(@project) - end - - step 'I visit project commit page' do - visit project_commit_path(@project, sample_commit.id) - end - - step 'I visit issue page "Release 0.4"' do - issue = Issue.find_by(title: "Release 0.4") - visit project_issue_path(issue.project, issue) - end - - step 'I visit project "Forum" labels page' do - project = Project.find_by(name: 'Forum') - visit project_labels_path(project) - end - - step 'I visit project "Shop" new label page' do - project = Project.find_by(name: 'Shop') - visit new_project_label_path(project) - end - - step 'I visit project "Forum" new label page' do - project = Project.find_by(name: 'Forum') - visit new_project_label_path(project) - end - - step 'I visit merge request page "Bug NS-04"' do - visit merge_request_path("Bug NS-04") - wait_for_requests - end - - step 'I visit merge request page "Bug NS-07"' do - visit merge_request_path("Bug NS-07") - wait_for_requests - end - - step 'I visit merge request page "Bug NS-08"' do - visit merge_request_path("Bug NS-08") - wait_for_requests - end - - step 'I visit merge request page "Bug CO-01"' do - mr = MergeRequest.find_by(title: "Bug CO-01") - visit project_merge_request_path(mr.target_project, mr) - wait_for_requests - end - - step 'I visit forked project "Shop" merge requests page' do - visit project_merge_requests_path(project) - end - - step 'I visit project "Shop" team page' do - visit project_project_members_path(project) - end - - step 'I visit project wiki page' do - visit project_wiki_path(@project, :home) - end - - # ---------------------------------------- - # Visibility Projects - # ---------------------------------------- - - step 'I visit project "Community" source page' do - project = Project.find_by(name: 'Community') - visit project_tree_path(project, root_ref) - end - - step 'I visit project "Internal" page' do - project = Project.find_by(name: "Internal") - visit project_path(project) - end - - step 'I visit project "Enterprise" page' do - project = Project.find_by(name: "Enterprise") - visit project_path(project) - end - - # ---------------------------------------- - # Empty Projects - # ---------------------------------------- - - step "I should not see command line instructions" do - expect(page).not_to have_css('.empty_wrapper') - end - - # ---------------------------------------- - # Public Projects - # ---------------------------------------- - step 'I visit the public groups area' do - visit explore_groups_path - end - - # ---------------------------------------- - # Snippets - # ---------------------------------------- - - step 'I visit project "Shop" snippets page' do - visit project_snippets_path(project) - end - - step 'I visit snippets page' do - visit explore_snippets_path - end - - def root_ref - @project.repository.root_ref - end - - def project - Project.find_by!(name: 'Shop') - end - - def merge_request_path(title) - mr = MergeRequest.find_by(title: title) - project_merge_request_path(mr.target_project, mr) - end -end diff --git a/features/steps/shared/project.rb b/features/steps/shared/project.rb deleted file mode 100644 index a1945cf5f3d..00000000000 --- a/features/steps/shared/project.rb +++ /dev/null @@ -1,132 +0,0 @@ -module SharedProject - include Spinach::DSL - - # Create a project without caring about what it's called - step "I own a project" do - @project = create(:project, :repository, namespace: @user.namespace) - @project.add_master(@user) - end - - step "I own a project in some group namespace" do - @group = create(:group, name: 'some group') - @project = create(:project, namespace: @group) - @project.add_master(@user) - end - - # Create a specific project called "Shop" - step 'I own project "Shop"' do - @project = Project.find_by(name: "Shop") - @project ||= create(:project, :repository, name: "Shop", namespace: @user.namespace) - @project.add_master(@user) - end - - def current_project - @project ||= Project.first - end - - # ---------------------------------------- - # Visibility of archived project - # ---------------------------------------- - - step 'I should not see project "Archive"' do - project = Project.find_by(name: "Archive") - expect(page).not_to have_content project.full_name - end - - step 'I should see project "Archive"' do - project = Project.find_by(name: "Archive") - expect(page).to have_content project.full_name - end - - # ---------------------------------------- - # Visibility level - # ---------------------------------------- - - step 'private project "Enterprise"' do - create(:project, :private, :repository, name: 'Enterprise') - end - - step 'I should see project "Enterprise"' do - expect(page).to have_content "Enterprise" - end - - step 'I should not see project "Enterprise"' do - expect(page).not_to have_content "Enterprise" - end - - step 'internal project "Internal"' do - create(:project, :internal, :repository, name: 'Internal') - end - - step 'I should see project "Internal"' do - page.within '.js-projects-list-holder' do - expect(page).to have_content "Internal" - end - end - - step 'I should not see project "Internal"' do - page.within '.js-projects-list-holder' do - expect(page).not_to have_content "Internal" - end - end - - step 'public project "Community"' do - create(:project, :public, :repository, name: 'Community') - end - - step 'I should see project "Community"' do - expect(page).to have_content "Community" - end - - step 'I should not see project "Community"' do - expect(page).not_to have_content "Community" - end - - step '"John Doe" owns private project "Enterprise"' do - user_owns_project( - user_name: 'John Doe', - project_name: 'Enterprise' - ) - end - - step '"Mary Jane" owns private project "Enterprise"' do - user_owns_project( - user_name: 'Mary Jane', - project_name: 'Enterprise' - ) - end - - step '"John Doe" owns internal project "Internal"' do - user_owns_project( - user_name: 'John Doe', - project_name: 'Internal', - visibility: :internal - ) - end - - step '"John Doe" owns public project "Community"' do - user_owns_project( - user_name: 'John Doe', - project_name: 'Community', - visibility: :public - ) - end - - step 'public empty project "Empty Public Project"' do - create :project_empty_repo, :public, name: "Empty Public Project" - end - - step 'project "Shop" has labels: "bug", "feature", "enhancement"' do - project = Project.find_by(name: "Shop") - create(:label, project: project, title: 'bug') - create(:label, project: project, title: 'feature') - create(:label, project: project, title: 'enhancement') - end - - def user_owns_project(user_name:, project_name:, visibility: :private) - user = user_exists(user_name, username: user_name.gsub(/\s/, '').underscore) - project = Project.find_by(name: project_name) - project ||= create(:project, visibility, name: project_name, namespace: user.namespace) - project.add_master(user) - end -end diff --git a/features/steps/shared/project_tab.rb b/features/steps/shared/project_tab.rb deleted file mode 100644 index 5a516ee33bc..00000000000 --- a/features/steps/shared/project_tab.rb +++ /dev/null @@ -1,66 +0,0 @@ -require_relative 'active_tab' - -module SharedProjectTab - include Spinach::DSL - include SharedActiveTab - - step 'the active main tab should be Project' do - ensure_active_main_tab('Overview') - end - - step 'the active main tab should be Repository' do - ensure_active_main_tab('Repository') - end - - step 'the active main tab should be Issues' do - ensure_active_main_tab('Issues') - end - - step 'the active sub tab should be Members' do - ensure_active_sub_tab('Members') - end - - step 'the active main tab should be Merge Requests' do - ensure_active_main_tab('Merge Requests') - end - - step 'the active main tab should be Snippets' do - ensure_active_main_tab('Snippets') - end - - step 'the active main tab should be Wiki' do - ensure_active_main_tab('Wiki') - end - - step 'the active main tab should be Members' do - ensure_active_main_tab('Members') - end - - step 'the active main tab should be Settings' do - ensure_active_main_tab('Settings') - end - - step 'the active sub tab should be Graph' do - ensure_active_sub_tab('Graph') - end - - step 'the active sub tab should be Files' do - ensure_active_sub_tab('Files') - end - - step 'the active sub tab should be Commits' do - ensure_active_sub_tab('Commits') - end - - step 'the active sub tab should be Home' do - ensure_active_sub_tab('Details') - end - - step 'the active sub tab should be Activity' do - ensure_active_sub_tab('Activity') - end - - step 'the active sub tab should be Charts' do - ensure_active_sub_tab('Charts') - end -end diff --git a/features/steps/shared/shortcuts.rb b/features/steps/shared/shortcuts.rb deleted file mode 100644 index a75a8474d26..00000000000 --- a/features/steps/shared/shortcuts.rb +++ /dev/null @@ -1,18 +0,0 @@ -module SharedShortcuts - include Spinach::DSL - - step 'I press "g" and "p"' do - find('body').native.send_key('g') - find('body').native.send_key('p') - end - - step 'I press "g" and "i"' do - find('body').native.send_key('g') - find('body').native.send_key('i') - end - - step 'I press "g" and "m"' do - find('body').native.send_key('g') - find('body').native.send_key('m') - end -end diff --git a/features/steps/shared/sidebar_active_tab.rb b/features/steps/shared/sidebar_active_tab.rb deleted file mode 100644 index 07fff16e867..00000000000 --- a/features/steps/shared/sidebar_active_tab.rb +++ /dev/null @@ -1,31 +0,0 @@ -module SharedSidebarActiveTab - include Spinach::DSL - - step 'no other main tabs should be active' do - expect(page).to have_selector('.nav-sidebar li.active', count: 1) - end - - def ensure_active_main_tab(content) - expect(find('.nav-sidebar li.active')).to have_content(content) - end - - step 'the active main tab should be Home' do - ensure_active_main_tab('Projects') - end - - step 'the active main tab should be Groups' do - ensure_active_main_tab('Groups') - end - - step 'the active main tab should be Projects' do - ensure_active_main_tab('Projects') - end - - step 'the active main tab should be Issues' do - ensure_active_main_tab('Issues') - end - - step 'the active main tab should be Merge Requests' do - ensure_active_main_tab('Merge Requests') - end -end diff --git a/features/steps/shared/user.rb b/features/steps/shared/user.rb deleted file mode 100644 index 9cadc91769d..00000000000 --- a/features/steps/shared/user.rb +++ /dev/null @@ -1,41 +0,0 @@ -module SharedUser - include Spinach::DSL - - step 'User "John Doe" exists' do - user_exists("John Doe", { username: "john_doe" }) - end - - step 'User "Mary Jane" exists' do - user_exists("Mary Jane", { username: "mary_jane" }) - end - - step 'gitlab user "Mike"' do - create(:user, name: "Mike") - end - - protected - - def user_exists(name, options = {}) - User.find_by(name: name) || create(:user, { name: name, admin: false }.merge(options)) - end - - step 'I have no ssh keys' do - @user.keys.delete_all - end - - step 'I click on "Personal projects" tab' do - page.within '.nav-links' do - click_link 'Personal projects' - end - - expect(page).to have_css('.tab-content #projects.active') - end - - step 'I click on "Contributed projects" tab' do - page.within '.nav-links' do - click_link 'Contributed projects' - end - - expect(page).to have_css('.tab-content #contributed.active') - end -end diff --git a/features/support/capybara.rb b/features/support/capybara.rb deleted file mode 100644 index 8879c9ab650..00000000000 --- a/features/support/capybara.rb +++ /dev/null @@ -1,50 +0,0 @@ -require 'capybara-screenshot/spinach' - -# Give CI some extra time -timeout = (ENV['CI'] || ENV['CI_SERVER']) ? 60 : 30 - -Capybara.register_driver :chrome do |app| - capabilities = Selenium::WebDriver::Remote::Capabilities.chrome( - # This enables access to logs with `page.driver.manage.get_log(:browser)` - loggingPrefs: { - browser: "ALL", - client: "ALL", - driver: "ALL", - server: "ALL" - } - ) - - options = Selenium::WebDriver::Chrome::Options.new - options.add_argument("window-size=1240,1400") - - # Chrome won't work properly in a Docker container in sandbox mode - options.add_argument("no-sandbox") - - # Run headless by default unless CHROME_HEADLESS specified - options.add_argument("headless") unless ENV['CHROME_HEADLESS'] =~ /^(false|no|0)$/i - - # Disable /dev/shm use in CI. See https://gitlab.com/gitlab-org/gitlab-ee/issues/4252 - options.add_argument("disable-dev-shm-usage") if ENV['CI'] || ENV['CI_SERVER'] - - Capybara::Selenium::Driver.new( - app, - browser: :chrome, - desired_capabilities: capabilities, - options: options - ) -end - -Capybara.javascript_driver = :chrome -Capybara.default_max_wait_time = timeout -Capybara.ignore_hidden_elements = false - -# Keep only the screenshots generated from the last failing test suite -Capybara::Screenshot.prune_strategy = :keep_last_run -# From https://github.com/mattheworiordan/capybara-screenshot/issues/84#issuecomment-41219326 -Capybara::Screenshot.register_driver(:chrome) do |driver, path| - driver.browser.save_screenshot(path) -end - -Spinach.hooks.before_run do - TestEnv.eager_load_driver_server -end diff --git a/features/support/db_cleaner.rb b/features/support/db_cleaner.rb deleted file mode 100644 index 31c922d23c3..00000000000 --- a/features/support/db_cleaner.rb +++ /dev/null @@ -1,11 +0,0 @@ -require 'database_cleaner' - -DatabaseCleaner[:active_record].strategy = :deletion - -Spinach.hooks.before_scenario do - DatabaseCleaner.start -end - -Spinach.hooks.after_scenario do - DatabaseCleaner.clean -end diff --git a/features/support/env.rb b/features/support/env.rb deleted file mode 100644 index 8fa2fcb6e3e..00000000000 --- a/features/support/env.rb +++ /dev/null @@ -1,60 +0,0 @@ -require './spec/simplecov_env' -SimpleCovEnv.start! - -ENV['RAILS_ENV'] = 'test' -require './config/environment' -require 'rspec/expectations' - -if ENV['CI'] - require 'knapsack' - Knapsack::Adapters::SpinachAdapter.bind -end - -WebMock.enable! - -%w(select2_helper test_env repo_helpers wait_for_requests project_forks_helper).each do |f| - require Rails.root.join('spec', 'support', 'helpers', f) -end - -%w(sidekiq webmock).each do |f| - require Rails.root.join('spec', 'support', f) -end - -Dir["#{Rails.root}/features/steps/shared/*.rb"].each { |file| require file } - -Spinach.hooks.before_run do - include RSpec::Mocks::ExampleMethods - include ActiveJob::TestHelper - include FactoryBot::Syntax::Methods - include GitlabRoutingHelper - - RSpec::Mocks.setup - TestEnv.init(mailer: false) - - # skip pre-receive hook check so we can use - # web editor and merge - TestEnv.disable_pre_receive -end - -Spinach.hooks.after_scenario do |scenario_data, step_definitions| - if scenario_data.tags.include?('javascript') - include WaitForRequests - block_and_wait_for_requests_complete - end -end - -module StdoutReporterWithScenarioLocation - # Override the standard reporter to show filename and line number next to each - # scenario for easy, focused re-runs - def before_scenario_run(scenario, step_definitions = nil) - @max_step_name_length = scenario.steps.map(&:name).map(&:length).max if scenario.steps.any? # rubocop:disable Gitlab/ModuleWithInstanceVariables - name = scenario.name - - # This number has no significance, it's just to line things up - max_length = @max_step_name_length + 19 # rubocop:disable Gitlab/ModuleWithInstanceVariables - out.puts "\n #{'Scenario:'.green} #{name.light_green.ljust(max_length)}" \ - " # #{scenario.feature.filename}:#{scenario.line}" - end -end - -Spinach::Reporter::Stdout.prepend(StdoutReporterWithScenarioLocation) diff --git a/features/support/gitaly.rb b/features/support/gitaly.rb deleted file mode 100644 index 3cd5f4ce497..00000000000 --- a/features/support/gitaly.rb +++ /dev/null @@ -1,3 +0,0 @@ -Spinach.hooks.before_scenario do - allow(Gitlab::GitalyClient).to receive(:feature_enabled?).and_return(true) -end diff --git a/features/support/login_helpers.rb b/features/support/login_helpers.rb deleted file mode 100644 index 540ff25a4f2..00000000000 --- a/features/support/login_helpers.rb +++ /dev/null @@ -1,19 +0,0 @@ -module LoginHelpers - # After inclusion, IntegrationHelpers calls these two methods that aren't - # supported by Spinach, so we perform the end results ourselves - class << self - def setup(*args) - Spinach.hooks.before_scenario do - Warden.test_mode! - end - end - - def teardown(*args) - Spinach.hooks.after_scenario do - Warden.test_reset! - end - end - end - - include Devise::Test::IntegrationHelpers -end diff --git a/features/support/rerun.rb b/features/support/rerun.rb deleted file mode 100644 index 60b78f9d050..00000000000 --- a/features/support/rerun.rb +++ /dev/null @@ -1,16 +0,0 @@ -# The spinach-rerun-reporter doesn't define the on_undefined_step -# See it here: https://github.com/javierav/spinach-rerun-reporter/blob/master/lib/spinach/reporter/rerun.rb -require 'spinach-rerun-reporter' - -module Spinach - class Reporter - class Rerun - def on_undefined_step(step_data, failure, step_definitions = nil) - super step_data, failure, step_definitions - - # save feature file and scenario line - @rerun << "#{current_feature.filename}:#{current_scenario.line}" - end - end - end -end diff --git a/lib/api/api_guard.rb b/lib/api/api_guard.rb index c2113551207..c17089759de 100644 --- a/lib/api/api_guard.rb +++ b/lib/api/api_guard.rb @@ -45,7 +45,9 @@ module API user = find_user_from_sources return unless user - forbidden!('User is blocked') unless Gitlab::UserAccess.new(user).allowed? && user.can?(:access_api) + unless api_access_allowed?(user) + forbidden!(api_access_denied_message(user)) + end user end @@ -72,6 +74,14 @@ module API end end end + + def api_access_allowed?(user) + Gitlab::UserAccess.new(user).allowed? && user.can?(:access_api) + end + + def api_access_denied_message(user) + Gitlab::Auth::UserAccessDeniedReason.new(user).rejection_message + end end module ClassMethods diff --git a/lib/gitlab/auth/blocked_user_tracker.rb b/lib/gitlab/auth/blocked_user_tracker.rb index dae03a179e4..7609a7b04f6 100644 --- a/lib/gitlab/auth/blocked_user_tracker.rb +++ b/lib/gitlab/auth/blocked_user_tracker.rb @@ -17,7 +17,9 @@ module Gitlab # message passed along by Warden. return unless message == User::BLOCKED_MESSAGE - login = env.dig(ACTIVE_RECORD_REQUEST_PARAMS, 'user', 'login') + # Check for either LDAP or regular GitLab account logins + login = env.dig(ACTIVE_RECORD_REQUEST_PARAMS, 'username') || + env.dig(ACTIVE_RECORD_REQUEST_PARAMS, 'user', 'login') return unless login.present? diff --git a/lib/gitlab/auth/user_access_denied_reason.rb b/lib/gitlab/auth/user_access_denied_reason.rb new file mode 100644 index 00000000000..af310aa12fc --- /dev/null +++ b/lib/gitlab/auth/user_access_denied_reason.rb @@ -0,0 +1,33 @@ +module Gitlab + module Auth + class UserAccessDeniedReason + def initialize(user) + @user = user + end + + def rejection_message + case rejection_type + when :internal + 'This action cannot be performed by internal users' + when :terms_not_accepted + 'You must accept the Terms of Service in order to perform this action. '\ + 'Please access GitLab from a web browser to accept these terms.' + else + 'Your account has been blocked.' + end + end + + private + + def rejection_type + if @user.internal? + :internal + elsif @user.required_terms_not_accepted? + :terms_not_accepted + else + :blocked + end + end + end + end +end diff --git a/lib/gitlab/build_access.rb b/lib/gitlab/build_access.rb new file mode 100644 index 00000000000..08a8f846ca5 --- /dev/null +++ b/lib/gitlab/build_access.rb @@ -0,0 +1,12 @@ +module Gitlab + class BuildAccess < UserAccess + attr_accessor :user, :project + + # This bypasses the `can?(:access_git)`-check we normally do in `UserAccess` + # for CI. That way if a user was able to trigger a pipeline, then the + # build is allowed to clone the project. + def can_access_git? + true + end + end +end diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index 5d47f8b2075..29a3a35812c 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -579,11 +579,6 @@ module Gitlab count_commits(from: from, to: to, **options) end - # Counts the amount of commits between `from` and `to`. - def count_commits_between(from, to, options = {}) - count_commits(from: from, to: to, **options) - end - # old_rev and new_rev are commit ID's # the result of this method is an array of Gitlab::Git::RawDiffChange def raw_changes_between(old_rev, new_rev) diff --git a/lib/gitlab/git/wiki.rb b/lib/gitlab/git/wiki.rb index 84a26fe4a6f..d75a5f15c29 100644 --- a/lib/gitlab/git/wiki.rb +++ b/lib/gitlab/git/wiki.rb @@ -67,7 +67,8 @@ module Gitlab end def page(title:, version: nil, dir: nil) - @repository.gitaly_migrate(:wiki_find_page) do |is_enabled| + @repository.gitaly_migrate(:wiki_find_page, + status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled| if is_enabled gitaly_find_page(title: title, version: version, dir: dir) else diff --git a/lib/gitlab/git_access.rb b/lib/gitlab/git_access.rb index 0d1ee73ca1a..db7c29be94b 100644 --- a/lib/gitlab/git_access.rb +++ b/lib/gitlab/git_access.rb @@ -2,8 +2,6 @@ # class return an instance of `GitlabAccessStatus` module Gitlab class GitAccess - include Gitlab::Utils::StrongMemoize - UnauthorizedError = Class.new(StandardError) NotFoundError = Class.new(StandardError) ProjectCreationError = Class.new(StandardError) @@ -17,7 +15,6 @@ module Gitlab deploy_key_upload: 'This deploy key does not have write access to this project.', no_repo: 'A repository for this project does not exist yet.', project_not_found: 'The project you were looking for could not be found.', - account_blocked: 'Your account has been blocked.', command_not_allowed: "The command you're trying to execute is not allowed.", upload_pack_disabled_over_http: 'Pulling over HTTP is not allowed.', receive_pack_disabled_over_http: 'Pushing over HTTP is not allowed.', @@ -108,8 +105,11 @@ module Gitlab end def check_active_user! - if user && !user_access.allowed? - raise UnauthorizedError, ERROR_MESSAGES[:account_blocked] + return unless user + + unless user_access.allowed? + message = Gitlab::Auth::UserAccessDeniedReason.new(user).rejection_message + raise UnauthorizedError, message end end @@ -340,6 +340,8 @@ module Gitlab def user_access @user_access ||= if ci? CiAccess.new + elsif user && request_from_ci_build? + BuildAccess.new(user, project: project) else UserAccess.new(user, project: project) end diff --git a/lib/gitlab/multi_collection_paginator.rb b/lib/gitlab/multi_collection_paginator.rb index 43921a8c1c0..fd5de73c526 100644 --- a/lib/gitlab/multi_collection_paginator.rb +++ b/lib/gitlab/multi_collection_paginator.rb @@ -5,7 +5,7 @@ module Gitlab def initialize(*collections, per_page: nil) raise ArgumentError.new('Only 2 collections are supported') if collections.size != 2 - @per_page = per_page || Kaminari.config.default_per_page + @per_page = (per_page || Kaminari.config.default_per_page).to_i @first_collection, @second_collection = collections end diff --git a/lib/gitlab/repo_path.rb b/lib/gitlab/repo_path.rb index 1fa2a19b0af..4888184403c 100644 --- a/lib/gitlab/repo_path.rb +++ b/lib/gitlab/repo_path.rb @@ -4,7 +4,8 @@ module Gitlab def self.parse(repo_path) wiki = false - project_path = strip_storage_path(repo_path.sub(/\.git\z/, ''), fail_on_not_found: false) + project_path = repo_path.sub(/\.git\z/, '').sub(%r{\A/}, '') + project, was_redirected = find_project(project_path) if project_path.end_with?('.wiki') && project.nil? @@ -17,22 +18,6 @@ module Gitlab [project, wiki, redirected_path] end - def self.strip_storage_path(repo_path, fail_on_not_found: true) - result = repo_path - - storage = Gitlab.config.repositories.storages.values.find do |params| - repo_path.start_with?(params.legacy_disk_path) - end - - if storage - result = result.sub(storage.legacy_disk_path, '') - elsif fail_on_not_found - raise NotFoundError.new("No known storage path matches #{repo_path.inspect}") - end - - result.sub(%r{\A/*}, '') - end - def self.find_project(project_path) project = Project.find_by_full_path(project_path, follow_redirects: true) was_redirected = project && project.full_path.casecmp(project_path) != 0 diff --git a/lib/gitlab/untrusted_regexp.rb b/lib/gitlab/untrusted_regexp.rb index fb25755391d..ce1cf737663 100644 --- a/lib/gitlab/untrusted_regexp.rb +++ b/lib/gitlab/untrusted_regexp.rb @@ -11,7 +11,11 @@ module Gitlab class UntrustedRegexp delegate :===, :source, to: :regexp - def initialize(pattern) + def initialize(pattern, multiline: false) + if multiline + pattern = "(?m)#{pattern}" + end + @regexp = RE2::Regexp.new(pattern, log_errors: false) raise RegexpError.new(regexp.error) unless regexp.ok? @@ -35,6 +39,19 @@ module Gitlab self.source == other.source end + # Handles regular expressions with the preferred RE2 library where possible + # via UntustedRegex. Falls back to Ruby's built-in regular expression library + # when the syntax would be invalid in RE2. + # + # One difference between these is `(?m)` multi-line mode. Ruby regex enables + # this by default, but also handles `^` and `$` differently. + # See: https://www.regular-expressions.info/modifiers.html + def self.with_fallback(pattern, multiline: false) + UntrustedRegexp.new(pattern, multiline: multiline) + rescue RegexpError + Regexp.new(pattern) + end + private attr_reader :regexp diff --git a/lib/tasks/gitlab/test.rake b/lib/tasks/gitlab/test.rake index 523b0fa055b..2222807fe13 100644 --- a/lib/tasks/gitlab/test.rake +++ b/lib/tasks/gitlab/test.rake @@ -4,7 +4,6 @@ namespace :gitlab do cmds = [ %w(rake brakeman), %w(rake rubocop), - %w(rake spinach), %w(rake spec), %w(rake karma) ] diff --git a/lib/tasks/spinach.rake b/lib/tasks/spinach.rake deleted file mode 100644 index 19ff13f06c0..00000000000 --- a/lib/tasks/spinach.rake +++ /dev/null @@ -1,60 +0,0 @@ -Rake::Task["spinach"].clear if Rake::Task.task_defined?('spinach') - -namespace :spinach do - namespace :project do - desc "GitLab | Spinach | Run project commits, issues and merge requests spinach features" - task :half do - run_spinach_tests('@project_commits,@project_issues,@project_merge_requests') - end - - desc "GitLab | Spinach | Run remaining project spinach features" - task :rest do - run_spinach_tests('~@admin,~@dashboard,~@profile,~@public,~@snippets,~@project_commits,~@project_issues,~@project_merge_requests') - end - end - - desc "GitLab | Spinach | Run project spinach features" - task :project do - run_spinach_tests('~@admin,~@dashboard,~@profile,~@public,~@snippets') - end - - desc "GitLab | Spinach | Run other spinach features" - task :other do - run_spinach_tests('@admin,@dashboard,@profile,@public,@snippets') - end - - desc "GitLab | Spinach | Run other spinach features" - task :builds do - run_spinach_tests('@builds') - end -end - -desc "GitLab | Run spinach" -task :spinach do - run_spinach_tests(nil) -end - -def run_system_command(cmd) - system({ 'RAILS_ENV' => 'test', 'force' => 'yes' }, *cmd) -end - -def run_spinach_command(args) - run_system_command(%w(spinach -r rerun) + args) -end - -def run_spinach_tests(tags) - success = run_spinach_command(%W(--tags #{tags})) - 3.times do |_| - break if success - break unless File.exist?('tmp/spinach-rerun.txt') - - tests = File.foreach('tmp/spinach-rerun.txt').map(&:chomp) - puts '' - puts "Spinach tests for #{tags}: Retrying tests... #{tests}".color(:red) - puts '' - sleep(3) - success = run_spinach_command(tests) - end - - raise("spinach tests for #{tags} failed!") unless success -end diff --git a/package.json b/package.json index 6d5932dcec6..59b4988b264 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "webpack-prod": "NODE_ENV=production webpack --config config/webpack.config.js" }, "dependencies": { - "@gitlab-org/gitlab-svgs": "^1.20.0", + "@gitlab-org/gitlab-svgs": "^1.22.0", "autosize": "^4.0.0", "axios": "^0.17.1", "babel-core": "^6.26.3", diff --git a/scripts/gitaly-test-build b/scripts/gitaly-test-build index b42ae2a2595..374401caf89 100755 --- a/scripts/gitaly-test-build +++ b/scripts/gitaly-test-build @@ -2,28 +2,29 @@ require 'fileutils' +require_relative 'gitaly_test' + # This script assumes tmp/tests/gitaly already contains the correct # Gitaly version. We just have to compile it and run its 'bundle -# install'. We have this separate script for that because weird things -# were happening in CI when we have a 'bundle exec' process that later -# called 'bundle install' using a different Gemfile, as happens with -# gitlab-ce and gitaly. +# install'. We have this separate script for that to avoid bundle +# poisoning in CI. This script should only be run in CI. +class GitalyTestBuild + include GitalyTest -tmp_tests_gitaly_dir = File.expand_path('../tmp/tests/gitaly', __dir__) + def run + abort 'gitaly build failed' unless system(env, 'make', chdir: tmp_tests_gitaly_dir) -# Use the top-level bundle vendor folder so that we don't reinstall gems twice -bundle_vendor_path = File.expand_path('../vendor', __dir__) + check_gitaly_config! -env = { - # This ensure the `clean` config set in `scripts/prepare_build.sh` isn't taken into account - 'BUNDLE_IGNORE_CONFIG' => 'true', - 'BUNDLE_GEMFILE' => File.join(tmp_tests_gitaly_dir, 'ruby', 'Gemfile'), - 'BUNDLE_FLAGS' => "--jobs=4 --path=#{bundle_vendor_path} --retry=3" -} + # Starting gitaly further validates its configuration + pid = start_gitaly + Process.kill('TERM', pid) -abort 'gitaly build failed' unless system(env, 'make', chdir: tmp_tests_gitaly_dir) + # Make the 'gitaly' executable look newer than 'GITALY_SERVER_VERSION'. + # Without this a gitaly executable created in the setup-test-env job + # will look stale compared to GITALY_SERVER_VERSION. + FileUtils.touch(File.join(tmp_tests_gitaly_dir, 'gitaly'), mtime: Time.now + (1 << 24)) + end +end -# Make the 'gitaly' executable look newer than 'GITALY_SERVER_VERSION'. -# Without this a gitaly executable created in the setup-test-env job -# will look stale compared to GITALY_SERVER_VERSION. -FileUtils.touch(File.join(tmp_tests_gitaly_dir, 'gitaly'), mtime: Time.now + (1 << 24)) +GitalyTestBuild.new.run diff --git a/scripts/gitaly-test-spawn b/scripts/gitaly-test-spawn index ecb68c6acc6..e9f91f75650 100755 --- a/scripts/gitaly-test-spawn +++ b/scripts/gitaly-test-spawn @@ -1,9 +1,23 @@ #!/usr/bin/env ruby -gitaly_dir = 'tmp/tests/gitaly' -env = { 'HOME' => File.expand_path('tmp/tests'), - 'GEM_PATH' => Gem.path.join(':') } -args = %W[#{gitaly_dir}/gitaly #{gitaly_dir}/config.toml] +# This script is used both in CI and in local development 'rspec' runs. -# Print the PID of the spawned process -puts spawn(env, *args, [:out, :err] => 'log/gitaly-test.log') +require_relative 'gitaly_test' + +class GitalyTestSpawn + include GitalyTest + + def run + check_gitaly_config! + + # # Uncomment line below to see all gitaly logs merged into CI trace + # spawn('sleep 1; tail -f log/gitaly-test.log') + + pid = start_gitaly + + # In local development this pid file is used by rspec. + IO.write(File.expand_path('../tmp/tests/gitaly.pid', __dir__), pid) + end +end + +GitalyTestSpawn.new.run diff --git a/scripts/gitaly_test.rb b/scripts/gitaly_test.rb new file mode 100644 index 00000000000..dee4c2eba7e --- /dev/null +++ b/scripts/gitaly_test.rb @@ -0,0 +1,97 @@ +# This file contains environment settings for gitaly when it's running +# as part of the gitlab-ce/ee test suite. +# +# Please be careful when modifying this file. Your changes must work +# both for local development rspec runs, and in CI. + +require 'socket' + +module GitalyTest + def tmp_tests_gitaly_dir + File.expand_path('../tmp/tests/gitaly', __dir__) + end + + def gemfile + File.join(tmp_tests_gitaly_dir, 'ruby', 'Gemfile') + end + + def env + env_hash = { + 'HOME' => File.expand_path('tmp/tests'), + 'GEM_PATH' => Gem.path.join(':'), + 'BUNDLE_APP_CONFIG' => File.join(File.dirname(gemfile), '.bundle/config'), + 'BUNDLE_FLAGS' => "--jobs=4 --retry=3", + 'BUNDLE_INSTALL_FLAGS' => nil, + 'BUNDLE_GEMFILE' => gemfile, + 'RUBYOPT' => nil + } + + if ENV['CI'] + bundle_path = File.expand_path('../vendor/gitaly-ruby', __dir__) + env_hash['BUNDLE_FLAGS'] << " --path=#{bundle_path}" + end + + env_hash + end + + def config_path + File.join(tmp_tests_gitaly_dir, 'config.toml') + end + + def start_gitaly + args = %W[#{tmp_tests_gitaly_dir}/gitaly #{config_path}] + pid = spawn(env, *args, [:out, :err] => 'log/gitaly-test.log') + + begin + try_connect! + rescue + Process.kill('TERM', pid) + raise + end + + pid + end + + def check_gitaly_config! + puts 'Checking gitaly-ruby bundle...' + abort 'bundle check failed' unless system(env, 'bundle', 'check', chdir: File.dirname(gemfile)) + end + + def read_socket_path + # This code needs to work in an environment where we cannot use bundler, + # so we cannot easily use the toml-rb gem. This ad-hoc parser should be + # good enough. + config_text = IO.read(config_path) + + config_text.lines.each do |line| + match_data = line.match(/^\s*socket_path\s*=\s*"([^"]*)"$/) + + return match_data[1] if match_data + end + + raise "failed to find socket_path in #{config_path}" + end + + def try_connect! + print "Trying to connect to gitaly: " + timeout = 20 + delay = 0.1 + socket = read_socket_path + + Integer(timeout / delay).times do + begin + UNIXSocket.new(socket) + puts ' OK' + + return + rescue Errno::ENOENT, Errno::ECONNREFUSED + print '.' + sleep delay + end + end + + puts ' FAILED' + + raise "could not connect to #{socket}" + end +end diff --git a/spec/controllers/concerns/send_file_upload_spec.rb b/spec/controllers/concerns/send_file_upload_spec.rb index f4c99ea4064..58bb91a0c80 100644 --- a/spec/controllers/concerns/send_file_upload_spec.rb +++ b/spec/controllers/concerns/send_file_upload_spec.rb @@ -51,6 +51,21 @@ describe SendFileUpload do end end + context 'with attachment' do + subject { controller.send_upload(uploader, attachment: 'test.js') } + + it 'sends a file with content-type of text/plain' do + expected_params = { + content_type: 'text/plain', + filename: 'test.js', + disposition: 'attachment' + } + expect(controller).to receive(:send_file).with(uploader.path, expected_params) + + subject + end + end + context 'when remote file is used' do before do stub_uploads_object_storage(uploader: uploader_class) diff --git a/spec/controllers/projects/jobs_controller_spec.rb b/spec/controllers/projects/jobs_controller_spec.rb index 2281cb420d9..a08fcea27a5 100644 --- a/spec/controllers/projects/jobs_controller_spec.rb +++ b/spec/controllers/projects/jobs_controller_spec.rb @@ -490,43 +490,43 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do id: job.id end - context 'when job has a trace artifact' do + context "when job has a trace artifact" do let(:job) { create(:ci_build, :trace_artifact, pipeline: pipeline) } it 'returns a trace' do response = subject expect(response).to have_gitlab_http_status(:ok) - expect(response.content_type).to eq 'text/plain; charset=utf-8' - expect(response.body).to eq job.job_artifacts_trace.open.read + expect(response.headers["Content-Type"]).to eq("text/plain; charset=utf-8") + expect(response.body).to eq(job.job_artifacts_trace.open.read) end end - context 'when job has a trace file' do + context "when job has a trace file" do let(:job) { create(:ci_build, :trace_live, pipeline: pipeline) } - it 'send a trace file' do + it "send a trace file" do response = subject expect(response).to have_gitlab_http_status(:ok) - expect(response.content_type).to eq 'text/plain; charset=utf-8' - expect(response.body).to eq 'BUILD TRACE' + expect(response.headers["Content-Type"]).to eq("text/plain; charset=utf-8") + expect(response.body).to eq("BUILD TRACE") end end - context 'when job has a trace in database' do + context "when job has a trace in database" do let(:job) { create(:ci_build, pipeline: pipeline) } before do - job.update_column(:trace, 'Sample trace') + job.update_column(:trace, "Sample trace") end - it 'send a trace file' do + it "send a trace file" do response = subject expect(response).to have_gitlab_http_status(:ok) - expect(response.content_type).to eq 'text/plain; charset=utf-8' - expect(response.body).to eq 'Sample trace' + expect(response.headers["Content-Type"]).to eq("text/plain; charset=utf-8") + expect(response.body).to eq("Sample trace") end end diff --git a/spec/factories/clusters/clusters.rb b/spec/factories/clusters/clusters.rb index 98566f907f9..0430762c1ff 100644 --- a/spec/factories/clusters/clusters.rb +++ b/spec/factories/clusters/clusters.rb @@ -4,8 +4,8 @@ FactoryBot.define do name 'test-cluster' trait :project do - after(:create) do |cluster, evaluator| - cluster.projects << create(:project) + before(:create) do |cluster, evaluator| + cluster.projects << create(:project, :repository) end end diff --git a/spec/features/groups/members/filter_members_spec.rb b/spec/features/groups/members/filter_members_spec.rb new file mode 100644 index 00000000000..5ddb5894624 --- /dev/null +++ b/spec/features/groups/members/filter_members_spec.rb @@ -0,0 +1,54 @@ +require 'spec_helper' + +feature 'Groups > Members > Filter members' do + let(:user) { create(:user) } + let(:user_with_2fa) { create(:user, :two_factor_via_otp) } + let(:group) { create(:group) } + + background do + group.add_owner(user) + group.add_master(user_with_2fa) + + sign_in(user) + end + + scenario 'shows all members' do + visit_members_list + + expect(first_member).to include(user.name) + expect(second_member).to include(user_with_2fa.name) + expect(page).to have_css('.member-filter-2fa-dropdown .dropdown-toggle-text', text: '2FA: Everyone') + end + + scenario 'shows only 2FA members' do + visit_members_list(two_factor: 'enabled') + + expect(first_member).to include(user_with_2fa.name) + expect(members_list.size).to eq(1) + expect(page).to have_css('.member-filter-2fa-dropdown .dropdown-toggle-text', text: '2FA: Enabled') + end + + scenario 'shows only non 2FA members' do + visit_members_list(two_factor: 'disabled') + + expect(first_member).to include(user.name) + expect(members_list.size).to eq(1) + expect(page).to have_css('.member-filter-2fa-dropdown .dropdown-toggle-text', text: '2FA: Disabled') + end + + def visit_members_list(options = {}) + visit group_group_members_path(group.to_param, options) + end + + def members_list + page.all('ul.content-list > li') + end + + def first_member + members_list.first.text + end + + def second_member + members_list.last.text + end +end diff --git a/spec/features/issuables/markdown_references/internal_references_spec.rb b/spec/features/issuables/markdown_references/internal_references_spec.rb index 8af4b157cd8..9613e22bf24 100644 --- a/spec/features/issuables/markdown_references/internal_references_spec.rb +++ b/spec/features/issuables/markdown_references/internal_references_spec.rb @@ -10,6 +10,7 @@ describe "Internal references", :js do let(:public_project_user) { public_project.owner } let(:public_project) { create(:project, :public, :repository) } let(:public_project_issue) { create(:issue, project: public_project) } + let(:public_project_merge_request) { create(:merge_request, source_project: public_project) } context "when referencing to open issue" do context "from private project" do @@ -77,4 +78,63 @@ describe "Internal references", :js do end end end + + context "when referencing to open merge request" do + context "from private project" do + context "from issue" do + before do + sign_in(private_project_user) + + visit(project_issue_path(private_project, private_project_issue)) + + add_note("##{public_project_merge_request.to_reference(private_project)}") + end + + context "when user doesn't have access to private project" do + before do + sign_in(public_project_user) + + visit(project_merge_request_path(public_project, public_project_merge_request)) + end + + it { expect(page).not_to have_css(".note") } + end + end + + context "from merge request" do + before do + sign_in(private_project_user) + + visit(project_merge_request_path(private_project, private_project_merge_request)) + + add_note("##{public_project_merge_request.to_reference(private_project)}") + end + + context "when user doesn't have access to private project" do + before do + sign_in(public_project_user) + + visit(project_merge_request_path(public_project, public_project_merge_request)) + end + + it "doesn't show any references" do + page.within(".merge-request-details") do + expect(page).not_to have_content("#merge-requests .merge-requests-title") + end + end + end + + context "when user has access to private project" do + before do + visit(project_merge_request_path(public_project, public_project_merge_request)) + end + + it "shows references" do + expect(page).to have_content("mentioned in merge request #{private_project_merge_request.to_reference(public_project)}") + .and have_content(private_project_user.name) + end + end + end + end + end end diff --git a/spec/features/projects/clusters/gcp_spec.rb b/spec/features/projects/clusters/gcp_spec.rb index fe334b531f0..a8a627d8806 100644 --- a/spec/features/projects/clusters/gcp_spec.rb +++ b/spec/features/projects/clusters/gcp_spec.rb @@ -182,6 +182,7 @@ feature 'Gcp Cluster', :js do it 'user sees a login page' do expect(page).to have_css('.signin-with-google') + expect(page).to have_link('Google account') end end diff --git a/spec/features/projects/commit/comments/user_adds_comment_spec.rb b/spec/features/projects/commit/comments/user_adds_comment_spec.rb new file mode 100644 index 00000000000..6397df086a7 --- /dev/null +++ b/spec/features/projects/commit/comments/user_adds_comment_spec.rb @@ -0,0 +1,170 @@ +require "spec_helper" + +describe "User adds a comment on a commit", :js do + include Spec::Support::Helpers::Features::NotesHelpers + include RepoHelpers + + let(:comment_text) { "XML attached" } + let(:another_comment_text) { "SVG attached" } + let(:project) { create(:project, :repository) } + let(:user) { create(:user) } + + before do + sign_in(user) + project.add_developer(user) + end + + context "inline view" do + before do + visit(project_commit_path(project, sample_commit.id)) + end + + it "adds a comment" do + page.within(".js-main-target-form") do + expect(page).not_to have_link("Cancel") + + emoji = ":+1:" + + fill_in("note[note]", with: "#{comment_text} #{emoji}") + + # Check on `Preview` tab + click_link("Preview") + + expect(find(".js-md-preview")).to have_content(comment_text).and have_css("gl-emoji") + expect(page).not_to have_css(".js-note-text") + + # Check on the `Write` tab + click_link("Write") + + expect(page).to have_field("note[note]", with: "#{comment_text} #{emoji}") + + # Submit comment from the `Preview` tab to get rid of a separate `it` block + # which would specially tests if everything gets cleared from the note form. + click_link("Preview") + click_button("Comment") + end + + wait_for_requests + + page.within(".note") do + expect(page).to have_content(comment_text).and have_css("gl-emoji") + end + + page.within(".js-main-target-form") do + expect(page).to have_field("note[note]", with: "").and have_no_css(".js-md-preview") + end + end + + context "when commenting on diff" do + it "adds a comment" do + page.within(".diff-file:nth-of-type(1)") do + # Open a form for a comment and check UI elements are visible and acting as expecting. + click_diff_line(sample_commit.line_code) + + expect(page).to have_css(".js-temp-notes-holder form.new-note") + .and have_css(".js-close-discussion-note-form", text: "Cancel") + + # The `Cancel` button closes the current form. The page should not have any open forms after that. + find(".js-close-discussion-note-form").click + + expect(page).not_to have_css("form.new_note") + + # Try to open the same form twice. There should be only one form opened. + click_diff_line(sample_commit.line_code) + click_diff_line(sample_commit.line_code) + + expect(page).to have_css("form.new-note", count: 1) + + # Fill in a form. + page.within("form[data-line-code='#{sample_commit.line_code}']") do + fill_in("note[note]", with: "#{comment_text} :smile:") + end + + # Open another form and check we have two forms now (because the first one is filled in). + click_diff_line(sample_commit.del_line_code) + + expect(page).to have_field("note[note]", with: "#{comment_text} :smile:") + .and have_field("note[note]", with: "") + + # Test Preview feature for both forms. + page.within("form[data-line-code='#{sample_commit.line_code}']") do + click_link("Preview") + end + + page.within("form[data-line-code='#{sample_commit.del_line_code}']") do + fill_in("note[note]", with: another_comment_text) + + click_link("Preview") + end + + expect(page).to have_css(".js-md-preview", visible: true, count: 2) + .and have_content(comment_text) + .and have_content(another_comment_text) + .and have_xpath("//gl-emoji[@data-name='smile']") + + # Test UI elements, then submit. + page.within("form[data-line-code='#{sample_commit.line_code}']") do + expect(find(".js-note-text", visible: false).text).to eq("") + expect(page).to have_css('.js-md-write-button') + + click_button("Comment") + end + + expect(page).to have_button("Reply...").and have_no_css("form.new_note") + end + + # A comment should be added and visible. + page.within(".diff-file:nth-of-type(1) .note") do + expect(page).to have_content(comment_text).and have_xpath("//gl-emoji[@data-name='smile']") + end + end + end + end + + context "side-by-side view" do + before do + visit(project_commit_path(project, sample_commit.id, view: "parallel")) + end + + it "adds a comment" do + new_comment = "New comment" + old_comment = "Old comment" + + # Left side. + click_parallel_diff_line(sample_commit.del_line_code) + + page.within(".diff-file:nth-of-type(1) form[data-line-code='#{sample_commit.del_line_code}']") do + fill_in("note[note]", with: old_comment) + click_button("Comment") + end + + page.within(".diff-file:nth-of-type(1) .notes_content.parallel.old") do + expect(page).to have_content(old_comment) + end + + # Right side. + click_parallel_diff_line(sample_commit.line_code) + + page.within(".diff-file:nth-of-type(1) form[data-line-code='#{sample_commit.line_code}']") do + fill_in("note[note]", with: new_comment) + click_button("Comment") + end + + wait_for_requests + + expect(all(".diff-file:nth-of-type(1) .notes_content.parallel.new")[1].text).to have_content(new_comment) + end + end + + private + + def click_diff_line(line) + find(".line_holder[id='#{line}'] td:nth-of-type(1)").hover + find(".line_holder[id='#{line}'] button").click + end + + def click_parallel_diff_line(line) + find(".line_holder.parallel td[id='#{line}']").find(:xpath, 'preceding-sibling::*[1][self::td]').hover + find(".line_holder.parallel button[data-line-code='#{line}']").click + end +end diff --git a/spec/features/projects/commit/comments/user_deletes_comments_spec.rb b/spec/features/projects/commit/comments/user_deletes_comments_spec.rb new file mode 100644 index 00000000000..a727cab4ac7 --- /dev/null +++ b/spec/features/projects/commit/comments/user_deletes_comments_spec.rb @@ -0,0 +1,37 @@ +require "spec_helper" + +describe "User deletes comments on a commit", :js do + include Spec::Support::Helpers::Features::NotesHelpers + include RepoHelpers + + let(:comment_text) { "XML attached" } + let(:project) { create(:project, :repository) } + let(:user) { create(:user) } + + before do + sign_in(user) + project.add_developer(user) + + visit(project_commit_path(project, sample_commit.id)) + + add_note(comment_text) + end + + it "deletes comment" do + page.within(".note") do + expect(page).to have_content(comment_text) + end + + page.within(".main-notes-list") do + note = find(".note") + note.hover + + find(".more-actions").click + find(".more-actions .dropdown-menu li", match: :first) + + accept_confirm { find(".js-note-delete").click } + end + + expect(page).not_to have_css(".note") + end +end diff --git a/spec/features/projects/commit/comments/user_edits_comments_spec.rb b/spec/features/projects/commit/comments/user_edits_comments_spec.rb new file mode 100644 index 00000000000..75bccd99f59 --- /dev/null +++ b/spec/features/projects/commit/comments/user_edits_comments_spec.rb @@ -0,0 +1,42 @@ +require "spec_helper" + +describe "User edits a comment on a commit", :js do + include Spec::Support::Helpers::Features::NotesHelpers + include RepoHelpers + + let(:project) { create(:project, :repository) } + let(:user) { create(:user) } + + before do + sign_in(user) + project.add_developer(user) + + visit(project_commit_path(project, sample_commit.id)) + + add_note("XML attached") + end + + it "edits comment" do + NEW_COMMENT_TEXT = "+1 Awesome!".freeze + + page.within(".main-notes-list") do + note = find(".note") + note.hover + + note.find(".js-note-edit").click + end + + page.find(".current-note-edit-form textarea") + + page.within(".current-note-edit-form") do + fill_in("note[note]", with: NEW_COMMENT_TEXT) + click_button("Save comment") + end + + wait_for_requests + + page.within(".note") do + expect(page).to have_content(NEW_COMMENT_TEXT) + end + end +end diff --git a/spec/features/projects/commit/user_comments_on_commit_spec.rb b/spec/features/projects/commit/user_comments_on_commit_spec.rb deleted file mode 100644 index 5174f793367..00000000000 --- a/spec/features/projects/commit/user_comments_on_commit_spec.rb +++ /dev/null @@ -1,110 +0,0 @@ -require "spec_helper" - -describe "User comments on commit", :js do - include Spec::Support::Helpers::Features::NotesHelpers - include RepoHelpers - - let(:project) { create(:project, :repository) } - let(:user) { create(:user) } - - COMMENT_TEXT = "XML attached".freeze - - before do - sign_in(user) - project.add_developer(user) - - visit(project_commit_path(project, sample_commit.id)) - end - - context "when adding new comment" do - it "adds comment" do - EMOJI = ":+1:".freeze - - page.within(".js-main-target-form") do - expect(page).not_to have_link("Cancel") - - fill_in("note[note]", with: "#{COMMENT_TEXT} #{EMOJI}") - - # Check on `Preview` tab - click_link("Preview") - - expect(find(".js-md-preview")).to have_content(COMMENT_TEXT).and have_css("gl-emoji") - expect(page).not_to have_css(".js-note-text") - - # Check on `Write` tab - click_link("Write") - - expect(page).to have_field("note[note]", with: "#{COMMENT_TEXT} #{EMOJI}") - - # Submit comment from the `Preview` tab to get rid of a separate `it` block - # which would specially tests if everything gets cleared from the note form. - click_link("Preview") - click_button("Comment") - end - - wait_for_requests - - page.within(".note") do - expect(page).to have_content(COMMENT_TEXT).and have_css("gl-emoji") - end - - page.within(".js-main-target-form") do - expect(page).to have_field("note[note]", with: "").and have_no_css(".js-md-preview") - end - end - end - - context "when editing comment" do - before do - add_note(COMMENT_TEXT) - end - - it "edits comment" do - NEW_COMMENT_TEXT = "+1 Awesome!".freeze - - page.within(".main-notes-list") do - note = find(".note") - note.hover - - note.find(".js-note-edit").click - end - - page.find(".current-note-edit-form textarea") - - page.within(".current-note-edit-form") do - fill_in("note[note]", with: NEW_COMMENT_TEXT) - click_button("Save comment") - end - - wait_for_requests - - page.within(".note") do - expect(page).to have_content(NEW_COMMENT_TEXT) - end - end - end - - context "when deleting comment" do - before do - add_note(COMMENT_TEXT) - end - - it "deletes comment" do - page.within(".note") do - expect(page).to have_content(COMMENT_TEXT) - end - - page.within(".main-notes-list") do - note = find(".note") - note.hover - - find(".more-actions").click - find(".more-actions .dropdown-menu li", match: :first) - - accept_confirm { find(".js-note-delete").click } - end - - expect(page).not_to have_css(".note") - end - end -end diff --git a/spec/features/projects/merge_requests/user_creates_merge_request_spec.rb b/spec/features/projects/merge_requests/user_creates_merge_request_spec.rb index f285c6c8783..1f21ef7b382 100644 --- a/spec/features/projects/merge_requests/user_creates_merge_request_spec.rb +++ b/spec/features/projects/merge_requests/user_creates_merge_request_spec.rb @@ -1,32 +1,84 @@ -require 'spec_helper' +require "spec_helper" -describe 'User creates a merge request', :js do +describe "User creates a merge request", :js do + include ProjectForksHelper + + let(:title) { "Some feature" } let(:project) { create(:project, :repository) } let(:user) { create(:user) } before do project.add_master(user) sign_in(user) + end + it "creates a merge request" do visit(project_new_merge_request_path(project)) - end - it 'creates a merge request' do - find('.js-source-branch').click - click_link('fix') + find(".js-source-branch").click + click_link("fix") - find('.js-target-branch').click - click_link('feature') + find(".js-target-branch").click + click_link("feature") - click_button('Compare branches') + click_button("Compare branches") - fill_in('merge_request_title', with: 'Wiki Feature') - click_button('Submit merge request') + fill_in("Title", with: title) + click_button("Submit merge request") - page.within('.merge-request') do - expect(page).to have_content('Wiki Feature') + page.within(".merge-request") do + expect(page).to have_content(title) end + end + + context "to a forked project" do + let(:forked_project) { fork_project(project, user, namespace: user.namespace, repository: true) } + + it "creates a merge request" do + visit(project_new_merge_request_path(forked_project)) + + expect(page).to have_content("Source branch").and have_content("Target branch") + expect(find("#merge_request_target_project_id", visible: false).value).to eq(project.id.to_s) + + click_button("Compare branches and continue") + + expect(page).to have_content("You must select source and target branch") + + first(".js-source-project").click + first(".dropdown-source-project a", text: forked_project.full_path) + + first(".js-target-project").click + first(".dropdown-target-project a", text: project.full_path) + + first(".js-source-branch").click - wait_for_requests + wait_for_requests + + source_branch = "fix" + + first(".js-source-branch-dropdown .dropdown-content a", text: source_branch).click + + click_button("Compare branches and continue") + + expect(page).to have_css("h3.page-title", text: "New Merge Request") + + page.within("form#new_merge_request") do + fill_in("Title", with: title) + end + + click_button("Assignee") + + expect(find(".js-assignee-search")["data-project-id"]).to eq(project.id.to_s) + + page.within(".dropdown-menu-user") do + expect(page).to have_content("Unassigned") + .and have_content(user.name) + .and have_content(project.users.first.name) + end + + click_button("Submit merge request") + + expect(page).to have_content(title).and have_content("Request to merge #{user.namespace.name}:#{source_branch} into master") + end end end diff --git a/spec/features/projects/pipelines/pipelines_spec.rb b/spec/features/projects/pipelines/pipelines_spec.rb index 90e28483c6c..9c165b17704 100644 --- a/spec/features/projects/pipelines/pipelines_spec.rb +++ b/spec/features/projects/pipelines/pipelines_spec.rb @@ -125,7 +125,7 @@ describe 'Pipelines', :js do context 'when canceling' do before do find('.js-pipelines-cancel-button').click - find('.js-primary-button').click + find('.js-modal-primary-action').click wait_for_requests end @@ -156,7 +156,6 @@ describe 'Pipelines', :js do context 'when retrying' do before do find('.js-pipelines-retry-button').click - find('.js-primary-button').click wait_for_requests end @@ -256,7 +255,7 @@ describe 'Pipelines', :js do context 'when canceling' do before do find('.js-pipelines-cancel-button').click - find('.js-primary-button').click + find('.js-modal-primary-action').click end it 'indicates that pipeline was canceled' do diff --git a/spec/features/projects/wiki/user_deletes_wiki_page_spec.rb b/spec/features/projects/wiki/user_deletes_wiki_page_spec.rb index ab9420fc38f..2c67cec6b67 100644 --- a/spec/features/projects/wiki/user_deletes_wiki_page_spec.rb +++ b/spec/features/projects/wiki/user_deletes_wiki_page_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -feature 'User deletes wiki page' do +feature 'User deletes wiki page', :js do let(:user) { create(:user) } let(:project) { create(:project, :wiki_repo, namespace: user.namespace) } let(:wiki_page) { create(:wiki_page, wiki: project.wiki) } @@ -13,6 +13,7 @@ feature 'User deletes wiki page' do it 'deletes a page' do click_on('Edit') click_on('Delete') + find('.js-modal-primary-action').click expect(page).to have_content('Page was successfully deleted') end diff --git a/spec/features/users/login_spec.rb b/spec/features/users/login_spec.rb index 94a2b289e64..6f968a2c590 100644 --- a/spec/features/users/login_spec.rb +++ b/spec/features/users/login_spec.rb @@ -437,5 +437,107 @@ feature 'Login' do expect(current_path).to eq(root_path) end + + context 'when 2FA is required for the user' do + before do + group = create(:group, require_two_factor_authentication: true) + group.add_developer(user) + end + + context 'when the user did not enable 2FA' do + it 'asks to set 2FA before asking to accept the terms' do + visit new_user_session_path + + fill_in 'user_login', with: user.email + fill_in 'user_password', with: '12345678' + + click_button 'Sign in' + + expect_to_be_on_terms_page + click_button 'Accept terms' + + expect(current_path).to eq(profile_two_factor_auth_path) + + fill_in 'pin_code', with: user.reload.current_otp + + click_button 'Register with two-factor app' + click_link 'Proceed' + + expect(current_path).to eq(profile_account_path) + end + end + + context 'when the user already enabled 2FA' do + before do + user.update!(otp_required_for_login: true, + otp_secret: User.generate_otp_secret(32)) + end + + it 'asks the user to accept the terms' do + visit new_user_session_path + + fill_in 'user_login', with: user.email + fill_in 'user_password', with: '12345678' + click_button 'Sign in' + + fill_in 'user_otp_attempt', with: user.reload.current_otp + click_button 'Verify code' + + expect_to_be_on_terms_page + click_button 'Accept terms' + + expect(current_path).to eq(root_path) + end + end + end + + context 'when the users password is expired' do + before do + user.update!(password_expires_at: Time.parse('2018-05-08 11:29:46 UTC')) + end + + it 'asks the user to accept the terms before setting a new password' do + visit new_user_session_path + + fill_in 'user_login', with: user.email + fill_in 'user_password', with: '12345678' + click_button 'Sign in' + + expect_to_be_on_terms_page + click_button 'Accept terms' + + expect(current_path).to eq(new_profile_password_path) + + fill_in 'user_current_password', with: '12345678' + fill_in 'user_password', with: 'new password' + fill_in 'user_password_confirmation', with: 'new password' + click_button 'Set new password' + + expect(page).to have_content('Password successfully changed') + end + end + + context 'when the user does not have an email configured' do + let(:user) { create(:omniauth_user, extern_uid: 'my-uid', provider: 'saml', email: 'temp-email-for-oauth-user@gitlab.localhost') } + + before do + stub_omniauth_saml_config(enabled: true, auto_link_saml_user: true, allow_single_sign_on: ['saml'], providers: [mock_saml_config]) + end + + it 'asks the user to accept the terms before setting an email' do + gitlab_sign_in_via('saml', user, 'my-uid') + + expect_to_be_on_terms_page + click_button 'Accept terms' + + expect(current_path).to eq(profile_path) + + fill_in 'Email', with: 'hello@world.com' + + click_button 'Update profile settings' + + expect(page).to have_content('Profile was successfully updated') + end + end end end diff --git a/spec/features/users/terms_spec.rb b/spec/features/users/terms_spec.rb index bf6b5fa3d6a..f9469adbfe3 100644 --- a/spec/features/users/terms_spec.rb +++ b/spec/features/users/terms_spec.rb @@ -81,4 +81,22 @@ describe 'Users > Terms' do expect(find_field('issue_description').value).to eq("We don't want to lose what the user typed") end end + + context 'when the terms are enforced' do + before do + enforce_terms + end + + context 'signing out', :js do + it 'allows the user to sign out without a response' do + visit terms_path + + find('.header-user-dropdown-toggle').click + click_link('Sign out') + + expect(page).to have_content('Sign in') + expect(page).to have_content('Register') + end + end + end end diff --git a/spec/javascripts/helpers/vue_mount_component_helper.js b/spec/javascripts/helpers/vue_mount_component_helper.js index effacbcff4e..a34a1add4e0 100644 --- a/spec/javascripts/helpers/vue_mount_component_helper.js +++ b/spec/javascripts/helpers/vue_mount_component_helper.js @@ -1,14 +1,30 @@ +import Vue from 'vue'; + +const mountComponent = (Component, props = {}, el = null) => new Component({ + propsData: props, +}).$mount(el); + export const createComponentWithStore = (Component, store, propsData = {}) => new Component({ store, propsData, }); +export const createComponentWithMixin = (mixins = [], state = {}, props = {}, template = '<div></div>') => { + const Component = Vue.extend({ + template, + mixins, + data() { + return props; + }, + }); + + return mountComponent(Component, props); +}; + export const mountComponentWithStore = (Component, { el, props, store }) => new Component({ store, propsData: props || { }, }).$mount(el); -export default (Component, props = {}, el = null) => new Component({ - propsData: props, -}).$mount(el); +export default mountComponent; diff --git a/spec/javascripts/pipelines/async_button_spec.js b/spec/javascripts/pipelines/async_button_spec.js deleted file mode 100644 index e0ea3649646..00000000000 --- a/spec/javascripts/pipelines/async_button_spec.js +++ /dev/null @@ -1,62 +0,0 @@ -import Vue from 'vue'; -import asyncButtonComp from '~/pipelines/components/async_button.vue'; -import eventHub from '~/pipelines/event_hub'; - -describe('Pipelines Async Button', () => { - let component; - let AsyncButtonComponent; - - beforeEach(() => { - AsyncButtonComponent = Vue.extend(asyncButtonComp); - - component = new AsyncButtonComponent({ - propsData: { - endpoint: '/foo', - title: 'Foo', - icon: 'repeat', - cssClass: 'bar', - pipelineId: 123, - type: 'explode', - }, - }).$mount(); - }); - - it('should render a button', () => { - expect(component.$el.tagName).toEqual('BUTTON'); - }); - - it('should render svg icon', () => { - expect(component.$el.querySelector('svg')).not.toBeNull(); - }); - - it('should render the provided title', () => { - expect(component.$el.getAttribute('data-original-title')).toContain('Foo'); - expect(component.$el.getAttribute('aria-label')).toContain('Foo'); - }); - - it('should render the provided cssClass', () => { - expect(component.$el.getAttribute('class')).toContain('bar'); - }); - - describe('With confirm dialog', () => { - it('should call the service when confimation is positive', () => { - eventHub.$on('openConfirmationModal', (data) => { - expect(data.pipelineId).toEqual(123); - expect(data.type).toEqual('explode'); - }); - - component = new AsyncButtonComponent({ - propsData: { - endpoint: '/foo', - title: 'Foo', - icon: 'fa fa-foo', - cssClass: 'bar', - pipelineId: 123, - type: 'explode', - }, - }).$mount(); - - component.$el.click(); - }); - }); -}); diff --git a/spec/javascripts/pipelines/pipelines_table_row_spec.js b/spec/javascripts/pipelines/pipelines_table_row_spec.js index de744739e42..05ca4cb9044 100644 --- a/spec/javascripts/pipelines/pipelines_table_row_spec.js +++ b/spec/javascripts/pipelines/pipelines_table_row_spec.js @@ -1,5 +1,6 @@ import Vue from 'vue'; import tableRowComp from '~/pipelines/components/pipelines_table_row.vue'; +import eventHub from '~/pipelines/event_hub'; describe('Pipelines Table Row', () => { const jsonFixtureName = 'pipelines/pipelines.json'; @@ -151,13 +152,37 @@ describe('Pipelines Table Row', () => { describe('actions column', () => { beforeEach(() => { - component = buildComponent(pipeline); + const withActions = Object.assign({}, pipeline); + withActions.flags.cancelable = true; + withActions.flags.retryable = true; + withActions.cancel_path = '/cancel'; + withActions.retry_path = '/retry'; + + component = buildComponent(withActions); }); it('should render the provided actions', () => { - expect( - component.$el.querySelectorAll('.table-section:nth-child(6) ul li').length, - ).toEqual(pipeline.details.manual_actions.length); + expect(component.$el.querySelector('.js-pipelines-retry-button')).not.toBeNull(); + expect(component.$el.querySelector('.js-pipelines-cancel-button')).not.toBeNull(); + }); + + it('emits `retryPipeline` event when retry button is clicked and toggles loading', () => { + eventHub.$on('retryPipeline', (endpoint) => { + expect(endpoint).toEqual('/retry'); + }); + + component.$el.querySelector('.js-pipelines-retry-button').click(); + expect(component.isRetrying).toEqual(true); + }); + + it('emits `openConfirmationModal` event when cancel button is clicked and toggles loading', () => { + eventHub.$on('openConfirmationModal', (data) => { + expect(data.endpoint).toEqual('/cancel'); + expect(data.pipelineId).toEqual(pipeline.id); + }); + + component.$el.querySelector('.js-pipelines-cancel-button').click(); + expect(component.isCancelling).toEqual(true); }); }); }); diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_maintainer_edit_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_maintainer_edit_spec.js deleted file mode 100644 index cee22d5342a..00000000000 --- a/spec/javascripts/vue_mr_widget/components/mr_widget_maintainer_edit_spec.js +++ /dev/null @@ -1,40 +0,0 @@ -import Vue from 'vue'; -import maintainerEditComponent from '~/vue_merge_request_widget/components/mr_widget_maintainer_edit.vue'; -import mountComponent from 'spec/helpers/vue_mount_component_helper'; - -describe('RWidgetMaintainerEdit', () => { - let Component; - let vm; - - beforeEach(() => { - Component = Vue.extend(maintainerEditComponent); - }); - - afterEach(() => { - vm.$destroy(); - }); - - describe('when a maintainer is allowed to edit', () => { - beforeEach(() => { - vm = mountComponent(Component, { - maintainerEditAllowed: true, - }); - }); - - it('it renders the message', () => { - expect(vm.$el.textContent.trim()).toEqual('Allows edits from maintainers'); - }); - }); - - describe('when a maintainer is not allowed to edit', () => { - beforeEach(() => { - vm = mountComponent(Component, { - maintainerEditAllowed: false, - }); - }); - - it('hides the message', () => { - expect(vm.$el.textContent.trim()).toEqual(''); - }); - }); -}); diff --git a/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js b/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js index e55c7649d40..30918428da2 100644 --- a/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js +++ b/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js @@ -1,5 +1,5 @@ import Vue from 'vue'; -import mrWidgetOptions from '~/vue_merge_request_widget/mr_widget_options'; +import mrWidgetOptions from '~/vue_merge_request_widget/mr_widget_options.vue'; import eventHub from '~/vue_merge_request_widget/event_hub'; import notify from '~/lib/utils/notify'; import { stateKey } from '~/vue_merge_request_widget/stores/state_maps'; diff --git a/spec/lib/gitlab/auth/blocked_user_tracker_spec.rb b/spec/lib/gitlab/auth/blocked_user_tracker_spec.rb index 726a3c1c83a..43b68e69131 100644 --- a/spec/lib/gitlab/auth/blocked_user_tracker_spec.rb +++ b/spec/lib/gitlab/auth/blocked_user_tracker_spec.rb @@ -17,12 +17,8 @@ describe Gitlab::Auth::BlockedUserTracker do end context 'failed login due to blocked user' do - let(:env) do - { - 'warden.options' => { message: User::BLOCKED_MESSAGE }, - described_class::ACTIVE_RECORD_REQUEST_PARAMS => { 'user' => { 'login' => user.username } } - } - end + let(:base_env) { { 'warden.options' => { message: User::BLOCKED_MESSAGE } } } + let(:env) { base_env.merge(request_env) } subject { described_class.log_if_user_blocked(env) } @@ -30,23 +26,37 @@ describe Gitlab::Auth::BlockedUserTracker do expect_any_instance_of(SystemHooksService).to receive(:execute_hooks_for).with(user, :failed_login) end - it 'logs a blocked user' do - user.block! + context 'via GitLab login' do + let(:request_env) { { described_class::ACTIVE_RECORD_REQUEST_PARAMS => { 'user' => { 'login' => user.username } } } } - expect(subject).to be_truthy - end + it 'logs a blocked user' do + user.block! + + expect(subject).to be_truthy + end - it 'logs a blocked user by e-mail' do - user.block! - env[described_class::ACTIVE_RECORD_REQUEST_PARAMS]['user']['login'] = user.email + it 'logs a blocked user by e-mail' do + user.block! + env[described_class::ACTIVE_RECORD_REQUEST_PARAMS]['user']['login'] = user.email - expect(subject).to be_truthy + expect(subject).to be_truthy + end end - it 'logs a LDAP blocked user' do - user.ldap_block! + context 'via LDAP login' do + let(:request_env) { { described_class::ACTIVE_RECORD_REQUEST_PARAMS => { 'username' => user.username } } } + + it 'logs a blocked user' do + user.block! + + expect(subject).to be_truthy + end + + it 'logs a LDAP blocked user' do + user.ldap_block! - expect(subject).to be_truthy + expect(subject).to be_truthy + end end end end diff --git a/spec/lib/gitlab/auth/user_access_denied_reason_spec.rb b/spec/lib/gitlab/auth/user_access_denied_reason_spec.rb new file mode 100644 index 00000000000..fa209bed74e --- /dev/null +++ b/spec/lib/gitlab/auth/user_access_denied_reason_spec.rb @@ -0,0 +1,34 @@ +require 'spec_helper' + +describe Gitlab::Auth::UserAccessDeniedReason do + include TermsHelper + let(:user) { build(:user) } + + let(:reason) { described_class.new(user) } + + describe '#rejection_message' do + subject { reason.rejection_message } + + context 'when a user is blocked' do + before do + user.block! + end + + it { is_expected.to match /blocked/ } + end + + context 'a user did not accept the enforced terms' do + before do + enforce_terms + end + + it { is_expected.to match /You must accept the Terms of Service/ } + end + + context 'when the user is internal' do + let(:user) { User.ghost } + + it { is_expected.to match /This action cannot be performed by internal users/ } + end + end +end diff --git a/spec/lib/gitlab/build_access_spec.rb b/spec/lib/gitlab/build_access_spec.rb new file mode 100644 index 00000000000..08f50bf4fac --- /dev/null +++ b/spec/lib/gitlab/build_access_spec.rb @@ -0,0 +1,23 @@ +require 'spec_helper' + +describe Gitlab::BuildAccess do + let(:user) { create(:user) } + let(:project) { create(:project) } + + describe '#can_do_action' do + subject { described_class.new(user, project: project).can_do_action?(:download_code) } + + context 'when the user can do an action on the project but cannot access git' do + before do + user.block! + project.add_developer(user) + end + + it { is_expected.to be(true) } + end + + context 'when the user cannot do an action on the project' do + it { is_expected.to be(false) } + end + end +end diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb index 6c625596605..317a932d5a6 100644 --- a/spec/lib/gitlab/git_access_spec.rb +++ b/spec/lib/gitlab/git_access_spec.rb @@ -1,7 +1,9 @@ require 'spec_helper' describe Gitlab::GitAccess do - set(:user) { create(:user) } + include TermsHelper + + let(:user) { create(:user) } let(:actor) { user } let(:project) { create(:project, :repository) } @@ -1040,6 +1042,96 @@ describe Gitlab::GitAccess do end end + context 'terms are enforced' do + before do + enforce_terms + end + + shared_examples 'access after accepting terms' do + let(:actions) do + [-> { pull_access_check }, + -> { push_access_check }] + end + + it 'blocks access when the user did not accept terms', :aggregate_failures do + actions.each do |action| + expect { action.call }.to raise_unauthorized(/You must accept the Terms of Service in order to perform this action/) + end + end + + it 'allows access when the user accepted the terms', :aggregate_failures do + accept_terms(user) + + actions.each do |action| + expect { action.call }.not_to raise_error + end + end + end + + describe 'as an anonymous user to a public project' do + let(:actor) { nil } + let(:project) { create(:project, :public, :repository) } + + it { expect { pull_access_check }.not_to raise_error } + end + + describe 'as a guest to a public project' do + let(:project) { create(:project, :public, :repository) } + + it_behaves_like 'access after accepting terms' do + let(:actions) { [-> { pull_access_check }] } + end + end + + describe 'as a reporter to the project' do + before do + project.add_reporter(user) + end + + it_behaves_like 'access after accepting terms' do + let(:actions) { [-> { pull_access_check }] } + end + end + + describe 'as a developer of the project' do + before do + project.add_developer(user) + end + + it_behaves_like 'access after accepting terms' + end + + describe 'as a master of the project' do + before do + project.add_master(user) + end + + it_behaves_like 'access after accepting terms' + end + + describe 'as an owner of the project' do + let(:project) { create(:project, :repository, namespace: user.namespace) } + + it_behaves_like 'access after accepting terms' + end + + describe 'when a ci build clones the project' do + let(:protocol) { 'http' } + let(:authentication_abilities) { [:build_download_code] } + let(:auth_result_type) { :build } + + before do + project.add_developer(user) + end + + it "doesn't block http pull" do + aggregate_failures do + expect { pull_access_check }.not_to raise_error + end + end + end + end + private def raise_unauthorized(message) diff --git a/spec/lib/gitlab/repo_path_spec.rb b/spec/lib/gitlab/repo_path_spec.rb index f030f371372..13940713dfc 100644 --- a/spec/lib/gitlab/repo_path_spec.rb +++ b/spec/lib/gitlab/repo_path_spec.rb @@ -45,25 +45,6 @@ describe ::Gitlab::RepoPath do end end - describe '.strip_storage_path' do - before do - allow(Gitlab.config.repositories).to receive(:storages).and_return({ - 'storage1' => Gitlab::GitalyClient::StorageSettings.new('path' => '/foo'), - 'storage2' => Gitlab::GitalyClient::StorageSettings.new('path' => '/bar') - }) - end - - it 'strips the storage path' do - expect(described_class.strip_storage_path('/bar/foo/qux/baz.git')).to eq('foo/qux/baz.git') - end - - it 'raises NotFoundError if no storage matches the path' do - expect { described_class.strip_storage_path('/doesnotexist/foo.git') }.to raise_error( - described_class::NotFoundError - ) - end - end - describe '.find_project' do let(:project) { create(:project) } let(:redirect) { project.route.create_redirect('foo/bar/baz') } diff --git a/spec/lib/gitlab/untrusted_regexp_spec.rb b/spec/lib/gitlab/untrusted_regexp_spec.rb index bed58d407ef..0ee7fa1e570 100644 --- a/spec/lib/gitlab/untrusted_regexp_spec.rb +++ b/spec/lib/gitlab/untrusted_regexp_spec.rb @@ -39,6 +39,14 @@ describe Gitlab::UntrustedRegexp do expect(result).to be_falsy end + + it 'can handle regular expressions in multiline mode' do + regexp = described_class.new('^\d', multiline: true) + + result = regexp === "Header\n\n1. Content" + + expect(result).to be_truthy + end end describe '#scan' do diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 3f2eb58f009..ad094b3ed48 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -2,6 +2,7 @@ require 'spec_helper' describe User do include ProjectForksHelper + include TermsHelper describe 'modules' do subject { described_class } @@ -2728,4 +2729,30 @@ describe User do .to change { RedirectRoute.where(path: 'foo').count }.by(-1) end end + + describe '#required_terms_not_accepted?' do + let(:user) { build(:user) } + subject { user.required_terms_not_accepted? } + + context "when terms are not enforced" do + it { is_expected.to be_falsy } + end + + context "when terms are enforced and accepted by the user" do + before do + enforce_terms + accept_terms(user) + end + + it { is_expected.to be_falsy } + end + + context "when terms are enforced but the user has not accepted" do + before do + enforce_terms + end + + it { is_expected.to be_truthy } + end + end end diff --git a/spec/policies/ci/build_policy_spec.rb b/spec/policies/ci/build_policy_spec.rb index 41cf2ef7225..9ca156deaa0 100644 --- a/spec/policies/ci/build_policy_spec.rb +++ b/spec/policies/ci/build_policy_spec.rb @@ -94,6 +94,19 @@ describe Ci::BuildPolicy do end end end + + context 'when maintainer is allowed to push to pipeline branch' do + let(:project) { create(:project, :public) } + let(:owner) { user } + + it 'enables update_build if user is maintainer' do + allow_any_instance_of(Project).to receive(:empty_repo?).and_return(false) + allow_any_instance_of(Project).to receive(:branch_allows_maintainer_push?).and_return(true) + + expect(policy).to be_allowed :update_build + expect(policy).to be_allowed :update_commit_status + end + end end describe 'rules for protected ref' do diff --git a/spec/policies/ci/pipeline_policy_spec.rb b/spec/policies/ci/pipeline_policy_spec.rb index 48a8064c5fc..a5e509cfa0f 100644 --- a/spec/policies/ci/pipeline_policy_spec.rb +++ b/spec/policies/ci/pipeline_policy_spec.rb @@ -62,5 +62,17 @@ describe Ci::PipelinePolicy, :models do end end end + + context 'when maintainer is allowed to push to pipeline branch' do + let(:project) { create(:project, :public) } + let(:owner) { user } + + it 'enables update_pipeline if user is maintainer' do + allow_any_instance_of(Project).to receive(:empty_repo?).and_return(false) + allow_any_instance_of(Project).to receive(:branch_allows_maintainer_push?).and_return(true) + + expect(policy).to be_allowed :update_pipeline + end + end end end diff --git a/spec/policies/global_policy_spec.rb b/spec/policies/global_policy_spec.rb index ec26810e371..873673b50ef 100644 --- a/spec/policies/global_policy_spec.rb +++ b/spec/policies/global_policy_spec.rb @@ -90,4 +90,94 @@ describe GlobalPolicy do it { is_expected.to be_allowed(:update_custom_attribute) } end end + + shared_examples 'access allowed when terms accepted' do |ability| + it { is_expected.not_to be_allowed(ability) } + + it "allows #{ability} when the user accepted the terms" do + accept_terms(current_user) + + is_expected.to be_allowed(ability) + end + end + + describe 'API access' do + context 'regular user' do + it { is_expected.to be_allowed(:access_api) } + end + + context 'admin' do + let(:current_user) { create(:admin) } + + it { is_expected.to be_allowed(:access_api) } + end + + context 'anonymous' do + let(:current_user) { nil } + + it { is_expected.to be_allowed(:access_api) } + end + + context 'when terms are enforced' do + before do + enforce_terms + end + + context 'regular user' do + it_behaves_like 'access allowed when terms accepted', :access_api + end + + context 'admin' do + let(:current_user) { create(:admin) } + + it_behaves_like 'access allowed when terms accepted', :access_api + end + + context 'anonymous' do + let(:current_user) { nil } + + it { is_expected.to be_allowed(:access_api) } + end + end + end + + describe 'git access' do + describe 'regular user' do + it { is_expected.to be_allowed(:access_git) } + end + + describe 'admin' do + let(:current_user) { create(:admin) } + + it { is_expected.to be_allowed(:access_git) } + end + + describe 'anonymous' do + let(:current_user) { nil } + + it { is_expected.to be_allowed(:access_git) } + end + + context 'when terms are enforced' do + before do + enforce_terms + end + + context 'regular user' do + it_behaves_like 'access allowed when terms accepted', :access_git + end + + context 'admin' do + let(:current_user) { create(:admin) } + + it_behaves_like 'access allowed when terms accepted', :access_git + end + + context 'anonymous' do + let(:current_user) { nil } + + it { is_expected.to be_allowed(:access_git) } + end + end + end end diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb index 8b9c4ac0b4b..6609f5f7afd 100644 --- a/spec/policies/project_policy_spec.rb +++ b/spec/policies/project_policy_spec.rb @@ -404,7 +404,7 @@ describe ProjectPolicy do ) end let(:maintainer_abilities) do - %w(create_build update_build create_pipeline update_pipeline) + %w(create_build create_pipeline) end subject { described_class.new(user, project) } diff --git a/spec/requests/api/helpers_spec.rb b/spec/requests/api/helpers_spec.rb index 837389451e8..d3ab44c0d7e 100644 --- a/spec/requests/api/helpers_spec.rb +++ b/spec/requests/api/helpers_spec.rb @@ -6,6 +6,7 @@ describe API::Helpers do include API::APIGuard::HelperMethods include described_class include SentryHelper + include TermsHelper let(:user) { create(:user) } let(:admin) { create(:admin) } @@ -163,6 +164,23 @@ describe API::Helpers do expect { current_user }.to raise_error /403/ end + context 'when terms are enforced' do + before do + enforce_terms + env[Gitlab::Auth::UserAuthFinders::PRIVATE_TOKEN_HEADER] = personal_access_token.token + end + + it 'returns a 403 when a user has not accepted the terms' do + expect { current_user }.to raise_error /You must accept the Terms of Service/ + end + + it 'sets the current user when the user accepted the terms' do + accept_terms(user) + + expect(current_user).to eq(user) + end + end + it "sets current_user" do env[Gitlab::Auth::UserAuthFinders::PRIVATE_TOKEN_HEADER] = personal_access_token.token expect(current_user).to eq(user) diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb index 494db30e8e0..2514dab1714 100644 --- a/spec/requests/git_http_spec.rb +++ b/spec/requests/git_http_spec.rb @@ -1,6 +1,7 @@ require "spec_helper" describe 'Git HTTP requests' do + include TermsHelper include GitHttpHelpers include WorkhorseHelpers include UserActivitiesHelpers @@ -824,4 +825,56 @@ describe 'Git HTTP requests' do end end end + + context 'when terms are enforced' do + let(:project) { create(:project, :repository) } + let(:user) { create(:user) } + let(:path) { "#{project.full_path}.git" } + let(:env) { { user: user.username, password: user.password } } + + before do + project.add_master(user) + enforce_terms + end + + it 'blocks git access when the user did not accept terms', :aggregate_failures do + clone_get(path, env) do |response| + expect(response).to have_gitlab_http_status(:forbidden) + end + + download(path, env) do |response| + expect(response).to have_gitlab_http_status(:forbidden) + end + + upload(path, env) do |response| + expect(response).to have_gitlab_http_status(:forbidden) + end + end + + context 'when the user accepted the terms' do + before do + accept_terms(user) + end + + it 'allows clones' do + clone_get(path, env) do |response| + expect(response).to have_gitlab_http_status(:ok) + end + end + + it_behaves_like 'pulls are allowed' + it_behaves_like 'pushes are allowed' + end + + context 'from CI' do + let(:build) { create(:ci_build, :running) } + let(:env) { { user: 'gitlab-ci-token', password: build.token } } + + before do + build.update!(user: user, project: project) + end + + it_behaves_like 'pulls are allowed' + end + end end diff --git a/spec/serializers/pipeline_serializer_spec.rb b/spec/serializers/pipeline_serializer_spec.rb index e88e86c2998..b741308e2c5 100644 --- a/spec/serializers/pipeline_serializer_spec.rb +++ b/spec/serializers/pipeline_serializer_spec.rb @@ -114,7 +114,9 @@ describe PipelineSerializer do Gitlab::GitalyClient.reset_counts end - shared_examples 'no N+1 queries' do + context 'with the same ref' do + let(:ref) { 'feature' } + it 'verifies number of queries', :request_store do recorded = ActiveRecord::QueryRecorder.new { subject } @@ -123,12 +125,6 @@ describe PipelineSerializer do end end - context 'with the same ref' do - let(:ref) { 'feature' } - - it_behaves_like 'no N+1 queries' - end - context 'with different refs' do def ref @sequence ||= 0 @@ -136,7 +132,16 @@ describe PipelineSerializer do "feature-#{@sequence}" end - it_behaves_like 'no N+1 queries' + it 'verifies number of queries', :request_store do + recorded = ActiveRecord::QueryRecorder.new { subject } + + # For each ref there is a permission check if maintainer can update + # pipeline. With the same ref this check is cached but if refs are + # different then there is an extra query per ref + # https://gitlab.com/gitlab-org/gitlab-ce/issues/46368 + expect(recorded.count).to be_within(1).of(51) + expect(recorded.cached_count).to eq(0) + end end def create_pipeline(status) diff --git a/spec/services/ci/retry_pipeline_service_spec.rb b/spec/services/ci/retry_pipeline_service_spec.rb index f1acfc48468..a73bd7a0268 100644 --- a/spec/services/ci/retry_pipeline_service_spec.rb +++ b/spec/services/ci/retry_pipeline_service_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe Ci::RetryPipelineService, '#execute' do + include ProjectForksHelper + let(:user) { create(:user) } let(:project) { create(:project) } let(:pipeline) { create(:ci_pipeline, project: project) } @@ -266,6 +268,33 @@ describe Ci::RetryPipelineService, '#execute' do end end + context 'when maintainer is allowed to push to forked project' do + let(:user) { create(:user) } + let(:project) { create(:project, :public) } + let(:forked_project) { fork_project(project) } + let(:pipeline) { create(:ci_pipeline, project: forked_project, ref: 'fixes') } + + before do + project.add_master(user) + create(:merge_request, + source_project: forked_project, + target_project: project, + source_branch: 'fixes', + allow_maintainer_to_push: true) + create_build('rspec 1', :failed, 1) + end + + it 'allows to retry failed pipeline' do + allow_any_instance_of(Project).to receive(:fetch_branch_allows_maintainer_push?).and_return(true) + allow_any_instance_of(Project).to receive(:empty_repo?).and_return(false) + + service.execute(pipeline) + + expect(build('rspec 1')).to be_pending + expect(pipeline.reload).to be_running + end + end + def statuses pipeline.reload.statuses end diff --git a/spec/services/clusters/create_service_spec.rb b/spec/services/clusters/create_service_spec.rb index 1c2f9c5cf43..1685dc748bd 100644 --- a/spec/services/clusters/create_service_spec.rb +++ b/spec/services/clusters/create_service_spec.rb @@ -8,80 +8,22 @@ describe Clusters::CreateService do subject { described_class.new(project, user, params).execute(access_token) } context 'when provider is gcp' do - shared_context 'valid params' do - let(:params) do - { - name: 'test-cluster', - provider_type: :gcp, - provider_gcp_attributes: { - gcp_project_id: 'gcp-project', - zone: 'us-central1-a', - num_nodes: 1, - machine_type: 'machine_type-a' - } - } - end - end - - shared_context 'invalid params' do - let(:params) do - { - name: 'test-cluster', - provider_type: :gcp, - provider_gcp_attributes: { - gcp_project_id: '!!!!!!!', - zone: 'us-central1-a', - num_nodes: 1, - machine_type: 'machine_type-a' - } - } - end - end - - shared_examples 'create cluster' do - it 'creates a cluster object and performs a worker' do - expect(ClusterProvisionWorker).to receive(:perform_async) - - expect { subject } - .to change { Clusters::Cluster.count }.by(1) - .and change { Clusters::Providers::Gcp.count }.by(1) - - expect(subject.name).to eq('test-cluster') - expect(subject.user).to eq(user) - expect(subject.project).to eq(project) - expect(subject.provider.gcp_project_id).to eq('gcp-project') - expect(subject.provider.zone).to eq('us-central1-a') - expect(subject.provider.num_nodes).to eq(1) - expect(subject.provider.machine_type).to eq('machine_type-a') - expect(subject.provider.access_token).to eq(access_token) - expect(subject.platform).to be_nil - end - end - - shared_examples 'error' do - it 'returns an error' do - expect(ClusterProvisionWorker).not_to receive(:perform_async) - expect { subject }.to change { Clusters::Cluster.count }.by(0) - expect(subject.errors[:"provider_gcp.gcp_project_id"]).to be_present - end - end - context 'when project has no clusters' do context 'when correct params' do - include_context 'valid params' + include_context 'valid cluster create params' - include_examples 'create cluster' + include_examples 'create cluster service success' end context 'when invalid params' do - include_context 'invalid params' + include_context 'invalid cluster create params' - include_examples 'error' + include_examples 'create cluster service error' end end context 'when project has a cluster' do - include_context 'valid params' + include_context 'valid cluster create params' let!(:cluster) { create(:cluster, :provided_by_gcp, :production_environment, projects: [project]) } it 'does not create a cluster' do diff --git a/spec/support/helpers/test_env.rb b/spec/support/helpers/test_env.rb index 1dad39fdab3..57aa07cf4fa 100644 --- a/spec/support/helpers/test_env.rb +++ b/spec/support/helpers/test_env.rb @@ -159,7 +159,11 @@ module TestEnv end spawn_script = Rails.root.join('scripts/gitaly-test-spawn').to_s - @gitaly_pid = Bundler.with_original_env { IO.popen([spawn_script], &:read).to_i } + Bundler.with_original_env do + raise "gitaly spawn failed" unless system(spawn_script) + end + @gitaly_pid = Integer(File.read('tmp/tests/gitaly.pid')) + Kernel.at_exit { stop_gitaly } wait_gitaly diff --git a/spec/support/services/clusters/create_service_shared.rb b/spec/support/services/clusters/create_service_shared.rb new file mode 100644 index 00000000000..43a2fd05498 --- /dev/null +++ b/spec/support/services/clusters/create_service_shared.rb @@ -0,0 +1,57 @@ +shared_context 'valid cluster create params' do + let(:params) do + { + name: 'test-cluster', + provider_type: :gcp, + provider_gcp_attributes: { + gcp_project_id: 'gcp-project', + zone: 'us-central1-a', + num_nodes: 1, + machine_type: 'machine_type-a' + } + } + end +end + +shared_context 'invalid cluster create params' do + let(:params) do + { + name: 'test-cluster', + provider_type: :gcp, + provider_gcp_attributes: { + gcp_project_id: '!!!!!!!', + zone: 'us-central1-a', + num_nodes: 1, + machine_type: 'machine_type-a' + } + } + end +end + +shared_examples 'create cluster service success' do + it 'creates a cluster object and performs a worker' do + expect(ClusterProvisionWorker).to receive(:perform_async) + + expect { subject } + .to change { Clusters::Cluster.count }.by(1) + .and change { Clusters::Providers::Gcp.count }.by(1) + + expect(subject.name).to eq('test-cluster') + expect(subject.user).to eq(user) + expect(subject.project).to eq(project) + expect(subject.provider.gcp_project_id).to eq('gcp-project') + expect(subject.provider.zone).to eq('us-central1-a') + expect(subject.provider.num_nodes).to eq(1) + expect(subject.provider.machine_type).to eq('machine_type-a') + expect(subject.provider.access_token).to eq(access_token) + expect(subject.platform).to be_nil + end +end + +shared_examples 'create cluster service error' do + it 'returns an error' do + expect(ClusterProvisionWorker).not_to receive(:perform_async) + expect { subject }.to change { Clusters::Cluster.count }.by(0) + expect(subject.errors[:"provider_gcp.gcp_project_id"]).to be_present + end +end diff --git a/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml b/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml index 020031af3cb..a00c6e89a1d 100644 --- a/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml +++ b/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml @@ -12,8 +12,10 @@ # AUTO_DEVOPS_DOMAIN must also be set as a variable at the group or project # level, or manually added below. # -# If you want to deploy to staging first, or enable canary deploys, -# uncomment the relevant jobs in the pipeline below. +# Continuous deployment to production is enabled by default. +# If you want to deploy to staging first, or enable incremental rollouts, +# set STAGING_ENABLED or INCREMENTAL_ROLLOUT_ENABLED environment variables. +# If you want to use canary deployments, uncomment the canary job. # # If Auto DevOps fails to detect the proper buildpack, or if you want to # specify a custom buildpack, set a project variable `BUILDPACK_URL` to the @@ -88,14 +90,6 @@ codequality: artifacts: paths: [codeclimate.json] -license_management: - image: registry.gitlab.com/gitlab-org/security-products/license-management:latest - allow_failure: true - script: - - license_management - artifacts: - paths: [gl-license-report.json] - performance: stage: performance image: docker:stable @@ -223,8 +217,8 @@ stop_review: # Staging deploys are disabled by default since # continuous deployment to production is enabled by default # If you prefer to automatically deploy to staging and -# only manually promote to production, enable this job by removing the dot (.), -# and uncomment the `when: manual` line in the `production` job. +# only manually promote to production, enable this job by setting +# STAGING_ENABLED. staging: stage: staging @@ -245,13 +239,9 @@ staging: kubernetes: active variables: - $STAGING_ENABLED - except: - variables: - - $INCREMENTAL_ROLLOUT_ENABLED # Canaries are disabled by default, but if you want them, -# and know what the downsides are, enable this job by removing the dot (.), -# and uncomment the `when: manual` line in the `production` job. +# and know what the downsides are, enable this job by removing the dot (.). .canary: stage: canary @@ -272,11 +262,6 @@ staging: - master kubernetes: active -# This job continuously deploys to production on every push to `master`. -# To make this a manual process, either because you're enabling `staging` -# or `canary` deploys, or you simply want more control over when you deploy -# to production, uncomment the `when: manual` line in the `production` job. - .production: &production_template stage: production script: @@ -310,6 +295,7 @@ production: production_manual: <<: *production_template when: manual + allow_failure: false only: refs: - master @@ -345,6 +331,7 @@ rollout 10%: <<: *rollout_template variables: ROLLOUT_PERCENTAGE: 10 + when: manual only: refs: - master @@ -379,6 +366,7 @@ rollout 50%: rollout 100%: <<: *production_template when: manual + allow_failure: false only: refs: - master @@ -428,14 +416,6 @@ rollout 100%: "registry.gitlab.com/gitlab-org/security-products/codequality:$SP_VERSION" /code } - function license_management() { - if echo $GITLAB_FEATURES |grep license_management > /dev/null ; then - /run.sh . - else - echo "License management is not available in your subscription" - fi - } - function sast() { case "$CI_SERVER_VERSION" in *-ee) @@ -562,12 +542,14 @@ rollout 100%: replicas=$(get_replicas "$track" "$percentage") - helm upgrade --reuse-values \ - --wait \ - --set replicaCount="$replicas" \ - --namespace="$KUBE_NAMESPACE" \ - "$name" \ - chart/ + if [[ -n "$(helm ls -q "^$name$")" ]]; then + helm upgrade --reuse-values \ + --wait \ + --set replicaCount="$replicas" \ + --namespace="$KUBE_NAMESPACE" \ + "$name" \ + chart/ + fi } function install_dependencies() { diff --git a/vendor/ingress/values.yaml b/vendor/ingress/values.yaml index a6b499953bf..d0c1673cefc 100644 --- a/vendor/ingress/values.yaml +++ b/vendor/ingress/values.yaml @@ -7,3 +7,8 @@ controller: podAnnotations: prometheus.io/scrape: "true" prometheus.io/port: "10254" + +rbac: + create: false + createRole: false + createClusterRole: false diff --git a/yarn.lock b/yarn.lock index b48a16c6ff1..f34bc81067d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -54,9 +54,9 @@ lodash "^4.2.0" to-fast-properties "^2.0.0" -"@gitlab-org/gitlab-svgs@^1.20.0": - version "1.20.0" - resolved "https://registry.yarnpkg.com/@gitlab-org/gitlab-svgs/-/gitlab-svgs-1.20.0.tgz#4c3fa3a91e0693114654b0066fb1ef04c0602047" +"@gitlab-org/gitlab-svgs@^1.22.0": + version "1.22.0" + resolved "https://registry.yarnpkg.com/@gitlab-org/gitlab-svgs/-/gitlab-svgs-1.22.0.tgz#9f2daefebcda911cba8341313c8c464c8087fe44" "@mrmlnc/readdir-enhanced@^2.2.1": version "2.2.1" |