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.
Files changed (127) 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 -2
  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/schema.rb +0 -6
  21. data/lib/decidim/api/test/component_context.rb +17 -19
  22. data/lib/decidim/api/test/factories.rb +33 -0
  23. data/lib/decidim/api/test/mutation_context.rb +38 -0
  24. data/lib/decidim/api/test/shared_examples/amendable_interface_examples.rb +14 -0
  25. data/lib/decidim/api/test/shared_examples/amendable_proposals_interface_examples.rb +50 -0
  26. data/lib/decidim/api/test/shared_examples/attachable_interface_examples.rb +40 -0
  27. data/lib/decidim/api/test/shared_examples/authorable_interface_examples.rb +46 -0
  28. data/lib/decidim/api/test/shared_examples/categories_container_examples.rb +22 -0
  29. data/lib/decidim/api/test/shared_examples/categorizable_interface_examples.rb +27 -0
  30. data/lib/decidim/api/test/shared_examples/coauthorable_interface_examples.rb +77 -0
  31. data/lib/decidim/api/test/shared_examples/commentable_interface_examples.rb +13 -0
  32. data/lib/decidim/api/test/shared_examples/fingerprintable_interface_examples.rb +17 -0
  33. data/lib/decidim/api/test/shared_examples/followable_interface_examples.rb +13 -0
  34. data/lib/decidim/api/test/shared_examples/input_filter_examples.rb +77 -0
  35. data/lib/decidim/api/test/shared_examples/input_sort_examples.rb +126 -0
  36. data/lib/decidim/api/test/shared_examples/likeable_interface_examples.rb +22 -0
  37. data/lib/decidim/api/test/shared_examples/localizable_interface_examples.rb +29 -0
  38. data/lib/decidim/api/test/shared_examples/participatory_space_resourcable_interface_examples.rb +61 -0
  39. data/lib/decidim/api/test/shared_examples/referable_interface_examples.rb +13 -0
  40. data/lib/decidim/api/test/shared_examples/scopable_interface_examples.rb +19 -0
  41. data/lib/decidim/api/test/shared_examples/statistics_examples.rb +30 -16
  42. data/lib/decidim/api/test/shared_examples/taxonomizable_interface_examples.rb +20 -0
  43. data/lib/decidim/api/test/shared_examples/timestamps_interface_examples.rb +21 -0
  44. data/lib/decidim/api/test/shared_examples/traceable_interface_examples.rb +49 -0
  45. data/lib/decidim/api/test/type_context.rb +9 -92
  46. data/lib/decidim/api/test.rb +21 -0
  47. data/lib/decidim/api/types/base_mutation.rb +5 -1
  48. data/lib/decidim/api/types/base_object.rb +4 -69
  49. data/lib/decidim/api/types.rb +3 -9
  50. data/lib/decidim/api/version.rb +1 -1
  51. data/lib/decidim/api.rb +23 -15
  52. data/lib/devise/models/api_authenticatable.rb +30 -0
  53. data/lib/devise/strategies/api_authenticatable.rb +21 -0
  54. data/lib/warden/jwt_auth/decidim_overrides.rb +42 -0
  55. metadata +66 -84
  56. data/config/locales/am-ET.yml +0 -1
  57. data/config/locales/ar.yml +0 -1
  58. data/config/locales/bg.yml +0 -1
  59. data/config/locales/bn-BD.yml +0 -1
  60. data/config/locales/bs-BA.yml +0 -1
  61. data/config/locales/ca-IT.yml +0 -8
  62. data/config/locales/ca.yml +0 -8
  63. data/config/locales/cs.yml +0 -7
  64. data/config/locales/da.yml +0 -1
  65. data/config/locales/de.yml +0 -1
  66. data/config/locales/el.yml +0 -1
  67. data/config/locales/en.yml +0 -8
  68. data/config/locales/eo.yml +0 -1
  69. data/config/locales/es-MX.yml +0 -8
  70. data/config/locales/es-PY.yml +0 -8
  71. data/config/locales/es.yml +0 -8
  72. data/config/locales/et.yml +0 -1
  73. data/config/locales/eu.yml +0 -8
  74. data/config/locales/fa-IR.yml +0 -1
  75. data/config/locales/fi-plain.yml +0 -8
  76. data/config/locales/fi.yml +0 -8
  77. data/config/locales/fr-CA.yml +0 -8
  78. data/config/locales/fr.yml +0 -8
  79. data/config/locales/ga-IE.yml +0 -1
  80. data/config/locales/gl.yml +0 -1
  81. data/config/locales/gn-PY.yml +0 -1
  82. data/config/locales/he-IL.yml +0 -1
  83. data/config/locales/hr.yml +0 -1
  84. data/config/locales/hu.yml +0 -1
  85. data/config/locales/id-ID.yml +0 -1
  86. data/config/locales/is-IS.yml +0 -1
  87. data/config/locales/it.yml +0 -1
  88. data/config/locales/ja.yml +0 -8
  89. data/config/locales/ka-GE.yml +0 -1
  90. data/config/locales/kaa.yml +0 -1
  91. data/config/locales/ko.yml +0 -1
  92. data/config/locales/lb.yml +0 -1
  93. data/config/locales/lo-LA.yml +0 -1
  94. data/config/locales/lt.yml +0 -1
  95. data/config/locales/lv.yml +0 -1
  96. data/config/locales/mt.yml +0 -1
  97. data/config/locales/nl.yml +0 -1
  98. data/config/locales/no.yml +0 -6
  99. data/config/locales/oc-FR.yml +0 -1
  100. data/config/locales/om-ET.yml +0 -1
  101. data/config/locales/pl.yml +0 -1
  102. data/config/locales/pt-BR.yml +0 -8
  103. data/config/locales/pt.yml +0 -1
  104. data/config/locales/ro-RO.yml +0 -8
  105. data/config/locales/ru.yml +0 -1
  106. data/config/locales/si-LK.yml +0 -1
  107. data/config/locales/sk.yml +0 -1
  108. data/config/locales/sl.yml +0 -1
  109. data/config/locales/so-SO.yml +0 -1
  110. data/config/locales/sq-AL.yml +0 -1
  111. data/config/locales/sr-CS.yml +0 -1
  112. data/config/locales/sv.yml +0 -8
  113. data/config/locales/sw-KE.yml +0 -1
  114. data/config/locales/th-TH.yml +0 -1
  115. data/config/locales/ti-ER.yml +0 -1
  116. data/config/locales/tr-TR.yml +0 -1
  117. data/config/locales/uk.yml +0 -1
  118. data/config/locales/val-ES.yml +0 -1
  119. data/config/locales/vi.yml +0 -1
  120. data/config/locales/zh-CN.yml +0 -1
  121. data/config/locales/zh-TW.yml +0 -1
  122. data/lib/decidim/api/alias_analyzer.rb +0 -23
  123. data/lib/decidim/api/decidim_introspection.rb +0 -51
  124. data/lib/decidim/api/errors/introspection_disabled_error.rb +0 -14
  125. data/lib/decidim/api/errors/recursion_limit_exceeded_error.rb +0 -14
  126. data/lib/decidim/api/errors/too_many_aliases_error.rb +0 -15
  127. data/lib/decidim/api/recursion_analyzer.rb +0 -74
