graphql_devise 0.11.1 → 0.12.1

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 (60) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +2 -1
  3. data/.rspec +1 -0
  4. data/.travis.yml +11 -3
  5. data/CHANGELOG.md +43 -0
  6. data/README.md +191 -32
  7. data/app/controllers/graphql_devise/application_controller.rb +4 -1
  8. data/app/controllers/graphql_devise/concerns/set_user_by_token.rb +23 -0
  9. data/app/controllers/graphql_devise/graphql_controller.rb +2 -0
  10. data/app/helpers/graphql_devise/mailer_helper.rb +2 -2
  11. data/config/locales/en.yml +1 -0
  12. data/config/routes.rb +14 -7
  13. data/graphql_devise.gemspec +6 -4
  14. data/lib/generators/graphql_devise/install_generator.rb +63 -30
  15. data/lib/graphql_devise.rb +39 -6
  16. data/lib/graphql_devise/default_operations/mutations.rb +6 -6
  17. data/lib/graphql_devise/default_operations/resolvers.rb +2 -2
  18. data/lib/graphql_devise/errors/authentication_error.rb +7 -0
  19. data/lib/graphql_devise/{detailed_user_error.rb → errors/detailed_user_error.rb} +1 -1
  20. data/lib/graphql_devise/errors/error_codes.rb +6 -0
  21. data/lib/graphql_devise/errors/execution_error.rb +4 -0
  22. data/lib/graphql_devise/{user_error.rb → errors/user_error.rb} +1 -1
  23. data/lib/graphql_devise/mount_method/operation_preparer.rb +2 -2
  24. data/lib/graphql_devise/mount_method/operation_preparers/default_operation_preparer.rb +6 -2
  25. data/lib/graphql_devise/mount_method/operation_preparers/gql_name_setter.rb +1 -1
  26. data/lib/graphql_devise/mount_method/operation_preparers/mutation_field_setter.rb +3 -2
  27. data/lib/graphql_devise/mount_method/operation_preparers/resolver_type_setter.rb +1 -1
  28. data/lib/graphql_devise/mount_method/operation_preparers/resource_name_setter.rb +2 -2
  29. data/lib/graphql_devise/mutations/resend_confirmation.rb +1 -4
  30. data/lib/graphql_devise/mutations/send_password_reset.rb +3 -1
  31. data/lib/graphql_devise/mutations/sign_up.rb +1 -5
  32. data/lib/graphql_devise/rails/routes.rb +5 -67
  33. data/lib/graphql_devise/resource_loader.rb +87 -0
  34. data/lib/graphql_devise/schema_plugin.rb +87 -0
  35. data/lib/graphql_devise/version.rb +1 -1
  36. data/spec/dummy/app/controllers/api/v1/graphql_controller.rb +11 -2
  37. data/spec/dummy/app/controllers/application_controller.rb +1 -0
  38. data/spec/dummy/app/graphql/dummy_schema.rb +9 -0
  39. data/spec/dummy/app/graphql/interpreter_schema.rb +9 -0
  40. data/spec/dummy/app/graphql/types/mutation_type.rb +1 -1
  41. data/spec/dummy/app/graphql/types/query_type.rb +10 -0
  42. data/spec/dummy/config/environments/test.rb +1 -1
  43. data/spec/dummy/config/routes.rb +1 -0
  44. data/spec/generators/graphql_devise/install_generator_spec.rb +62 -30
  45. data/spec/rails_helper.rb +4 -1
  46. data/spec/requests/graphql_controller_spec.rb +80 -0
  47. data/spec/requests/mutations/resend_confirmation_spec.rb +2 -14
  48. data/spec/requests/mutations/send_password_reset_spec.rb +8 -3
  49. data/spec/requests/user_controller_spec.rb +180 -24
  50. data/spec/services/mount_method/operation_preparer_spec.rb +8 -3
  51. data/spec/services/mount_method/operation_preparers/custom_operation_preparer_spec.rb +1 -1
  52. data/spec/services/mount_method/operation_preparers/default_operation_preparer_spec.rb +15 -8
  53. data/spec/services/mount_method/operation_preparers/mutation_field_setter_spec.rb +18 -4
  54. data/spec/services/mount_method/operation_preparers/resource_name_setter_spec.rb +1 -1
  55. data/spec/services/resource_loader_spec.rb +82 -0
  56. data/spec/services/schema_plugin_spec.rb +26 -0
  57. data/spec/spec_helper.rb +1 -1
  58. metadata +107 -89
  59. data/lib/graphql_devise/error_codes.rb +0 -5
  60. data/spec/support/generators/file_helpers.rb +0 -12
