123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306 |
- # Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
- require 'rails_helper'
- RSpec.describe Gql::Queries::Ticket::Articles, type: :graphql do
- context 'when fetching tickets' do
- let(:agent) { create(:agent) }
- let(:query) do
- <<~QUERY
- query ticketArticles(
- $ticketId: ID
- $ticketInternalId: Int
- $ticketNumber: String
- ) {
- ticketArticles(
- ticket: {
- ticketId: $ticketId
- ticketInternalId: $ticketInternalId
- ticketNumber: $ticketNumber
- }
- ) {
- totalCount
- edges {
- node {
- id
- internalId
- from {
- raw
- parsed {
- name
- emailAddress
- isSystemAddress
- }
- }
- to {
- raw
- parsed {
- name
- emailAddress
- isSystemAddress
- }
- }
- cc {
- raw
- parsed {
- name
- emailAddress
- isSystemAddress
- }
- }
- subject
- replyTo {
- raw
- parsed {
- name
- emailAddress
- isSystemAddress
- }
- }
- messageId
- messageIdMd5
- inReplyTo
- contentType
- references
- attachments {
- name
- }
- attachmentsWithoutInline {
- name
- }
- preferences
- securityState {
- type
- signingSuccess
- signingMessage
- encryptionSuccess
- encryptionMessage
- }
- body
- bodyWithUrls
- internal
- createdAt
- author {
- id
- fullname
- firstname
- lastname
- }
- createdBy {
- id
- firstname
- lastname
- fullname
- }
- type {
- name
- }
- sender {
- name
- }
- }
- cursor
- }
- pageInfo {
- endCursor
- hasNextPage
- }
- }
- }
- QUERY
- end
- let(:variables) { { ticketId: gql.id(ticket) } }
- let(:customer) { create(:customer) }
- let(:ticket) { create(:ticket, customer: customer) }
- let(:cc) { 'Zammad CI <ci@zammad.org>' }
- let(:to) { '@unparseable_address' }
- let(:cid) { "#{SecureRandom.uuid}@zammad.example.com" }
- let!(:articles) do
- 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|
- create(
- :store,
- object: 'Ticket::Article',
- o_id: article.id,
- data: 'fake',
- filename: 'inline_image.jpg',
- preferences: {
- 'Content-Type' => 'image/jpeg',
- 'Mime-Type' => 'image/jpeg',
- 'Content-ID' => "<#{cid}>",
- 'Content-Disposition' => 'inline',
- }
- )
- create(
- :store,
- object: 'Ticket::Article',
- o_id: article.id,
- data: 'fake',
- filename: 'attached_image.jpg',
- preferences: {
- 'Content-Type' => 'image/jpeg',
- 'Mime-Type' => 'image/jpeg',
- 'Content-ID' => "<#{cid}.not.referenced>",
- 'Content-Disposition' => 'inline',
- }
- )
- end
- end
- let!(:internal_article) { create(:ticket_article, :outbound_email, ticket: ticket, internal: true) }
- let(:response_articles) { gql.result.nodes }
- let(:response_total_count) { gql.result.data['totalCount'] }
- before do
- gql.execute(query, variables: variables)
- end
- context 'with an agent', authenticated_as: :agent do
- context 'with permission' do
- let(:agent) { create(:agent, groups: [ticket.group]) }
- let(:article1) { articles.first }
- let(:inline_url) { "/api/v1/ticket_attachment/#{article1['ticket_id']}/#{article1['id']}/#{article1.attachments.first[:id]}?view=inline" }
- let(:expected_article1) do
- {
- 'subject' => article1.subject,
- 'cc' => {
- 'parsed' => [
- {
- 'emailAddress' => 'ci@zammad.org',
- 'name' => 'Zammad CI',
- 'isSystemAddress' => false,
- },
- ],
- 'raw' => cc,
- },
- 'to' => {
- 'parsed' => nil,
- 'raw' => to,
- },
- 'references' => article1.references,
- 'type' => {
- 'name' => article1.type.name,
- },
- 'sender' => {
- 'name' => article1.sender.name,
- },
- 'securityState' => nil,
- 'body' => "<img src=\"cid:#{cid}\"> some text",
- 'bodyWithUrls' => "<img src=\"#{inline_url}\" style=\"max-width:100%;\"> some text",
- 'attachments' => [{ 'name'=>'inline_image.jpg' }, { 'name'=>'attached_image.jpg' }],
- 'attachmentsWithoutInline' => [{ 'name'=>'attached_image.jpg' }],
- }
- end
- it 'finds public and internal articles' do
- expect(response_total_count).to eq(articles.count + 1)
- end
- it 'finds article content' do
- expect(response_articles.first).to include(expected_article1)
- end
- context 'with ticketInternalId' do
- let(:variables) { { ticketInternalId: ticket.id } }
- it 'finds articles' do
- expect(response_total_count).to eq(articles.count + 1)
- end
- end
- context 'with ticketNumber' do
- let(:variables) { { ticketNumber: ticket.number } }
- it 'finds articles' do
- expect(response_total_count).to eq(articles.count + 1)
- end
- end
- context 'with securityState information' do
- let(:articles) do
- create_list(
- :ticket_article, 1, :outbound_email, ticket: ticket, to: to, cc: cc,
- preferences: {
- 'security' => { 'type' => 'S/MIME', 'sign' => { 'success' => false, 'comment' => 'Message is not signed by sender.' }, 'encryption' => { 'success' => false, 'comment' => nil } }
- }
- )
- end
- let(:expected_security_state) do
- {
- 'type' => 'SMIME',
- 'signingSuccess' => false,
- 'signingMessage' => 'Message is not signed by sender.',
- 'encryptionSuccess' => false,
- 'encryptionMessage' => nil,
- }
- end
- it 'includes securityStatus information' do
- expect(response_articles.first).to include({ 'securityState' => expected_security_state })
- end
- end
- context 'when has originBy' do
- let(:articles) { create_list(:ticket_article, 1, :inbound_phone, ticket: ticket, origin_by: agent, created_by: create(:agent, groups: [ticket.group])) }
- it 'loads originBy' do
- expect(response_articles.first)
- .to include(
- 'author' => include('fullname' => agent.fullname),
- 'createdBy' => be_present
- )
- end
- end
- end
- context 'without permission' do
- it 'raises authorization error' do
- expect(gql.result.error_type).to eq(Exceptions::Forbidden)
- end
- end
- context 'without ticket' do
- let(:ticket) { create(:ticket).tap(&:destroy) }
- let(:articles) { [] }
- let(:internal_article) { [] }
- it 'fetches no ticket' do
- expect(gql.result.error_type).to eq(ActiveRecord::RecordNotFound)
- end
- end
- end
- context 'with a customer', authenticated_as: :customer do
- let(:variables) { { ticketId: gql.id(ticket) } }
- it 'finds only public articles' do
- expect(response_total_count).to eq(articles.count)
- end
- it 'does not find internal articles' do
- expect(response_articles.pluck(:id)).to not_include(internal_article.id)
- end
- context 'when has originBy' do
- let(:origin_by) { create(:agent) }
- let(:articles) do
- create_list(:ticket_article, 1, :inbound_phone, ticket: ticket, origin_by: origin_by, created_by: create(:agent, groups: [ticket.group]))
- end
- it 'loads originBy' do
- expect(response_articles.first)
- .to include(
- 'author' => include(
- 'fullname' => nil, # fullname is filtered out for customers
- 'firstname' => origin_by.firstname
- ),
- 'createdBy' => be_present
- )
- end
- end
- end
- it_behaves_like 'graphql responds with error if unauthenticated'
- end
- end
|