graphql_devise 0.16.0 → 0.17.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphqlDevise
4
+ module Mutations
5
+ class ResendConfirmationWithToken < Base
6
+ argument :email, String, required: true
7
+ argument :confirm_url, String, required: true
8
+
9
+ field :message, String, null: false
10
+
11
+ def resolve(email:, confirm_url:)
12
+ check_redirect_url_whitelist!(confirm_url)
13
+
14
+ resource = find_confirmable_resource(email)
15
+
16
+ if resource
17
+ yield resource if block_given?
18
+
19
+ if resource.confirmed? && !resource.pending_reconfirmation?
20
+ raise_user_error(I18n.t('graphql_devise.confirmations.already_confirmed'))
21
+ end
22
+
23
+ resource.send_confirmation_instructions(
24
+ redirect_url: confirm_url,
25
+ template_path: ['graphql_devise/mailer']
26
+ )
27
+
28
+ { message: I18n.t('graphql_devise.confirmations.send_instructions', email: email) }
29
+ else
30
+ raise_user_error(I18n.t('graphql_devise.confirmations.user_not_found', email: email))
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ def find_confirmable_resource(email)
37
+ email_insensitive = get_case_insensitive_field(:email, email)
38
+ resource = find_resource(:unconfirmed_email, email_insensitive) if resource_class.reconfirmable
39
+ resource ||= find_resource(:email, email_insensitive)
40
+ resource
41
+ end
42
+ end
43
+ end
44
+ end
@@ -31,7 +31,7 @@ module GraphqlDevise
31
31
 
32
32
  unless resource.confirmed?
33
33
  resource.send_confirmation_instructions(
34
- redirect_url: confirm_success_url,
34
+ redirect_url: redirect_url,
35
35
  template_path: ['graphql_devise/mailer'],
36
36
  schema_url: controller.full_url_without_params
37
37
  )
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module GraphqlDevise
4
- VERSION = '0.16.0'.freeze
4
+ VERSION = '0.17.0'.freeze
5
5
  end
@@ -7,17 +7,18 @@ class DummySchema < GraphQL::Schema
7
7
  public_introspection: true,
8
8
  resource_loaders: [
9
9
  GraphqlDevise::ResourceLoader.new(
10
- 'User',
10
+ User,
11
11
  only: [
12
12
  :login,
13
13
  :confirm_account,
14
14
  :send_password_reset,
15
15
  :resend_confirmation,
16
+ :resend_confirmation_with_token,
16
17
  :check_password_token
17
18
  ]
18
19
  ),
19
- GraphqlDevise::ResourceLoader.new('Guest', only: [:logout]),
20
- GraphqlDevise::ResourceLoader.new('SchemaUser')
20
+ GraphqlDevise::ResourceLoader.new(Guest, only: [:logout]),
21
+ GraphqlDevise::ResourceLoader.new(SchemaUser)
21
22
  ]
22
23
  )
23
24
 
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mutations
4
+ class Register < GraphqlDevise::Mutations::Register
5
+ argument :name, String, required: false
6
+
7
+ field :user, Types::UserType, null: true
8
+
9
+ def resolve(email:, **attrs)
10
+ original_payload = super
11
+ original_payload.merge(user: original_payload[:authenticatable])
12
+ end
13
+ end
14
+ end
@@ -2,8 +2,9 @@
2
2
 
3
3
  Rails.application.routes.draw do
