graphql_devise 0.13.6 → 0.15.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 (53) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +118 -0
  3. data/Appraisals +39 -5
  4. data/CHANGELOG.md +64 -6
  5. data/README.md +135 -50
  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/reset_password_instructions.html.erb +7 -1
  14. data/config/locales/en.yml +1 -0
  15. data/docs/usage/reset_password_flow.md +90 -0
  16. data/graphql_devise.gemspec +2 -2
  17. data/lib/generators/graphql_devise/install_generator.rb +1 -1
  18. data/lib/graphql_devise.rb +20 -6
  19. data/lib/graphql_devise/concerns/controller_methods.rb +3 -3
  20. data/lib/graphql_devise/default_operations/mutations.rb +10 -6
  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 +6 -4
  24. data/lib/graphql_devise/mount_method/operation_preparers/{resource_name_setter.rb → resource_klass_setter.rb} +4 -4
  25. data/lib/graphql_devise/mutations/send_password_reset_with_token.rb +37 -0
  26. data/lib/graphql_devise/mutations/update_password_with_token.rb +38 -0
  27. data/lib/graphql_devise/resolvers/confirm_account.rb +1 -1
  28. data/lib/graphql_devise/resource_loader.rb +26 -11
  29. data/lib/graphql_devise/schema_plugin.rb +35 -16
  30. data/lib/graphql_devise/version.rb +1 -1
  31. data/spec/dummy/app/controllers/api/v1/graphql_controller.rb +13 -2
  32. data/spec/dummy/app/graphql/dummy_schema.rb +4 -3
  33. data/spec/dummy/app/graphql/mutations/reset_admin_password_with_token.rb +13 -0
  34. data/spec/dummy/config/routes.rb +4 -2
  35. data/spec/dummy/db/migrate/20200623003142_create_schema_users.rb +0 -1
  36. data/spec/dummy/db/schema.rb +0 -1
  37. data/spec/generators/graphql_devise/install_generator_spec.rb +1 -1
  38. data/spec/graphql/user_queries_spec.rb +120 -0
  39. data/spec/requests/graphql_controller_spec.rb +12 -11
  40. data/spec/requests/mutations/send_password_reset_with_token_spec.rb +78 -0
  41. data/spec/requests/mutations/update_password_with_token_spec.rb +119 -0
  42. data/spec/requests/queries/check_password_token_spec.rb +1 -1
  43. data/spec/requests/queries/introspection_query_spec.rb +149 -0
  44. data/spec/requests/user_controller_spec.rb +29 -9
  45. data/spec/services/mount_method/operation_preparer_spec.rb +5 -5
  46. data/spec/services/mount_method/operation_preparers/custom_operation_preparer_spec.rb +5 -5
  47. data/spec/services/mount_method/operation_preparers/default_operation_preparer_spec.rb +5 -5
  48. data/spec/services/mount_method/operation_preparers/{resource_name_setter_spec.rb → resource_klass_setter_spec.rb} +6 -6
  49. data/spec/services/resource_loader_spec.rb +5 -5
  50. data/spec/support/contexts/graphql_request.rb +11 -3
  51. data/spec/support/contexts/schema_test.rb +14 -0
  52. metadata +31 -14
  53. data/.travis.yml +0 -79
@@ -73,7 +73,6 @@ ActiveRecord::Schema.define(version: 2020_06_23_003142) do
73
73
  t.text "tokens"
74
74
  t.datetime "created_at", precision: 6, null: false
75
75
  t.datetime "updated_at", precision: 6, null: false
76
- t.index "\"unlock_token\"", name: "index_schema_users_on_unlock_token", unique: true
77
76
  t.index ["confirmation_token"], name: "index_schema_users_on_confirmation_token", unique: true
78
77
  t.index ["email"], name: "index_schema_users_on_email", unique: true
79
78
  t.index ["reset_password_token"], name: "index_schema_users_on_reset_password_token", unique: true
@@ -33,7 +33,7 @@ RSpec.describe GraphqlDevise::InstallGenerator, type: :generator do
33
33
 
34
34
  assert_file 'app/controllers/application_controller.rb', /^\s{2}include GraphqlDevise::Concerns::SetUserByToken/
35
35
 
36
- assert_file 'app/graphql/gqld_dummy_schema.rb', /\s+#{Regexp.escape("GraphqlDevise::ResourceLoader.new('Admin')")}/
36
+ assert_file 'app/graphql/gqld_dummy_schema.rb', /\s+#{Regexp.escape("GraphqlDevise::ResourceLoader.new(Admin)")}/
37
37
  end
