# -*- coding: utf-8 -*- ########################################################################## # OpenLP - Open Source Lyrics Projection # # ---------------------------------------------------------------------- # # Copyright (c) 2008-2023 OpenLP Developers # # ---------------------------------------------------------------------- # # This program is free software: you can redistribute it and/or modify # # it under the terms of the GNU General Public License as published by # # the Free Software Foundation, either version 3 of the License, or # # (at your option) any later version. # # # # This program is distributed in the hope that it will be useful, # # but WITHOUT ANY WARRANTY; without even the implied warranty of # # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # # GNU General Public License for more details. # # # # You should have received a copy of the GNU General Public License # # along with this program. If not, see . # ########################################################################## """ This module contains tests for the CCLI SongSelect importer. It needs re-writing at some point to load real HTML pages from disk and then test the behaviour based on those. That way if and when CCLI change their page layout, changing the tests would just be a case of re-downloading the HTML pages and changing the code to use the new layout. """ from unittest import TestCase from unittest.mock import MagicMock, patch, sentinel from PyQt5 import QtWidgets, QtCore from openlp.core.common.registry import Registry from openlp.plugins.songs.forms.songselectform import SongSelectForm from openlp.plugins.songs.lib import Song from openlp.plugins.songs.lib.songselect import BASE_URL, LOGIN_PAGE, Pages, SongSelectImport from tests.helpers.songfileimport import SongImportTestHelper from tests.helpers.testmixin import TestMixin from tests.utils.constants import RESOURCE_PATH TEST_PATH = RESOURCE_PATH / 'songs' / 'songselect' class TestSongSelectImport(TestCase, TestMixin): """ Test the :class:`~openlp.plugins.songs.lib.songselect.SongSelectImport` class """ def test_constructor(self): """ Test that constructing a basic SongSelectImport object works correctly """ # GIVEN: The SongSelectImporter class and a mocked out build_opener # WHEN: An object is instantiated importer = SongSelectImport(sentinel.db_manager, sentinel.webview) # THEN: The object should have the correct properties assert importer.db_manager is sentinel.db_manager, 'The db_manager should be set' assert importer.webview is sentinel.webview, 'The webview should be set' def test_get_page_type_login(self): """ Test get_page_type to spot the login page """ # GIVEN: A importer object, and a mocked url importer = SongSelectImport(None, None) url = QtCore.QUrl('https://profile.ccli.com/account/signin?appContext=SongSelect&' 'returnUrl=https%3a%2f%2fsongselect.ccli.com%2f') page = MagicMock(url=MagicMock(return_value=url)) importer.webview = MagicMock(page=MagicMock(return_value=page)) # WHEN: The method is run result = importer.get_page_type() # THEN: The correct type should be returned assert result == Pages.Login def test_get_page_type_home(self): """ Test get_page_type to spot the home page """ # GIVEN: A importer object, and a mocked url importer = SongSelectImport(None, None) url = QtCore.QUrl('https://songselect.ccli.com') page = MagicMock(url=MagicMock(return_value=url)) importer.webview = MagicMock(page=MagicMock(return_value=page)) # WHEN: The method is run result = importer.get_page_type() # THEN: The correct type should be returned assert result == Pages.Home def test_get_page_type_search(self): """ Test get_page_type to spot the search page """ # GIVEN: A importer object, and a mocked url importer = SongSelectImport(None, None) url = QtCore.QUrl('https://songselect.ccli.com/search/results?SearchText=test') page = MagicMock(url=MagicMock(return_value=url)) importer.webview = MagicMock(page=MagicMock(return_value=page)) # WHEN: The method is run result = importer.get_page_type() # THEN: The correct type should be returned assert result == Pages.Search def test_get_page_type_song(self): """ Test get_page_type to spot the login page """ # GIVEN: A importer object, and a mocked url importer = SongSelectImport(None, None) url = QtCore.QUrl('https://songselect.ccli.com/Songs/7115744/song_name/view_lyrics') page = MagicMock(url=MagicMock(return_value=url)) importer.webview = MagicMock(page=MagicMock(return_value=page)) # WHEN: The method is run result = importer.get_page_type() # THEN: The correct type should be returned assert result == Pages.Song def test_get_page_type_other(self): """ Test get_page_type to spot the login page """ # GIVEN: A importer object, and a mocked url importer = SongSelectImport(None, None) url = QtCore.QUrl('https://openlp.org') page = MagicMock(url=MagicMock(return_value=url)) importer.webview = MagicMock(page=MagicMock(return_value=page)) # WHEN: The method is run result = importer.get_page_type() # THEN: The correct type should be returned assert result == Pages.Other @patch('openlp.plugins.songs.lib.songselect.wait_for') def test_run_javascript(self, mocked_wait_for): """ Test run javascript calls the page object """ # GIVEN: A importer object and mocked run js fn def runJs(script, handle_result): handle_result('processed_{}'.format(script)) importer = SongSelectImport(None, None) importer.webview = MagicMock() page = MagicMock() page.runJavaScript = runJs importer.webview.page.return_value = page # WHEN: The set login field method is called result = importer._run_javascript('2 + 2') # THEN: The javascript should have been called on the page object assert result == 'processed_2 + 2' def test_reset_webview(self): """ Check that the setUrl method is called when the reset webview method is called """ # GIVEN: A importer object and mock webview importer = SongSelectImport(None, None) importer.webview = MagicMock() # WHEN: The reset_webview method is called importer.reset_webview() # THEN: The setUrl function should have been called importer.webview.setUrl.assert_called_with(QtCore.QUrl(LOGIN_PAGE)) @patch('openlp.plugins.songs.lib.songselect.SongSelectImport.set_page') def test_set_home_page(self, mocked_set_page): """ Test that when the home method is called, it attempts to go to the home page """ # GIVEN: A importer object importer = SongSelectImport(None, None) # WHEN: The home method is called importer.set_home_page() # THEN: set_page is called once with the base url mocked_set_page.assert_called_with(BASE_URL) @patch('openlp.plugins.songs.lib.songselect.SongSelectImport._run_javascript') def test_set_page(self, mocked_run_js): """ Test set page runs the correct script """ # GIVEN: A importer object importer = SongSelectImport(None, None) # WHEN: The set login field method is called importer.set_page('my_new_page') # THEN: The javascript called should contain the correct values mocked_run_js.assert_called_with('document.location = "my_new_page"') @patch('openlp.plugins.songs.lib.songselect.SongSelectImport._run_javascript') def test_set_login_fields(self, mocked_run_js): """ Test correct js is sent to set login fields """ # GIVEN: A importer object importer = SongSelectImport(None, None) # WHEN: The set login field method is called importer.set_login_fields('my_username', 'my_password') # THEN: The javascript called should contain the correct values mocked_run_js.assert_called_with(('document.getElementById("EmailAddress").value = "my_username";' 'document.getElementById("Password").value = "my_password";' )) @patch('openlp.plugins.songs.lib.songselect.SongSelectImport._run_javascript') @patch('openlp.plugins.songs.lib.songselect.wait_for') def test_get_page(self, mocked_wait_for, mocked_run_js): """ Test get page sends js requests """ # GIVEN: A importer object importer = SongSelectImport(None, None) mocked_run_js.return_value = True # WHEN: The get page method is called importer.get_page("https://example.com") # THEN: The javascript should be run assert mocked_run_js.call_count == 2, 'Should be called once for request and once for fetch' mocked_wait_for.assert_called_once() def test_get_song_number_from_url(self): """ Test the ccli number can be correctly obtained from a url """ # GIVEN: A importer object importer = SongSelectImport(None, None) # WHEN: The function is called with a valid url result = importer.get_song_number_from_url('https://songselect.ccli.com/Songs/7115744/way-maker') # THEN: The ccli number is returned assert result == '7115744', 'Should have found the ccli number from the url' def test_get_song_number_from_url_nonumber(self): """ Test the ccli number function returns None when no number is found """ # GIVEN: A importer object importer = SongSelectImport(None, None) # WHEN: The function is called with a valid url result = importer.get_song_number_from_url('https://songselect.ccli.com/search/results?SearchText=song+7115744') # THEN: The returned value should be None assert result is None @patch('openlp.plugins.songs.lib.songselect.SongSelectImport.get_song_number_from_url') @patch('openlp.plugins.songs.lib.songselect.SongSelectImport.get_page') def test_get_song_page_raises_exception(self, mocked_get_page, mock_get_num): """ Test that when BeautifulSoup gets a bad song page the get_song() method returns None """ # GIVEN: A mocked callback and an importer object mocked_get_page.side_effect = None mocked_callback = MagicMock() importer = SongSelectImport(None, MagicMock()) # WHEN: get_song is called result = importer.get_song(callback=mocked_callback) # THEN: The callback should have been called once and None should be returned mocked_callback.assert_called_with() assert result is None, 'The get_song() method should have returned None' @patch('openlp.plugins.songs.lib.songselect.SongSelectImport.get_song_number_from_url') @patch('openlp.plugins.songs.lib.songselect.SongSelectImport.get_page') @patch('openlp.plugins.songs.lib.songselect.BeautifulSoup') def test_get_song_lyrics_raise_exception(self, MockedBeautifulSoup, mocked_get_page, mock_get_num): """ Test that when BeautifulSoup gets a bad lyrics page the get_song() method returns None """ # GIVEN: A bunch of mocked out stuff and an importer object song_page = MagicMock(return_value={'href': '/lyricpage'}) MockedBeautifulSoup.side_effect = [song_page, TypeError('Test Error')] mocked_callback = MagicMock() importer = SongSelectImport(None, MagicMock()) # WHEN: get_song is called result = importer.get_song(callback=mocked_callback) # THEN: The callback should have been called twice and None should be returned assert 2 == mocked_callback.call_count, 'The callback should have been called twice' assert result is None, 'The get_song() method should have returned None' @patch('openlp.plugins.songs.lib.songselect.log.exception') @patch('openlp.plugins.songs.lib.songselect.SongSelectImport.get_song_number_from_url') @patch('openlp.plugins.songs.lib.songselect.SongSelectImport.get_page') def test_get_song_no_access(self, mocked_get_page, mock_get_num, mock_log_exception): """ Test that the get_song() handles the case when the user's CCLI account has no access to the song """ fake_song_page = '''

