graphql_devise 0.11.0 → 0.12.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 (51) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +4 -1
  3. data/.rspec +1 -0
  4. data/.travis.yml +36 -18
  5. data/CHANGELOG.md +56 -0
  6. data/README.md +274 -35
  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/routes.rb +18 -0
  12. data/graphql_devise.gemspec +3 -3
  13. data/lib/generators/graphql_devise/install_generator.rb +63 -30
  14. data/lib/graphql_devise.rb +32 -0
  15. data/lib/graphql_devise/concerns/controller_methods.rb +23 -0
  16. data/lib/graphql_devise/mount_method/operation_preparer.rb +2 -2
  17. data/lib/graphql_devise/mount_method/operation_preparers/resource_name_setter.rb +1 -1
  18. data/lib/graphql_devise/mutations/login.rb +4 -1
  19. data/lib/graphql_devise/mutations/resend_confirmation.rb +4 -1
  20. data/lib/graphql_devise/mutations/send_password_reset.rb +3 -2
  21. data/lib/graphql_devise/mutations/sign_up.rb +1 -9
  22. data/lib/graphql_devise/rails/routes.rb +5 -76
  23. data/lib/graphql_devise/resource_loader.rb +87 -0
  24. data/{app/graphql → lib}/graphql_devise/schema.rb +0 -1
  25. data/lib/graphql_devise/schema_plugin.rb +87 -0
  26. data/lib/graphql_devise/version.rb +1 -1
  27. data/spec/dummy/app/controllers/api/v1/graphql_controller.rb +11 -2
  28. data/spec/dummy/app/controllers/application_controller.rb +1 -0
  29. data/spec/dummy/app/graphql/dummy_schema.rb +9 -0
  30. data/spec/dummy/app/graphql/interpreter_schema.rb +9 -0
  31. data/spec/dummy/app/graphql/types/mutation_type.rb +1 -1
  32. data/spec/dummy/app/graphql/types/query_type.rb +10 -0
  33. data/spec/dummy/config/environments/test.rb +1 -1
  34. data/spec/dummy/config/routes.rb +1 -0
  35. data/spec/generators/graphql_devise/install_generator_spec.rb +62 -30
  36. data/spec/rails_helper.rb +4 -1
  37. data/spec/requests/graphql_controller_spec.rb +80 -0
  38. data/spec/requests/mutations/login_spec.rb +14 -2
  39. data/spec/requests/mutations/resend_confirmation_spec.rb +24 -9
  40. data/spec/requests/mutations/send_password_reset_spec.rb +9 -1
  41. data/spec/requests/mutations/sign_up_spec.rb +10 -0
  42. data/spec/requests/user_controller_spec.rb +180 -24
  43. data/spec/services/mount_method/operation_preparer_spec.rb +2 -2
  44. data/spec/services/mount_method/operation_preparers/custom_operation_preparer_spec.rb +1 -1
  45. data/spec/services/mount_method/operation_preparers/default_operation_preparer_spec.rb +1 -1
  46. data/spec/services/mount_method/operation_preparers/resource_name_setter_spec.rb +1 -1
  47. data/spec/services/resource_loader_spec.rb +82 -0
  48. data/spec/services/schema_plugin_spec.rb +26 -0
  49. data/spec/spec_helper.rb +1 -1
  50. metadata +33 -8
  51. data/spec/support/generators/file_helpers.rb +0 -12
@@ -6,12 +6,13 @@ RSpec.describe 'Login Requests' do
6
6
  let(:password) { '12345678' }
7
7
 
8
8
  context 'when using the user model' do
9
- let(:user) { create(:user, :confirmed, password: password) }
9
+ let!(:user) { create(:user, :confirmed, password: password, email: 'vvega@wallaceinc.com') }
10
+ let(:email) { user.email }
10
11
  let(:query) do
11
12
  <<-GRAPHQL
