songs2slides

A tool that automatically finds song lyrics and creates lyric slideshows
git clone https://git.ashermorgan.net/songs2slides/
Log | Files | Refs | README

commit 29fe8b1b30203b8046648f560f9a3ff4dc9d7306
parent 2865cd9dc7cfa8b2d5023c48e2436df57ff8789d
Author: Asher Morgan <59518073+ashermorgan@users.noreply.github.com>
Date:   Sun, 14 Apr 2024 11:34:47 -0700

Migrate from unittest to pytest

Diffstat:
M.github/workflows/main.yml | 4++--
Mrequirements.txt | 2++
Mtests/test_core.py | 762+++++++++++++++++++++++++++++++++++++++----------------------------------------
Mtests/test_routes.py | 505++++++++++++++++++++++++++++++++++++++++---------------------------------------
4 files changed, 639 insertions(+), 634 deletions(-)

diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml @@ -19,5 +19,5 @@ jobs: - name: Install dependencies run: pip install -r requirements.txt - - name: Run tests with unittest - run: python -m unittest + - name: Run tests with pytest + run: python -m pytest diff --git a/requirements.txt b/requirements.txt @@ -2,3 +2,5 @@ flask python-dotenv python-pptx requests +pytest +pytest-mock diff --git a/tests/test_core.py b/tests/test_core.py @@ -1,392 +1,390 @@ -import unittest -from unittest.mock import patch, call +import pytest from songs2slides import core -class TestCore(unittest.TestCase): - def test_filter_lyrics_inline(self): - # Declare raw lyrics and expected cleaned lyrics - lyrics = 'A[remove]B\nC(remove)D' - expected = 'AB\nCD' - - # Clean lyrics - result = core.filter_lyrics(lyrics) - - # Assert slides are correct - self.assertEqual(result, expected) - - def test_filter_lyrics_whole_lines(self): - # Declare raw lyrics and expected cleaned lyrics - lyrics = 'A\n[remove]\nB\n(remove)\nC' - expected = 'A\nB\nC' - - # Clean lyrics - result = core.filter_lyrics(lyrics) - - # Assert slides are correct - self.assertEqual(result, expected) - - def test_filter_lyrics_multiple_lines(self): - # Declare raw lyrics and expected cleaned lyrics - lyrics = 'A\n[re\nmove]\nB\n(re\nmove)\nC' - expected = 'A\nB\nC' - - # Clean lyrics - result = core.filter_lyrics(lyrics) - - # Assert slides are correct - self.assertEqual(result, expected) - - def test_filter_lyrics_blank_lines(self): - # Declare raw lyrics and expected cleaned lyrics - lyrics = 'A\n[remove]\n\n(remove)\nB' - expected = 'A\n\nB' - - # Clean lyrics - result = core.filter_lyrics(lyrics) - - # Assert slides are correct - self.assertEqual(result, expected) - - def test_filter_lyrics_all(self): - # Declare raw lyrics and expected cleaned lyrics - lyrics = 'A[remove]B\n[remove]\n\nC(remove)D\n(re\nmove)' - expected = 'AB\n\nCD' - - # Clean lyrics - result = core.filter_lyrics(lyrics) - - # Assert slides are correct - self.assertEqual(result, expected) - - def test_filter_lyrics_empty_string(self): - # Clean lyrics - result = core.filter_lyrics('') - - # Assert slides are correct - self.assertEqual(result, '') - - def test_get_song_data_success(self): - with patch('songs2slides.core.os.getenv') as mocked_env, \ - patch('songs2slides.core.requests.get') as mocked_get, \ - patch('songs2slides.core.filter_lyrics') as mocked_clean: - - # Mock os.getenv, requests.get, and core.filter_lyrics - mocked_env.side_effect = [ - 'api://lyrics/{artist}/{title}', - 'Bearer secrettoken' - ] - mocked_get.return_value.json.return_value = { - 'lyrics': 'raw', - 'title': 'Foo', - 'artist': 'Bar', - } - mocked_get.return_value.status_code = 200 - mocked_clean.return_value = 'clean' - - # Get song data - song_data = core.get_song_data('foo', 'bar') - - # Assert mocked methods were used correctly - mocked_env.assert_has_calls([ - call('API_URL'), - call('API_AUTH', None) - ]) - mocked_get.assert_called_with('api://lyrics/bar/foo', headers={ - 'Authorization': 'Bearer secrettoken' - }) - mocked_clean.assert_called_with('raw') - - # Assert song data is correct - self.assertEqual(song_data.title, 'Foo') - self.assertEqual(song_data.artist, 'Bar') - self.assertEqual(song_data.lyrics, 'clean') - - def test_get_song_data_no_auth_header(self): - with patch('songs2slides.core.os.getenv') as mocked_env, \ - patch('songs2slides.core.requests.get') as mocked_get, \ - patch('songs2slides.core.filter_lyrics') as mocked_clean: - - # Mock os.getenv, requests.get, and core.filter_lyrics - mocked_env.side_effect = [ - 'api://lyrics/{artist}/{title}', - None, - ] - mocked_get.return_value.json.return_value = { - 'lyrics': 'raw', - 'title': 'Foo', - 'artist': 'Bar', - } - mocked_get.return_value.status_code = 200 - mocked_clean.return_value = 'clean' - - # Get song data - song_data = core.get_song_data('foo', 'bar') - - # Assert mocked methods were used correctly - mocked_env.assert_has_calls([ - call('API_URL'), - call('API_AUTH', None) - ]) - mocked_get.assert_called_with('api://lyrics/bar/foo', headers={}) - mocked_clean.assert_called_with('raw') - - # Assert song data is correct - self.assertEqual(song_data.title, 'Foo') - self.assertEqual(song_data.artist, 'Bar') - self.assertEqual(song_data.lyrics, 'clean') - - def test_get_song_data_no_url(self): - with patch('songs2slides.core.os.getenv') as mocked_env, \ - patch('songs2slides.core.requests.get') as mocked_get: - - # Mock os.getenv and requests.get - mocked_env.return_value = None - mocked_get.return_value.text = b'{}' - mocked_get.return_value.status_code = 200 - - # Try to get song data - with self.assertRaises(Exception): - song_data = core.get_song_data('foo', 'bar') - - # Assert request was not called - mocked_get.assert_not_called() - - def test_get_song_data_not_found(self): - with patch('songs2slides.core.os.getenv') as mocked_env, \ - patch('songs2slides.core.requests.get') as mocked_get: - - # Mock os.getenv and requests.get - mocked_env.side_effect = [ - 'api://lyrics/{artist}/{title}', - 'Bearer secrettoken' - ] - mocked_get.return_value.text = b'{}' - mocked_get.return_value.status_code = 200 - - # Try to get song data - with self.assertRaises(Exception): - song_data = core.get_song_data('foo', 'bar') - - # Assert request was called - mocked_get.assert_called_with('api://lyrics/bar/foo', headers={ - 'Authorization': 'Bearer secrettoken' - }) - - def test_parse_song_lyrics_basic(self): - # Declare song data and expected slides - lyrics = 'A\nB\nC\nD\nE\nF\n\nG\nH' - expected = ['A\nB\nC\nD', 'E\nF', 'G\nH'] - - # Get slide content - result = core.parse_song_lyrics(lyrics, 4) - - # Assert slides are correct - self.assertEqual(result, expected) - - def test_parse_song_lyrics_3_lines_per_slide(self): - # Declare song data and expected slides - lyrics = 'A\nB\nC\nD\nE\nF\n\nG\nH' - expected = ['A\nB\nC', 'D\nE\nF', 'G\nH'] - - # Get slide content - result = core.parse_song_lyrics(lyrics, 3) - - # Assert slides are correct - self.assertEqual(result, expected) - - def test_parse_song_lyrics_empty_string(self): - # Declare song data and expected slides - lyrics = '' - expected = [] - - # Get slide content - result = core.parse_song_lyrics(lyrics, 4) - - # Assert slides are correct - self.assertEqual(result, expected) - - def test_parse_song_lyrics_one_line(self): - # Declare song data and expected slides - lyrics = 'A' - expected = ['A'] - - # Get slide content - result = core.parse_song_lyrics(lyrics, 4) - - # Assert slides are correct - self.assertEqual(result, expected) - - def test_parse_song_lyrics_one_slide(self): - # Declare song data and expected slides - lyrics = 'A\nB\nC\nD' - expected = ['A\nB\nC\nD'] +def test_filter_lyrics_inline(): + # Declare raw lyrics and expected cleaned lyrics + lyrics = 'A[remove]B\nC(remove)D' + expected = 'AB\nCD' + + # Clean lyrics + result = core.filter_lyrics(lyrics) + + # Assert slides are correct + assert result == expected + +def test_filter_lyrics_whole_lines(): + # Declare raw lyrics and expected cleaned lyrics + lyrics = 'A\n[remove]\nB\n(remove)\nC' + expected = 'A\nB\nC' + + # Clean lyrics + result = core.filter_lyrics(lyrics) + + # Assert slides are correct + assert result == expected + +def test_filter_lyrics_multiple_lines(): + # Declare raw lyrics and expected cleaned lyrics + lyrics = 'A\n[re\nmove]\nB\n(re\nmove)\nC' + expected = 'A\nB\nC' + + # Clean lyrics + result = core.filter_lyrics(lyrics) + + # Assert slides are correct + assert result == expected + +def test_filter_lyrics_blank_lines(): + # Declare raw lyrics and expected cleaned lyrics + lyrics = 'A\n[remove]\n\n(remove)\nB' + expected = 'A\n\nB' + + # Clean lyrics + result = core.filter_lyrics(lyrics) + + # Assert slides are correct + assert result == expected + +def test_filter_lyrics_all(): + # Declare raw lyrics and expected cleaned lyrics + lyrics = 'A[remove]B\n[remove]\n\nC(remove)D\n(re\nmove)' + expected = 'AB\n\nCD' + + # Clean lyrics + result = core.filter_lyrics(lyrics) + + # Assert slides are correct + assert result == expected + +def test_filter_lyrics_empty_string(): + # Clean lyrics + result = core.filter_lyrics('') + + # Assert slides are correct + assert result == '' + +def test_get_song_data_success(mocker): + # Mock os.getenv, requests.get, and filter_lyrics + mocker.patch('songs2slides.core.os.getenv') + mocker.patch('songs2slides.core.requests.get') + mocker.patch('songs2slides.core.filter_lyrics') + core.os.getenv.side_effect = [ + 'api://lyrics/{artist}/{title}', + 'Bearer secrettoken' + ] + core.requests.get.return_value.json.return_value = { + 'lyrics': 'raw', + 'title': 'Foo', + 'artist': 'Bar', + } + core.requests.get.return_value.status_code = 200 + core.filter_lyrics.return_value = 'clean' + + # Get song data + song_data = core.get_song_data('foo', 'bar') + + # Assert mocked methods were used correctly + core.os.getenv.assert_has_calls([ + mocker.call('API_URL'), + mocker.call('API_AUTH', None) + ]) + core.requests.get.assert_called_with('api://lyrics/bar/foo', headers={ + 'Authorization': 'Bearer secrettoken' + }) + core.filter_lyrics.assert_called_with('raw') + + # Assert song data is correct + assert song_data.title == 'Foo' + assert song_data.artist == 'Bar' + assert song_data.lyrics == 'clean' + +def test_get_song_data_no_auth_header(mocker): + # Mock os.getenv, requests.get, and filter_lyrics + mocker.patch('songs2slides.core.os.getenv') + mocker.patch('songs2slides.core.requests.get') + mocker.patch('songs2slides.core.filter_lyrics') + core.os.getenv.side_effect = [ + 'api://lyrics/{artist}/{title}', + None, + ] + core.requests.get.return_value.json.return_value = { + 'lyrics': 'raw', + 'title': 'Foo', + 'artist': 'Bar', + } + core.requests.get.return_value.status_code = 200 + core.filter_lyrics.return_value = 'clean' + + # Get song data + song_data = core.get_song_data('foo', 'bar') + + # Assert mocked methods were used correctly + core.os.getenv.assert_has_calls([ + mocker.call('API_URL'), + mocker.call('API_AUTH', None) + ]) + core.requests.get.assert_called_with('api://lyrics/bar/foo', headers={}) + core.filter_lyrics.assert_called_with('raw') + + # Assert song data is correct + assert song_data.title == 'Foo' + assert song_data.artist == 'Bar' + assert song_data.lyrics == 'clean' + +def test_get_song_data_no_url(mocker): + # Mock os.getenv and requests.get + mocker.patch('songs2slides.core.os.getenv') + mocker.patch('songs2slides.core.requests.get') + core.os.getenv.return_value = None + core.requests.get.return_value.text = b'{}' + core.requests.get.return_value.status_code = 200 + + # Try to get song data + with pytest.raises(Exception): + song_data = core.get_song_data('foo', 'bar') + + # Assert request was not called + core.requests.get.assert_not_called() + +def test_get_song_data_not_found(mocker): + # Mock os.getenv and requests.get + mocker.patch('songs2slides.core.os.getenv') + mocker.patch('songs2slides.core.requests.get') + core.os.getenv.side_effect = [ + 'api://lyrics/{artist}/{title}', + 'Bearer secrettoken' + ] + core.requests.get.return_value.text = b'{}' + core.requests.get.return_value.status_code = 200 + + # Try to get song data + with pytest.raises(Exception): + song_data = core.get_song_data('foo', 'bar') + + # Assert request was called + core.requests.get.assert_called_with('api://lyrics/bar/foo', headers={ + 'Authorization': 'Bearer secrettoken' + }) + +def test_parse_song_lyrics_basic(): + # Declare song data and expected slides + lyrics = 'A\nB\nC\nD\nE\nF\n\nG\nH' + expected = ['A\nB\nC\nD', 'E\nF', 'G\nH'] + + # Get slide content + result = core.parse_song_lyrics(lyrics, 4) + + # Assert slides are correct + assert result == expected + +def test_parse_song_lyrics_3_lines_per_slide(): + # Declare song data and expected slides + lyrics = 'A\nB\nC\nD\nE\nF\n\nG\nH' + expected = ['A\nB\nC', 'D\nE\nF', 'G\nH'] + + # Get slide content + result = core.parse_song_lyrics(lyrics, 3) + + # Assert slides are correct + assert result == expected + +def test_parse_song_lyrics_empty_string(): + # Declare song data and expected slides + lyrics = '' + expected = [] + + # Get slide content + result = core.parse_song_lyrics(lyrics, 4) + + # Assert slides are correct + assert result == expected + +def test_parse_song_lyrics_one_line(): + # Declare song data and expected slides + lyrics = 'A' + expected = ['A'] + + # Get slide content + result = core.parse_song_lyrics(lyrics, 4) + + # Assert slides are correct + assert result == expected + +def test_parse_song_lyrics_one_slide(): + # Declare song data and expected slides + lyrics = 'A\nB\nC\nD' + expected = ['A\nB\nC\nD'] + + # Get slide content + result = core.parse_song_lyrics(lyrics, 4) + + # Assert slides are correct + assert result == expected + +def test_parse_song_lyrics_tripple_newlines(): + # Declare song data and expected slides + lyrics = 'A\nB\n\n\nC\nD' + expected = ['A\nB', '', 'C\nD'] + + # Get slide content + result = core.parse_song_lyrics(lyrics, 4) + + # Assert slides are correct + assert result == expected + +def test_parse_song_lyrics_extra_whitespace(): + # Declare song data and expected slides + lyrics = ' A\n B \nC D\nE ' + expected = ['A\nB\nC D\nE'] - # Get slide content - result = core.parse_song_lyrics(lyrics, 4) - - # Assert slides are correct - self.assertEqual(result, expected) + # Get slide content + result = core.parse_song_lyrics(lyrics, 4) + + # Assert slides are correct + assert result == expected - def test_parse_song_lyrics_tripple_newlines(self): - # Declare song data and expected slides - lyrics = 'A\nB\n\n\nC\nD' - expected = ['A\nB', '', 'C\nD'] +def test_parse_song_lyrics_extra_newlines(): + # Declare song data and expected slides + lyrics = '\n\n\nA\n\n\n\n\nB\n\n\n' + expected = ['A', '', 'B'] + + # Get slide content + result = core.parse_song_lyrics(lyrics, 4) - # Get slide content - result = core.parse_song_lyrics(lyrics, 4) + # Assert slides are correct + assert result == expected - # Assert slides are correct - self.assertEqual(result, expected) +def test_assemble_slides_calls_parse_song_lyrics(mocker): + # Mock parse_song_lyrics + mocker.patch('songs2slides.core.parse_song_lyrics') + core.parse_song_lyrics.side_effect = [['aaa'], ['b1', 'b2']] - def test_parse_song_lyrics_extra_whitespace(self): - # Declare song data and expected slides - lyrics = ' A\n B \nC D\nE ' - expected = ['A\nB\nC D\nE'] - - # Get slide content - result = core.parse_song_lyrics(lyrics, 4) - - # Assert slides are correct - self.assertEqual(result, expected) - - def test_parse_song_lyrics_extra_newlines(self): - # Declare song data and expected slides - lyrics = '\n\n\nA\n\n\n\n\nB\n\n\n' - expected = ['A', '', 'B'] - - # Get slide content - result = core.parse_song_lyrics(lyrics, 4) - - # Assert slides are correct - self.assertEqual(result, expected) - - def test_assemble_slides_calls_parse_song_lyrics(self): - with patch('songs2slides.core.parse_song_lyrics') as mocked_parse: - - # Mock parse_song_lyrics - mocked_parse.side_effect = [['aaa'], ['b1', 'b2']] - - # Declare song data and expected slides - songs = [ - core.SongData('T1', 'A1', 'l1'), - core.SongData('T2', 'A3', 'l2'), - ] - expected = ['T1', 'aaa', '', 'T2', 'b1', 'b2'] - - # Get slides - slides = core.assemble_slides(songs) - - # Assert slides are correct - self.assertEqual(slides, expected) - - # Assert parse_song_lyrics called - mocked_parse.assert_has_calls([call('L1', 4), call('L2', 4)]) - - def test_assemble_slides_default(self): - # Declare song data and expected slides - songs = [ - core.SongData('t1', 'a1', 'l1\nl2\nl3\nl4\nl5'), - core.SongData('T2', 'A3', 'L6\nL7\n\nL8\n\n\nL9'), - ] - expected = [ - 'T1', 'L1\nL2\nL3\nL4', 'L5', '', - 'T2', 'L6\nL7', 'L8', '', 'L9', - ] - - # Get slides - slides = core.assemble_slides(songs) - - # Assert slides are correct - self.assertEqual(slides, expected) - - def test_assemble_slides_custom_lines_per_slide(self): - with patch('songs2slides.core.parse_song_lyrics') as mocked_parse: - - # Mock parse_song_lyrics - mocked_parse.side_effect = [['aaa'], ['b1', 'b2']] - - # Declare song data and expected slides - songs = [ - core.SongData('T1', 'A1', 'l1'), - core.SongData('T2', 'A3', 'l2'), - ] - expected = ['T1', 'aaa', '', 'T2', 'b1', 'b2'] - - # Get slides - slides = core.assemble_slides(songs, lines_per_slide = 3) - - # Assert slides are correct - self.assertEqual(slides, expected) - - # Assert parse_song_lyrics called correctly - mocked_parse.assert_has_calls([call('L1', 3), call('L2', 3)]) - - def test_assemble_slides_no_title_slides(self): - # Declare song data and expected slides - songs = [ - core.SongData('t1', 'a1', 'l1\nl2\nl3\nl4\nl5'), - core.SongData('T2', 'A3', 'L6\nL7\n\nL8\n\n\nL9'), - ] - expected = [ - 'L1\nL2\nL3\nL4', 'L5', '', - 'L6\nL7', 'L8', '', 'L9', - ] - - # Get slides - slides = core.assemble_slides(songs, title_slides = False) - - # Assert slides are correct - self.assertEqual(slides, expected) - - def test_assemble_slides_no_blank_slides(self): - # Declare song data and expected slides - songs = [ - core.SongData('t1', 'a1', 'l1\nl2\nl3\nl4\nl5'), - core.SongData('T2', 'A3', 'L6\nL7\n\nL8\n\n\nL9'), - ] - expected = [ - 'T1', 'L1\nL2\nL3\nL4', 'L5', - 'T2', 'L6\nL7', 'L8', '', 'L9', - ] - - # Get slides - slides = core.assemble_slides(songs, blank_slides = False) - - # Assert slides are correct - self.assertEqual(slides, expected) - - def test_assemble_slides_no_extra_slides(self): - # Declare song data and expected slides - songs = [ - core.SongData('t1', 'a1', 'l1\nl2\nl3\nl4\nl5'), - core.SongData('T2', 'A3', 'L6\nL7\n\nL8\n\n\nL9'), + # Declare song data and expected slides + songs = [ + core.SongData('T1', 'A1', 'l1'), + core.SongData('T2', 'A3', 'l2'), ] - expected = [ - 'L1\nL2\nL3\nL4', 'L5', - 'L6\nL7', 'L8', '', 'L9', + expected = ['T1', 'aaa', '', 'T2', 'b1', 'b2'] + + # Get slides + slides = core.assemble_slides(songs) + + # Assert slides are correct + assert slides == expected + + # Assert parse_song_lyrics called + core.parse_song_lyrics.assert_has_calls([ + mocker.call('L1', 4), mocker.call('L2', 4) + ]) + +def test_assemble_slides_default(): + # Declare song data and expected slides + songs = [ + core.SongData('t1', 'a1', 'l1\nl2\nl3\nl4\nl5'), + core.SongData('T2', 'A3', 'L6\nL7\n\nL8\n\n\nL9'), + ] + expected = [ + 'T1', 'L1\nL2\nL3\nL4', 'L5', '', + 'T2', 'L6\nL7', 'L8', '', 'L9', + ] + + # Get slides + slides = core.assemble_slides(songs) + + # Assert slides are correct + assert slides == expected + +def test_assemble_slides_custom_lines_per_slide(mocker): + # Mock parse_song_lyrics + mocker.patch('songs2slides.core.parse_song_lyrics') + core.parse_song_lyrics.side_effect = [['aaa'], ['b1', 'b2']] + + # Declare song data and expected slides + songs = [ + core.SongData('T1', 'A1', 'l1'), + core.SongData('T2', 'A3', 'l2'), ] - - # Get slides - slides = core.assemble_slides(songs, title_slides = False, blank_slides = False) - - # Assert slides are correct - self.assertEqual(slides, expected) - - def test_assemble_slides_no_songs(self): - # Declare expected slides - expected = [] - - # Get slides - slides = core.assemble_slides([]) - - # Assert slides are correct - self.assertEqual(slides, expected) - - def test_create_pptx(self): - with patch('songs2slides.core.pptx.presentation.Presentation.save') as mocked_save: - # Create PowerPoint - core.create_pptx(['A', 'B\nC', 'D'], 'test.pptx') - - # Assert PowerPoint was saved - mocked_save.assert_called_with('test.pptx') + expected = ['T1', 'aaa', '', 'T2', 'b1', 'b2'] + + # Get slides + slides = core.assemble_slides(songs, lines_per_slide = 3) + + # Assert slides are correct + assert slides == expected + + # Assert parse_song_lyrics called correctly + core.parse_song_lyrics.assert_has_calls([ + mocker.call('L1', 3), mocker.call('L2', 3) + ]) + +def test_assemble_slides_no_title_slides(): + # Declare song data and expected slides + songs = [ + core.SongData('t1', 'a1', 'l1\nl2\nl3\nl4\nl5'), + core.SongData('T2', 'A3', 'L6\nL7\n\nL8\n\n\nL9'), + ] + expected = [ + 'L1\nL2\nL3\nL4', 'L5', '', + 'L6\nL7', 'L8', '', 'L9', + ] + + # Get slides + slides = core.assemble_slides(songs, title_slides = False) + + # Assert slides are correct + assert slides == expected + +def test_assemble_slides_no_blank_slides(): + # Declare song data and expected slides + songs = [ + core.SongData('t1', 'a1', 'l1\nl2\nl3\nl4\nl5'), + core.SongData('T2', 'A3', 'L6\nL7\n\nL8\n\n\nL9'), + ] + expected = [ + 'T1', 'L1\nL2\nL3\nL4', 'L5', + 'T2', 'L6\nL7', 'L8', '', 'L9', + ] + + # Get slides + slides = core.assemble_slides(songs, blank_slides = False) + + # Assert slides are correct + assert slides == expected + +def test_assemble_slides_no_extra_slides(): + # Declare song data and expected slides + songs = [ + core.SongData('t1', 'a1', 'l1\nl2\nl3\nl4\nl5'), + core.SongData('T2', 'A3', 'L6\nL7\n\nL8\n\n\nL9'), + ] + expected = [ + 'L1\nL2\nL3\nL4', 'L5', + 'L6\nL7', 'L8', '', 'L9', + ] + + # Get slides + slides = core.assemble_slides(songs, title_slides = False, blank_slides = False) + + # Assert slides are correct + assert slides == expected + +def test_assemble_slides_no_songs(): + # Declare expected slides + expected = [] + + # Get slides + slides = core.assemble_slides([]) + + # Assert slides are correct + assert slides == expected + +def test_create_pptx(mocker): + # Mock Presentation.save + mocker.patch('songs2slides.core.pptx.presentation.Presentation.save') + + # Create PowerPoint + core.create_pptx(['A', 'B\nC', 'D'], 'test.pptx') + + # Assert PowerPoint was saved + core.pptx.presentation.Presentation.save.assert_called_with('test.pptx') diff --git a/tests/test_routes.py b/tests/test_routes.py @@ -1,250 +1,255 @@ -import unittest -from unittest.mock import patch, call - -from songs2slides import create_app, core - -class TestRoutes(unittest.TestCase): - def setUp(self): - self.app = create_app() - self.client = self.app.test_client() - - def test_get_lyrics_basic(self): - with patch('songs2slides.core.get_song_data') as mocked_get, \ - patch('songs2slides.core.parse_song_lyrics') as mocked_parse, \ - patch('songs2slides.routes.render_template') as mocked_render: - - # Mock get_song_data and parse_song_lyrics - songs = [ - core.SongData('T1', 'A1', 'L1'), - core.SongData('T2', 'A2', 'L2'), - ] - mocked_get.side_effect = songs - mocked_parse.side_effect = ['L1', 'L2'] - - # Send request - self.client.post('/create/step-2/', data={ - 'title-1': 'T1', - 'artist-1': 'A1', - 'title-2': 'T2', - 'artist-2': 'A2', - }) - - # Assert mocks called correctly - mocked_get.assert_has_calls([call('T1', 'A1'), call('T2', 'A2')]) - mocked_parse.assert_has_calls([call('L1', 4), call('L2', 4)]) - mocked_render.assert_called_with('create-step-2.html', songs=songs, missing=0) - - def test_get_lyrics_one_error(self): - with patch('songs2slides.core.get_song_data') as mocked_get, \ - patch('songs2slides.core.parse_song_lyrics') as mocked_parse, \ - patch('songs2slides.routes.render_template') as mocked_render: - - # Mock get_song_data and parse_song_lyrics - songs = [ - core.SongData('T1', 'A1', None), - core.SongData('T2', 'A2', 'L2'), - ] - mocked_get.side_effect = [Exception(), songs[1]] - mocked_parse.side_effect = ['L1', 'L2'] - - # Send request - self.client.post('/create/step-2/', data={ - 'title-1': 'T1', - 'artist-1': 'A1', - 'title-2': 'T2', - 'artist-2': 'A2', - }) - - # Assert mocks called correctly - mocked_get.assert_has_calls([call('T1', 'A1'), call('T2', 'A2')]) - mocked_parse.assert_has_calls([call('L2', 4)]) - mocked_render.assert_called_with('create-step-2.html', songs=songs, missing=1) - - def test_get_lyrics_missing_artist(self): - with patch('songs2slides.core.get_song_data') as mocked_get: - - # Send request - res = self.client.post('/create/step-2/', data={ - 'title-1': 'T1', - 'title-2': 'T2', - 'artist-2': 'A2', - }) - - # Assert mocks not called - mocked_get.assert_not_called() - - # Assert response has 400 status code - self.assertEqual(res.status_code, 400) - - def test_update_lyrics(self): - with patch('songs2slides.routes.render_template') as mocked_render: - - # Send request - self.client.post('/create/step-3/', data={ - 'title-1': 'T1', - 'artist-1': 'A1', - 'lyrics-1': 'L1', - 'title-2': 'T2', - 'artist-2': 'A2', - 'lyrics-2': 'L2', - 'output-type': 'html', - 'title-slides': 'on', - }) - - # Assert render_template called correctly - mocked_render.assert_called_with('create-step-3.html', songs=[ - core.SongData('T1', 'A1', 'L1'), - core.SongData('T2', 'A2', 'L2'), - ]) - - def test_create_slides_basic(self): - with patch('songs2slides.core.assemble_slides') as mocked_assemble, \ - patch('songs2slides.core.create_pptx') as mocked_create, \ - patch('songs2slides.routes.send_file') as mocked_send: - - # Send request - self.client.post('/slides/', data={ - 'title-1': 'T1', - 'artist-1': 'A1', - 'lyrics-1': 'L1', - 'title-2': 'T2', - 'artist-2': 'A2', - 'lyrics-2': 'L2', - 'output-type': 'pptx', - 'title-slides': 'on', - 'blank-slides': 'on', - }) - - # Assert mocks called correctly - mocked_assemble.assert_called_with([ - core.SongData('T1', 'A1', 'L1'), - core.SongData('T2', 'A2', 'L2'), - ], - lines_per_slide = None, - title_slides = True, - blank_slides = True, - ) - file = mocked_create.call_args.args[1] - mocked_send.assert_called_with(file, as_attachment=True, - download_name='slides.pptx') - - def test_create_slides_mising_artist(self): - with patch('songs2slides.core.assemble_slides') as mocked_assemble: - - # Send request - res = self.client.post('/slides/', data={ - 'title-1': 'T1', - 'artist-1': 'A1', - 'lyrics-1': 'L1', - 'title-2': 'T2', - 'lyrics-2': 'L2', - 'output-type': 'pptx', - 'title-slides': 'on', - 'blank-slides': 'on', - }) - - # Assert response has 400 status code - self.assertEqual(res.status_code, 400) - - # Assert assemble_slides not called - mocked_assemble.assert_not_called() - - def test_create_slides_html_slides(self): - with patch('songs2slides.core.assemble_slides') as mocked_assemble, \ - patch('songs2slides.core.create_pptx') as mocked_create, \ - patch('songs2slides.routes.render_template') as mocked_render: - - # Mock assemble_slides - slides = ['T1', 'L1\nL2', 'L3', 'T2', 'L4'] - mocked_assemble.return_value = slides - - # Send request - self.client.post('/slides/', data={ - 'title-1': 'T1', - 'artist-1': 'A1', - 'lyrics-1': 'L1', - 'title-2': 'T2', - 'artist-2': 'A2', - 'lyrics-2': 'L2', - 'output-type': 'html', - 'title-slides': 'on', - 'blank-slides': 'on', - }) - - # Assert mocks called correctly - mocked_assemble.assert_called_with([ - core.SongData('T1', 'A1', 'L1'), - core.SongData('T2', 'A2', 'L2'), - ], - lines_per_slide = None, - title_slides = True, - blank_slides = True, - ) - mocked_create.assert_not_called() - mocked_render.assert_called_with('slides.html', slides=slides) - - def test_create_slides_no_title_slides(self): - with patch('songs2slides.core.assemble_slides') as mocked_assemble, \ - patch('songs2slides.core.create_pptx') as mocked_create, \ - patch('songs2slides.routes.render_template') as mocked_render: - - # Mock assemble_slides - slides = ['T1', 'L1\nL2', 'L3', 'T2', 'L4'] - mocked_assemble.return_value = slides - - # Send request - self.client.post('/slides/', data={ - 'title-1': 'T1', - 'artist-1': 'A1', - 'lyrics-1': 'L1', - 'title-2': 'T2', - 'artist-2': 'A2', - 'lyrics-2': 'L2', - 'output-type': 'html', - 'blank-slides': 'on', - }) - - # Assert mocks called correctly - mocked_assemble.assert_called_with([ - core.SongData('T1', 'A1', 'L1'), - core.SongData('T2', 'A2', 'L2'), - ], - lines_per_slide = None, - title_slides = False, - blank_slides = True, - ) - mocked_create.assert_not_called() - mocked_render.assert_called_with('slides.html', slides=slides) - - def test_create_slides_no_blank_slides(self): - with patch('songs2slides.core.assemble_slides') as mocked_assemble, \ - patch('songs2slides.core.create_pptx') as mocked_create, \ - patch('songs2slides.routes.render_template') as mocked_render: - - # Mock assemble_slides - slides = ['T1', 'L1\nL2', 'L3', 'T2', 'L4'] - mocked_assemble.return_value = slides - - # Send request - self.client.post('/slides/', data={ - 'title-1': 'T1', - 'artist-1': 'A1', - 'lyrics-1': 'L1', - 'title-2': 'T2', - 'artist-2': 'A2', - 'lyrics-2': 'L2', - 'output-type': 'html', - 'title-slides': 'on', - }) - - # Assert mocks called correctly - mocked_assemble.assert_called_with([ - core.SongData('T1', 'A1', 'L1'), - core.SongData('T2', 'A2', 'L2'), - ], - lines_per_slide = None, - title_slides = True, - blank_slides = False, - ) - mocked_create.assert_not_called() - mocked_render.assert_called_with('slides.html', slides=slides) +import pytest + +from songs2slides import create_app, core, routes + +@pytest.fixture(autouse=True) +def client(): + app = create_app() + return app.test_client() + +def test_get_lyrics_basic(client, mocker): + # Mock get_song_data, parse_song_lyrics, and render_template + mocker.patch('songs2slides.core.get_song_data') + mocker.patch('songs2slides.core.parse_song_lyrics') + mocker.patch('songs2slides.routes.render_template') + songs = [ + core.SongData('T1', 'A1', 'L1'), + core.SongData('T2', 'A2', 'L2'), + ] + core.get_song_data.side_effect = songs + core.parse_song_lyrics.side_effect = ['L1', 'L2'] + + # Send request + client.post('/create/step-2/', data={ + 'title-1': 'T1', + 'artist-1': 'A1', + 'title-2': 'T2', + 'artist-2': 'A2', + }) + + # Assert mocks called correctly + core.get_song_data.assert_has_calls([ + mocker.call('T1', 'A1'), mocker.call('T2', 'A2') + ]) + core.parse_song_lyrics.assert_has_calls([ + mocker.call('L1', 4), mocker.call('L2', 4) + ]) + routes.render_template.assert_called_with('create-step-2.html', + songs=songs, missing=0) + +def test_get_lyrics_one_error(client, mocker): + # Mock get_song_data, parse_song_lyrics, and render_template + mocker.patch('songs2slides.core.get_song_data') + mocker.patch('songs2slides.core.parse_song_lyrics') + mocker.patch('songs2slides.routes.render_template') + songs = [ + core.SongData('T1', 'A1', None), + core.SongData('T2', 'A2', 'L2'), + ] + core.get_song_data.side_effect = [Exception(), songs[1]] + core.parse_song_lyrics.side_effect = ['L1', 'L2'] + + # Send request + client.post('/create/step-2/', data={ + 'title-1': 'T1', + 'artist-1': 'A1', + 'title-2': 'T2', + 'artist-2': 'A2', + }) + + # Assert mocks called correctly + core.get_song_data.assert_has_calls([ + mocker.call('T1', 'A1'), mocker.call('T2', 'A2') + ]) + core.parse_song_lyrics.assert_has_calls([mocker.call('L2', 4)]) + routes.render_template.assert_called_with('create-step-2.html', songs=songs, missing=1) + +def test_get_lyrics_missing_artist(client, mocker): + # Mock get_song_data + mocker.patch('songs2slides.core.get_song_data') + + # Send request + res = client.post('/create/step-2/', data={ + 'title-1': 'T1', + 'title-2': 'T2', + 'artist-2': 'A2', + }) + + # Assert mocks not called + core.get_song_data.assert_not_called() + + # Assert response has 400 status code + assert res.status_code == 400 + +def test_update_lyrics(client, mocker): + # Mock render_template + mocker.patch('songs2slides.routes.render_template') + + # Send request + client.post('/create/step-3/', data={ + 'title-1': 'T1', + 'artist-1': 'A1', + 'lyrics-1': 'L1', + 'title-2': 'T2', + 'artist-2': 'A2', + 'lyrics-2': 'L2', + 'output-type': 'html', + 'title-slides': 'on', + }) + + # Assert render_template called correctly + routes.render_template.assert_called_with('create-step-3.html', songs=[ + core.SongData('T1', 'A1', 'L1'), + core.SongData('T2', 'A2', 'L2'), + ]) + +def test_create_slides_basic(client, mocker): + # Mock assemble_slides, create_pptx, and send_file + mocker.patch('songs2slides.core.assemble_slides') + mocker.patch('songs2slides.core.create_pptx') + mocker.patch('songs2slides.routes.send_file') + + # Send request + client.post('/slides/', data={ + 'title-1': 'T1', + 'artist-1': 'A1', + 'lyrics-1': 'L1', + 'title-2': 'T2', + 'artist-2': 'A2', + 'lyrics-2': 'L2', + 'output-type': 'pptx', + 'title-slides': 'on', + 'blank-slides': 'on', + }) + + # Assert mocks called correctly + core.assemble_slides.assert_called_with([ + core.SongData('T1', 'A1', 'L1'), + core.SongData('T2', 'A2', 'L2'), + ], + lines_per_slide = None, + title_slides = True, + blank_slides = True, + ) + file = core.create_pptx.call_args.args[1] + routes.send_file.assert_called_with(file, as_attachment=True, + download_name='slides.pptx') + +def test_create_slides_mising_artist(client, mocker): + # Mock assemble_slides + mocker.patch('songs2slides.core.assemble_slides') + + # Send request + res = client.post('/slides/', data={ + 'title-1': 'T1', + 'artist-1': 'A1', + 'lyrics-1': 'L1', + 'title-2': 'T2', + 'lyrics-2': 'L2', + 'output-type': 'pptx', + 'title-slides': 'on', + 'blank-slides': 'on', + }) + + # Assert response has 400 status code + assert res.status_code == 400 + + # Assert assemble_slides not called + core.assemble_slides.assert_not_called() + +def test_create_slides_html_slides(client, mocker): + # Mock assemble_slides, create_pptx, render_template + mocker.patch('songs2slides.core.assemble_slides') + mocker.patch('songs2slides.core.create_pptx') + mocker.patch('songs2slides.routes.render_template') + slides = ['T1', 'L1\nL2', 'L3', 'T2', 'L4'] + core.assemble_slides.return_value = slides + + # Send request + client.post('/slides/', data={ + 'title-1': 'T1', + 'artist-1': 'A1', + 'lyrics-1': 'L1', + 'title-2': 'T2', + 'artist-2': 'A2', + 'lyrics-2': 'L2', + 'output-type': 'html', + 'title-slides': 'on', + 'blank-slides': 'on', + }) + + # Assert mocks called correctly + core.assemble_slides.assert_called_with([ + core.SongData('T1', 'A1', 'L1'), + core.SongData('T2', 'A2', 'L2'), + ], + lines_per_slide = None, + title_slides = True, + blank_slides = True, + ) + core.create_pptx.assert_not_called() + routes.render_template.assert_called_with('slides.html', slides=slides) + +def test_create_slides_no_title_slides(client, mocker): + # Mock assemble_slides, create_pptx, render_template + mocker.patch('songs2slides.core.assemble_slides') + mocker.patch('songs2slides.core.create_pptx') + mocker.patch('songs2slides.routes.render_template') + slides = ['T1', 'L1\nL2', 'L3', 'T2', 'L4'] + core.assemble_slides.return_value = slides + + # Send request + client.post('/slides/', data={ + 'title-1': 'T1', + 'artist-1': 'A1', + 'lyrics-1': 'L1', + 'title-2': 'T2', + 'artist-2': 'A2', + 'lyrics-2': 'L2', + 'output-type': 'html', + 'blank-slides': 'on', + }) + + # Assert mocks called correctly + core.assemble_slides.assert_called_with([ + core.SongData('T1', 'A1', 'L1'), + core.SongData('T2', 'A2', 'L2'), + ], + lines_per_slide = None, + title_slides = False, + blank_slides = True, + ) + core.create_pptx.assert_not_called() + routes.render_template.assert_called_with('slides.html', slides=slides) + +def test_create_slides_no_blank_slides(client, mocker): + # Mock assemble_slides, create_pptx, render_template + mocker.patch('songs2slides.core.assemble_slides') + mocker.patch('songs2slides.core.create_pptx') + mocker.patch('songs2slides.routes.render_template') + slides = ['T1', 'L1\nL2', 'L3', 'T2', 'L4'] + core.assemble_slides.return_value = slides + + # Send request + client.post('/slides/', data={ + 'title-1': 'T1', + 'artist-1': 'A1', + 'lyrics-1': 'L1', + 'title-2': 'T2', + 'artist-2': 'A2', + 'lyrics-2': 'L2', + 'output-type': 'html', + 'title-slides': 'on', + }) + + # Assert mocks called correctly + core.assemble_slides.assert_called_with([ + core.SongData('T1', 'A1', 'L1'), + core.SongData('T2', 'A2', 'L2'), + ], + lines_per_slide = None, + title_slides = True, + blank_slides = False, + ) + core.create_pptx.assert_not_called() + routes.render_template.assert_called_with('slides.html', slides=slides)