decidim-api 0.30.8 → 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 -2
- 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/schema.rb +0 -6
- data/lib/decidim/api/test/component_context.rb +17 -19
- 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 -92
- data/lib/decidim/api/test.rb +21 -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 -9
- data/lib/decidim/api/version.rb +1 -1
- data/lib/decidim/api.rb +23 -15
- 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 -84
- data/config/locales/am-ET.yml +0 -1
- data/config/locales/ar.yml +0 -1
- data/config/locales/bg.yml +0 -1
- data/config/locales/bn-BD.yml +0 -1
- data/config/locales/bs-BA.yml +0 -1
- data/config/locales/ca-IT.yml +0 -8
- data/config/locales/ca.yml +0 -8
- data/config/locales/cs.yml +0 -7
- data/config/locales/da.yml +0 -1
- data/config/locales/de.yml +0 -1
- data/config/locales/el.yml +0 -1
- data/config/locales/en.yml +0 -8
- data/config/locales/eo.yml +0 -1
- data/config/locales/es-MX.yml +0 -8
- data/config/locales/es-PY.yml +0 -8
- data/config/locales/es.yml +0 -8
- data/config/locales/et.yml +0 -1
- data/config/locales/eu.yml +0 -8
- data/config/locales/fa-IR.yml +0 -1
- data/config/locales/fi-plain.yml +0 -8
- data/config/locales/fi.yml +0 -8
- data/config/locales/fr-CA.yml +0 -8
- data/config/locales/fr.yml +0 -8
- data/config/locales/ga-IE.yml +0 -1
- data/config/locales/gl.yml +0 -1
- data/config/locales/gn-PY.yml +0 -1
- data/config/locales/he-IL.yml +0 -1
- data/config/locales/hr.yml +0 -1
- data/config/locales/hu.yml +0 -1
- data/config/locales/id-ID.yml +0 -1
- data/config/locales/is-IS.yml +0 -1
- data/config/locales/it.yml +0 -1
- data/config/locales/ja.yml +0 -8
- data/config/locales/ka-GE.yml +0 -1
- data/config/locales/kaa.yml +0 -1
- data/config/locales/ko.yml +0 -1
- data/config/locales/lb.yml +0 -1
- data/config/locales/lo-LA.yml +0 -1
- data/config/locales/lt.yml +0 -1
- data/config/locales/lv.yml +0 -1
- data/config/locales/mt.yml +0 -1
- data/config/locales/nl.yml +0 -1
- data/config/locales/no.yml +0 -6
- data/config/locales/oc-FR.yml +0 -1
- data/config/locales/om-ET.yml +0 -1
- data/config/locales/pl.yml +0 -1
- data/config/locales/pt-BR.yml +0 -8
- data/config/locales/pt.yml +0 -1
- data/config/locales/ro-RO.yml +0 -8
- data/config/locales/ru.yml +0 -1
- data/config/locales/si-LK.yml +0 -1
- data/config/locales/sk.yml +0 -1
- data/config/locales/sl.yml +0 -1
- data/config/locales/so-SO.yml +0 -1
- data/config/locales/sq-AL.yml +0 -1
- data/config/locales/sr-CS.yml +0 -1
- data/config/locales/sv.yml +0 -8
- data/config/locales/sw-KE.yml +0 -1
- data/config/locales/th-TH.yml +0 -1
- data/config/locales/ti-ER.yml +0 -1
- data/config/locales/tr-TR.yml +0 -1
- data/config/locales/uk.yml +0 -1
- data/config/locales/val-ES.yml +0 -1
- data/config/locales/vi.yml +0 -1
- data/config/locales/zh-CN.yml +0 -1
- data/config/locales/zh-TW.yml +0 -1
- data/lib/decidim/api/alias_analyzer.rb +0 -23
- data/lib/decidim/api/decidim_introspection.rb +0 -51
- data/lib/decidim/api/errors/introspection_disabled_error.rb +0 -14
- data/lib/decidim/api/errors/recursion_limit_exceeded_error.rb +0 -14
- data/lib/decidim/api/errors/too_many_aliases_error.rb +0 -15
- data/lib/decidim/api/recursion_analyzer.rb +0 -74
|
@@ -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
|
data/lib/decidim/api/schema.rb
CHANGED
|
@@ -7,16 +7,10 @@ module Decidim
|
|
|
7
7
|
mutation(MutationType)
|
|
8
8
|
query(QueryType)
|
|
9
9
|
|
|
10
|
-
introspection(DecidimIntrospection)
|
|
11
|
-
query_analyzer RecursionAnalyzer
|
|
12
|
-
query_analyzer AliasAnalyzer
|
|
13
|
-
|
|
14
10
|
default_max_page_size Decidim::Api.schema_max_per_page
|
|
15
11
|
max_depth Decidim::Api.schema_max_depth
|
|
16
12
|
max_complexity Decidim::Api.schema_max_complexity
|
|
17
13
|
|
|
18
|
-
query_analyzer AliasAnalyzer
|
|
19
|
-
|
|
20
14
|
orphan_types(Api.orphan_types)
|
|
21
15
|
end
|
|
22
16
|
end
|
|
@@ -1,10 +1,7 @@
|
|
|
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
|
-
include_examples "when the introspection is disabled"
|
|
8
5
|
|
|
9
6
|
let(:schema) { Decidim::Api::Schema }
|
|
10
7
|
|
|
@@ -25,6 +22,7 @@ shared_context "with a graphql decidim component" do
|
|
|
25
22
|
name {
|
|
26
23
|
translation(locale: "#{locale}")
|
|
27
24
|
}
|
|
25
|
+
url
|
|
28
26
|
weight
|
|
29
27
|
__typename
|
|
30
28
|
...fooComponent
|
|
@@ -107,9 +105,9 @@ shared_examples "with resource visibility" do
|
|
|
107
105
|
it_behaves_like "graphQL visible resource"
|
|
108
106
|
end
|
|
109
107
|
|
|
110
|
-
context "when the user is space
|
|
108
|
+
context "when the user is space evaluator" do
|
|
111
109
|
let!(:current_user) { create(:user, :confirmed, organization: current_organization) }
|
|
112
|
-
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") }
|
|
113
111
|
it_behaves_like "graphQL visible resource"
|
|
114
112
|
end
|
|
115
113
|
|
|
@@ -159,9 +157,9 @@ shared_examples "with resource visibility" do
|
|
|
159
157
|
it_behaves_like "graphQL hidden component"
|
|
160
158
|
end
|
|
161
159
|
|
|
162
|
-
context "when the user is space
|
|
160
|
+
context "when the user is space evaluator" do
|
|
163
161
|
let!(:current_user) { create(:user, :confirmed, organization: current_organization) }
|
|
164
|
-
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") }
|
|
165
163
|
it_behaves_like "graphQL visible resource"
|
|
166
164
|
end
|
|
167
165
|
|
|
@@ -229,9 +227,9 @@ shared_examples "with resource visibility" do
|
|
|
229
227
|
it_behaves_like "graphQL visible resource"
|
|
230
228
|
end
|
|
231
229
|
|
|
232
|
-
context "when the user is space
|
|
230
|
+
context "when the user is space evaluator" do
|
|
233
231
|
let!(:current_user) { create(:user, :confirmed, organization: current_organization) }
|
|
234
|
-
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") }
|
|
235
233
|
it_behaves_like "graphQL visible resource"
|
|
236
234
|
end
|
|
237
235
|
|
|
@@ -275,9 +273,9 @@ shared_examples "with resource visibility" do
|
|
|
275
273
|
it_behaves_like "graphQL hidden component"
|
|
276
274
|
end
|
|
277
275
|
|
|
278
|
-
context "when the user is space
|
|
276
|
+
context "when the user is space evaluator" do
|
|
279
277
|
let!(:current_user) { create(:user, :confirmed, organization: current_organization) }
|
|
280
|
-
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") }
|
|
281
279
|
it_behaves_like "graphQL visible resource"
|
|
282
280
|
end
|
|
283
281
|
|
|
@@ -326,9 +324,9 @@ shared_examples "with resource visibility" do
|
|
|
326
324
|
it_behaves_like "graphQL hidden space"
|
|
327
325
|
end
|
|
328
326
|
|
|
329
|
-
context "when the user is space
|
|
327
|
+
context "when the user is space evaluator" do
|
|
330
328
|
let!(:current_user) { create(:user, :confirmed, organization: current_organization) }
|
|
331
|
-
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") }
|
|
332
330
|
it_behaves_like "graphQL hidden space"
|
|
333
331
|
end
|
|
334
332
|
|
|
@@ -369,9 +367,9 @@ shared_examples "with resource visibility" do
|
|
|
369
367
|
it_behaves_like "graphQL hidden space"
|
|
370
368
|
end
|
|
371
369
|
|
|
372
|
-
context "when the user is space
|
|
370
|
+
context "when the user is space evaluator" do
|
|
373
371
|
let!(:current_user) { create(:user, :confirmed, organization: current_organization) }
|
|
374
|
-
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") }
|
|
375
373
|
it_behaves_like "graphQL hidden space"
|
|
376
374
|
end
|
|
377
375
|
it_behaves_like "graphQL space hidden to visitor"
|
|
@@ -414,9 +412,9 @@ shared_examples "with resource visibility" do
|
|
|
414
412
|
it_behaves_like "graphQL hidden space"
|
|
415
413
|
end
|
|
416
414
|
|
|
417
|
-
context "when the user is space
|
|
415
|
+
context "when the user is space evaluator" do
|
|
418
416
|
let!(:current_user) { create(:user, :confirmed, organization: current_organization) }
|
|
419
|
-
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") }
|
|
420
418
|
it_behaves_like "graphQL hidden space"
|
|
421
419
|
end
|
|
422
420
|
|
|
@@ -457,9 +455,9 @@ shared_examples "with resource visibility" do
|
|
|
457
455
|
it_behaves_like "graphQL hidden space"
|
|
458
456
|
end
|
|
459
457
|
|
|
460
|
-
context "when the user is space
|
|
458
|
+
context "when the user is space evaluator" do
|
|
461
459
|
let!(:current_user) { create(:user, :confirmed, organization: current_organization) }
|
|
462
|
-
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") }
|
|
463
461
|
it_behaves_like "graphQL hidden space"
|
|
464
462
|
end
|
|
465
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
|