Files
gh-christianlouis-quizzical…/tests/test_models.py
T
2026-03-12 01:16:36 +00:00

631 lines
22 KiB
Python

"""Tests for database models."""
import pytest
from datetime import datetime, timedelta
from musicround.models import (
db, User, Role, UserPreferences, Tag, SongTag, Song,
Round, RoundExport, SystemSetting, ImportJobRecord,
)
class TestRoleModel:
"""Tests for the Role model."""
def test_role_creation(self, app):
"""Test basic role creation and persistence."""
role = Role(name='admin', description='Administrator role')
db.session.add(role)
db.session.commit()
fetched = Role.query.filter_by(name='admin').first()
assert fetched is not None
assert fetched.name == 'admin'
assert fetched.description == 'Administrator role'
def test_role_repr(self, app):
"""Test Role __repr__."""
role = Role(name='editor', description='Editor')
db.session.add(role)
db.session.commit()
assert 'editor' in repr(role)
assert 'Role' in repr(role)
def test_role_unique_name(self, app):
"""Test that role names must be unique."""
from sqlalchemy.exc import IntegrityError
role1 = Role(name='unique_role')
role2 = Role(name='unique_role')
db.session.add(role1)
db.session.commit()
db.session.add(role2)
with pytest.raises(IntegrityError):
db.session.commit()
db.session.rollback()
class TestUserModel:
"""Tests for the User model."""
def test_user_creation(self, app):
"""Test basic user creation."""
user = User(username='john', email='john@example.com')
user.password = 'SecurePass123!'
db.session.add(user)
db.session.commit()
fetched = User.query.filter_by(username='john').first()
assert fetched is not None
assert fetched.email == 'john@example.com'
assert fetched.active is True
assert fetched.is_admin is False
def test_password_hashing(self, app):
"""Test that passwords are hashed on assignment."""
user = User(username='hashtest', email='hash@example.com')
user.password = 'MySecret!'
assert user.password_hash is not None
assert user.password_hash != 'MySecret!'
def test_password_check_correct(self, app):
"""Test check_password returns True for correct password."""
user = User(username='passtest', email='pass@example.com')
user.password = 'CorrectPassword!'
assert user.check_password('CorrectPassword!') is True
def test_password_check_incorrect(self, app):
"""Test check_password returns False for wrong password."""
user = User(username='wrongpass', email='wrong@example.com')
user.password = 'CorrectPassword!'
assert user.check_password('WrongPassword') is False
def test_password_check_no_hash(self, app):
"""Test check_password returns False when no hash is set."""
user = User(username='nohash', email='nohash@example.com')
user.password_hash = None
assert user.check_password('anything') is False
def test_password_getter_raises(self, app):
"""Test that reading the password attribute raises AttributeError."""
user = User(username='readonly', email='read@example.com')
user.password = 'secret'
with pytest.raises(AttributeError):
_ = user.password
def test_set_token(self, app):
"""Test that set_token generates a unique reset token."""
user = User(username='tokentest', email='token@example.com')
token = user.set_token()
assert token is not None
assert len(token) > 0
assert user.reset_token == token
def test_set_token_is_unique(self, app):
"""Test that two consecutive tokens are different."""
user = User(username='uniquetoken', email='unique@example.com')
token1 = user.set_token()
token2 = user.set_token()
assert token1 != token2
def test_has_role_true(self, app):
"""Test has_role returns True when user has the role."""
role = Role(name='moderator')
user = User(username='modrole', email='mod@example.com')
user.roles.append(role)
db.session.add_all([role, user])
db.session.commit()
assert user.has_role('moderator') is True
def test_has_role_false(self, app):
"""Test has_role returns False when user does not have the role."""
user = User(username='norole', email='norole@example.com')
db.session.add(user)
db.session.commit()
assert user.has_role('admin') is False
def test_is_admin_by_role(self, app):
"""Test is_admin_by_role checks roles correctly."""
admin_role = Role(name='admin')
admin_user = User(username='roleadmin', email='roleadmin@example.com')
admin_user.roles.append(admin_role)
normal_user = User(username='normalrole', email='normalrole@example.com')
db.session.add_all([admin_role, admin_user, normal_user])
db.session.commit()
assert admin_user.is_admin_by_role() is True
assert normal_user.is_admin_by_role() is False
def test_user_repr(self, app):
"""Test User __repr__."""
user = User(username='reprtest', email='repr@example.com')
db.session.add(user)
db.session.commit()
assert 'reprtest' in repr(user)
assert 'User' in repr(user)
def test_user_defaults(self, app):
"""Test that User model defaults are applied correctly."""
user = User(username='defaults', email='defaults@example.com')
db.session.add(user)
db.session.commit()
assert user.active is True
assert user.is_admin is False
assert user.auth_provider == 'local'
assert user.dropbox_export_path == '/QuizzicalBeats'
class TestUserPreferencesModel:
"""Tests for the UserPreferences model."""
def test_preferences_creation(self, app):
"""Test UserPreferences creation with defaults."""
user = User(username='prefuser', email='pref@example.com')
db.session.add(user)
db.session.commit()
prefs = UserPreferences(user_id=user.id)
db.session.add(prefs)
db.session.commit()
fetched = UserPreferences.query.filter_by(user_id=user.id).first()
assert fetched is not None
assert fetched.default_tts_service == 'polly'
assert fetched.enable_intro is True
assert fetched.theme == 'light'
def test_preferences_relationship(self, app):
"""Test that User.preferences relationship works."""
user = User(username='reluser', email='rel@example.com')
db.session.add(user)
db.session.commit()
prefs = UserPreferences(user_id=user.id, theme='dark')
db.session.add(prefs)
db.session.commit()
db.session.refresh(user)
assert user.preferences is not None
assert user.preferences.theme == 'dark'
class TestTagModel:
"""Tests for the Tag model."""
def test_tag_creation(self, app):
"""Test basic Tag creation."""
tag = Tag(name='rock')
db.session.add(tag)
db.session.commit()
fetched = Tag.query.filter_by(name='rock').first()
assert fetched is not None
assert fetched.name == 'rock'
def test_tag_repr(self, app):
"""Test Tag __repr__."""
tag = Tag(name='pop')
db.session.add(tag)
db.session.commit()
assert 'pop' in repr(tag)
assert 'Tag' in repr(tag)
def test_tag_unique_name(self, app):
"""Test that tag names must be unique."""
from sqlalchemy.exc import IntegrityError
tag1 = Tag(name='unique_tag')
tag2 = Tag(name='unique_tag')
db.session.add(tag1)
db.session.commit()
db.session.add(tag2)
with pytest.raises(IntegrityError):
db.session.commit()
db.session.rollback()
class TestSongModel:
"""Tests for the Song model."""
def _make_song(self, **kwargs):
"""Helper to create a valid Song instance."""
defaults = {
'title': 'Test Song',
'artist': 'Test Artist',
'genre': 'Rock',
'year': 2000,
}
defaults.update(kwargs)
return Song(**defaults)
def test_song_creation(self, app):
"""Test basic Song creation."""
song = self._make_song(
title='Bohemian Rhapsody',
artist='Queen',
spotify_id='abc123',
)
db.session.add(song)
db.session.commit()
fetched = Song.query.filter_by(title='Bohemian Rhapsody').first()
assert fetched is not None
assert fetched.artist == 'Queen'
def test_song_repr(self, app):
"""Test Song __repr__."""
song = self._make_song(title='Repr Song', artist='Repr Artist')
db.session.add(song)
db.session.commit()
r = repr(song)
assert 'Repr Song' in r
assert 'Repr Artist' in r
def test_song_to_dict(self, app):
"""Test Song.to_dict() returns expected keys."""
song = self._make_song(
title='Dict Song',
artist='Dict Artist',
album_name='Dict Album',
cover_url='http://example.com/cover.jpg',
preview_url='http://example.com/preview.mp3',
spotify_id='spotify123',
deezer_id=456,
isrc='TEST1234567',
year=1995,
genre='Jazz',
popularity=75,
)
db.session.add(song)
db.session.commit()
d = song.to_dict()
assert d['title'] == 'Dict Song'
assert d['artist'] == 'Dict Artist'
assert d['album_name'] == 'Dict Album'
assert d['cover_url'] == 'http://example.com/cover.jpg'
assert d['preview_url'] == 'http://example.com/preview.mp3'
assert d['spotify_id'] == 'spotify123'
assert d['isrc'] == 'TEST1234567'
assert d['year'] == 1995
assert d['genre'] == 'Jazz'
assert d['popularity'] == 75
assert d['last_used'] is None
# Audio feature keys present
for key in ('acousticness', 'danceability', 'energy', 'tempo', 'valence'):
assert key in d
def test_song_to_dict_with_last_used(self, app):
"""Test Song.to_dict() formats last_used correctly."""
song = self._make_song(title='LastUsed Song', artist='Artist')
song.last_used = datetime(2024, 6, 15, 12, 0, 0)
db.session.add(song)
db.session.commit()
d = song.to_dict()
assert d['last_used'] == '2024-06-15 12:00:00'
def test_song_tags_relationship(self, app):
"""Test Song-Tag many-to-many relationship."""
song = self._make_song(title='Tagged Song', artist='Artist')
tag = Tag(name='tagged_rock')
db.session.add_all([song, tag])
db.session.commit()
song.tags.append(tag)
db.session.commit()
fetched = Song.query.filter_by(title='Tagged Song').first()
assert len(fetched.tags) == 1
assert fetched.tags[0].name == 'tagged_rock'
d = fetched.to_dict()
assert len(d['tags']) == 1
assert d['tags'][0]['name'] == 'tagged_rock'
def test_song_defaults(self, app):
"""Test Song model default values."""
song = self._make_song()
db.session.add(song)
db.session.commit()
assert song.used_count == 0
assert song.source == 'spotify'
class TestRoundModel:
"""Tests for the Round model."""
def _make_songs(self, count=3):
"""Helper to create and persist Song objects."""
songs = []
for i in range(count):
song = Song(title=f'Round Song {i}', artist='Band', genre='Pop')
db.session.add(song)
songs.append(song)
db.session.commit()
return songs
def test_round_creation(self, app):
"""Test Round creation with required fields."""
songs = self._make_songs()
song_ids = ','.join(str(s.id) for s in songs)
round_ = Round(
name='My Quiz Round',
round_type='genre',
round_criteria_used='Rock',
songs=song_ids,
genre='Rock',
)
db.session.add(round_)
db.session.commit()
fetched = Round.query.filter_by(name='My Quiz Round').first()
assert fetched is not None
assert fetched.round_type == 'genre'
def test_round_repr(self, app):
"""Test Round __repr__."""
songs = self._make_songs(1)
round_ = Round(
name='Repr Round',
round_type='decade',
round_criteria_used='1980s',
songs=str(songs[0].id),
)
db.session.add(round_)
db.session.commit()
assert 'Repr Round' in repr(round_)
def test_round_reset_generated_status(self, app):
"""Test reset_generated_status sets mp3_generated and pdf_generated to False."""
songs = self._make_songs(1)
round_ = Round(
round_type='genre',
round_criteria_used='Pop',
songs=str(songs[0].id),
mp3_generated=True,
pdf_generated=True,
)
db.session.add(round_)
db.session.commit()
round_.reset_generated_status()
assert round_.mp3_generated is False
assert round_.pdf_generated is False
def test_round_song_list(self, app):
"""Test Round.song_list property returns the correct Song objects."""
songs = self._make_songs(2)
song_ids = ','.join(str(s.id) for s in songs)
round_ = Round(
round_type='genre',
round_criteria_used='Pop',
songs=song_ids,
)
db.session.add(round_)
db.session.commit()
result = round_.song_list
assert len(result) == 2
result_ids = {s.id for s in result}
assert result_ids == {s.id for s in songs}
def test_round_defaults(self, app):
"""Test Round model default values."""
songs = self._make_songs(1)
round_ = Round(
round_type='genre',
round_criteria_used='Rock',
songs=str(songs[0].id),
)
db.session.add(round_)
db.session.commit()
assert round_.mp3_generated is False
assert round_.pdf_generated is False
class TestRoundExportModel:
"""Tests for the RoundExport model."""
def test_round_export_creation(self, app):
"""Test RoundExport creation."""
songs = [Song(title='Export Song', artist='Artist', genre='Pop')]
db.session.add_all(songs)
db.session.commit()
round_ = Round(
round_type='genre', round_criteria_used='Pop',
songs=str(songs[0].id),
)
user = User(username='exportuser', email='export@example.com')
db.session.add_all([round_, user])
db.session.commit()
export = RoundExport(
round_id=round_.id,
user_id=user.id,
export_type='dropbox',
destination='/QuizzicalBeats',
status='success',
)
db.session.add(export)
db.session.commit()
fetched = RoundExport.query.filter_by(round_id=round_.id).first()
assert fetched is not None
assert fetched.export_type == 'dropbox'
assert fetched.status == 'success'
def test_round_export_repr(self, app):
"""Test RoundExport __repr__."""
songs = [Song(title='Repr Export Song', artist='Artist', genre='Pop')]
db.session.add_all(songs)
db.session.commit()
round_ = Round(round_type='genre', round_criteria_used='Pop', songs=str(songs[0].id))
db.session.add(round_)
db.session.commit()
export = RoundExport(round_id=round_.id, export_type='email', status='failed')
db.session.add(export)
db.session.commit()
assert 'RoundExport' in repr(export)
class TestSystemSettingModel:
"""Tests for the SystemSetting model."""
def test_set_and_get(self, app):
"""Test SystemSetting.set() and SystemSetting.get()."""
SystemSetting.set('site_title', 'Quizzical Beats')
value = SystemSetting.get('site_title')
assert value == 'Quizzical Beats'
def test_get_missing_key(self, app):
"""Test SystemSetting.get() returns default for missing key."""
value = SystemSetting.get('nonexistent_key', default='fallback')
assert value == 'fallback'
def test_get_missing_key_none_default(self, app):
"""Test SystemSetting.get() returns None by default for missing key."""
value = SystemSetting.get('another_missing_key')
assert value is None
def test_update_existing_setting(self, app):
"""Test that SystemSetting.set() updates an existing setting."""
SystemSetting.set('update_key', 'initial_value')
SystemSetting.set('update_key', 'updated_value')
assert SystemSetting.get('update_key') == 'updated_value'
def test_all_settings(self, app):
"""Test SystemSetting.all_settings() returns all key-value pairs."""
SystemSetting.set('key_a', 'value_a')
SystemSetting.set('key_b', 'value_b')
settings = SystemSetting.all_settings()
assert settings.get('key_a') == 'value_a'
assert settings.get('key_b') == 'value_b'
class TestImportJobRecordModel:
"""Tests for the ImportJobRecord model."""
def _make_user(self):
user = User(username='importuser', email='importjob@example.com')
db.session.add(user)
db.session.commit()
return user
def test_import_job_creation(self, app):
"""Test ImportJobRecord creation."""
user = self._make_user()
job = ImportJobRecord(
service_name='spotify',
item_type='playlist',
item_id='playlist123',
user_id=user.id,
priority=5,
)
db.session.add(job)
db.session.commit()
fetched = ImportJobRecord.query.filter_by(item_id='playlist123').first()
assert fetched is not None
assert fetched.status == 'pending'
assert fetched.imported_count == 0
def test_import_job_repr(self, app):
"""Test ImportJobRecord __repr__."""
user = self._make_user()
job = ImportJobRecord(
service_name='deezer', item_type='album',
item_id='album456', user_id=user.id, priority=10,
)
db.session.add(job)
db.session.commit()
r = repr(job)
assert 'deezer' in r
assert 'album456' in r
def test_duration_none_when_incomplete(self, app):
"""Test duration property returns None when job is not completed."""
user = self._make_user()
job = ImportJobRecord(
service_name='spotify', item_type='track',
item_id='track789', user_id=user.id, priority=10,
)
db.session.add(job)
db.session.commit()
assert job.duration is None
def test_duration_calculated(self, app):
"""Test duration property calculates seconds correctly."""
user = self._make_user()
start = datetime(2024, 1, 1, 12, 0, 0)
end = datetime(2024, 1, 1, 12, 0, 45)
job = ImportJobRecord(
service_name='spotify', item_type='track',
item_id='track_dur', user_id=user.id, priority=10,
started_at=start, completed_at=end,
)
db.session.add(job)
db.session.commit()
assert job.duration == 45.0
def test_item_url_spotify_playlist(self, app):
"""Test item_url for Spotify playlist."""
user = self._make_user()
job = ImportJobRecord(
service_name='spotify', item_type='playlist',
item_id='myplaylist', user_id=user.id, priority=10,
)
assert job.item_url == 'https://open.spotify.com/playlist/myplaylist'
def test_item_url_spotify_album(self, app):
"""Test item_url for Spotify album."""
user = self._make_user()
job = ImportJobRecord(
service_name='spotify', item_type='album',
item_id='myalbum', user_id=user.id, priority=10,
)
assert job.item_url == 'https://open.spotify.com/album/myalbum'
def test_item_url_spotify_track(self, app):
"""Test item_url for Spotify track."""
user = self._make_user()
job = ImportJobRecord(
service_name='spotify', item_type='track',
item_id='mytrack', user_id=user.id, priority=10,
)
assert job.item_url == 'https://open.spotify.com/track/mytrack'
def test_item_url_deezer_playlist(self, app):
"""Test item_url for Deezer playlist."""
user = self._make_user()
job = ImportJobRecord(
service_name='deezer', item_type='playlist',
item_id='deezerplist', user_id=user.id, priority=10,
)
assert job.item_url == 'https://www.deezer.com/playlist/deezerplist'
def test_item_url_deezer_album(self, app):
"""Test item_url for Deezer album."""
user = self._make_user()
job = ImportJobRecord(
service_name='deezer', item_type='album',
item_id='deezeralbum', user_id=user.id, priority=10,
)
assert job.item_url == 'https://www.deezer.com/album/deezeralbum'
def test_item_url_deezer_track(self, app):
"""Test item_url for Deezer track."""
user = self._make_user()
job = ImportJobRecord(
service_name='deezer', item_type='track',
item_id='deezertrack', user_id=user.id, priority=10,
)
assert job.item_url == 'https://www.deezer.com/track/deezertrack'
def test_item_url_unknown_service(self, app):
"""Test item_url returns None for unknown service."""
user = self._make_user()
job = ImportJobRecord(
service_name='unknown', item_type='playlist',
item_id='something', user_id=user.id, priority=10,
)
assert job.item_url is None