graphql_devise 0.14.0 → 0.16.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +118 -0
  3. data/Appraisals +39 -5
  4. data/CHANGELOG.md +68 -6
  5. data/README.md +150 -51
  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/docs/usage/reset_password_flow.md +90 -0
  14. data/graphql_devise.gemspec +2 -2
  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/mount_method/operation_preparer.rb +6 -6
  19. data/lib/graphql_devise/mount_method/operation_preparers/custom_operation_preparer.rb +6 -4
  20. data/lib/graphql_devise/mount_method/operation_preparers/default_operation_preparer.rb +6 -4
  21. data/lib/graphql_devise/mount_method/operation_preparers/{resource_name_setter.rb → resource_klass_setter.rb} +4 -4
  22. data/lib/graphql_devise/resolvers/confirm_account.rb +1 -1
  23. data/lib/graphql_devise/resource_loader.rb +26 -11
  24. data/lib/graphql_devise/schema_plugin.rb +41 -18
  25. data/lib/graphql_devise/version.rb +1 -1
  26. data/spec/dummy/app/controllers/api/v1/graphql_controller.rb +13 -2
  27. data/spec/dummy/app/graphql/dummy_schema.rb +4 -3
  28. data/spec/dummy/app/graphql/types/query_type.rb +5 -0
  29. data/spec/dummy/config/routes.rb +2 -1
  30. data/spec/dummy/db/migrate/20200623003142_create_schema_users.rb +0 -1
  31. data/spec/dummy/db/migrate/20210516211417_add_vip_to_users.rb +5 -0
  32. data/spec/dummy/db/schema.rb +4 -4
  33. data/spec/generators/graphql_devise/install_generator_spec.rb +1 -1
  34. data/spec/graphql/user_queries_spec.rb +120 -0
  35. data/spec/requests/graphql_controller_spec.rb +12 -11
  36. data/spec/requests/queries/introspection_query_spec.rb +149 -0
  37. data/spec/requests/user_controller_spec.rb +93 -32
  38. data/spec/services/mount_method/operation_preparer_spec.rb +5 -5
  39. data/spec/services/mount_method/operation_preparers/custom_operation_preparer_spec.rb +5 -5
  40. data/spec/services/mount_method/operation_preparers/default_operation_preparer_spec.rb +5 -5
  41. data/spec/services/mount_method/operation_preparers/{resource_name_setter_spec.rb → resource_klass_setter_spec.rb} +6 -6
  42. data/spec/services/resource_loader_spec.rb +5 -5
  43. data/spec/support/contexts/graphql_request.rb +11 -3
  44. data/spec/support/contexts/schema_test.rb +14 -0
  45. metadata +25 -14
  46. data/.travis.yml +0 -79
@@ -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,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
@@ -5,6 +5,14 @@ require 'rails_helper'
5
5
  RSpec.describe "Integrations with the user's controller" do
6
6
  include_context 'with graphql query request'
7
7
 
8
+ shared_examples 'returns a must authenticate error' do |field|
9
+ it 'returns a must sign in error' do
10
+ expect(json_response[:errors]).to contain_exactly(
11
+ hash_including(message: "#{field} field requires authentication", extensions: { code: 'AUTHENTICATION_ERROR' })
12
+ )
13
+ end
14
+ end
15
+
8
16
  let(:user) { create(:user, :confirmed) }
9
17
 
10
18
  describe 'publicField' do
@@ -31,15 +39,6 @@ RSpec.describe "Integrations with the user's controller" do
31
39
  expect(json_response[:data][:publicField]).to eq('Field does not require authentication')
32
40
  end
33
41
  end
34
-
35
- context 'when using the failing route' do
36
- it 'raises an invalid resource_name error' do
37
- expect { post_request('/api/v1/failing') }.to raise_error(
38
- GraphqlDevise::Error,
39
- 'Invalid resource_name `fail` provided to `graphql_context`. Possible values are: [:user, :admin, :guest, :users_customer, :schema_user].'
40
- )
41
- end
42
- end
43
42
  end
44
43
 
45
44
  describe 'privateField' do
@@ -51,6 +50,22 @@ RSpec.describe "Integrations with the user's controller" do
51
50
  GRAPHQL
52
51
  end
53
52
 