12
13
  mutation {
13
14
  userLogin(
14
- email: "#{user.email}",
15
+ email: "#{email}",
15
16
  password: "#{password}"
16
17
  ) {
17
18
  user { email name signInCount }
@@ -40,6 +41,17 @@ RSpec.describe 'Login Requests' do
40
41
  )
41
42
  expect(json_response[:errors]).to be_nil
42
43
  end
44
+
45
+ context 'when email address uses different casing' do
46
+ let(:email) { 'vVeGa@wallaceinc.com' }
47
+
48
+ it 'honors devise configuration for case insensitive fields' do
49
+ expect(response).to include_auth_headers
50
+ expect(json_response[:data][:userLogin]).to include(
51
+ user: { email: user.email, name: user.name, signInCount: 1 }
52
+ )
53
+ end
54
+ end
43
55
  end
44
56
 
45
57
  context 'when credentials are invalid' do
@@ -3,7 +3,7 @@ require 'rails_helper'
3
3
  RSpec.describe 'Resend confirmation' do
4
4
  include_context 'with graphql query request'
5
5
 
6
- let(:user) { create(:user, confirmed_at: nil) }
6
+ let!(:user) { create(:user, confirmed_at: nil, email: 'mwallace@wallaceinc.com') }
7
7
  let(:email) { user.email }
8
8
  let(:id) { user.id }
9
9
  let(:redirect) { Faker::Internet.url }
@@ -28,20 +28,20 @@ RSpec.describe 'Resend confirmation' do
28
28
  it 'sends an email to the user with confirmation url and returns a success message' do
29
29
  expect { post_request }.to change(ActionMailer::Base.deliveries, :count).by(1)
30
30
  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.',
31
32
  authenticatable: {
32
- id: id,
33
+ id: id,
33
34
  email: email
34
- },
35
- message: "You will receive an email with instructions for how to confirm your email address in a few minutes."
35
+ }
36
36
  )
37
-
37
+
38
38
  email = Nokogiri::HTML(ActionMailer::Base.deliveries.last.body.encoded)
39
39
  link = email.css('a').first
40
40
  confirm_link_msg_text = email.css('p')[1].inner_html
41
41
  confirm_account_link_text = link.inner_html
42
42
 
43
- expect(confirm_link_msg_text).to eq("You can confirm your account email through the link below:")
44
- expect(confirm_account_link_text).to eq("Confirm my account")
43
+ expect(confirm_link_msg_text).to eq('You can confirm your account email through the link below:')
44
+ expect(confirm_account_link_text).to eq('Confirm my account')
45
45
 
46
46
  # TODO: Move to feature spec
47
47
  expect do
@@ -50,6 +50,21 @@ RSpec.describe 'Resend confirmation' do
50
50
  end.to change(user, :confirmed_at).from(NilClass).to(ActiveSupport::TimeWithZone)
51
51
  end
52
52
 
53
+ context 'when email address uses different casing' do
54
+ let(:email) { 'mWallace@wallaceinc.com' }
55
+
56
+ it 'honors devise configuration for case insensitive fields' do
57
+ expect { post_request }.to change(ActionMailer::Base.deliveries, :count).by(1)
58
+ 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
+ }
64
+ )
65
+ end
66
+ end
67
+
53
68
  context 'when the user has already been confirmed' do
54
69
  before { user.confirm }
55
70
 
@@ -58,7 +73,7 @@ RSpec.describe 'Resend confirmation' do
58
73
  expect(json_response[:data][:userResendConfirmation]).to be_nil
59
74
  expect(json_response[:errors]).to contain_exactly(
60
75
  hash_including(
61
- message: "Email was already confirmed, please try signing in",
76
+ message: 'Email was already confirmed, please try signing in',
62
77
  extensions: { code: 'USER_ERROR' }
63
78
  )
64
79
  )
@@ -74,7 +89,7 @@ RSpec.describe 'Resend confirmation' do
74
89
  expect(json_response[:data][:userResendConfirmation]).to be_nil
75
90
  expect(json_response[:errors]).to contain_exactly(
76
91
  hash_including(
77
- message: "Unable to find user with email '#{email}'.",
92
+ message: "Unable to find user with email '#{email}'.",
78
93
  extensions: { code: 'USER_ERROR' }
79
94
  )
80
95
  )
@@ -3,7 +3,7 @@ require 'rails_helper'
3
3
  RSpec.describe 'Send Password Reset Requests' do
4
4
  include_context 'with graphql query request'
5
5
 
6
- let(:user) { create(:user, :confirmed) }
6
+ let!(:user) { create(:user, :confirmed, email: 'jwinnfield@wallaceinc.com') }
7
7
  let(:email) { user.email }
8
8
  let(:redirect_url) { Faker::Internet.url }
9
9
  let(:query) do
@@ -36,6 +36,14 @@ RSpec.describe 'Send Password Reset Requests' do
36
36
  end
37
37
  end
38
38
 
39
+ context 'when email address uses different casing' do
40
+ let(:email) { 'jWinnfield@wallaceinc.com' }
41
+
42
+ it 'honors devise configuration for case insensitive fields' do
43
+ expect { post_request }.to change(ActionMailer::Base.deliveries, :count).by(1)
44
+ end
45
+ end
46
+
39
47
  context 'when user email is not found' do
40
48
  let(:email) { 'nothere@gmail.com' }
41
49
 
@@ -55,6 +55,16 @@ RSpec.describe 'Sign Up process' do
55
55
  user.reload
56
56
  end.to change { user.active_for_authentication? }.to(true)
57
57
  end