38
38
  end
39
39
 
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails_helper'
4
+
5
+ RSpec.describe 'Users controller specs' do
6
+ include_context 'with graphql schema test'
7
+
8
+ let(:schema) { DummySchema }
9
+ let(:user) { create(:user, :confirmed) }
10
+ let(:field) { 'privateField' }
11
+ let(:public_message) { 'Field does not require authentication' }
12
+ let(:private_message) { 'Field will always require authentication' }
13
+ let(:private_error) do
14
+ {
15
+ message: "#{field} field requires authentication",
16
+ extensions: { code: 'AUTHENTICATION_ERROR' }
17
+ }
18
+ end
19
+
20
+ describe 'publicField' do
21
+ let(:query) do
22
+ <<-GRAPHQL
23
+ query {
24
+ publicField
25
+ }
26
+ GRAPHQL
27
+ end
28
+
29
+ context 'when using a regular schema' do
30
+ it 'does not require authentication' do
31
+ expect(response[:data][:publicField]).to eq(public_message)
32
+ end
33
+ end
34
+ end
35
+
36
+ describe 'privateField' do
37
+ let(:query) do
38
+ <<-GRAPHQL
39
+ query {
40
+ privateField
41
+ }
42
+ GRAPHQL
43
+ end
44
+
45
+ context 'when using a regular schema' do
46
+ context 'when user is authenticated' do
47
+ let(:resource) { user }
48
+
49
+ it 'allows to perform the query' do
50
+ expect(response[:data][:privateField]).to eq(private_message)
51
+ end
52
+
53
+ context 'when using a SchemaUser' do
54
+ let(:resource) { create(:schema_user, :confirmed) }
55
+
56
+ it 'allows to perform the query' do
57
+ expect(response[:data][:privateField]).to eq(private_message)
58
+ end
59
+ end
60
+ end
61
+ end
62
+
63
+ context 'when using an interpreter schema' do
64
+ let(:schema) { InterpreterSchema }
65
+
66
+ context 'when user is authenticated' do
67
+ let(:resource) { user }
68
+
69
+ it 'allows to perform the query' do
70
+ expect(response[:data][:privateField]).to eq(private_message)
71
+ end
72
+ end
73
+ end
74
+ end
75
+
76
+ describe 'user' do
77
+ let(:user_data) { { email: user.email, id: user.id } }
78
+ let(:query) do
79
+ <<-GRAPHQL
80
+ query {
81
+ user(
82
+ id: #{user.id}
83
+ ) {
84
+ id
85
+ email
86
+ }
87
+ }
88
+ GRAPHQL
89
+ end
90
+
91
+ context 'when using a regular schema' do
92
+ context 'when user is authenticated' do
93
+ let(:resource) { user }
94
+
95
+ it 'allows to perform the query' do
96
+ expect(response[:data][:user]).to match(**user_data)
97
+ end
98
+ end
99
+ end
100
+
101
+ context 'when using an interpreter schema' do
102
+ let(:schema) { InterpreterSchema }
103
+
104
+ context 'when user is authenticated' do
105
+ let(:resource) { user }
106
+
107
+ it 'allows to perform the query' do
108
+ expect(response[:data][:user]).to match(**user_data)
109
+ end
110
+ end
111
+
112
+ context 'when user is not authenticated' do
113
+ # Interpreter schema fields are public unless specified otherwise (plugin setting)
114
+ it 'allows to perform the query' do
115
+ expect(response[:data][:user]).to match(**user_data)
116
+ end
117
+ end
118
+ end
119
+ end
120
+ end
@@ -6,20 +6,13 @@ RSpec.describe GraphqlDevise::GraphqlController do
6
6
  let(:password) { 'password123' }
7
7
  let(:user) { create(:user, :confirmed, password: password) }
8
8
  let(:params) { { query: query, variables: variables } }
9
- let(:request_params) do
10
- if Rails::VERSION::MAJOR >= 5
11
- { params: params }
12
- else
13
- params
14
- end
15
- end
16
9
 
17
10
  context 'when variables are a string' do
18
11
  let(:variables) { "{\"email\": \"#{user.email}\"}" }
19
12
  let(:query) { "mutation($email: String!) { userLogin(email: $email, password: \"#{password}\") { user { email name signInCount } } }" }
20
13
 
21
14
  it 'parses the string variables' do
22
- post '/api/v1/graphql_auth', request_params
15
+ post_request('/api/v1/graphql_auth')
23
16
 