4
4
  mount_graphql_devise_for 'User', at: '/api/v1/graphql_auth', operations: {
5
- login: Mutations::Login,
6
- sign_up: Mutations::SignUp
5
+ login: Mutations::Login,
6
+ sign_up: Mutations::SignUp,
7
+ register: Mutations::Register
7
8
  }, additional_mutations: {
8
9
  register_confirmed_user: Mutations::RegisterConfirmedUser
9
10
  }, additional_queries: {
@@ -13,7 +14,7 @@ Rails.application.routes.draw do
13
14
  mount_graphql_devise_for(
14
15
  Admin,
15
16
  authenticatable_type: Types::CustomAdminType,
16
- skip: [:sign_up, :check_password_token],
17
+ skip: [:sign_up, :register, :check_password_token],
17
18
  operations: {
18
19
  confirm_account: Resolvers::ConfirmAdminAccount,
19
20
  update_password_with_token: Mutations::ResetAdminPasswordWithToken
@@ -23,7 +24,7 @@ Rails.application.routes.draw do
23
24
 
24
25
  mount_graphql_devise_for(
25
26
  'Guest',
26
- only: [:login, :logout, :sign_up],
27
+ only: [:login, :logout, :sign_up, :register],
27
28
  at: '/api/v1/guest/graphql_auth'
28
29
  )
29
30
 
@@ -4,6 +4,57 @@ require 'rails_helper'
4
4
 
5
5
  RSpec.describe GraphqlDevise::Model::WithEmailUpdater do
6
6
  describe '#call' do
7
+ shared_examples 'all required arguments are provided' do |base_attributes|
8
+ let(:attributes) { base_attributes.merge(email: 'new@gmail.com', name: 'Updated Name') }
9
+
10
+ it 'postpones email update' do
11
+ expect do
12
+ updater
13
+ resource.reload
14
+ end.to not_change(resource, :email).from(resource.email).and(
15
+ not_change(resource, :uid).from(resource.uid)
16
+ ).and(
17
+ change(resource, :unconfirmed_email).from(nil).to('new@gmail.com')
18
+ ).and(
19
+ change(resource, :name).from(resource.name).to('Updated Name')
20
+ )
21
+ end
22
+
23
+ it 'sends out a confirmation email to the unconfirmed_email' do
24
+ expect { updater }.to change(ActionMailer::Base.deliveries, :count).by(1)
25
+
26
+ email = ActionMailer::Base.deliveries.first
27
+ expect(email.to).to contain_exactly('new@gmail.com')
28
+ end
29
+
30
+ context 'when email value is the same on the DB' do
31
+ let(:attributes) { base_attributes.merge(email: resource.email, name: 'changed') }
32
+
33
+ it 'updates attributes and does not send confirmation email' do
34
+ expect do
35
+ updater
36
+ resource.reload
37
+ end.to change(resource, :name).from(resource.name).to('changed').and(
38
+ not_change(resource, :email).from(resource.email)
39
+ ).and(
40
+ not_change(ActionMailer::Base.deliveries, :count).from(0)
41
+ )
42
+ end
43
+ end
44
+
45
+ context 'when provided params are invalid' do
46
+ let(:attributes) { base_attributes.merge(email: 'newgmail.com', name: '') }
47
+
48
+ it 'returns false and adds errors to the model' do
49
+ expect(updater).to be_falsey
50
+ expect(resource.errors.full_messages).to contain_exactly(
51
+ 'Email is not an email',
52
+ "Name can't be blank"
53
+ )
54
+ end
55
+ end
56
+ end
57
+
7
58
  subject(:updater) { described_class.new(resource, attributes).call }
8
59
 
9
60
  context 'when the model does not have an unconfirmed_email column' do
@@ -38,90 +89,68 @@ RSpec.describe GraphqlDevise::Model::WithEmailUpdater do
38
89
  end
39
90
 
40
91
  context 'when attributes contain email' do
41
- context 'when schema_url is missing' do
42
- let(:attributes) { { email: 'new@gmail.com', name: 'Updated Name' } }
43
-
44
- it 'raises an error' do
45
- expect { updater }.to raise_error(
46
- GraphqlDevise::Error,
47
- 'Method `update_with_email` requires attributes `confirmation_success_url` and `schema_url` for email reconfirmation to work'
48
- )
49
- end
92
+ context 'when confirmation_success_url is used' do
93
+ it_behaves_like 'all required arguments are provided', schema_url: 'http://localhost/test', confirmation_success_url: 'https://google.com'
50
94
 
51
- context 'when email will not change' do
52
- let(:attributes) { { email: resource.email, name: 'changed' } }
53
-
54
- it 'updates name and does not raise an error' do
55
- expect do
56
- updater
57
- resource.reload
58
- end.to change(resource, :name).from(resource.name).to('changed').and(
59
- not_change(resource, :email).from(resource.email)
60
- ).and(
61
- not_change(ActionMailer::Base.deliveries, :count).from(0)
95
+ context 'when confirmation_success_url is missing and no default is set' do
96
+ let(:attributes) { { email: 'new@gmail.com', name: 'Updated Name', schema_url: 'http://localhost/test' } }
97
+
98
+ before { allow(DeviseTokenAuth).to receive(:default_confirm_success_url).and_return(nil) }
99
+
100
+ it 'raises an error' do
101
+ expect { updater }.to raise_error(
102
+ GraphqlDevise::Error,
103
+ 'Method `update_with_email` requires attribute `confirmation_url` for email reconfirmation to work'
62
104
  )
63
105
  end
106
+
107
+ context 'when email will not change' do
108
+ let(:attributes) { { email: resource.email, name: 'changed', confirmation_success_url: 'https://google.com' } }
109
+
110
+ it 'updates name and does not raise an error' do
111
+ expect do
112
+ updater
113
+ resource.reload
114
+ end.to change(resource, :name).from(resource.name).to('changed').and(
115
+ not_change(resource, :email).from(resource.email)
116
+ ).and(
117
+ not_change(ActionMailer::Base.deliveries, :count).from(0)
118
+ )
119
+ end
120
+ end
64
121
  end
65
122
  end
66
123
 
67
- context 'when only confirmation_success_url is missing' do
68
- let(:attributes) { { email: 'new@gmail.com', name: 'Updated Name', schema_url: 'http://localhost/test' } }
124
+ context 'when confirm_url is used' do
125
+ it_behaves_like 'all required arguments are provided', confirmation_url: 'https://google.com'
69
126
 
70
- it 'uses DTA default_confirm_success_url on the email' do
71
- expect { updater }.to change(ActionMailer::Base.deliveries, :count).by(1)
72
-
73
- email = ActionMailer::Base.deliveries.first
74
- expect(email.body.decoded).to include(CGI.escape('https://google.com'))
127
+ context 'when arguments hash has strings as keys' do
128
+ it_behaves_like 'all required arguments are provided', 'confirmation_url' => 'https://google.com'
75
129
  end
76
130
  end
77
131
 
78
- context 'when both required urls are provided' do
79
- let(:attributes) { { email: 'new@gmail.com', name: 'Updated Name', schema_url: 'http://localhost/test', confirmation_success_url: 'https://google.com' } }
80
-
81
- it 'postpones email update' do
82
- expect do
83
- updater
84
- resource.reload
85
- end.to not_change(resource, :email).from(resource.email).and(
86
- not_change(resource, :uid).from(resource.uid)
87
- ).and(
88
- change(resource, :unconfirmed_email).from(nil).to('new@gmail.com')
89
- ).and(
90
- change(resource, :name).from(resource.name).to('Updated Name')
91
- )
92
- end
93
-
94
- it 'sends out a confirmation email to the unconfirmed_email' do
95
- expect { updater }.to change(ActionMailer::Base.deliveries, :count).by(1)
132
+ context 'when no confirmation url is provided is provided' do
133
+ context 'when schema_url is provided' do
134
+ let(:attributes) { { email: 'new@gmail.com', name: 'Updated Name', schema_url: 'http://localhost/test' } }
96
135
 
97
- email = ActionMailer::Base.deliveries.first
98
- expect(email.to).to contain_exactly('new@gmail.com')
99
- end
136
+ it 'uses DTA default_confirm_success_url on the email with redirect flow' do
137
+ expect { updater }.to change(ActionMailer::Base.deliveries, :count).by(1)
100
138
 
101
- context 'when email value is the same on the DB' do
102
- let(:attributes) { { email: resource.email, name: 'changed', schema_url: 'http://localhost/test', confirmation_success_url: 'https://google.com' } }
103
-
104
- it 'updates attributes and does not send confirmation email' do
105
- expect do
106
- updater
107
- resource.reload
108
- end.to change(resource, :name).from(resource.name).to('changed').and(
109
- not_change(resource, :email).from(resource.email)
110
- ).and(
111
- not_change(ActionMailer::Base.deliveries, :count).from(0)
112
- )
139
+ email = ActionMailer::Base.deliveries.first
140
+ expect(email.body.decoded).to include(CGI.escape('https://google.com'))
141
+ expect(email.body.decoded).to include(CGI.escape('ConfirmAccount('))
113
142
  end
114
143
  end
115
144
 
116
- context 'when provided params are invalid' do
117
- let(:attributes) { { email: 'newgmail.com', name: '', schema_url: 'http://localhost/test', confirmation_success_url: 'https://google.com' } }
145
+ context 'when schema_url is not provided' do
146
+ let(:attributes) { { email: 'new@gmail.com', name: 'Updated Name' } }
118
147
 
119
- it 'returns false and adds errors to the model' do
120
- expect(updater).to be_falsey
121
- expect(resource.errors.full_messages).to contain_exactly(
122
- 'Email is not an email',
123
- "Name can't be blank"
124
- )
148
+ it 'uses DTA default_confirm_success_url on the email and new confirmation flow' do
149
+ expect { updater }.to change(ActionMailer::Base.deliveries, :count).by(1)
150
+
151
+ email = ActionMailer::Base.deliveries.first
152
+ expect(email.body.decoded).to include(CGI.escape('https://google.com'))
153
+ expect(email.body.decoded).to include('?confirmationToken=')
125
154
  end
126
155
  end
127
156
  end
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails_helper'
4
+
5
+ RSpec.describe 'Registration confirmation with token' do
6
+ include_context 'with graphql query request'
7
+
8
+ context 'when using the user model' do
9
+ let(:user) { create(:user, confirmed_at: nil) }
10
+ let(:query) do
11
+ <<-GRAPHQL
12
+ mutation {
13
+ userConfirmRegistrationWithToken(
14
+ confirmationToken: "#{token}"
15
+ ) {
16
+ authenticatable {
17
+ email
18
+ name
19
+ }
20
+ credentials { client }
21
+ }
22
+ }
23
+ GRAPHQL
24
+ end
25
+
26
+ context 'when confirmation token is correct' do
27
+ let(:token) { user.confirmation_token }
28
+
29
+ before do
30
+ user.send_confirmation_instructions(
31
+ template_path: ['graphql_devise/mailer']
32
+ )
33
+ end
34
+
35
+ it 'confirms the resource and returns credentials' do
36
+ expect do
37
+ post_request
38
+ user.reload
39
+ end.to(change(user, :confirmed_at).from(nil))
40
+
41
+ expect(json_response[:data][:userConfirmRegistrationWithToken]).to include(
42
+ authenticatable: { email: user.email, name: user.name },
43
+ credentials: { client: user.tokens.keys.first }
44
+ )
45
+
46
+ expect(user).to be_active_for_authentication
47
+ end
48
+
49
+ context 'when unconfirmed_email is present' do
50
+ let(:user) { create(:user, :confirmed, unconfirmed_email: 'vvega@wallaceinc.com') }
51
+
52
+ it 'confirms the unconfirmed email' do
53
+ expect do
54
+ post_request
55
+ user.reload
56
+ end.to change(user, :email).from(user.email).to('vvega@wallaceinc.com').and(
57
+ change(user, :unconfirmed_email).from('vvega@wallaceinc.com').to(nil)
58
+ )
59
+ end
60
+ end
61
+ end
62
+
63
+ context 'when reset password token is not found' do
64
+ let(:token) { "#{user.confirmation_token}-invalid" }
65
+
66
+ it 'does *NOT* confirm the user' do
67
+ expect do
68
+ post_request
69
+ user.reload
70
+ end.not_to change(user, :confirmed_at).from(nil)
71
+
72
+ expect(json_response[:errors]).to contain_exactly(
73
+ hash_including(
74
+ message: 'Invalid confirmation token. Please try again',
75
+ extensions: { code: 'USER_ERROR' }
76
+ )
77
+ )
78
+ end
79
+ end
80
+ end
81
+
82
+ context 'when using the admin model' do
83
+ let(:admin) { create(:admin, confirmed_at: nil) }
84
+ let(:query) do
85
+ <<-GRAPHQL
86
+ mutation {
87
+ adminConfirmRegistrationWithToken(
88
+ confirmationToken: "#{token}"
89
+ ) {
90
+ authenticatable { email }
91
+ }
92
+ }
93
+ GRAPHQL
94
+ end
95
+
96
+ context 'when confirmation token is correct' do
97
+ let(:token) { admin.confirmation_token }
98
+
99
+ before do
100
+ admin.send_confirmation_instructions(
101
+ template_path: ['graphql_devise/mailer']
102
+ )
103
+ end
104
+
105
+ it 'confirms the resource and persists credentials on the DB' do
106
+ expect do
107
+ get_request
108
+ admin.reload
109
+ end.to change(admin, :confirmed_at).from(nil).and(
110
+ change { admin.tokens.keys.count }.from(0).to(1)
111
+ )
112
+
113
+ expect(admin).to be_active_for_authentication
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,166 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails_helper'
4
+
5
+ RSpec.describe 'Registration process' do
6
+ include_context 'with graphql query request'
7
+
8
+ let(:name) { Faker::Name.name }
9
+ let(:password) { Faker::Internet.password }
10
+ let(:email) { Faker::Internet.email }
11
+ let(:redirect) { 'https://google.com' }
12
+
13
+ context 'when using the user model' do
14
+ let(:query) do
15
+ <<-GRAPHQL
16
+ mutation {
17
+ userRegister(
18
+ email: "#{email}"
19
+ name: "#{name}"
20
+ password: "#{password}"
21
+ passwordConfirmation: "#{password}"
22
+ confirmUrl: "#{redirect}"
23
+ ) {
24
+ credentials { accessToken }
25
+ user {
26
+ email
27
+ name
28
+ }
29
+ }
30
+ }
31
+ GRAPHQL
32
+ end
33
+
34
+ context 'when redirect_url is not whitelisted' do
35
+ let(:redirect) { 'https://not-safe.com' }
36
+
37
+ it 'returns a not whitelisted redirect url error' do
38
+ expect { post_request }.to(
39
+ not_change(User, :count)
40
+ .and(not_change(ActionMailer::Base.deliveries, :count))
41
+ )
42
+
43
+ expect(json_response[:errors]).to containing_exactly(
44
+ hash_including(
45
+ message: "Redirect to '#{redirect}' not allowed.",
46
+ extensions: { code: 'USER_ERROR' }
47
+ )
48
+ )
49
+ end
50
+ end
51
+
52
+ context 'when params are correct' do
53
+ it 'creates a new resource that requires confirmation' do
54
+ expect { post_request }.to(
55
+ change(User, :count).by(1)
56
+ .and(change(ActionMailer::Base.deliveries, :count).by(1))
57
+ )
58
+
59
+ user = User.last
60
+
61
+ expect(user).not_to be_active_for_authentication
62
+ expect(user.confirmed_at).to be_nil
63
+ expect(user).to be_valid_password(password)
64
+ expect(json_response[:data][:userRegister]).to include(
65
+ credentials: nil,
66
+ user: {
67
+ email: email,
68
+ name: name
69
+ }
70
+ )
71
+
72
+ email = Nokogiri::HTML(ActionMailer::Base.deliveries.last.body.encoded)
73
+ confirm_link = email.css('a').first['href']
74
+ confirm_token = confirm_link.match(/\?confirmationToken\=(?<token>.+)\z/)[:token]
75
+
76
+ expect(User.confirm_by_token(confirm_token)).to eq(user)
77
+ end
78
+
79
+ context 'when email address uses different casing' do
80
+ let(:email) { 'miaWallace@wallaceinc.com' }
81
+
82
+ it 'honors devise configuration for case insensitive fields' do
83
+ expect { post_request }.to change(ActionMailer::Base.deliveries, :count).by(1)
84
+ expect(User.last.email).to eq('miawallace@wallaceinc.com')
85
+ expect(json_response[:data][:userRegister]).to include(user: { email: 'miawallace@wallaceinc.com', name: name })
86
+ end
87
+ end
88
+ end
89
+
90
+ context 'when required params are missing' do
91
+ let(:email) { '' }
92
+
93
+ it 'does *NOT* create resource a resource nor send an email' do
94
+ expect { post_request }.to(
95
+ not_change(User, :count)
96
+ .and(not_change(ActionMailer::Base.deliveries, :count))
97
+ )
98
+
99
+ expect(json_response[:data][:userRegister]).to be_nil
100
+ expect(json_response[:errors]).to containing_exactly(
101
+ hash_including(
102
+ message: "User couldn't be registered",
103
+ extensions: { code: 'USER_ERROR', detailed_errors: ["Email can't be blank"] }
104
+ )
105
+ )
106
+ end
107
+ end
108
+ end
109
+
110
+ context 'when using the admin model' do
111
+ let(:query) do
112
+ <<-GRAPHQL
113
+ mutation {
114
+ adminRegister(
115
+ email: "#{email}"
116
+ password: "#{password}"
117
+ passwordConfirmation: "#{password}"
118
+ ) {
119
+ authenticatable {
120
+ email
121
+ }
122
+ }
123
+ }
124
+ GRAPHQL
125
+ end
126
+
127
+ before { post_request }
128
+
129
+ it 'skips the register mutation' do
130
+ expect(json_response[:errors]).to contain_exactly(
131
+ hash_including(message: "Field 'adminRegister' doesn't exist on type 'Mutation'")
132
+ )
133
+ end
134
+ end
135
+
136
+ context 'when using the guest model' do
137
+ let(:query) do
138
+ <<-GRAPHQL
139
+ mutation {
140
+ guestRegister(
141
+ email: "#{email}"
142
+ password: "#{password}"
143
+ passwordConfirmation: "#{password}"
144
+ ) {
145
+ credentials { accessToken client uid }
146
+ authenticatable {
147
+ email
148
+ }
149
+ }
150
+ }
151
+ GRAPHQL
152
+ end
153
+
154
+ it 'returns credentials as no confirmation is required' do
155
+ expect { post_request }.to change(Guest, :count).from(0).to(1)
156
+
157
+ expect(json_response[:data][:guestRegister]).to include(
158
+ authenticatable: { email: email },
159
+ credentials: hash_including(
160
+ uid: email,
161
+ client: Guest.last.tokens.keys.first
162
+ )
163
+ )
164
+ end
165
+ end
166
+ end