1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342
|
# frozen_string_literal: true
require "spec_helper"
describe GraphQL::Schema::Visibility do
class VisSchema < GraphQL::Schema
class BaseField < GraphQL::Schema::Field
def initialize(*args, admin_only: false, **kwargs, &block)
super(*args, **kwargs, &block)
@admin_only = admin_only
end
def visible?(ctx)
super && (@admin_only ? !!ctx[:is_admin] : true)
end
end
class BaseObject < GraphQL::Schema::Object
field_class(BaseField)
def self.visible?(ctx)
(@admin_only ? !!ctx[:is_admin] : true) && super
end
def self.admin_only(new_value = nil)
if new_value.nil?
@admin_only
else
@admin_only = new_value
end
end
end
class Product < BaseObject
field :name, String
field :price, Integer
field :cost_of_goods_sold, Integer, admin_only: true
end
class Widget < BaseObject
admin_only(true)
field :name, String
end
class WidgetKind < GraphQL::Schema::Enum
value :FOO
value :BAR
end
class Query < BaseObject
field :products, [Product]
field :widget, Widget do
argument :type, WidgetKind
end
def products
[{ name: "Pool Noodle", price: 100, cost_of_goods_sold: 5 }]
end
end
query(Query)
use GraphQL::Schema::Visibility, profiles: { public: {}, admin: { is_admin: true } }, preload: true
end
class DynVisSchema < VisSchema
use GraphQL::Schema::Visibility, profiles: { public: {}, admin: {} }, dynamic: true, preload: false
end
class PreloadDynVisSchema < VisSchema
use GraphQL::Schema::Visibility, profiles: { public: {}, admin: {} }, dynamic: true, preload: true
end
def exec_query(...)
VisSchema.execute(...)
end
describe "top-level schema caches" do
it "re-uses results" do
assert_equal DynVisSchema.types.object_id, DynVisSchema.types.object_id
assert_equal PreloadDynVisSchema.types.object_id, PreloadDynVisSchema.types.object_id
end
end
it "hides unused arguments" do
schema_sdl = VisSchema.to_definition(context: { visibility_profile: :public })
refute_includes schema_sdl, "WidgetKind"
end
describe "running queries" do
it "requires context[:visibility]" do
err = assert_raises ArgumentError do
exec_query("{ products { name } }")
end
expected_msg = "VisSchema expects a visibility profile, but `visibility_profile:` wasn't passed. Provide a `visibility_profile:` value or add `dynamic: true` to your visibility configuration."
assert_equal expected_msg, err.message
end
it "requires a context[:visibility] which is on the list" do
err = assert_raises ArgumentError do
exec_query("{ products { name } }", visibility_profile: :nonsense )
end
expected_msg = "`:nonsense` isn't allowed for `visibility_profile:` (must be one of :public, :admin). Or, add `:nonsense` to the list of profiles in the schema definition."
assert_equal expected_msg, err.message
end
it "permits `nil` when nil is on the list" do
res = DynVisSchema.execute("{ products { name } }")
assert_equal 1, res["data"]["products"].size
assert_nil res.context.types.name
assert_equal [], DynVisSchema.visibility.cached_profiles.keys
end
it "uses the named visibility" do
res = exec_query("{ products { name } }", visibility_profile: :public)
assert_equal ["Pool Noodle"], res["data"]["products"].map { |p| p["name"] }
assert_equal :public, res.context.types.name
assert res.context.types.equal?(VisSchema.visibility.cached_profiles[:public]), "It uses the cached instance"
res = exec_query("{ products { costOfGoodsSold } }", visibility_profile: :public)
assert_equal ["Field 'costOfGoodsSold' doesn't exist on type 'Product'"], res["errors"].map { |e| e["message"] }
res = exec_query("{ products { name costOfGoodsSold } }", visibility_profile: :admin)
assert_equal [{ "name" => "Pool Noodle", "costOfGoodsSold" => 5}], res["data"]["products"]
end
it "works with subclasses" do
child_schema = Class.new(VisSchema) do
query(VisSchema::Query)
end
res = child_schema.execute("{ products { name } }", visibility_profile: :public)
assert_equal ["Pool Noodle"], res["data"]["products"].map { |p| p["name"] }
assert_equal :public, res.context.types.name
end
end
describe "preloading profiles" do
it "preloads when true" do
assert_equal [:public, :admin], VisSchema.visibility.cached_profiles.keys, "preload: true"
assert_equal 0, DynVisSchema.visibility.cached_profiles.size, "preload: false"
end
describe "when no profile is defined" do
class NoProfileSchema < GraphQL::Schema
class ExampleExtension < GraphQL::Schema::FieldExtension; end
class OtherExampleExtension < GraphQL::Schema::FieldExtension; end
class Query < GraphQL::Schema::Object
field :str do
type(String)
extension(ExampleExtension)
end
end
class Mutation < GraphQL::Schema::Object
field :str do
type(String)
extension(ExampleExtension)
end
end
class Subscription < GraphQL::Schema::Object
field :str do
type(String)
extension(ExampleExtension)
end
end
class OrphanType < GraphQL::Schema::Object
field :str do
type(String)
extension(ExampleExtension)
extension(OtherExampleExtension)
end
end
# This one is added before `Visibility`
subscription(Subscription)
use GraphQL::Schema::Visibility, preload: true
query { Query }
mutation { Mutation }
orphan_types(OrphanType)
module CustomIntrospection
class DynamicFields < GraphQL::Introspection::DynamicFields
field :__hello do
type(String)
extension(OtherExampleExtension)
end
end
end
end
it "still preloads" do
assert_equal [NoProfileSchema::ExampleExtension], NoProfileSchema::Query.all_field_definitions.first.extensions.map(&:class)
assert_equal [NoProfileSchema::ExampleExtension], NoProfileSchema::Mutation.all_field_definitions.first.extensions.map(&:class)
assert_equal [NoProfileSchema::ExampleExtension], NoProfileSchema::Subscription.all_field_definitions.first.extensions.map(&:class)
assert_equal [NoProfileSchema::ExampleExtension, NoProfileSchema::OtherExampleExtension], NoProfileSchema::OrphanType.all_field_definitions.first.extensions.map(&:class)
custom_int_field = NoProfileSchema::CustomIntrospection::DynamicFields.all_field_definitions.find { |f| f.original_name == :__hello }
assert_equal [], custom_int_field.extensions
NoProfileSchema.introspection(NoProfileSchema::CustomIntrospection)
assert_equal [NoProfileSchema::OtherExampleExtension], custom_int_field.extensions.map(&:class)
end
end
end
describe "lazy-loading root types" do
class NoVisSchema < GraphQL::Schema
self.visibility = nil
@use_visibility_profile = false
end
class LazyLoadingSchema < NoVisSchema
class ExampleExtension < GraphQL::Schema::FieldExtension; end
class OtherExampleExtension < GraphQL::Schema::FieldExtension; end
class Query < GraphQL::Schema::Object
field :str, fallback_value: "Query field" do
type(String)
extension(ExampleExtension)
end
end
class Mutation < GraphQL::Schema::Object
field :str, fallback_value: "Mutation field" do
type(String)
extension(ExampleExtension)
end
end
class Subscription < GraphQL::Schema::Object
field :str do
type(String)
extension(ExampleExtension)
end
end
class OrphanType < GraphQL::Schema::Object
field :str do
type(String)
extension(ExampleExtension)
extension(OtherExampleExtension)
end
end
# This one is added before `Visibility`
subscription(Subscription)
use GraphQL::Schema::Visibility, preload: false
query { Query }
mutation { Mutation }
orphan_types(OrphanType)
end
it "loads types as-needed" do
assert_equal [LazyLoadingSchema::ExampleExtension], LazyLoadingSchema::Subscription.all_field_definitions.first.extensions.map(&:class)
assert_equal [], LazyLoadingSchema::Query.all_field_definitions.first.extensions.map(&:class)
assert_equal [], LazyLoadingSchema::Mutation.all_field_definitions.first.extensions.map(&:class)
assert_equal [], LazyLoadingSchema::OrphanType.all_field_definitions.first.extensions.map(&:class)
res = LazyLoadingSchema.execute("{ __typename }")
assert_equal "Query", res["data"]["__typename"]
assert_equal [], LazyLoadingSchema::Query.all_field_definitions.first.extensions.map(&:class)
assert_equal [LazyLoadingSchema::ExampleExtension], LazyLoadingSchema::Subscription.all_field_definitions.first.extensions.map(&:class)
assert_equal [], LazyLoadingSchema::Mutation.all_field_definitions.first.extensions.map(&:class)
assert_equal [], LazyLoadingSchema::OrphanType.all_field_definitions.first.extensions.map(&:class)
res = LazyLoadingSchema.execute("{ str }")
assert_equal "Query field", res["data"]["str"]
assert_equal [LazyLoadingSchema::ExampleExtension], LazyLoadingSchema::Query.all_field_definitions.first.extensions.map(&:class)
assert_equal [LazyLoadingSchema::ExampleExtension], LazyLoadingSchema::Subscription.all_field_definitions.first.extensions.map(&:class)
assert_equal [], LazyLoadingSchema::Mutation.all_field_definitions.first.extensions.map(&:class)
assert_equal [], LazyLoadingSchema::OrphanType.all_field_definitions.first.extensions.map(&:class)
res = LazyLoadingSchema.execute("mutation { str }")
assert_equal "Mutation field", res["data"]["str"]
assert_equal [LazyLoadingSchema::ExampleExtension], LazyLoadingSchema::Query.all_field_definitions.first.extensions.map(&:class)
assert_equal [LazyLoadingSchema::ExampleExtension], LazyLoadingSchema::Subscription.all_field_definitions.first.extensions.map(&:class)
assert_equal [LazyLoadingSchema::ExampleExtension], LazyLoadingSchema::Mutation.all_field_definitions.first.extensions.map(&:class)
assert_equal [], LazyLoadingSchema::OrphanType.all_field_definitions.first.extensions.map(&:class)
end
end
describe "interfaces thru superclass" do
class InterfaceSuperclassSchema < GraphQL::Schema
module Node
include GraphQL::Schema::Interface
field :id, ID
end
class NodeObject < GraphQL::Schema::Object
implements Node
end
class Thing < NodeObject
field :name, String
end
class Query < GraphQL::Schema::Object
field :node, Node
def node
{ id: "101", name: "Hat" }
end
field :thing, Thing
end
query(Query)
def self.resolve_type(...); Thing; end
use GraphQL::Schema::Visibility
end
end
it "Can use interface relationship properly" do
res = InterfaceSuperclassSchema.execute("{ node { id ... on Thing { name } } }")
assert_equal "Hat", res["data"]["node"]["name"]
end
it "defaults to preload: true for Rails.env.staging?" do
if defined?(Rails)
prev_rails = Rails
Object.send :remove_const, :Rails
end
mock_env = OpenStruct.new(:staging? => true)
Object.const_set(:Rails, OpenStruct.new(env: mock_env))
schema = Class.new(GraphQL::Schema) do
use GraphQL::Schema::Visibility
end
assert Rails.env.staging?
assert schema.visibility.preload?
mock_env[:staging?] = false
mock_env[:test?] = true
schema = Class.new(GraphQL::Schema) do
use GraphQL::Schema::Visibility
end
assert Rails.env.test?
refute Rails.env.staging?
refute schema.visibility.preload?
ensure
Object.send(:remove_const, :Rails)
if prev_rails
Object.const_set(:Rails, prev_rails)
end
end
end
|