53
+ context 'when authenticating before using the GQL schema' do
54
+ before { post_request('/api/v1/controller_auth') }
55
+
56
+ context 'when user is authenticated' do
57
+ let(:headers) { create(:schema_user).create_new_auth_token }
58
+
59
+ it 'allows authentication at the controller level' do
60
+ expect(json_response[:data][:privateField]).to eq('Field will always require authentication')
61
+ end
62
+ end
63
+
64
+ context 'when user is not authenticated' do
65
+ it_behaves_like 'returns a must authenticate error', 'privateField'
66
+ end
67
+ end
68
+
54
69
  context 'when using a regular schema' do
55
70
  before { post_request('/api/v1/graphql') }
56
71
 
@@ -71,9 +86,14 @@ RSpec.describe "Integrations with the user's controller" do
71
86
  end
72
87
 
73
88
  context 'when user is not authenticated' do
74
- it 'returns a must sign in error' do
75
- expect(json_response[:errors]).to contain_exactly(
76
- hash_including(message: 'privateField field requires authentication', extensions: { code: 'AUTHENTICATION_ERROR' })
89
+ it_behaves_like 'returns a must authenticate error', 'privateField'
90
+ end
91
+
92
+ context 'when using the failing route' do
93
+ it 'raises an invalid resource_name error' do
94
+ expect { post_request('/api/v1/failing') }.to raise_error(
95
+ GraphqlDevise::Error,
96
+ 'Invalid resource_name `fail` provided to `graphql_context`. Possible values are: [:user, :admin, :guest, :users_customer, :schema_user].'
77
97
  )
78
98
  end
79
99
  end
@@ -91,11 +111,7 @@ RSpec.describe "Integrations with the user's controller" do
91
111
  end
92
112
 
93
113
  context 'when user is not authenticated' do
94
- it 'returns a must sign in error' do
95
- expect(json_response[:errors]).to contain_exactly(
96
- hash_including(message: 'privateField field requires authentication', extensions: { code: 'AUTHENTICATION_ERROR' })
97
- )
98
- end
114
+ it_behaves_like 'returns a must authenticate error', 'privateField'
99
115
  end
100
116
  end
101
117
  end
@@ -121,11 +137,7 @@ RSpec.describe "Integrations with the user's controller" do
121
137
  end
122
138
 
123
139
  context 'when user is not authenticated' do
124
- it 'returns a must sign in error' do
125
- expect(json_response[:errors]).to contain_exactly(
126
- hash_including(message: 'dummyMutation field requires authentication', extensions: { code: 'AUTHENTICATION_ERROR' })
127
- )
128
- end
140
+ it_behaves_like 'returns a must authenticate error', 'dummyMutation'
129
141
  end
130
142
  end
131
143
 
@@ -141,11 +153,7 @@ RSpec.describe "Integrations with the user's controller" do
141
153
  end
142
154
 
143
155
  context 'when user is not authenticated' do
144
- it 'returns a must sign in error' do
145
- expect(json_response[:errors]).to contain_exactly(
146
- hash_including(message: 'dummyMutation field requires authentication', extensions: { code: 'AUTHENTICATION_ERROR' })
147
- )
148
- end
156
+ it_behaves_like 'returns a must authenticate error', 'dummyMutation'
149
157
  end
150
158
  end
151
159
  end
@@ -179,11 +187,7 @@ RSpec.describe "Integrations with the user's controller" do
179
187
  end
180
188
 
181
189
  context 'when user is not authenticated' do
182
- it 'returns a must sign in error' do
183
- expect(json_response[:errors]).to contain_exactly(
184
- hash_including(message: 'user field requires authentication', extensions: { code: 'AUTHENTICATION_ERROR' })
185
- )
186
- end
190
+ it_behaves_like 'returns a must authenticate error', 'user'
187
191
  end
188
192
  end
189
193
 
@@ -251,4 +255,61 @@ RSpec.describe "Integrations with the user's controller" do
251
255
  )
252
256
  end
253
257
  end
