1# frozen_string_literal: true 2require 'spec_helper' 3 4RSpec.describe Gitlab::Graphql::Present::FieldExtension do 5 include GraphqlHelpers 6 7 let_it_be(:user) { create(:user) } 8 9 let(:object) { double(value: 'foo') } 10 let(:owner) { fresh_object_type } 11 let(:field_name) { 'value' } 12 let(:field) do 13 ::Types::BaseField.new(name: field_name, type: GraphQL::Types::String, null: true, owner: owner) 14 end 15 16 let(:base_presenter) do 17 Class.new(SimpleDelegator) do 18 def initialize(object, **options) 19 super(object) 20 @object = object 21 @options = options 22 end 23 end 24 end 25 26 def resolve_value 27 resolve_field(field, object, current_user: user, object_type: owner) 28 end 29 30 context 'when the object does not declare a presenter' do 31 it 'does not affect normal resolution' do 32 expect(resolve_value).to eq 'foo' 33 end 34 end 35 36 context 'when the field is declared on an interface, and implemented by a presenter' do 37 let(:interface) do 38 Module.new do 39 include ::Types::BaseInterface 40 41 field :interface_field, GraphQL::Types::String, null: true 42 end 43 end 44 45 let(:implementation) do 46 type = fresh_object_type('Concrete') 47 type.present_using(concrete_impl) 48 type.implements(interface) 49 type 50 end 51 52 def concrete_impl 53 Class.new(base_presenter) do 54 def interface_field 55 'made of concrete' 56 end 57 end 58 end 59 60 it 'resolves the interface field using the implementation from the presenter' do 61 field = ::Types::BaseField.new(name: :interface_field, type: GraphQL::Types::String, null: true, owner: interface) 62 value = resolve_field(field, object, object_type: implementation) 63 64 expect(value).to eq 'made of concrete' 65 end 66 67 context 'when the implementation is inherited' do 68 it 'resolves the interface field using the implementation from the presenter' do 69 subclass = Class.new(implementation) { graphql_name 'Subclass' } 70 field = ::Types::BaseField.new(name: :interface_field, type: GraphQL::Types::String, null: true, owner: interface) 71 value = resolve_field(field, object, object_type: subclass) 72 73 expect(value).to eq 'made of concrete' 74 end 75 end 76 end 77 78 describe 'interactions with inheritance' do 79 def parent 80 type = fresh_object_type('Parent') 81 type.present_using(provide_foo) 82 type.field :foo, ::GraphQL::Types::Int, null: true 83 type.field :value, ::GraphQL::Types::String, null: true 84 type 85 end 86 87 def child 88 type = Class.new(parent) 89 type.graphql_name 'Child' 90 type.present_using(provide_bar) 91 type.field :bar, ::GraphQL::Types::Int, null: true 92 type 93 end 94 95 def provide_foo 96 Class.new(base_presenter) do 97 def foo 98 100 99 end 100 end 101 end 102 103 def provide_bar 104 Class.new(base_presenter) do 105 def bar 106 101 107 end 108 end 109 end 110 111 it 'can resolve value, foo and bar' do 112 type = child 113 value = resolve_field(:value, object, object_type: type) 114 foo = resolve_field(:foo, object, object_type: type) 115 bar = resolve_field(:bar, object, object_type: type) 116 117 expect([value, foo, bar]).to eq ['foo', 100, 101] 118 end 119 end 120 121 shared_examples 'calling the presenter method' do 122 it 'calls the presenter method' do 123 expect(resolve_value).to eq presenter.new(object, current_user: user).send(field_name) 124 end 125 end 126 127 context 'when the object declares a presenter' do 128 before do 129 owner.present_using(presenter) 130 end 131 132 context 'when the presenter overrides the original method' do 133 def twice 134 Class.new(base_presenter) do 135 def value 136 @object.value * 2 137 end 138 end 139 end 140 141 let(:presenter) { twice } 142 143 it_behaves_like 'calling the presenter method' 144 end 145 146 # This is exercised here using an explicit `resolve:` proc, but 147 # @resolver_proc values are used in field instrumentation as well. 148 context 'when the field uses a resolve proc' do 149 let(:presenter) { base_presenter } 150 let(:field) do 151 ::Types::BaseField.new( 152 name: field_name, 153 type: GraphQL::Types::String, 154 null: true, 155 owner: owner, 156 resolve: ->(obj, args, ctx) { 'Hello from a proc' } 157 ) 158 end 159 160 specify { expect(resolve_value).to eq 'Hello from a proc' } 161 end 162 163 context 'when the presenter provides a new method' do 164 def presenter 165 Class.new(base_presenter) do 166 def current_username 167 "Hello #{@options[:current_user]&.username} from the presenter!" 168 end 169 end 170 end 171 172 context 'when we select the original field' do 173 it 'is unaffected' do 174 expect(resolve_value).to eq 'foo' 175 end 176 end 177 178 context 'when we select the new field' do 179 let(:field_name) { 'current_username' } 180 181 it_behaves_like 'calling the presenter method' 182 end 183 end 184 end 185end 186