articles_spec.rb 9.7 KB

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