24
17
  expect(json_response).to match(
25
18
  data: { userLogin: { user: { email: user.email, name: user.name, signInCount: 1 } } }
@@ -31,7 +24,7 @@ RSpec.describe GraphqlDevise::GraphqlController do
31
24
  let(:query) { "mutation { userLogin(email: \"#{user.email}\", password: \"#{password}\") { user { email name signInCount } } }" }
32
25
 
33
26
  it 'returns an empty hash as variables' do
34
- post '/api/v1/graphql_auth', request_params
27
+ post_request('/api/v1/graphql_auth')
35
28
 
36
29
  expect(json_response).to match(
37
30
  data: { userLogin: { user: { email: user.email, name: user.name, signInCount: 1 } } }
@@ -46,7 +39,7 @@ RSpec.describe GraphqlDevise::GraphqlController do
46
39
 
47
40
  it 'raises an error' do
48
41
  expect do
49
- post '/api/v1/graphql_auth', request_params
42
+ post_request('/api/v1/graphql_auth')
50
43
  end.to raise_error(ArgumentError)
51
44
  end
52
45
  end
@@ -62,7 +55,7 @@ RSpec.describe GraphqlDevise::GraphqlController do
62
55
  end
63
56
 
64
57
  it 'executes multiple queries in the same request' do
65
- post '/api/v1/graphql_auth', request_params
58
+ post_request('/api/v1/graphql_auth')
66
59
 
67
60
  expect(json_response).to match(
68
61
  [
@@ -79,4 +72,12 @@ RSpec.describe GraphqlDevise::GraphqlController do
79
72
  )
80
73
  end
81
74
  end
75
+
76
+ def post_request(path)
77
+ if Rails::VERSION::MAJOR >= 5
78
+ post(path, params: params)
79
+ else
80
+ post(path, params)
81
+ end
82
+ end
82
83
  end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails_helper'
4
+
5
+ RSpec.describe 'Send Password Reset Requests' do
6
+ include_context 'with graphql query request'
7
+
8
+ let!(:user) { create(:user, :confirmed, email: 'jwinnfield@wallaceinc.com') }
9
+ let(:email) { user.email }
10
+ let(:redirect_url) { 'https://google.com' }
11
+ let(:query) do
12
+ <<-GRAPHQL
13
+ mutation {
14
+ userSendPasswordResetWithToken(
15
+ email: "#{email}",
16
+ redirectUrl: "#{redirect_url}"
17
+ ) {
18
+ message
19
+ }
20
+ }
21
+ GRAPHQL
22
+ end
23
+
24
+ context 'when redirect_url is not whitelisted' do
25
+ let(:redirect_url) { 'https://not-safe.com' }
26
+
27
+ it 'returns a not whitelisted redirect url error' do
28
+ expect { post_request }.to not_change(ActionMailer::Base.deliveries, :count)
29
+
30
+ expect(json_response[:errors]).to containing_exactly(
31
+ hash_including(
32
+ message: "Redirect to '#{redirect_url}' not allowed.",
33
+ extensions: { code: 'USER_ERROR' }
34
+ )
35
+ )
36
+ end
37
+ end
38
+
39
+ context 'when params are correct' do
40
+ context 'when using the gem schema' do
41
+ it 'sends password reset email' do
42
+ expect { post_request }.to change(ActionMailer::Base.deliveries, :count).by(1)
43
+
44
+ expect(json_response[:data][:userSendPasswordResetWithToken]).to include(
45
+ message: 'You will receive an email with instructions on how to reset your password in a few minutes.'
46
+ )
47
+
48
+ email = Nokogiri::HTML(ActionMailer::Base.deliveries.last.body.encoded)
49
+ link = email.css('a').first
50
+
51
+ expect(link['href']).to include(redirect_url + '?reset_password_token')
52
+ end
53
+ end
54
+ end
55
+
56
+ context 'when email address uses different casing' do
57
+ let(:email) { 'jWinnfield@wallaceinc.com' }
58
+
59
+ it 'honors devise configuration for case insensitive fields' do
60
+ expect { post_request }.to change(ActionMailer::Base.deliveries, :count).by(1)
61
+ expect(json_response[:data][:userSendPasswordResetWithToken]).to include(
62
+ message: 'You will receive an email with instructions on how to reset your password in a few minutes.'
63
+ )
64
+ end
65
+ end
66
+
67
+ context 'when user email is not found' do
68
+ let(:email) { 'nothere@gmail.com' }
69
+
70
+ before { post_request }
71
+
72
+ it 'returns an error' do
73
+ expect(json_response[:errors]).to contain_exactly(
74
+ hash_including(message: 'User was not found or was not logged in.', extensions: { code: 'USER_ERROR' })
75
+ )
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails_helper'
4
+
5
+ RSpec.describe 'Update Password With Token' do
6
+ include_context 'with graphql query request'
7
+
8
+ let(:password) { '12345678' }
9
+ let(:password_confirmation) { password }
10
+
11
+ context 'when using the user model' do
12
+ let(:user) { create(:user, :confirmed) }
13
+ let(:query) do
14
+ <<-GRAPHQL
15
+ mutation {
16
+ userUpdatePasswordWithToken(
17
+ resetPasswordToken: "#{token}",
18
+ password: "#{password}",
19
+ passwordConfirmation: "#{password_confirmation}"
20
+ ) {
21
+ authenticatable { email }
22
+ credentials { accessToken }
23
+ }
24
+ }
25
+ GRAPHQL
26
+ end
27
+
28
+ context 'when reset password token is valid' do
29
+ let(:token) { user.send(:set_reset_password_token) }
30
+
31
+ it 'updates the password' do
32
+ expect do
33
+ post_request
34
+ user.reload
35
+ end.to change(user, :encrypted_password)
36
+
37
+ expect(user).to be_valid_password(password)
38
+ expect(json_response[:data][:userUpdatePasswordWithToken][:credentials]).to be_nil
39
+ expect(json_response[:data][:userUpdatePasswordWithToken][:authenticatable]).to include(email: user.email)
40
+ end
41
+
42
+ context 'when token has expired' do
43
+ it 'returns an expired token error' do
44
+ travel_to 10.hours.ago do
45
+ token
46
+ end
47
+
48
+ post_request
49
+
50
+ expect(json_response[:errors]).to contain_exactly(
51
+ hash_including(message: 'Reset password token is no longer valid.', extensions: { code: 'USER_ERROR' })
52
+ )
53
+ end
54
+ end
55
+
56
+ context 'when password confirmation does not match' do
57
+ let(:password_confirmation) { 'does not match' }
58
+
59
+ it 'returns an error' do
60
+ post_request
61
+
62
+ expect(json_response[:errors]).to contain_exactly(
63
+ hash_including(
64
+ message: 'Unable to update user password',
65
+ extensions: { code: 'USER_ERROR', detailed_errors: ["Password confirmation doesn't match Password"] }
66
+ )
67
+ )
68
+ end
69
+ end
70
+ end
71
+
72
+ context 'when reset password token is not found' do
73
+ let(:token) { user.send(:set_reset_password_token) + 'invalid' }
74
+
75
+ it 'returns an error' do
76
+ post_request
77
+
78
+ expect(json_response[:errors]).to contain_exactly(
79
+ hash_including(message: 'No user found for the specified reset token.', extensions: { code: 'USER_ERROR' })
80
+ )
81
+ end
82
+ end
83
+ end
84
+
85
+ context 'when using the admin model' do
86
+ let(:admin) { create(:admin, :confirmed) }
87
+ let(:query) do
88
+ <<-GRAPHQL
89
+ mutation {
90
+ adminUpdatePasswordWithToken(
91
+ resetPasswordToken: "#{token}",
92
+ password: "#{password}",
93
+ passwordConfirmation: "#{password_confirmation}"
94
+ ) {
95
+ authenticatable { email }
96
+ credentials { uid }
97
+ }
98
+ }
99
+ GRAPHQL
100
+ end
101
+
102
+ context 'when reset password token is valid' do
103
+ let(:token) { admin.send(:set_reset_password_token) }
104
+
105
+ it 'updates the password' do
106
+ expect do
107
+ post_request
108
+ admin.reload
109
+ end.to change(admin, :encrypted_password)
110
+
111
+ expect(admin).to be_valid_password(password)
112
+ expect(json_response[:data][:adminUpdatePasswordWithToken]).to include(
113
+ credentials: { uid: admin.email },
114
+ authenticatable: { email: admin.email }
115
+ )
116
+ end
117
+ end
118
+ end
119
+ end
@@ -89,7 +89,7 @@ RSpec.describe 'Check Password Token Requests' do
89
89
  context 'when reset password token is not found' do
90
90
  let(:token) { user.send(:set_reset_password_token) + 'invalid' }
91
91
 
92
- it 'redirects to redirect url' do
92
+ it 'returns an error message' do
93
93
  get_request
94
94
 
95
95
  expect(json_response[:errors]).to contain_exactly(
@@ -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