123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358 |
- # Copyright (C) 2012-2025 Zammad Foundation, https://zammad-foundation.org/
- require 'rails_helper'
- RSpec.describe 'Manage > Channels > Microsoft 365 Graph Email', type: :system do
- let(:client_id) { SecureRandom.uuid }
- let(:client_secret) { SecureRandom.urlsafe_base64(40) }
- let(:client_tenant) { SecureRandom.uuid }
- let(:callback_url) { "#{Setting.get('http_type')}://#{Setting.get('fqdn')}#{Rails.configuration.api_path}/external_credentials/microsoft_graph/callback" }
- context 'without an existing app configuration' do
- before do
- visit '#channels/microsoft_graph'
- end
- it 'creates a new app configuration' do
- find('.btn--success', text: 'Connect Microsoft 365 App').click
- in_modal do
- fill_in 'client_id', with: client_id
- fill_in 'client_secret', with: client_secret
- fill_in 'client_tenant', with: client_tenant
- check_input_field_value('callback_url', callback_url)
- check_copy_to_clipboard_text('callback_url', callback_url)
- click_on 'Submit'
- end
- expect(ExternalCredential.last).to have_attributes(
- name: 'microsoft_graph',
- credentials: include({
- 'client_id' => client_id,
- 'client_secret' => client_secret,
- 'client_tenant' => client_tenant,
- })
- )
- end
- end
- context 'with an existing app configuration' do
- let(:external_credential) { create(:microsoft_graph_credential) }
- before do
- external_credential
- end
- context 'when adding an account' do
- let(:shared_mailbox) { Faker::Internet.unique.email }
- before do
- visit '#channels/microsoft_graph'
- end
- it 'shows mailbox type dialog' do
- find('.btn--success', text: 'Add Account').click
- in_modal do
- check_select_field_value('mailbox_type', 'user')
- expect(page).to have_no_css('[name="shared_mailbox"]')
- set_select_field_value('mailbox_type', 'shared')
- click_on 'Authenticate'
- expect(page).to have_validation_message_for('[name="shared_mailbox"]')
- set_input_field_value('shared_mailbox', shared_mailbox)
- # We stop short of redirecting to the Microsoft login page.
- # click_on 'Authenticate'
- end
- end
- end
- context 'when editing an account' do
- let(:channel) { create(:microsoft_graph_channel, group: group1, inbound_options: { 'folder_id' => folder_id1, 'keep_on_server' => true }, active: false) }
- let(:group1) { create(:group) }
- let(:group2) { create(:group) }
- let(:folder_id1) { Base64.strict_encode64(Faker::Crypto.unique.sha256) }
- let(:folder_id2) { Base64.strict_encode64(Faker::Crypto.unique.sha256) }
- let(:folders) do
- [
- {
- 'id' => folder_id1,
- 'displayName' => Faker::Lorem.unique.word,
- 'childFolders' => [],
- },
- {
- 'id' => folder_id2,
- 'displayName' => Faker::Lorem.unique.word,
- 'childFolders' => [],
- },
- ]
- end
- before do
- channel && group2
- allow_any_instance_of(Channel).to receive(:refresh_xoauth2!).and_return(true)
- allow_any_instance_of(MicrosoftGraph).to receive(:get_message_folders_tree).and_return(folders)
- allow(EmailHelper::Probe).to receive(:inbound).and_return({ result: 'ok' })
- visit '#channels/microsoft_graph'
- find('.js-editInbound', text: 'Edit').click
- end
- it 'displays inbound configuration dialog' do
- in_modal do
- # TODO: Re-enable when the tree select filter mechanism is fixed to account for primitive values.
- # check_tree_select_field_value('group_id', group1.id.to_s)
- check_tree_select_field_value('options::folder_id', folder_id1)
- check_select_field_value('options::keep_on_server', 'true')
- set_tree_select_value('group_id', group2.id.to_s)
- set_tree_select_value('options::folder_id', folder_id2)
- set_select_field_label('options::keep_on_server', 'no')
- click_on 'Save'
- end
- expect(channel.reload).to have_attributes(
- group_id: group2.id,
- options: include({
- 'inbound' => include({
- 'options' => include({
- 'folder_id' => folder_id2,
- 'keep_on_server' => false,
- }),
- }),
- }),
- )
- end
- end
- context 'when editing destination group' do
- let(:channel) { create(:microsoft_graph_channel, group: group1, active: false) }
- let(:group1) { create(:group) }
- let(:group2) { create(:group) }
- let(:folders) { [] }
- before do
- channel && group2
- allow_any_instance_of(Channel).to receive(:refresh_xoauth2!).and_return(true)
- allow_any_instance_of(MicrosoftGraph).to receive(:get_message_folders_tree).and_return(folders)
- allow(EmailHelper::Probe).to receive(:inbound).and_return({ result: 'ok' })
- visit '#channels/microsoft_graph'
- find('.js-channelGroupChange', text: group1.name).click
- end
- it 'displays destination group dialog' do
- in_modal do
- # TODO: Re-enable when the tree select filter mechanism is fixed to account for primitive values.
- # check_tree_select_field_value('group_id', group1.id.to_s)
- set_tree_select_value('group_id', group2.id.to_s)
- click_on 'Submit'
- end
- expect(channel.reload).to have_attributes(group_id: group2.id)
- end
- end
- context 'when toggling an account' do
- let(:channel) { create(:microsoft_graph_channel, active: false) }
- before do
- channel
- visit '#channels/microsoft_graph'
- end
- it 'switches channel between enabled and disabled state' do
- find('.js-enable', text: 'Enable').click
- expect(channel.reload.active).to be(true)
- find('.js-disable', text: 'Disable').click
- expect(channel.reload.active).to be(false)
- end
- end
- context 'when deleting an account' do
- let(:channel) { create(:microsoft_graph_channel, active: false) }
- let(:email_address) { create(:email_address, email: channel.options.dig('inbound', 'options', 'user'), channel: channel) }
- before do
- channel && email_address
- visit '#channels/microsoft_graph'
- find('.js-delete', text: 'Delete').click
- end
- it 'destroys the channel and the associated email address' do
- in_modal do
- click_on 'Yes'
- end
- expect { channel.reload }.to raise_error(ActiveRecord::RecordNotFound)
- expect(page).to have_content('Notice: Unassigned email addresses, assign them to a channel or delete them.')
- find('.js-emailAddressDelete').click
- in_modal do
- click_on 'Delete'
- end
- expect { email_address.reload }.to raise_error(ActiveRecord::RecordNotFound)
- end
- end
- context 'when being redirected by a successful auth flow' do
- let(:channel) { create(:microsoft_graph_channel, active: false) }
- let(:group) { create(:group) }
- let(:folder_id) { Base64.strict_encode64(Faker::Crypto.unique.sha256) }
- let(:folders) do
- [
- {
- 'id' => folder_id,
- 'displayName' => Faker::Lorem.unique.word,
- 'childFolders' => [],
- },
- ]
- end
- before do
- channel && group
- allow_any_instance_of(Channel).to receive(:refresh_xoauth2!).and_return(true)
- allow_any_instance_of(MicrosoftGraph).to receive(:get_message_folders_tree).and_return(folders)
- allow(EmailHelper::Probe).to receive(:inbound).and_return({ result: 'ok' })
- visit "#channels/microsoft_graph/#{channel.id}"
- end
- it 'displays inbound configuration dialog' do
- in_modal do
- # TODO: Re-enable when the tree select filter mechanism is fixed to account for primitive values.
- # check_tree_select_field_value('group_id', Group.first.id.to_s)
- check_tree_select_field_value('options::folder_id', '')
- set_tree_select_value('group_id', group.id.to_s)
- set_tree_select_value('options::folder_id', folder_id)
- set_select_field_label('options::keep_on_server', 'yes')
- click_on 'Save'
- end
- expect(channel.reload).to have_attributes(
- group_id: group.id,
- options: include({
- 'inbound' => include({
- 'options' => include({
- 'folder_id' => folder_id,
- 'keep_on_server' => true,
- }),
- }),
- }),
- )
- end
- end
- context 'when being redirected with a wrong user' do
- let(:email_address) { Faker::Internet.unique.email }
- let(:channel) { create(:microsoft_graph_channel, microsoft_user: email_address, active: false) }
- before do
- visit "#channels/microsoft_graph/error/user_mismatch/channel/#{channel.id}"
- end
- it 'displays user mismatch dialog' do
- in_modal do
- expect(page).to have_content('The entered user for reauthentication differs from the user that was used for setting up your Microsoft365 channel initially.')
- expect(page).to have_content('To avoid fetching an incorrect Microsoft365 mailbox, the reauthentication process was aborted.')
- expect(page).to have_content('Please start the reauthentication again and enter the correct credentials.')
- expect(page).to have_content("Current User: #{email_address}")
- click_on 'Close'
- end
- end
- end
- context 'when being redirected with an email address already in use' do
- let(:email_address) { Faker::Internet.unique.email }
- before do
- visit "#channels/microsoft_graph/error/duplicate_email_address/param/#{CGI.escapeURIComponent(email_address)}"
- end
- it 'displays duplicate email address dialog' do
- in_modal do
- expect(page).to have_content("The email address #{email_address} is already in use by another account.")
- click_on 'Close'
- end
- end
- end
- context 'when the API throws an error' do
- let(:channel) { create(:microsoft_graph_channel, active: false) }
- let(:error) do
- {
- message: 'The mailbox is either inactive, soft-deleted, or is hosted on-premise.',
- code: 'MailboxNotEnabledForRESTAPI',
- }
- end
- before do
- channel
- allow_any_instance_of(Channel).to receive(:refresh_xoauth2!).and_return(true)
- allow_any_instance_of(MicrosoftGraph).to receive(:get_message_folders_tree).and_raise(MicrosoftGraph::ApiError, error)
- visit '#channels/microsoft_graph'
- find('.js-editInbound', text: 'Edit').click
- end
- it 'displays original error message and a helpful hint' do
- in_modal do
- expect(page).to have_content("#{error[:message]} (#{error[:code]})")
- 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.')
- click_on 'Cancel & Go Back'
- end
- end
- end
- end
- def check_copy_to_clipboard_text(field_name, clipboard_text)
- find(".js-copy[data-target-field='#{field_name}']").click
- # Add a temporary text input element to the page, so we can paste the clipboard text into it and compare the value.
- # Programmatic clipboard management requires extra browser permissions and does not work in all of them.
- page.execute_script "$('<input name=\"clipboard_#{field_name}\" type=\"text\" class=\"form-control\">').insertAfter($('input[name=#{field_name}]'));"
- input_field = find("input[name='clipboard_#{field_name}']")
- .send_keys('')
- .click
- .send_keys([magic_key, 'v'])
- expect(input_field.value).to eq(clipboard_text)
- page.execute_script "$('input[name=\"clipboard_#{field_name}\"]').addClass('is-hidden');"
- end
- end
|