articles_spec.rb 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310
  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) { Faker::Internet.unique.email }
  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' => [
  171. {
  172. 'emailAddress' => to,
  173. 'name' => nil,
  174. 'isSystemAddress' => false,
  175. }
  176. ],
  177. 'raw' => to,
  178. },
  179. 'type' => {
  180. 'name' => article1.type.name,
  181. },
  182. 'sender' => {
  183. 'name' => article1.sender.name,
  184. },
  185. 'securityState' => nil,
  186. 'body' => "<img src=\"cid:#{cid}\"> some text",
  187. 'bodyWithUrls' => "<img src=\"#{inline_url}\" style=\"max-width:100%;\"> some text",
  188. 'attachments' => [{ 'name'=>'inline_image.jpg' }, { 'name'=>'attached_image.jpg' }],
  189. 'attachmentsWithoutInline' => [{ 'name'=>'attached_image.jpg' }],
  190. }
  191. end
  192. it 'finds public and internal articles' do
  193. expect(response_total_count).to eq(articles.count + 1)
  194. end
  195. it 'finds article content' do
  196. expect(response_articles.first).to include(expected_article1)
  197. end
  198. context 'with ticketInternalId' do
  199. let(:variables) { { ticketInternalId: ticket.id } }
  200. it 'finds articles' do
  201. expect(response_total_count).to eq(articles.count + 1)
  202. end
  203. end
  204. context 'with ticketNumber' do
  205. let(:variables) { { ticketNumber: ticket.number } }
  206. it 'finds articles' do
  207. expect(response_total_count).to eq(articles.count + 1)
  208. end
  209. end
  210. context 'with securityState information' do
  211. let(:articles) do
  212. create_list(
  213. :ticket_article, 1, :outbound_email, ticket: ticket, to: to, cc: cc,
  214. preferences: {
  215. 'security' => { 'type' => 'S/MIME', 'sign' => { 'success' => false, 'comment' => 'Message is not signed by sender.' }, 'encryption' => { 'success' => false, 'comment' => nil } }
  216. }
  217. )
  218. end
  219. let(:expected_security_state) do
  220. {
  221. 'type' => 'SMIME',
  222. 'signingSuccess' => false,
  223. 'signingMessage' => 'Message is not signed by sender.',
  224. 'encryptionSuccess' => false,
  225. 'encryptionMessage' => nil,
  226. }
  227. end
  228. it 'includes securityStatus information' do
  229. expect(response_articles.first).to include({ 'securityState' => expected_security_state })
  230. end
  231. end
  232. context 'when has originBy' do
  233. let(:articles) { create_list(:ticket_article, 1, :inbound_phone, ticket: ticket, origin_by: agent, created_by: create(:agent, groups: [ticket.group])) }
  234. it 'loads originBy' do
  235. expect(response_articles.first)
  236. .to include(
  237. 'author' => include('fullname' => agent.fullname),
  238. 'createdBy' => be_present
  239. )
  240. end
  241. end
  242. end
  243. context 'without permission' do
  244. it 'raises authorization error' do
  245. expect(gql.result.error_type).to eq(Exceptions::Forbidden)
  246. end
  247. end
  248. context 'without ticket' do
  249. let(:ticket) { create(:ticket).tap(&:destroy) }
  250. let(:articles) { [] }
  251. let(:internal_article) { [] }
  252. it 'fetches no ticket' do
  253. expect(gql.result.error_type).to eq(ActiveRecord::RecordNotFound)
  254. end
  255. end
  256. end
  257. context 'with a customer', authenticated_as: :customer do
  258. let(:variables) { { ticketId: gql.id(ticket) } }
  259. it 'finds only public articles' do
  260. expect(response_total_count).to eq(articles.count)
  261. end
  262. it 'does not find internal articles' do
  263. expect(response_articles.pluck(:id)).to not_include(internal_article.id)
  264. end
  265. context 'when has originBy' do
  266. let(:origin_by) { create(:agent) }
  267. let(:articles) do
  268. create_list(:ticket_article, 1, :inbound_phone, ticket: ticket, origin_by: origin_by, created_by: create(:agent, groups: [ticket.group]))
  269. end
  270. it 'loads originBy' do
  271. expect(response_articles.first)
  272. .to include(
  273. 'author' => include(
  274. 'fullname' => nil, # fullname is filtered out for customers
  275. 'firstname' => origin_by.firstname
  276. ),
  277. 'createdBy' => be_present
  278. )
  279. end
  280. end
  281. end
  282. it_behaves_like 'graphql responds with error if unauthenticated'
  283. end
  284. end