# -*- coding: utf-8 -*- ########################################################################## # OpenLP - Open Source Lyrics Projection # # ---------------------------------------------------------------------- # # Copyright (c) 2008-2022 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 . # ########################################################################## """ Package to test the openlp.plugins.songs.forms.songmaintenanceform package. """ import pytest import os from unittest.mock import MagicMock, call, create_autospec, patch, ANY from PyQt5 import QtCore, QtWidgets from openlp.core.common.i18n import UiStrings from openlp.core.common.registry import Registry from openlp.core.lib.db import Manager from openlp.plugins.songs.lib.db import init_schema, Book, Song, SongBookEntry from openlp.plugins.songs.forms.songmaintenanceform import SongMaintenanceForm from sqlalchemy.sql import and_ @pytest.fixture() def form_env(settings): main_window = QtWidgets.QMainWindow() Registry().register('main_window', main_window) mocked_manager = MagicMock() frm = SongMaintenanceForm(mocked_manager) yield frm, mocked_manager del frm del main_window def test_constructor(form_env): """ Test that a SongMaintenanceForm is created successfully """ # GIVEN: A SongMaintenanceForm form = form_env[0] mocked_manager = form_env[1] # WHEN: The form is created # THEN: It should have some of the right components assert form is not None assert form.manager is mocked_manager @patch.object(QtWidgets.QDialog, 'exec') def test_exect(mocked_exec, form_env): """ Test the song maintenance form being executed """ # GIVEN: A song maintenance form form = form_env[0] mocked_exec.return_value = True # WHEN: The song mainetnance form is executed with patch.object(form, 'type_list_widget') as mocked_type_list_widget, \ patch.object(form, 'reset_authors') as mocked_reset_authors, \ patch.object(form, 'reset_topics') as mocked_reset_topics, \ patch.object(form, 'reset_song_books') as mocked_reset_song_books: result = form.exec(from_song_edit=True) # THEN: The correct methods should have been called assert form.from_song_edit is True mocked_type_list_widget.setCurrentRow.assert_called_once_with(0) mocked_reset_authors.assert_called_once_with() mocked_reset_topics.assert_called_once_with() mocked_reset_song_books.assert_called_once_with() mocked_type_list_widget.setFocus.assert_called_once_with() mocked_exec.assert_called_once_with(form) assert result is True def test_get_current_item_id_no_item(form_env): """ Test _get_current_item_id() when there's no item """ # GIVEN: A song maintenance form without a selected item form = form_env[0] mocked_list_widget = MagicMock() mocked_list_widget.currentItem.return_value = None # WHEN: _get_current_item_id() is called result = form._get_current_item_id(mocked_list_widget) # THEN: The result should be -1 mocked_list_widget.currentItem.assert_called_once_with() assert result == -1 def test_get_current_item_id(form_env): """ Test _get_current_item_id() when there's a valid item """ # GIVEN: A song maintenance form with a selected item form = form_env[0] mocked_item = MagicMock() mocked_item.data.return_value = 7 mocked_list_widget = MagicMock() mocked_list_widget.currentItem.return_value = mocked_item # WHEN: _get_current_item_id() is called result = form._get_current_item_id(mocked_list_widget) # THEN: The result should be -1 mocked_list_widget.currentItem.assert_called_once_with() mocked_item.data.assert_called_once_with(QtCore.Qt.UserRole) assert result == 7 @patch('openlp.plugins.songs.forms.songmaintenanceform.critical_error_message_box') def test_delete_item_no_item_id(mocked_critical_error_message_box, form_env): """ Test the _delete_item() method when there is no item selected """ # GIVEN: Some mocked items form = form_env[0] mocked_item_class = MagicMock() mocked_list_widget = MagicMock() mocked_reset_func = MagicMock() dialog_title = 'Delete Item' delete_text = 'Are you sure you want to delete this item?' error_text = 'There was a problem deleting this item' # WHEN: _delete_item() is called with patch.object(form, '_get_current_item_id') as mocked_get_current_item_id: mocked_get_current_item_id.return_value = -1 form._delete_item(mocked_item_class, mocked_list_widget, mocked_reset_func, dialog_title, delete_text, error_text) # THEN: The right things should have been called mocked_get_current_item_id.assert_called_once_with(mocked_list_widget) mocked_critical_error_message_box.assert_called_once_with(dialog_title, UiStrings().NISs) @patch('openlp.plugins.songs.forms.songmaintenanceform.critical_error_message_box') def test_delete_item_invalid_item(mocked_critical_error_message_box, form_env): """ Test the _delete_item() method when the item doesn't exist in the database """ # GIVEN: Some mocked items form = form_env[0] mocked_manager = form_env[1] mocked_manager.get_object.return_value = None mocked_item_class = MagicMock() mocked_list_widget = MagicMock() mocked_reset_func = MagicMock() dialog_title = 'Delete Item' delete_text = 'Are you sure you want to delete this item?' error_text = 'There was a problem deleting this item' # WHEN: _delete_item() is called with patch.object(form, '_get_current_item_id') as mocked_get_current_item_id: mocked_get_current_item_id.return_value = 1 form._delete_item(mocked_item_class, mocked_list_widget, mocked_reset_func, dialog_title, delete_text, error_text) # THEN: The right things should have been called mocked_get_current_item_id.assert_called_once_with(mocked_list_widget) mocked_manager.get_object.assert_called_once_with(mocked_item_class, 1) mocked_critical_error_message_box.assert_called_once_with(dialog_title, error_text) @patch('openlp.plugins.songs.forms.songmaintenanceform.critical_error_message_box') def test_delete_item(mocked_critical_error_message_box, form_env): """ Test the _delete_item() method """ # GIVEN: Some mocked items form = form_env[0] mocked_manager = form_env[1] mocked_item = MagicMock() mocked_item.songs = [] mocked_item.id = 1 mocked_manager.get_object.return_value = mocked_item mocked_critical_error_message_box.return_value = QtWidgets.QMessageBox.Yes mocked_item_class = MagicMock() mocked_list_widget = MagicMock() mocked_reset_func = MagicMock() dialog_title = 'Delete Item' delete_text = 'Are you sure you want to delete this item?' error_text = 'There was a problem deleting this item' # WHEN: _delete_item() is called with patch.object(form, '_get_current_item_id') as mocked_get_current_item_id: mocked_get_current_item_id.return_value = 1 form._delete_item(mocked_item_class, mocked_list_widget, mocked_reset_func, dialog_title, delete_text, error_text) # THEN: The right things should have been called mocked_get_current_item_id.assert_called_once_with(mocked_list_widget) mocked_manager.get_object.assert_called_once_with(mocked_item_class, 1) mocked_critical_error_message_box.assert_called_once_with(dialog_title, delete_text, form, True) mocked_manager.delete_object.assert_called_once_with(mocked_item_class, 1) mocked_reset_func.assert_called_once_with() @patch('openlp.plugins.songs.forms.songmaintenanceform.critical_error_message_box') def test_delete_book_assigned(mocked_critical_error_message_box, form_env): """ Test the _delete_item() method """ # GIVEN: Some mocked items form = form_env[0] mocked_manager = form_env[1] mocked_item = create_autospec(Book, spec_set=True) mocked_item.id = 1 mocked_manager.get_object.return_value = mocked_item mocked_critical_error_message_box.return_value = QtWidgets.QMessageBox.Yes mocked_item_class = MagicMock() mocked_list_widget = MagicMock() mocked_reset_func = MagicMock() dialog_title = 'Delete Book' delete_text = 'Are you sure you want to delete the selected book?' error_text = 'This book cannot be deleted, it is currenty assigned to at least one song.' # WHEN: _delete_item() is called with patch.object(form, '_get_current_item_id') as mocked_get_current_item_id: mocked_get_current_item_id.return_value = 1 form._delete_item(mocked_item_class, mocked_list_widget, mocked_reset_func, dialog_title, delete_text, error_text) # THEN: The right things should have been called mocked_get_current_item_id.assert_called_once_with(mocked_list_widget) mocked_manager.get_object.assert_called_once_with(mocked_item_class, 1) mocked_critical_error_message_box.assert_called_once_with(dialog_title, error_text) mocked_manager.delete_object.assert_not_called() mocked_reset_func.assert_not_called() @patch('openlp.plugins.songs.forms.songmaintenanceform.QtWidgets.QListWidgetItem') @patch('openlp.plugins.songs.forms.songmaintenanceform.Author') def test_reset_authors(MockedAuthor, MockedQListWidgetItem, form_env): """ Test the reset_authors() method """ # GIVEN: A mocked authors_list_widget and a few other mocks form = form_env[0] mocked_manager = form_env[1] mocked_author1 = MagicMock() mocked_author1.display_name = 'John Newton' mocked_author1.id = 1 mocked_author2 = MagicMock() mocked_author2.display_name = '' mocked_author2.first_name = 'John' mocked_author2.last_name = 'Wesley' mocked_author2.id = 2 mocked_authors = [mocked_author1, mocked_author2] mocked_author_item1 = MagicMock() mocked_author_item2 = MagicMock() MockedQListWidgetItem.side_effect = [mocked_author_item1, mocked_author_item2] MockedAuthor.display_name = None mocked_manager.get_all_objects.return_value = mocked_authors # WHEN: reset_authors() is called with patch.object(form, 'authors_list_widget') as mocked_authors_list_widget: form.reset_authors() # THEN: The authors list should be reset expected_widget_item_calls = [call('John Wesley'), call('John Newton')] mocked_authors_list_widget.clear.assert_called_once_with() mocked_manager.get_all_objects.assert_called_once_with(MockedAuthor) # Do not care which order items are called since the order is different on macos vs others MockedQListWidgetItem.assert_has_calls(expected_widget_item_calls, any_order=True) mocked_author_item1.setData.assert_called_once_with(QtCore.Qt.UserRole, ANY) mocked_author_item2.setData.assert_called_once_with(QtCore.Qt.UserRole, ANY) mocked_authors_list_widget.addItem.assert_has_calls([ call(mocked_author_item1), call(mocked_author_item2)]) @patch('openlp.plugins.songs.forms.songmaintenanceform.QtWidgets.QListWidgetItem') @patch('openlp.plugins.songs.forms.songmaintenanceform.Topic') def test_reset_topics(MockedTopic, MockedQListWidgetItem, form_env): """ Test the reset_topics() method """ # GIVEN: Some mocked out objects and methods form = form_env[0] mocked_manager = form_env[1] MockedTopic.name = 'Grace' mocked_topic = MagicMock() mocked_topic.id = 1 mocked_topic.name = 'Grace' mocked_manager.get_all_objects.return_value = [mocked_topic] mocked_topic_item = MagicMock() MockedQListWidgetItem.return_value = mocked_topic_item # WHEN: reset_topics() is called with patch.object(form, 'topics_list_widget') as mocked_topic_list_widget: form.reset_topics() # THEN: The topics list should be reset correctly mocked_topic_list_widget.clear.assert_called_once_with() mocked_manager.get_all_objects.assert_called_once_with(MockedTopic) MockedQListWidgetItem.assert_called_once_with('Grace') mocked_topic_item.setData.assert_called_once_with(QtCore.Qt.UserRole, 1) mocked_topic_list_widget.addItem.assert_called_once_with(mocked_topic_item) @patch('openlp.plugins.songs.forms.songmaintenanceform.QtWidgets.QListWidgetItem') @patch('openlp.plugins.songs.forms.songmaintenanceform.Book') def test_reset_song_books(MockedBook, MockedQListWidgetItem, form_env): """ Test the reset_song_books() method """ # GIVEN: Some mocked out objects and methods form = form_env[0] mocked_manager = form_env[1] MockedBook.name = 'Hymnal' mocked_song_book = MagicMock() mocked_song_book.id = 1 mocked_song_book.name = 'Hymnal' mocked_song_book.publisher = 'Hymns and Psalms, Inc.' mocked_manager.get_all_objects.return_value = [mocked_song_book] mocked_song_book_item = MagicMock() MockedQListWidgetItem.return_value = mocked_song_book_item # WHEN: reset_song_books() is called with patch.object(form, 'song_books_list_widget') as mocked_song_book_list_widget: form.reset_song_books() # THEN: The song_books list should be reset correctly mocked_song_book_list_widget.clear.assert_called_once_with() mocked_manager.get_all_objects.assert_called_once_with(MockedBook) MockedQListWidgetItem.assert_called_once_with('Hymnal (Hymns and Psalms, Inc.)') mocked_song_book_item.setData.assert_called_once_with(QtCore.Qt.UserRole, 1) mocked_song_book_list_widget.addItem.assert_called_once_with(mocked_song_book_item) @patch('openlp.plugins.songs.forms.songmaintenanceform.and_') @patch('openlp.plugins.songs.forms.songmaintenanceform.Author') def test_check_author_exists(MockedAuthor, mocked_and, form_env): """ Test the check_author_exists() method """ # GIVEN: A bunch of mocked out stuff form = form_env[0] mocked_manager = form_env[1] MockedAuthor.first_name = 'John' MockedAuthor.last_name = 'Newton' MockedAuthor.display_name = 'John Newton' mocked_new_author = MagicMock() mocked_new_author.first_name = 'John' mocked_new_author.last_name = 'Newton' mocked_new_author.display_name = 'John Newton' mocked_and.return_value = True mocked_authors = [MagicMock(), MagicMock()] mocked_manager.get_all_objects.return_value = mocked_authors # WHEN: check_author_exists() is called with patch.object(form, '_check_object_exists') as mocked_check_object_exists: mocked_check_object_exists.return_value = True result = form.check_author_exists(mocked_new_author, edit=True) # THEN: The correct result is returned mocked_and.assert_called_once_with(True, True, True) mocked_manager.get_all_objects.assert_called_once_with(MockedAuthor, True) mocked_check_object_exists.assert_called_once_with(mocked_authors, mocked_new_author, True) assert result is True @patch('openlp.plugins.songs.forms.songmaintenanceform.Topic') def test_check_topic_exists(MockedTopic, form_env): """ Test the check_topic_exists() method """ # GIVEN: Some mocked stuff form = form_env[0] mocked_manager = form_env[1] MockedTopic.name = 'Grace' mocked_new_topic = MagicMock() mocked_new_topic.name = 'Grace' mocked_topics = [MagicMock(), MagicMock()] mocked_manager.get_all_objects.return_value = mocked_topics # WHEN: check_topic_exists() is run with patch.object(form, '_check_object_exists') as mocked_check_object_exists: mocked_check_object_exists.return_value = True result = form.check_topic_exists(mocked_new_topic, True) # THEN: The correct things should have been called mocked_manager.get_all_objects.assert_called_once_with(MockedTopic, True) mocked_check_object_exists.assert_called_once_with(mocked_topics, mocked_new_topic, True) assert result is True @patch('openlp.plugins.songs.forms.songmaintenanceform.and_') @patch('openlp.plugins.songs.forms.songmaintenanceform.Book') def test_check_song_book_exists(MockedBook, mocked_and, form_env): """ Test the check_song_book_exists() method """ # GIVEN: Some mocked stuff form = form_env[0] mocked_manager = form_env[1] MockedBook.name = 'Hymns' MockedBook.publisher = 'Christian Songs' mocked_new_book = MagicMock() mocked_new_book.name = 'Hymns' mocked_new_book.publisher = 'Christian Songs' mocked_and.return_value = True mocked_books = [MagicMock(), MagicMock()] mocked_manager.get_all_objects.return_value = mocked_books # WHEN: check_book_exists() is run with patch.object(form, '_check_object_exists') as mocked_check_object_exists: mocked_check_object_exists.return_value = True result = form.check_song_book_exists(mocked_new_book, True) # THEN: The correct things should have been called mocked_and.assert_called_once_with(True, True) mocked_manager.get_all_objects.assert_called_once_with(MockedBook, True) mocked_check_object_exists.assert_called_once_with(mocked_books, mocked_new_book, True) assert result is True def test_check_object_exists_no_existing_objects(form_env): """ Test the _check_object_exists() method when there are no existing objects """ # GIVEN: A SongMaintenanceForm instance # WHEN: _check_object_exists() is called without existing objects form = form_env[0] result = form._check_object_exists([], None, False) # THEN: The result should be True assert result is True def test_check_object_exists_without_edit(form_env): """ Test the _check_object_exists() method when edit is false """ # GIVEN: A SongMaintenanceForm instance # WHEN: _check_object_exists() is called with edit set to false form = form_env[0] result = form._check_object_exists([MagicMock()], None, False) # THEN: The result should be False assert result is False def test_check_object_exists_not_found(form_env): """ Test the _check_object_exists() method when the object is not found """ # GIVEN: A SongMaintenanceForm instance and some mocked objects form = form_env[0] mocked_existing_objects = [MagicMock(id=1)] mocked_new_object = MagicMock(id=2) # WHEN: _check_object_exists() is called with edit set to false result = form._check_object_exists(mocked_existing_objects, mocked_new_object, True) # THEN: The result should be False assert result is False def test_check_object_exists(form_env): """ Test the _check_object_exists() method """ # GIVEN: A SongMaintenanceForm instance and some mocked objects form = form_env[0] mocked_existing_objects = [MagicMock(id=1)] mocked_new_object = MagicMock(id=1) # WHEN: _check_object_exists() is called with edit set to false result = form._check_object_exists(mocked_existing_objects, mocked_new_object, True) # THEN: The result should be False assert result is True def test_merge_song_books(registry, settings, temp_folder): """ Test the functionality of merging 2 song books. """ # GIVEN a test database populated with test data, and a song maintenance form db_tmp_path = os.path.join(temp_folder, 'test-songs-2.9.2.sqlite') manager = Manager('songs', init_schema, db_file_path=db_tmp_path) # create 2 song books, both with the same name book1 = Book() book1.name = 'test book1' book1.publisher = '' manager.save_object(book1) book2 = Book() book2.name = 'test book1' book2.publisher = '' manager.save_object(book2) # create 3 songs, all with same search_title song1 = Song() song1.title = 'test song1' song1.lyrics = 'lyrics1' song1.search_title = 'test song' song1.search_lyrics = 'lyrics1' manager.save_object(song1) song2 = Song() song2.title = 'test song2' song2.lyrics = 'lyrics2' song2.search_title = 'test song' song2.search_lyrics = 'lyrics2' manager.save_object(song2) song3 = Song() song3.title = 'test song3' song3.lyrics = 'lyrics3' song3.search_title = 'test song' song3.search_lyrics = 'lyrics3' manager.save_object(song3) # associate songs with song books song1.add_songbook_entry(book1, '10') song2.add_songbook_entry(book1, '20') song2.add_songbook_entry(book2, '30') song3.add_songbook_entry(book1, '40') song3.add_songbook_entry(book2, '') song_maintenance_form = SongMaintenanceForm(manager) # WHEN the song books are merged, getting rid of book1 song_maintenance_form.merge_song_books(book1) # THEN the database should reflect correctly the merge songs = manager.get_all_objects(Song, Song.search_title == 'test song') songbook1_entries = manager.get_all_objects(SongBookEntry, SongBookEntry.songbook_id == book1.id) songbook2_entries = manager.get_all_objects(SongBookEntry, SongBookEntry.songbook_id == book2.id) song1_book2_entry = manager.get_all_objects(SongBookEntry, and_(SongBookEntry.songbook_id == book2.id, SongBookEntry.song_id == song1.id)) song2_book2_entry = manager.get_all_objects(SongBookEntry, and_(SongBookEntry.songbook_id == book2.id, SongBookEntry.song_id == song2.id)) song3_book2_entry = manager.get_all_objects(SongBookEntry, and_(SongBookEntry.songbook_id == book2.id, SongBookEntry.song_id == song3.id)) books = manager.get_all_objects(Book, Book.name == 'test book1') # song records should not be deleted assert len(songs) == 3 # the old book should have been deleted, with its songs_songbooks records assert len(books) == 1 assert len(songbook1_entries) == 0 # each of the 3 songs should be associated with book2 assert len(songbook2_entries) == 3 # the individual SongBookEntry records should be correct assert len(song1_book2_entry) == 1 assert song1_book2_entry[0].entry == '10' assert len(song2_book2_entry) == 1 # entry field should not be overridden, as it was set previously for book2 assert song2_book2_entry[0].entry == '30' assert len(song3_book2_entry) == 1 # entry field should be overridden, as it was not set previously assert song3_book2_entry[0].entry == '40'