58
+
59
+ context 'when email address uses different casing' do
60
+ let(:email) { 'miaWallace@wallaceinc.com' }
61
+
62
+ it 'honors devise configuration for case insensitive fields' do
63
+ expect { post_request }.to change(ActionMailer::Base.deliveries, :count).by(1)
64
+ expect(User.last.email).to eq('miawallace@wallaceinc.com')
65
+ expect(json_response[:data][:userSignUp]).to include(user: { email: 'miawallace@wallaceinc.com', name: name })
66
+ end
67
+ end
58
68
  end
59
69
 
60
70
  context 'when required params are missing' do
@@ -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: 'USER_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: 'USER_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: 'USER_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: 'USER_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: 'USER_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,7 +13,7 @@ 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' }
16
+ let(:mapping) { :user }
17
17
  let(:selected) { { login: double(:login_default), logout: logout_class } }
18
18
  let(:preparer) { double(:preparer, call: logout_class) }
19
19
  let(:custom) { { login: double(:custom_login, graphql_name: nil) } }
@@ -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,7 +9,7 @@ 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
14
  let(:operations) { { login: login_operation, logout: logout_operation, sign_up: sign_up_operation, confirm: confirm_operation } }
15
15
  let(:custom_keys) { [:login, :logout] }
@@ -5,7 +5,7 @@ RSpec.describe GraphqlDevise::MountMethod::OperationPreparers::ResourceNameSette
5
5
  subject(:prepared_operation) { described_class.new(mapping_name).call(operation) }
6
6
 
7
7
  let(:operation) { double(:operation) }
8
- let(:mapping_name) { 'user' }
8
+ let(:mapping_name) { :user }
9
9
 
10
10
  it 'sets a gql name to the operation' do
11
11
  expect(prepared_operation.instance_variable_get(:@resource_name)).to eq(:user)
@@ -0,0 +1,82 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe GraphqlDevise::ResourceLoader do
4
+ describe '#call' do
5
+ subject(:loader) { described_class.new(resource, options, routing).call(query, mutation) }
6
+
7
+ let(:query) { class_double(GraphQL::Schema::Object) }
8
+ let(:mutation) { class_double(GraphQL::Schema::Object) }
9
+ let(:routing) { false }
10
+ let(:mounted) { false }
11
+ let(:resource) { 'User' }
12
+ let(:options) { { only: [:login, :confirm_account] } }
13
+
14
+ before do
15
+ allow(GraphqlDevise).to receive(:add_mapping).with(:user, resource)
16
+ allow(GraphqlDevise).to receive(:resource_mounted?).with(:user).and_return(mounted)
17
+ allow(GraphqlDevise).to receive(:mount_resource).with(:user)
18
+ end
19
+
20
+ it 'loads operations into the provided types' do
21
+ expect(query).to receive(:field).with(:user_confirm_account, resolver: instance_of(Class), authenticate: false)
22
+ expect(mutation).to receive(:field).with(:user_login, mutation: instance_of(Class), authenticate: false)
23
+ expect(GraphqlDevise).to receive(:add_mapping).with(:user, resource)
24
+ expect(GraphqlDevise).not_to receive(:mount_resource)
25
+
26
+ returned = loader
27
+
28
+ expect(returned).to be_a(Struct)
29
+ end
30
+
31
+ context 'when mutation is nil' do
32
+ let(:mutation) { nil }
33
+
34
+ it 'raises an error' do
35
+ expect { loader }.to raise_error(
36
+ GraphqlDevise::Error,
37
+ 'You need to provide a mutation type unless all mutations are skipped'
38
+ )
39
+ end
40
+ end
41
+
42
+ context 'when query is nil' do
43
+ let(:query) { nil }
44
+
45
+ before { allow(mutation).to receive(:field) }
46
+
47
+ it 'raises an error' do
48
+ expect { loader }.to raise_error(
49
+ GraphqlDevise::Error,
50
+ 'You need to provide a query type unless all queries are skipped'
51
+ )
52
+ end
53
+ end
54
+
55
+ context 'when invoked from router' do
56
+ let(:routing) { true }
57
+
58
+ before do
59
+ allow(query).to receive(:field)
60
+ allow(mutation).to receive(:field)
61
+ end
62
+
63
+ it 'adds mappings' do
64
+ expect(GraphqlDevise).to receive(:add_mapping).with(:user, resource)
65
+ expect(GraphqlDevise).to receive(:mount_resource).with(:user)
66
+
67
+ loader
68
+ end
69
+
70
+ context 'when resource was already mounted' do
71
+ before { allow(GraphqlDevise).to receive(:resource_mounted?).with(:user).and_return(true) }
72
+
73
+ it 'skips schema loading' do
74
+ expect(query).not_to receive(:field)
75
+ expect(mutation).not_to receive(:field)
76
+ expect(GraphqlDevise).not_to receive(:add_mapping).with(:user, resource)
77
+ expect(GraphqlDevise).not_to receive(:mount_resource)
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end