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.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/decidim/api/application_controller.rb +20 -0
  3. data/app/controllers/decidim/api/queries_controller.rb +33 -1
  4. data/app/controllers/decidim/api/sessions_controller.rb +61 -0
  5. data/app/models/decidim/api/api_user.rb +82 -0
  6. data/app/models/decidim/api/jwt_denylist.rb +11 -0
  7. data/app/packs/entrypoints/decidim_api_graphiql.js +2 -1
  8. data/app/presenters/decidim/api/api_user_presenter.rb +23 -0
  9. data/config/assets.rb +2 -2
  10. data/config/initializers/devise.rb +26 -0
  11. data/config/routes.rb +8 -0
  12. data/decidim-api.gemspec +2 -1
  13. data/docs/usage.md +98 -8
  14. data/lib/decidim/api/component_mutation_type.rb +19 -0
  15. data/lib/decidim/api/devise.rb +12 -0
  16. data/lib/decidim/api/engine.rb +2 -2
  17. data/lib/decidim/api/graphql_permissions.rb +125 -0
  18. data/lib/decidim/api/mutation_type.rb +10 -0
  19. data/lib/decidim/api/required_scopes.rb +31 -0
  20. data/lib/decidim/api/test/component_context.rb +17 -18
  21. data/lib/decidim/api/test/factories.rb +33 -0
  22. data/lib/decidim/api/test/mutation_context.rb +38 -0
  23. data/lib/decidim/api/test/shared_examples/amendable_interface_examples.rb +14 -0
  24. data/lib/decidim/api/test/shared_examples/amendable_proposals_interface_examples.rb +50 -0
  25. data/lib/decidim/api/test/shared_examples/attachable_interface_examples.rb +40 -0
  26. data/lib/decidim/api/test/shared_examples/authorable_interface_examples.rb +46 -0
  27. data/lib/decidim/api/test/shared_examples/categories_container_examples.rb +22 -0
  28. data/lib/decidim/api/test/shared_examples/categorizable_interface_examples.rb +27 -0
  29. data/lib/decidim/api/test/shared_examples/coauthorable_interface_examples.rb +77 -0
  30. data/lib/decidim/api/test/shared_examples/commentable_interface_examples.rb +13 -0
  31. data/lib/decidim/api/test/shared_examples/fingerprintable_interface_examples.rb +17 -0
  32. data/lib/decidim/api/test/shared_examples/followable_interface_examples.rb +13 -0
  33. data/lib/decidim/api/test/shared_examples/input_filter_examples.rb +77 -0
  34. data/lib/decidim/api/test/shared_examples/input_sort_examples.rb +126 -0
  35. data/lib/decidim/api/test/shared_examples/likeable_interface_examples.rb +22 -0
  36. data/lib/decidim/api/test/shared_examples/localizable_interface_examples.rb +29 -0
  37. data/lib/decidim/api/test/shared_examples/participatory_space_resourcable_interface_examples.rb +61 -0
  38. data/lib/decidim/api/test/shared_examples/referable_interface_examples.rb +13 -0
  39. data/lib/decidim/api/test/shared_examples/scopable_interface_examples.rb +19 -0
  40. data/lib/decidim/api/test/shared_examples/statistics_examples.rb +30 -16
  41. data/lib/decidim/api/test/shared_examples/taxonomizable_interface_examples.rb +20 -0
  42. data/lib/decidim/api/test/shared_examples/timestamps_interface_examples.rb +21 -0
  43. data/lib/decidim/api/test/shared_examples/traceable_interface_examples.rb +49 -0
  44. data/lib/decidim/api/test/type_context.rb +9 -1
  45. data/lib/decidim/api/test.rb +22 -0
  46. data/lib/decidim/api/types/base_mutation.rb +5 -1
  47. data/lib/decidim/api/types/base_object.rb +4 -69
  48. data/lib/decidim/api/types.rb +3 -0
  49. data/lib/decidim/api/version.rb +1 -1
  50. data/lib/decidim/api.rb +25 -5
  51. data/lib/devise/models/api_authenticatable.rb +30 -0
  52. data/lib/devise/strategies/api_authenticatable.rb +21 -0
  53. data/lib/warden/jwt_auth/decidim_overrides.rb +42 -0
  54. 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 valuator" do
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: "valuator") }
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 valuator" do
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: "valuator") }
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 valuator" do
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: "valuator") }
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 valuator" do
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: "valuator") }
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 valuator" do
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: "valuator") }
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 valuator" do
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: "valuator") }
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 valuator" do
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: "valuator") }
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 valuator" do
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: "valuator") }
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