258
+
259
+ describe 'vipField' do
260
+ let(:error_message) { 'Field available only for VIP Users' }
261
+ let(:query) do
262
+ <<-GRAPHQL
263
+ query { vipField }
264
+ GRAPHQL
265
+ end
266
+
267
+ context 'when using a regular schema' do
268
+ before { post_request('/api/v1/graphql') }
269
+
270
+ context 'when user is authenticated' do
271
+ let(:headers) { user.create_new_auth_token }
272
+
273
+ context 'when schema user is VIP' do
274
+ let(:user) { create(:user, :confirmed, vip: true) }
275
+
276
+ it 'allows to perform the query' do
277
+ expect(json_response[:data][:vipField]).to eq(error_message)
278
+ end
279
+ end
280
+
281
+ context 'when schema user is not VIP' do
282
+ it_behaves_like 'returns a must authenticate error', 'vipField'
283
+ end
284
+ end
285
+
286
+ context 'when user is not authenticated' do
287
+ it_behaves_like 'returns a must authenticate error', 'vipField'
288
+ end
289
+ end
290
+
291
+ context 'when using the interpreter schema' do
292
+ before { post_request('/api/v1/interpreter') }
293
+
294
+ context 'when user is authenticated' do
295
+ let(:headers) { user.create_new_auth_token }
296
+
297
+ context 'when schema user is VIP' do
298
+ let(:user) { create(:user, :confirmed, vip: true) }
299
+
300
+ it 'allows to perform the query' do
301
+ expect(json_response[:data][:vipField]).to eq(error_message)
302
+ end
303
+ end
304
+
305
+ context 'when schema user is not VIP' do
306
+ it_behaves_like 'returns a must authenticate error', 'vipField'
307
+ end
308
+ end
309
+
310
+ context 'when user is not authenticated' do
311
+ it_behaves_like 'returns a must authenticate error', 'vipField'
312
+ end
313
+ end
314
+ end
254
315
  end
@@ -1,12 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'spec_helper'
3
+ require 'rails_helper'
4
4
 
5
5
  RSpec.describe GraphqlDevise::MountMethod::OperationPreparer do
6
6
  describe '#call' do
7
7
  subject(:prepared_operations) do
8
8
  described_class.new(
9
- mapping_name: mapping,
9
+ model: model,
10
10
  selected_operations: selected,
11
11
  preparer: preparer,
12
12
  custom: custom,
@@ -15,14 +15,14 @@ RSpec.describe GraphqlDevise::MountMethod::OperationPreparer do
15
15
  end
16
16
 
17
17
  let(:logout_class) { Class.new(GraphQL::Schema::Resolver) }
18
- let(:mapping) { :user }
18
+ let(:model) { User }
19
19
  let(:preparer) { double(:preparer, call: logout_class) }
20
20
  let(:custom) { { login: double(:custom_login, graphql_name: nil) } }
21
21
  let(:additional) { { user_additional: double(:user_additional) } }
22
22
  let(:selected) do
23
23
  {
24
- login: { klass: double(:login_default) },
25
- logout:{ klass: logout_class }
24
+ login: { klass: double(:login_default) },
25
+ logout: { klass: logout_class }
26
26
  }
27
27
  end
28
28
 
@@ -1,14 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'spec_helper'
3
+ require 'rails_helper'
4
4
 
5
5
  RSpec.describe GraphqlDevise::MountMethod::OperationPreparers::CustomOperationPreparer do
6
6
  describe '#call' do
7
- subject(:prepared) { described_class.new(selected_keys: selected_keys, custom_operations: operations, mapping_name: mapping_name).call }
7
+ subject(:prepared) { described_class.new(selected_keys: selected_keys, custom_operations: operations, model: model).call }
8
8
 
9
9
  let(:login_operation) { double(:confirm_operation, graphql_name: nil) }
10
10
  let(:logout_operation) { double(:sign_up_operation, graphql_name: nil) }
11
- let(:mapping_name) { :user }
11
+ let(:model) { User }
12
12
  let(:operations) { { login: login_operation, logout: logout_operation, invalid: double(:invalid) } }
13
13
  let(:selected_keys) { [:login, :logout, :sign_up, :confirm] }
14
14
 
@@ -22,8 +22,8 @@ RSpec.describe GraphqlDevise::MountMethod::OperationPreparers::CustomOperationPr
22
22
 
23
23
  prepared
24
24
 
25
- expect(login_operation.instance_variable_get(:@resource_name)).to eq(:user)
26
- expect(logout_operation.instance_variable_get(:@resource_name)).to eq(:user)
25
+ expect(login_operation.instance_variable_get(:@resource_klass)).to eq(User)
26
+ expect(logout_operation.instance_variable_get(:@resource_klass)).to eq(User)
27
27
  end
28
28
 
29
29
  context 'when no selected keys are provided' do