graphql_devise 0.14.1 → 0.17.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +118 -0
  3. data/Appraisals +26 -6
  4. data/CHANGELOG.md +72 -6
  5. data/README.md +184 -69
  6. data/Rakefile +2 -1
  7. data/app/controllers/graphql_devise/concerns/additional_controller_methods.rb +72 -0
  8. data/app/controllers/graphql_devise/concerns/set_user_by_token.rb +5 -27
  9. data/app/controllers/graphql_devise/graphql_controller.rb +1 -1
  10. data/app/helpers/graphql_devise/mailer_helper.rb +2 -2
  11. data/app/models/graphql_devise/concerns/additional_model_methods.rb +21 -0
  12. data/app/models/graphql_devise/concerns/model.rb +6 -9
  13. data/app/views/graphql_devise/mailer/confirmation_instructions.html.erb +7 -1
  14. data/graphql_devise.gemspec +1 -1
  15. data/lib/generators/graphql_devise/install_generator.rb +1 -1
  16. data/lib/graphql_devise.rb +20 -6
  17. data/lib/graphql_devise/concerns/controller_methods.rb +3 -3
  18. data/lib/graphql_devise/default_operations/mutations.rb +14 -8
  19. data/lib/graphql_devise/default_operations/resolvers.rb +2 -2
  20. data/lib/graphql_devise/model/with_email_updater.rb +34 -8
  21. data/lib/graphql_devise/mount_method/operation_preparer.rb +6 -6
  22. data/lib/graphql_devise/mount_method/operation_preparers/custom_operation_preparer.rb +6 -4
  23. data/lib/graphql_devise/mount_method/operation_preparers/default_operation_preparer.rb +7 -5
  24. data/lib/graphql_devise/mount_method/operation_preparers/{resource_name_setter.rb → resource_klass_setter.rb} +4 -4
  25. data/lib/graphql_devise/mount_method/operation_sanitizer.rb +13 -1
  26. data/lib/graphql_devise/mutations/confirm_registration_with_token.rb +30 -0
  27. data/lib/graphql_devise/mutations/register.rb +60 -0
  28. data/lib/graphql_devise/mutations/resend_confirmation_with_token.rb +44 -0
  29. data/lib/graphql_devise/mutations/sign_up.rb +1 -1
  30. data/lib/graphql_devise/resolvers/confirm_account.rb +1 -1
  31. data/lib/graphql_devise/resource_loader.rb +26 -11
  32. data/lib/graphql_devise/schema_plugin.rb +31 -10
  33. data/lib/graphql_devise/version.rb +1 -1
  34. data/spec/dummy/app/controllers/api/v1/graphql_controller.rb +13 -2
  35. data/spec/dummy/app/graphql/dummy_schema.rb +8 -6
  36. data/spec/dummy/app/graphql/mutations/register.rb +14 -0
  37. data/spec/dummy/app/graphql/types/query_type.rb +5 -0
  38. data/spec/dummy/config/routes.rb +7 -5
  39. data/spec/dummy/db/migrate/20200623003142_create_schema_users.rb +0 -1
  40. data/spec/dummy/db/migrate/20210516211417_add_vip_to_users.rb +5 -0
  41. data/spec/dummy/db/schema.rb +4 -4
  42. data/spec/generators/graphql_devise/install_generator_spec.rb +1 -1
  43. data/spec/graphql/user_queries_spec.rb +3 -1
  44. data/spec/graphql_devise/model/with_email_updater_spec.rb +97 -68
  45. data/spec/requests/graphql_controller_spec.rb +12 -11
  46. data/spec/requests/mutations/confirm_registration_with_token_spec.rb +117 -0
  47. data/spec/requests/mutations/register_spec.rb +166 -0
  48. data/spec/requests/mutations/resend_confirmation_with_token_spec.rb +137 -0
  49. data/spec/requests/queries/introspection_query_spec.rb +149 -0
  50. data/spec/requests/user_controller_spec.rb +86 -25
  51. data/spec/services/mount_method/operation_preparer_spec.rb +5 -5
  52. data/spec/services/mount_method/operation_preparers/custom_operation_preparer_spec.rb +5 -5
  53. data/spec/services/mount_method/operation_preparers/default_operation_preparer_spec.rb +5 -5
  54. data/spec/services/mount_method/operation_preparers/{resource_name_setter_spec.rb → resource_klass_setter_spec.rb} +6 -6
  55. data/spec/services/mount_method/operation_sanitizer_spec.rb +3 -3
  56. data/spec/services/resource_loader_spec.rb +5 -5
  57. data/spec/support/contexts/graphql_request.rb +11 -3
  58. metadata +29 -12
  59. data/.travis.yml +0 -86
