Merge pull request #4950 from opf/feature/24062/user-create-api
[24062] Add POST api/v3/userspull/4963/head
commit
f3e7210813
@ -0,0 +1,52 @@ |
||||
#-- encoding: UTF-8 |
||||
#-- copyright |
||||
# OpenProject is a project management system. |
||||
# Copyright (C) 2012-2015 the OpenProject Foundation (OPF) |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License version 3. |
||||
# |
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: |
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang |
||||
# Copyright (C) 2010-2013 the ChiliProject Team |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License |
||||
# as published by the Free Software Foundation; either version 2 |
||||
# of the License, or (at your option) any later version. |
||||
# |
||||
# This program is distributed in the hope that it will be useful, |
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
# GNU General Public License for more details. |
||||
# |
||||
# You should have received a copy of the GNU General Public License |
||||
# along with this program; if not, write to the Free Software |
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||||
# |
||||
# See doc/COPYRIGHT.rdoc for more details. |
||||
#++ |
||||
|
||||
require 'model_contract' |
||||
|
||||
module Users |
||||
class BaseContract < ::ModelContract |
||||
attribute :type |
||||
attribute :login |
||||
attribute :firstname |
||||
attribute :lastname |
||||
attribute :name |
||||
attribute :mail |
||||
attribute :status |
||||
|
||||
def initialize(user, current_user) |
||||
super(user) |
||||
|
||||
@current_user = current_user |
||||
end |
||||
|
||||
private |
||||
|
||||
attr_reader :current_user |
||||
end |
||||
end |
@ -0,0 +1,58 @@ |
||||
#-- encoding: UTF-8 |
||||
#-- copyright |
||||
# OpenProject is a project management system. |
||||
# Copyright (C) 2012-2015 the OpenProject Foundation (OPF) |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License version 3. |
||||
# |
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: |
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang |
||||
# Copyright (C) 2010-2013 the ChiliProject Team |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License |
||||
# as published by the Free Software Foundation; either version 2 |
||||
# of the License, or (at your option) any later version. |
||||
# |
||||
# This program is distributed in the hope that it will be useful, |
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
# GNU General Public License for more details. |
||||
# |
||||
# You should have received a copy of the GNU General Public License |
||||
# along with this program; if not, write to the Free Software |
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||||
# |
||||
# See doc/COPYRIGHT.rdoc for more details. |
||||
#++ |
||||
|
||||
require 'users/base_contract' |
||||
|
||||
module Users |
||||
class CreateContract < BaseContract |
||||
validate :user_allowed_to_add |
||||
|
||||
attribute :password do |
||||
# when user's are created as 'active', a password must be set |
||||
errors.add :password, :blank if model.active? && model.password.blank? |
||||
end |
||||
|
||||
attribute :status do |
||||
unless model.active? || model.invited? |
||||
# New users may only have these two statuses |
||||
errors.add :status, :invalid_on_create |
||||
end |
||||
end |
||||
|
||||
private |
||||
|
||||
## |
||||
# Users can only be created by Admins |
||||
def user_allowed_to_add |
||||
unless current_user.admin? |
||||
errors.add :base, :error_unauthorized |
||||
end |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,106 @@ |
||||
#-- encoding: UTF-8 |
||||
#-- copyright |
||||
# OpenProject is a project management system. |
||||
# Copyright (C) 2012-2015 the OpenProject Foundation (OPF) |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License version 3. |
||||
# |
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: |
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang |
||||
# Copyright (C) 2010-2013 the ChiliProject Team |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License |
||||
# as published by the Free Software Foundation; either version 2 |
||||
# of the License, or (at your option) any later version. |
||||
# |
||||
# This program is distributed in the hope that it will be useful, |
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
# GNU General Public License for more details. |
||||
# |
||||
# You should have received a copy of the GNU General Public License |
||||
# along with this program; if not, write to the Free Software |
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||||
# |
||||
# See doc/COPYRIGHT.rdoc for more details. |
||||
#++ |
||||
|
||||
require 'work_packages/create_contract' |
||||
require 'concerns/user_invitation' |
||||
|
||||
module Users |
||||
class CreateUserService |
||||
include Concerns::Contracted |
||||
|
||||
self.contract = Users::CreateContract |
||||
|
||||
attr_reader :current_user |
||||
|
||||
def initialize(current_user:) |
||||
@current_user = current_user |
||||
end |
||||
|
||||
def call(new_user) |
||||
User.execute_as current_user do |
||||
create(new_user) |
||||
end |
||||
end |
||||
|
||||
private |
||||
|
||||
def create(new_user) |
||||
initialize_contract(new_user) |
||||
|
||||
return create_regular(new_user) unless new_user.invited? |
||||
|
||||
# As we're basing on the user's mail, this parameter is required |
||||
# before we're able to validate the contract or user |
||||
if new_user.mail.blank? |
||||
contract.errors.add :mail, :blank |
||||
build_result(new_user, contract.errors) |
||||
else |
||||
create_invited(new_user) |
||||
end |
||||
end |
||||
|
||||
def build_result(result, errors) |
||||
success = result.is_a?(User) && errors.empty? |
||||
ServiceResult.new(success: success, errors: errors, result: result) |
||||
end |
||||
|
||||
## |
||||
# Create regular user |
||||
def create_regular(new_user) |
||||
result, errors = validate_and_save(new_user) |
||||
ServiceResult.new(success: result, errors: errors, result: new_user) |
||||
end |
||||
|
||||
## |
||||
# User creation flow for users that are invited. |
||||
# Re-uses UserInvitation and thus avoids +validate_and_save+ |
||||
def create_invited(new_user) |
||||
# Assign values other than mail to new_user |
||||
::UserInvitation.assign_user_attributes new_user |
||||
|
||||
# Check contract validity before moving to UserInvitation |
||||
if !contract.validate |
||||
build_result(new_user, contract.errors) |
||||
end |
||||
|
||||
invite_user! new_user |
||||
end |
||||
|
||||
def invite_user!(new_user) |
||||
invited = ::UserInvitation.invite_user! new_user |
||||
new_user.errors.add :base, I18n.t(:error_can_not_invite_user) unless invited.is_a? User |
||||
|
||||
build_result(invited, new_user.errors) |
||||
end |
||||
|
||||
def initialize_contract(new_user) |
||||
self.contract = self.class.contract.new(new_user, current_user) |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,66 @@ |
||||
#-- copyright |
||||
# OpenProject is a project management system. |
||||
# Copyright (C) 2012-2015 the OpenProject Foundation (OPF) |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License version 3. |
||||
# |
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: |
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang |
||||
# Copyright (C) 2010-2013 the ChiliProject Team |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License |
||||
# as published by the Free Software Foundation; either version 2 |
||||
# of the License, or (at your option) any later version. |
||||
# |
||||
# This program is distributed in the hope that it will be useful, |
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
# GNU General Public License for more details. |
||||
# |
||||
# You should have received a copy of the GNU General Public License |
||||
# along with this program; if not, write to the Free Software |
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||||
# |
||||
# See doc/COPYRIGHT.rdoc for more details. |
||||
#++ |
||||
|
||||
require 'api/v3/users/user_representer' |
||||
require 'users/create_user_service' |
||||
|
||||
module API |
||||
module V3 |
||||
module Users |
||||
module CreateUser |
||||
extend Grape::API::Helpers |
||||
## |
||||
# Call the user create service for the current request |
||||
# and return the service result API representation |
||||
def create_user(request_body, current_user) |
||||
payload = ::API::V3::Users::UserRepresenter.create(User.new, current_user: current_user) |
||||
new_user = payload.from_hash(request_body) |
||||
|
||||
result = call_service(new_user, current_user) |
||||
represent_service_result(result, current_user) |
||||
end |
||||
|
||||
private |
||||
|
||||
def represent_service_result(result, current_user) |
||||
if result.success? |
||||
status 201 |
||||
::API::V3::Users::UserRepresenter.create(result.result, current_user: current_user) |
||||
else |
||||
fail ::API::Errors::ErrorBase.create_and_merge_errors(result.errors) |
||||
end |
||||
end |
||||
|
||||
def call_service(new_user, current_user) |
||||
create_service = ::Users::CreateUserService.new(current_user: current_user) |
||||
create_service.call(new_user) |
||||
end |
||||
end |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,83 @@ |
||||
#-- encoding: UTF-8 |
||||
#-- copyright |
||||
# OpenProject is a project management system. |
||||
# Copyright (C) 2012-2015 the OpenProject Foundation (OPF) |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License version 3. |
||||
# |
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: |
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang |
||||
# Copyright (C) 2010-2013 the ChiliProject Team |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License |
||||
# as published by the Free Software Foundation; either version 2 |
||||
# of the License, or (at your option) any later version. |
||||
# |
||||
# This program is distributed in the hope that it will be useful, |
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
# GNU General Public License for more details. |
||||
# |
||||
# You should have received a copy of the GNU General Public License |
||||
# along with this program; if not, write to the Free Software |
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||||
# |
||||
# See doc/COPYRIGHT.rdoc for more details. |
||||
#++ |
||||
|
||||
require 'spec_helper' |
||||
|
||||
describe Users::CreateContract do |
||||
let(:user) { FactoryGirl.build_stubbed(:user) } |
||||
|
||||
subject(:contract) { described_class.new(user, current_user) } |
||||
|
||||
def expect_valid(valid, symbols = {}) |
||||
expect(contract.validate).to eq(valid) |
||||
|
||||
symbols.each do |key, arr| |
||||
expect(contract.errors.symbols_for(key)).to match_array arr |
||||
end |
||||
end |
||||
|
||||
shared_examples 'is valid' do |
||||
it 'is valid' do |
||||
expect_valid(true) |
||||
end |
||||
end |
||||
|
||||
context 'when admin' do |
||||
let(:current_user) { FactoryGirl.build_stubbed(:admin) } |
||||
|
||||
it_behaves_like 'is valid' |
||||
|
||||
describe 'requires a password set when active' do |
||||
before do |
||||
user.password = nil |
||||
user.activate |
||||
end |
||||
|
||||
it 'is invalid' do |
||||
expect_valid(false, password: %i(blank)) |
||||
end |
||||
|
||||
context 'when password is set' do |
||||
before do |
||||
user.password = user.password_confirmation = 'password!password!' |
||||
end |
||||
|
||||
it_behaves_like 'is valid' |
||||
end |
||||
end |
||||
end |
||||
|
||||
context 'when not admin' do |
||||
let(:current_user) { FactoryGirl.build_stubbed(:user) } |
||||
|
||||
it 'is invalid' do |
||||
expect_valid(false, base: %i(error_unauthorized)) |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,206 @@ |
||||
#-- encoding: UTF-8 |
||||
#-- copyright |
||||
# OpenProject is a project management system. |
||||
# Copyright (C) 2012-2015 the OpenProject Foundation (OPF) |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License version 3. |
||||
# |
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: |
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang |
||||
# Copyright (C) 2010-2013 the ChiliProject Team |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License |
||||
# as published by the Free Software Foundation; either version 2 |
||||
# of the License, or (at your option) any later version. |
||||
# |
||||
# This program is distributed in the hope that it will be useful, |
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
# GNU General Public License for more details. |
||||
# |
||||
# You should have received a copy of the GNU General Public License |
||||
# along with this program; if not, write to the Free Software |
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||||
# |
||||
# See doc/COPYRIGHT.rdoc for more details. |
||||
|
||||
require 'spec_helper' |
||||
require 'rack/test' |
||||
|
||||
describe ::API::V3::Users::UsersAPI, type: :request do |
||||
include API::V3::Utilities::PathHelper |
||||
|
||||
let(:path) { api_v3_paths.users } |
||||
let(:user) { FactoryGirl.build(:admin) } |
||||
let(:parameters) { {} } |
||||
|
||||
before do |
||||
login_as(user) |
||||
end |
||||
|
||||
def send_request |
||||
post path, params: parameters.to_json, headers: { 'Content-Type' => 'application/json' } |
||||
end |
||||
|
||||
let(:errors) { parse_json(response.body)['_embedded']['errors'] } |
||||
|
||||
shared_context 'represents the created user' do |expected_attributes| |
||||
it 'returns the represented user' do |
||||
send_request |
||||
|
||||
expect(response.status).to eq(201) |
||||
expect(response.body).to have_json_type(Object).at_path('_links') |
||||
expect(response.body) |
||||
.to be_json_eql('User'.to_json) |
||||
.at_path('_type') |
||||
|
||||
parameters.merge!(expected_attributes) if expected_attributes |
||||
|
||||
user = User.find_by!(login: parameters.fetch(:login, parameters[:email])) |
||||
expect(user.firstname).to eq(parameters[:firstName]) |
||||
expect(user.lastname).to eq(parameters[:lastName]) |
||||
expect(user.mail).to eq(parameters[:email]) |
||||
end |
||||
end |
||||
|
||||
describe 'empty request body' do |
||||
it 'returns an erroneous response' do |
||||
send_request |
||||
|
||||
expect(response.status).to eq(422) |
||||
|
||||
expect(errors.count).to eq(5) |
||||
expect(errors.collect { |el| el['_embedded']['details']['attribute'] }) |
||||
.to contain_exactly('password', 'login', 'firstname', 'lastname', 'email') |
||||
|
||||
expect(response.body) |
||||
.to be_json_eql('urn:openproject-org:api:v3:errors:MultipleErrors'.to_json) |
||||
.at_path('errorIdentifier') |
||||
end |
||||
end |
||||
|
||||
describe 'active status' do |
||||
let(:status) { 'active' } |
||||
let(:password) { 'admin!admin!' } |
||||
let(:parameters) { |
||||
{ |
||||
status: status, |
||||
login: 'myusername', |
||||
firstName: 'Foo', |
||||
lastName: 'Bar', |
||||
email: 'foobar@example.org', |
||||
password: password |
||||
} |
||||
} |
||||
|
||||
it 'returns the represented user' do |
||||
send_request |
||||
|
||||
expect(response.body).not_to have_json_path("_embedded/errors") |
||||
expect(response.body).to have_json_type(Object).at_path('_links') |
||||
expect(response.body) |
||||
.to be_json_eql('User'.to_json) |
||||
.at_path('_type') |
||||
end |
||||
|
||||
it_behaves_like 'represents the created user' |
||||
|
||||
context 'empty password' do |
||||
let(:password) { '' } |
||||
|
||||
it 'marks the password missing and too short' do |
||||
send_request |
||||
|
||||
expect(errors.count).to eq(2) |
||||
expect(errors.collect { |el| el['_embedded']['details']['attribute'] }) |
||||
.to match_array %w(password password) |
||||
end |
||||
end |
||||
end |
||||
|
||||
describe 'invited status' do |
||||
let(:status) { 'invited' } |
||||
let(:invitation_request) { |
||||
{ |
||||
status: status, |
||||
email: 'foo@example.org' |
||||
} |
||||
} |
||||
|
||||
describe 'invitation successful' do |
||||
before do |
||||
expect(OpenProject::Notifications).to receive(:send) do |event, _| |
||||
expect(event).to eq 'user_invited' |
||||
end |
||||
end |
||||
|
||||
context 'only mail set' do |
||||
let(:parameters) { invitation_request } |
||||
|
||||
it_behaves_like 'represents the created user', |
||||
firstName: 'foo', |
||||
lastName: '@example.org' |
||||
|
||||
it 'sets the other attributes' do |
||||
send_request |
||||
|
||||
user = User.find_by!(login: 'foo@example.org') |
||||
expect(user.firstname).to eq('foo') |
||||
expect(user.lastname).to eq('@example.org') |
||||
expect(user.mail).to eq('foo@example.org') |
||||
end |
||||
end |
||||
|
||||
context 'mail and name set' do |
||||
let(:parameters) { invitation_request.merge(firstName: 'First', lastName: 'Last') } |
||||
|
||||
it_behaves_like 'represents the created user' |
||||
end |
||||
end |
||||
|
||||
context 'missing email' do |
||||
let(:parameters) { { status: status } } |
||||
|
||||
it 'marks the mail as missing' do |
||||
send_request |
||||
|
||||
expect(response.body) |
||||
.to be_json_eql('urn:openproject-org:api:v3:errors:PropertyConstraintViolation'.to_json) |
||||
.at_path('errorIdentifier') |
||||
expect(response.body) |
||||
.to be_json_eql('email'.to_json) |
||||
.at_path('_embedded/details/attribute') |
||||
end |
||||
end |
||||
end |
||||
|
||||
describe 'invalid status' do |
||||
let(:parameters) { { status: 'blablu' } } |
||||
|
||||
it 'returns an erroneous response' do |
||||
send_request |
||||
|
||||
expect(response.status).to eq(422) |
||||
|
||||
expect(errors).not_to be_empty |
||||
expect(response.body) |
||||
.to be_json_eql('urn:openproject-org:api:v3:errors:MultipleErrors'.to_json) |
||||
.at_path('errorIdentifier') |
||||
|
||||
expect(errors.collect { |el| el['message'] }) |
||||
.to include 'Status is not a valid status for new users.' |
||||
end |
||||
end |
||||
|
||||
describe 'unauthorized user' do |
||||
let(:user) { FactoryGirl.build(:user) } |
||||
let(:parameters) { { status: 'invited', email: 'foo@example.org' } } |
||||
|
||||
it 'returns an erroneous response' do |
||||
send_request |
||||
expect(response.status).to eq(403) |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,98 @@ |
||||
#-- encoding: UTF-8 |
||||
#-- copyright |
||||
# OpenProject is a project management system. |
||||
# Copyright (C) 2012-2015 the OpenProject Foundation (OPF) |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License version 3. |
||||
# |
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: |
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang |
||||
# Copyright (C) 2010-2013 the ChiliProject Team |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License |
||||
# as published by the Free Software Foundation; either version 2 |
||||
# of the License, or (at your option) any later version. |
||||
# |
||||
# This program is distributed in the hope that it will be useful, |
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
# GNU General Public License for more details. |
||||
# |
||||
# You should have received a copy of the GNU General Public License |
||||
# along with this program; if not, write to the Free Software |
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||||
# |
||||
# See doc/COPYRIGHT.rdoc for more details. |
||||
|
||||
require 'spec_helper' |
||||
|
||||
describe Users::CreateUserService do |
||||
let(:current_user) { FactoryGirl.build_stubbed(:user) } |
||||
let(:new_user) { FactoryGirl.build_stubbed(:user) } |
||||
let(:instance) { described_class.new(current_user: current_user) } |
||||
|
||||
describe '.contract' do |
||||
it 'uses the CreateContract contract' do |
||||
expect(described_class.contract).to eql Users::CreateContract |
||||
end |
||||
end |
||||
|
||||
describe '.new' do |
||||
it 'takes a user which is available as a getter' do |
||||
expect(instance.current_user).to eql current_user |
||||
end |
||||
end |
||||
|
||||
describe '#call' do |
||||
subject { instance.call(new_user) } |
||||
let(:validates) { true } |
||||
let(:saves) { true } |
||||
|
||||
before do |
||||
allow(new_user).to receive(:save).and_return(saves) |
||||
allow_any_instance_of(Users::CreateContract).to receive(:validate).and_return(validates) |
||||
end |
||||
|
||||
context 'if contract validates and the user saves' do |
||||
it 'is successful' do |
||||
expect(subject).to be_success |
||||
end |
||||
|
||||
it 'has no errors' do |
||||
expect(subject.errors).to be_empty |
||||
end |
||||
|
||||
it 'returns the user as a result' do |
||||
result = subject.result |
||||
expect(result).to be_a User |
||||
end |
||||
end |
||||
|
||||
context 'if contract does not validate' do |
||||
let(:validates) { false } |
||||
|
||||
it 'is unsuccessful' do |
||||
expect(subject).to_not be_success |
||||
end |
||||
end |
||||
|
||||
context 'if user does not save' do |
||||
let(:saves) { false } |
||||
let(:errors) { double('errors') } |
||||
|
||||
it 'is unsuccessful' do |
||||
expect(subject).to_not be_success |
||||
end |
||||
|
||||
it "returns the user's errors" do |
||||
allow(new_user) |
||||
.to receive(:errors) |
||||
.and_return errors |
||||
|
||||
expect(subject.errors).to eql errors |
||||
end |
||||
end |
||||
end |
||||
end |
Loading…
Reference in new issue