articles_spec.rb 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304
  1. # Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
  2. require 'rails_helper'
  3. RSpec.describe Gql::Queries::Ticket::Articles, type: :graphql do
  4. context 'when fetching tickets' do
  5. let(:agent) { create(:agent) }
  6. let(:query) do
  7. <<~QUERY
  8. query ticketArticles(
  9. $ticketId: ID
  10. $ticketInternalId: Int
  11. $ticketNumber: String
  12. ) {
  13. ticketArticles(
  14. ticket: {
  15. ticketId: $ticketId
  16. ticketInternalId: $ticketInternalId
  17. ticketNumber: $ticketNumber
  18. }
  19. ) {
  20. totalCount
  21. edges {
  22. node {
  23. id
  24. internalId
  25. from {
  26. raw
  27. parsed {
  28. name
  29. emailAddress
  30. isSystemAddress
  31. }
  32. }
  33. to {
  34. raw
  35. parsed {
  36. name
  37. emailAddress
  38. isSystemAddress
  39. }
  40. }
  41. cc {
  42. raw
  43. parsed {
  44. name
  45. emailAddress
  46. isSystemAddress
  47. }
  48. }
  49. subject
  50. replyTo {
  51. raw
  52. parsed {
  53. name
  54. emailAddress
  55. isSystemAddress
  56. }
  57. }
  58. messageId
  59. messageIdMd5
  60. inReplyTo
  61. contentType
  62. attachments {
  63. name
  64. }
  65. attachmentsWithoutInline {
  66. name
  67. }
  68. preferences
  69. securityState {
  70. type
  71. signingSuccess
  72. signingMessage
  73. encryptionSuccess
  74. encryptionMessage
  75. }
  76. body
  77. bodyWithUrls
  78. internal
  79. createdAt
  80. author {
  81. id
  82. fullname
  83. firstname
  84. lastname
  85. }
  86. createdBy {
  87. id
  88. firstname
  89. lastname
  90. fullname
  91. }
  92. type {
  93. name
  94. }
  95. sender {
  96. name
  97. }
  98. }
  99. cursor
  100. }
  101. pageInfo {
  102. endCursor
  103. hasNextPage
  104. }
  105. }
  106. }
  107. QUERY
  108. end
  109. let(:variables) { { ticketId: gql.id(ticket) } }
  110. let(:customer) { create(:customer) }
  111. let(:ticket) { create(:ticket, customer: customer) }
  112. let(:cc) { 'Zammad CI <ci@zammad.org>' }
  113. let(:to) { '@unparseable_address' }
  114. let(:cid) { "#{SecureRandom.uuid}@zammad.example.com" }
  115. let!(:articles) do
  116. create_list(:ticket_article, 2, :outbound_email, ticket: ticket, to: to, cc: cc, content_type: 'text/html', body: "<img src=\"cid:#{cid}\"> some text") do |article, _i|
  117. create(
  118. :store,
  119. object: 'Ticket::Article',
  120. o_id: article.id,
  121. data: 'fake',
  122. filename: 'inline_image.jpg',
  123. preferences: {
  124. 'Content-Type' => 'image/jpeg',
  125. 'Mime-Type' => 'image/jpeg',
  126. 'Content-ID' => "<#{cid}>",
  127. 'Content-Disposition' => 'inline',
  128. }
  129. )
  130. create(
  131. :store,
  132. object: 'Ticket::Article',
  133. o_id: article.id,
  134. data: 'fake',
  135. filename: 'attached_image.jpg',
  136. preferences: {
  137. 'Content-Type' => 'image/jpeg',
  138. 'Mime-Type' => 'image/jpeg',
  139. 'Content-ID' => "<#{cid}.not.referenced>",
  140. 'Content-Disposition' => 'inline',
  141. }
  142. )
  143. end
  144. end
  145. let!(:internal_article) { create(:ticket_article, :outbound_email, ticket: ticket, internal: true) }
  146. let(:response_articles) { gql.result.nodes }
  147. let(:response_total_count) { gql.result.data['totalCount'] }
  148. before do
  149. gql.execute(query, variables: variables)
  150. end
  151. context 'with an agent', authenticated_as: :agent do
  152. context 'with permission' do
  153. let(:agent) { create(:agent, groups: [ticket.group]) }
  154. let(:article1) { articles.first }
  155. let(:inline_url) { "/api/v1/ticket_attachment/#{article1['ticket_id']}/#{article1['id']}/#{article1.attachments.first[:id]}?view=inline" }
  156. let(:expected_article1) do
  157. {
  158. 'subject' => article1.subject,
  159. 'cc' => {
  160. 'parsed' => [
  161. {
  162. 'emailAddress' => 'ci@zammad.org',
  163. 'name' => 'Zammad CI',
  164. 'isSystemAddress' => false,
  165. },
  166. ],
  167. 'raw' => cc,
  168. },
  169. 'to' => {
  170. 'parsed' => nil,
  171. 'raw' => to,
  172. },
  173. 'type' => {
  174. 'name' => article1.type.name,
  175. },
  176. 'sender' => {
  177. 'name' => article1.sender.name,
  178. },
  179. 'securityState' => nil,
  180. 'body' => "<img src=\"cid:#{cid}\"> some text",
  181. 'bodyWithUrls' => "<img src=\"#{inline_url}\" style=\"max-width:100%;\"> some text",
  182. 'attachments' => [{ 'name'=>'inline_image.jpg' }, { 'name'=>'attached_image.jpg' }],
  183. 'attachmentsWithoutInline' => [{ 'name'=>'attached_image.jpg' }],
  184. }
  185. end
  186. it 'finds public and internal articles' do
  187. expect(response_total_count).to eq(articles.count + 1)
  188. end
  189. it 'finds article content' do
  190. expect(response_articles.first).to include(expected_article1)
  191. end
  192. context 'with ticketInternalId' do
  193. let(:variables) { { ticketInternalId: ticket.id } }
  194. it 'finds articles' do
  195. expect(response_total_count).to eq(articles.count + 1)
  196. end
  197. end
  198. context 'with ticketNumber' do
  199. let(:variables) { { ticketNumber: ticket.number } }
  200. it 'finds articles' do
  201. expect(response_total_count).to eq(articles.count + 1)
  202. end
  203. end
  204. context 'with securityState information' do
  205. let(:articles) do
  206. create_list(
  207. :ticket_article, 1, :outbound_email, ticket: ticket, to: to, cc: cc,
  208. preferences: {
  209. 'security' => { 'type' => 'S/MIME', 'sign' => { 'success' => false, 'comment' => 'Message is not signed by sender.' }, 'encryption' => { 'success' => false, 'comment' => nil } }
  210. }
  211. )
  212. end
  213. let(:expected_security_state) do
  214. {
  215. 'type' => 'SMIME',
  216. 'signingSuccess' => false,
  217. 'signingMessage' => 'Message is not signed by sender.',
  218. 'encryptionSuccess' => false,
  219. 'encryptionMessage' => nil,
  220. }
  221. end
  222. it 'includes securityStatus information' do
  223. expect(response_articles.first).to include({ 'securityState' => expected_security_state })
  224. end
  225. end
  226. context 'when has originBy' do
  227. let(:articles) { create_list(:ticket_article, 1, :inbound_phone, ticket: ticket, origin_by: agent, created_by: create(:agent, groups: [ticket.group])) }
  228. it 'loads originBy' do
  229. expect(response_articles.first)
  230. .to include(
  231. 'author' => include('fullname' => agent.fullname),
  232. 'createdBy' => be_present
  233. )
  234. end
  235. end
  236. end
  237. context 'without permission' do
  238. it 'raises authorization error' do
  239. expect(gql.result.error_type).to eq(Exceptions::Forbidden)
  240. end
  241. end
  242. context 'without ticket' do
  243. let(:ticket) { create(:ticket).tap(&:destroy) }
  244. let(:articles) { [] }
  245. let(:internal_article) { [] }
  246. it 'fetches no ticket' do
  247. expect(gql.result.error_type).to eq(ActiveRecord::RecordNotFound)
  248. end
  249. end
  250. end
  251. context 'with a customer', authenticated_as: :customer do
  252. let(:variables) { { ticketId: gql.id(ticket) } }
  253. it 'finds only public articles' do
  254. expect(response_total_count).to eq(articles.count)
  255. end
  256. it 'does not find internal articles' do
  257. expect(response_articles.pluck(:id)).to not_include(internal_article.id)
  258. end
  259. context 'when has originBy' do
  260. let(:origin_by) { create(:agent) }
  261. let(:articles) do
  262. create_list(:ticket_article, 1, :inbound_phone, ticket: ticket, origin_by: origin_by, created_by: create(:agent, groups: [ticket.group]))
  263. end
  264. it 'loads originBy' do
  265. expect(response_articles.first)
  266. .to include(
  267. 'author' => include(
  268. 'fullname' => nil, # fullname is filtered out for customers
  269. 'firstname' => origin_by.firstname
  270. ),
  271. 'createdBy' => be_present
  272. )
  273. end
  274. end
  275. end
  276. it_behaves_like 'graphql responds with error if unauthenticated'
  277. end
  278. end