graphql.rb 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247
  1. # Copyright (C) 2012-2023 Zammad Foundation, https://zammad-foundation.org/
  2. require 'graphql/gql/shared_examples/fails_if_unauthenticated'
  3. module ZammadSpecSupportGraphql
  4. #
  5. # A stub implementation of ActionCable.
  6. # Any methods to support the mock backend have `mock` in the name.
  7. # Taken from github.com/rmosolgo/graphql-ruby/blob/master/spec/graphql/subscriptions/action_cable_subscriptions_spec.rb
  8. #
  9. class MockActionCable
  10. class MockChannel
  11. def initialize
  12. @mock_broadcasted_messages = []
  13. end
  14. attr_reader :mock_broadcasted_messages
  15. def stream_from(stream_name, coder: nil, &block)
  16. # Rails uses `coder`, we don't
  17. block ||= ->(msg) { @mock_broadcasted_messages << msg }
  18. MockActionCable.mock_stream_for(stream_name).add_mock_channel(self, block)
  19. end
  20. def mock_broadcasted_at(index)
  21. data = mock_broadcasted_messages.dig(index, :result)
  22. return if !data
  23. GraphQLHelpers::Result.new(data)
  24. end
  25. def mock_broadcasted_first
  26. mock_broadcasted_at(0)
  27. end
  28. end
  29. class MockStream
  30. def initialize
  31. @mock_channels = {}
  32. end
  33. def add_mock_channel(channel, handler)
  34. @mock_channels[channel] = handler
  35. end
  36. def mock_broadcast(message)
  37. @mock_channels.each do |_channel, handler|
  38. handler&.call(message)
  39. end
  40. end
  41. end
  42. class << self
  43. def clear_mocks
  44. @mock_streams = {}
  45. end
  46. def server
  47. self
  48. end
  49. def broadcast(stream_name, message)
  50. stream = @mock_streams[stream_name]
  51. stream&.mock_broadcast(message)
  52. end
  53. def mock_stream_for(stream_name)
  54. @mock_streams[stream_name] ||= MockStream.new
  55. end
  56. def build_mock_channel
  57. MockChannel.new
  58. end
  59. def mock_stream_names
  60. @mock_streams.keys
  61. end
  62. end
  63. end
  64. #
  65. # Create a mock channel that can be passed to graphql_execute like this:
  66. #
  67. # let(:mock_channel) { build_mock_channel }
  68. #
  69. # gql.execute(query, context: { channel: mock_channel })
  70. #
  71. delegate :build_mock_channel, to: MockActionCable
  72. #
  73. # A set of GraphQL helpers. Access them in a :graphql test via `gql.*`.
  74. #
  75. class GraphQLHelpers
  76. #
  77. # Encapsulates the GraphQL result.
  78. #
  79. # gql.result.*
  80. #
  81. class Result
  82. attr_reader :payload
  83. def initialize(payload)
  84. @payload = payload
  85. end
  86. #
  87. # Access the data payload. This asserts that only one operation was executed
  88. # and that no errors are present.
  89. #
  90. # expect(gql.response.data).to include(...)
  91. #
  92. def data
  93. assert('GraphQL result does not contain errors') do
  94. @payload['errors'].nil?
  95. end
  96. assert('GraphQL result contains exactly one data entry') do
  97. @payload['data']&.count == 1
  98. end
  99. @payload['data'].values.first
  100. end
  101. #
  102. # Access the edges->node data payload from `#data()` in a convenient way.
  103. #
  104. # expect(gql.response.nodes.first).to include(...)
  105. #
  106. # Also can operate on a subentry in the hash rather than the top level.
  107. #
  108. # expect(gql.response.nodes('first_level', 'second_level').first).to include(...)
  109. #
  110. def nodes(*subkeys)
  111. content = data.dig(*subkeys, 'edges')
  112. assert('GraphQL result contains node entries') do
  113. !content.nil?
  114. end
  115. content.pluck('node')
  116. end
  117. #
  118. # Access an error entry. This asserts that only one error and no data payload is present.
  119. #
  120. # expect(gql.result.error).to include(...)
  121. #
  122. def error
  123. assert('GraphQL result does not contain data') do
  124. @payload['data'].nil? || @payload['data'].values.first.nil?
  125. end
  126. assert('GraphQL result contains exactly one error entry') do
  127. @payload['errors']&.count == 1
  128. end
  129. @payload['errors'][0]
  130. end
  131. #
  132. # Access the error type from `#error()` and return it as a Ruby class.
  133. #
  134. # expect(gql.result.error_type).to eq(ActiveRecord::RecordNotFound)
  135. #
  136. def error_type
  137. assert('GraphQL result has error type') do
  138. error.dig('extensions', 'type').present?
  139. end
  140. error.dig('extensions', 'type').constantize
  141. end
  142. #
  143. # Access the error message from `#error()`.
  144. #
  145. # expect(gql.result.error_message).to eq('Something went wrong in this test...')
  146. #
  147. def error_message
  148. error['message']
  149. end
  150. private
  151. def assert(message)
  152. raise "Assertion '#{message}' failed, graphql result:\n#{PP.pp(payload, '')}" if !yield
  153. end
  154. end
  155. attr_writer :graphql_current_user
  156. attr_accessor :result
  157. # Shortcut to generate a GraphQL ID for an object.
  158. def id(object)
  159. Gql::ZammadSchema.id_from_object(object)
  160. end
  161. #
  162. # Run a graphql query.
  163. #
  164. # before do
  165. # gql.execute(query, variables: { ... })
  166. # end
  167. #
  168. # Afterwards, the `Result` can be accessed via
  169. #
  170. # gql.result
  171. #
  172. def execute(query, variables: {}, context: {})
  173. context[:current_user] ||= @graphql_current_user
  174. if @graphql_current_user
  175. # TODO: we only fake a SID for now, create a real session?
  176. context[:sid] = SecureRandom.hex(16)
  177. # we need to set the current_user_id in the UserInfo context as well
  178. UserInfo.current_user_id = context[:current_user].id
  179. end
  180. @result = Result.new(Gql::ZammadSchema.execute(query, variables: variables, context: context).to_h)
  181. end
  182. end
  183. def gql
  184. @gql ||= GraphQLHelpers.new
  185. end
  186. end
  187. RSpec.configure do |config|
  188. config.include ZammadSpecSupportGraphql, type: :graphql
  189. config.prepend_before(:each, type: :graphql) do
  190. ZammadSpecSupportGraphql::MockActionCable.clear_mocks
  191. Gql::ZammadSchema.subscriptions = GraphQL::Subscriptions::ActionCableSubscriptions.new(
  192. action_cable: ZammadSpecSupportGraphql::MockActionCable, action_cable_coder: JSON, schema: Gql::ZammadSchema
  193. )
  194. end
  195. config.append_after(:each, type: :graphql) do
  196. Gql::ZammadSchema.subscriptions = GraphQL::Subscriptions::ActionCableSubscriptions.new(schema: Gql::ZammadSchema)
  197. end
  198. # This helper allows you to authenticate as a given user in :graphql specs
  199. # via the example metadata, rather than directly:
  200. #
  201. # it 'does something', authenticated_as: :user
  202. #
  203. # In order for this to work, you must define the user in a `let` block first:
  204. #
  205. # let(:user) { create(:customer) }
  206. #
  207. config.before(:each, :authenticated_as, type: :graphql) do |example|
  208. gql.graphql_current_user = authenticated_as_get_user example.metadata[:authenticated_as], return_type: :user
  209. end
  210. end