microsoft_graph_spec.rb 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443
  1. # Copyright (C) 2012-2025 Zammad Foundation, https://zammad-foundation.org/
  2. require 'rails_helper'
  3. RSpec.describe 'Manage > Channels > Microsoft 365 Graph Email', time_zone: 'Europe/London', type: :system do
  4. let(:client_id) { SecureRandom.uuid }
  5. let(:client_secret) { SecureRandom.urlsafe_base64(40) }
  6. let(:client_tenant) { SecureRandom.uuid }
  7. let(:callback_url) { "#{Setting.get('http_type')}://#{Setting.get('fqdn')}#{Rails.configuration.api_path}/external_credentials/microsoft_graph/callback" }
  8. context 'without an existing app configuration' do
  9. before do
  10. visit '#channels/microsoft_graph'
  11. end
  12. it 'creates a new app configuration' do
  13. find('.btn--success', text: 'Connect Microsoft 365 App').click
  14. in_modal do
  15. fill_in 'client_id', with: client_id
  16. fill_in 'client_secret', with: client_secret
  17. fill_in 'client_tenant', with: client_tenant
  18. check_input_field_value('callback_url', callback_url)
  19. check_copy_to_clipboard_text('callback_url', callback_url)
  20. click_on 'Submit'
  21. end
  22. expect(ExternalCredential.last).to have_attributes(
  23. name: 'microsoft_graph',
  24. credentials: include(
  25. client_id: client_id,
  26. client_secret: client_secret,
  27. client_tenant: client_tenant,
  28. )
  29. )
  30. end
  31. end
  32. context 'with an existing app configuration' do
  33. let(:external_credential) { create(:microsoft_graph_credential) }
  34. before do
  35. external_credential
  36. end
  37. context 'when adding an account' do
  38. let(:shared_mailbox) { Faker::Internet.unique.email }
  39. before do
  40. visit '#channels/microsoft_graph'
  41. end
  42. it 'shows mailbox type dialog' do
  43. find('.btn--success', text: 'Add Account').click
  44. in_modal do
  45. check_select_field_value('mailbox_type', 'user')
  46. expect(page).to have_no_css('[name="shared_mailbox"]')
  47. set_select_field_value('mailbox_type', 'shared')
  48. click_on 'Authenticate'
  49. expect(page).to have_validation_message_for('[name="shared_mailbox"]')
  50. set_input_field_value('shared_mailbox', shared_mailbox)
  51. end
  52. end
  53. end
  54. context 'when editing an account' do
  55. let(:channel) { create(:microsoft_graph_channel, group: group1, inbound_options: { 'folder_id' => folder_id1, 'keep_on_server' => true }, active: false) }
  56. let(:group1) { create(:group) }
  57. let(:group2) { create(:group) }
  58. let(:state) { Ticket::State.find_by(name: 'open') }
  59. let(:folder_id1) { Base64.strict_encode64(Faker::Crypto.unique.sha256) }
  60. let(:folder_id2) { Base64.strict_encode64(Faker::Crypto.unique.sha256) }
  61. let(:folders) do
  62. [
  63. {
  64. 'id' => folder_id1,
  65. 'displayName' => Faker::Lorem.unique.word,
  66. 'childFolders' => [],
  67. },
  68. {
  69. 'id' => folder_id2,
  70. 'displayName' => Faker::Lorem.unique.word,
  71. 'childFolders' => [],
  72. },
  73. ]
  74. end
  75. before do
  76. channel && group2
  77. allow_any_instance_of(Channel).to receive(:refresh_xoauth2!).and_return(true)
  78. allow_any_instance_of(MicrosoftGraph).to receive(:get_message_folders_tree).and_return(folders)
  79. allow(EmailHelper::Probe).to receive(:inbound).and_return({ result: 'ok' })
  80. end
  81. context 'when editing a freshly added account' do
  82. before do
  83. visit "#channels/microsoft_graph/#{channel.id}"
  84. end
  85. context 'when no emails exist' do
  86. before do
  87. allow(EmailHelper::Probe).to receive(:inbound).and_return({ result: 'ok', content_messages: 0 })
  88. end
  89. it 'does not display archive dialog but saves channel' do
  90. in_modal do
  91. set_tree_select_value('group_id', group1.id.to_s)
  92. set_tree_select_value('options::folder_id', folder_id2)
  93. click_on 'Save'
  94. end
  95. expect(channel.reload).to have_attributes(
  96. active: true,
  97. options: include(inbound: include(options: include(folder_id: folder_id2)))
  98. )
  99. end
  100. end
  101. context 'when some emails exist' do
  102. before do
  103. allow(EmailHelper::Probe).to receive(:inbound).and_return({ result: 'ok', content_messages: 123 })
  104. end
  105. it 'displays inbound configuration dialog' do
  106. visit "#channels/microsoft_graph/#{channel.id}"
  107. in_modal do
  108. check_tree_select_field_value('group_id', group1.id.to_s)
  109. check_tree_select_field_value('options::folder_id', folder_id1)
  110. check_select_field_value('options::keep_on_server', 'true')
  111. set_tree_select_value('group_id', group2.id.to_s)
  112. set_tree_select_value('options::folder_id', folder_id2)
  113. set_select_field_label('options::keep_on_server', 'no')
  114. click_on 'Save'
  115. end
  116. in_modal do
  117. set_select_field_value('options::archive_state_id', state.id.to_s)
  118. set_date_field_value('options::archive_before', '12/01/2024')
  119. click_on 'Submit'
  120. end
  121. expect(channel.reload).to have_attributes(
  122. group_id: group2.id,
  123. active: true,
  124. options: include(
  125. inbound: include(
  126. options: include(
  127. folder_id: folder_id2,
  128. keep_on_server: false,
  129. archive: true,
  130. archive_state_id: state.id.to_s,
  131. archive_before: '2024-12-01T08:00:00.000Z'
  132. ),
  133. ),
  134. ),
  135. )
  136. end
  137. end
  138. end
  139. context 'when editing an existing channel' do
  140. before do
  141. channel.options[:inbound][:options]
  142. .merge!(archive: true, archive_state_id: state.id.to_s, archive_before: '2024-12-01T08:00:00.000Z')
  143. channel.save!
  144. allow(EmailHelper::Probe).to receive(:inbound).and_return({ result: 'ok', content_messages: 0 })
  145. visit '#channels/microsoft_graph'
  146. find('.js-editInbound', text: 'Edit').click
  147. end
  148. it 'displays inbound configuration dialog' do
  149. in_modal do
  150. check_tree_select_field_value('group_id', group1.id.to_s)
  151. check_tree_select_field_value('options::folder_id', folder_id1)
  152. check_select_field_value('options::keep_on_server', 'true')
  153. set_tree_select_value('group_id', group2.id.to_s)
  154. set_tree_select_value('options::folder_id', folder_id2)
  155. set_select_field_label('options::keep_on_server', 'no')
  156. click_on 'Save'
  157. end
  158. in_modal do
  159. check_switch_field_value('options::archive', true)
  160. check_select_field_value('options::archive_state_id', state.id.to_s)
  161. check_date_field_value('options::archive_before', '12/01/2024')
  162. click '.js-switch'
  163. click_on 'Submit'
  164. end
  165. expect(channel.reload).to have_attributes(
  166. group_id: group2.id,
  167. active: false,
  168. options: include(
  169. inbound: include(
  170. options: include(
  171. folder_id: folder_id2,
  172. keep_on_server: false,
  173. archive: false,
  174. archive_state_id: state.id.to_s,
  175. archive_before: '2024-12-01T08:00:00.000Z'
  176. ),
  177. ),
  178. ),
  179. )
  180. end
  181. end
  182. end
  183. context 'when editing destination group' do
  184. let(:channel) { create(:microsoft_graph_channel, group: group1, active: false) }
  185. let(:group1) { create(:group) }
  186. let(:group2) { create(:group) }
  187. let(:folders) { [] }
  188. before do
  189. channel && group2
  190. allow_any_instance_of(Channel).to receive(:refresh_xoauth2!).and_return(true)
  191. allow_any_instance_of(MicrosoftGraph).to receive(:get_message_folders_tree).and_return(folders)
  192. allow(EmailHelper::Probe).to receive(:inbound).and_return({ result: 'ok' })
  193. visit '#channels/microsoft_graph'
  194. find('.js-channelGroupChange', text: group1.name).click
  195. end
  196. it 'displays destination group dialog' do
  197. in_modal do
  198. check_tree_select_field_value('group_id', group1.id.to_s)
  199. set_tree_select_value('group_id', group2.id.to_s)
  200. click_on 'Submit'
  201. end
  202. expect(channel.reload).to have_attributes(group_id: group2.id)
  203. end
  204. end
  205. context 'when toggling an account' do
  206. let(:channel) { create(:microsoft_graph_channel, active: false) }
  207. before do
  208. channel
  209. visit '#channels/microsoft_graph'
  210. end
  211. it 'switches channel between enabled and disabled state' do
  212. find('.js-enable', text: 'Enable').click
  213. expect(channel.reload.active).to be(true)
  214. find('.js-disable', text: 'Disable').click
  215. expect(channel.reload.active).to be(false)
  216. end
  217. end
  218. context 'when deleting an account' do
  219. let(:channel) { create(:microsoft_graph_channel, active: false) }
  220. let(:email_address) { create(:email_address, email: channel.options.dig('inbound', 'options', 'user'), channel: channel) }
  221. before do
  222. channel && email_address
  223. visit '#channels/microsoft_graph'
  224. find('.js-delete', text: 'Delete').click
  225. end
  226. it 'destroys the channel and the associated email address' do
  227. in_modal do
  228. click_on 'Yes'
  229. end
  230. expect { channel.reload }.to raise_error(ActiveRecord::RecordNotFound)
  231. expect(page).to have_content('Notice: Unassigned email addresses, assign them to a channel or delete them.')
  232. find('.js-emailAddressDelete').click
  233. in_modal do
  234. click_on 'Delete'
  235. end
  236. expect { email_address.reload }.to raise_error(ActiveRecord::RecordNotFound)
  237. end
  238. end
  239. context 'when being redirected by a successful auth flow' do
  240. let(:channel) { create(:microsoft_graph_channel, active: false) }
  241. let(:group) { create(:group) }
  242. let(:folder_id) { Base64.strict_encode64(Faker::Crypto.unique.sha256) }
  243. let(:folders) do
  244. [
  245. {
  246. 'id' => folder_id,
  247. 'displayName' => Faker::Lorem.unique.word,
  248. 'childFolders' => [],
  249. },
  250. ]
  251. end
  252. before do
  253. channel && group
  254. allow_any_instance_of(Channel).to receive(:refresh_xoauth2!).and_return(true)
  255. allow_any_instance_of(MicrosoftGraph).to receive(:get_message_folders_tree).and_return(folders)
  256. allow(EmailHelper::Probe).to receive(:inbound).and_return({ result: 'ok' })
  257. visit "#channels/microsoft_graph/#{channel.id}"
  258. end
  259. it 'displays inbound configuration dialog' do
  260. in_modal do
  261. check_tree_select_field_value('group_id', Group.first.id.to_s)
  262. check_tree_select_field_value('options::folder_id', '')
  263. set_tree_select_value('group_id', group.id.to_s)
  264. set_tree_select_value('options::folder_id', folder_id)
  265. set_select_field_label('options::keep_on_server', 'yes')
  266. click_on 'Save'
  267. end
  268. expect(channel.reload).to have_attributes(
  269. group_id: group.id,
  270. options: include(
  271. inbound: include(
  272. options: include(
  273. folder_id: folder_id,
  274. keep_on_server: true,
  275. ),
  276. ),
  277. ),
  278. )
  279. end
  280. end
  281. context 'when being redirected with a wrong user' do
  282. let(:email_address) { Faker::Internet.unique.email }
  283. let(:channel) { create(:microsoft_graph_channel, microsoft_user: email_address, active: false) }
  284. before do
  285. visit "#channels/microsoft_graph/error/user_mismatch/channel/#{channel.id}"
  286. end
  287. it 'displays user mismatch dialog' do
  288. in_modal do
  289. expect(page).to have_content('The entered user for reauthentication differs from the user that was used for setting up your Microsoft365 channel initially.')
  290. expect(page).to have_content('To avoid fetching an incorrect Microsoft365 mailbox, the reauthentication process was aborted.')
  291. expect(page).to have_content('Please start the reauthentication again and enter the correct credentials.')
  292. expect(page).to have_content("Current User: #{email_address}")
  293. click_on 'Close'
  294. end
  295. end
  296. end
  297. context 'when being redirected with an email address already in use' do
  298. let(:email_address) { Faker::Internet.unique.email }
  299. before do
  300. visit "#channels/microsoft_graph/error/duplicate_email_address/param/#{CGI.escapeURIComponent(email_address)}"
  301. end
  302. it 'displays duplicate email address dialog' do
  303. in_modal do
  304. expect(page).to have_content("The email address #{email_address} is already in use by another account.")
  305. click_on 'Close'
  306. end
  307. end
  308. end
  309. context 'when the API throws an error' do
  310. let(:channel) { create(:microsoft_graph_channel, active: false) }
  311. let(:error) do
  312. {
  313. message: 'The mailbox is either inactive, soft-deleted, or is hosted on-premise.',
  314. code: 'MailboxNotEnabledForRESTAPI',
  315. }
  316. end
  317. before do
  318. channel
  319. allow_any_instance_of(Channel).to receive(:refresh_xoauth2!).and_return(true)
  320. allow_any_instance_of(MicrosoftGraph).to receive(:get_message_folders_tree).and_raise(MicrosoftGraph::ApiError, error)
  321. visit '#channels/microsoft_graph'
  322. find('.js-editInbound', text: 'Edit').click
  323. end
  324. it 'displays original error message and a helpful hint' do
  325. in_modal do
  326. expect(page).to have_content("#{error[:message]} (#{error[:code]})")
  327. expect(page).to have_content('Did you verify that the user has access to the mailbox? Or consider removing this channel and switch to using a different mailbox type.')
  328. click_on 'Cancel & Go Back'
  329. end
  330. end
  331. end
  332. end
  333. def check_copy_to_clipboard_text(field_name, clipboard_text)
  334. find(".js-copy[data-target-field='#{field_name}']").click
  335. # Add a temporary text input element to the page, so we can paste the clipboard text into it and compare the value.
  336. # Programmatic clipboard management requires extra browser permissions and does not work in all of them.
  337. page.execute_script "$('<input name=\"clipboard_#{field_name}\" type=\"text\" class=\"form-control\">').insertAfter($('input[name=#{field_name}]'));"
  338. input_field = find("input[name='clipboard_#{field_name}']")
  339. .send_keys('')
  340. .click
  341. .send_keys([magic_key, 'v'])
  342. expect(input_field.value).to eq(clipboard_text)
  343. page.execute_script "$('input[name=\"clipboard_#{field_name}\"]').addClass('is-hidden');"
  344. end
  345. end