@@ -38,5 +38,8 @@ RSpec.configure do |config|
38
38
  config.include(Requests::JsonHelpers, type: :request)
39
39
  config.include(Requests::AuthHelpers, type: :request)
40
40
  config.include(ActiveSupport::Testing::TimeHelpers)
41
- config.include(Generators::FileHelpers, type: :generator)
41
+
42
+ config.before(:suite) do
43
+ ActionController::Base.allow_forgery_protection = true
44
+ end
42
45
  end
@@ -0,0 +1,80 @@
1
+ require 'rails_helper'
2
+
3
+ RSpec.describe GraphqlDevise::GraphqlController do
4
+ let(:password) { 'password123' }
5
+ let(:user) { create(:user, :confirmed, password: password) }
6
+ let(:params) { { query: query, variables: variables } }
7
+ let(:request_params) do
8
+ if Rails::VERSION::MAJOR >= 5
9
+ { params: params }
10
+ else
11
+ params
12
+ end
13
+ end
14
+
15
+ context 'when variables are a string' do
16
+ let(:variables) { "{\"email\": \"#{user.email}\"}" }
17
+ let(:query) { "mutation($email: String!) { userLogin(email: $email, password: \"#{password}\") { user { email name signInCount } } }" }
18
+
19
+ it 'parses the string variables' do
20
+ post '/api/v1/graphql_auth', request_params
21
+
22
+ expect(json_response).to match(
23
+ data: { userLogin: { user: { email: user.email, name: user.name, signInCount: 1 } } }
24
+ )
25
+ end
26
+
27
+ context 'when variables is an empty string' do
28
+ let(:variables) { '' }
29
+ let(:query) { "mutation { userLogin(email: \"#{user.email}\", password: \"#{password}\") { user { email name signInCount } } }" }
30
+
31
+ it 'returns an empty hash as variables' do
32
+ post '/api/v1/graphql_auth', request_params
33
+
34
+ expect(json_response).to match(
35
+ data: { userLogin: { user: { email: user.email, name: user.name, signInCount: 1 } } }
36
+ )
37
+ end
38
+ end
39
+ end
40
+
41
+ context 'when variables are not a string or hash' do
42
+ let(:variables) { 1 }
43
+ let(:query) { "mutation($email: String!) { userLogin(email: $email, password: \"#{password}\") { user { email name signInCount } } }" }
44
+
45
+ it 'raises an error' do
46
+ expect do
47
+ post '/api/v1/graphql_auth', request_params
48
+ end.to raise_error(ArgumentError)
49
+ end
50
+ end
51
+
52
+ context 'when multiplexing queries' do
53
+ let(:params) do
54
+ {
55
+ _json: [
56
+ { query: "mutation { userLogin(email: \"#{user.email}\", password: \"#{password}\") { user { email name signInCount } } }" },
57
+ { query: "mutation { userLogin(email: \"#{user.email}\", password: \"wrong password\") { user { email name signInCount } } }" }
58
+ ]
59
+ }
60
+ end
61
+
62
+ it 'executes multiple queries in the same request' do
63
+ post '/api/v1/graphql_auth', request_params
64
+
65
+ expect(json_response).to match(
66
+ [
67
+ { data: { userLogin: { user: { email: user.email, name: user.name, signInCount: 1 } } } },
68
+ {
69
+ data: { userLogin: nil },
70
+ errors: [
71
+ hash_including(
72
+ message: 'Invalid login credentials. Please try again.', extensions: { code: 'USER_ERROR' }
73
+ )
74
+ ]
75
+ }
76
+ ]
77
+ )
78
+ end
79
+ end
80
+ end
@@ -15,10 +15,6 @@ RSpec.describe 'Resend confirmation' do
15
15
  redirectUrl:"#{redirect}"
