create_spec.rb 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704
  1. # Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
  2. require 'rails_helper'
  3. RSpec.describe Gql::Mutations::Ticket::Create, :aggregate_failures, type: :graphql do
  4. let(:query) do
  5. <<~QUERY
  6. mutation ticketCreate($input: TicketCreateInput!) {
  7. ticketCreate(input: $input) {
  8. ticket {
  9. id
  10. title
  11. group {
  12. name
  13. }
  14. priority {
  15. name
  16. }
  17. customer {
  18. fullname
  19. }
  20. owner {
  21. fullname
  22. }
  23. objectAttributeValues {
  24. attribute {
  25. name
  26. }
  27. value
  28. }
  29. tags
  30. }
  31. errors {
  32. message
  33. field
  34. }
  35. }
  36. }
  37. QUERY
  38. end
  39. let(:agent) { create(:agent, groups: [ Group.find_by(name: 'Users')]) }
  40. let(:customer) { create(:customer) }
  41. let(:user) { agent }
  42. let(:group) { agent.groups.first }
  43. let(:priority) { Ticket::Priority.last }
  44. let(:article_payload) { nil }
  45. let(:input_base_payload) do
  46. {
  47. title: 'Ticket Create Mutation Test',
  48. groupId: gql.id(group),
  49. priorityId: gql.id(priority),
  50. customer: { id: gql.id(customer) },
  51. ownerId: gql.id(agent),
  52. tags: %w[foo bar],
  53. article: article_payload
  54. # pending_time: 10.minutes.from_now,
  55. # type: ...
  56. }
  57. end
  58. let(:input_payload) { input_base_payload }
  59. let(:variables) { { input: input_payload } }
  60. let(:expected_base_response) do
  61. {
  62. 'id' => gql.id(Ticket.last),
  63. 'title' => 'Ticket Create Mutation Test',
  64. 'owner' => { 'fullname' => agent.fullname },
  65. 'group' => { 'name' => agent.groups.first.name },
  66. 'customer' => { 'fullname' => customer.fullname },
  67. 'priority' => { 'name' => Ticket::Priority.last.name },
  68. 'tags' => %w[foo bar],
  69. 'objectAttributeValues' => [],
  70. }
  71. end
  72. let(:expected_response) do
  73. expected_base_response
  74. end
  75. def it_creates_ticket(articles: 0, stores: 0)
  76. expect { gql.execute(query, variables: variables) }
  77. .to change(Ticket, :count).by(1)
  78. .and change(Ticket::Article, :count).by(articles)
  79. .and change(Store, :count).by(stores)
  80. end
  81. def it_fails_to_create_ticket
  82. expect { gql.execute(query, variables: variables) }
  83. .not_to change(Ticket, :count)
  84. end
  85. context 'when creating a new ticket' do
  86. context 'with an agent', authenticated_as: :agent do
  87. it 'creates Ticket record' do
  88. it_creates_ticket
  89. expect(gql.result.data[:ticket]).to eq(expected_response)
  90. end
  91. context 'without title' do
  92. let(:input_payload) { input_base_payload.tap { |h| h[:title] = ' ' } }
  93. it 'fails validation' do
  94. it_fails_to_create_ticket
  95. expect(gql.result.error_message).to include('Variable $input of type TicketCreateInput! was provided invalid value for title')
  96. end
  97. end
  98. context 'with custom object_attribute', db_strategy: :reset do
  99. let(:object_attribute) do
  100. screens = { create: { 'admin.organization': { shown: true, required: false } } }
  101. create(:object_manager_attribute_text, object_name: 'Ticket', screens: screens).tap do |_oa|
  102. ObjectManager::Attribute.migration_execute
  103. end
  104. end
  105. let(:input_payload) do
  106. input_base_payload.merge(
  107. {
  108. objectAttributeValues: [ { name: object_attribute.name, value: 'object_attribute_value' } ]
  109. }
  110. )
  111. end
  112. let(:expected_response) do
  113. expected_base_response.merge(
  114. {
  115. 'objectAttributeValues' => [{ 'attribute' => { 'name'=>object_attribute.name }, 'value' => 'object_attribute_value' }]
  116. }
  117. )
  118. end
  119. it 'creates the ticket' do
  120. it_creates_ticket
  121. expect(gql.result.data[:ticket]).to eq(expected_response)
  122. end
  123. end
  124. context 'with links' do
  125. let!(:other_ticket) { create(:ticket, group: agent.groups.first) }
  126. let(:links) do
  127. [
  128. { linkObjectId: gql.id(other_ticket), linkType: 'child' },
  129. { linkObjectId: gql.id(other_ticket), linkType: 'normal' },
  130. ]
  131. end
  132. let(:input_payload) { input_base_payload.merge(links:) }
  133. it 'creates the ticket and adds links' do
  134. it_creates_ticket
  135. expect(Link.list(link_object: 'Ticket', link_object_value: Ticket.last.id)).to contain_exactly(
  136. { 'link_object' => 'Ticket', 'link_object_value' => other_ticket.id, 'link_type' => 'parent' },
  137. { 'link_object' => 'Ticket', 'link_object_value' => other_ticket.id, 'link_type' => 'normal' },
  138. )
  139. end
  140. end
  141. context 'with issue tracker links' do
  142. let(:github_link) { 'https://github.com/issue/123' }
  143. let(:input_payload) { input_base_payload.merge(externalReferences: { github: [github_link] }) }
  144. before { Setting.set('github_integration', true) }
  145. it 'creates the ticket and adds issue trackeer links' do
  146. it_creates_ticket
  147. expect(Ticket.last.preferences)
  148. .to include('github' => include('issue_links' => contain_exactly(github_link)))
  149. end
  150. end
  151. context 'when customer is provided as an email address' do
  152. let(:email_address) { Faker::Internet.email }
  153. let(:input_payload) { input_base_payload.merge(customer: { email: email_address }) }
  154. context 'with valid email address' do
  155. it 'creates the ticket and a new customer' do
  156. it_creates_ticket
  157. expect(User.find_by(email: email_address)).to be_present
  158. expect(gql.result.data[:ticket][:customer][:fullname]).to eq(User.find_by(email: email_address).fullname)
  159. end
  160. end
  161. context 'with invalid email address' do
  162. let(:email_address) { 'invalid-email' }
  163. it 'fails to create the ticket' do
  164. it_fails_to_create_ticket
  165. expect(gql.result.error_message).to include('The email address is invalid.')
  166. end
  167. end
  168. context 'with valid email address of an existing customer' do
  169. let(:email_address) { customer.email }
  170. it 'creates the ticket' do
  171. it_creates_ticket
  172. expect(gql.result.data[:ticket][:customer][:fullname]).to eq(customer.fullname)
  173. end
  174. end
  175. end
  176. context 'when creating the ticket in a group with only :create permission' do
  177. let(:group) { create(:group) }
  178. let(:owner) { create(:agent, groups: [group]) }
  179. let(:input_payload) { input_base_payload.merge(ownerId: gql.id(owner)) }
  180. before do
  181. user.groups << group
  182. user.group_names_access_map = { user.groups.first.name => ['full'], group.name => ['create'] }
  183. end
  184. it 'creates the ticket in the correct group, but returns an error trying to access the new ticket' do
  185. expect { gql.execute(query, variables: variables) }.to change(Ticket, :count).by(1)
  186. expect(Ticket.last.group.id).to eq(group.id)
  187. expect(gql.result.payload['data']['ticketCreate']).to eq({ 'ticket' => nil, 'errors' => nil }) # Mutation did run, but data retrieval was not authorized.
  188. expect(gql.result.payload['errors'].first['message']).to eq('Access forbidden by Gql::Types::TicketType')
  189. expect(gql.result.payload['errors'].first['extensions']['type']).to eq('Exceptions::Forbidden')
  190. end
  191. end
  192. context 'when creating the ticket in a group without email address' do
  193. let(:group) { create(:group, email_address: nil) }
  194. let(:agent) { create(:agent, groups: [group]) }
  195. let(:article_payload) { { body: 'dummy', type: 'email' } }
  196. let(:input_payload) { input_base_payload.merge(groupId: gql.id(group)) }
  197. it 'fails to create the ticket' do
  198. it_fails_to_create_ticket
  199. expect(gql.result.payload['data']['ticketCreate']).to eq(
  200. {
  201. 'ticket' => nil,
  202. 'errors' => [
  203. {
  204. 'message' => 'This group has no email address configured for outgoing communication.',
  205. 'field' => 'group_id'
  206. }
  207. ]
  208. }
  209. )
  210. end
  211. end
  212. context 'with no permission to the group' do
  213. let(:group) { create(:group) }
  214. it 'raises an error', :aggregate_failures do
  215. it_fails_to_create_ticket
  216. expect(gql.result.error_type).to eq(Exceptions::Forbidden)
  217. expect(gql.result.error_message).to eq('Access forbidden by Gql::Types::GroupType')
  218. end
  219. end
  220. context 'with article' do
  221. before do
  222. Group.find(agent.groups.first.id).update(email_address: create(:email_address))
  223. end
  224. context 'with inline attachments' do
  225. let(:body) do
  226. <<~BODY
  227. This is a test article with inline attachments.
  228. <img tabindex="0" style="width: 421px; max-width: 100%;" src="data:image/png;base64,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" />
  229. BODY
  230. end
  231. let(:article_payload) do
  232. {
  233. body: body,
  234. contentType: 'text/html',
  235. }
  236. end
  237. it 'creates a new ticket + a new article with inline attachments' do
  238. it_creates_ticket(articles: 1, stores: 1)
  239. expect(Store.last.filename).to eq('image1.png')
  240. end
  241. end
  242. context 'with attachments' do
  243. let(:article_payload) do
  244. form_id = SecureRandom.uuid
  245. file_name = 'file1.txt'
  246. file_type = 'text/plain'
  247. file_content = Base64.strict_encode64('file1')
  248. UploadCache.new(form_id).tap do |cache|
  249. cache.add(
  250. data: file_content,
  251. filename: file_name,
  252. preferences: { 'Content-Type' => file_type },
  253. created_by_id: agent.id
  254. )
  255. end
  256. {
  257. body: 'dummy',
  258. contentType: 'text/html',
  259. attachments: {
  260. formId: form_id,
  261. files: [
  262. {
  263. name: file_name,
  264. type: file_type,
  265. content: file_content,
  266. },
  267. ],
  268. },
  269. }
  270. end
  271. it 'creates a new ticket + a new article with attachments' do
  272. it_creates_ticket(articles: 1, stores: 1)
  273. expect(Store.last.filename).to eq('file1.txt')
  274. end
  275. end
  276. context 'with inline attachments + attachments' do
  277. let(:body) do
  278. <<~BODY
  279. This is a test article with inline attachments.
  280. <img tabindex="0" style="width: 421px; max-width: 100%;" src="data:image/png;base64,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" />
  281. BODY
  282. end
  283. let(:article_payload) do
  284. form_id = SecureRandom.uuid
  285. file_name = 'file1.txt'
  286. file_type = 'text/plain'
  287. file_content = Base64.strict_encode64('file1')
  288. UploadCache.new(form_id).tap do |cache|
  289. cache.add(
  290. data: file_content,
  291. filename: file_name,
  292. preferences: { 'Content-Type' => file_type },
  293. created_by_id: agent.id
  294. )
  295. end
  296. {
  297. body: body,
  298. contentType: 'text/html',
  299. attachments: {
  300. formId: form_id,
  301. files: [
  302. {
  303. name: file_name,
  304. type: file_type,
  305. content: file_content,
  306. },
  307. ],
  308. },
  309. }
  310. end
  311. it 'creates a new ticket + a new article with inline attachments + attachments' do
  312. it_creates_ticket(articles: 1, stores: 2)
  313. expect(Store.last.filename).to eq('image1.png')
  314. end
  315. end
  316. context 'with a specific sender' do
  317. let(:article_payload) do
  318. {
  319. body: 'dummy',
  320. sender: 'Agent',
  321. }
  322. end
  323. it 'creates a new ticket + a new article with a specific sender' do
  324. it_creates_ticket(articles: 1)
  325. expect(Ticket.last.articles.last.sender.name).to eq('Agent')
  326. end
  327. it 'sets correct "to" and "from" values', :aggregate_failures do
  328. it_creates_ticket(articles: 1)
  329. expect(Ticket.last.articles.last)
  330. .to have_attributes(
  331. from: agent.fullname,
  332. to: "#{customer.fullname} <#{customer.email}>"
  333. )
  334. end
  335. end
  336. context 'with no type' do
  337. let(:article_payload) do
  338. {
  339. body: 'dummy',
  340. }
  341. end
  342. it 'creates a new ticket + a new article, but falls back to type "note"' do
  343. it_creates_ticket(articles: 1)
  344. expect(Ticket.last.articles.last.type.name).to eq('note')
  345. end
  346. end
  347. context 'with a specific type' do
  348. let(:article_payload) do
  349. {
  350. body: 'dummy',
  351. type: Ticket::Article::Type.first.name,
  352. to: 'dummy@example.org',
  353. }
  354. end
  355. it 'creates a new ticket + a new article with a specific type' do
  356. it_creates_ticket(articles: 1)
  357. expect(Ticket.last.articles.last.type.name).to eq(Ticket::Article::Type.first.name)
  358. end
  359. context 'with all integrations disabled' do
  360. let(:article_payload) do
  361. {
  362. body: 'dummy',
  363. to: ['to@example.com'],
  364. type: 'email',
  365. security: {
  366. method: 'SMIME',
  367. options: %w[encryption sign]
  368. }
  369. }
  370. end
  371. before do
  372. Setting.set('smime_integration', false)
  373. Setting.set('pgp_integration', false)
  374. end
  375. it 'doesn\'t set security if security integrations are not enabled', :aggregate_failures do
  376. it_creates_ticket(articles: 1)
  377. expect(Ticket.last.articles.last.preferences[:security]).to be_nil
  378. end
  379. end
  380. context 'with smime enabled' do
  381. let(:article_payload) do
  382. {
  383. body: 'dummy',
  384. to: ['to@example.com'],
  385. type: 'email',
  386. security: {
  387. method: 'SMIME',
  388. options: %w[encryption sign]
  389. }
  390. }
  391. end
  392. before do
  393. Setting.set('smime_integration', true)
  394. Setting.set('pgp_integration', false)
  395. end
  396. it 'creates a new ticket with correct security preferences', :aggregate_failures do
  397. it_creates_ticket(articles: 1)
  398. expect(Ticket.last.articles.last.preferences[:security]).to eq(
  399. 'type' => 'S/MIME',
  400. 'encryption' => { 'success' => true },
  401. 'sign' => { 'success' => true },
  402. )
  403. end
  404. end
  405. context 'with pgp enabled' do
  406. let(:article_payload) do
  407. {
  408. body: 'dummy',
  409. to: ['to@example.com'],
  410. type: 'email',
  411. security: {
  412. method: 'PGP',
  413. options: %w[encryption sign]
  414. }
  415. }
  416. end
  417. before do
  418. Setting.set('smime_integration', false)
  419. Setting.set('pgp_integration', true)
  420. end
  421. it 'creates a new ticket with correct security preferences', :aggregate_failures do
  422. it_creates_ticket(articles: 1)
  423. expect(Ticket.last.articles.last.preferences[:security]).to eq(
  424. 'type' => 'PGP',
  425. 'encryption' => { 'success' => true },
  426. 'sign' => { 'success' => true },
  427. )
  428. end
  429. end
  430. end
  431. end
  432. context 'with to: and cc: being string values' do
  433. let(:article_payload) do
  434. {
  435. body: 'dummy',
  436. to: 'to@example.com',
  437. cc: 'cc@example.com',
  438. }
  439. end
  440. it 'creates a new ticket + a new article and sets correct "to" and "cc" values', :aggregate_failures do
  441. it_creates_ticket(articles: 1)
  442. expect(Ticket.last.articles.last).to have_attributes(to: 'to@example.com', cc: 'cc@example.com')
  443. end
  444. end
  445. context 'with to: and cc: containing array values' do
  446. let(:article_payload) do
  447. {
  448. body: 'dummy',
  449. to: ['to@example.com', 'to2@example.com'],
  450. cc: ['cc@example.com', 'cc2@example.com'],
  451. }
  452. end
  453. it 'creates a new ticket + a new article and sets correct "to" and "cc" values', :aggregate_failures do
  454. it_creates_ticket(articles: 1)
  455. expect(Ticket.last.articles.last).to have_attributes(to: 'to@example.com, to2@example.com', cc: 'cc@example.com, cc2@example.com')
  456. end
  457. end
  458. context 'with a shared draft' do
  459. let(:shared_draft) { create(:ticket_shared_draft_start, group:) }
  460. let(:input_payload) do
  461. input_base_payload
  462. .merge(sharedDraftId: Gql::ZammadSchema.id_from_object(shared_draft))
  463. end
  464. it 'passed to ticket create service' do
  465. expect_any_instance_of(Service::Ticket::Create)
  466. .to receive(:execute)
  467. .with(ticket_data: include(shared_draft:))
  468. .and_call_original
  469. gql.execute(query, variables: variables)
  470. end
  471. end
  472. end
  473. context 'with a customer', authenticated_as: :customer do
  474. let(:input_payload) { input_base_payload.tap { |h| h.delete(:customer) } }
  475. let(:expected_response) do
  476. expected_base_response.merge(
  477. {
  478. 'owner' => { 'fullname' => nil },
  479. 'priority' => { 'name' => Ticket::Priority.where(default_create: true).first.name },
  480. 'tags' => nil
  481. }
  482. )
  483. end
  484. it 'creates the ticket with filtered values' do
  485. it_creates_ticket
  486. expect(gql.result.data[:ticket]).to eq(expected_response)
  487. end
  488. context 'when sending a different customerId' do
  489. let(:input_payload) { input_base_payload.tap { |h| h[:customer][:id] = gql.id(create(:customer)) } }
  490. it 'fails creating a ticket with permission exception' do
  491. it_fails_to_create_ticket
  492. expect(gql.result.error_type).to eq(Exceptions::Forbidden)
  493. expect(gql.result.error_message).to eq('Access forbidden by Gql::Types::UserType')
  494. end
  495. end
  496. context 'with links' do
  497. let!(:other_ticket) { create(:ticket, customer: customer) }
  498. let(:links) do
  499. [
  500. { linkObjectId: gql.id(other_ticket), linkType: 'child' },
  501. { linkObjectId: gql.id(other_ticket), linkType: 'normal' },
  502. ]
  503. end
  504. let(:input_payload) { input_base_payload.merge(links:) }
  505. it 'creates the ticket without links' do
  506. it_creates_ticket
  507. expect(Link.list(link_object: 'Ticket', link_object_value: Ticket.last.id)).to eq([])
  508. end
  509. end
  510. context 'with issue tracker links' do
  511. let(:github_link) { 'https://github.com/issue/123' }
  512. let(:input_payload) { input_base_payload.merge(externalReferences: { github: [github_link] }) }
  513. before { Setting.set('github_integration', true) }
  514. it 'creates the ticket and adds issue tracker links' do
  515. it_creates_ticket
  516. expect(Ticket.last.preferences).not_to include('github')
  517. end
  518. end
  519. context 'with article' do
  520. context 'with a forbidden sender' do
  521. let(:article_payload) do
  522. {
  523. body: 'dummy',
  524. sender: 'Agent',
  525. }
  526. end
  527. it 'creates a new ticket + a new article, but falls back to "Customer" as sender' do
  528. it_creates_ticket(articles: 1)
  529. expect(Ticket.last.articles.last.sender.name).to eq('Customer')
  530. end
  531. end
  532. context 'with type "phone"' do
  533. let(:article_payload) do
  534. {
  535. body: 'dummy',
  536. type: 'phone',
  537. }
  538. end
  539. it 'creates a new ticket + a new article, but falls back to "note" as type' do
  540. it_creates_ticket(articles: 1)
  541. expect(Ticket.last.articles.last.type.name).to eq('note')
  542. end
  543. it 'sets correct "to" and "from" values', :aggregate_failures do
  544. it_creates_ticket(articles: 1)
  545. expect(Ticket.last.articles.last)
  546. .to have_attributes(
  547. to: Ticket.last.group.name,
  548. from: customer.fullname
  549. )
  550. end
  551. end
  552. context 'with an article flagged as internal' do
  553. let(:article_payload) do
  554. {
  555. body: 'dummy',
  556. internal: true,
  557. }
  558. end
  559. it 'creates a new ticket + a new article, but flags it as not internal' do
  560. it_creates_ticket(articles: 1)
  561. expect(Ticket.last.articles.last.internal).to be(false)
  562. end
  563. end
  564. end
  565. end
  566. context 'with an agent that has a specific role limited to create/update permission', authenticated_as: :user do
  567. let(:user) { create(:user, roles: [api_role]) }
  568. let(:api_role) do
  569. role = create(:role, name: 'API', permission_names: ['ticket.agent'])
  570. role.group_names_access_map = {
  571. Group.first.name => %w[create],
  572. }
  573. role
  574. end
  575. let(:input_payload) do
  576. {
  577. title: 'Test title for issue #4647',
  578. groupId: gql.id(Group.first),
  579. customer: { id: gql.id(customer) },
  580. article: article_payload,
  581. }
  582. end
  583. let(:article_payload) do
  584. {
  585. type: 'web',
  586. internal: false,
  587. sender: 'Customer',
  588. subject: 'Test subject',
  589. body: SecureRandom.uuid,
  590. }
  591. end
  592. before { Trigger.destroy_all } # triggers may cause additional articles to be created
  593. it 'contains correct "origin_by" + "from" information' do
  594. gql.execute(query, variables: variables)
  595. expect(Ticket.last.articles.last).to have_attributes(
  596. origin_by_id: customer.id,
  597. from: "#{customer.fullname} <#{customer.email}>",
  598. )
  599. end
  600. end
  601. end
  602. end