diff options
52 files changed, 510 insertions, 609 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index f506b1b375e..24627b5faca 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -333,6 +333,7 @@ migration paths: - bundle install --without postgres production --jobs $(nproc) $FLAGS --retry=3 - bundle exec rake db:drop db:create db:schema:load db:seed_fu - git checkout $CI_COMMIT_SHA + - bundle install --without postgres production --jobs $(nproc) $FLAGS --retry=3 - source scripts/prepare_build.sh - bundle exec rake db:migrate @@ -327,6 +327,7 @@ group :test do gem 'test_after_commit', '~> 1.1' gem 'sham_rack', '~> 1.3.6' gem 'timecop', '~> 0.8.0' + gem 'concurrent-ruby', '~> 1.0.5' end gem 'octokit', '~> 4.6.2' diff --git a/Gemfile.lock b/Gemfile.lock index 043ca4f8800..07be5d7aded 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -128,7 +128,7 @@ GEM execjs coffee-script-source (1.10.0) colorize (0.7.7) - concurrent-ruby (1.0.4) + concurrent-ruby (1.0.5) connection_pool (2.2.1) crack (0.4.3) safe_yaml (~> 1.0.0) @@ -868,6 +868,7 @@ DEPENDENCIES chronic (~> 0.10.2) chronic_duration (~> 0.10.6) coffee-rails (~> 4.1.0) + concurrent-ruby (~> 1.0.5) connection_pool (~> 2.0) creole (~> 0.5.0) d3_rails (~> 3.5.0) diff --git a/app/assets/javascripts/activities.js b/app/assets/javascripts/activities.js index aebda7780e1..d816df831eb 100644 --- a/app/assets/javascripts/activities.js +++ b/app/assets/javascripts/activities.js @@ -1,6 +1,7 @@ /* eslint-disable no-param-reassign, class-methods-use-this */ /* global Pager */ -/* global Cookies */ + +import Cookies from 'js-cookie'; class Activities { constructor() { diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js index 9349918f7a0..c743dd551d7 100644 --- a/app/assets/javascripts/awards_handler.js +++ b/app/assets/javascripts/awards_handler.js @@ -1,4 +1,4 @@ -/* global Cookies */ +import Cookies from 'js-cookie'; import emojiMap from 'emojis/digests.json'; import emojiAliases from 'emojis/aliases.json'; diff --git a/app/assets/javascripts/boards/components/board_blank_state.js b/app/assets/javascripts/boards/components/board_blank_state.js index 52893d4642b..3fc68457961 100644 --- a/app/assets/javascripts/boards/components/board_blank_state.js +++ b/app/assets/javascripts/boards/components/board_blank_state.js @@ -1,5 +1,7 @@ /* global ListLabel */ -/* global Cookies */ + +import Cookies from 'js-cookie'; + const Store = gl.issueBoards.BoardsStore; export default { diff --git a/app/assets/javascripts/boards/stores/boards_store.js b/app/assets/javascripts/boards/stores/boards_store.js index 28ecb322df7..8912f234aa6 100644 --- a/app/assets/javascripts/boards/stores/boards_store.js +++ b/app/assets/javascripts/boards/stores/boards_store.js @@ -1,7 +1,8 @@ /* eslint-disable comma-dangle, space-before-function-paren, one-var, no-shadow, dot-notation, max-len */ -/* global Cookies */ /* global List */ +import Cookies from 'js-cookie'; + (() => { window.gl = window.gl || {}; window.gl.issueBoards = window.gl.issueBoards || {}; diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js index 18b682fd5cb..ae17d05e679 100644 --- a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js +++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js @@ -1,9 +1,8 @@ -/* global Cookies */ /* global Flash */ import Vue from 'vue'; +import Cookies from 'js-cookie'; -window.Cookies = require('js-cookie'); require('./components/stage_code_component'); require('./components/stage_issue_component'); require('./components/stage_plan_component'); diff --git a/app/assets/javascripts/diff_notes/components/diff_note_avatars.js b/app/assets/javascripts/diff_notes/components/diff_note_avatars.js index dd7081aefb7..0297add94d5 100644 --- a/app/assets/javascripts/diff_notes/components/diff_note_avatars.js +++ b/app/assets/javascripts/diff_notes/components/diff_note_avatars.js @@ -1,4 +1,6 @@ -/* global CommentsStore Cookies notes */ +/* global CommentsStore */ +/* global notes */ + import Vue from 'vue'; import collapseIcon from '../icons/collapse_icon.svg'; diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index 3557f6f617e..d1a662459e1 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -41,9 +41,9 @@ import GroupsList from './groups_list'; import ProjectsList from './projects_list'; import MiniPipelineGraph from './mini_pipeline_graph_dropdown'; import BlobLinePermalinkUpdater from './blob/blob_line_permalink_updater'; +import UserCallout from './user_callout'; const ShortcutsBlob = require('./shortcuts_blob'); -const UserCallout = require('./user_callout'); (function() { var Dispatcher; diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js index c6bb7fda8f2..22352950452 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js @@ -384,7 +384,7 @@ import FilteredSearchContainer from './container'; paths.push(`search=${sanitized}`); } - const parameterizedUrl = `?scope=all&utf8=✓&${paths.join('&')}`; + const parameterizedUrl = `?scope=all&utf8=%E2%9C%93&${paths.join('&')}`; if (this.updateObject) { this.updateObject(parameterizedUrl); diff --git a/app/assets/javascripts/issuable_context.js b/app/assets/javascripts/issuable_context.js index 115312d4b83..834b98e8601 100644 --- a/app/assets/javascripts/issuable_context.js +++ b/app/assets/javascripts/issuable_context.js @@ -1,8 +1,9 @@ /* eslint-disable func-names, space-before-function-paren, wrap-iife, no-new, comma-dangle, quotes, prefer-arrow-callback, consistent-return, one-var, no-var, one-var-declaration-per-line, no-underscore-dangle, max-len */ /* global UsersSelect */ -/* global Cookies */ /* global bp */ +import Cookies from 'js-cookie'; + (function() { this.IssuableContext = (function() { function IssuableContext(currentUser) { diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index 81d5748191d..b1ca0dc091d 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -1,6 +1,5 @@ /* eslint-disable func-names, space-before-function-paren, no-var, quotes, consistent-return, prefer-arrow-callback, comma-dangle, object-shorthand, no-new, max-len, no-multi-spaces, import/newline-after-import, import/first */ /* global bp */ -/* global Cookies */ /* global Flash */ /* global ConfirmDangerModal */ /* global Aside */ @@ -24,7 +23,6 @@ import './extensions/array'; window.jQuery = jQuery; window.$ = jQuery; window._ = _; -window.Cookies = Cookies; window.Pikaday = Pikaday; window.Dropzone = Dropzone; window.Sortable = Sortable; diff --git a/app/assets/javascripts/merge_conflicts/merge_conflict_store.js b/app/assets/javascripts/merge_conflicts/merge_conflict_store.js index 07632d6225c..c4e379a4a0b 100644 --- a/app/assets/javascripts/merge_conflicts/merge_conflict_store.js +++ b/app/assets/javascripts/merge_conflicts/merge_conflict_store.js @@ -1,7 +1,7 @@ /* eslint-disable comma-dangle, object-shorthand, no-param-reassign, camelcase, no-nested-ternary, no-continue, max-len */ -/* global Cookies */ import Vue from 'vue'; +import Cookies from 'js-cookie'; ((global) => { global.mergeConflicts = global.mergeConflicts || {}; diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js index 190336dbd20..d9692269c38 100644 --- a/app/assets/javascripts/merge_request_tabs.js +++ b/app/assets/javascripts/merge_request_tabs.js @@ -1,10 +1,10 @@ /* eslint-disable no-new, class-methods-use-this */ /* global Breakpoints */ -/* global Cookies */ /* global Flash */ +import Cookies from 'js-cookie'; + require('./breakpoints'); -window.Cookies = require('js-cookie'); require('./flash'); /* eslint-disable max-len */ diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js index 47cc34e7a20..1d563c63f39 100644 --- a/app/assets/javascripts/notes.js +++ b/app/assets/javascripts/notes.js @@ -1,14 +1,14 @@ /* eslint-disable no-restricted-properties, func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-use-before-define, camelcase, no-unused-expressions, quotes, max-len, one-var, one-var-declaration-per-line, default-case, prefer-template, consistent-return, no-alert, no-return-assign, no-param-reassign, prefer-arrow-callback, no-else-return, comma-dangle, no-new, brace-style, no-lonely-if, vars-on-top, no-unused-vars, no-sequences, no-shadow, newline-per-chained-call, no-useless-escape */ /* global Flash */ /* global Autosave */ -/* global Cookies */ /* global ResolveService */ /* global mrRefreshWidgetUrl */ +import Cookies from 'js-cookie'; + require('./autosave'); window.autosize = require('vendor/autosize'); window.Dropzone = require('dropzone'); -window.Cookies = require('js-cookie'); require('./dropzone_input'); require('./gfm_auto_complete'); require('vendor/jquery.caret'); // required by jquery.atwho diff --git a/app/assets/javascripts/project.js b/app/assets/javascripts/project.js index db7ceaa2421..f944fcc5a58 100644 --- a/app/assets/javascripts/project.js +++ b/app/assets/javascripts/project.js @@ -1,7 +1,8 @@ /* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, quotes, consistent-return, no-new, prefer-arrow-callback, no-return-assign, one-var, one-var-declaration-per-line, object-shorthand, comma-dangle, no-else-return, newline-per-chained-call, no-shadow, vars-on-top, prefer-template, max-len */ -/* global Cookies */ /* global ProjectSelect */ +import Cookies from 'js-cookie'; + (function() { this.Project = (function() { function Project() { diff --git a/app/assets/javascripts/right_sidebar.js b/app/assets/javascripts/right_sidebar.js index 903862cac6b..7298a7d5347 100644 --- a/app/assets/javascripts/right_sidebar.js +++ b/app/assets/javascripts/right_sidebar.js @@ -1,5 +1,6 @@ /* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-unused-vars, consistent-return, one-var, one-var-declaration-per-line, quotes, prefer-template, object-shorthand, comma-dangle, no-else-return, no-param-reassign, max-len */ -/* global Cookies */ + +import Cookies from 'js-cookie'; (function() { var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; }; diff --git a/app/assets/javascripts/user.js b/app/assets/javascripts/user.js index 059e6c628b3..19c9efe7fbd 100644 --- a/app/assets/javascripts/user.js +++ b/app/assets/javascripts/user.js @@ -1,5 +1,6 @@ /* eslint-disable class-methods-use-this, comma-dangle, arrow-parens, no-param-reassign */ -/* global Cookies */ + +import Cookies from 'js-cookie'; ((global) => { global.User = class { diff --git a/app/assets/javascripts/user_callout.js b/app/assets/javascripts/user_callout.js index 99419e85b20..b27d252a3ef 100644 --- a/app/assets/javascripts/user_callout.js +++ b/app/assets/javascripts/user_callout.js @@ -1,4 +1,4 @@ -/* global Cookies */ +import Cookies from 'js-cookie'; const userCalloutElementName = '.user-callout'; const closeButton = '.close-user-callout'; @@ -27,7 +27,7 @@ const USER_CALLOUT_TEMPLATE = ` </div> </div>`; -class UserCallout { +export default class UserCallout { constructor() { this.isCalloutDismissed = Cookies.get(USER_CALLOUT_COOKIE); this.userCalloutBody = $(userCalloutElementName); @@ -56,5 +56,3 @@ class UserCallout { } } } - -module.exports = UserCallout; diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index b7ce081a5cd..6a6e335d314 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -64,8 +64,11 @@ class ApplicationController < ActionController::Base # This filter handles both private tokens and personal access tokens def authenticate_user_from_private_token! - token_string = params[:private_token].presence || request.headers['PRIVATE-TOKEN'].presence - user = User.find_by_authentication_token(token_string) || User.find_by_personal_access_token(token_string) + token = params[:private_token].presence || request.headers['PRIVATE-TOKEN'].presence + + return unless token.present? + + user = User.find_by_authentication_token(token) || User.find_by_personal_access_token(token) if user && can?(user, :log_in) # Notice we are passing store false, so the user is not diff --git a/app/controllers/projects/deploy_keys_controller.rb b/app/controllers/projects/deploy_keys_controller.rb index 1502b734f37..d0c44e297e3 100644 --- a/app/controllers/projects/deploy_keys_controller.rb +++ b/app/controllers/projects/deploy_keys_controller.rb @@ -31,8 +31,10 @@ class Projects::DeployKeysController < Projects::ApplicationController end def disable - @project.deploy_keys_projects.find_by(deploy_key_id: params[:id]).destroy + deploy_key_project = @project.deploy_keys_projects.find_by(deploy_key_id: params[:id]) + return render_404 unless deploy_key_project + deploy_key_project.destroy! redirect_to_repository_settings(@project) end diff --git a/app/models/project_services/chat_notification_service.rb b/app/models/project_services/chat_notification_service.rb index 200be99f36b..75834103db5 100644 --- a/app/models/project_services/chat_notification_service.rb +++ b/app/models/project_services/chat_notification_service.rb @@ -6,7 +6,7 @@ class ChatNotificationService < Service default_value_for :category, 'chat' prop_accessor :webhook, :username, :channel - boolean_accessor :notify_only_broken_pipelines + boolean_accessor :notify_only_broken_pipelines, :notify_only_default_branch validates :webhook, presence: true, url: true, if: :activated? @@ -17,6 +17,7 @@ class ChatNotificationService < Service if properties.nil? self.properties = {} self.notify_only_broken_pipelines = true + self.notify_only_default_branch = true end end @@ -29,6 +30,19 @@ class ChatNotificationService < Service pipeline wiki_page] end + def fields + default_fields + build_event_channels + end + + def default_fields + [ + { type: 'text', name: 'webhook', placeholder: "e.g. #{webhook_placeholder}" }, + { type: 'text', name: 'username', placeholder: 'e.g. GitLab' }, + { type: 'checkbox', name: 'notify_only_broken_pipelines' }, + { type: 'checkbox', name: 'notify_only_default_branch' }, + ] + end + def execute(data) return unless supported_events.include?(data[:object_kind]) return unless webhook.present? @@ -123,6 +137,17 @@ class ChatNotificationService < Service end def should_pipeline_be_notified?(data) + notify_for_ref?(data) && notify_for_pipeline?(data) + end + + def notify_for_ref?(data) + return true if data[:object_attributes][:tag] + return true unless notify_only_default_branch + + data[:object_attributes][:ref] == project.default_branch + end + + def notify_for_pipeline?(data) case data[:object_attributes][:status] when 'success' !notify_only_broken_pipelines? diff --git a/app/models/project_services/mattermost_service.rb b/app/models/project_services/mattermost_service.rb index 1156d050622..0362ed172c7 100644 --- a/app/models/project_services/mattermost_service.rb +++ b/app/models/project_services/mattermost_service.rb @@ -22,19 +22,11 @@ class MattermostService < ChatNotificationService </ol>' end - def fields - default_fields + build_event_channels - end - - def default_fields - [ - { type: 'text', name: 'webhook', placeholder: 'e.g. http://mattermost_host/hooks/…' }, - { type: 'text', name: 'username', placeholder: 'e.g. GitLab' }, - { type: 'checkbox', name: 'notify_only_broken_pipelines' }, - ] - end - def default_channel_placeholder "Channel handle (e.g. town-square)" end + + def webhook_placeholder + 'http://mattermost.example.com/hooks/…' + end end diff --git a/app/models/project_services/slack_service.rb b/app/models/project_services/slack_service.rb index b657db6f9ee..71da0af75f6 100644 --- a/app/models/project_services/slack_service.rb +++ b/app/models/project_services/slack_service.rb @@ -21,19 +21,11 @@ class SlackService < ChatNotificationService </ol>' end - def fields - default_fields + build_event_channels - end - - def default_fields - [ - { type: 'text', name: 'webhook', placeholder: 'e.g. https://hooks.slack.com/services/…' }, - { type: 'text', name: 'username', placeholder: 'e.g. GitLab' }, - { type: 'checkbox', name: 'notify_only_broken_pipelines' }, - ] - end - def default_channel_placeholder "Channel name (e.g. general)" end + + def webhook_placeholder + 'https://hooks.slack.com/services/…' + end end diff --git a/app/models/user.rb b/app/models/user.rb index 8c7ad5d5174..5d19d873f43 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -324,6 +324,8 @@ class User < ActiveRecord::Base end def find_by_personal_access_token(token_string) + return unless token_string + PersonalAccessTokensFinder.new(state: 'active').find_by(token: token_string)&.user end diff --git a/app/services/boards/issues/list_service.rb b/app/services/boards/issues/list_service.rb index 83f51947bd4..cb6d30396ec 100644 --- a/app/services/boards/issues/list_service.rb +++ b/app/services/boards/issues/list_service.rb @@ -3,7 +3,7 @@ module Boards class ListService < BaseService def execute issues = IssuesFinder.new(current_user, filter_params).execute - issues = without_board_labels(issues) unless movable_list? + issues = without_board_labels(issues) unless list issues = with_list_label(issues) if movable_list? issues.order_by_position_and_priority end diff --git a/app/views/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml index d31ced004a0..e31fa5fbe95 100644 --- a/app/views/dashboard/todos/index.html.haml +++ b/app/views/dashboard/todos/index.html.haml @@ -19,12 +19,13 @@ .nav-controls - if @todos.any?(&:pending?) - = link_to destroy_all_dashboard_todos_path(todos_filter_params), class: 'btn btn-loading js-todos-mark-all', method: :delete, data: { href: destroy_all_dashboard_todos_path(todos_filter_params) } do - Mark all as done - = icon('spinner spin') - = link_to bulk_restore_dashboard_todos_path, class: 'btn btn-loading js-todos-undo-all hidden', method: :patch , data: { href: bulk_restore_dashboard_todos_path(todos_filter_params) } do - Undo mark all as done - = icon('spinner spin') + .append-right-default + = link_to destroy_all_dashboard_todos_path(todos_filter_params), class: 'btn btn-loading js-todos-mark-all', method: :delete, data: { href: destroy_all_dashboard_todos_path(todos_filter_params) } do + Mark all as done + = icon('spinner spin') + = link_to bulk_restore_dashboard_todos_path, class: 'btn btn-loading js-todos-undo-all hidden', method: :patch , data: { href: bulk_restore_dashboard_todos_path(todos_filter_params) } do + Undo mark all as done + = icon('spinner spin') .todos-filters .row-content-block.second-block diff --git a/changelogs/unreleased/24215-closed-issues-board.yml b/changelogs/unreleased/24215-closed-issues-board.yml new file mode 100644 index 00000000000..678ec34b274 --- /dev/null +++ b/changelogs/unreleased/24215-closed-issues-board.yml @@ -0,0 +1,4 @@ +--- +title: Display all closed issues in “done” board list +merge_request: +author: diff --git a/changelogs/unreleased/29555-align-all-todo.yml b/changelogs/unreleased/29555-align-all-todo.yml new file mode 100644 index 00000000000..c1555a96a92 --- /dev/null +++ b/changelogs/unreleased/29555-align-all-todo.yml @@ -0,0 +1,4 @@ +--- +title: align Mark all as done with other Done buttons on Todos page +merge_request: +author: diff --git a/changelogs/unreleased/filter-bar-fix-ie.yml b/changelogs/unreleased/filter-bar-fix-ie.yml new file mode 100644 index 00000000000..f1fa7d9b177 --- /dev/null +++ b/changelogs/unreleased/filter-bar-fix-ie.yml @@ -0,0 +1,4 @@ +--- +title: Fixed filtered search not working in IE +merge_request: +author: diff --git a/changelogs/unreleased/zj-chat-notification-default-branch.yml b/changelogs/unreleased/zj-chat-notification-default-branch.yml new file mode 100644 index 00000000000..fa0052d5034 --- /dev/null +++ b/changelogs/unreleased/zj-chat-notification-default-branch.yml @@ -0,0 +1,4 @@ +--- +title: Only send chat notifications for the default branch +merge_request: +author: diff --git a/config/environments/test.rb b/config/environments/test.rb index fb25d3a8b14..a25c5016a3b 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -1,4 +1,7 @@ Rails.application.configure do + # Make sure the middleware is inserted first in middleware chain + config.middleware.insert_before('ActionDispatch::Static', 'Gitlab::Testing::RequestBlockerMiddleware') + # Settings specified here will take precedence over those in config/application.rb # The test environment is used exclusively to run your application's diff --git a/doc/administration/high_availability/nfs.md b/doc/administration/high_availability/nfs.md index 3893d837006..bf1aa6b9ac5 100644 --- a/doc/administration/high_availability/nfs.md +++ b/doc/administration/high_availability/nfs.md @@ -26,7 +26,7 @@ options: circumstances it could lead to data loss if a failure occurs before data has synced. -## Client mount options +## NFS Client mount options Below is an example of an NFS mount point we use on GitLab.com: diff --git a/lib/gitlab/etag_caching/store.rb b/lib/gitlab/etag_caching/store.rb index 9532e432f78..0039fc01c8f 100644 --- a/lib/gitlab/etag_caching/store.rb +++ b/lib/gitlab/etag_caching/store.rb @@ -1,7 +1,7 @@ module Gitlab module EtagCaching class Store - EXPIRY_TIME = 10.minutes + EXPIRY_TIME = 20.minutes REDIS_NAMESPACE = 'etag:'.freeze def get(key) diff --git a/lib/gitlab/testing/request_blocker_middleware.rb b/lib/gitlab/testing/request_blocker_middleware.rb new file mode 100644 index 00000000000..aa67fa08577 --- /dev/null +++ b/lib/gitlab/testing/request_blocker_middleware.rb @@ -0,0 +1,61 @@ +# rubocop:disable Style/ClassVars + +# This is inspired by http://www.salsify.com/blog/engineering/tearing-capybara-ajax-tests +# Rack middleware that keeps track of the number of active requests and can block new requests. +module Gitlab + module Testing + class RequestBlockerMiddleware + @@num_active_requests = Concurrent::AtomicFixnum.new(0) + @@block_requests = Concurrent::AtomicBoolean.new(false) + + # Returns the number of requests the server is currently processing. + def self.num_active_requests + @@num_active_requests.value + end + + # Prevents the server from accepting new requests. Any new requests will return an HTTP + # 503 status. + def self.block_requests! + @@block_requests.value = true + end + + # Allows the server to accept requests again. + def self.allow_requests! + @@block_requests.value = false + end + + def initialize(app) + @app = app + end + + def call(env) + increment_active_requests + if block_requests? + block_request(env) + else + @app.call(env) + end + ensure + decrement_active_requests + end + + private + + def block_requests? + @@block_requests.true? + end + + def block_request(env) + [503, {}, []] + end + + def increment_active_requests + @@num_active_requests.increment + end + + def decrement_active_requests + @@num_active_requests.decrement + end + end + end +end diff --git a/spec/features/admin/admin_settings_spec.rb b/spec/features/admin/admin_settings_spec.rb index 03daab12c8f..5099441dce2 100644 --- a/spec/features/admin/admin_settings_spec.rb +++ b/spec/features/admin/admin_settings_spec.rb @@ -34,6 +34,7 @@ feature 'Admin updates settings', feature: true do fill_in 'Username', with: 'test_user' fill_in 'service_push_channel', with: '#test_channel' page.check('Notify only broken pipelines') + page.check('Notify only default branch') check_all_events click_on 'Save' diff --git a/spec/features/participants_autocomplete_spec.rb b/spec/features/participants_autocomplete_spec.rb index c2545b0c259..decad589c23 100644 --- a/spec/features/participants_autocomplete_spec.rb +++ b/spec/features/participants_autocomplete_spec.rb @@ -1,101 +1,62 @@ require 'spec_helper' -feature 'Member autocomplete', feature: true do - include WaitForAjax - +feature 'Member autocomplete', :js do let(:project) { create(:project, :public) } let(:user) { create(:user) } - let(:participant) { create(:user) } let(:author) { create(:user) } + let(:note) { create(:note, noteable: noteable, project: noteable.project) } before do - allow_any_instance_of(Commit).to receive(:author).and_return(author) - login_as user + note # actually create the note + login_as(user) end - shared_examples "open suggestions" do - it 'displays suggestions' do - expect(page).to have_selector('.atwho-view', visible: true) - end - - it 'suggests author' do - page.within('.atwho-view', visible: true) do - expect(page).to have_content(author.username) + shared_examples "open suggestions when typing @" do + before do + page.within('.new-note') do + find('#note_note').send_keys('@') end end - it 'suggests participant' do + it 'suggests noteable author and note author' do page.within('.atwho-view', visible: true) do - expect(page).to have_content(participant.username) + expect(page).to have_content(author.username) + expect(page).to have_content(note.author.username) end end end - context 'adding a new note on a Issue', js: true do + context 'adding a new note on a Issue' do + let(:noteable) { create(:issue, author: author, project: project) } before do - issue = create(:issue, author: author, project: project) - create(:note, note: 'Ultralight Beam', noteable: issue, - project: project, author: participant) - visit_issue(project, issue) + visit namespace_project_issue_path(project.namespace, project, noteable) end - context 'when typing @' do - include_examples "open suggestions" - before do - open_member_suggestions - end - end + include_examples "open suggestions when typing @" end - context 'adding a new note on a Merge Request ', js: true do + context 'adding a new note on a Merge Request' do + let(:noteable) do + create(:merge_request, source_project: project, + target_project: project, author: author) + end before do - merge = create(:merge_request, source_project: project, target_project: project, author: author) - create(:note, note: 'Ultralight Beam', noteable: merge, - project: project, author: participant) - visit_merge_request(project, merge) + visit namespace_project_merge_request_path(project.namespace, project, noteable) end - context 'when typing @' do - include_examples "open suggestions" - before do - open_member_suggestions - end - end + include_examples "open suggestions when typing @" end - context 'adding a new note on a Commit ', js: true do - let(:commit) { project.commit } + context 'adding a new note on a Commit' do + let(:noteable) { project.commit } + let(:note) { create(:note_on_commit, project: project, commit_id: project.commit.id) } before do - allow(commit).to receive(:author).and_return(author) - create(:note_on_commit, author: participant, project: project, commit_id: project.repository.commit.id, note: 'No More Parties in LA') - visit_commit(project, commit) - end - - context 'when typing @' do - include_examples "open suggestions" - before do - open_member_suggestions - end - end - end + allow_any_instance_of(Commit).to receive(:author).and_return(author) - def open_member_suggestions - page.within('.new-note') do - find('#note_note').send_keys('@') + visit namespace_project_commit_path(project.namespace, project, noteable) end - wait_for_ajax - end - - def visit_issue(project, issue) - visit namespace_project_issue_path(project.namespace, project, issue) - end - - def visit_merge_request(project, merge) - visit namespace_project_merge_request_path(project.namespace, project, merge) - end - def visit_commit(project, commit) - visit namespace_project_commit_path(project.namespace, project, commit) + include_examples "open suggestions when typing @" end end diff --git a/spec/features/projects/deploy_keys_spec.rb b/spec/features/projects/deploy_keys_spec.rb new file mode 100644 index 00000000000..0b997f130ea --- /dev/null +++ b/spec/features/projects/deploy_keys_spec.rb @@ -0,0 +1,26 @@ +require 'spec_helper' + +describe 'Project deploy keys', feature: true do + let(:user) { create(:user) } + let(:project) { create(:project_empty_repo) } + + before do + project.team << [user, :master] + login_as(user) + end + + describe 'removing key' do + before do + create(:deploy_keys_project, project: project) + end + + it 'removes association between project and deploy key' do + visit namespace_project_settings_repository_path(project.namespace, project) + + page.within '.deploy-keys' do + expect { click_on 'Remove' } + .to change { project.deploy_keys.count }.by(-1) + end + end + end +end diff --git a/spec/javascripts/boards/boards_store_spec.js b/spec/javascripts/boards/boards_store_spec.js index a033ac04da6..e21f4ca2bc0 100644 --- a/spec/javascripts/boards/boards_store_spec.js +++ b/spec/javascripts/boards/boards_store_spec.js @@ -1,12 +1,12 @@ /* eslint-disable comma-dangle, one-var, no-unused-vars */ /* global BoardService */ /* global boardsMockInterceptor */ -/* global Cookies */ /* global listObj */ /* global listObjDuplicate */ /* global ListIssue */ import Vue from 'vue'; +import Cookies from 'js-cookie'; require('~/lib/utils/url_utility'); require('~/boards/models/issue'); diff --git a/spec/javascripts/filtered_search/filtered_search_manager_spec.js b/spec/javascripts/filtered_search/filtered_search_manager_spec.js index 113161c21c6..848c7656a8d 100644 --- a/spec/javascripts/filtered_search/filtered_search_manager_spec.js +++ b/spec/javascripts/filtered_search/filtered_search_manager_spec.js @@ -58,7 +58,7 @@ const FilteredSearchSpecHelper = require('../helpers/filtered_search_spec_helper }); describe('search', () => { - const defaultParams = '?scope=all&utf8=✓&state=opened'; + const defaultParams = '?scope=all&utf8=%E2%9C%93&state=opened'; it('should search with a single word', (done) => { input.value = 'searchTerm'; diff --git a/spec/javascripts/test_bundle.js b/spec/javascripts/test_bundle.js index 16df1ad4f28..15465588223 100644 --- a/spec/javascripts/test_bundle.js +++ b/spec/javascripts/test_bundle.js @@ -8,7 +8,6 @@ jasmine.getJSONFixtures().fixturesPath = 'base/spec/javascripts/fixtures'; require('~/commons/index.js'); window.$ = window.jQuery = require('jquery'); window._ = require('underscore'); -window.Cookies = require('js-cookie'); // stub expected globals window.gl = window.gl || {}; diff --git a/spec/javascripts/user_callout_spec.js b/spec/javascripts/user_callout_spec.js index 205e72af600..2398149d3ad 100644 --- a/spec/javascripts/user_callout_spec.js +++ b/spec/javascripts/user_callout_spec.js @@ -1,7 +1,7 @@ -const UserCallout = require('~/user_callout'); +import Cookies from 'js-cookie'; +import UserCallout from '~/user_callout'; const USER_CALLOUT_COOKIE = 'user_callout_dismissed'; -const Cookie = window.Cookies; describe('UserCallout', function () { const fixtureName = 'static/user_callout.html.raw'; @@ -9,7 +9,7 @@ describe('UserCallout', function () { beforeEach(() => { loadFixtures(fixtureName); - Cookie.remove(USER_CALLOUT_COOKIE); + Cookies.remove(USER_CALLOUT_COOKIE); this.userCallout = new UserCallout(); this.closeButton = $('.close-user-callout'); @@ -18,25 +18,25 @@ describe('UserCallout', function () { }); it('does not show when cookie is set not defined', () => { - expect(Cookie.get(USER_CALLOUT_COOKIE)).toBeUndefined(); + expect(Cookies.get(USER_CALLOUT_COOKIE)).toBeUndefined(); expect(this.userCalloutContainer.is(':visible')).toBe(true); }); it('shows when cookie is set to false', () => { - Cookie.set(USER_CALLOUT_COOKIE, 'false'); + Cookies.set(USER_CALLOUT_COOKIE, 'false'); - expect(Cookie.get(USER_CALLOUT_COOKIE)).toBeDefined(); + expect(Cookies.get(USER_CALLOUT_COOKIE)).toBeDefined(); expect(this.userCalloutContainer.is(':visible')).toBe(true); }); it('hides when user clicks on the dismiss-icon', () => { this.closeButton.click(); - expect(Cookie.get(USER_CALLOUT_COOKIE)).toBe('true'); + expect(Cookies.get(USER_CALLOUT_COOKIE)).toBe('true'); }); it('hides when user clicks on the "check it out" button', () => { this.userCalloutBtn.click(); - expect(Cookie.get(USER_CALLOUT_COOKIE)).toBe('true'); + expect(Cookies.get(USER_CALLOUT_COOKIE)).toBe('true'); }); }); @@ -46,7 +46,7 @@ describe('UserCallout when cookie is present', function () { beforeEach(() => { loadFixtures(fixtureName); - Cookie.set(USER_CALLOUT_COOKIE, 'true'); + Cookies.set(USER_CALLOUT_COOKIE, 'true'); this.userCallout = new UserCallout(); this.userCalloutContainer = $('.user-callout'); }); diff --git a/spec/mailers/emails/profile_spec.rb b/spec/mailers/emails/profile_spec.rb index e1877d5fde0..5ca936f28f0 100644 --- a/spec/mailers/emails/profile_spec.rb +++ b/spec/mailers/emails/profile_spec.rb @@ -5,6 +5,16 @@ describe Notify do include EmailSpec::Matchers include_context 'gitlab email notification' + shared_examples 'a new user email' do + it 'is sent to the new user with the correct subject and body' do + aggregate_failures do + is_expected.to deliver_to new_user_address + is_expected.to have_subject(/^Account was created for you$/i) + is_expected.to have_body_text(new_user_address) + end + end + end + describe 'profile notifications' do describe 'for new users, the email' do let(:example_site_path) { root_path } diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb index 6ee91576676..4b72eb2eaa3 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -24,14 +24,14 @@ describe Notify do let(:previous_assignee) { create(:user, name: 'Previous Assignee') } shared_examples 'an assignee email' do - it 'is sent as the author' do - sender = subject.header[:from].addrs[0] - expect(sender.display_name).to eq(current_user.name) - expect(sender.address).to eq(gitlab_sender) - end + it 'is sent to the assignee as the author' do + sender = subject.header[:from].addrs.first - it 'is sent to the assignee' do - is_expected.to deliver_to assignee.email + aggregate_failures do + expect(sender.display_name).to eq(current_user.name) + expect(sender.address).to eq(gitlab_sender) + expect(subject).to deliver_to(assignee.email) + end end end @@ -49,12 +49,11 @@ describe Notify do it_behaves_like 'it should show Gmail Actions View Issue link' it_behaves_like 'an unsubscribeable thread' - it 'has the correct subject' do - is_expected.to have_referable_subject(issue) - end - - it 'contains a link to the new issue' do - is_expected.to have_body_text namespace_project_issue_path(project.namespace, project, issue) + it 'has the correct subject and body' do + aggregate_failures do + is_expected.to have_referable_subject(issue) + is_expected.to have_body_text(namespace_project_issue_path(project.namespace, project, issue)) + end end context 'when enabled email_author_in_body' do @@ -63,7 +62,7 @@ describe Notify do end it 'contains a link to note author' do - is_expected.to have_html_escaped_body_text issue.author_name + is_expected.to have_html_escaped_body_text(issue.author_name) is_expected.to have_body_text 'wrote:' end end @@ -95,20 +94,13 @@ describe Notify do expect(sender.address).to eq(gitlab_sender) end - it 'has the correct subject' do - is_expected.to have_referable_subject(issue, reply: true) - end - - it 'contains the name of the previous assignee' do - is_expected.to have_html_escaped_body_text previous_assignee.name - end - - it 'contains the name of the new assignee' do - is_expected.to have_html_escaped_body_text assignee.name - end - - it 'contains a link to the issue' do - is_expected.to have_body_text namespace_project_issue_path(project.namespace, project, issue) + it 'has the correct subject and body' do + aggregate_failures do + is_expected.to have_referable_subject(issue, reply: true) + is_expected.to have_html_escaped_body_text(previous_assignee.name) + is_expected.to have_html_escaped_body_text(assignee.name) + is_expected.to have_body_text(namespace_project_issue_path(project.namespace, project, issue)) + end end end @@ -129,16 +121,12 @@ describe Notify do expect(sender.address).to eq(gitlab_sender) end - it 'has the correct subject' do - is_expected.to have_referable_subject(issue, reply: true) - end - - it 'contains the names of the added labels' do - is_expected.to have_body_text 'foo, bar, and baz' - end - - it 'contains a link to the issue' do - is_expected.to have_body_text namespace_project_issue_path(project.namespace, project, issue) + it 'has the correct subject and body' do + aggregate_failures do + is_expected.to have_referable_subject(issue, reply: true) + is_expected.to have_body_text('foo, bar, and baz') + is_expected.to have_body_text(namespace_project_issue_path(project.namespace, project, issue)) + end end end @@ -158,20 +146,13 @@ describe Notify do expect(sender.address).to eq(gitlab_sender) end - it 'has the correct subject' do - is_expected.to have_referable_subject(issue, reply: true) - end - - it 'contains the new status' do - is_expected.to have_body_text status - end - - it 'contains the user name' do - is_expected.to have_html_escaped_body_text current_user.name - end - - it 'contains a link to the issue' do - is_expected.to have_body_text(namespace_project_issue_path project.namespace, project, issue) + it 'has the correct subject and body' do + aggregate_failures do + is_expected.to have_referable_subject(issue, reply: true) + is_expected.to have_body_text(status) + is_expected.to have_html_escaped_body_text(current_user.name) + is_expected.to have_body_text(namespace_project_issue_path project.namespace, project, issue) + end end end @@ -189,18 +170,15 @@ describe Notify do is_expected.to have_body_text 'Issue was moved to another project' end - it 'has the correct subject' do - is_expected.to have_referable_subject(issue, reply: true) - end - - it 'contains link to new issue' do + it 'has the correct subject and body' do new_issue_url = namespace_project_issue_path(new_issue.project.namespace, new_issue.project, new_issue) - is_expected.to have_body_text new_issue_url - end - it 'contains a link to the original issue' do - is_expected.to have_body_text namespace_project_issue_path(project.namespace, project, issue) + aggregate_failures do + is_expected.to have_referable_subject(issue, reply: true) + is_expected.to have_body_text(new_issue_url) + is_expected.to have_body_text(namespace_project_issue_path(project.namespace, project, issue)) + end end end end @@ -220,20 +198,13 @@ describe Notify do it_behaves_like 'it should show Gmail Actions View Merge request link' it_behaves_like 'an unsubscribeable thread' - it 'has the correct subject' do - is_expected.to have_referable_subject(merge_request) - end - - it 'contains a link to the new merge request' do - is_expected.to have_body_text namespace_project_merge_request_path(project.namespace, project, merge_request) - end - - it 'contains the source branch for the merge request' do - is_expected.to have_body_text merge_request.source_branch - end - - it 'contains the target branch for the merge request' do - is_expected.to have_body_text merge_request.target_branch + it 'has the correct subject and body' do + aggregate_failures do + is_expected.to have_referable_subject(merge_request) + is_expected.to have_body_text(namespace_project_merge_request_path(project.namespace, project, merge_request)) + is_expected.to have_body_text(merge_request.source_branch) + is_expected.to have_body_text(merge_request.target_branch) + end end context 'when enabled email_author_in_body' do @@ -275,20 +246,13 @@ describe Notify do expect(sender.address).to eq(gitlab_sender) end - it 'has the correct subject' do - is_expected.to have_referable_subject(merge_request, reply: true) - end - - it 'contains the name of the previous assignee' do - is_expected.to have_html_escaped_body_text previous_assignee.name - end - - it 'contains the name of the new assignee' do - is_expected.to have_html_escaped_body_text assignee.name - end - - it 'contains a link to the merge request' do - is_expected.to have_body_text namespace_project_merge_request_path(project.namespace, project, merge_request) + it 'has the correct subject and body' do + aggregate_failures do + is_expected.to have_referable_subject(merge_request, reply: true) + is_expected.to have_html_escaped_body_text(previous_assignee.name) + is_expected.to have_body_text(namespace_project_merge_request_path(project.namespace, project, merge_request)) + is_expected.to have_html_escaped_body_text(assignee.name) + end end end @@ -309,16 +273,10 @@ describe Notify do expect(sender.address).to eq(gitlab_sender) end - it 'has the correct subject' do + it 'has the correct subject and body' do is_expected.to have_referable_subject(merge_request, reply: true) - end - - it 'contains the names of the added labels' do - is_expected.to have_body_text 'foo, bar, and baz' - end - - it 'contains a link to the merge request' do - is_expected.to have_body_text namespace_project_merge_request_path(project.namespace, project, merge_request) + is_expected.to have_body_text('foo, bar, and baz') + is_expected.to have_body_text(namespace_project_merge_request_path(project.namespace, project, merge_request)) end end @@ -338,20 +296,13 @@ describe Notify do expect(sender.address).to eq(gitlab_sender) end - it 'has the correct subject' do - is_expected.to have_referable_subject(merge_request, reply: true) - end - - it 'contains the new status' do - is_expected.to have_body_text status - end - - it 'contains the user name' do - is_expected.to have_html_escaped_body_text current_user.name - end - - it 'contains a link to the merge request' do - is_expected.to have_body_text namespace_project_merge_request_path(project.namespace, project, merge_request) + it 'has the correct subject and body' do + aggregate_failures do + is_expected.to have_referable_subject(merge_request, reply: true) + is_expected.to have_body_text(status) + is_expected.to have_html_escaped_body_text(current_user.name) + is_expected.to have_body_text(namespace_project_merge_request_path(project.namespace, project, merge_request)) + end end end @@ -371,16 +322,12 @@ describe Notify do expect(sender.address).to eq(gitlab_sender) end - it 'has the correct subject' do - is_expected.to have_referable_subject(merge_request, reply: true) - end - - it 'contains the new status' do - is_expected.to have_body_text 'merged' - end - - it 'contains a link to the merge request' do - is_expected.to have_body_text namespace_project_merge_request_path(project.namespace, project, merge_request) + it 'has the correct subject and body' do + aggregate_failures do + is_expected.to have_referable_subject(merge_request, reply: true) + is_expected.to have_body_text('merged') + is_expected.to have_body_text(namespace_project_merge_request_path(project.namespace, project, merge_request)) + end end end end @@ -395,16 +342,10 @@ describe Notify do it_behaves_like 'it should not have Gmail Actions links' it_behaves_like "a user cannot unsubscribe through footer link" - it 'has the correct subject' do - is_expected.to have_subject "#{project.name} | Project was moved" - end - - it 'contains name of project' do + it 'has the correct subject and body' do + is_expected.to have_subject("#{project.name} | Project was moved") is_expected.to have_html_escaped_body_text project.name_with_namespace - end - - it 'contains new user role' do - is_expected.to have_body_text project.ssh_url_to_repo + is_expected.to have_body_text(project.ssh_url_to_repo) end end @@ -597,14 +538,14 @@ describe Notify do shared_examples 'a note email' do it_behaves_like 'it should have Gmail Actions links' - it 'is sent as the author' do + it 'is sent to the given recipient as the author' do sender = subject.header[:from].addrs[0] - expect(sender.display_name).to eq(note_author.name) - expect(sender.address).to eq(gitlab_sender) - end - it 'is sent to the given recipient' do - is_expected.to deliver_to recipient.notification_email + aggregate_failures do + expect(sender.display_name).to eq(note_author.name) + expect(sender.address).to eq(gitlab_sender) + expect(subject).to deliver_to(recipient.notification_email) + end end it 'contains the message from the note' do @@ -641,12 +582,11 @@ describe Notify do it_behaves_like 'it should show Gmail Actions View Commit link' it_behaves_like 'a user cannot unsubscribe through footer link' - it 'has the correct subject' do - is_expected.to have_subject "Re: #{project.name} | #{commit.title.strip} (#{commit.short_id})" - end - - it 'contains a link to the commit' do - is_expected.to have_body_text commit.short_id + it 'has the correct subject and body' do + aggregate_failures do + is_expected.to have_subject("Re: #{project.name} | #{commit.title.strip} (#{commit.short_id})") + is_expected.to have_body_text(commit.short_id) + end end end @@ -664,12 +604,11 @@ describe Notify do it_behaves_like 'it should show Gmail Actions View Merge request link' it_behaves_like 'an unsubscribeable thread' - it 'has the correct subject' do - is_expected.to have_referable_subject(merge_request, reply: true) - end - - it 'contains a link to the merge request note' do - is_expected.to have_body_text note_on_merge_request_path + it 'has the correct subject and body' do + aggregate_failures do + is_expected.to have_referable_subject(merge_request, reply: true) + is_expected.to have_body_text note_on_merge_request_path + end end end @@ -687,12 +626,11 @@ describe Notify do it_behaves_like 'it should show Gmail Actions View Issue link' it_behaves_like 'an unsubscribeable thread' - it 'has the correct subject' do - is_expected.to have_referable_subject(issue, reply: true) - end - - it 'contains a link to the issue note' do - is_expected.to have_body_text note_on_issue_path + it 'has the correct subject and body' do + aggregate_failures do + is_expected.to have_referable_subject(issue, reply: true) + is_expected.to have_body_text(note_on_issue_path) + end end end end @@ -717,14 +655,14 @@ describe Notify do it_behaves_like 'it should have Gmail Actions links' - it 'is sent as the author' do + it 'is sent to the given recipient as the author' do sender = subject.header[:from].addrs[0] - expect(sender.display_name).to eq(note_author.name) - expect(sender.address).to eq(gitlab_sender) - end - it 'is sent to the given recipient' do - is_expected.to deliver_to recipient.notification_email + aggregate_failures do + expect(sender.display_name).to eq(note_author.name) + expect(sender.address).to eq(gitlab_sender) + expect(subject).to deliver_to(recipient.notification_email) + end end it 'contains the message from the note' do @@ -934,21 +872,20 @@ describe Notify do is_expected.to deliver_to 'new-email@mail.com' end - it 'has the correct subject' do - is_expected.to have_subject 'Confirmation instructions | A Nice Suffix' - end - - it 'includes a link to the site' do - is_expected.to have_body_text example_site_path + it 'has the correct subject and body' do + aggregate_failures do + is_expected.to have_subject('Confirmation instructions | A Nice Suffix') + is_expected.to have_body_text(example_site_path) + end end end describe 'email on push for a created branch' do let(:example_site_path) { root_path } let(:user) { create(:user) } - let(:tree_path) { namespace_project_tree_path(project.namespace, project, "master") } + let(:tree_path) { namespace_project_tree_path(project.namespace, project, "empty-branch") } - subject { Notify.repository_push_email(project.id, author_id: user.id, ref: 'refs/heads/master', action: :create) } + subject { Notify.repository_push_email(project.id, author_id: user.id, ref: 'refs/heads/empty-branch', action: :create) } it_behaves_like 'it should not have Gmail Actions links' it_behaves_like 'a user cannot unsubscribe through footer link' @@ -961,12 +898,11 @@ describe Notify do expect(sender.address).to eq(gitlab_sender) end - it 'has the correct subject' do - is_expected.to have_subject "[Git][#{project.full_path}] Pushed new branch master" - end - - it 'contains a link to the branch' do - is_expected.to have_body_text tree_path + it 'has the correct subject and body' do + aggregate_failures do + is_expected.to have_subject("[Git][#{project.full_path}] Pushed new branch empty-branch") + is_expected.to have_body_text(tree_path) + end end end @@ -988,12 +924,11 @@ describe Notify do expect(sender.address).to eq(gitlab_sender) end - it 'has the correct subject' do - is_expected.to have_subject "[Git][#{project.full_path}] Pushed new tag v1.0" - end - - it 'contains a link to the tag' do - is_expected.to have_body_text tree_path + it 'has the correct subject and body' do + aggregate_failures do + is_expected.to have_subject("[Git][#{project.full_path}] Pushed new tag v1.0") + is_expected.to have_body_text(tree_path) + end end end @@ -1064,24 +999,14 @@ describe Notify do expect(sender.address).to eq(gitlab_sender) end - it 'has the correct subject' do - is_expected.to have_subject "[Git][#{project.full_path}][master] #{commits.length} commits: Ruby files modified" - end - - it 'includes commits list' do - is_expected.to have_body_text 'Change some files' - end - - it 'includes diffs with character-level highlighting' do - is_expected.to have_body_text 'def</span> <span class="nf">archive_formats_regex' - end - - it 'contains a link to the diff' do - is_expected.to have_body_text diff_path - end - - it 'does not contain the misleading footer' do - is_expected.not_to have_body_text 'you are a member of' + it 'has the correct subject and body' do + aggregate_failures do + is_expected.to have_subject("[Git][#{project.full_path}][master] #{commits.length} commits: Ruby files modified") + is_expected.to have_body_text('Change some files') + is_expected.to have_body_text('def</span> <span class="nf">archive_formats_regex') + is_expected.to have_body_text(diff_path) + is_expected.not_to have_body_text('you are a member of') + end end context "when set to send from committer email if domain matches" do @@ -1098,13 +1023,13 @@ describe Notify do end it "is sent from the committer email" do - sender = subject.header[:from].addrs[0] - expect(sender.address).to eq(user.email) - end + from = subject.header[:from].addrs.first + reply = subject.header[:reply_to].addrs.first - it "is set to reply to the committer email" do - sender = subject.header[:reply_to].addrs[0] - expect(sender.address).to eq(user.email) + aggregate_failures do + expect(from.address).to eq(user.email) + expect(reply.address).to eq(user.email) + end end end @@ -1115,13 +1040,13 @@ describe Notify do end it "is sent from the default email" do - sender = subject.header[:from].addrs[0] - expect(sender.address).to eq(gitlab_sender) - end + from = subject.header[:from].addrs.first + reply = subject.header[:reply_to].addrs.first - it "is set to reply to the default email" do - sender = subject.header[:reply_to].addrs[0] - expect(sender.address).to eq(gitlab_sender_reply_to) + aggregate_failures do + expect(from.address).to eq(gitlab_sender) + expect(reply.address).to eq(gitlab_sender_reply_to) + end end end @@ -1132,13 +1057,13 @@ describe Notify do end it "is sent from the default email" do - sender = subject.header[:from].addrs[0] - expect(sender.address).to eq(gitlab_sender) - end + from = subject.header[:from].addrs.first + reply = subject.header[:reply_to].addrs.first - it "is set to reply to the default email" do - sender = subject.header[:reply_to].addrs[0] - expect(sender.address).to eq(gitlab_sender_reply_to) + aggregate_failures do + expect(from.address).to eq(gitlab_sender) + expect(reply.address).to eq(gitlab_sender_reply_to) + end end end end @@ -1166,20 +1091,13 @@ describe Notify do expect(sender.address).to eq(gitlab_sender) end - it 'has the correct subject' do - is_expected.to have_subject "[Git][#{project.full_path}][master] #{commits.first.title}" - end - - it 'includes commits list' do - is_expected.to have_body_text 'Change some files' - end - - it 'includes diffs with character-level highlighting' do - is_expected.to have_body_text 'def</span> <span class="nf">archive_formats_regex' - end - - it 'contains a link to the diff' do - is_expected.to have_body_text diff_path + it 'has the correct subject and body' do + aggregate_failures do + is_expected.to have_subject("[Git][#{project.full_path}][master] #{commits.first.title}") + is_expected.to have_body_text('Change some files') + is_expected.to have_body_text('def</span> <span class="nf">archive_formats_regex') + is_expected.to have_body_text(diff_path) + end end end diff --git a/spec/services/boards/issues/list_service_spec.rb b/spec/services/boards/issues/list_service_spec.rb index 22115c6566d..d841bdaa292 100644 --- a/spec/services/boards/issues/list_service_spec.rb +++ b/spec/services/boards/issues/list_service_spec.rb @@ -30,6 +30,7 @@ describe Boards::Issues::ListService, services: true do let!(:closed_issue2) { create(:labeled_issue, :closed, project: project, labels: [p3]) } let!(:closed_issue3) { create(:issue, :closed, project: project) } let!(:closed_issue4) { create(:labeled_issue, :closed, project: project, labels: [p1]) } + let!(:closed_issue5) { create(:labeled_issue, :closed, project: project, labels: [development]) } before do project.team << [user, :developer] @@ -57,7 +58,7 @@ describe Boards::Issues::ListService, services: true do issues = described_class.new(project, user, params).execute - expect(issues).to eq [closed_issue4, closed_issue2, closed_issue3, closed_issue1] + expect(issues).to eq [closed_issue4, closed_issue2, closed_issue5, closed_issue3, closed_issue1] end it 'returns opened issues that have label list applied when listing issues from a label list' do diff --git a/spec/services/issuable/bulk_update_service_spec.rb b/spec/services/issuable/bulk_update_service_spec.rb index 0475f38fe5e..7a1ac027310 100644 --- a/spec/services/issuable/bulk_update_service_spec.rb +++ b/spec/services/issuable/bulk_update_service_spec.rb @@ -138,7 +138,7 @@ describe Issuable::BulkUpdateService, services: true do let(:labels) { [bug, regression] } it 'updates the labels of all issues passed to the labels passed' do - expect(issues.map(&:reload).map(&:label_ids)).to all(eq(labels.map(&:id))) + expect(issues.map(&:reload).map(&:label_ids)).to all(match_array(labels.map(&:id))) end it 'does not update issues not passed in' do diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index ceb3209331f..5ab8f0d981a 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -35,7 +35,8 @@ RSpec.configure do |config| config.include Warden::Test::Helpers, type: :request config.include LoginHelpers, type: :feature config.include SearchHelpers, type: :feature - config.include WaitForAjax, type: :feature + config.include WaitForRequests, :js + config.include WaitForAjax, :js config.include StubConfiguration config.include EmailHelpers, type: :mailer config.include TestEnv diff --git a/spec/support/notify_shared_examples.rb b/spec/support/notify_shared_examples.rb index a3724b801b3..16a425f2ca2 100644 --- a/spec/support/notify_shared_examples.rb +++ b/spec/support/notify_shared_examples.rb @@ -27,24 +27,14 @@ shared_examples 'a multiple recipients email' do end shared_examples 'an email sent from GitLab' do - it 'is sent from GitLab' do + it 'has the characteristics of an email sent from GitLab' do sender = subject.header[:from].addrs[0] - expect(sender.display_name).to eq(gitlab_sender_display_name) - expect(sender.address).to eq(gitlab_sender) - end - - it 'has a Reply-To address' do reply_to = subject.header[:reply_to].addresses - expect(reply_to).to eq([gitlab_sender_reply_to]) - end - - context 'when custom suffix for email subject is set' do - before do - stub_config_setting(email_subject_suffix: 'A Nice Suffix') - end - it 'ends the subject with the suffix' do - is_expected.to have_subject /\ \| A Nice Suffix$/ + aggregate_failures do + expect(sender.display_name).to eq(gitlab_sender_display_name) + expect(sender.address).to eq(gitlab_sender) + expect(reply_to).to eq([gitlab_sender_reply_to]) end end end @@ -56,43 +46,40 @@ shared_examples 'an email that contains a header with author username' do end shared_examples 'an email with X-GitLab headers containing project details' do - it 'has X-GitLab-Project* headers' do - is_expected.to have_header 'X-GitLab-Project', /#{project.name}/ - is_expected.to have_header 'X-GitLab-Project-Id', /#{project.id}/ - is_expected.to have_header 'X-GitLab-Project-Path', /#{project.path_with_namespace}/ + it 'has X-GitLab-Project headers' do + aggregate_failures do + is_expected.to have_header('X-GitLab-Project', /#{project.name}/) + is_expected.to have_header('X-GitLab-Project-Id', /#{project.id}/) + is_expected.to have_header('X-GitLab-Project-Path', /#{project.path_with_namespace}/) + end end end shared_examples 'a new thread email with reply-by-email enabled' do - let(:regex) { /\A<reply\-(.*)@#{Gitlab.config.gitlab.host}>\Z/ } - - it 'has a Message-ID header' do - is_expected.to have_header 'Message-ID', "<#{model.class.model_name.singular_route_key}_#{model.id}@#{Gitlab.config.gitlab.host}>" - end + it 'has the characteristics of a threaded email' do + host = Gitlab.config.gitlab.host + route_key = "#{model.class.model_name.singular_route_key}_#{model.id}" - it 'has a References header' do - is_expected.to have_header 'References', regex + aggregate_failures do + is_expected.to have_header('Message-ID', "<#{route_key}@#{host}>") + is_expected.to have_header('References', /\A<reply\-.*@#{host}>\Z/ ) + end end end shared_examples 'a thread answer email with reply-by-email enabled' do include_examples 'an email with X-GitLab headers containing project details' - let(:regex) { /\A<#{model.class.model_name.singular_route_key}_#{model.id}@#{Gitlab.config.gitlab.host}> <reply\-(.*)@#{Gitlab.config.gitlab.host}>\Z/ } - - it 'has a Message-ID header' do - is_expected.to have_header 'Message-ID', /\A<(.*)@#{Gitlab.config.gitlab.host}>\Z/ - end - - it 'has a In-Reply-To header' do - is_expected.to have_header 'In-Reply-To', "<#{model.class.model_name.singular_route_key}_#{model.id}@#{Gitlab.config.gitlab.host}>" - end - it 'has a References header' do - is_expected.to have_header 'References', regex - end + it 'has the characteristics of a threaded reply' do + host = Gitlab.config.gitlab.host + route_key = "#{model.class.model_name.singular_route_key}_#{model.id}" - it 'has a subject that begins with Re: ' do - is_expected.to have_subject /^Re: / + aggregate_failures do + is_expected.to have_header('Message-ID', /\A<.*@#{host}>\Z/) + is_expected.to have_header('In-Reply-To', "<#{route_key}@#{host}>") + is_expected.to have_header('References', /\A<#{route_key}@#{host}> <reply\-.*@#{host}>\Z/ ) + is_expected.to have_subject(/^Re: /) + end end end @@ -136,80 +123,77 @@ shared_examples 'an answer to an existing thread with reply-by-email enabled' do end end -shared_examples 'a new user email' do - it 'is sent to the new user' do - is_expected.to deliver_to new_user_address - end - - it 'has the correct subject' do - is_expected.to have_subject /^Account was created for you$/i - end - - it 'contains the new user\'s login name' do - is_expected.to have_body_text /#{new_user_address}/ - end -end - shared_examples 'it should have Gmail Actions links' do - it { is_expected.to have_body_text '<script type="application/ld+json">' } - it { is_expected.to have_body_text /ViewAction/ } + it do + aggregate_failures do + is_expected.to have_body_text('<script type="application/ld+json">') + is_expected.to have_body_text('ViewAction') + end + end end shared_examples 'it should not have Gmail Actions links' do - it { is_expected.not_to have_body_text '<script type="application/ld+json">' } - it { is_expected.not_to have_body_text /ViewAction/ } + it do + aggregate_failures do + is_expected.not_to have_body_text('<script type="application/ld+json">') + is_expected.not_to have_body_text('ViewAction') + end + end end shared_examples 'it should show Gmail Actions View Issue link' do it_behaves_like 'it should have Gmail Actions links' - it { is_expected.to have_body_text /View Issue/ } + it { is_expected.to have_body_text('View Issue') } end shared_examples 'it should show Gmail Actions View Merge request link' do it_behaves_like 'it should have Gmail Actions links' - it { is_expected.to have_body_text /View Merge request/ } + it { is_expected.to have_body_text('View Merge request') } end shared_examples 'it should show Gmail Actions View Commit link' do it_behaves_like 'it should have Gmail Actions links' - it { is_expected.to have_body_text /View Commit/ } + it { is_expected.to have_body_text('View Commit') } end shared_examples 'an unsubscribeable thread' do it_behaves_like 'an unsubscribeable thread with incoming address without %{key}' - it 'has a List-Unsubscribe header in the correct format' do - is_expected.to have_header 'List-Unsubscribe', /unsubscribe/ - is_expected.to have_header 'List-Unsubscribe', /mailto/ - is_expected.to have_header 'List-Unsubscribe', /^<.+,.+>$/ + it 'has a List-Unsubscribe header in the correct format, and a body link' do + aggregate_failures do + is_expected.to have_header('List-Unsubscribe', /unsubscribe/) + is_expected.to have_header('List-Unsubscribe', /mailto/) + is_expected.to have_header('List-Unsubscribe', /^<.+,.+>$/) + is_expected.to have_body_text('unsubscribe') + end end - - it { is_expected.to have_body_text /unsubscribe/ } end shared_examples 'an unsubscribeable thread with incoming address without %{key}' do include_context 'reply-by-email is enabled with incoming address without %{key}' - it 'has a List-Unsubscribe header in the correct format' do - is_expected.to have_header 'List-Unsubscribe', /unsubscribe/ - is_expected.not_to have_header 'List-Unsubscribe', /mailto/ - is_expected.to have_header 'List-Unsubscribe', /^<[^,]+>$/ + it 'has a List-Unsubscribe header in the correct format, and a body link' do + aggregate_failures do + is_expected.to have_header('List-Unsubscribe', /unsubscribe/) + is_expected.not_to have_header('List-Unsubscribe', /mailto/) + is_expected.to have_header('List-Unsubscribe', /^<[^,]+>$/) + is_expected.to have_body_text('unsubscribe') + end end - - it { is_expected.to have_body_text /unsubscribe/ } end shared_examples 'a user cannot unsubscribe through footer link' do - it 'does not have a List-Unsubscribe header' do - is_expected.not_to have_header 'List-Unsubscribe', /unsubscribe/ + it 'does not have a List-Unsubscribe header or a body link' do + aggregate_failures do + is_expected.not_to have_header('List-Unsubscribe', /unsubscribe/) + is_expected.not_to have_body_text('unsubscribe') + end end - - it { is_expected.not_to have_body_text /unsubscribe/ } end shared_examples 'an email with a labels subscriptions link in its footer' do - it { is_expected.to have_body_text /label subscriptions/ } + it { is_expected.to have_body_text('label subscriptions') } end diff --git a/spec/support/slack_mattermost_notifications_shared_examples.rb b/spec/support/slack_mattermost_notifications_shared_examples.rb index 704922b6cf4..b902fe90707 100644 --- a/spec/support/slack_mattermost_notifications_shared_examples.rb +++ b/spec/support/slack_mattermost_notifications_shared_examples.rb @@ -324,5 +324,24 @@ RSpec.shared_examples 'slack or mattermost notifications' do it_behaves_like 'call Slack/Mattermost API' end end + + context 'only notify for the default branch' do + context 'when enabled' do + let(:pipeline) do + create(:ci_pipeline, project: project, status: 'failed', ref: 'not-the-default-branch') + end + + before do + chat_service.notify_only_default_branch = true + end + + it 'does not call the Slack/Mattermost API for pipeline events' do + data = Gitlab::DataBuilder::Pipeline.build(pipeline) + result = chat_service.execute(data) + + expect(result).to be_falsy + end + end + end end end diff --git a/spec/support/wait_for_requests.rb b/spec/support/wait_for_requests.rb new file mode 100644 index 00000000000..0bfa7f72ff8 --- /dev/null +++ b/spec/support/wait_for_requests.rb @@ -0,0 +1,32 @@ +module WaitForRequests + extend self + + # This is inspired by http://www.salsify.com/blog/engineering/tearing-capybara-ajax-tests + def wait_for_requests_complete + Gitlab::Testing::RequestBlockerMiddleware.block_requests! + wait_for('pending AJAX requests complete') do + Gitlab::Testing::RequestBlockerMiddleware.num_active_requests.zero? + end + ensure + Gitlab::Testing::RequestBlockerMiddleware.allow_requests! + end + + # Waits until the passed block returns true + def wait_for(condition_name, max_wait_time: Capybara.default_max_wait_time, polling_interval: 0.01) + wait_until = Time.now + max_wait_time.seconds + loop do + break if yield + if Time.now > wait_until + raise "Condition not met: #{condition_name}" + else + sleep(polling_interval) + end + end + end +end + +RSpec.configure do |config| + config.after(:each, :js) do + wait_for_requests_complete + end +end diff --git a/vendor/assets/javascripts/js.cookie.js b/vendor/assets/javascripts/js.cookie.js deleted file mode 100644 index 92dbba162c4..00000000000 --- a/vendor/assets/javascripts/js.cookie.js +++ /dev/null @@ -1,156 +0,0 @@ -/*! - * JavaScript Cookie v2.1.3 - * https://github.com/js-cookie/js-cookie - * - * Copyright 2006, 2015 Klaus Hartl & Fagner Brack - * Released under the MIT license - */ -;(function (factory) { - var registeredInModuleLoader = false; - if (typeof define === 'function' && define.amd) { - define(factory); - registeredInModuleLoader = true; - } - if (typeof exports === 'object') { - module.exports = factory(); - registeredInModuleLoader = true; - } - if (!registeredInModuleLoader) { - var OldCookies = window.Cookies; - var api = window.Cookies = factory(); - api.noConflict = function () { - window.Cookies = OldCookies; - return api; - }; - } -}(function () { - function extend () { - var i = 0; - var result = {}; - for (; i < arguments.length; i++) { - var attributes = arguments[ i ]; - for (var key in attributes) { - result[key] = attributes[key]; - } - } - return result; - } - - function init (converter) { - function api (key, value, attributes) { - var result; - if (typeof document === 'undefined') { - return; - } - - // Write - - if (arguments.length > 1) { - attributes = extend({ - path: '/' - }, api.defaults, attributes); - - if (typeof attributes.expires === 'number') { - var expires = new Date(); - expires.setMilliseconds(expires.getMilliseconds() + attributes.expires * 864e+5); - attributes.expires = expires; - } - - try { - result = JSON.stringify(value); - if (/^[\{\[]/.test(result)) { - value = result; - } - } catch (e) {} - - if (!converter.write) { - value = encodeURIComponent(String(value)) - .replace(/%(23|24|26|2B|3A|3C|3E|3D|2F|3F|40|5B|5D|5E|60|7B|7D|7C)/g, decodeURIComponent); - } else { - value = converter.write(value, key); - } - - key = encodeURIComponent(String(key)); - key = key.replace(/%(23|24|26|2B|5E|60|7C)/g, decodeURIComponent); - key = key.replace(/[\(\)]/g, escape); - - return (document.cookie = [ - key, '=', value, - attributes.expires ? '; expires=' + attributes.expires.toUTCString() : '', // use expires attribute, max-age is not supported by IE - attributes.path ? '; path=' + attributes.path : '', - attributes.domain ? '; domain=' + attributes.domain : '', - attributes.secure ? '; secure' : '' - ].join('')); - } - - // Read - - if (!key) { - result = {}; - } - - // To prevent the for loop in the first place assign an empty array - // in case there are no cookies at all. Also prevents odd result when - // calling "get()" - var cookies = document.cookie ? document.cookie.split('; ') : []; - var rdecode = /(%[0-9A-Z]{2})+/g; - var i = 0; - - for (; i < cookies.length; i++) { - var parts = cookies[i].split('='); - var cookie = parts.slice(1).join('='); - - if (cookie.charAt(0) === '"') { - cookie = cookie.slice(1, -1); - } - - try { - var name = parts[0].replace(rdecode, decodeURIComponent); - cookie = converter.read ? - converter.read(cookie, name) : converter(cookie, name) || - cookie.replace(rdecode, decodeURIComponent); - - if (this.json) { - try { - cookie = JSON.parse(cookie); - } catch (e) {} - } - - if (key === name) { - result = cookie; - break; - } - - if (!key) { - result[name] = cookie; - } - } catch (e) {} - } - - return result; - } - - api.set = api; - api.get = function (key) { - return api.call(api, key); - }; - api.getJSON = function () { - return api.apply({ - json: true - }, [].slice.call(arguments)); - }; - api.defaults = {}; - - api.remove = function (key, attributes) { - api(key, '', extend(attributes, { - expires: -1 - })); - }; - - api.withConverter = init; - - return api; - } - - return init(function () {}); -}));
\ No newline at end of file |