16
16
  ) {
17
17
  message
18
- authenticatable {
19
- id
20
- email
21
- }
22
18
  }
23
19
  }
24
20
  GRAPHQL
@@ -28,11 +24,7 @@ RSpec.describe 'Resend confirmation' do
28
24
  it 'sends an email to the user with confirmation url and returns a success message' do
29
25
  expect { post_request }.to change(ActionMailer::Base.deliveries, :count).by(1)
30
26
  expect(json_response[:data][:userResendConfirmation]).to include(
31
- message: 'You will receive an email with instructions for how to confirm your email address in a few minutes.',
32
- authenticatable: {
33
- id: id,
34
- email: email
35
- }
27
+ message: 'You will receive an email with instructions for how to confirm your email address in a few minutes.'
36
28
  )
37
29
 
38
30
  email = Nokogiri::HTML(ActionMailer::Base.deliveries.last.body.encoded)
@@ -56,11 +48,7 @@ RSpec.describe 'Resend confirmation' do
56
48
  it 'honors devise configuration for case insensitive fields' do
57
49
  expect { post_request }.to change(ActionMailer::Base.deliveries, :count).by(1)
58
50
  expect(json_response[:data][:userResendConfirmation]).to include(
59
- message: 'You will receive an email with instructions for how to confirm your email address in a few minutes.',
60
- authenticatable: {
61
- id: id,
62
- email: user.email
63
- }
51
+ message: 'You will receive an email with instructions for how to confirm your email address in a few minutes.'
64
52
  )
65
53
  end
66
54
  end
@@ -13,9 +13,7 @@ RSpec.describe 'Send Password Reset Requests' do
13
13
  email: "#{email}",
14
14
  redirectUrl: "#{redirect_url}"
15
15
  ) {
16
- authenticatable {
17
- email
18
- }
16
+ message
19
17
  }
20
18
  }
21
19
  GRAPHQL
@@ -25,6 +23,10 @@ RSpec.describe 'Send Password Reset Requests' do
25
23
  it 'sends password reset email' do
26
24
  expect { post_request }.to change(ActionMailer::Base.deliveries, :count).by(1)
27
25
 
26
+ expect(json_response[:data][:userSendPasswordReset]).to include(
27
+ message: 'You will receive an email with instructions on how to reset your password in a few minutes.'
28
+ )
29
+
28
30
  email = Nokogiri::HTML(ActionMailer::Base.deliveries.last.body.encoded)
29
31
  link = email.css('a').first
30
32
 
@@ -41,6 +43,9 @@ RSpec.describe 'Send Password Reset Requests' do
41
43
 
42
44
  it 'honors devise configuration for case insensitive fields' do
43
45
  expect { post_request }.to change(ActionMailer::Base.deliveries, :count).by(1)
46
+ expect(json_response[:data][:userSendPasswordReset]).to include(
47
+ message: 'You will receive an email with instructions on how to reset your password in a few minutes.'
48
+ )
44
49
  end
45
50
  end
46
51
 
@@ -1,40 +1,196 @@
1
1
  require 'rails_helper'
2
2
 
3
- RSpec.describe 'Integrations with the user controller' do
3
+ RSpec.describe "Integrations with the user's controller" do
4
4
  include_context 'with graphql query request'
5
5
 
6
6
  let(:user) { create(:user, :confirmed) }
