forked from openlp/openlp
1009 lines
44 KiB
Python
1009 lines
44 KiB
Python
# -*- 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 <https://www.gnu.org/licenses/>. #
|
|
##########################################################################
|
|
|
|
"""
|
|
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 = '''<!DOCTYPE html><html><body>
|
|
<div class="content-title">
|
|
<h1>Song Title</h1>
|
|
<ul class="authors">
|
|
<li><a>Author 1</a></li>
|
|
<li><a>Author 2</a></li>
|
|
</ul>
|
|
</div>
|
|
<div class="song-content-data"><ul><li><strong>1234_cclinumber_5678</strong></li></ul></div>
|
|
<section class="page-section">
|
|
<a title="View song lyrics" data-open="ssUpgradeModal"></a>
|
|
</section>
|
|
<ul class="song-meta-list">
|
|
<li>Themes</li><li><a>theme1</a></li><li><a>theme2</a></li>
|
|
</ul>
|
|
</body></html>
|
|
'''
|
|
fake_lyrics_page = '''<!DOCTYPE html><html><body>
|
|
<div class="song-viewer lyrics">
|
|
<h3>Verse 1</h3>
|
|
<p>verse thing 1<br>line 2</p>
|
|
<h3>Verse 2</h3>
|
|
<p>verse thing 2</p>
|
|
</div>
|
|
<ul class="copyright">
|
|
<li>Copy thing</li><li>Copy thing 2</li>
|
|
</ul>
|
|
</body></html>
|
|
'''
|
|
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 = '''<!DOCTYPE html><html><body>
|
|
<div class="content-title">
|
|
<h1>Song Title</h1>
|
|
<ul class="authors">
|
|
<li><a>Author 1</a></li>
|
|
<li><a>Author 2</a></li>
|
|
</ul>
|
|
</div>
|
|
<div class="song-content-data"><ul><li><strong>1234_cclinumber_5678</strong></li></ul></div>
|
|
<section class="page-section">
|
|
<a title="View song lyrics" href="pretend link"></a>
|
|
</section>
|
|
<ul class="song-meta-list">
|
|
<li>Themes</li><li><a>theme1</a></li><li><a>theme2</a></li>
|
|
</ul>
|
|
</body></html>
|
|
'''
|
|
fake_lyrics_page = '''<!DOCTYPE html><html><body>
|
|
<div class="song-viewer lyrics">
|
|
<h3>Verse 1</h3>
|
|
<p>verse thing 1<br>line 2</p>
|
|
<h3>Verse 2</h3>
|
|
<p>verse thing 2</p>
|
|
<h3>Spoken Words</h3>
|
|
<p>completely custom verse type</p>
|
|
</div>
|
|
<ul class="copyright">
|
|
<li>Copy thing</li><li>Copy thing 2</li>
|
|
</ul>
|
|
</body></html>
|
|
'''
|
|
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.populate.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.populate.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.populate.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.populate.call_count == 2, 'Should have created 2 new topics'
|
|
MockedTopic.populate.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'))
|