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,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
|
data/lib/decidim/api/test/shared_examples/participatory_space_resourcable_interface_examples.rb
ADDED
|
@@ -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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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(
|
|
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
|
-
|
|
39
|
+
scopes: api_scopes
|
|
48
40
|
},
|
|
49
41
|
variables:
|
|
50
42
|
)
|
|
51
43
|
|
|
52
|
-
|
|
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
|
|