diff options
57 files changed, 402 insertions, 189 deletions
diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index 2a01bc4d44d..731ce57c245 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -272,7 +272,7 @@ header { .header-user { .dropdown-menu-nav { - width: 140px; + min-width: 140px; margin-top: -5px; } } diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index 692142c5887..1431673027f 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -96,13 +96,6 @@ padding-right: 4px; } - &.ci-success_with_warnings { - - i { - color: $gl-warning; - } - } - @media (max-width: $screen-xs-max) { flex-wrap: wrap; } diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index 974100bdff0..0c7019dc64f 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -94,6 +94,10 @@ padding: 10px 8px; } + td.stage-cell { + padding: 10px 0; + } + .commit-link { padding: 9px 8px 10px; } @@ -291,12 +295,14 @@ height: 22px; margin: 3px 6px 3px 0; - .tooltip { - white-space: nowrap; + // Hack to show a button tooltip inline + button.has-tooltip + .tooltip { + min-width: 105px; } - .tooltip-inner { - padding: 3px 4px; + // Bootstrap way of showing the content inline for anchors. + a.has-tooltip { + white-space: nowrap; } &:not(:last-child) { diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index bb47e2a8bf7..bf6be3d516b 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -12,7 +12,6 @@ class ApplicationController < ActionController::Base before_action :authenticate_user_from_private_token! before_action :authenticate_user! before_action :validate_user_service_ticket! - before_action :reject_blocked! before_action :check_password_expiration before_action :check_2fa_requirement before_action :ldap_security_check @@ -87,22 +86,8 @@ class ApplicationController < ActionController::Base logger.error "\n#{exception.class.name} (#{exception.message}):\n#{application_trace.join}" end - def reject_blocked! - if current_user && current_user.blocked? - sign_out current_user - flash[:alert] = "Your account is blocked. Retry when an admin has unblocked it." - redirect_to new_user_session_path - end - end - def after_sign_in_path_for(resource) - if resource.is_a?(User) && resource.respond_to?(:blocked?) && resource.blocked? - sign_out resource - flash[:alert] = "Your account is blocked. Retry when an admin has unblocked it." - new_user_session_path - else - stored_location_for(:redirect) || stored_location_for(resource) || root_path - end + stored_location_for(:redirect) || stored_location_for(resource) || root_path end def after_sign_out_path_for(resource) diff --git a/app/controllers/explore/application_controller.rb b/app/controllers/explore/application_controller.rb index a1ab8b99048..baf54520b9c 100644 --- a/app/controllers/explore/application_controller.rb +++ b/app/controllers/explore/application_controller.rb @@ -1,5 +1,5 @@ class Explore::ApplicationController < ApplicationController - skip_before_action :authenticate_user!, :reject_blocked! + skip_before_action :authenticate_user! layout 'explore' end diff --git a/app/controllers/groups/group_members_controller.rb b/app/controllers/groups/group_members_controller.rb index 4f273a8d4f0..0cbf3eb58a3 100644 --- a/app/controllers/groups/group_members_controller.rb +++ b/app/controllers/groups/group_members_controller.rb @@ -9,7 +9,7 @@ class Groups::GroupMembersController < Groups::ApplicationController @sort = params[:sort].presence || sort_value_name @project = @group.projects.find(params[:project_id]) if params[:project_id] - @members = @group.group_members + @members = GroupMembersFinder.new(@group).execute @members = @members.non_invite unless can?(current_user, :admin_group, @group) @members = @members.search(params[:search]) if params[:search].present? @members = @members.sort(@sort) diff --git a/app/controllers/help_controller.rb b/app/controllers/help_controller.rb index 37feff79999..87c0f8905ff 100644 --- a/app/controllers/help_controller.rb +++ b/app/controllers/help_controller.rb @@ -1,5 +1,5 @@ class HelpController < ApplicationController - skip_before_action :authenticate_user!, :reject_blocked! + skip_before_action :authenticate_user! layout 'help' diff --git a/app/controllers/koding_controller.rb b/app/controllers/koding_controller.rb index f3759b4c0ea..6b1e64ce819 100644 --- a/app/controllers/koding_controller.rb +++ b/app/controllers/koding_controller.rb @@ -1,5 +1,5 @@ class KodingController < ApplicationController - before_action :check_integration!, :authenticate_user!, :reject_blocked! + before_action :check_integration! layout 'koding' def index diff --git a/app/controllers/projects/uploads_controller.rb b/app/controllers/projects/uploads_controller.rb index 50ba33ed570..61686499bd3 100644 --- a/app/controllers/projects/uploads_controller.rb +++ b/app/controllers/projects/uploads_controller.rb @@ -1,6 +1,6 @@ class Projects::UploadsController < Projects::ApplicationController - skip_before_action :reject_blocked!, :project, - :repository, if: -> { action_name == 'show' && image_or_video? } + skip_before_action :project, :repository, + if: -> { action_name == 'show' && image_or_video? } before_action :authorize_upload_file!, only: [:create] diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb index 6576ebd5235..612d69cf557 100644 --- a/app/controllers/search_controller.rb +++ b/app/controllers/search_controller.rb @@ -1,5 +1,5 @@ class SearchController < ApplicationController - skip_before_action :authenticate_user!, :reject_blocked! + skip_before_action :authenticate_user! include SearchHelper diff --git a/app/finders/group_members_finder.rb b/app/finders/group_members_finder.rb new file mode 100644 index 00000000000..9f2206346ce --- /dev/null +++ b/app/finders/group_members_finder.rb @@ -0,0 +1,20 @@ +class GroupMembersFinder < Projects::ApplicationController + def initialize(group) + @group = group + end + + def execute + group_members = @group.members + + return group_members unless @group.parent + + parents_members = GroupMember.non_request. + where(source_id: @group.ancestors.select(:id)). + where.not(user_id: @group.users.select(:id)) + + wheres = ["members.id IN (#{group_members.select(:id).to_sql})"] + wheres << "members.id IN (#{parents_members.select(:id).to_sql})" + + GroupMember.where(wheres.join(' OR ')) + end +end diff --git a/app/models/group.rb b/app/models/group.rb index a5b92283daa..cc6624ff4aa 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -206,7 +206,7 @@ class Group < Namespace end def members_with_parents - GroupMember.where(requested_at: nil, source_id: ancestors.map(&:id).push(id)) + GroupMember.non_request.where(source_id: ancestors.map(&:id).push(id)) end def users_with_parents diff --git a/app/models/member.rb b/app/models/member.rb index 26a6054e00d..d07f270b757 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -47,6 +47,7 @@ class Member < ActiveRecord::Base scope :invite, -> { where.not(invite_token: nil) } scope :non_invite, -> { where(invite_token: nil) } scope :request, -> { where.not(requested_at: nil) } + scope :non_request, -> { where(requested_at: nil) } scope :has_access, -> { active.where('access_level > 0') } diff --git a/app/models/user.rb b/app/models/user.rb index 33666b4f35b..f9245cdb82a 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -167,6 +167,15 @@ class User < ActiveRecord::Base def blocked? true end + + def active_for_authentication? + false + end + + def inactive_message + "Your account has been blocked. Please contact your GitLab " \ + "administrator if you think this is an error." + end end end diff --git a/app/views/projects/ci/pipelines/_pipeline.html.haml b/app/views/projects/ci/pipelines/_pipeline.html.haml index ac0fd87fd8d..f852f2e3fd7 100644 --- a/app/views/projects/ci/pipelines/_pipeline.html.haml +++ b/app/views/projects/ci/pipelines/_pipeline.html.haml @@ -15,7 +15,7 @@ - else %span.api.monospace API - if pipeline.latest? - %span.label.label-success.has-tooltip{ title: 'Latest job for this branch' } latest + %span.label.label-success.has-tooltip{ title: 'Latest pipeline for this branch' } latest - if pipeline.triggered? %span.label.label-primary triggered - if pipeline.yaml_errors.present? @@ -61,7 +61,7 @@ .btn-group.inline - if actions.any? .btn-group - %button.dropdown-toggle.btn.btn-default.has-tooltip.js-pipeline-dropdown-manual-actions{ type: 'button', title: 'Manual job', data: { toggle: 'dropdown', placement: 'top' }, 'aria-label' => 'Manual job' } + %button.dropdown-toggle.btn.btn-default.has-tooltip.js-pipeline-dropdown-manual-actions{ type: 'button', title: 'Manual pipeline', data: { toggle: 'dropdown', placement: 'top' }, 'aria-label' => 'Manual pipeline' } = custom_icon('icon_play') = icon('caret-down', 'aria-hidden' => 'true') %ul.dropdown-menu.dropdown-menu-align-right diff --git a/app/views/projects/environments/terminal.html.haml b/app/views/projects/environments/terminal.html.haml index 1d49e9cbaf7..ef0dd0eda3c 100644 --- a/app/views/projects/environments/terminal.html.haml +++ b/app/views/projects/environments/terminal.html.haml @@ -16,6 +16,8 @@ .col-sm-6 .nav-controls + = link_to @environment.external_url, class: 'btn btn-default' do + = icon('external-link') = render 'projects/deployments/actions', deployment: @environment.last_deployment .terminal-container{ class: container_class } diff --git a/app/views/projects/merge_requests/widget/_heading.html.haml b/app/views/projects/merge_requests/widget/_heading.html.haml index bef76f16ca7..e3062f47788 100644 --- a/app/views/projects/merge_requests/widget/_heading.html.haml +++ b/app/views/projects/merge_requests/widget/_heading.html.haml @@ -9,8 +9,9 @@ Pipeline = link_to "##{@pipeline.id}", namespace_project_pipeline_path(@pipeline.project.namespace, @pipeline.project, @pipeline.id), class: 'pipeline' = ci_label_for_status(status) - .mr-widget-pipeline-graph - = render 'shared/mini_pipeline_graph', pipeline: @pipeline, klass: 'js-pipeline-inline-mr-widget-graph' + - if @pipeline.stages.any? + .mr-widget-pipeline-graph + = render 'shared/mini_pipeline_graph', pipeline: @pipeline, klass: 'js-pipeline-inline-mr-widget-graph' %span for = succeed "." do diff --git a/app/views/projects/merge_requests/widget/_show.html.haml b/app/views/projects/merge_requests/widget/_show.html.haml index 5de59473840..0b0fb7854c2 100644 --- a/app/views/projects/merge_requests/widget/_show.html.haml +++ b/app/views/projects/merge_requests/widget/_show.html.haml @@ -16,13 +16,13 @@ gitlab_icon: "#{asset_path 'gitlab_logo.png'}", ci_status: "#{@merge_request.head_pipeline ? @merge_request.head_pipeline.status : ''}", ci_message: { - normal: "Job {{status}} for \"{{title}}\"", - preparing: "{{status}} job for \"{{title}}\"" + normal: "Pipeline {{status}} for \"{{title}}\"", + preparing: "{{status}} pipeline for \"{{title}}\"" }, ci_enable: #{@project.ci_service ? "true" : "false"}, ci_title: { - preparing: "{{status}} job", - normal: "Job {{status}}" + preparing: "{{status}} pipeline", + normal: "Pipeline {{status}}" }, ci_sha: "#{@merge_request.head_pipeline ? @merge_request.head_pipeline.short_sha : ''}", ci_pipeline: #{@merge_request.head_pipeline.try(:id).to_json}, diff --git a/app/views/projects/merge_requests/widget/open/_build_failed.html.haml b/app/views/projects/merge_requests/widget/open/_build_failed.html.haml index a18c2ad768f..3979d5fa8ed 100644 --- a/app/views/projects/merge_requests/widget/open/_build_failed.html.haml +++ b/app/views/projects/merge_requests/widget/open/_build_failed.html.haml @@ -1,6 +1,6 @@ %h4 = icon('exclamation-triangle') - The job for this merge request failed + The pipeline for this merge request failed %p Please retry the job or push a new commit to fix the failure. diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index cd685f7d0eb..41473fae4de 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -94,9 +94,8 @@ .form-group.project-visibility-level-holder = f.label :visibility_level, class: 'label-light' do Visibility Level - = link_to "(?)", help_page_path("public_access/public_access") - = render 'shared/visibility_level', f: f, visibility_level: default_project_visibility, can_change_visibility_level: true, form_model: @project - + = link_to icon('question-circle'), help_page_path("public_access/public_access") + = render 'shared/visibility_level', f: f, visibility_level: default_project_visibility, can_change_visibility_level: true, form_model: @project, with_label: false = f.submit 'Create project', class: "btn btn-create project-submit", tabindex: 4 = link_to 'Cancel', dashboard_projects_path, class: 'btn btn-cancel' diff --git a/app/views/projects/triggers/_index.html.haml b/app/views/projects/triggers/_index.html.haml index 5cb1818ae54..33883facf9b 100644 --- a/app/views/projects/triggers/_index.html.haml +++ b/app/views/projects/triggers/_index.html.haml @@ -65,7 +65,7 @@ In the %code .gitlab-ci.yml of another project, include the following snippet. - The project will be rebuilt at the end of the job. + The project will be rebuilt at the end of the pipeline. %pre :plain @@ -89,7 +89,7 @@ %p.light Add %code variables[VARIABLE]=VALUE - to an API request. Variable values can be used to distinguish between triggered jobs and normal jobs. + to an API request. Variable values can be used to distinguish between triggered pipelines and normal pipelines. With cURL: diff --git a/app/views/shared/_visibility_level.html.haml b/app/views/shared/_visibility_level.html.haml index b11257ee0e6..73efec88bb1 100644 --- a/app/views/shared/_visibility_level.html.haml +++ b/app/views/shared/_visibility_level.html.haml @@ -1,8 +1,11 @@ +- with_label = local_assigns.fetch(:with_label, true) + .form-group.project-visibility-level-holder - = f.label :visibility_level, class: 'control-label' do - Visibility Level - = link_to icon('question-circle'), help_page_path("public_access/public_access") - .col-sm-10 + - if with_label + = f.label :visibility_level, class: 'control-label' do + Visibility Level + = link_to icon('question-circle'), help_page_path("public_access/public_access") + %div{ :class => ("col-sm-10" if with_label) } - if can_change_visibility_level = render('shared/visibility_radios', model_method: :visibility_level, form: f, selected_level: visibility_level, form_model: form_model) - else diff --git a/app/views/shared/members/_member.html.haml b/app/views/shared/members/_member.html.haml index 659d4c905fc..239387fc9fa 100644 --- a/app/views/shared/members/_member.html.haml +++ b/app/views/shared/members/_member.html.haml @@ -19,9 +19,9 @@ %label.label.label-danger %strong Blocked - - if source.instance_of?(Group) && !@group + - if source.instance_of?(Group) && source != @group · - = link_to source.name, source, class: "member-group-link" + = link_to source.full_name, source, class: "member-group-link" .hidden-xs.cgray - if member.request? @@ -44,8 +44,9 @@ = link_to member.created_by.name, user_path(member.created_by) = time_ago_with_tooltip(member.created_at) - if show_roles + - current_resource = @project || @group .controls.member-controls - - if show_controls && (member.respond_to?(:group) && @group) || (member.respond_to?(:project) && @project) + - if show_controls && member.source == current_resource - if user != current_user = form_for member, remote: true, html: { class: 'form-horizontal js-edit-member-form' } do |f| = f.hidden_field :access_level diff --git a/changelogs/unreleased/1051-api-create-users-without-password.yml b/changelogs/unreleased/1051-api-create-users-without-password.yml new file mode 100644 index 00000000000..24b5a73b45c --- /dev/null +++ b/changelogs/unreleased/1051-api-create-users-without-password.yml @@ -0,0 +1,4 @@ +--- +title: Optionally make users created via the API set their password +merge_request: 8957 +author: Joost Rijneveld diff --git a/changelogs/unreleased/27336-add-environment-url-link-to-terminal-page.yml b/changelogs/unreleased/27336-add-environment-url-link-to-terminal-page.yml new file mode 100644 index 00000000000..dd4907166c4 --- /dev/null +++ b/changelogs/unreleased/27336-add-environment-url-link-to-terminal-page.yml @@ -0,0 +1,4 @@ +--- +title: Added external environment link to web terminal view +merge_request: 8303 +author: diff --git a/changelogs/unreleased/27783-fix-fe-doc-broken-link.yml b/changelogs/unreleased/27783-fix-fe-doc-broken-link.yml new file mode 100644 index 00000000000..429110e9178 --- /dev/null +++ b/changelogs/unreleased/27783-fix-fe-doc-broken-link.yml @@ -0,0 +1,4 @@ +--- +title: Fixes FE Doc broken link +merge_request: 9120 +author: diff --git a/changelogs/unreleased/27955-mr-notification-use-pipeline-language.yml b/changelogs/unreleased/27955-mr-notification-use-pipeline-language.yml new file mode 100644 index 00000000000..d9f78db4bec --- /dev/null +++ b/changelogs/unreleased/27955-mr-notification-use-pipeline-language.yml @@ -0,0 +1,4 @@ +--- +title: Show Pipeline(not Job) in MR desktop notification +merge_request: +author: diff --git a/changelogs/unreleased/27963-tooltips-jobs.yml b/changelogs/unreleased/27963-tooltips-jobs.yml new file mode 100644 index 00000000000..ba418d86433 --- /dev/null +++ b/changelogs/unreleased/27963-tooltips-jobs.yml @@ -0,0 +1,4 @@ +--- +title: Fix tooltips in mini pipeline graph +merge_request: +author: diff --git a/changelogs/unreleased/27987-skipped-pipeline-mr-graph.yml b/changelogs/unreleased/27987-skipped-pipeline-mr-graph.yml new file mode 100644 index 00000000000..e4287d6276c --- /dev/null +++ b/changelogs/unreleased/27987-skipped-pipeline-mr-graph.yml @@ -0,0 +1,4 @@ +--- +title: Show pipeline graph in MR widget if there are any stages +merge_request: +author: diff --git a/changelogs/unreleased/27991-success-with-warnings-caret.yml b/changelogs/unreleased/27991-success-with-warnings-caret.yml new file mode 100644 index 00000000000..703d34a5ede --- /dev/null +++ b/changelogs/unreleased/27991-success-with-warnings-caret.yml @@ -0,0 +1,4 @@ +--- +title: Fix icon colors in merge request widget mini graph +merge_request: +author: diff --git a/changelogs/unreleased/fix-job-to-pipeline-renaming.yml b/changelogs/unreleased/fix-job-to-pipeline-renaming.yml new file mode 100644 index 00000000000..d5f34b4b25d --- /dev/null +++ b/changelogs/unreleased/fix-job-to-pipeline-renaming.yml @@ -0,0 +1,4 @@ +--- +title: Fix job to pipeline renaming +merge_request: 9147 +author: diff --git a/changelogs/unreleased/issue-newproj-layout.yml b/changelogs/unreleased/issue-newproj-layout.yml new file mode 100644 index 00000000000..d15e8b7d1e5 --- /dev/null +++ b/changelogs/unreleased/issue-newproj-layout.yml @@ -0,0 +1,4 @@ +--- +title: Removed duplicate "Visibility Level" label on New Project page +merge_request: +author: Robert Marcano diff --git a/changelogs/unreleased/rs-warden-blocked-users.yml b/changelogs/unreleased/rs-warden-blocked-users.yml new file mode 100644 index 00000000000..c0c23fb6f11 --- /dev/null +++ b/changelogs/unreleased/rs-warden-blocked-users.yml @@ -0,0 +1,4 @@ +--- +title: Don't perform Devise trackable updates on blocked User records +merge_request: 8915 +author: diff --git a/changelogs/unreleased/sh-add-index-to-ci-trigger-requests.yml b/changelogs/unreleased/sh-add-index-to-ci-trigger-requests.yml new file mode 100644 index 00000000000..bab76812a17 --- /dev/null +++ b/changelogs/unreleased/sh-add-index-to-ci-trigger-requests.yml @@ -0,0 +1,4 @@ +--- +title: Add index to ci_trigger_requests for commit_id +merge_request: +author: diff --git a/config/initializers/rspec_profiling.rb b/config/initializers/rspec_profiling.rb index f462e654b2c..0ef9f51e5cf 100644 --- a/config/initializers/rspec_profiling.rb +++ b/config/initializers/rspec_profiling.rb @@ -4,6 +4,12 @@ module RspecProfilingConnection end end +module RspecProfilingGitBranchCi + def branch + ENV['CI_BUILD_REF_NAME'] || super + end +end + if Rails.env.test? RspecProfiling.configure do |config| if ENV['RSPEC_PROFILING_POSTGRES_URL'] @@ -11,4 +17,6 @@ if Rails.env.test? config.collector = RspecProfiling::Collectors::PSQL end end + + RspecProfiling::VCS::Git.prepend(RspecProfilingGitBranchCi) if ENV.has_key?('CI') end diff --git a/db/migrate/20170210075922_add_index_to_ci_trigger_requests_for_commit_id.rb b/db/migrate/20170210075922_add_index_to_ci_trigger_requests_for_commit_id.rb new file mode 100644 index 00000000000..61e49c14fc0 --- /dev/null +++ b/db/migrate/20170210075922_add_index_to_ci_trigger_requests_for_commit_id.rb @@ -0,0 +1,11 @@ +class AddIndexToCiTriggerRequestsForCommitId < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def change + add_concurrent_index :ci_trigger_requests, :commit_id + end +end diff --git a/db/schema.rb b/db/schema.rb index 3fef5b82073..d71911eaf14 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20170206101030) do +ActiveRecord::Schema.define(version: 20170210075922) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -367,6 +367,8 @@ ActiveRecord::Schema.define(version: 20170206101030) do t.integer "commit_id" end + add_index "ci_trigger_requests", ["commit_id"], name: "index_ci_trigger_requests_on_commit_id", using: :btree + create_table "ci_triggers", force: :cascade do |t| t.string "token" t.integer "project_id" diff --git a/doc/api/users.md b/doc/api/users.md index fea9bdf9639..ed3469521fc 100644 --- a/doc/api/users.md +++ b/doc/api/users.md @@ -216,7 +216,7 @@ Parameters: ## User creation -Creates a new user. Note only administrators can create new users. +Creates a new user. Note only administrators can create new users. Either `password` or `reset_password` should be specified (`reset_password` takes priority). ``` POST /users @@ -225,7 +225,8 @@ POST /users Parameters: - `email` (required) - Email -- `password` (required) - Password +- `password` (optional) - Password +- `reset_password` (optional) - Send user password reset link - true or false(default) - `username` (required) - Username - `name` (required) - Name - `skype` (optional) - Skype ID diff --git a/doc/development/frontend.md b/doc/development/frontend.md index 75fdf3d8e63..ba47998de49 100644 --- a/doc/development/frontend.md +++ b/doc/development/frontend.md @@ -50,7 +50,7 @@ Let's look into each of them: This is the index file of your new feature. This is where the root Vue instance of the new feature should be. -Don't forget to follow [these steps.][page-specific-javascript] +Don't forget to follow [these steps.][page_specific_javascript] **A folder for Components** @@ -250,23 +250,17 @@ information. ### Running frontend tests -`rake teaspoon` runs the frontend-only (JavaScript) tests. +`rake karma` runs the frontend-only (JavaScript) tests. It consists of two subtasks: -- `rake teaspoon:fixtures` (re-)generates fixtures -- `rake teaspoon:tests` actually executes the tests +- `rake karma:fixtures` (re-)generates fixtures +- `rake karma:tests` actually executes the tests -As long as the fixtures don't change, `rake teaspoon:tests` is sufficient +As long as the fixtures don't change, `rake karma:tests` is sufficient (and saves you some time). -If you need to debug your tests and/or application code while they're -running, navigate to [localhost:3000/teaspoon](http://localhost:3000/teaspoon) -in your browser, open DevTools, and run tests for individual files by clicking -on them. This is also much faster than setting up and running tests from the -command line. - Please note: Not all of the frontend fixtures are generated. Some are still static -files. These will not be touched by `rake teaspoon:fixtures`. +files. These will not be touched by `rake karma:fixtures`. ## Design Patterns @@ -323,54 +317,13 @@ gl.MyThing = MyThing; For our currently-supported browsers, see our [requirements][requirements]. -[rails]: http://rubyonrails.org/ -[haml]: http://haml.info/ -[hamlit]: https://github.com/k0kubun/hamlit -[hamlit-limits]: https://github.com/k0kubun/hamlit/blob/master/REFERENCE.md#limitations -[scss]: http://sass-lang.com/ -[es6]: https://babeljs.io/ -[sprockets]: https://github.com/rails/sprockets -[jquery]: https://jquery.com/ -[vue]: http://vuejs.org/ -[vue-docs]: http://vuejs.org/guide/index.html -[web-page-test]: http://www.webpagetest.org/ -[pagespeed-insights]: https://developers.google.com/speed/pagespeed/insights/ -[google-devtools-profiling]: https://developers.google.com/web/tools/chrome-devtools/profile/?hl=en -[browser-diet]: https://browserdiet.com/ -[d3]: https://d3js.org/ -[chartjs]: http://www.chartjs.org/ -[page-specific-js-example]: https://gitlab.com/gitlab-org/gitlab-ce/blob/13bb9ed77f405c5f6ee4fdbc964ecf635c9a223f/app/views/projects/graphs/_head.html.haml#L6-8 -[chrome-accessibility-developer-tools]: https://github.com/GoogleChrome/accessibility-developer-tools -[audit-rules]: https://github.com/GoogleChrome/accessibility-developer-tools/wiki/Audit-Rules -[observatory-cli]: https://github.com/mozilla/http-observatory-cli -[qualys-ssl]: https://www.ssllabs.com/ssltest/analyze.html -[secure_headers]: https://github.com/twitter/secureheaders -[mdn-csp]: https://developer.mozilla.org/en-US/docs/Web/Security/CSP -[github-eng-csp]: http://githubengineering.com/githubs-csp-journey/ -[dropbox-csp-1]: https://blogs.dropbox.com/tech/2015/09/on-csp-reporting-and-filtering/ -[dropbox-csp-2]: https://blogs.dropbox.com/tech/2015/09/unsafe-inline-and-nonce-deployment/ -[dropbox-csp-3]: https://blogs.dropbox.com/tech/2015/09/csp-the-unexpected-eval/ -[dropbox-csp-4]: https://blogs.dropbox.com/tech/2015/09/csp-third-party-integrations-and-privilege-separation/ -[mdn-sri]: https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity -[github-eng-sri]: http://githubengineering.com/subresource-integrity/ -[sprockets-sri]: https://github.com/rails/sprockets-rails#sri-support -[xss]: https://en.wikipedia.org/wiki/Cross-site_scripting -[scss-style-guide]: scss_styleguide.md -[requirements]: ../install/requirements.md#supported-web-browsers -[issue-boards]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/app/assets/javascripts/boards -[environments-table]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/app/assets/javascripts/environments -[page_specific_javascript]: https://docs.gitlab.com/ce/development/frontend.html#page-specific-javascript -[component-system]: https://vuejs.org/v2/guide/#Composing-with-Components -[state-management]: https://vuejs.org/v2/guide/state-management.html#Simple-State-Management-from-Scratch -[vue-resource-repo]: https://github.com/pagekit/vue-resource -[issue-boards-service]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/app/assets/javascripts/boards/services/board_service.js.es6 ## Gotchas ### Spec errors due to use of ES6 features in `.js` files If you see very generic JavaScript errors (e.g. `jQuery is undefined`) being -thrown in Teaspoon, Spinach, or Rspec tests but can't reproduce them manually, +thrown in Karma, Spinach, or Rspec tests but can't reproduce them manually, you may have included `ES6`-style JavaScript in files that don't have the `.js.es6` file extension. Either use ES5-friendly JavaScript or rename the file you're working in (`git mv <file.js> <file.js.es6>`). @@ -438,3 +391,46 @@ Scenario: Developer can approve merge request Then I should see approved merge request "Bug NS-04" ``` + + +[rails]: http://rubyonrails.org/ +[haml]: http://haml.info/ +[hamlit]: https://github.com/k0kubun/hamlit +[hamlit-limits]: https://github.com/k0kubun/hamlit/blob/master/REFERENCE.md#limitations +[scss]: http://sass-lang.com/ +[es6]: https://babeljs.io/ +[sprockets]: https://github.com/rails/sprockets +[jquery]: https://jquery.com/ +[vue]: http://vuejs.org/ +[vue-docs]: http://vuejs.org/guide/index.html +[web-page-test]: http://www.webpagetest.org/ +[pagespeed-insights]: https://developers.google.com/speed/pagespeed/insights/ +[google-devtools-profiling]: https://developers.google.com/web/tools/chrome-devtools/profile/?hl=en +[browser-diet]: https://browserdiet.com/ +[d3]: https://d3js.org/ +[chartjs]: http://www.chartjs.org/ +[page-specific-js-example]: https://gitlab.com/gitlab-org/gitlab-ce/blob/13bb9ed77f405c5f6ee4fdbc964ecf635c9a223f/app/views/projects/graphs/_head.html.haml#L6-8 +[chrome-accessibility-developer-tools]: https://github.com/GoogleChrome/accessibility-developer-tools +[audit-rules]: https://github.com/GoogleChrome/accessibility-developer-tools/wiki/Audit-Rules +[observatory-cli]: https://github.com/mozilla/http-observatory-cli +[qualys-ssl]: https://www.ssllabs.com/ssltest/analyze.html +[secure_headers]: https://github.com/twitter/secureheaders +[mdn-csp]: https://developer.mozilla.org/en-US/docs/Web/Security/CSP +[github-eng-csp]: http://githubengineering.com/githubs-csp-journey/ +[dropbox-csp-1]: https://blogs.dropbox.com/tech/2015/09/on-csp-reporting-and-filtering/ +[dropbox-csp-2]: https://blogs.dropbox.com/tech/2015/09/unsafe-inline-and-nonce-deployment/ +[dropbox-csp-3]: https://blogs.dropbox.com/tech/2015/09/csp-the-unexpected-eval/ +[dropbox-csp-4]: https://blogs.dropbox.com/tech/2015/09/csp-third-party-integrations-and-privilege-separation/ +[mdn-sri]: https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity +[github-eng-sri]: http://githubengineering.com/subresource-integrity/ +[sprockets-sri]: https://github.com/rails/sprockets-rails#sri-support +[xss]: https://en.wikipedia.org/wiki/Cross-site_scripting +[scss-style-guide]: scss_styleguide.md +[requirements]: ../install/requirements.md#supported-web-browsers +[issue-boards]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/app/assets/javascripts/boards +[environments-table]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/app/assets/javascripts/environments +[page_specific_javascript]: https://docs.gitlab.com/ce/development/frontend.html#page-specific-javascript +[component-system]: https://vuejs.org/v2/guide/#Composing-with-Components +[state-management]: https://vuejs.org/v2/guide/state-management.html#Simple-State-Management-from-Scratch +[vue-resource-repo]: https://github.com/pagekit/vue-resource +[issue-boards-service]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/app/assets/javascripts/boards/services/board_service.js.es6 diff --git a/doc/development/rake_tasks.md b/doc/development/rake_tasks.md index 827db7e99b8..dcd978c4cd3 100644 --- a/doc/development/rake_tasks.md +++ b/doc/development/rake_tasks.md @@ -17,14 +17,14 @@ Note: `db:setup` calls `db:seed` but this does nothing. 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 teaspoon` to run the teaspoon test 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: Both `rake spinach` and `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 -full test suite is passed. +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 +full test suite is passed. Note: You can't run `rspec .` since this will try to run all the `_spec.rb` files it can find, also the ones in `/tmp` diff --git a/doc/development/testing.md b/doc/development/testing.md index dbea6b9c9aa..761847b2bab 100644 --- a/doc/development/testing.md +++ b/doc/development/testing.md @@ -31,9 +31,8 @@ GitLab uses [factory_girl] as a test fixture replacement. ## JavaScript -GitLab uses [Teaspoon] to run its [Jasmine] JavaScript specs. They can be run on -the command line via `bundle exec teaspoon`, or via a web browser at -`http://localhost:3000/teaspoon` when the Rails server is running. +GitLab uses [Karma] to run its [Jasmine] JavaScript specs. They can be run on +the command line via `bundle exec karma`. - JavaScript tests live in `spec/javascripts/`, matching the folder structure of `app/assets/javascripts/`: `app/assets/javascripts/behaviors/autosize.js.es6` has a corresponding @@ -51,7 +50,7 @@ the command line via `bundle exec teaspoon`, or via a web browser at [`Notification`](https://developer.mozilla.org/en-US/docs/Web/API/notification), which will have to be stubbed. -[Teaspoon]: https://github.com/modeset/teaspoon +[Karma]: https://github.com/karma-runner/karma [Jasmine]: https://github.com/jasmine/jasmine ## RSpec diff --git a/doc/user/project/img/protected_branches_devs_can_push.png b/doc/user/project/img/protected_branches_devs_can_push.png Binary files differindex 1c05cb8fd36..320e6eb7fee 100644 --- a/doc/user/project/img/protected_branches_devs_can_push.png +++ b/doc/user/project/img/protected_branches_devs_can_push.png diff --git a/lib/api/users.rb b/lib/api/users.rb index 4980a90f952..82ac3886ac3 100644 --- a/lib/api/users.rb +++ b/lib/api/users.rb @@ -82,7 +82,9 @@ module API end params do requires :email, type: String, desc: 'The email of the user' - requires :password, type: String, desc: 'The password of the new user' + optional :password, type: String, desc: 'The password of the new user' + optional :reset_password, type: Boolean, desc: 'Flag indicating the user will be sent a password reset token' + at_least_one_of :password, :reset_password requires :name, type: String, desc: 'The name of the user' requires :username, type: String, desc: 'The username of the user' use :optional_attributes @@ -94,8 +96,18 @@ module API user_params = declared_params(include_missing: false) identity_attrs = user_params.slice(:provider, :extern_uid) confirm = user_params.delete(:confirm) + user = User.new(user_params.except(:extern_uid, :provider, :reset_password)) + + if user_params.delete(:reset_password) + user.attributes = { + force_random_password: true, + password_expires_at: nil, + created_by_id: current_user.id + } + user.generate_password + user.generate_reset_token + end - user = User.new(user_params.except(:extern_uid, :provider)) user.skip_confirmation! unless confirm if identity_attrs.any? diff --git a/spec/controllers/projects/uploads_controller_spec.rb b/spec/controllers/projects/uploads_controller_spec.rb index f1c8891e87a..0347e789576 100644 --- a/spec/controllers/projects/uploads_controller_spec.rb +++ b/spec/controllers/projects/uploads_controller_spec.rb @@ -170,68 +170,24 @@ describe Projects::UploadsController do project.team << [user, :master] end - context "when the user is blocked" do + context "when the file exists" do before do - user.block - project.team << [user, :master] - end - - context "when the file exists" do - before do - allow_any_instance_of(FileUploader).to receive(:file).and_return(jpg) - allow(jpg).to receive(:exists?).and_return(true) - end - - context "when the file is an image" do - before do - allow_any_instance_of(FileUploader).to receive(:image?).and_return(true) - end - - it "responds with status 200" do - go - - expect(response).to have_http_status(200) - end - end - - context "when the file is not an image" do - it "redirects to the sign in page" do - go - - expect(response).to redirect_to(new_user_session_path) - end - end + allow_any_instance_of(FileUploader).to receive(:file).and_return(jpg) + allow(jpg).to receive(:exists?).and_return(true) end - context "when the file doesn't exist" do - it "redirects to the sign in page" do - go + it "responds with status 200" do + go - expect(response).to redirect_to(new_user_session_path) - end + expect(response).to have_http_status(200) end end - context "when the user isn't blocked" do - context "when the file exists" do - before do - allow_any_instance_of(FileUploader).to receive(:file).and_return(jpg) - allow(jpg).to receive(:exists?).and_return(true) - end - - it "responds with status 200" do - go - - expect(response).to have_http_status(200) - end - end - - context "when the file doesn't exist" do - it "responds with status 404" do - go + context "when the file doesn't exist" do + it "responds with status 404" do + go - expect(response).to have_http_status(404) - end + expect(response).to have_http_status(404) end end end diff --git a/spec/factories/users.rb b/spec/factories/users.rb index c6f7869516e..1732b1a0081 100644 --- a/spec/factories/users.rb +++ b/spec/factories/users.rb @@ -14,6 +14,14 @@ FactoryGirl.define do admin true end + trait :blocked do + after(:build) { |user, _| user.block! } + end + + trait :external do + external true + end + trait :two_factor do two_factor_via_otp end diff --git a/spec/features/admin/admin_labels_spec.rb b/spec/features/admin/admin_labels_spec.rb index eaa42aad0a7..6d6c9165c83 100644 --- a/spec/features/admin/admin_labels_spec.rb +++ b/spec/features/admin/admin_labels_spec.rb @@ -35,15 +35,16 @@ RSpec.describe 'admin issues labels' do it 'deletes all labels', js: true do page.within '.labels' do page.all('.btn-remove').each do |remove| - wait_for_ajax remove.click + wait_for_ajax end end - page.within '.manage-labels-list' do - expect(page).not_to have_content('bug') - expect(page).not_to have_content('feature_label') - end + wait_for_ajax + + expect(page).to have_content("There are no labels yet") + expect(page).not_to have_content('bug') + expect(page).not_to have_content('feature_label') end end diff --git a/spec/features/environment_spec.rb b/spec/features/environment_spec.rb index 2f49e89b4e4..c203e1f20c1 100644 --- a/spec/features/environment_spec.rb +++ b/spec/features/environment_spec.rb @@ -101,6 +101,22 @@ feature 'Environment', :feature do scenario 'it shows the terminal button' do expect(page).to have_terminal_button end + + context 'web terminal', :js do + before do + # Stub #terminals as it causes js-enabled feature specs to render the page incorrectly + allow_any_instance_of(Environment).to receive(:terminals) { nil } + visit terminal_namespace_project_environment_path(project.namespace, project, environment) + end + + it 'displays a web terminal' do + expect(page).to have_selector('#terminal') + end + + it 'displays a link to the environment external url' do + expect(page).to have_link(nil, href: environment.external_url) + end + end end context 'for developer' do diff --git a/spec/features/groups/members/list_spec.rb b/spec/features/groups/members/list_spec.rb new file mode 100644 index 00000000000..109de39b2dd --- /dev/null +++ b/spec/features/groups/members/list_spec.rb @@ -0,0 +1,40 @@ +require 'spec_helper' + +feature 'Groups members list', feature: true do + let(:user1) { create(:user, name: 'John Doe') } + let(:user2) { create(:user, name: 'Mary Jane') } + let(:group) { create(:group) } + let(:nested_group) { create(:group, parent: group) } + + background do + login_as(user1) + end + + scenario 'show members from current group and parent' do + group.add_developer(user1) + nested_group.add_developer(user2) + + visit group_group_members_path(nested_group) + + expect(first_row.text).to include(user1.name) + expect(second_row.text).to include(user2.name) + end + + scenario 'show user once if member of both current group and parent' do + group.add_developer(user1) + nested_group.add_developer(user1) + + visit group_group_members_path(nested_group) + + expect(first_row.text).to include(user1.name) + expect(second_row).to be_blank + end + + def first_row + page.all('ul.content-list > li')[0] + end + + def second_row + page.all('ul.content-list > li')[1] + end +end diff --git a/spec/features/login_spec.rb b/spec/features/login_spec.rb index ab7d89306db..ae609160e18 100644 --- a/spec/features/login_spec.rb +++ b/spec/features/login_spec.rb @@ -32,6 +32,22 @@ feature 'Login', feature: true do end end + describe 'with a blocked account' do + it 'prevents the user from logging in' do + user = create(:user, :blocked) + + login_with(user) + + expect(page).to have_content('Your account has been blocked.') + end + + it 'does not update Devise trackable attributes' do + user = create(:user, :blocked) + + expect { login_with(user) }.not_to change { user.reload.sign_in_count } + end + end + describe 'with two-factor authentication' do def enter_code(code) fill_in 'user_otp_attempt', with: code diff --git a/spec/features/merge_requests/mini_pipeline_graph_spec.rb b/spec/features/merge_requests/mini_pipeline_graph_spec.rb index b08bd36bde9..84ad8765d8f 100644 --- a/spec/features/merge_requests/mini_pipeline_graph_spec.rb +++ b/spec/features/merge_requests/mini_pipeline_graph_spec.rb @@ -66,7 +66,7 @@ feature 'Mini Pipeline Graph', :js, :feature do end it 'should close when toggle is clicked again' do - toggle.click + toggle.trigger('click') expect(toggle.find(:xpath, '..')).not_to have_selector('.mini-pipeline-graph-dropdown-menu') end diff --git a/spec/finders/group_members_finder_spec.rb b/spec/finders/group_members_finder_spec.rb new file mode 100644 index 00000000000..b762756f9ce --- /dev/null +++ b/spec/finders/group_members_finder_spec.rb @@ -0,0 +1,32 @@ +require 'spec_helper' + +describe GroupMembersFinder, '#execute' do + let(:group) { create(:group) } + let(:nested_group) { create(:group, :access_requestable, parent: group) } + let(:user1) { create(:user) } + let(:user2) { create(:user) } + let(:user3) { create(:user) } + let(:user4) { create(:user) } + + it 'returns members for top-level group' do + member1 = group.add_master(user1) + member2 = group.add_master(user2) + member3 = group.add_master(user3) + + result = described_class.new(group).execute + + expect(result.to_a).to eq([member3, member2, member1]) + end + + it 'returns members for nested group' do + group.add_master(user2) + nested_group.request_access(user4) + member1 = group.add_master(user1) + member3 = nested_group.add_master(user2) + member4 = nested_group.add_master(user3) + + result = described_class.new(nested_group).execute + + expect(result.to_a).to eq([member4, member3, member1]) + end +end diff --git a/spec/javascripts/behaviors/quick_submit_spec.js b/spec/javascripts/behaviors/quick_submit_spec.js index b84126c0e3d..1541037888f 100644 --- a/spec/javascripts/behaviors/quick_submit_spec.js +++ b/spec/javascripts/behaviors/quick_submit_spec.js @@ -40,7 +40,7 @@ require('~/behaviors/quick_submit'); expect($('input[type=submit]')).toBeDisabled(); return expect($('button[type=submit]')).toBeDisabled(); }); - // We cannot stub `navigator.userAgent` for CI's `rake teaspoon` task, so we'll + // We cannot stub `navigator.userAgent` for CI's `rake karma` task, so we'll // only run the tests that apply to the current platform if (navigator.userAgent.match(/Macintosh/)) { it('responds to Meta+Enter', function() { diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb index 008c15c4de3..68ad429608d 100644 --- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb +++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb @@ -4,6 +4,16 @@ module Ci describe GitlabCiYamlProcessor, lib: true do let(:path) { 'path' } + describe 'our current .gitlab-ci.yml' do + let(:config) { File.read("#{Rails.root}/.gitlab-ci.yml") } + + it 'is valid' do + error_message = described_class.validation_message(config) + + expect(error_message).to be_nil + end + end + describe '#build_attributes' do describe 'coverage entry' do subject { described_class.new(config, path).build_attributes(:rspec) } diff --git a/spec/models/member_spec.rb b/spec/models/member_spec.rb index 16e2144d6a1..c720cc9f2c2 100644 --- a/spec/models/member_spec.rb +++ b/spec/models/member_spec.rb @@ -129,6 +129,14 @@ describe Member, models: true do it { expect(described_class.request).not_to include @accepted_request_member } end + describe '.non_request' do + it { expect(described_class.non_request).to include @master } + it { expect(described_class.non_request).to include @invited_member } + it { expect(described_class.non_request).to include @accepted_invite_member } + it { expect(described_class.non_request).not_to include @requested_member } + it { expect(described_class.non_request).to include @accepted_request_member } + end + describe '.developers' do subject { described_class.developers.to_a } diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb index 15592f1f702..f78bde6f53a 100644 --- a/spec/requests/api/groups_spec.rb +++ b/spec/requests/api/groups_spec.rb @@ -35,7 +35,8 @@ describe API::Groups, api: true do expect(response).to have_http_status(200) expect(json_response).to be_an Array expect(json_response.length).to eq(1) - expect(json_response.first['name']).to eq(group1.name) + expect(json_response) + .to satisfy_one { |group| group['name'] == group1.name } end it "does not include statistics" do @@ -70,7 +71,7 @@ describe API::Groups, api: true do repository_size: 123, lfs_objects_size: 234, build_artifacts_size: 345, - } + }.stringify_keys project1.statistics.update!(attributes) @@ -78,7 +79,8 @@ describe API::Groups, api: true do expect(response).to have_http_status(200) expect(json_response).to be_an Array - expect(json_response.first['statistics']).to eq attributes.stringify_keys + expect(json_response) + .to satisfy_one { |group| group['statistics'] == attributes } end end diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index 8692f9da976..5958012672e 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -190,6 +190,18 @@ describe API::Users, api: true do expect(new_user.external).to be_truthy end + it "creates user with reset password" do + post api('/users', admin), attributes_for(:user, reset_password: true).except(:password) + + expect(response).to have_http_status(201) + + user_id = json_response['id'] + new_user = User.find(user_id) + + expect(new_user).not_to eq(nil) + expect(new_user.recently_sent_password_reset?).to eq(true) + end + it "does not create user with invalid email" do post api('/users', admin), email: 'invalid email', diff --git a/spec/support/matchers/satisfy_matchers.rb b/spec/support/matchers/satisfy_matchers.rb new file mode 100644 index 00000000000..585915bac93 --- /dev/null +++ b/spec/support/matchers/satisfy_matchers.rb @@ -0,0 +1,19 @@ +# These matchers are a syntactic hack to provide more readable expectations for +# an Enumerable object. +# +# They take advantage of the `all?`, `none?`, and `one?` methods, and the fact +# that RSpec provides a `be_something` matcher for all predicates. +# +# Example: +# +# # Ensure exactly one object in an Array satisfies a condition +# expect(users.one? { |u| u.admin? }).to eq true +# +# # The same thing, but using the `be_one` matcher +# expect(users).to be_one { |u| u.admin? } +# +# # The same thing again, but using `satisfy_one` for improved readability +# expect(users).to satisfy_one { |u| u.admin? } +RSpec::Matchers.alias_matcher :satisfy_all, :be_all +RSpec::Matchers.alias_matcher :satisfy_none, :be_none +RSpec::Matchers.alias_matcher :satisfy_one, :be_one |