decidim-api 0.30.2 → 0.31.0.rc1
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.
- checksums.yaml +4 -4
- data/app/controllers/decidim/api/application_controller.rb +20 -0
- data/app/controllers/decidim/api/queries_controller.rb +33 -1
- data/app/controllers/decidim/api/sessions_controller.rb +61 -0
- data/app/models/decidim/api/api_user.rb +82 -0
- data/app/models/decidim/api/jwt_denylist.rb +11 -0
- data/app/packs/entrypoints/decidim_api_graphiql.js +2 -1
- data/app/presenters/decidim/api/api_user_presenter.rb +23 -0
- data/config/assets.rb +2 -2
- data/config/initializers/devise.rb +26 -0
- data/config/routes.rb +8 -0
- data/decidim-api.gemspec +2 -1
- data/docs/usage.md +98 -8
- data/lib/decidim/api/component_mutation_type.rb +19 -0
- data/lib/decidim/api/devise.rb +12 -0
- data/lib/decidim/api/engine.rb +2 -2
- data/lib/decidim/api/graphql_permissions.rb +125 -0
- data/lib/decidim/api/mutation_type.rb +10 -0
- data/lib/decidim/api/required_scopes.rb +31 -0
- data/lib/decidim/api/test/component_context.rb +17 -18
- data/lib/decidim/api/test/factories.rb +33 -0
- data/lib/decidim/api/test/mutation_context.rb +38 -0
- data/lib/decidim/api/test/shared_examples/amendable_interface_examples.rb +14 -0
- data/lib/decidim/api/test/shared_examples/amendable_proposals_interface_examples.rb +50 -0
- data/lib/decidim/api/test/shared_examples/attachable_interface_examples.rb +40 -0
- data/lib/decidim/api/test/shared_examples/authorable_interface_examples.rb +46 -0
- data/lib/decidim/api/test/shared_examples/categories_container_examples.rb +22 -0
- data/lib/decidim/api/test/shared_examples/categorizable_interface_examples.rb +27 -0
- data/lib/decidim/api/test/shared_examples/coauthorable_interface_examples.rb +77 -0
- data/lib/decidim/api/test/shared_examples/commentable_interface_examples.rb +13 -0
- data/lib/decidim/api/test/shared_examples/fingerprintable_interface_examples.rb +17 -0
- data/lib/decidim/api/test/shared_examples/followable_interface_examples.rb +13 -0
- data/lib/decidim/api/test/shared_examples/input_filter_examples.rb +77 -0
- data/lib/decidim/api/test/shared_examples/input_sort_examples.rb +126 -0
- data/lib/decidim/api/test/shared_examples/likeable_interface_examples.rb +22 -0
- data/lib/decidim/api/test/shared_examples/localizable_interface_examples.rb +29 -0
- data/lib/decidim/api/test/shared_examples/participatory_space_resourcable_interface_examples.rb +61 -0
- data/lib/decidim/api/test/shared_examples/referable_interface_examples.rb +13 -0
- data/lib/decidim/api/test/shared_examples/scopable_interface_examples.rb +19 -0
- data/lib/decidim/api/test/shared_examples/statistics_examples.rb +30 -16
- data/lib/decidim/api/test/shared_examples/taxonomizable_interface_examples.rb +20 -0
- data/lib/decidim/api/test/shared_examples/timestamps_interface_examples.rb +21 -0
- data/lib/decidim/api/test/shared_examples/traceable_interface_examples.rb +49 -0
- data/lib/decidim/api/test/type_context.rb +9 -1
- data/lib/decidim/api/test.rb +22 -0
- data/lib/decidim/api/types/base_mutation.rb +5 -1
- data/lib/decidim/api/types/base_object.rb +4 -69
- data/lib/decidim/api/types.rb +3 -0
- data/lib/decidim/api/version.rb +1 -1
- data/lib/decidim/api.rb +25 -5
- data/lib/devise/models/api_authenticatable.rb +30 -0
- data/lib/devise/strategies/api_authenticatable.rb +21 -0
- data/lib/warden/jwt_auth/decidim_overrides.rb +42 -0
- metadata +66 -12
@@ -0,0 +1,125 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Decidim
|
4
|
+
module Api
|
5
|
+
# Adds methods to Authorize the API objects.
|
6
|
+
module GraphqlPermissions
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
include Decidim::Api::RequiredScopes
|
10
|
+
|
11
|
+
class_methods do
|
12
|
+
def authorized?(object, context)
|
13
|
+
return false unless scope_authorized?(context)
|
14
|
+
|
15
|
+
chain = []
|
16
|
+
|
17
|
+
subject = determine_subject_name(object)
|
18
|
+
context[subject] = object
|
19
|
+
|
20
|
+
chain.unshift(allowed_to?(:read, :participatory_space, object, context)) if object.respond_to?(:participatory_space)
|
21
|
+
chain.unshift(allowed_to?(:read, :component, object, context)) if object.respond_to?(:component) && object.component.present?
|
22
|
+
|
23
|
+
super && chain.all?
|
24
|
+
end
|
25
|
+
|
26
|
+
def determine_subject_name(object)
|
27
|
+
object.class.name.split("::").last.underscore.to_sym
|
28
|
+
end
|
29
|
+
|
30
|
+
# This is a simplified adaptation of allowed_to? from NeedsPermission concern
|
31
|
+
# @param action [Symbol] The action performed. Most cases the action is :read
|
32
|
+
# @param subject [Object] The name of the subject. Ex: :participatory_space, :component, or object
|
33
|
+
# @param object [ActiveModel::Base] The object that is being represented.
|
34
|
+
# @param context [GraphQL::Query::Context] The GraphQL context
|
35
|
+
#
|
36
|
+
# @return Boolean
|
37
|
+
def allowed_to?(action, subject, object, context, scope: :public)
|
38
|
+
unless subject.is_a?(::Symbol)
|
39
|
+
subject = determine_subject_name(object)
|
40
|
+
context[subject] = object
|
41
|
+
end
|
42
|
+
|
43
|
+
permission_action = Decidim::PermissionAction.new(scope:, action:, subject:)
|
44
|
+
|
45
|
+
permission_chain(object).inject(permission_action) do |current_permission_action, permission_class|
|
46
|
+
permission_context =
|
47
|
+
if scope == :admin
|
48
|
+
local_admin_context(object, context)
|
49
|
+
else
|
50
|
+
local_context(object, context)
|
51
|
+
end
|
52
|
+
|
53
|
+
permission_class.new(
|
54
|
+
context[:current_user],
|
55
|
+
current_permission_action,
|
56
|
+
permission_context
|
57
|
+
).permissions
|
58
|
+
end.allowed?
|
59
|
+
end
|
60
|
+
|
61
|
+
# Injects into context object current_participatory_space and current_component keys as they are needed
|
62
|
+
#
|
63
|
+
# @param object [ActiveModel::Base] The object that is being represented.
|
64
|
+
# @param context [GraphQL::Query::Context] The GraphQL context
|
65
|
+
#
|
66
|
+
# @return Hash
|
67
|
+
def local_context(object, context)
|
68
|
+
context[:current_participatory_space] = object.participatory_space if object.respond_to?(:participatory_space)
|
69
|
+
context[:current_component] =
|
70
|
+
if object.is_a?(Decidim::Component)
|
71
|
+
object
|
72
|
+
elsif object.respond_to?(:component)
|
73
|
+
object.component
|
74
|
+
end
|
75
|
+
|
76
|
+
context.to_h
|
77
|
+
end
|
78
|
+
|
79
|
+
def local_admin_context(object, context)
|
80
|
+
context = local_context(object, context)
|
81
|
+
|
82
|
+
component = context[:current_component]
|
83
|
+
return context unless component
|
84
|
+
return context unless component.respond_to?(:current_settings)
|
85
|
+
return context unless component.respond_to?(:settings)
|
86
|
+
return context unless component.respond_to?(:organization)
|
87
|
+
|
88
|
+
context[:current_settings] = component.current_settings
|
89
|
+
context[:component_settings] = component.settings
|
90
|
+
context[:current_organization] = component.organization
|
91
|
+
|
92
|
+
context
|
93
|
+
end
|
94
|
+
|
95
|
+
# Creates the permission chain arrau that contains all the permission classes required to authorize a certain resource
|
96
|
+
# We are using unshift as we need the Admin and base permissions to be last in the chain
|
97
|
+
# @param object [ActiveModel::Base] The object that is being represented.
|
98
|
+
#
|
99
|
+
# @return [Decidim::DefaultPermissions]
|
100
|
+
def permission_chain(object)
|
101
|
+
permissions = [
|
102
|
+
Decidim::Admin::Permissions,
|
103
|
+
Decidim::Permissions
|
104
|
+
]
|
105
|
+
|
106
|
+
if object.is_a?(Decidim::Component)
|
107
|
+
permissions.unshift(object.participatory_space.manifest.permissions_class)
|
108
|
+
permissions.unshift(object.manifest.permissions_class)
|
109
|
+
else
|
110
|
+
permissions.unshift(object.participatory_space.manifest.permissions_class) if object.respond_to?(:participatory_space)
|
111
|
+
permissions.unshift(object.component.manifest.permissions_class) if object.respond_to?(:component) && object.component.present?
|
112
|
+
end
|
113
|
+
|
114
|
+
permissions
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
private
|
119
|
+
|
120
|
+
delegate :allowed_to?, to: :class
|
121
|
+
|
122
|
+
attr_reader :action
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
@@ -5,6 +5,16 @@ module Decidim
|
|
5
5
|
# This type represents the root mutation type of the whole API
|
6
6
|
class MutationType < Decidim::Api::Types::BaseObject
|
7
7
|
description "The root mutation of this schema"
|
8
|
+
|
9
|
+
required_scopes "api:write"
|
10
|
+
|
11
|
+
field :component, Decidim::Api::ComponentMutationType, "The component of this schema", null: false do
|
12
|
+
argument :id, GraphQL::Types::ID, "The Comment's unique ID", required: true
|
13
|
+
end
|
14
|
+
|
15
|
+
def component(id:)
|
16
|
+
context[:current_organization]&.published_components&.find(id)
|
17
|
+
end
|
8
18
|
end
|
9
19
|
end
|
10
20
|
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Decidim
|
4
|
+
module Api
|
5
|
+
# Adds methods to the API objects for validating the API scopes.
|
6
|
+
module RequiredScopes
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
class_methods do
|
10
|
+
def required_scopes(*scopes)
|
11
|
+
@required_scopes = scopes
|
12
|
+
end
|
13
|
+
|
14
|
+
def determine_required_scopes
|
15
|
+
return @required_scopes if @required_scopes.present?
|
16
|
+
return unless superclass.respond_to?(:determine_required_scopes)
|
17
|
+
|
18
|
+
superclass.determine_required_scopes
|
19
|
+
end
|
20
|
+
|
21
|
+
def scope_authorized?(context)
|
22
|
+
req_scopes = determine_required_scopes
|
23
|
+
return true unless req_scopes.is_a?(Array)
|
24
|
+
|
25
|
+
scopes = context[:scopes] # ::Doorkeeper::OAuth::Scopes
|
26
|
+
scopes.scopes?(req_scopes)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -1,7 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "decidim/api/test/type_context"
|
4
|
-
|
5
3
|
shared_context "with a graphql decidim component" do
|
6
4
|
include_context "with a graphql class type"
|
7
5
|
|
@@ -24,6 +22,7 @@ shared_context "with a graphql decidim component" do
|
|
24
22
|
name {
|
25
23
|
translation(locale: "#{locale}")
|
26
24
|
}
|
25
|
+
url
|
27
26
|
weight
|
28
27
|
__typename
|
29
28
|
...fooComponent
|
@@ -106,9 +105,9 @@ shared_examples "with resource visibility" do
|
|
106
105
|
it_behaves_like "graphQL visible resource"
|
107
106
|
end
|
108
107
|
|
109
|
-
context "when the user is space
|
108
|
+
context "when the user is space evaluator" do
|
110
109
|
let!(:current_user) { create(:user, :confirmed, organization: current_organization) }
|
111
|
-
let!(:role) { create(:participatory_process_user_role, participatory_process:, user: current_user, role: "
|
110
|
+
let!(:role) { create(:participatory_process_user_role, participatory_process:, user: current_user, role: "evaluator") }
|
112
111
|
it_behaves_like "graphQL visible resource"
|
113
112
|
end
|
114
113
|
|
@@ -158,9 +157,9 @@ shared_examples "with resource visibility" do
|
|
158
157
|
it_behaves_like "graphQL hidden component"
|
159
158
|
end
|
160
159
|
|
161
|
-
context "when the user is space
|
160
|
+
context "when the user is space evaluator" do
|
162
161
|
let!(:current_user) { create(:user, :confirmed, organization: current_organization) }
|
163
|
-
let!(:role) { create(:participatory_process_user_role, participatory_process:, user: current_user, role: "
|
162
|
+
let!(:role) { create(:participatory_process_user_role, participatory_process:, user: current_user, role: "evaluator") }
|
164
163
|
it_behaves_like "graphQL visible resource"
|
165
164
|
end
|
166
165
|
|
@@ -228,9 +227,9 @@ shared_examples "with resource visibility" do
|
|
228
227
|
it_behaves_like "graphQL visible resource"
|
229
228
|
end
|
230
229
|
|
231
|
-
context "when the user is space
|
230
|
+
context "when the user is space evaluator" do
|
232
231
|
let!(:current_user) { create(:user, :confirmed, organization: current_organization) }
|
233
|
-
let!(:role) { create(:assembly_user_role, assembly: participatory_process, user: current_user, role: "
|
232
|
+
let!(:role) { create(:assembly_user_role, assembly: participatory_process, user: current_user, role: "evaluator") }
|
234
233
|
it_behaves_like "graphQL visible resource"
|
235
234
|
end
|
236
235
|
|
@@ -274,9 +273,9 @@ shared_examples "with resource visibility" do
|
|
274
273
|
it_behaves_like "graphQL hidden component"
|
275
274
|
end
|
276
275
|
|
277
|
-
context "when the user is space
|
276
|
+
context "when the user is space evaluator" do
|
278
277
|
let!(:current_user) { create(:user, :confirmed, organization: current_organization) }
|
279
|
-
let!(:role) { create(:assembly_user_role, assembly: participatory_process, user: current_user, role: "
|
278
|
+
let!(:role) { create(:assembly_user_role, assembly: participatory_process, user: current_user, role: "evaluator") }
|
280
279
|
it_behaves_like "graphQL visible resource"
|
281
280
|
end
|
282
281
|
|
@@ -325,9 +324,9 @@ shared_examples "with resource visibility" do
|
|
325
324
|
it_behaves_like "graphQL hidden space"
|
326
325
|
end
|
327
326
|
|
328
|
-
context "when the user is space
|
327
|
+
context "when the user is space evaluator" do
|
329
328
|
let!(:current_user) { create(:user, :confirmed, organization: current_organization) }
|
330
|
-
let!(:role) { create(:participatory_process_user_role, participatory_process:, user: current_user, role: "
|
329
|
+
let!(:role) { create(:participatory_process_user_role, participatory_process:, user: current_user, role: "evaluator") }
|
331
330
|
it_behaves_like "graphQL hidden space"
|
332
331
|
end
|
333
332
|
|
@@ -368,9 +367,9 @@ shared_examples "with resource visibility" do
|
|
368
367
|
it_behaves_like "graphQL hidden space"
|
369
368
|
end
|
370
369
|
|
371
|
-
context "when the user is space
|
370
|
+
context "when the user is space evaluator" do
|
372
371
|
let!(:current_user) { create(:user, :confirmed, organization: current_organization) }
|
373
|
-
let!(:role) { create(:participatory_process_user_role, participatory_process:, user: current_user, role: "
|
372
|
+
let!(:role) { create(:participatory_process_user_role, participatory_process:, user: current_user, role: "evaluator") }
|
374
373
|
it_behaves_like "graphQL hidden space"
|
375
374
|
end
|
376
375
|
it_behaves_like "graphQL space hidden to visitor"
|
@@ -413,9 +412,9 @@ shared_examples "with resource visibility" do
|
|
413
412
|
it_behaves_like "graphQL hidden space"
|
414
413
|
end
|
415
414
|
|
416
|
-
context "when the user is space
|
415
|
+
context "when the user is space evaluator" do
|
417
416
|
let!(:current_user) { create(:user, :confirmed, organization: current_organization) }
|
418
|
-
let!(:role) { create(:participatory_process_user_role, participatory_process:, user: current_user, role: "
|
417
|
+
let!(:role) { create(:participatory_process_user_role, participatory_process:, user: current_user, role: "evaluator") }
|
419
418
|
it_behaves_like "graphQL hidden space"
|
420
419
|
end
|
421
420
|
|
@@ -456,9 +455,9 @@ shared_examples "with resource visibility" do
|
|
456
455
|
it_behaves_like "graphQL hidden space"
|
457
456
|
end
|
458
457
|
|
459
|
-
context "when the user is space
|
458
|
+
context "when the user is space evaluator" do
|
460
459
|
let!(:current_user) { create(:user, :confirmed, organization: current_organization) }
|
461
|
-
let!(:role) { create(:participatory_process_user_role, participatory_process:, user: current_user, role: "
|
460
|
+
let!(:role) { create(:participatory_process_user_role, participatory_process:, user: current_user, role: "evaluator") }
|
462
461
|
it_behaves_like "graphQL hidden space"
|
463
462
|
end
|
464
463
|
it_behaves_like "graphQL space hidden to visitor"
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
FactoryBot.define do
|
4
|
+
sequence :api_key do |n|
|
5
|
+
"api_user_#{n}"
|
6
|
+
end
|
7
|
+
|
8
|
+
factory :api_user, class: "Decidim::Api::ApiUser" do
|
9
|
+
transient do
|
10
|
+
api_secret { "decidim123456789" }
|
11
|
+
end
|
12
|
+
|
13
|
+
name { generate(:name) }
|
14
|
+
api_key { generate(:api_key) }
|
15
|
+
nickname { generate(:nickname) }
|
16
|
+
organization
|
17
|
+
locale { organization.default_locale }
|
18
|
+
tos_agreement { "1" }
|
19
|
+
confirmation_sent_at { Time.current }
|
20
|
+
accepted_tos_version { organization.tos_version }
|
21
|
+
admin { true }
|
22
|
+
admin_terms_accepted_at { Time.current }
|
23
|
+
notifications_sending_frequency { "none" }
|
24
|
+
email_on_moderations { false }
|
25
|
+
password_updated_at { Time.current }
|
26
|
+
previous_passwords { [] }
|
27
|
+
extended_data { {} }
|
28
|
+
|
29
|
+
after(:build) do |user, evaluator|
|
30
|
+
user.api_secret = evaluator.api_secret
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "decidim/api/test/type_context"
|
4
|
+
shared_context "with a graphql class mutation" do
|
5
|
+
include_context "with a graphql class type"
|
6
|
+
|
7
|
+
let!(:current_user) do
|
8
|
+
case user_type
|
9
|
+
when :admin
|
10
|
+
create(:user, :admin, :confirmed, organization: current_organization)
|
11
|
+
when :api_user
|
12
|
+
create(:api_user, organization: current_organization)
|
13
|
+
else
|
14
|
+
create(:user, :confirmed, organization: current_organization)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
let(:user_type) { :user }
|
18
|
+
let(:api_scopes) do
|
19
|
+
if user_type == :api_user
|
20
|
+
Doorkeeper::OAuth::Scopes.from_array(["api:read", "api:write"])
|
21
|
+
else
|
22
|
+
Doorkeeper::OAuth::Scopes.from_array(Doorkeeper.config.scopes.all)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
let(:schema) do
|
27
|
+
klass = type_class
|
28
|
+
field_name = klass.graphql_name.underscore.to_sym
|
29
|
+
root = root_klass
|
30
|
+
Class.new(Decidim::Api::Schema) do
|
31
|
+
mutation(Class.new(root) do
|
32
|
+
graphql_name klass.graphql_name
|
33
|
+
|
34
|
+
field field_name, mutation: klass
|
35
|
+
end)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "spec_helper"
|
4
|
+
|
5
|
+
shared_examples_for "amendable interface" do
|
6
|
+
describe "amendments" do
|
7
|
+
let(:query) { "{ amendments { id } }" }
|
8
|
+
|
9
|
+
it "includes the amendments id" do
|
10
|
+
amendments_ids = response["amendments"].map { |amendment| amendment["id"].to_i }
|
11
|
+
expect(amendments_ids).to include(*model.amendments.map(&:id))
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "spec_helper"
|
4
|
+
|
5
|
+
shared_examples_for "amendable proposals interface" do
|
6
|
+
describe "amendments" do
|
7
|
+
let(:query) do
|
8
|
+
'{ amendments {
|
9
|
+
state
|
10
|
+
amendable { ...on Proposal { title { translation(locale: "en") } } }
|
11
|
+
amendableType
|
12
|
+
emendation { ...on Proposal { title { translation(locale: "en") } } }
|
13
|
+
emendationType
|
14
|
+
amender { name }
|
15
|
+
} }'
|
16
|
+
end
|
17
|
+
|
18
|
+
it "includes the amendments states" do
|
19
|
+
amendments_states = response["amendments"].map { |amendment| amendment["state"] }
|
20
|
+
expect(amendments_states).to include(*model.amendments.map(&:state))
|
21
|
+
end
|
22
|
+
|
23
|
+
it "amendable types matches Proposals Type" do
|
24
|
+
response["amendments"].each do |amendment|
|
25
|
+
expect(amendment["amendableType"]).to eq("Decidim::Proposals::Proposal")
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
it "emendation types matches Proposals Type" do
|
30
|
+
response["amendments"].each do |amendment|
|
31
|
+
expect(amendment["emendationType"]).to eq("Decidim::Proposals::Proposal")
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
it "returns amendable as parent proposal" do
|
36
|
+
amendment_amendables = response["amendments"].map { |amendment| amendment["amendable"] }.map { |title| title["title"]["translation"] }
|
37
|
+
expect(amendment_amendables).to include(*model.amendments.map(&:amendable).map { |p| p.title["en"] })
|
38
|
+
end
|
39
|
+
|
40
|
+
it "returns emendations received" do
|
41
|
+
amendment_emendations = response["amendments"].map { |amendment| amendment["emendation"] }.map { |title| title["title"]["translation"] }
|
42
|
+
expect(amendment_emendations).to include(*model.amendments.map(&:emendation).map { |p| p.title["en"] })
|
43
|
+
end
|
44
|
+
|
45
|
+
it "returns amender as emendation author" do
|
46
|
+
amendment_amenders = response["amendments"].map { |amendment| amendment["amender"] }
|
47
|
+
expect(amendment_amenders).to include(*model.amendments.map(&:amender).map { |p| { "name" => p.name } })
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "spec_helper"
|
4
|
+
|
5
|
+
shared_examples_for "attachable interface" do
|
6
|
+
let!(:attached_to) { model }
|
7
|
+
let!(:attachments) { create_list(:attachment, 3, attached_to:) }
|
8
|
+
|
9
|
+
describe "attachments" do
|
10
|
+
let(:query) { "{ attachments { url } }" }
|
11
|
+
|
12
|
+
it "includes the attachment urls" do
|
13
|
+
attachment_urls = response["attachments"].map { |attachment| attachment["url"] }
|
14
|
+
expect(attachment_urls).to include_blob_urls(*attachments.map(&:file).map(&:blob))
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
shared_examples_for "attachable collection interface with attachment" do
|
20
|
+
context "when the model has an attachment collection" do
|
21
|
+
let!(:attachment_collection) { create(:attachment_collection, collection_for: model) }
|
22
|
+
|
23
|
+
describe "attachment_collections" do
|
24
|
+
let(:query) { '{ attachmentCollections { name { translation(locale:"en") } } }' }
|
25
|
+
|
26
|
+
it "includes the name of collection" do
|
27
|
+
expect(response["attachmentCollections"][0]["name"]["translation"]).to include(translated(attachment_collection.name))
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
describe "attachments" do
|
32
|
+
let(:attached_to) { attachment_collection }
|
33
|
+
let(:query) { "{ attachmentCollections { attachments { url } } }" }
|
34
|
+
|
35
|
+
include_examples "attachable interface" do
|
36
|
+
let!(:attached_to) { attachment_collection }
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "spec_helper"
|
4
|
+
|
5
|
+
shared_examples_for "authorable interface" do
|
6
|
+
describe "author" do
|
7
|
+
describe "when author is not present" do
|
8
|
+
let(:author) { nil }
|
9
|
+
let(:query) { "{ author { name } }" }
|
10
|
+
|
11
|
+
before do
|
12
|
+
model.update(author:)
|
13
|
+
end
|
14
|
+
|
15
|
+
it "does not include the author" do
|
16
|
+
expect(response["author"]).to be_nil
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
describe "with a regular user" do
|
21
|
+
let(:author) { create(:user, :confirmed, organization: model.participatory_space.organization) }
|
22
|
+
let(:query) { "{ author { name } }" }
|
23
|
+
|
24
|
+
before do
|
25
|
+
model.update(author:)
|
26
|
+
end
|
27
|
+
|
28
|
+
it "includes the user's name" do
|
29
|
+
expect(response["author"]["name"]).to eq(author.name)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
describe "with an organization" do
|
34
|
+
let(:organization) { model.participatory_space.organization }
|
35
|
+
let(:query) { "{ author { name } }" }
|
36
|
+
|
37
|
+
before do
|
38
|
+
model.update(author: organization)
|
39
|
+
end
|
40
|
+
|
41
|
+
it "does not return a main author" do
|
42
|
+
expect(response["author"]).to eq(nil)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "spec_helper"
|
4
|
+
|
5
|
+
shared_examples_for "categories container interface" do
|
6
|
+
describe "categories" do
|
7
|
+
let!(:categories) { create_list(:category, 3, participatory_space: model) }
|
8
|
+
let!(:subcategories) do
|
9
|
+
categories.map { |cat| create_list(:subcategory, 3, parent: cat) }.flatten
|
10
|
+
end
|
11
|
+
let(:other_space) { create(:participatory_process, organization: model.organization) }
|
12
|
+
let!(:other_categories) { create_list(:category, 3, participatory_space: other_space) }
|
13
|
+
let(:category_ids) { [categories.map(&:id), subcategories.map(&:id)].flatten }
|
14
|
+
let(:query) { "{ categories { id } }" }
|
15
|
+
|
16
|
+
it "returns its categories" do
|
17
|
+
ids = response["categories"].map { |cat| cat["id"].to_i }
|
18
|
+
expect(ids).to match_array(category_ids)
|
19
|
+
expect(ids).not_to include(*other_categories.map(&:id))
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "spec_helper"
|
4
|
+
|
5
|
+
shared_examples_for "categorizable interface" do
|
6
|
+
let!(:category) { create(:category, participatory_space: model.participatory_space) }
|
7
|
+
|
8
|
+
describe "category" do
|
9
|
+
let(:query) { "{ category { id } }" }
|
10
|
+
|
11
|
+
context "when model has category" do
|
12
|
+
before do
|
13
|
+
model.update(category:)
|
14
|
+
end
|
15
|
+
|
16
|
+
it "has a category" do
|
17
|
+
expect(response).to include("category" => { "id" => category.id.to_s })
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
context "when model has no category" do
|
22
|
+
it "returns null" do
|
23
|
+
expect(response).to include("category" => nil)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "spec_helper"
|
4
|
+
|
5
|
+
shared_examples_for "coauthorable interface" do
|
6
|
+
describe "author" do
|
7
|
+
let(:author) { model.creator_author }
|
8
|
+
|
9
|
+
describe "with a regular user" do
|
10
|
+
let(:query) { "{ author { name } }" }
|
11
|
+
|
12
|
+
it "returns the user's name as the author name" do
|
13
|
+
expect(response["author"]["name"]).to eq(author.name)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
describe "with a several coauthors" do
|
18
|
+
let(:query) { "{ author { name } authors { name } authorsCount }" }
|
19
|
+
let(:coauthor) { create(:user, :confirmed, organization: model.participatory_space.organization) }
|
20
|
+
|
21
|
+
before do
|
22
|
+
model.add_coauthor coauthor
|
23
|
+
model.save!
|
24
|
+
end
|
25
|
+
|
26
|
+
context "when both are users" do
|
27
|
+
it "returns 2 total co-authors" do
|
28
|
+
expect(response["authorsCount"]).to eq(2)
|
29
|
+
end
|
30
|
+
|
31
|
+
it "returns an array of authors" do
|
32
|
+
expect(response["authors"].count).to eq(2)
|
33
|
+
expect(response["authors"]).to include("name" => author.name)
|
34
|
+
expect(response["authors"]).to include("name" => coauthor.name)
|
35
|
+
end
|
36
|
+
|
37
|
+
it "returns a main author" do
|
38
|
+
expect(response["author"]["name"]).to eq(author.name)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
context "when author is the organization" do
|
43
|
+
let(:model) { create(:proposal, :official, component:) }
|
44
|
+
|
45
|
+
it "returns 2 total co-authors" do
|
46
|
+
expect(response["authorsCount"]).to eq(2)
|
47
|
+
end
|
48
|
+
|
49
|
+
it "returns 1 author in authors array" do
|
50
|
+
expect(response["authors"].count).to eq(1)
|
51
|
+
expect(response["authors"]).to include("name" => coauthor.name)
|
52
|
+
end
|
53
|
+
|
54
|
+
it "does not return a main author" do
|
55
|
+
expect(response["author"]).to eq(nil)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
context "when author is a meeting" do
|
60
|
+
let(:model) { create(:proposal, :official_meeting, component:) }
|
61
|
+
|
62
|
+
it "returns 2 total co-authors" do
|
63
|
+
expect(response["authorsCount"]).to eq(2)
|
64
|
+
end
|
65
|
+
|
66
|
+
it "returns 1 author in authors array" do
|
67
|
+
expect(response["authors"].count).to eq(1)
|
68
|
+
expect(response["authors"]).to include("name" => coauthor.name)
|
69
|
+
end
|
70
|
+
|
71
|
+
it "does not return a main author" do
|
72
|
+
expect(response["author"]).to eq(nil)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "spec_helper"
|
4
|
+
|
5
|
+
shared_examples_for "commentable interface" do
|
6
|
+
describe "total_comments_count" do
|
7
|
+
let(:query) { "{ totalCommentsCount }" }
|
8
|
+
|
9
|
+
it "includes the field" do
|
10
|
+
expect(response["totalCommentsCount"]).to eq(model.comments_count)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "spec_helper"
|
4
|
+
|
5
|
+
shared_examples_for "fingerprintable interface" do
|
6
|
+
describe "fingerprint" do
|
7
|
+
let(:query) { "{ fingerprint { value source } }" }
|
8
|
+
|
9
|
+
it "returns the fingerprint value" do
|
10
|
+
expect(response["fingerprint"]["value"]).to eq(model.fingerprint.value)
|
11
|
+
end
|
12
|
+
|
13
|
+
it "returns the fingerprint source" do
|
14
|
+
expect(response["fingerprint"]["source"]).to eq(model.fingerprint.source)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|