Files
grecia/spec/lib/graphql_spec.rb
Finn Heemeyer c984e666ff Add new GraphQL types, schema (with fields) & base mutation
The current consul GraphQL API has two problems.

1) It uses some unnecessary complicated magic to automatically create
   the GraphQL types and querys using an `api.yml` file. This approach
   is over-engineered, complex and has no benefits. It's just harder to
   understand the code for people which are not familiar with the
   project (like me, lol).

2) It uses a deprecated DSL [1] that is soon going to be removed from
   `graphql-ruby` completely. We are already seeing deprecation warning
   because of this (see References).

There was one problem. I wanted to create the API so that it is fully
backwards compatible with the old one, BUT the old one uses field names
which are directly derived from the ruby code, which results in
snake_case field names - not the GraphQL way. When I'm using the
graphql-ruby Class-based syntax, it automatically creates the fields in
camelCase, which breaks backwards-compatibility.

So I've added deprecated snake_case field names to keep it
backwards-compatible.

[1] https://graphql-ruby.org/schema/class_based_api.html
2022-06-01 11:41:09 +02:00

643 lines
25 KiB
Ruby

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_is_present = ((response["errors"].first["message"] =~ /Field '#{field_name}' doesn't exist on type '[[:alnum:]]*'/) == 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
let(:user) { create(:user) }
let(:proposal) { create(:proposal, author: user) }
it "returns fields of Int type" do
response = execute("{ proposal(id: #{proposal.id}) { cached_votes_up } }")
expect(dig(response, "data.proposal.cached_votes_up")).to eq(proposal.cached_votes_up)
end
it "returns fields of String type" do
response = execute("{ proposal(id: #{proposal.id}) { title } }")
expect(dig(response, "data.proposal.title")).to eq(proposal.title)
end
it "returns belongs_to associations" do
response = execute("{ proposal(id: #{proposal.id}) { public_author { username } } }")
expect(dig(response, "data.proposal.public_author.username")).to eq(proposal.public_author.username)
end
it "returns has_many associations" do
comments_author = create(:user)
comment_1 = create(:comment, author: comments_author, commentable: proposal)
comment_2 = create(:comment, author: comments_author, commentable: proposal)
response = execute("{ proposal(id: #{proposal.id}) { comments { edges { node { body } } } } }")
comments = dig(response, "data.proposal.comments.edges").map { |edge| edge["node"] }
comment_bodies = comments.map { |comment| comment["body"] }
expect(comment_bodies).to match_array([comment_1.body, comment_2.body])
end
it "hides confidential fields of Int type" do
response = execute("{ user(id: #{user.id}) { failed_census_calls_count } }")
expect(hidden_field?(response, "failed_census_calls_count")).to be_truthy
end
it "hides confidential fields of String type" do
response = execute("{ user(id: #{user.id}) { encrypted_password } }")
expect(hidden_field?(response, "encrypted_password")).to be_truthy
end
it "hides confidential has_one associations" do
user.administrator = create(:administrator)
response = execute("{ user(id: #{user.id}) { administrator { id } } }")
expect(hidden_field?(response, "administrator")).to be_truthy
end
it "hides confidential belongs_to associations" do
create(:failed_census_call, user: user)
response = execute("{ user(id: #{user.id}) { failed_census_calls { id } } }")
expect(hidden_field?(response, "failed_census_calls")).to be_truthy
end
it "hides confidential has_many associations" do
create(:direct_message, sender: user)
response = execute("{ user(id: #{user.id}) { direct_messages_sent { id } } }")
expect(hidden_field?(response, "direct_messages_sent")).to be_truthy
end
it "hides confidential fields inside deeply nested queries" do
response = execute("{ proposals(first: 1) { edges { node { public_author { encrypted_password } } } } }")
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 "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
it "only returns comments from proposals, debates and polls" do
create(:comment, commentable: create(:proposal))
create(:comment, commentable: create(:debate))
create(:comment, commentable: create(:poll))
build(:comment, commentable: create(:budget_investment)).save!(skip_validation: true)
response = execute("{ comments { edges { node { commentable_type } } } }")
received_commentables = extract_fields(response, "comments", "commentable_type")
expect(received_commentables).to match_array ["Proposal", "Debate", "Poll"]
end
it "displays comments 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_comment = create(:comment, user: visible_author)
hidden_comment = create(:comment, user: hidden_author)
response = execute("{ comments { edges { node { body } } } }")
received_comments = extract_fields(response, "comments", "body")
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")
response = execute("{ comments { edges { node { body } } } }")
received_comments = extract_fields(response, "comments", "body")
expect(received_comments).to match_array ["Visible"]
end
it "does not include comments from hidden proposals" do
visible_proposal = create(:proposal)
hidden_proposal = create(:proposal, :hidden)
create(:comment, commentable: visible_proposal, body: "I can see the proposal")
create(:comment, commentable: hidden_proposal, body: "Someone hid the proposal!")
response = execute("{ comments { edges { node { body } } } }")
received_comments = extract_fields(response, "comments", "body")
expect(received_comments).to match_array ["I can see the proposal"]
end
it "does not include comments from hidden debates" do
visible_debate = create(:debate)
hidden_debate = create(:debate, :hidden)
create(:comment, commentable: visible_debate, body: "I can see the debate")
create(:comment, commentable: hidden_debate, body: "Someone hid the debate!")
response = execute("{ comments { edges { node { body } } } }")
received_comments = extract_fields(response, "comments", "body")
expect(received_comments).to match_array ["I can see the debate"]
end
it "does not include comments from hidden polls" do
visible_poll = create(:poll)
hidden_poll = create(:poll, :hidden)
create(:comment, commentable: visible_poll, body: "I can see the poll")
create(:comment, commentable: hidden_poll, body: "This poll is hidden!")
response = execute("{ comments { edges { node { body } } } }")
received_comments = extract_fields(response, "comments", "body")
expect(received_comments).to match_array ["I can see the poll"]
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([])
response = execute("{ comments { edges { node { body } } } }")
received_comments = extract_fields(response, "comments", "body")
expect(received_comments).not_to include(not_public_debate_comment.body)
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([])
response = execute("{ comments { edges { node { body } } } }")
received_comments = extract_fields(response, "comments", "body")
expect(received_comments).not_to include(not_public_proposal_comment.body)
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([])
response = execute("{ comments { edges { node { body } } } }")
received_comments = extract_fields(response, "comments", "body")
expect(received_comments).not_to include(not_public_poll_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")
response = execute("{ comments { edges { node { body } } } }")
received_comments = extract_fields(response, "comments", "body")
expect(received_comments).not_to include "Valuation comment"
end
end
describe "Geozones" do
it "returns geozones" do
geozone_names = [create(:geozone), create(:geozone)].map(&:name)
response = execute("{ geozones { edges { node { name } } } }")
received_names = extract_fields(response, "geozones", "name")
expect(received_names).to match_array geozone_names
end
end
describe "Proposal notifications" do
it "does not include proposal notifications for hidden proposals" do
visible_proposal = create(:proposal)
hidden_proposal = create(:proposal, :hidden)
create(:proposal_notification, proposal: visible_proposal, title: "I can see the proposal")
create(:proposal_notification, proposal: hidden_proposal, title: "Someone hid the proposal!")
response = execute("{ proposal_notifications { edges { node { title } } } }")
received_notifications = extract_fields(response, "proposal_notifications", "title")
expect(received_notifications).to match_array ["I can see the proposal"]
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([])
response = execute("{ proposal_notifications { edges { node { title } } } }")
received_notifications = extract_fields(response, "proposal_notifications", "title")
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
it "only display tags with kind nil or category" do
create(:tag, name: "Parks")
create(:tag, :category, name: "Health")
create(:tag, name: "Admin tag", kind: "admin")
create(:proposal, tag_list: "Parks")
create(:proposal, tag_list: "Health")
create(:proposal, tag_list: "Admin tag")
response = execute("{ tags { edges { node { name } } } }")
received_tags = extract_fields(response, "tags", "name")
expect(received_tags).to match_array ["Parks", "Health"]
end
context "uppercase and lowercase tags" do
let(:uppercase_tag) { create(:tag, name: "Health") }
let(:lowercase_tag) { create(:tag, name: "health") }
it "works OK when both tags are present for proposals" do
create(:proposal).tags = [uppercase_tag]
create(:proposal).tags = [lowercase_tag]
response = execute("{ tags { edges { node { name } } } }")
received_tags = extract_fields(response, "tags", "name")
expect(received_tags).to match_array ["Health", "health"]
end
it "works OK when both tags are present for proposals" do
create(:debate).tags = [uppercase_tag]
create(:debate).tags = [lowercase_tag]
response = execute("{ tags { edges { node { name } } } }")
received_tags = extract_fields(response, "tags", "name")
expect(received_tags).to match_array ["Health", "health"]
end
end
it "does not display tags for hidden proposals" do
create(:proposal, tag_list: "Health")
create(:proposal, :hidden, tag_list: "SPAM")
response = execute("{ tags { edges { node { name } } } }")
received_tags = extract_fields(response, "tags", "name")
expect(received_tags).to match_array ["Health"]
end
it "does not display tags for hidden debates" do
create(:debate, tag_list: "Health, Transportation")
create(:debate, :hidden, tag_list: "SPAM")
response = execute("{ tags { edges { node { name } } } }")
received_tags = extract_fields(response, "tags", "name")
expect(received_tags).to match_array ["Health", "Transportation"]
end
it "does not display tags for taggings that are not public" do
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")
expect(received_tags).not_to include("Health")
end
end
describe "Votes" do
it "only returns votes from proposals, debates and comments" do
create(:proposal, voters: [create(:user)])
create(:debate, voters: [create(:user)])
create(:comment, voters: [create(:user)])
create(:budget_investment, voters: [create(:user)])
response = execute("{ votes { edges { node { votable_type } } } }")
received_votables = extract_fields(response, "votes", "votable_type")
expect(received_votables).to match_array ["Proposal", "Debate", "Comment"]
end
it "does not include votes from hidden debates" do
create(:debate, id: 1, voters: [create(:user)])
create(:debate, :hidden, id: 2, voters: [create(:user)])
response = execute("{ votes { edges { node { votable_id } } } }")
received_debates = extract_fields(response, "votes", "votable_id")
expect(received_debates).to match_array [1]
end
it "does not include votes of hidden proposals" do
create(:proposal, id: 1, voters: [create(:user)])
create(:proposal, :hidden, id: 2, voters: [create(:user)])
response = execute("{ votes { edges { node { votable_id } } } }")
received_proposals = extract_fields(response, "votes", "votable_id")
expect(received_proposals).to match_array [1]
end
it "does not include votes of hidden comments" do
create(:comment, id: 1, voters: [create(:user)])
create(:comment, :hidden, id: 2, voters: [create(:user)])
response = execute("{ votes { edges { node { votable_id } } } }")
received_comments = extract_fields(response, "votes", "votable_id")
expect(received_comments).to match_array [1]
end
it "does not include votes of comments from a hidden proposal" do
visible_proposal = create(:proposal)
hidden_proposal = create(:proposal, :hidden)
create(:comment, id: 1, commentable: visible_proposal, voters: [create(:user)])
create(:comment, id: 2, commentable: hidden_proposal, voters: [create(:user)])
response = execute("{ votes { edges { node { votable_id } } } }")
received_votables = extract_fields(response, "votes", "votable_id")
expect(received_votables).to match_array [1]
end
it "does not include votes of comments from a hidden debate" do
visible_debate = create(:debate)
hidden_debate = create(:debate, :hidden)
create(:comment, id: 1, commentable: visible_debate, voters: [create(:user)])
create(:comment, id: 2, commentable: hidden_debate, voters: [create(:user)])
response = execute("{ votes { edges { node { votable_id } } } }")
received_votables = extract_fields(response, "votes", "votable_id")
expect(received_votables).to match_array [1]
end
it "does not include votes of debates that are not public" do
allow(Vote).to receive(:public_for_api).and_return([])
not_public_debate = create(:debate, voters: [create(:user)])
response = execute("{ votes { edges { node { votable_id } } } }")
received_votables = extract_fields(response, "votes", "votable_id")
expect(received_votables).not_to include(not_public_debate.id)
end
it "does not include votes of a hidden proposals" do
allow(Vote).to receive(:public_for_api).and_return([])
not_public_proposal = create(:proposal, voters: [create(:user)])
response = execute("{ votes { edges { node { votable_id } } } }")
received_votables = extract_fields(response, "votes", "votable_id")
expect(received_votables).not_to include(not_public_proposal.id)
end
it "does not include votes of a hidden comments" do
allow(Vote).to receive(:public_for_api).and_return([])
not_public_comment = create(:comment, voters: [create(:user)])
response = execute("{ votes { edges { node { votable_id } } } }")
received_votables = extract_fields(response, "votes", "votable_id")
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
end