@@ -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
@@ -0,0 +1,137 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails_helper'
4
+
5
+ RSpec.describe 'Resend confirmation with token' do
6
+ include_context 'with graphql query request'
7
+
8
+ let(:confirmed_at) { nil }
9
+ let!(:user) { create(:user, confirmed_at: nil, email: 'mwallace@wallaceinc.com') }
10
+ let(:email) { user.email }
11
+ let(:id) { user.id }
12
+ let(:confirm_url) { 'https://google.com' }
13
+ let(:query) do
14
+ <<-GRAPHQL
15
+ mutation {
16
+ userResendConfirmationWithToken(
17
+ email:"#{email}",
18
+ confirmUrl:"#{confirm_url}"
19
+ ) {
20
+ message
21
+ }
22
+ }
23
+ GRAPHQL
24
+ end
25
+
26
+ context 'when confirm_url is not whitelisted' do
27
+ let(:confirm_url) { 'https://not-safe.com' }
28
+
29
+ it 'returns a not whitelisted confirm url error' do
30
+ expect { post_request }.to not_change(ActionMailer::Base.deliveries, :count)
31
+
32
+ expect(json_response[:errors]).to containing_exactly(
33
+ hash_including(
34
+ message: "Redirect to '#{confirm_url}' not allowed.",
35
+ extensions: { code: 'USER_ERROR' }
36
+ )
37
+ )
38
+ end
39
+ end
40
+
41
+ context 'when params are correct' do
42
+ context 'when using the gem schema' do
43
+ it 'sends an email to the user with confirmation url and returns a success message' do
44
+ expect { post_request }.to change(ActionMailer::Base.deliveries, :count).by(1)
45
+ expect(json_response[:data][:userResendConfirmationWithToken]).to include(
46
+ message: 'You will receive an email with instructions for how to confirm your email address in a few minutes.'
47
+ )
48
+
49
+ email = Nokogiri::HTML(ActionMailer::Base.deliveries.last.body.encoded)
50
+ confirm_link = email.css('a').first['href']
51
+ confirm_token = confirm_link.match(/\?confirmationToken\=(?<token>.+)\z/)[:token]
52
+
53
+ expect(User.confirm_by_token(confirm_token)).to eq(user)
54
+ end
55
+ end
56
+
57
+ context 'when using a custom schema' do
58
+ let(:custom_path) { '/api/v1/graphql' }
59
+
60
+ it 'sends an email to the user with confirmation url and returns a success message' do
61
+ expect { post_request(custom_path) }.to change(ActionMailer::Base.deliveries, :count).by(1)
62
+ expect(json_response[:data][:userResendConfirmationWithToken]).to include(
63
+ message: 'You will receive an email with instructions for how to confirm your email address in a few minutes.'
64
+ )
65
+
66
+ email = Nokogiri::HTML(ActionMailer::Base.deliveries.last.body.encoded)
67
+ confirm_link = email.css('a').first['href']
68
+ confirm_token = confirm_link.match(/\?confirmationToken\=(?<token>.+)\z/)[:token]
69
+
70
+ expect(User.confirm_by_token(confirm_token)).to eq(user)
71
+ end
72
+ end
73
+
74
+ context 'when email address uses different casing' do
75
+ let(:email) { 'mWallace@wallaceinc.com' }
76
+
77
+ it 'honors devise configuration for case insensitive fields' do
78
+ expect { post_request }.to change(ActionMailer::Base.deliveries, :count).by(1)
79
+ expect(json_response[:data][:userResendConfirmationWithToken]).to include(
80
+ message: 'You will receive an email with instructions for how to confirm your email address in a few minutes.'
81
+ )
82
+ end
83
+ end
84
+
85
+ context 'when the user has already been confirmed' do
86
+ before { user.confirm }
87
+
88
+ it 'does *NOT* send an email and raises an error' do
89
+ expect { post_request }.to not_change(ActionMailer::Base.deliveries, :count)
90
+ expect(json_response[:data][:userResendConfirmationWithToken]).to be_nil
91
+ expect(json_response[:errors]).to contain_exactly(
92
+ hash_including(
93
+ message: 'Email was already confirmed, please try signing in',
94
+ extensions: { code: 'USER_ERROR' }
95
+ )
96
+ )
97
+ end
98
+ end
99
+ end
100
+
101
+ context 'when the email was changed' do
102
+ let(:confirmed_at) { 2.seconds.ago }
103
+ let(:email) { 'new-email@wallaceinc.com' }
104
+ let(:new_email) { email }
105
+
106
+ before do
107
+ user.update_with_email(
108
+ email: new_email,
109
+ schema_url: 'http://localhost/test',
110
+ confirmation_success_url: 'https://google.com'
111
+ )
112
+ end
113
+
114
+ it 'sends new confirmation email' do
115
+ expect { post_request }.to change(ActionMailer::Base.deliveries, :count).by(1)
116
+ expect(ActionMailer::Base.deliveries.first.to).to contain_exactly(new_email)
117
+ expect(json_response[:data][:userResendConfirmationWithToken]).to include(
118
+ message: 'You will receive an email with instructions for how to confirm your email address in a few minutes.'
119
+ )
120
+ end
121
+ end
122
+
123
+ context "when the email isn't in the system" do
124
+ let(:email) { 'notthere@gmail.com' }
125
+
126
+ it 'does *NOT* send an email and raises an error' do
127
+ expect { post_request }.to not_change(ActionMailer::Base.deliveries, :count)
128
+ expect(json_response[:data][:userResendConfirmationWithToken]).to be_nil
129
+ expect(json_response[:errors]).to contain_exactly(
130
+ hash_including(
131
+ message: "Unable to find user with email '#{email}'.",
132
+ extensions: { code: 'USER_ERROR' }
133
+ )
134
+ )
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,149 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails_helper'
4
+
5
+ RSpec.describe 'Login Requests' do
6
+ include_context 'with graphql query request'
7
+
8
+ let(:query) do
9
+ <<-GRAPHQL
10
+ query IntrospectionQuery {
11
+ __schema {
12
+ queryType { name }
13
+ mutationType { name }
14
+ subscriptionType { name }
15
+ types {
16
+ ...FullType
17
+ }
18
+ directives {
19
+ name
20
+ description
21
+ args {
22
+ ...InputValue
23
+ }
24
+ onOperation
25
+ onFragment
26
+ onField
27
+ }
28
+ }
29
+ }
30
+
31
+ fragment FullType on __Type {
32
+ kind
33
+ name
34
+ description
35
+ fields(includeDeprecated: true) {
36
+ name
37
+ description
38
+ args {
39
+ ...InputValue
40
+ }
41
+ type {
42
+ ...TypeRef
43
+ }
44
+ isDeprecated
45
+ deprecationReason
46
+ }
47
+ inputFields {
48
+ ...InputValue
49
+ }
50
+ interfaces {
51
+ ...TypeRef
52
+ }
53
+ enumValues(includeDeprecated: true) {
54
+ name
55
+ description
56
+ isDeprecated
57
+ deprecationReason
58
+ }
59
+ possibleTypes {
60
+ ...TypeRef
61
+ }
62
+ }
63
+
64
+ fragment InputValue on __InputValue {
65
+ name
66
+ description
67
+ type { ...TypeRef }
68
+ defaultValue
69
+ }
70
+
71
+ fragment TypeRef on __Type {
72
+ kind
73
+ name
74
+ ofType {
75
+ kind
76
+ name
77
+ ofType {
78
+ kind
79
+ name
80
+ ofType {
81
+ kind
82
+ name
83
+ }
84
+ }
85
+ }
86
+ }
87
+
88
+ GRAPHQL
89
+ end
90
+
91
+ context 'when using a schema plugin to mount devise operations' do
92
+ context 'when schema plugin is set to authenticate by default' do
93
+ context 'when the resource is authenticated' do
94
+ let(:user) { create(:user, :confirmed) }
95
+ let(:headers) { user.create_new_auth_token }
96
+
97
+ it 'return the schema information' do
98
+ post_request('/api/v1/graphql')
99
+
100
+ expect(json_response[:data][:__schema].keys).to contain_exactly(
101
+ :queryType, :mutationType, :subscriptionType, :types, :directives
102
+ )
103
+ end
104
+ end
105
+
106
+ context 'when the resource is *NOT* authenticated' do
107
+ context 'and instrospection is set to be public' do
108
+ it 'return the schema information' do
109
+ post_request('/api/v1/graphql')
110
+
111
+ expect(json_response[:data][:__schema].keys).to contain_exactly(
112
+ :queryType, :mutationType, :subscriptionType, :types, :directives
113
+ )
114
+ end
115
+ end
116
+
117
+ context 'and introspection is set to require auth' do
118
+ before do
119
+ allow_any_instance_of(GraphqlDevise::SchemaPlugin).to(
120
+ receive(:public_introspection).and_return(false)
121
+ )
122
+ end
123
+
124
+ it 'return an error' do
125
+ post_request('/api/v1/graphql')
126
+
127
+ expect(json_response[:data]).to be_nil
128
+ expect(json_response[:errors]).to contain_exactly(
129
+ hash_including(
130
+ message: '__schema field requires authentication',
131
+ extensions: { code: 'AUTHENTICATION_ERROR' }
132
+ )
133
+ )
134
+ end
135
+ end
136
+ end
137
+ end
138
+
139
+ context 'when schema plugin is set *NOT* to authenticate by default' do
140
+ it 'return the schema information' do
141
+ post_request('/api/v1/interpreter')
142
+
143
+ expect(json_response[:data][:__schema].keys).to contain_exactly(
144
+ :queryType, :mutationType, :subscriptionType, :types, :directives
145
+ )
146
+ end
147
+ end
148
+ end
149
+ end