microsoft_graph_spec.rb 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261
  1. # Copyright (C) 2012-2025 Zammad Foundation, https://zammad-foundation.org/
  2. require 'rails_helper'
  3. RSpec.describe MicrosoftGraph, :aggregate_failures, integration: true, required_envs: %w[MICROSOFTGRAPH_REFRESH_TOKEN MICROSOFT365_CLIENT_ID MICROSOFT365_CLIENT_SECRET MICROSOFT365_CLIENT_TENANT MICROSOFT365_USER], use_vcr: true do
  4. let(:token) do
  5. {
  6. created_at: 1.hour.ago,
  7. client_id: ENV['MICROSOFT365_CLIENT_ID'],
  8. client_secret: ENV['MICROSOFT365_CLIENT_SECRET'],
  9. client_tenant: ENV['MICROSOFT365_CLIENT_TENANT'],
  10. refresh_token: ENV['MICROSOFTGRAPH_REFRESH_TOKEN'],
  11. }.with_indifferent_access
  12. end
  13. let(:client_access_token) { ExternalCredential::MicrosoftGraph.refresh_token(token)[:access_token] }
  14. let(:client_mailbox) { ENV['MICROSOFT365_USER'] }
  15. let(:client) do
  16. VCR.configure do |c|
  17. c.filter_sensitive_data('<MICROSOFT365_USER>') { ENV['MICROSOFT365_USER'] }
  18. c.filter_sensitive_data('<MICROSOFT365_CLIENT_ID>') { token[:client_id] }
  19. c.filter_sensitive_data('<MICROSOFT365_CLIENT_SECRET>') { token[:client_secret] }
  20. c.filter_sensitive_data('<MICROSOFT365_CLIENT_TENANT>') { token[:client_tenant] }
  21. c.filter_sensitive_data('<MICROSOFTGRAPH_REFRESH_TOKEN>') { token[:refresh_token] }
  22. c.filter_sensitive_data('<MICROSOFTGRAPH_ACCESS_TOKEN>') { client_access_token }
  23. end
  24. described_class.new(access_token: client_access_token, mailbox: client_mailbox)
  25. end
  26. # Tests #create_message_folder, #get_message_folder_details, #delete_message_folder
  27. describe 'folder lifecycle' do
  28. let(:folder_name) { "rspec-graph-client-#{SecureRandom.uuid}" }
  29. before do
  30. VCR.configure do |c|
  31. c.filter_sensitive_data('<FOLDER_NAME>') { folder_name }
  32. end
  33. end
  34. it 'tests folder lifecycle' do
  35. new_folder = client.create_message_folder(folder_name)
  36. fetched_folder = client.get_message_folder_details(new_folder['id'])
  37. expect(fetched_folder['displayName']).to eq(folder_name)
  38. client.delete_message_folder(new_folder['id'])
  39. end
  40. end
  41. # Tests #store_mocked_message, #get_raw_message, #get_message_basic_details, #mark_message_as_read, #message_delete, #list_messages
  42. describe 'message lifecycle' do
  43. let(:folder_name) { "rspec-graph-client-#{SecureRandom.uuid}" }
  44. let(:mail_subject) { "rspec-graph-client-#{SecureRandom.uuid}" }
  45. let(:folder) { client.create_message_folder(folder_name) }
  46. let(:message) do
  47. {
  48. subject: mail_subject,
  49. body: { content: 'Test email' },
  50. from: {
  51. emailAddress: { address: 'from@example.com' }
  52. },
  53. toRecipients: [
  54. {
  55. emailAddress: { address: 'test@example.com' }
  56. }
  57. ],
  58. isRead: false,
  59. }
  60. end
  61. before do
  62. VCR.configure do |c|
  63. c.filter_sensitive_data('<FOLDER_NAME>') { folder_name }
  64. c.filter_sensitive_data('<MAIL_SUBJECT>') { mail_subject }
  65. end
  66. folder
  67. end
  68. after { client.delete_message_folder(folder['id']) }
  69. it 'tests message lifecycle' do
  70. new_message = client.store_mocked_message(message, folder_id: folder['id'])
  71. raw = client.get_raw_message(new_message['id'])
  72. expect(raw).to include("Subject: #{message[:subject]}")
  73. expect(raw).to include(message[:body][:content])
  74. details = client.get_message_basic_details(new_message['id'])
  75. expect(details).to include(size: be_positive)
  76. # Message is marked as unread on creation, should appear in unread messages list
  77. expect(client.list_messages(folder_id: folder['id'], unread_only: true))
  78. .to include(include(id: new_message['id']))
  79. client.mark_message_as_read(new_message['id'])
  80. # After being marked as unread, should be gone from the same list
  81. expect(client.list_messages(folder_id: folder['id'], unread_only: true))
  82. .not_to include(include(id: new_message['id']))
  83. # Either way, message shows up into not-filtered-by-read-state list
  84. expect(client.list_messages(folder_id: folder['id']))
  85. .to include(include(id: new_message['id']))
  86. client.delete_message(new_message['id'])
  87. end
  88. end
  89. describe '#get_message_folders_tree' do
  90. let(:top_folder_name) { "rspec-graph-client-#{SecureRandom.uuid}" }
  91. before do
  92. VCR.configure do |c|
  93. c.filter_sensitive_data('<TOP_FOLDER_TREE>') { top_folder_name }
  94. end
  95. top_level_folder = client.create_message_folder(top_folder_name)
  96. client.create_message_folder('dead-end', parent_folder_id: top_level_folder['id'])
  97. second_level_folder = client.create_message_folder('2nd-level', parent_folder_id: top_level_folder['id'])
  98. client.create_message_folder('3rd-level', parent_folder_id: second_level_folder['id'])
  99. end
  100. it 'returns tree structure of a folder' do
  101. expect(client.get_message_folders_tree).to include(
  102. include(
  103. displayName: top_folder_name,
  104. childFolders: include(
  105. include(displayName: '2nd-level', childFolders: [
  106. include(displayName: '3rd-level', childFolders: be_blank)
  107. ]),
  108. include(displayName: 'dead-end', childFolders: be_blank)
  109. )
  110. )
  111. )
  112. end
  113. end
  114. # Also checks #get_message_basic_details since headers are present on real messages only
  115. describe '#send_mail' do
  116. let(:mail_subject) { "rspec-graph-client-#{SecureRandom.uuid}" }
  117. let(:mail) do
  118. {
  119. to: ENV['MICROSOFT365_USER'],
  120. subject: mail_subject,
  121. body: 'Test email',
  122. }
  123. end
  124. before do
  125. VCR.configure do |c|
  126. # Looks like VCR cannot have repeating filter names in the same spec file
  127. # Thus using a slightly different string here
  128. c.filter_sensitive_data('<SEND_MAIL_SUBJECT>') { mail_subject }
  129. end
  130. end
  131. it 'sends an email' do
  132. client.send_message(Channel::EmailBuild.build(mail))
  133. # wait for email to arrive
  134. if !VCR.turned_on? || VCR.current_cassette.recording?
  135. sleep 3
  136. end
  137. mails = client.list_messages(unread_only: true, select: 'id,subject')
  138. expect(mails).to include(
  139. include(subject: mail_subject)
  140. )
  141. test_email_id = mails.find { |elem| elem[:subject] == mail_subject }['id']
  142. details = client.get_message_basic_details(test_email_id)
  143. expect(details).to include(size: be_positive, headers: include(Subject: mail_subject))
  144. client.delete_message(test_email_id)
  145. end
  146. end
  147. describe '#make_paginated_request' do
  148. let(:page_solo) { { value: %w[A B] } }
  149. let(:page_1) { page_solo.merge '@odata.nextLink': 'page_2' }
  150. let(:page_2) { { value: %w[C], '@odata.nextLink': 'page_3' } }
  151. let(:page_3) { { value: %w[D E] } }
  152. context 'when response is single-page' do
  153. before do
  154. allow(client).to receive(:make_request)
  155. .with('path', params: { test: true })
  156. .and_return(page_solo.with_indifferent_access)
  157. end
  158. it 'returns value' do
  159. response = client.send(:make_paginated_request, 'path', params: { test: true })
  160. expect(response).to eq %w[A B]
  161. end
  162. end
  163. context 'when response is paginated' do
  164. before do
  165. allow(client).to receive(:make_request)
  166. .with('path', params: { test: true })
  167. .and_return(page_1.with_indifferent_access)
  168. allow(client).to receive(:make_request)
  169. .with('page_2')
  170. .and_return(page_2.with_indifferent_access)
  171. allow(client).to receive(:make_request)
  172. .with('page_3')
  173. .and_return(page_3.with_indifferent_access)
  174. end
  175. context 'when follow_pagination: false' do
  176. it 'returns value of the first page only' do
  177. response = client.send(:make_paginated_request, 'path', params: { test: true }, follow_pagination: false)
  178. expect(response).to eq %w[A B]
  179. end
  180. end
  181. context 'when follow_pagination: true' do
  182. it 'returns concatenated values' do
  183. response = client.send(:make_paginated_request, 'path', params: { test: true })
  184. expect(response).to eq %w[A B C D E]
  185. end
  186. it 'raises error if loop limit is reached' do
  187. stub_const("#{described_class}::PAGINATED_MAX_LOOPS", 1)
  188. expect { client.send(:make_paginated_request, 'path', params: { test: true }) }
  189. .to raise_error(described_class::ApiError)
  190. end
  191. end
  192. end
  193. end
  194. describe '#headers_to_hash' do
  195. let(:input) do
  196. [
  197. { name: 'A', value: 'B' },
  198. { name: 'ABC', value: 'BBB' }
  199. ]
  200. end
  201. let(:output) { { 'A' => 'B', 'ABC' => 'BBB' } }
  202. it 'converts array-of-hashes to a simplified hash' do
  203. expect(client.send(:headers_to_hash, input)).to eq(output)
  204. end
  205. end
  206. end