7
- let(:query) do
8
- <<-GRAPHQL
9
- query {
10
- user(
11
- id: #{user.id}
12
- ) {
13
- id
14
- email
7
+
8
+ describe 'publicField' do
9
+ let(:query) do
10
+ <<-GRAPHQL
11
+ query {
12
+ publicField
13
+ }
14
+ GRAPHQL
15
+ end
16
+
17
+ context 'when using a regular schema' do
18
+ before { post_request('/api/v1/graphql') }
19
+
20
+ it 'does not require authentication' do
21
+ expect(json_response[:data][:publicField]).to eq('Field does not require authentication')
22
+ end
23
+ end
24
+
25
+ context 'when using an interpreter schema' do
26
+ before { post_request('/api/v1/interpreter') }
27
+
28
+ it 'does not require authentication' do
29
+ expect(json_response[:data][:publicField]).to eq('Field does not require authentication')
30
+ end
31
+ end
32
+ end
33
+
34
+ describe 'privateField' do
35
+ let(:query) do
36
+ <<-GRAPHQL
37
+ query {
38
+ privateField
15
39
  }
16
- }
17
- GRAPHQL
40
+ GRAPHQL
41
+ end
42
+
43
+ context 'when using a regular schema' do
44
+ before { post_request('/api/v1/graphql') }
45
+
46
+ context 'when user is authenticated' do
47
+ let(:headers) { user.create_new_auth_token }
48
+
49
+ it 'allow to perform the query' do
50
+ expect(json_response[:data][:privateField]).to eq('Field will always require authentication')
51
+ end
52
+ end
53
+
54
+ context 'when user is not authenticated' do
55
+ it 'returns a must sign in error' do
56
+ expect(json_response[:errors]).to contain_exactly(
57
+ hash_including(message: 'privateField field requires authentication', extensions: { code: 'AUTHENTICATION_ERROR' })
58
+ )
59
+ end
60
+ end
61
+ end
62
+
63
+ context 'when using an interpreter schema' do
64
+ before { post_request('/api/v1/interpreter') }
65
+
66
+ context 'when user is authenticated' do
67
+ let(:headers) { user.create_new_auth_token }
68
+
69
+ it 'allow to perform the query' do
70
+ expect(json_response[:data][:privateField]).to eq('Field will always require authentication')
71
+ end
72
+ end
73
+
74
+ context 'when user is not authenticated' do
75
+ it 'returns a must sign in error' do
76
+ expect(json_response[:errors]).to contain_exactly(
77
+ hash_including(message: 'privateField field requires authentication', extensions: { code: 'AUTHENTICATION_ERROR' })
78
+ )
79
+ end
80
+ end
81
+ end
18
82
  end
19
83
 
20
- before { post_request('/api/v1/graphql') }
84
+ describe 'dummyMutation' do
85
+ let(:query) do
86
+ <<-GRAPHQL
87
+ mutation {
88
+ dummyMutation
89
+ }
90
+ GRAPHQL
91
+ end
92
+
93
+ context 'when using a regular schema' do
94
+ before { post_request('/api/v1/graphql') }
95
+
96
+ context 'when user is authenticated' do
97
+ let(:headers) { user.create_new_auth_token }
21
98
 
22
- context 'when user is authenticated' do
23
- let(:headers) { user.create_new_auth_token }
99
+ it 'allow to perform the query' do
100
+ expect(json_response[:data][:dummyMutation]).to eq('Necessary so GraphQL gem does not complain about empty mutation type')
101
+ end
102
+ end
24
103
 
25
- it 'allow to perform the query' do
26
- expect(json_response[:data][:user]).to match(
27
- email: user.email,
28
- id: user.id
29
- )
104
+ context 'when user is not authenticated' do
105
+ it 'returns a must sign in error' do
106
+ expect(json_response[:errors]).to contain_exactly(
107
+ hash_including(message: 'dummyMutation field requires authentication', extensions: { code: 'AUTHENTICATION_ERROR' })
108
+ )
109
+ end
110
+ end
111
+ end
112
+
113
+ context 'when using an interpreter schema' do
114
+ before { post_request('/api/v1/interpreter') }
115
+
116
+ context 'when user is authenticated' do
117
+ let(:headers) { user.create_new_auth_token }
118
+
119
+ it 'allow to perform the query' do
120
+ expect(json_response[:data][:dummyMutation]).to eq('Necessary so GraphQL gem does not complain about empty mutation type')
121
+ end
122
+ end
123
+
124
+ context 'when user is not authenticated' do
125
+ it 'returns a must sign in error' do
126
+ expect(json_response[:errors]).to contain_exactly(
127
+ hash_including(message: 'dummyMutation field requires authentication', extensions: { code: 'AUTHENTICATION_ERROR' })
128
+ )
129
+ end
130
+ end
30
131
  end
31
132
  end
32
133
 
33
- context 'when user is not authenticated' do
34
- it 'returns a must sign in error' do
35
- expect(json_response[:errors]).to contain_exactly(
36
- 'You need to sign in or sign up before continuing.'
37
- )
134
+ describe 'user' do
135
+ let(:query) do
136
+ <<-GRAPHQL
137
+ query {
138
+ user(
139
+ id: #{user.id}
140
+ ) {
141
+ id
142
+ email
143
+ }
144
+ }
145
+ GRAPHQL
146
+ end
147
+
148
+ context 'when using a regular schema' do
149
+ before { post_request('/api/v1/graphql') }
150
+
151
+ context 'when user is authenticated' do
152
+ let(:headers) { user.create_new_auth_token }
153
+
154
+ it 'allow to perform the query' do
155
+ expect(json_response[:data][:user]).to match(
156
+ email: user.email,
157
+ id: user.id
158
+ )
159
+ end
160
+ end
161
+
162
+ context 'when user is not authenticated' do
163
+ it 'returns a must sign in error' do
164
+ expect(json_response[:errors]).to contain_exactly(
165
+ hash_including(message: 'user field requires authentication', extensions: { code: 'AUTHENTICATION_ERROR' })
166
+ )
167
+ end
168
+ end
169
+ end
170
+
171
+ context 'when using an interpreter schema' do
172
+ before { post_request('/api/v1/interpreter') }
173
+
174
+ context 'when user is authenticated' do
175
+ let(:headers) { user.create_new_auth_token }
176
+
177
+ it 'allow to perform the query' do
178
+ expect(json_response[:data][:user]).to match(
179
+ email: user.email,
180
+ id: user.id
181
+ )
182
+ end
183
+ end
184
+
185
+ context 'when user is not authenticated' do
186
+ # Interpreter schema fields are public unless specified otherwise (plugin setting)
187
+ it 'allow to perform the query' do
188
+ expect(json_response[:data][:user]).to match(
189
+ email: user.email,
190
+ id: user.id
191
+ )
192
+ end
193
+ end
38
194
  end
39
195
  end
40
196
  end
@@ -4,7 +4,7 @@ RSpec.describe GraphqlDevise::MountMethod::OperationPreparer do
4
4
  describe '#call' do
5
5
  subject(:prepared_operations) do
6
6
  described_class.new(
7
- resource: resource,
7
+ mapping_name: mapping,
8
8
  selected_operations: selected,
9
9
  preparer: preparer,
10
10
  custom: custom,
@@ -13,11 +13,16 @@ RSpec.describe GraphqlDevise::MountMethod::OperationPreparer do
13
13
  end
14
14
 
15
15
  let(:logout_class) { Class.new(GraphQL::Schema::Resolver) }
16
- let(:resource) { 'User' }
17
- let(:selected) { { login: double(:login_default), logout: logout_class } }
16
+ let(:mapping) { :user }
18
17
  let(:preparer) { double(:preparer, call: logout_class) }
19
18
  let(:custom) { { login: double(:custom_login, graphql_name: nil) } }
20
19
  let(:additional) { { user_additional: double(:user_additional) } }
20
+ let(:selected) do
21
+ {
22
+ login: { klass: double(:login_default) },
23
+ logout:{ klass: logout_class }
24
+ }
25
+ end
21
26
 
22
27
  it 'is expected to return all provided operation keys' do
23
28
  expect(prepared_operations.keys).to contain_exactly(
@@ -6,7 +6,7 @@ RSpec.describe GraphqlDevise::MountMethod::OperationPreparers::CustomOperationPr
6
6
 
7
7
  let(:login_operation) { double(:confirm_operation, graphql_name: nil) }
8
8
  let(:logout_operation) { double(:sign_up_operation, graphql_name: nil) }
9
- let(:mapping_name) { 'user' }
9
+ let(:mapping_name) { :user }
10
10
  let(:operations) { { login: login_operation, logout: logout_operation, invalid: double(:invalid) } }
11
11
  let(:selected_keys) { [:login, :logout, :sign_up, :confirm] }
12
12
 
@@ -9,20 +9,27 @@ RSpec.describe GraphqlDevise::MountMethod::OperationPreparers::DefaultOperationP
9
9
  let(:sign_up_operation) { double(:sign_up_operation, graphql_name: nil) }
10
10
  let(:login_operation) { double(:confirm_operation, graphql_name: nil) }
11
11
  let(:logout_operation) { double(:sign_up_operation, graphql_name: nil) }
12
- let(:mapping_name) { 'user' }
12
+ let(:mapping_name) { :user }
13
13
  let(:preparer) { double(:preparer) }
14
- let(:operations) { { login: login_operation, logout: logout_operation, sign_up: sign_up_operation, confirm: confirm_operation } }
15
14
  let(:custom_keys) { [:login, :logout] }
15
+ let(:operations) do
16
+ {
17
+ confirm: { klass: confirm_operation, authenticatable: false },
18
+ sign_up: { klass: sign_up_operation, authenticatable: true },
19
+ login: { klass: login_operation, authenticatable: true },
20
+ logout: { klass: logout_operation, authenticatable: true }
21
+ }
22
+ end
16
23
 
17
24
  before do
18
25
  allow(default_preparer).to receive(:child_class).with(confirm_operation).and_return(confirm_operation)
19
26
  allow(default_preparer).to receive(:child_class).with(sign_up_operation).and_return(sign_up_operation)
20
27
  allow(default_preparer).to receive(:child_class).with(login_operation).and_return(login_operation)
21
28
  allow(default_preparer).to receive(:child_class).with(logout_operation).and_return(logout_operation)
22
- allow(preparer).to receive(:call).with(confirm_operation).and_return(confirm_operation)
23
- allow(preparer).to receive(:call).with(sign_up_operation).and_return(sign_up_operation)
24
- allow(preparer).to receive(:call).with(login_operation).and_return(login_operation)
25
- allow(preparer).to receive(:call).with(logout_operation).and_return(logout_operation)
29
+ allow(preparer).to receive(:call).with(confirm_operation, authenticatable: false).and_return(confirm_operation)
30
+ allow(preparer).to receive(:call).with(sign_up_operation, authenticatable: true).and_return(sign_up_operation)
31
+ allow(preparer).to receive(:call).with(login_operation, authenticatable: true).and_return(login_operation)
32
+ allow(preparer).to receive(:call).with(logout_operation, authenticatable: true).and_return(logout_operation)
26
33
  end
27
34
 
28
35
  it 'returns only those operations with no custom operation provided' do
@@ -32,8 +39,8 @@ RSpec.describe GraphqlDevise::MountMethod::OperationPreparers::DefaultOperationP
32
39
  it 'prepares default operations' do
33
40
  expect(confirm_operation).to receive(:graphql_name).with('UserConfirm')
34
41
  expect(sign_up_operation).to receive(:graphql_name).with('UserSignUp')
35
- expect(preparer).to receive(:call).with(confirm_operation)
36
- expect(preparer).to receive(:call).with(sign_up_operation)
42
+ expect(preparer).to receive(:call).with(confirm_operation, authenticatable: false)
43
+ expect(preparer).to receive(:call).with(sign_up_operation, authenticatable: true)
37
44
 
38
45
  prepared
39
46