graphql_devise 0.11.4 → 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 (37) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +2 -1
  3. data/CHANGELOG.md +8 -0
  4. data/README.md +187 -20
  5. data/app/controllers/graphql_devise/application_controller.rb +4 -5
  6. data/app/controllers/graphql_devise/concerns/set_user_by_token.rb +23 -0
  7. data/app/controllers/graphql_devise/graphql_controller.rb +2 -0
  8. data/app/helpers/graphql_devise/mailer_helper.rb +2 -2
  9. data/config/routes.rb +2 -0
  10. data/graphql_devise.gemspec +3 -3
  11. data/lib/generators/graphql_devise/install_generator.rb +28 -5
  12. data/lib/graphql_devise.rb +16 -4
  13. data/lib/graphql_devise/mount_method/operation_preparer.rb +2 -2
  14. data/lib/graphql_devise/mount_method/operation_preparers/resource_name_setter.rb +1 -1
  15. data/lib/graphql_devise/mutations/sign_up.rb +1 -5
  16. data/lib/graphql_devise/rails/routes.rb +5 -72
  17. data/lib/graphql_devise/resource_loader.rb +87 -0
  18. data/lib/graphql_devise/schema_plugin.rb +87 -0
  19. data/lib/graphql_devise/version.rb +1 -1
  20. data/spec/dummy/app/controllers/api/v1/graphql_controller.rb +6 -2
  21. data/spec/dummy/app/graphql/dummy_schema.rb +9 -0
  22. data/spec/dummy/app/graphql/interpreter_schema.rb +9 -0
  23. data/spec/dummy/app/graphql/types/mutation_type.rb +1 -1
  24. data/spec/dummy/app/graphql/types/query_type.rb +10 -0
  25. data/spec/dummy/config/routes.rb +1 -0
  26. data/spec/generators/graphql_devise/install_generator_spec.rb +21 -0
  27. data/spec/rails_helper.rb +0 -1
  28. data/spec/requests/graphql_controller_spec.rb +80 -0
  29. data/spec/requests/user_controller_spec.rb +180 -24
  30. data/spec/services/mount_method/operation_preparer_spec.rb +2 -2
  31. data/spec/services/mount_method/operation_preparers/custom_operation_preparer_spec.rb +1 -1
  32. data/spec/services/mount_method/operation_preparers/default_operation_preparer_spec.rb +1 -1
  33. data/spec/services/mount_method/operation_preparers/resource_name_setter_spec.rb +1 -1
  34. data/spec/services/resource_loader_spec.rb +82 -0
  35. data/spec/services/schema_plugin_spec.rb +26 -0
  36. metadata +31 -5
  37. data/spec/support/generators/file_helpers.rb +0 -12
@@ -38,7 +38,6 @@ 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)
42
41
 
43
42
  config.before(:suite) do
44
43
  ActionController::Base.allow_forgery_protection = true
@@ -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
@@ -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
@@ -0,0 +1,26 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe GraphqlDevise::SchemaPlugin do
4
+ describe '#call' do
5
+ subject(:plugin) { described_class.new(query: query, mutation: mutation, resource_loaders: loaders) }
6
+
7
+ let(:query) { instance_double(GraphQL::Schema::Object) }
8
+ let(:mutation) { instance_double(GraphQL::Schema::Object) }
9
+
10
+ context 'when loaders are not provided' do
11
+ let(:loaders) { [] }
12
+
13
+ it 'does not fail' do
14
+ expect { plugin }.not_to raise_error
15
+ end
16
+ end
17
+
18
+ context 'when a loaders is not an instance of loader' do
19
+ let(:loaders) { ['not a loader instance'] }
20
+
21
+ it 'raises an error' do
22
+ expect { plugin }.to raise_error(GraphqlDevise::Error, 'Invalid resource loader instance')
23
+ end
24
+ end
25
+ end
26
+ end