From b1b963f90a963f5a95a2291bbe8e049b4b3015ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javi=20Mart=C3=ADn?= Date: Tue, 23 Jul 2024 17:04:59 +0200 Subject: [PATCH 1/3] Fix public_for_api association tests These tests were always passing because they were stubbing the response of the same method they were testing. For example, we were testing the result of `Comment.public_for_api` and stubbing it at the same time. So we're now stubbing the result of the associations; for example, in order to test `Comment.public_for_api`, we're stubbing the response of `Debate.public_for_api`. Now the tests fail if, for instance, the implementation of `Comment.public_for_api` returns all comments. --- spec/lib/graphql_spec.rb | 28 ++++++++++++---------------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/spec/lib/graphql_spec.rb b/spec/lib/graphql_spec.rb index f03f42ecb..af2a1af4e 100644 --- a/spec/lib/graphql_spec.rb +++ b/spec/lib/graphql_spec.rb @@ -362,9 +362,8 @@ describe "Consul Schema" do end it "does not include comments of debates that are not public" do - not_public_debate = create(:debate, :hidden) - not_public_debate_comment = create(:comment, commentable: not_public_debate) - allow(Comment).to receive(:public_for_api).and_return([]) + allow(Debate).to receive(:public_for_api).and_return([]) + not_public_debate_comment = create(:comment, commentable: create(:debate)) response = execute("{ comments { edges { node { body } } } }") received_comments = extract_fields(response, "comments", "body") @@ -373,9 +372,8 @@ describe "Consul Schema" do end it "does not include comments of proposals that are not public" do - not_public_proposal = create(:proposal) - not_public_proposal_comment = create(:comment, commentable: not_public_proposal) - allow(Comment).to receive(:public_for_api).and_return([]) + allow(Proposal).to receive(:public_for_api).and_return([]) + not_public_proposal_comment = create(:comment, commentable: create(:proposal)) response = execute("{ comments { edges { node { body } } } }") received_comments = extract_fields(response, "comments", "body") @@ -384,9 +382,8 @@ describe "Consul Schema" do end it "does not include comments of polls that are not public" do - not_public_poll = create(:poll) - not_public_poll_comment = create(:comment, commentable: not_public_poll) - allow(Comment).to receive(:public_for_api).and_return([]) + allow(Poll).to receive(:public_for_api).and_return([]) + not_public_poll_comment = create(:comment, commentable: create(:poll)) response = execute("{ comments { edges { node { body } } } }") received_comments = extract_fields(response, "comments", "body") @@ -462,9 +459,8 @@ describe "Consul Schema" do end it "does not include proposal notifications for proposals that are not public" do - not_public_proposal = create(:proposal) - not_public_proposal_notification = create(:proposal_notification, proposal: not_public_proposal) - allow(ProposalNotification).to receive(:public_for_api).and_return([]) + allow(Proposal).to receive(:public_for_api).and_return([]) + not_public_proposal_notification = create(:proposal_notification, proposal: create(:proposal)) response = execute("{ proposal_notifications { edges { node { title } } } }") received_notifications = extract_fields(response, "proposal_notifications", "title") @@ -558,8 +554,8 @@ describe "Consul Schema" do end it "does not display tags for taggings that are not public" do + allow(Proposal).to receive(:public_for_api).and_return([]) create(:proposal, tag_list: "Health") - allow(Tag).to receive(:public_for_api).and_return([]) response = execute("{ tags { edges { node { name } } } }") received_tags = extract_fields(response, "tags", "name") @@ -638,7 +634,7 @@ describe "Consul Schema" do end it "does not include votes of debates that are not public" do - allow(Vote).to receive(:public_for_api).and_return([]) + allow(Debate).to receive(:public_for_api).and_return([]) not_public_debate = create(:debate, voters: [create(:user)]) response = execute("{ votes { edges { node { votable_id } } } }") @@ -648,7 +644,7 @@ describe "Consul Schema" do end it "does not include votes of a hidden proposals" do - allow(Vote).to receive(:public_for_api).and_return([]) + allow(Proposal).to receive(:public_for_api).and_return([]) not_public_proposal = create(:proposal, voters: [create(:user)]) response = execute("{ votes { edges { node { votable_id } } } }") @@ -658,7 +654,7 @@ describe "Consul Schema" do end it "does not include votes of a hidden comments" do - allow(Vote).to receive(:public_for_api).and_return([]) + allow(Comment).to receive(:public_for_api).and_return([]) not_public_comment = create(:comment, voters: [create(:user)]) response = execute("{ votes { edges { node { votable_id } } } }") From ba558b149023863676589414a2ebedc08c2c7d07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javi=20Mart=C3=ADn?= Date: Tue, 23 Jul 2024 02:09:11 +0200 Subject: [PATCH 2/3] Reorganize graphql specs Back in commit c984e666f, we reorganized the code related to the GraphQL API, but we didn't reorganize the tests. So we're doing it now, since we're going to fix a potential issue and add some tests for it. --- spec/graphql/types/budget_type_spec.rb | 34 ++ spec/graphql/types/comment_type_spec.rb | 23 ++ spec/graphql/types/debate_type_spec.rb | 36 ++ spec/graphql/types/milestone_type_spec.rb | 11 + .../types/proposal_notification_type_spec.rb | 26 ++ spec/graphql/types/proposal_type_spec.rb | 46 +++ .../types/query_type_spec.rb} | 378 ++++-------------- spec/graphql/types/user_type_spec.rb | 45 +++ spec/graphql/types/vote_type_spec.rb | 13 + spec/support/common_actions.rb | 1 + spec/support/common_actions/graphql_api.rb | 31 ++ 11 files changed, 334 insertions(+), 310 deletions(-) create mode 100644 spec/graphql/types/budget_type_spec.rb create mode 100644 spec/graphql/types/comment_type_spec.rb create mode 100644 spec/graphql/types/debate_type_spec.rb create mode 100644 spec/graphql/types/milestone_type_spec.rb create mode 100644 spec/graphql/types/proposal_notification_type_spec.rb create mode 100644 spec/graphql/types/proposal_type_spec.rb rename spec/{lib/graphql_spec.rb => graphql/types/query_type_spec.rb} (66%) create mode 100644 spec/graphql/types/user_type_spec.rb create mode 100644 spec/graphql/types/vote_type_spec.rb create mode 100644 spec/support/common_actions/graphql_api.rb diff --git a/spec/graphql/types/budget_type_spec.rb b/spec/graphql/types/budget_type_spec.rb new file mode 100644 index 000000000..b9c79959d --- /dev/null +++ b/spec/graphql/types/budget_type_spec.rb @@ -0,0 +1,34 @@ +require "rails_helper" + +describe Types::BudgetType do + describe "#investment" do + it "does not include hidden comments" do + budget = create(:budget) + investment = create(:budget_investment, budget: budget) + + create(:comment, commentable: investment, body: "Visible") + create(:comment, :hidden, commentable: investment, body: "Hidden") + + query = <<~GRAPHQL + { + budget(id: #{budget.id}) { + investment(id: #{investment.id}) { + comments { + edges { + node { + body + } + } + } + } + } + } + GRAPHQL + + response = execute(query) + received_bodies = extract_fields(response, "budget.investment.comments", "body") + + expect(received_bodies).to eq ["Visible"] + end + end +end diff --git a/spec/graphql/types/comment_type_spec.rb b/spec/graphql/types/comment_type_spec.rb new file mode 100644 index 000000000..0fcb5a2f7 --- /dev/null +++ b/spec/graphql/types/comment_type_spec.rb @@ -0,0 +1,23 @@ +require "rails_helper" + +describe Types::CommentType do + it "does not link author if public activity is set to false" do + create(:user, :with_comment, username: "public", public_activity: true) + create(:user, :with_comment, username: "private", public_activity: false) + + response = execute("{ comments { edges { node { public_author { username } } } } }") + received_authors = extract_fields(response, "comments", "public_author.username") + + expect(received_authors).to match_array ["public"] + end + + it "only returns date and hour for created_at" do + created_at = Time.zone.parse("2017-12-31 9:30:15") + create(:comment, created_at: created_at) + + response = execute("{ comments { edges { node { public_created_at } } } }") + received_timestamps = extract_fields(response, "comments", "public_created_at") + + expect(Time.zone.parse(received_timestamps.first)).to eq Time.zone.parse("2017-12-31 9:00:00") + end +end diff --git a/spec/graphql/types/debate_type_spec.rb b/spec/graphql/types/debate_type_spec.rb new file mode 100644 index 000000000..caae3e0c8 --- /dev/null +++ b/spec/graphql/types/debate_type_spec.rb @@ -0,0 +1,36 @@ +require "rails_helper" + +describe Types::DebateType do + it "does not link author if public activity is set to false" do + create(:user, :with_debate, username: "public", public_activity: true) + create(:user, :with_debate, username: "private", public_activity: false) + + response = execute("{ debates { edges { node { public_author { username } } } } }") + received_authors = extract_fields(response, "debates", "public_author.username") + + expect(received_authors).to match_array ["public"] + end + + it "only returns date and hour for created_at" do + created_at = Time.zone.parse("2017-12-31 9:30:15") + create(:debate, created_at: created_at) + + response = execute("{ debates { edges { node { public_created_at } } } }") + received_timestamps = extract_fields(response, "debates", "public_created_at") + + expect(Time.zone.parse(received_timestamps.first)).to eq Time.zone.parse("2017-12-31 9:00:00") + end + + it "only returns tags with kind nil or category" do + create(:tag, name: "Parks") + create(:tag, :category, name: "Health") + create(:tag, name: "Admin tag", kind: "admin") + + debate = create(:debate, tag_list: "Parks, Health, Admin tag") + + response = execute("{ debate(id: #{debate.id}) { tags { edges { node { name } } } } }") + received_tags = dig(response, "data.debate.tags.edges").map { |node| node["node"]["name"] } + + expect(received_tags).to match_array ["Parks", "Health"] + end +end diff --git a/spec/graphql/types/milestone_type_spec.rb b/spec/graphql/types/milestone_type_spec.rb new file mode 100644 index 000000000..11c2a971a --- /dev/null +++ b/spec/graphql/types/milestone_type_spec.rb @@ -0,0 +1,11 @@ +require "rails_helper" + +describe Types::MilestoneType do + it "formats publication date like in view" do + milestone = create(:milestone, publication_date: Time.zone.parse("2024-07-02 11:45:17")) + + response = execute("{ milestone(id: #{milestone.id}) { id publication_date } }") + received_publication_date = dig(response, "data.milestone.publication_date") + expect(received_publication_date).to eq "2024-07-02" + end +end diff --git a/spec/graphql/types/proposal_notification_type_spec.rb b/spec/graphql/types/proposal_notification_type_spec.rb new file mode 100644 index 000000000..b13da62a5 --- /dev/null +++ b/spec/graphql/types/proposal_notification_type_spec.rb @@ -0,0 +1,26 @@ +require "rails_helper" + +describe Types::ProposalNotificationType do + it "only returns date and hour for created_at" do + created_at = Time.zone.parse("2017-12-31 9:30:15") + create(:proposal_notification, created_at: created_at) + + response = execute("{ proposal_notifications { edges { node { public_created_at } } } }") + received_timestamps = extract_fields(response, "proposal_notifications", "public_created_at") + + expect(Time.zone.parse(received_timestamps.first)).to eq Time.zone.parse("2017-12-31 9:00:00") + end + + it "only links proposal if public" do + visible_proposal = create(:proposal, title: "Visible") + hidden_proposal = create(:proposal, :hidden, title: "Hidden") + + create(:proposal_notification, proposal: visible_proposal) + create(:proposal_notification, proposal: hidden_proposal) + + response = execute("{ proposal_notifications { edges { node { proposal { title } } } } }") + received_proposals = extract_fields(response, "proposal_notifications", "proposal.title") + + expect(received_proposals).to match_array ["Visible"] + end +end diff --git a/spec/graphql/types/proposal_type_spec.rb b/spec/graphql/types/proposal_type_spec.rb new file mode 100644 index 000000000..0db67509d --- /dev/null +++ b/spec/graphql/types/proposal_type_spec.rb @@ -0,0 +1,46 @@ +require "rails_helper" + +describe Types::ProposalType do + it "does not link author if public activity is set to false" do + create(:user, :with_proposal, username: "public", public_activity: true) + create(:user, :with_proposal, username: "private", public_activity: false) + + response = execute("{ proposals { edges { node { public_author { username } } } } }") + received_authors = extract_fields(response, "proposals", "public_author.username") + + expect(received_authors).to match_array ["public"] + end + + it "only returns date and hour for created_at" do + created_at = Time.zone.parse("2017-12-31 9:30:15") + create(:proposal, created_at: created_at) + + response = execute("{ proposals { edges { node { public_created_at } } } }") + received_timestamps = extract_fields(response, "proposals", "public_created_at") + + expect(Time.zone.parse(received_timestamps.first)).to eq Time.zone.parse("2017-12-31 9:00:00") + end + + it "only returns tags with kind nil or category" do + create(:tag, name: "Parks") + create(:tag, :category, name: "Health") + create(:tag, name: "Admin tag", kind: "admin") + + proposal = create(:proposal, tag_list: "Parks, Health, Admin tag") + + response = execute("{ proposal(id: #{proposal.id}) { tags { edges { node { name } } } } }") + received_tags = dig(response, "data.proposal.tags.edges").map { |node| node["node"]["name"] } + + expect(received_tags).to match_array ["Parks", "Health"] + end + + it "returns nested votes for a proposal" do + proposal = create(:proposal, voters: [create(:user), create(:user)]) + + response = execute("{ proposal(id: #{proposal.id}) " \ + "{ votes_for { edges { node { public_created_at } } } } }") + + votes = response["data"]["proposal"]["votes_for"]["edges"] + expect(votes.count).to eq(2) + end +end diff --git a/spec/lib/graphql_spec.rb b/spec/graphql/types/query_type_spec.rb similarity index 66% rename from spec/lib/graphql_spec.rb rename to spec/graphql/types/query_type_spec.rb index af2a1af4e..17ac4fc4a 100644 --- a/spec/lib/graphql_spec.rb +++ b/spec/graphql/types/query_type_spec.rb @@ -1,36 +1,6 @@ require "rails_helper" -def execute(query_string, context = {}, variables = {}) - ConsulSchema.execute(query_string, context: context, variables: variables) -end - -def dig(response, path) - response.dig(*path.split(".")) -end - -def hidden_field?(response, field_name) - data_is_empty = response["data"].nil? - error_message = /Field '#{field_name}' doesn't exist on type '[[:alnum:]]*'/ - - error_is_present = ((response["errors"].first["message"] =~ error_message) == 0) - data_is_empty && error_is_present -end - -def extract_fields(response, collection_name, field_chain) - fields = field_chain.split(".") - dig(response, "data.#{collection_name}.edges").map do |node| - begin - if fields.size > 1 - node["node"][fields.first][fields.second] - else - node["node"][fields.first] - end - rescue NoMethodError - end - end.compact -end - -describe "Consul Schema" do +describe Types::QueryType do let(:user) { create(:user) } let(:proposal) { create(:proposal, author: user) } @@ -94,175 +64,7 @@ describe "Consul Schema" do expect(hidden_field?(response, "encrypted_password")).to be_truthy end - describe "Users" do - let(:user) { create(:user, public_activity: false) } - - it "does not link debates if activity is not public" do - create(:debate, author: user) - - response = execute("{ user(id: #{user.id}) { public_debates { edges { node { title } } } } }") - received_debates = dig(response, "data.user.public_debates.edges") - - expect(received_debates).to eq [] - end - - it "does not link proposals if activity is not public" do - create(:proposal, author: user) - - response = execute("{ user(id: #{user.id}) { public_proposals { edges { node { title } } } } }") - received_proposals = dig(response, "data.user.public_proposals.edges") - - expect(received_proposals).to eq [] - end - - it "does not link comments if activity is not public" do - create(:comment, author: user) - - response = execute("{ user(id: #{user.id}) { public_comments { edges { node { body } } } } }") - received_comments = dig(response, "data.user.public_comments.edges") - - expect(received_comments).to eq [] - end - end - - describe "Proposals" do - it "does not include hidden proposals" do - create(:proposal, title: "Visible") - create(:proposal, :hidden, title: "Hidden") - - response = execute("{ proposals { edges { node { title } } } }") - received_titles = extract_fields(response, "proposals", "title") - - expect(received_titles).to match_array ["Visible"] - end - - it "includes proposals of authors even if public activity is set to false" do - visible_author = create(:user, public_activity: true) - hidden_author = create(:user, public_activity: false) - - visible_proposal = create(:proposal, author: visible_author) - hidden_proposal = create(:proposal, author: hidden_author) - - response = execute("{ proposals { edges { node { title } } } }") - received_titles = extract_fields(response, "proposals", "title") - - expect(received_titles).to match_array [visible_proposal.title, hidden_proposal.title] - end - - it "does not link author if public activity is set to false" do - create(:user, :with_proposal, username: "public", public_activity: true) - create(:user, :with_proposal, username: "private", public_activity: false) - - response = execute("{ proposals { edges { node { public_author { username } } } } }") - received_authors = extract_fields(response, "proposals", "public_author.username") - - expect(received_authors).to match_array ["public"] - end - - it "only returns date and hour for created_at" do - created_at = Time.zone.parse("2017-12-31 9:30:15") - create(:proposal, created_at: created_at) - - response = execute("{ proposals { edges { node { public_created_at } } } }") - received_timestamps = extract_fields(response, "proposals", "public_created_at") - - expect(Time.zone.parse(received_timestamps.first)).to eq Time.zone.parse("2017-12-31 9:00:00") - end - - it "only retruns tags with kind nil or category" do - create(:tag, name: "Parks") - create(:tag, :category, name: "Health") - create(:tag, name: "Admin tag", kind: "admin") - - proposal = create(:proposal, tag_list: "Parks, Health, Admin tag") - - response = execute("{ proposal(id: #{proposal.id}) { tags { edges { node { name } } } } }") - received_tags = dig(response, "data.proposal.tags.edges").map { |node| node["node"]["name"] } - - expect(received_tags).to match_array ["Parks", "Health"] - end - - it "returns nested votes for a proposal" do - proposal = create(:proposal, voters: [create(:user), create(:user)]) - - response = execute("{ proposal(id: #{proposal.id}) " \ - "{ votes_for { edges { node { public_created_at } } } } }") - - votes = response["data"]["proposal"]["votes_for"]["edges"] - expect(votes.count).to eq(2) - end - end - - describe "Budgets" do - it "does not include unpublished budgets" do - create(:budget, :drafting, name: "Draft") - - response = execute("{ budgets { edges { node { name } } } }") - received_names = extract_fields(response, "budgets", "name") - - expect(received_names).to eq [] - end - end - - describe "Debates" do - it "does not include hidden debates" do - create(:debate, title: "Visible") - create(:debate, :hidden, title: "Hidden") - - response = execute("{ debates { edges { node { title } } } }") - received_titles = extract_fields(response, "debates", "title") - - expect(received_titles).to match_array ["Visible"] - end - - it "includes debates of authors even if public activity is set to false" do - visible_author = create(:user, public_activity: true) - hidden_author = create(:user, public_activity: false) - - visible_debate = create(:debate, author: visible_author) - hidden_debate = create(:debate, author: hidden_author) - - response = execute("{ debates { edges { node { title } } } }") - received_titles = extract_fields(response, "debates", "title") - - expect(received_titles).to match_array [visible_debate.title, hidden_debate.title] - end - - it "does not link author if public activity is set to false" do - create(:user, :with_debate, username: "public", public_activity: true) - create(:user, :with_debate, username: "private", public_activity: false) - - response = execute("{ debates { edges { node { public_author { username } } } } }") - received_authors = extract_fields(response, "debates", "public_author.username") - - expect(received_authors).to match_array ["public"] - end - - it "only returns date and hour for created_at" do - created_at = Time.zone.parse("2017-12-31 9:30:15") - create(:debate, created_at: created_at) - - response = execute("{ debates { edges { node { public_created_at } } } }") - received_timestamps = extract_fields(response, "debates", "public_created_at") - - expect(Time.zone.parse(received_timestamps.first)).to eq Time.zone.parse("2017-12-31 9:00:00") - end - - it "only retruns tags with kind nil or category" do - create(:tag, name: "Parks") - create(:tag, :category, name: "Health") - create(:tag, name: "Admin tag", kind: "admin") - - debate = create(:debate, tag_list: "Parks, Health, Admin tag") - - response = execute("{ debate(id: #{debate.id}) { tags { edges { node { name } } } } }") - received_tags = dig(response, "data.debate.tags.edges").map { |node| node["node"]["name"] } - - expect(received_tags).to match_array ["Parks", "Health"] - end - end - - describe "Comments" do + describe "#comments" do it "only returns comments from proposals, debates, polls and Budget::Investment" do create(:comment, commentable: create(:proposal)) create(:comment, commentable: create(:debate)) @@ -289,16 +91,6 @@ describe "Consul Schema" do expect(received_comments).to match_array [visible_comment.body, hidden_comment.body] end - it "does not link author if public activity is set to false" do - create(:user, :with_comment, username: "public", public_activity: true) - create(:user, :with_comment, username: "private", public_activity: false) - - response = execute("{ comments { edges { node { public_author { username } } } } }") - received_authors = extract_fields(response, "comments", "public_author.username") - - expect(received_authors).to match_array ["public"] - end - it "does not include hidden comments" do create(:comment, body: "Visible") create(:comment, :hidden, body: "Hidden") @@ -401,27 +193,6 @@ describe "Consul Schema" do expect(received_comments).not_to include(not_public_investment_comment.body) end - it "only links public comments" do - user = create(:administrator).user - create(:comment, author: user, body: "Public") - create(:budget_investment_comment, author: user, valuation: true, body: "Valuation") - - response = execute("{ user(id: #{user.id}) { public_comments { edges { node { body } } } } }") - received_comments = dig(response, "data.user.public_comments.edges") - - expect(received_comments).to eq [{ "node" => { "body" => "Public" }}] - end - - it "only returns date and hour for created_at" do - created_at = Time.zone.parse("2017-12-31 9:30:15") - create(:comment, created_at: created_at) - - response = execute("{ comments { edges { node { public_created_at } } } }") - received_timestamps = extract_fields(response, "comments", "public_created_at") - - expect(Time.zone.parse(received_timestamps.first)).to eq Time.zone.parse("2017-12-31 9:00:00") - end - it "does not include valuation comments" do create(:comment, body: "Regular comment") create(:comment, :valuation, body: "Valuation comment") @@ -433,7 +204,68 @@ describe "Consul Schema" do end end - describe "Geozones" do + describe "#budgets" do + it "does not include unpublished budgets" do + create(:budget, :drafting, name: "Draft") + + response = execute("{ budgets { edges { node { name } } } }") + received_names = extract_fields(response, "budgets", "name") + + expect(received_names).to eq [] + end + end + + describe "#debates" do + it "does not include hidden debates" do + create(:debate, title: "Visible") + create(:debate, :hidden, title: "Hidden") + + response = execute("{ debates { edges { node { title } } } }") + received_titles = extract_fields(response, "debates", "title") + + expect(received_titles).to match_array ["Visible"] + end + + it "includes debates of authors even if public activity is set to false" do + visible_author = create(:user, public_activity: true) + hidden_author = create(:user, public_activity: false) + + visible_debate = create(:debate, author: visible_author) + hidden_debate = create(:debate, author: hidden_author) + + response = execute("{ debates { edges { node { title } } } }") + received_titles = extract_fields(response, "debates", "title") + + expect(received_titles).to match_array [visible_debate.title, hidden_debate.title] + end + end + + describe "#proposals" do + it "does not include hidden proposals" do + create(:proposal, title: "Visible") + create(:proposal, :hidden, title: "Hidden") + + response = execute("{ proposals { edges { node { title } } } }") + received_titles = extract_fields(response, "proposals", "title") + + expect(received_titles).to match_array ["Visible"] + end + + it "includes proposals of authors even if public activity is set to false" do + visible_author = create(:user, public_activity: true) + hidden_author = create(:user, public_activity: false) + + visible_proposal = create(:proposal, author: visible_author) + hidden_proposal = create(:proposal, author: hidden_author) + + response = execute("{ proposals { edges { node { title } } } }") + received_titles = extract_fields(response, "proposals", "title") + + expect(received_titles).to match_array [visible_proposal.title, hidden_proposal.title] + end + end + + describe "#geozones" do it "returns geozones" do geozone_names = [create(:geozone), create(:geozone)].map(&:name) @@ -444,7 +276,7 @@ describe "Consul Schema" do end end - describe "Proposal notifications" do + describe "#proposal_notifications" do it "does not include proposal notifications for hidden proposals" do visible_proposal = create(:proposal) hidden_proposal = create(:proposal, :hidden) @@ -467,32 +299,9 @@ describe "Consul Schema" do expect(received_notifications).not_to include(not_public_proposal_notification.title) end - - it "only returns date and hour for created_at" do - created_at = Time.zone.parse("2017-12-31 9:30:15") - create(:proposal_notification, created_at: created_at) - - response = execute("{ proposal_notifications { edges { node { public_created_at } } } }") - received_timestamps = extract_fields(response, "proposal_notifications", "public_created_at") - - expect(Time.zone.parse(received_timestamps.first)).to eq Time.zone.parse("2017-12-31 9:00:00") - end - - it "only links proposal if public" do - visible_proposal = create(:proposal, title: "Visible") - hidden_proposal = create(:proposal, :hidden, title: "Hidden") - - create(:proposal_notification, proposal: visible_proposal) - create(:proposal_notification, proposal: hidden_proposal) - - response = execute("{ proposal_notifications { edges { node { proposal { title } } } } }") - received_proposals = extract_fields(response, "proposal_notifications", "proposal.title") - - expect(received_proposals).to match_array ["Visible"] - end end - describe "Tags" do + describe "#tags" do it "only display tags with kind nil or category" do create(:tag, name: "Parks") create(:tag, :category, name: "Health") @@ -522,7 +331,7 @@ describe "Consul Schema" do expect(received_tags).to match_array ["Health", "health"] end - it "works OK when both tags are present for proposals" do + it "works OK when both tags are present for debates" do create(:debate).tags = [uppercase_tag] create(:debate).tags = [lowercase_tag] @@ -564,7 +373,7 @@ describe "Consul Schema" do end end - describe "Votes" do + describe "#votes" do it "only returns votes from proposals, debates and comments" do create(:proposal, voters: [create(:user)]) create(:debate, voters: [create(:user)]) @@ -662,56 +471,5 @@ describe "Consul Schema" do expect(received_votables).not_to include(not_public_comment.id) end - - it "only returns date and hour for created_at" do - created_at = Time.zone.parse("2017-12-31 9:30:15") - create(:vote, created_at: created_at) - - response = execute("{ votes { edges { node { public_created_at } } } }") - received_timestamps = extract_fields(response, "votes", "public_created_at") - - expect(Time.zone.parse(received_timestamps.first)).to eq Time.zone.parse("2017-12-31 9:00:00") - end - end - - describe "Milestone" do - it "formats publication date like in view" do - milestone = create(:milestone, publication_date: Time.zone.parse("2024-07-02 11:45:17")) - - response = execute("{ milestone(id: #{milestone.id}) { id publication_date } }") - received_publication_date = dig(response, "data.milestone.publication_date") - expect(received_publication_date).to eq "2024-07-02" - end - end - - describe "Budget investment" do - it "does not include hidden comments" do - budget = create(:budget) - investment = create(:budget_investment, budget: budget) - - create(:comment, commentable: investment, body: "Visible") - create(:comment, :hidden, commentable: investment, body: "Hidden") - - query = <<~GRAPHQL - { - budget(id: #{budget.id}) { - investment(id: #{investment.id}) { - comments { - edges { - node { - body - } - } - } - } - } - } - GRAPHQL - - response = execute(query) - received_bodies = extract_fields(response, "budget.investment.comments", "body") - - expect(received_bodies).to eq ["Visible"] - end end end diff --git a/spec/graphql/types/user_type_spec.rb b/spec/graphql/types/user_type_spec.rb new file mode 100644 index 000000000..e81baaf90 --- /dev/null +++ b/spec/graphql/types/user_type_spec.rb @@ -0,0 +1,45 @@ +require "rails_helper" + +describe Types::UserType do + context "activity is not public" do + let(:user) { create(:user, public_activity: false) } + + it "does not link debates" do + create(:debate, author: user) + + response = execute("{ user(id: #{user.id}) { public_debates { edges { node { title } } } } }") + received_debates = dig(response, "data.user.public_debates.edges") + + expect(received_debates).to eq [] + end + + it "does not link proposals" do + create(:proposal, author: user) + + response = execute("{ user(id: #{user.id}) { public_proposals { edges { node { title } } } } }") + received_proposals = dig(response, "data.user.public_proposals.edges") + + expect(received_proposals).to eq [] + end + + it "does not link comments" do + create(:comment, author: user) + + response = execute("{ user(id: #{user.id}) { public_comments { edges { node { body } } } } }") + received_comments = dig(response, "data.user.public_comments.edges") + + expect(received_comments).to eq [] + end + end + + it "only links public comments" do + user = create(:administrator).user + create(:comment, author: user, body: "Public") + create(:budget_investment_comment, author: user, valuation: true, body: "Valuation") + + response = execute("{ user(id: #{user.id}) { public_comments { edges { node { body } } } } }") + received_comments = dig(response, "data.user.public_comments.edges") + + expect(received_comments).to eq [{ "node" => { "body" => "Public" }}] + end +end diff --git a/spec/graphql/types/vote_type_spec.rb b/spec/graphql/types/vote_type_spec.rb new file mode 100644 index 000000000..ea3563877 --- /dev/null +++ b/spec/graphql/types/vote_type_spec.rb @@ -0,0 +1,13 @@ +require "rails_helper" + +describe Types::VoteType do + it "only returns date and hour for created_at" do + created_at = Time.zone.parse("2017-12-31 9:30:15") + create(:vote, created_at: created_at) + + response = execute("{ votes { edges { node { public_created_at } } } }") + received_timestamps = extract_fields(response, "votes", "public_created_at") + + expect(Time.zone.parse(received_timestamps.first)).to eq Time.zone.parse("2017-12-31 9:00:00") + end +end diff --git a/spec/support/common_actions.rb b/spec/support/common_actions.rb index ea415fa61..355ce41da 100644 --- a/spec/support/common_actions.rb +++ b/spec/support/common_actions.rb @@ -7,6 +7,7 @@ module CommonActions include Debates include Documents include Emails + include GraphQLAPI include Images include Maps include Notifications diff --git a/spec/support/common_actions/graphql_api.rb b/spec/support/common_actions/graphql_api.rb new file mode 100644 index 000000000..6deceb138 --- /dev/null +++ b/spec/support/common_actions/graphql_api.rb @@ -0,0 +1,31 @@ +module GraphQLAPI + def execute(query_string, context = {}, variables = {}) + ConsulSchema.execute(query_string, context: context, variables: variables) + end + + def dig(response, path) + response.dig(*path.split(".")) + end + + def hidden_field?(response, field_name) + data_is_empty = response["data"].nil? + error_message = /Field '#{field_name}' doesn't exist on type '[[:alnum:]]*'/ + + error_is_present = ((response["errors"].first["message"] =~ error_message) == 0) + data_is_empty && error_is_present + end + + def extract_fields(response, collection_name, field_chain) + fields = field_chain.split(".") + dig(response, "data.#{collection_name}.edges").map do |node| + begin + if fields.size > 1 + node["node"][fields.first][fields.second] + else + node["node"][fields.first] + end + rescue NoMethodError + end + end.compact + end +end From b01364d26b48efeb589bed588b92d2e94b53765c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javi=20Mart=C3=ADn?= Date: Tue, 23 Jul 2024 19:21:13 +0200 Subject: [PATCH 3/3] Make sure we only return public records in the API When returning a collection of records in the API, we were making sure we only returned public ones. However, when returning individual records, we were not checking that. In practice, this wasn't a big issue, since most `public_for_api` methods return all records, but it could affect Consul Democracy installations which might have customized their `public_for_api` method. The only exception was the `budget` method, since it was returning budgets that were still in drafting. --- app/graphql/types/budget_type.rb | 2 +- app/graphql/types/query_type.rb | 20 ++--- spec/graphql/types/query_type_spec.rb | 109 ++++++++++++++++++++++++++ 3 files changed, 120 insertions(+), 11 deletions(-) diff --git a/app/graphql/types/budget_type.rb b/app/graphql/types/budget_type.rb index 92ac2dfbe..87e2cef3a 100644 --- a/app/graphql/types/budget_type.rb +++ b/app/graphql/types/budget_type.rb @@ -13,7 +13,7 @@ module Types end def investment(id:) - Budget::Investment.find(id) + investments.find(id) end end end diff --git a/app/graphql/types/query_type.rb b/app/graphql/types/query_type.rb index cb823c8dd..ca90a77e3 100644 --- a/app/graphql/types/query_type.rb +++ b/app/graphql/types/query_type.rb @@ -62,7 +62,7 @@ module Types end def budget(id:) - Budget.find(id) + budgets.find(id) end def comments @@ -70,7 +70,7 @@ module Types end def comment(id:) - Comment.find(id) + comments.find(id) end def debates @@ -78,7 +78,7 @@ module Types end def debate(id:) - Debate.find(id) + debates.find(id) end def geozones @@ -86,7 +86,7 @@ module Types end def geozone(id:) - Geozone.find(id) + geozones.find(id) end def milestones @@ -94,7 +94,7 @@ module Types end def milestone(id:) - Milestone.find(id) + milestones.find(id) end def proposals @@ -102,7 +102,7 @@ module Types end def proposal(id:) - Proposal.find(id) + proposals.find(id) end def proposal_notifications @@ -110,7 +110,7 @@ module Types end def proposal_notification(id:) - ProposalNotification.find(id) + proposal_notifications.find(id) end def tags @@ -118,7 +118,7 @@ module Types end def tag(id:) - Tag.find(id) + tags.find(id) end def users @@ -126,7 +126,7 @@ module Types end def user(id:) - User.find(id) + users.find(id) end def votes @@ -134,7 +134,7 @@ module Types end def vote(id:) - Vote.find(id) + votes.find(id) end end end diff --git a/spec/graphql/types/query_type_spec.rb b/spec/graphql/types/query_type_spec.rb index 17ac4fc4a..6e050fd0c 100644 --- a/spec/graphql/types/query_type_spec.rb +++ b/spec/graphql/types/query_type_spec.rb @@ -204,6 +204,16 @@ describe Types::QueryType do end end + describe "#comment" do + it "does not find comments that are not public" do + comment = create(:comment, :valuation, body: "Valuation comment") + + expect do + execute("{ comment(id: #{comment.id}) { body } }") + end.to raise_exception ActiveRecord::RecordNotFound + end + end + describe "#budgets" do it "does not include unpublished budgets" do create(:budget, :drafting, name: "Draft") @@ -215,6 +225,16 @@ describe Types::QueryType do end end + describe "#budget" do + it "does not find budgets that are not public" do + budget = create(:budget, :drafting) + + expect do + execute("{ budget(id: #{budget.id}) { name } }") + end.to raise_exception ActiveRecord::RecordNotFound + end + end + describe "#debates" do it "does not include hidden debates" do create(:debate, title: "Visible") @@ -240,6 +260,17 @@ describe Types::QueryType do end end + describe "#debate" do + it "does not find debates that are not public" do + allow(Debate).to receive(:public_for_api).and_return(Debate.none) + debate = create(:debate) + + expect do + execute("{ debate(id: #{debate.id}) { title } } ") + end.to raise_exception ActiveRecord::RecordNotFound + end + end + describe "#proposals" do it "does not include hidden proposals" do create(:proposal, title: "Visible") @@ -265,6 +296,17 @@ describe Types::QueryType do end end + describe "#proposal" do + it "does not find proposals that are not public" do + allow(Proposal).to receive(:public_for_api).and_return(Proposal.none) + proposal = create(:proposal) + + expect do + execute("{ proposal(id: #{proposal.id}) { title } } ") + end.to raise_exception ActiveRecord::RecordNotFound + end + end + describe "#geozones" do it "returns geozones" do geozone_names = [create(:geozone), create(:geozone)].map(&:name) @@ -276,6 +318,28 @@ describe Types::QueryType do end end + describe "#geozone" do + it "does not find geozones that are not public" do + allow(Geozone).to receive(:public_for_api).and_return(Geozone.none) + geozone = create(:geozone) + + expect do + execute("{ geozone(id: #{geozone.id}) { name } }") + end.to raise_exception ActiveRecord::RecordNotFound + end + end + + describe "#milestone" do + it "does not find milestones that are not public" do + investment = create(:budget_investment, budget: create(:budget, :drafting)) + milestone = create(:milestone, milestoneable: investment) + + expect do + execute("{ milestone(id: #{milestone.id}) { title } }") + end.to raise_exception ActiveRecord::RecordNotFound + end + end + describe "#proposal_notifications" do it "does not include proposal notifications for hidden proposals" do visible_proposal = create(:proposal) @@ -301,6 +365,17 @@ describe Types::QueryType do end end + describe "#proposal_notification" do + it "does not find proposal notifications that are not public" do + allow(Proposal).to receive(:public_for_api).and_return(Proposal.none) + notification = create(:proposal_notification) + + expect do + execute("{ proposal_notification(id: #{notification.id}) { title } } ") + end.to raise_exception ActiveRecord::RecordNotFound + end + end + describe "#tags" do it "only display tags with kind nil or category" do create(:tag, name: "Parks") @@ -373,6 +448,29 @@ describe Types::QueryType do end end + describe "#tag" do + it "does not find tags that are not public" do + allow(Proposal).to receive(:public_for_api).and_return(Proposal.none) + tag = create(:tag, name: "Health") + create(:proposal, tag_list: "Health") + + expect do + execute("{ tag(id: #{tag.id}) { name } } ") + end.to raise_exception ActiveRecord::RecordNotFound + end + end + + describe "#user" do + it "does not find users that are not public" do + allow(User).to receive(:public_for_api).and_return(User.none) + user = create(:user) + + expect do + execute("{ user(id: #{user.id}) { username } } ") + end.to raise_exception ActiveRecord::RecordNotFound + end + end + describe "#votes" do it "only returns votes from proposals, debates and comments" do create(:proposal, voters: [create(:user)]) @@ -472,4 +570,15 @@ describe Types::QueryType do expect(received_votables).not_to include(not_public_comment.id) end end + + describe "#vote" do + it "does not find votes that are not public" do + allow(Debate).to receive(:public_for_api).and_return(Debate.none) + vote = create(:vote, votable: create(:debate)) + + expect do + execute("{ vote(id: #{vote.id}) { votable_id } }") + end.to raise_exception ActiveRecord::RecordNotFound + end + end end