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