Song Title

''' fake_lyrics_page = '''

Verse 1

verse thing 1
line 2

Verse 2

verse thing 2

''' mocked_get_page.side_effect = [fake_song_page, fake_lyrics_page] mocked_callback = MagicMock() importer = SongSelectImport(None, MagicMock()) # WHEN: get_song is called result = importer.get_song(callback=mocked_callback) # THEN: None should be returned assert result is None, 'The get_song() method should have returned None' @patch('openlp.plugins.songs.lib.songselect.SongSelectImport.get_song_number_from_url') @patch('openlp.plugins.songs.lib.songselect.SongSelectImport.get_page') def test_get_song(self, mocked_get_page, mock_get_num): """ Test that the get_song() method returns the correct song details """ fake_song_page = '''

Song Title

''' fake_lyrics_page = '''

Verse 1

verse thing 1
line 2

Verse 2

verse thing 2

Spoken Words

completely custom verse type

''' mocked_get_page.side_effect = [fake_song_page, fake_lyrics_page] mocked_callback = MagicMock() importer = SongSelectImport(None, MagicMock()) # WHEN: get_song is called result = importer.get_song(callback=mocked_callback) # THEN: The callback should have been called three times and the song should be returned assert 3 == mocked_callback.call_count, 'The callback should have been called twice' assert result is not None, 'The get_song() method should have returned a song dictionary' assert result['title'] == 'Song Title' assert result['authors'] == ['Author 1', 'Author 2'] assert result['copyright'] == 'Copy thing/Copy thing 2' assert result['topics'] == ['theme1', 'theme2'] assert result['ccli_number'] == '1234_cclinumber_5678' assert result['verses'] == [{'label': 'Verse 1', 'lyrics': 'verse thing 1\nline 2'}, {'label': 'Verse 2', 'lyrics': 'verse thing 2'}, {'label': 'Spoken Words', 'lyrics': 'completely custom verse type'}] @patch('openlp.plugins.songs.lib.songselect.clean_song') @patch('openlp.plugins.songs.lib.songselect.Topic') @patch('openlp.plugins.songs.lib.songselect.Author') def test_save_song_new_author(self, MockedAuthor, MockedTopic, mocked_clean_song): """ Test that saving a song with a new author performs the correct actions """ # GIVEN: A song to save, and some mocked out objects song_dict = { 'title': 'Arky Arky', 'authors': ['Public Domain'], 'verses': [ {'label': 'Verse 1', 'lyrics': 'The Lord told Noah: there\'s gonna be a floody, floody'}, {'label': 'Chorus 1', 'lyrics': 'So, rise and shine, and give God the glory, glory'}, {'label': 'Verse 2', 'lyrics': 'The Lord told Noah to build him an arky, arky'} ], 'copyright': 'Public Domain', 'ccli_number': '123456' } MockedAuthor.display_name.__eq__.return_value = False MockedTopic.name.__eq__.return_value = False mocked_db_manager = MagicMock() mocked_db_manager.get_object_filtered.return_value = None importer = SongSelectImport(mocked_db_manager, MagicMock()) # WHEN: The song is saved to the database result = importer.save_song(song_dict) # THEN: The return value should be a Song class and the mocked_db_manager should have been called assert isinstance(result, Song), 'The returned value should be a Song object' mocked_clean_song.assert_called_with(mocked_db_manager, result) assert 2 == mocked_db_manager.save_object.call_count, \ 'The save_object() method should have been called twice' mocked_db_manager.get_object_filtered.assert_called_with(MockedAuthor, False) MockedAuthor.assert_called_with(first_name='Public', last_name='Domain', display_name='Public Domain') assert 1 == len(result.authors_songs), 'There should only be one author' @patch('openlp.plugins.songs.lib.songselect.clean_song') @patch('openlp.plugins.songs.lib.songselect.Author') def test_save_song_existing_author(self, MockedAuthor, mocked_clean_song): """ Test that saving a song with an existing author performs the correct actions """ # GIVEN: A song to save, and some mocked out objects song_dict = { 'title': 'Arky Arky', 'authors': ['Public Domain'], 'verses': [ {'label': 'Verse 1', 'lyrics': 'The Lord told Noah: there\'s gonna be a floody, floody'}, {'label': 'Chorus 1', 'lyrics': 'So, rise and shine, and give God the glory, glory'}, {'label': 'Verse 2', 'lyrics': 'The Lord told Noah to build him an arky, arky'} ], 'copyright': 'Public Domain', 'ccli_number': '123456' } MockedAuthor.display_name.__eq__.return_value = False mocked_db_manager = MagicMock() mocked_db_manager.get_object_filtered.return_value = MagicMock() importer = SongSelectImport(mocked_db_manager, MagicMock()) # WHEN: The song is saved to the database result = importer.save_song(song_dict) # THEN: The return value should be a Song class and the mocked_db_manager should have been called assert isinstance(result, Song), 'The returned value should be a Song object' mocked_clean_song.assert_called_with(mocked_db_manager, result) assert 2 == mocked_db_manager.save_object.call_count, \ 'The save_object() method should have been called twice' mocked_db_manager.get_object_filtered.assert_called_with(MockedAuthor, False) assert 0 == MockedAuthor.call_count, 'A new author should not have been instantiated' assert 1 == len(result.authors_songs), 'There should only be one author' @patch('openlp.plugins.songs.lib.songselect.clean_song') @patch('openlp.plugins.songs.lib.songselect.Author') def test_save_song_unknown_author(self, MockedAuthor, mocked_clean_song): """ Test that saving a song with an author name of only one word performs the correct actions """ # GIVEN: A song to save, and some mocked out objects song_dict = { 'title': 'Arky Arky', 'authors': ['Unknown'], 'verses': [ {'label': 'Verse 1', 'lyrics': 'The Lord told Noah: there\'s gonna be a floody, floody'}, {'label': 'Chorus 1', 'lyrics': 'So, rise and shine, and give God the glory, glory'}, {'label': 'Verse 2', 'lyrics': 'The Lord told Noah to build him an arky, arky'} ], 'copyright': 'Public Domain', 'ccli_number': '123456' } MockedAuthor.display_name.__eq__.return_value = False mocked_db_manager = MagicMock() mocked_db_manager.get_object_filtered.return_value = None importer = SongSelectImport(mocked_db_manager, MagicMock()) # WHEN: The song is saved to the database result = importer.save_song(song_dict) # THEN: The return value should be a Song class and the mocked_db_manager should have been called assert isinstance(result, Song), 'The returned value should be a Song object' mocked_clean_song.assert_called_with(mocked_db_manager, result) assert 2 == mocked_db_manager.save_object.call_count, \ 'The save_object() method should have been called twice' mocked_db_manager.get_object_filtered.assert_called_with(MockedAuthor, False) MockedAuthor.assert_called_with(first_name='Unknown', last_name='', display_name='Unknown') assert 1 == len(result.authors_songs), 'There should only be one author' @patch('openlp.plugins.songs.lib.songselect.clean_song') @patch('openlp.plugins.songs.lib.songselect.Topic') @patch('openlp.plugins.songs.lib.songselect.Author') def test_save_song_topics(self, MockedAuthor, MockedTopic, mocked_clean_song): """ Test that saving a song with topics performs the correct actions Also check that a verse with no number is retitled to 1 """ # GIVEN: A song to save, and some mocked out objects song_dict = { 'title': 'Arky Arky', 'authors': ['Public Domain'], 'verses': [ {'label': 'Verse', 'lyrics': 'The Lord told Noah: there\'s gonna be a floody, floody'}, {'label': 'Chorus 1', 'lyrics': 'So, rise and shine, and give God the glory, glory'}, {'label': 'Verse 2', 'lyrics': 'The Lord told Noah to build him an arky, arky'} ], 'copyright': 'Public Domain', 'ccli_number': '123456', 'topics': ['Old Testement', 'Flood'] } def save_object(b): b.topics = [] MockedAuthor.display_name.__eq__.return_value = False MockedTopic.name.__eq__.return_value = False mocked_db_manager = MagicMock() mocked_db_manager.get_object_filtered.return_value = None mocked_db_manager.save_object = save_object importer = SongSelectImport(mocked_db_manager, MagicMock()) # WHEN: The song is saved to the database result = importer.save_song(song_dict) # THEN: The return value should be a Song class and the topics should have been added assert isinstance(result, Song), 'The returned value should be a Song object' mocked_clean_song.assert_called_with(mocked_db_manager, result) assert MockedTopic.call_count == 2, 'Should have created 2 new topics' MockedTopic.assert_called_with(name='Flood') assert 1 == len(result.authors_songs), 'There should only be one author' class TestSongSelectForm(TestCase, TestMixin): """ Test the :class:`~openlp.plugins.songs.forms.songselectform.SongSelectForm` class """ def setUp(self): """ Some set up for this test suite """ self.setup_application() self.app.setApplicationVersion('0.0') self.app.process_events = lambda: None Registry.create() Registry().register('application', self.app) Registry().register('settings', MagicMock()) self.grid_patcher = patch('openlp.plugins.songs.forms.songselectdialog.QtWidgets.QGridLayout') self.web_patcher = patch('openlp.plugins.songs.forms.songselectdialog.WebEngineView') self.vbox_patcher = patch('openlp.plugins.songs.forms.songselectdialog.QtWidgets.QVBoxLayout') self.grid_patcher.start() self.web_patcher.start() self.vbox_patcher.start() def tearDown(self): self.grid_patcher.stop() self.web_patcher.stop() self.vbox_patcher.stop() def test_create_form(self): """ Test that we can create the SongSelect form """ # GIVEN: The SongSelectForm class and a mocked db manager mocked_plugin = MagicMock() mocked_db_manager = MagicMock() # WHEN: We create an instance ssform = SongSelectForm(None, mocked_plugin, mocked_db_manager) # THEN: The correct properties should have been assigned assert mocked_plugin == ssform.plugin, 'The correct plugin should have been assigned' assert mocked_db_manager == ssform.db_manager, 'The correct db_manager should have been assigned' @patch('openlp.plugins.songs.lib.songselect.SongSelectImport') def test_initialise(self, mocked_ss_import): """ Test the initialise method """ # GIVEN: The SongSelectForm ssform = SongSelectForm(None, MagicMock(), MagicMock()) # WHEN: The initialise method is run ssform.initialise() # THEN: The import object should exist, song var should be None, and the page hooked up assert ssform.song is None assert isinstance(ssform.song_select_importer, SongSelectImport), 'SongSelectImport object should be created' assert ssform.webview.page.call_count == 2, 'Page should be called twice, once for each load handler' @patch('openlp.plugins.songs.forms.songselectform.QtWidgets.QDialog.exec') def test_exec(self, mocked_exec): """ Test the exec method """ # GIVEN: The SongSelectForm ssform = SongSelectForm(None, MagicMock(), MagicMock()) ssform.song_select_importer = MagicMock() ssform.stacked_widget = MagicMock() # WHEN: The initialise method is run ssform.exec() # THEN: Should have reset webview, set stack to 0 and pass on the event ssform.song_select_importer.reset_webview.assert_called_once() ssform.stacked_widget.setCurrentIndex.assert_called_with(0) mocked_exec.assert_called_once() @patch('openlp.plugins.songs.forms.songselectform.QtWidgets.QDialog.done') @patch('openlp.plugins.songs.forms.songselectform.QtWidgets.QProgressDialog') def test_done(self, mocked_prog_dialog, mocked_done): """ Test the done method closes th dialog """ # GIVEN: The SongSelectForm ssform = SongSelectForm(None, MagicMock(), MagicMock()) ssform.song_select_importer = MagicMock() # WHEN: The initialise method is run ssform.done('result') # THEN: Should have passed on the event mocked_done.assert_called_once() def test_page_load_started(self): """ Test the page_load_started method """ # GIVEN: The SongSelectForm ssform = SongSelectForm(None, MagicMock(), MagicMock()) ssform.song_progress_bar = MagicMock() ssform.import_button = MagicMock() ssform.view_button = MagicMock() ssform.back_button = MagicMock() ssform.url_bar = MagicMock() ssform.message_area = MagicMock() # WHEN: The method is run ssform.page_load_started() # THEN: The UI should be set up accordingly (working bar and disabled buttons) ssform.song_progress_bar.setMaximum.assert_called_with(0) ssform.song_progress_bar.setVisible.assert_called_with(True) ssform.import_button.setEnabled.assert_called_with(False) ssform.view_button.setEnabled.assert_called_with(False) ssform.back_button.setEnabled.assert_called_with(False) ssform.message_area.setText.assert_called_with('') def test_page_loaded_login(self): """ Test the page_loaded method for a "Login" page """ # GIVEN: The SongSelectForm and mocked login page ssform = SongSelectForm(None, MagicMock(), MagicMock()) ssform.song_select_importer = MagicMock() ssform.song_select_importer.get_page_type.return_value = Pages.Login ssform.signin_page_loaded = MagicMock() ssform.url_bar = MagicMock() # WHEN: The method is run ssform.page_loaded(True) # THEN: The signin page method should be called ssform.signin_page_loaded.assert_called_once() def test_page_loaded_song(self): """ Test the page_loaded method for a "Song" page """ # GIVEN: The SongSelectForm and mocked song page ssform = SongSelectForm(None, MagicMock(), MagicMock()) ssform.song_select_importer = MagicMock() ssform.song_select_importer.get_page_type.return_value = Pages.Song ssform.song_progress_bar = MagicMock() ssform.url_bar = MagicMock() # WHEN: The method is run ssform.page_loaded(True) # THEN: Progress bar should have been set max 3 (for loading song) ssform.song_progress_bar.setMaximum.assert_called_with(3) ssform.song_progress_bar.setVisible.call_count == 2 @patch('openlp.plugins.songs.forms.songselectform.translate') def test_page_loaded_song_no_access(self, mocked_translate): """ Test the page_loaded method for a "Song" page to which the CCLI account has no access """ # GIVEN: The SongSelectForm and mocked song page and translate function ssform = SongSelectForm(None, MagicMock(), MagicMock()) ssform.song_select_importer = MagicMock() ssform.song_select_importer.get_page_type.return_value = Pages.Song ssform.song_select_importer.get_song.return_value = None ssform.song_progress_bar = MagicMock() ssform.url_bar = MagicMock() ssform.message_area = MagicMock() mocked_translate.return_value = 'some message' # WHEN: The method is run ssform.page_loaded(True) # THEN: The no access message should be shown and the progress bar should be less than 3 ssform.message_area.setText.assert_called_with('some message') ssform.song_progress_bar.setValue.call_count < 4 def test_page_loaded_other(self): """ Test the page_loaded method for an "Other" page """ # GIVEN: The SongSelectForm and mocked other page ssform = SongSelectForm(None, MagicMock(), MagicMock()) ssform.song_select_importer = MagicMock() ssform.song_select_importer.get_page_type.return_value = Pages.Other ssform.song_progress_bar = MagicMock() ssform.back_button = MagicMock() ssform.url_bar = MagicMock() # WHEN: The method is run ssform.page_loaded(True) # THEN: Back button should be available ssform.back_button.setEnabled.assert_called_with(True) def test_signin_page_loaded(self): """ Test that the signin_page_loaded method calls the appropriate method """ # GIVEN: The SongSelectForm and mocked settings ssform = SongSelectForm(None, MagicMock(), MagicMock()) ssform.song_select_importer = MagicMock() ssform.settings.value = MagicMock(side_effect=['user', 'pass']) # WHEN: The method is run ssform.signin_page_loaded() # THEN: Correct values should have been sent from the settings ssform.song_select_importer.set_login_fields.assert_called_with('user', 'pass') @patch('openlp.plugins.songs.forms.songselectdialog.QtWidgets.QListWidgetItem') def test_view_song(self, mock_qtlist): """ Test that the _view_song method does the important stuff """ # GIVEN: The SongSelectForm, mocks and a song ssform = SongSelectForm(None, MagicMock(), MagicMock()) ssform.stacked_widget = MagicMock() ssform.title_edit = MagicMock() ssform.copyright_edit = MagicMock() ssform.ccli_edit = MagicMock() ssform.lyrics_table_widget = MagicMock() ssform.author_list_widget = MagicMock() ssform.song = { 'title': 'Song Title', 'copyright': 'copy thing', 'ccli_number': '1234', 'authors': ['Bob', 'Jo'], 'verses': [{'lyrics': 'hello', 'label': 'Verse 1'}] } # WHEN: The method is run ssform._view_song() # THEN: Page should have changed in the stacked widget and ui should have been updated ssform.stacked_widget.setCurrentIndex.assert_called_with(1) ssform.title_edit.setText.assert_called_with('Song Title') ssform.copyright_edit.setText.assert_called_with('copy thing') ssform.ccli_edit.setText.assert_called_with('1234') assert ssform.lyrics_table_widget.setItem.call_count > 0 assert ssform.author_list_widget.addItem.call_count > 0 @patch('openlp.plugins.songs.forms.songselectform.QtWidgets.QMessageBox.critical') def test_view_song_invalid(self, mock_message): """ Test that the _view_song doesn't mess up when the song doesn't exist """ # GIVEN: The SongSelectForm, mocks and a song ssform = SongSelectForm(None, MagicMock(), MagicMock()) ssform.stacked_widget = MagicMock() ssform.song = None # WHEN: The method is run ssform._view_song() # THEN: Page should not have changed and a warning should show assert ssform.stacked_widget.setCurrentIndex.call_count == 0 mock_message.assert_called_once() def test_on_url_bar_return_pressed(self): """ Test that the on_url_bar_return_pressed method changes the page """ # GIVEN: The SongSelectForm, mocks and a song ssform = SongSelectForm(None, MagicMock(), MagicMock()) ssform.url_bar = MagicMock() ssform.url_bar.text.return_value = "test" ssform.song_select_importer = MagicMock() # WHEN: The method is run ssform.on_url_bar_return_pressed() # THEN: Page should not have changed and a warning should show ssform.song_select_importer.set_page.assert_called_with("test") @patch('openlp.plugins.songs.forms.songselectform.and_') @patch('openlp.plugins.songs.forms.songselectform.Song') @patch('openlp.plugins.songs.forms.songselectform.QtWidgets.QMessageBox.information') @patch('openlp.plugins.songs.forms.songselectform.QtWidgets.QMessageBox.question') @patch('openlp.plugins.songs.forms.songselectform.translate') def test_on_import(self, mocked_trans, mocked_quest, mocked_info, mocked_song, mocked_and): """ Test that when a song is imported and the user clicks the "yes" button, the UI goes back to the previous page """ # GIVEN: A valid SongSelectForm with a mocked out QMessageBox.question() method mocked_trans.side_effect = lambda *args: args[1] mocked_quest.return_value = QtWidgets.QMessageBox.Yes ssform = SongSelectForm(None, MagicMock(), MagicMock()) mocked_song_select_importer = MagicMock() ssform.song_select_importer = mocked_song_select_importer ssform.song = {'ccli_number': '1234'} # WHEN: The import button is clicked, and the user clicks Yes with patch.object(ssform, 'on_back_button_clicked') as mocked_on_back_button_clicked: ssform.on_import_button_clicked() # THEN: The on_back_button_clicked() method should have been called mocked_song_select_importer.save_song.assert_called_with({'ccli_number': '1234'}) mocked_quest.assert_not_called() mocked_info.assert_called_once() mocked_on_back_button_clicked.assert_called_with(True) assert ssform.song is None @patch('openlp.plugins.songs.forms.songselectform.len') @patch('openlp.plugins.songs.forms.songselectform.and_') @patch('openlp.plugins.songs.forms.songselectform.Song') @patch('openlp.plugins.songs.forms.songselectform.QtWidgets.QMessageBox.information') @patch('openlp.plugins.songs.forms.songselectform.QtWidgets.QMessageBox.question') @patch('openlp.plugins.songs.forms.songselectform.translate') def test_on_import_duplicate_yes_clicked(self, mock_trans, mock_q, mocked_info, mock_song, mock_and, mock_len): """ Test that when a duplicate song is imported and the user clicks the "yes" button, the song is imported """ # GIVEN: A valid SongSelectForm with a mocked out QMessageBox.question() method mock_len.return_value = 1 mock_trans.side_effect = lambda *args: args[1] mock_q.return_value = QtWidgets.QMessageBox.Yes ssform = SongSelectForm(None, MagicMock(), MagicMock()) mocked_song_select_importer = MagicMock() ssform.song_select_importer = mocked_song_select_importer ssform.song = {'ccli_number': '1234'} # WHEN: The import button is clicked, and the user clicks Yes with patch.object(ssform, 'on_back_button_clicked') as mocked_on_back_button_clicked: ssform.on_import_button_clicked() # THEN: Should have been saved and the on_back_button_clicked() method should have been called mocked_song_select_importer.save_song.assert_called_with({'ccli_number': '1234'}) mock_q.assert_called_once() mocked_info.assert_called_once() mocked_on_back_button_clicked.assert_called_once() assert ssform.song is None @patch('openlp.plugins.songs.forms.songselectform.len') @patch('openlp.plugins.songs.forms.songselectform.and_') @patch('openlp.plugins.songs.forms.songselectform.Song') @patch('openlp.plugins.songs.forms.songselectform.QtWidgets.QMessageBox.information') @patch('openlp.plugins.songs.forms.songselectform.QtWidgets.QMessageBox.question') @patch('openlp.plugins.songs.forms.songselectform.translate') def test_on_import_duplicate_no_clicked(self, mock_trans, mock_q, mocked_info, mock_song, mock_and, mock_len): """ Test that when a duplicate song is imported and the user clicks the "no" button, the UI exits """ # GIVEN: A valid SongSelectForm with a mocked out QMessageBox.question() method mock_len.return_value = 1 mock_trans.side_effect = lambda *args: args[1] mock_q.return_value = QtWidgets.QMessageBox.No ssform = SongSelectForm(None, MagicMock(), MagicMock()) mocked_song_select_importer = MagicMock() ssform.song_select_importer = mocked_song_select_importer ssform.song = {'ccli_number': '1234'} # WHEN: The import button is clicked, and the user clicks No with patch.object(ssform, 'on_back_button_clicked') as mocked_on_back_button_clicked: ssform.on_import_button_clicked() # THEN: Should have not been saved assert mocked_song_select_importer.save_song.call_count == 0 mock_q.assert_called_once() mocked_info.assert_not_called() mocked_on_back_button_clicked.assert_not_called() assert ssform.song is not None def test_on_back_button_clicked_preview(self): """ Test that when the back button is clicked on preview screen, the stacked widget is set back one page """ # GIVEN: A SongSelect form, stacked widget on page 1 ssform = SongSelectForm(None, MagicMock(), MagicMock()) ssimporter = MagicMock() ssform.song_select_importer = MagicMock() ssform.song_select_importer.set_home_page = ssimporter with patch.object(ssform, 'stacked_widget') as mocked_stacked_widget: mocked_stacked_widget.currentIndex.return_value = 1 # WHEN: The preview back button is clicked ssform.on_back_button_clicked() # THEN: The stacked widget should be set back one page and webpage is NOT put back to the home page mocked_stacked_widget.setCurrentIndex.assert_called_with(0) ssimporter.assert_not_called() def test_on_back_button_clicked_force(self): """ Test that when the back button method is triggered with the force param set, the page should be changed """ # GIVEN: A SongSelect form, stacked widget on page 1 ssform = SongSelectForm(None, MagicMock(), MagicMock()) ssimporter = MagicMock() ssform.song_select_importer = MagicMock() ssform.song_select_importer.set_home_page = ssimporter with patch.object(ssform, 'stacked_widget') as mocked_stacked_widget: mocked_stacked_widget.currentIndex.return_value = 1 # WHEN: The preview back button is clicked with force param ssform.on_back_button_clicked(True) # THEN: The stacked widget should be set back one page and webpage is NOT put back to the home page mocked_stacked_widget.setCurrentIndex.assert_called_with(0) ssimporter.assert_called_once() def test_on_back_button_clicked(self): """ Test that when the back button is clicked, the stacked widget is set to page 0 and set to home page """ # GIVEN: A SongSelect form, stacked widget on page 0 ssform = SongSelectForm(None, MagicMock(), MagicMock()) ssimporter = MagicMock() ssform.song_select_importer = MagicMock() ssform.song_select_importer.set_home_page = ssimporter with patch.object(ssform, 'stacked_widget') as mocked_stacked_widget: mocked_stacked_widget.currentIndex.return_value = 0 # WHEN: The back button is clicked ssform.on_back_button_clicked() # THEN: The stacked widget should be set back one page mocked_stacked_widget.setCurrentIndex.assert_called_with(0) ssimporter.assert_called_with() def test_update_song_progress(self): """ Test the _update_song_progress() method """ # GIVEN: A SongSelect form ssform = SongSelectForm(None, MagicMock(), MagicMock()) # WHEN: _update_song_progress() is called with patch.object(ssform, 'song_progress_bar') as mocked_song_progress_bar: mocked_song_progress_bar.value.return_value = 2 ssform._update_song_progress() # THEN: The song progress bar should be updated mocked_song_progress_bar.setValue.assert_called_with(3) def test_on_view_button_clicked(self): """ Test that view song function is run when the view button is clicked """ # GIVEN: A SongSelect form ssform = SongSelectForm(None, MagicMock(), MagicMock()) # WHEN: A song result is double-clicked with patch.object(ssform, '_view_song') as mocked_view_song: ssform.on_view_button_clicked() # THEN: The song is fetched and shown to the user mocked_view_song.assert_called_with() def test_songselect_file_import(): """ Test that loading a SongSelect file works correctly on various files """ with SongImportTestHelper('CCLIFileImport', 'cclifile') as helper: helper.file_import([TEST_PATH / 'TestSong.bin'], helper.load_external_result_data(TEST_PATH / 'TestSong-bin.json')) helper.file_import([TEST_PATH / 'TestSong.txt'], helper.load_external_result_data(TEST_PATH / 'TestSong-txt.json'))