graphql_devise 0.14.1 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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