@@ -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
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ shared_examples_for "followable interface" do
6
+ describe "follows_count" do
7
+ let(:query) { "{ followsCount }" }
8
+
9
+ it "includes the field" do
10
+ expect(response["followsCount"]).to eq(model.follows_count)
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ shared_examples_for "collection has before/since input filter" do |collection, field|
4
+ context "when date is before the past" do
5
+ let(:query) { %[{ #{collection}(filter: {#{field}Before: "#{2.days.ago.to_date}"}) { id } }] }
6
+
7
+ it "finds nothing" do
8
+ ids = response[collection].map { |item| item["id"] }
9
+ expect(ids).to eq([])
10
+ end
11
+ end
12
+
13
+ context "when date is before the future" do
14
+ let(:query) { %[{ #{collection}(filter: {#{field}Before: "#{2.days.from_now.to_date}"}) { id } }] }
15
+
16
+ it "finds the models " do
17
+ ids = response[collection].map { |item| item["id"] }
18
+ expect(ids).to match_array(models.map(&:id).map(&:to_s))
19
+ end
20
+ end
21
+
22
+ context "when date is after the future" do
23
+ let(:query) { %[{ #{collection}(filter: {#{field}Since: "#{2.days.from_now.to_date}"}) { id } }] }
24
+
25
+ it "finds nothing " do
26
+ ids = response[collection].map { |item| item["id"] }
27
+ expect(ids).to eq([])
28
+ end
29
+ end
30
+
31
+ context "when date is after the past" do
32
+ let(:query) { %[{ #{collection}(filter: {#{field}Since: "#{2.days.ago.to_date}"}) { id } }] }
33
+
34
+ it "finds the models " do
35
+ ids = response[collection].map { |item| item["id"] }
36
+ expect(ids).to match_array(models.map(&:id).map(&:to_s))
37
+ end
38
+ end
39
+ end
40
+
41
+ shared_examples_for "connection has before/since input filter" do |connection, field|
42
+ context "when date is before the past" do
43
+ let(:query) { %[{ #{connection}(filter: {#{field}Before: "#{2.days.ago.to_date}"}) { edges { node { id } } } }] }
44
+
45
+ it "finds nothing" do
46
+ ids = response[connection]["edges"].map { |edge| edge["node"]["id"] }
47
+ expect(ids).to eq([])
48
+ end
49
+ end
50
+
51
+ context "when date is before the future" do
52
+ let(:query) { %[{ #{connection}(filter: {#{field}Before: "#{2.days.from_now.to_date}"}) { edges { node { id } } } }] }
53
+
54
+ it "finds the models " do
55
+ ids = response[connection]["edges"].map { |edge| edge["node"]["id"] }
56
+ expect(ids).to match_array(models.map(&:id).map(&:to_s))
57
+ end
58
+ end
59
+
60
+ context "when date is after the future" do
61
+ let(:query) { %[{ #{connection}(filter: {#{field}Since: "#{2.days.from_now.to_date}"}) { edges { node { id } } } }] }
62
+
63
+ it "finds nothing " do
64
+ ids = response[connection]["edges"].map { |edge| edge["node"]["id"] }
65
+ expect(ids).to eq([])
66
+ end
67
+ end
68
+
69
+ context "when date is after the past" do
70
+ let(:query) { %[{ #{connection}(filter: {#{field}Since: "#{2.days.ago.to_date}"}) { edges { node { id } } } }] }
71
+
72
+ it "finds the models " do
73
+ ids = response[connection]["edges"].map { |edge| edge["node"]["id"] }
74
+ expect(ids).to match_array(models.map(&:id).map(&:to_s))
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,126 @@
1
+ # frozen_string_literal: true
2
+
3
+ shared_examples_for "collection has input sort" do |collection, field|
4
+ describe "ASC" do
5
+ let(:query) { %[{ #{collection}(order: {#{field}: "ASC"}) { id } }] }
6
+
7
+ it "returns expected order" do
8
+ ids = response[collection].map { |item| item["id"] }
9
+ replies_ids = models.sort_by(&field.underscore.to_sym).map(&:id).map(&:to_s)
10
+ expect(ids).to eq(replies_ids)
11
+ expect(ids).not_to eq(replies_ids.reverse)
12
+ end
13
+ end
14
+
15
+ describe "DESC" do
16
+ let(:query) { %[{ #{collection}(order: {#{field}: "DESC"}) { id } }] }
17
+
18
+ it "returns reversed order" do
19
+ ids = response[collection].map { |item| item["id"] }
20
+ replies_ids = models.sort_by(&field.underscore.to_sym).map(&:id).map(&:to_s)
21
+ expect(ids).not_to eq(replies_ids)
22
+ expect(ids).to eq(replies_ids.reverse)
23
+ end
24
+ end
25
+ end
26
+
27
+ shared_examples_for "collection has i18n input sort" do |collection, field|
28
+ context "when locale is not specified" do
29
+ describe "ASC" do
30
+ let(:query) { %[{ #{collection}(order: { #{field}: "ASC" }) { id } }] }
31
+
32
+ it "returns alphabetical order" do
33
+ response_ids = response[collection].map { |item| item["id"].to_i }
34
+ ids = models.sort_by { |item| item.public_send(field.to_sym)[current_organization.default_locale] }.map { |item| item.id.to_i }
35
+ expect(response_ids).to eq(ids)
36
+ expect(response_ids).not_to eq(ids.reverse)
37
+ end
38
+ end
39
+
40
+ describe "DESC" do
41
+ let(:query) { %[{ #{collection}(order: { #{field}: "DESC" }) { id } }] }
42
+
43
+ it "returns revered alphabetical order" do
44
+ response_ids = response[collection].map { |item| item["id"].to_i }
45
+ ids = models.sort_by { |item| item.public_send(field.to_sym)[current_organization.default_locale] }.map { |item| item.id.to_i }
46
+ expect(response_ids).not_to eq(ids)
47
+ expect(response_ids).to eq(ids.reverse)
48
+ end
49
+ end
50
+ end
51
+
52
+ context "when locale is specified" do
53
+ describe "ASC" do
54
+ let(:query) { %[{ #{collection}(order: { #{field}: "ASC", locale: "ca" }) { id } }] }
55
+
56
+ it "returns alphabetical order" do
57
+ response_ids = response[collection].map { |item| item["id"].to_i }
58
+ ids = models.sort_by { |item| item.public_send(field.to_sym)["ca"] }.map { |item| item.id.to_i }
59
+ expect(response_ids).to eq(ids)
60
+ expect(response_ids).not_to eq(ids.reverse)
61
+ end
62
+ end
63
+
64
+ describe "DESC" do
65
+ let(:query) { %[{ #{collection}(order: { #{field}: "DESC", locale: "ca" }) { id } }] }
66
+
67
+ it "returns revered alphabetical order" do
68
+ response_ids = response[collection].map { |item| item["id"].to_i }
69
+ ids = models.sort_by { |item| item.public_send(field.to_sym)["ca"] }.map { |item| item.id.to_i }
70
+ expect(response_ids).not_to eq(ids)
71
+ expect(response_ids).to eq(ids.reverse)
72
+ end
73
+ end
74
+
75
+ context "when locale does not exist in the organization" do
76
+ let(:query) { %[{ #{collection}(order: { #{field}: "DESC", locale: "de" }) { id } }] }
77
+
78
+ it "returns all the component ordered" do
79
+ expect { response }.to raise_exception(Exception)
80
+ end
81
+ end
82
+ end
83
+ end
84
+
85
+ shared_examples_for "connection has input sort" do |connection, field|
86
+ describe "ASC" do
87
+ let(:query) { %[{ #{connection}(order: {#{field}: "ASC"}) { edges { node { id } } } }] }
88
+
89
+ it "returns expected order" do
90
+ ids = response[connection]["edges"].map { |edge| edge["node"]["id"] }
91
+ replies_ids = models.sort_by(&field.underscore.to_sym).map(&:id).map(&:to_s)
92
+ expect(ids).to eq(replies_ids)
93
+ end
94
+ end
95
+
96
+ describe "DESC" do
97
+ let(:query) { %[{ #{connection}(order: {#{field}: "DESC"}) { edges { node { id } } } }] }
98
+
99
+ it "returns reversed order" do
100
+ ids = response[connection]["edges"].map { |edge| edge["node"]["id"] }
101
+ replies_ids = models.sort_by(&field.underscore.to_sym).map(&:id).map(&:to_s)
102
+ expect(ids).to eq(replies_ids.reverse)
103
+ end
104
+ end
105
+ end
106
+
107
+ # This example requires a let!(:most_liked)
108
+ shared_examples_for "connection has like_count sort" do |connection|
109
+ describe "ASC" do
110
+ let(:query) { %[{ #{connection}(order: {likeCount: "ASC"}) { edges { node { id } } } }] }
111
+
112
+ it "returns the most liked last" do
113
+ expect(response[connection]["edges"].count).to eq(4)
114
+ expect(response[connection]["edges"].last["node"]["id"]).to eq(most_liked.id.to_s)
115
+ end
116
+ end
117
+
118
+ describe "DESC" do
119
+ let(:query) { %[{ #{connection}(order: {likeCount: "DESC"}) { edges { node { id } } } }] }
120
+
121
+ it "returns the most liked first" do
122
+ expect(response[connection]["edges"].count).to eq(4)
123
+ expect(response[connection]["edges"].first["node"]["id"]).to eq(most_liked.id.to_s)
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ shared_examples_for "likeable interface" do
6
+ describe "likesCount" do
7
+ let(:query) { "{ likesCount }" }
8
+
9
+ it "returns the amount of likes for this query" do
10
+ expect(response["likesCount"]).to eq(model.likes.count)
11
+ end
12
+ end
13
+
14
+ describe "likes" do
15
+ let(:query) { "{ likes { name } }" }
16
+
17
+ it "returns the likes this query has received" do
18
+ like_names = response["likes"].map { |like| like["name"] }
19
+ expect(like_names).to include(*model.likes.map(&:author).map(&:name))
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ shared_examples_for "localizable interface" do
6
+ describe "address" do
7
+ let(:query) { "{ address }" }
8
+
9
+ it "returns the address of this proposal" do
10
+ expect(response["address"]).to eq(model.address)
11
+ end
12
+ end
13
+
14
+ describe "coordinates" do
15
+ let(:query) { "{ coordinates { latitude longitude } }" }
16
+
17
+ before do
18
+ model.latitude = 2
19
+ model.longitude = 40
20
+ model.save!
21
+ end
22
+ it "returns the meeting's address" do
23
+ expect(response["coordinates"]).to include(
24
+ "latitude" => model.latitude,
25
+ "longitude" => model.longitude
26
+ )
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ shared_examples_for "participatory space resourcable interface" do
6
+ let!(:process1) { create(:participatory_process, organization: model.organization) }
7
+ let!(:process2) { create(:participatory_process, organization: model.organization) }
8
+ let!(:process3) { create(:participatory_process, organization: model.organization) }
9
+ let!(:model) { create(:assembly) }
10
+
11
+ context "when checks is has steps" do
12
+ describe "hasSteps" do
13
+ let(:query) { "{hasSteps}" }
14
+
15
+ it "has the field" do
16
+ expect(response["hasSteps"]).to be(false)
17
+ end
18
+ end
19
+
20
+ describe "allows_steps" do
21
+ let(:query) { "{allowsSteps}" }
22
+
23
+ it "has the field" do
24
+ expect(response["allowsSteps"]).to be(false)
25
+ end
26
+ end
27
+ end
28
+
29
+ context "when linked from the model" do
30
+ describe "linkedParticipatorySpaces" do
31
+ let(:query) { "{ linkedParticipatorySpaces { participatorySpace { id } } }" }
32
+
33
+ before do
34
+ model.link_participatory_space_resources([process1, process2], :included_participatory_processes)
35
+ end
36
+
37
+ it "includes the linked resources" do
38
+ ids = response["linkedParticipatorySpaces"].map { |l| l["participatorySpace"]["id"] }
39
+ expect(ids).to include(process1.id.to_s, process2.id.to_s)
40
+ expect(ids).not_to include(process3.id.to_s)
41
+ end
42
+ end
43
+ end
44
+
45
+ context "when linked towards the model" do
46
+ describe "linkedParticipatorySpaces" do
47
+ let(:query) { "{ linkedParticipatorySpaces { participatorySpace { id } } }" }
48
+
49
+ before do
50
+ process1.link_participatory_space_resources(model, :included_participatory_processes)
51
+ process2.link_participatory_space_resources(model, :included_participatory_processes)
52
+ end
53
+
54
+ it "includes the linked resources" do
55
+ ids = response["linkedParticipatorySpaces"].map { |l| l["participatorySpace"]["id"] }
56
+ expect(ids).to include(process1.id.to_s, process2.id.to_s)
57
+ expect(ids).not_to include(process3.id.to_s)
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ shared_examples_for "referable interface" do
6
+ describe "reference" do
7
+ let(:query) { "{ reference }" }
8
+
9
+ it "includes the field" do
10
+ expect(response["reference"]).to eq(model.reference.to_s)
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ shared_examples_for "scopable interface" do
6
+ let!(:scope) { create(:scope, organization: model.participatory_space.organization) }
7
+
8
+ before do
9
+ model.update(scope:)
10
+ end
11
+
12
+ describe "scope" do
13
+ let(:query) { "{ scope { id } }" }
14
+
15
+ it "has a scope" do
16
+ expect(response).to include("scope" => { "id" => scope.id.to_s })
17
+ end
18
+ end
19
+ end
@@ -4,21 +4,21 @@ require "spec_helper"
4
4
 
5
5
  shared_examples "implements stats type" do
6
6
  context "when the space implements stats" do
7
- let(:expected_data) do
8
- {
9
- "stats" => [
10
- { "name" => "dummies_count_high", "value" => 0 },
11
- { "name" => "pages_count", "value" => 0 },
12
- { "name" => "proposals_count", "value" => 0 },
13
- { "name" => "meetings_count", "value" => 0 },
14
- { "name" => "budgets_count", "value" => 0 },
15
- { "name" => "surveys_count", "value" => 0 },
16
- { "name" => "results_count", "value" => 0 },
17
- { "name" => "debates_count", "value" => 0 },
18
- { "name" => "sortitions_count", "value" => 0 },
19
- { "name" => "posts_count", "value" => 0 }
20
- ]
21
- }
7
+ before do
8
+ allow(Decidim::ParticipatoryProcesses::ParticipatoryProcessStatsPresenter).to receive(:new)
9
+ .and_return(double(collection: [
10
+ { name: "dummies_count_high", data: [0], tooltip_key: "dummies_count_high_tooltip" },
11
+ { name: "pages_count", data: [0], tooltip_key: "pages_count_tooltip" },
12
+ { name: "proposals_count", data: [0], tooltip_key: "proposals_count_tooltip" },
13
+ { name: "meetings_count", data: [0], tooltip_key: "meetings_count_tooltip" },
14
+ { name: "projects_count", data: [0], tooltip_key: "budgets_count_tooltip" },
15
+ { name: "surveys_count", data: [0], tooltip_key: "surveys_count_tooltip" },
16
+ { name: "results_count", data: [0], tooltip_key: "results_count_tooltip" },
17
+ { name: "debates_count", data: [0], tooltip_key: "debates_count_tooltip" },
18
+ { name: "sortitions_count", data: [0], tooltip_key: "sortitions_count_tooltip" },
19
+ { name: "posts_count", data: [0], tooltip_key: "posts_count_tooltip" },
20
+ { name: "collaborative_texts_count", data: [0], tooltip_key: "collaborative_texts_count_tooltip" }
21
+ ]))
22
22
  end
23
23
 
24
24
  it "executes successfully" do
@@ -26,7 +26,21 @@ shared_examples "implements stats type" do
26
26
  end
27
27
 
28
28
  it "returns the correct response" do
29
- expect(stats_response).to match_array(expected_data["stats"])
29
+ expect(stats_response).to match_array(
30
+ [
31
+ { "name" => { "translation" => "Dummies high" }, "value" => 0 },
32
+ { "name" => { "translation" => "Pages" }, "value" => 0 },
33
+ { "name" => { "translation" => "Proposals" }, "value" => 0 },
34
+ { "name" => { "translation" => "Meetings" }, "value" => 0 },
35
+ { "name" => { "translation" => "Budgets" }, "value" => 0 },
36
+ { "name" => { "translation" => "Surveys" }, "value" => 0 },
37
+ { "name" => { "translation" => "Results" }, "value" => 0 },
38
+ { "name" => { "translation" => "Debates" }, "value" => 0 },
39
+ { "name" => { "translation" => "Sortitions" }, "value" => 0 },
40
+ { "name" => { "translation" => "Posts" }, "value" => 0 },
41
+ { "name" => { "translation" => "Collaborative texts" }, "value" => 0 }
42
+ ]
43
+ )
30
44
  end
31
45
  end
32
46
  end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ shared_examples_for "taxonomizable interface" do
6
+ let!(:root_taxonomy) { create(:taxonomy, organization:) }
7
+ let!(:taxonomy) { create(:taxonomy, parent: root_taxonomy, organization:) }
8
+
9
+ before do
10
+ model.update(taxonomies: [taxonomy])
11
+ end
12
+
13
+ describe "taxonomies" do
14
+ let(:query) { "{ taxonomies { id } }" }
15
+
16
+ it "has taxonomies" do
17
+ expect(response).to include("taxonomies" => [{ "id" => taxonomy.id.to_s }])
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ shared_examples_for "timestamps interface" do
6
+ describe "createdAt" do
7
+ let(:query) { "{ createdAt }" }
8
+
9
+ it "returns when was this query created at" do
10
+ expect(response["createdAt"]).to eq(model.created_at.to_time.iso8601)
11
+ end
12
+ end
13
+
14
+ describe "updatedAt" do
15
+ let(:query) { "{ updatedAt }" }
16
+
17
+ it "returns when was this query updated at" do
18
+ expect(response["updatedAt"]).to eq(model.updated_at.to_time.iso8601)
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ shared_examples_for "traceable interface" do
6
+ let(:field) { :title }
7
+
8
+ describe "traceable", versioning: true do
9
+ let(:version_author) { try(:author) || model.try(:creator_identity) || model.try(:normalized_author) }
10
+
11
+ before { Decidim.traceability.update!(model, version_author, field => { en: "test" }) }
12
+
13
+ context "when field createdAt" do
14
+ let(:query) { "{ versions { createdAt } }" }
15
+
16
+ it "returns created_at field of the version to iso format" do
17
+ dates = response["versions"].map { |version| version["createdAt"] }
18
+ expect(dates).to include(*model.versions.map { |version| version.created_at.to_time.iso8601 })
19
+ end
20
+ end
21
+
22
+ context "when field id" do
23
+ let(:query) { "{ versions { id } }" }
24
+
25
+ it "returns ID field of the version" do
26
+ ids = response["versions"].map { |version| version["id"].to_i }
27
+ expect(ids).to include(*model.versions.map(&:id))
28
+ end
29
+ end
30
+
31
+ context "when field editor" do
32
+ let(:query) { "{ versions { editor { name } } }" }
33
+
34
+ it "returns editor field of the versions" do
35
+ editors = response["versions"].map { |version| version["editor"]["name"] if version["editor"] }
36
+ expect(editors).to include(*model.versions.map { |version| version.editor.name if version.respond_to? :editor })
37
+ end
38
+ end
39
+
40
+ context "when field changeset" do
41
+ let(:query) { "{ versions { changeset } }" }
42
+
43
+ it "returns changeset field of the versions" do
44
+ changesets = response["versions"].map { |version| version["changeset"] }
45
+ expect(changesets).to include(*model.versions.map(&:changeset))
46
+ end
47
+ end
48
+ end
49
+ end
@@ -4,11 +4,17 @@ shared_context "with a graphql class type" do
4
4
  let!(:current_organization) { create(:organization) }
5
5
  let!(:current_user) { create(:user, :confirmed, organization: current_organization) }
6
6
  let!(:current_component) { create(:component) }
7
+ let(:api_scopes) do
8
+ if current_user.present?
9
+ Doorkeeper::OAuth::Scopes.from_array(Doorkeeper.config.scopes.all)
10
+ else
11
+ Doorkeeper::OAuth::Scopes.from_string("api:read")
12
+ end
13
+ end
7
14
  let(:model) { OpenStruct.new({}) }
8
15
  let(:type_class) { described_class }
9
16
  let(:variables) { {} }
10
17
  let(:root_value) { model }
11
- let(:can_introspect) { Decidim::Api.enable_anonymous_introspection || current_user&.admin? }
12
18
 
13
19
  let(:schema) do
14
20
  klass = type_class
@@ -22,20 +28,6 @@ shared_context "with a graphql class type" do
22
28
  execute_query query, variables.stringify_keys
23
29
  end
24
30
 
25
- def raise_proper_error(error)
26
- code = error.dig("extensions", "code")
27
-
28
- # Matches the error code with the Error class
29
- # For instance, if the error code is NOT_FOUND_ERROR then it will raise the "Decidim::Api::Errors::NotFoundError" class
30
- raise "Decidim::Api::Errors::#{code.downcase.classify}".constantize, error["message"] if %w(
31
- INTROSPECTION_DISABLED_ERROR
32
- TOO_MANY_ALIASES_ERROR
33
- RECURSION_LIMIT_EXCEEDED_ERROR
34
- ).include?(code)
35
-
36
- raise GraphQL::ExecutionError, error["message"]
37
- end
38
-
39
31
  def execute_query(query, variables)
40
32
  result = schema.execute(
41
33
  query,
@@ -44,92 +36,17 @@ shared_context "with a graphql class type" do
44
36
  current_organization:,
45
37
  current_user:,
46
38
  current_component:,
47
- can_introspect:
39
+ scopes: api_scopes
48
40
  },
49
41
  variables:
50
42
  )
51
43
 
52
- raise_proper_error(result["errors"].first) if result["errors"]
44
+ raise StandardError, result["errors"].map { |e| e["message"] }.join(", ") if result["errors"]
53
45
 
54
46
  result["data"]
55
47
  end
56
48
  end
57
49
 
58
- shared_examples "when the introspection is disabled" do
59
- shared_examples "check introspection behavior" do
60
- context "and the user is not authenticated" do
61
- let!(:current_user) { nil }
62
-
63
- it "raises an Decidim::Api::Errors::IntrospectionDisabledError" do
64
- expect { response }.to raise_error(Decidim::Api::Errors::IntrospectionDisabledError, "Introspection is disabled for this request")
65
- end
66
- end
67
-
68
- context "and the user is not an admin" do
69
- let!(:current_user) { create(:user, :confirmed, organization: current_organization) }
70
-
71
- it "raises an Decidim::Api::Errors::IntrospectionDisabledError" do
72
- expect { response }.to raise_error(Decidim::Api::Errors::IntrospectionDisabledError, "Introspection is disabled for this request")
73
- end
74
- end
75
-
76
- context "and the user is an admin" do
77
- let!(:current_user) { create(:user, :confirmed, :admin, organization: current_organization) }
78
-
79
- it "runs successfully" do
80
- expect { response }.not_to raise_error
81
- end
82
- end
83
-
84
- context "and the setting is true" do
85
- before do
86
- allow(Decidim::Api).to receive(:enable_anonymous_introspection).and_return(true)
87
- end
88
-
89
- it "runs successfully" do
90
- expect { response }.not_to raise_error
91
- end
92
- end
93
-
94
- context "and the setting is false" do
95
- before do
96
- allow(Decidim::Api).to receive(:enable_anonymous_introspection).and_return(false)
97
- end
98
- it "raises an Decidim::Api::Errors::IntrospectionDisabledError" do
99
- expect { response }.to raise_error(Decidim::Api::Errors::IntrospectionDisabledError, "Introspection is disabled for this request")
100
- end
101
- end
102
- end
103
-
104
- context "when requesting the schema introspection" do
105
- let(:query) do
106
- %( query { __schema { types { fields { type { fields { type { name } } } } } } } )
107
- end
108
-
109
- it_behaves_like "check introspection behavior"
110
- end
111
-
112
- context "when requesting the type introspection" do
113
- let(:query) do
114
- %( query CircularIntrospection {
115
- __type(name: "User") {
116
- fields {
117
- type {
118
- fields {
119
- type {
120
- name
121
- }
122
- }
123
- }
124
- }
125
- }
126
- } )
127
- end
128
-
129
- it_behaves_like "check introspection behavior"
130
- end
131
- end
132
-
133
50
  shared_context "with a graphql scalar class type" do
134
51
  include_context "with a graphql class type"
135
52