add tests

This commit is contained in:
evilchili 2022-12-05 01:06:57 -08:00
parent d5934a99fb
commit 72dc85ac74
22 changed files with 236 additions and 80 deletions

View File

@ -1,6 +0,0 @@
HOST=127.0.0.1
DEBUG=1
USERNAME=test_username
PASSWORD=test_password
MEDIA_ROOT=/test
MEDIA_GLOB=*.flac,*.mp3

View File

@ -1,7 +1,8 @@
import logging
import os
from pathlib import Path
from groove.exceptions import ConfigurationError, ThemeMissingException
from groove.exceptions import ConfigurationError, ThemeMissingException, ThemeConfigurationError
_setup_hint = "You may be able to solve this error by running 'groove setup'."
_reinstall_hint = "You might need to reinstall Groove On Demand to fix this error."
@ -12,11 +13,12 @@ def root():
if not path:
raise ConfigurationError(f"GROOVE_ON_DEMAND_ROOT is not defined in your environment.\n\n{_setup_hint}")
path = Path(path).expanduser()
if not path.exists or not path.is_dir:
if not path.exists() or not path.is_dir():
raise ConfigurationError(
"The Groove on Demand root directory (GROOVE_ON_DEMAND_ROOT) "
f"does not exist or isn't a directory.\n\n{_reinstall_hint}"
)
logging.debug(f"Root is {path}")
return Path(path)
@ -25,46 +27,63 @@ def media_root():
if not path:
raise ConfigurationError(f"MEDIA_ROOT is not defined in your environment.\n\n{_setup_hint}")
path = Path(path).expanduser()
if not path.exists and path.is_dir:
if not path.exists() or not path.is_dir():
raise ConfigurationError(
"The media_root directory (MEDIA_ROOT) doesn't exist, or isn't a directory.\n\n{_setup_hint}"
)
logging.debug(f"Media root is {path}")
return path
def media(relpath):
return themes_root() / Path(relpath)
path = media_root() / Path(relpath)
return path
def static_root():
dirname = os.environ.get('STATIC_PATH', 'static')
path = root() / Path(dirname)
if not path.exists or not path.is_dir:
if not path.exists() or not path.is_dir():
raise ConfigurationError(
f"The static assets directory {dirname} (STATIC_PATH) "
f"doesn't exist, or isn't a directory.\n\n{_reinstall_hint}"
)
logging.debug(f"Static root is {path}")
return path
def static(relpath):
return static_root() / Path(relpath)
def static(relpath, theme=None):
if theme:
root = theme.path / Path('static')
if not root.is_dir():
raise ThemeConfigurationError(
f"The themes directory {relpath} (THEMES_PATH) "
f"doesn't contain a 'static' directory."
)
path = root / Path(relpath)
logging.debug(f"Checking for {path}")
if path.exists():
return path
path = static_root() / Path(relpath)
logging.debug(f"Defaulting to {path}")
return path
def themes_root():
dirname = os.environ.get('THEMES_PATH', 'themes')
path = root() / Path(dirname)
if not path.exists or not path.is_dir:
if not path.exists() or not path.is_dir():
raise ConfigurationError(
f"The themes directory {dirname} (THEMES_PATH) "
f"doesn't exist, or isn't a directory.\n\n{_reinstall_hint}"
)
logging.debug(f"Themes root is {path}")
return path
def theme(name):
path = themes_root() / Path(name)
if not path.exists or not path.is_dir:
if not path.exists() or not path.is_dir():
available = ','.join(available_themes)
raise ThemeMissingException(
f"A theme directory named {name} does not exist or isn't a directory. "
@ -74,10 +93,6 @@ def theme(name):
return path
def theme_static(relpath):
return Path('static') / Path(relpath)
def theme_template(template_name):
return Path('templates') / Path(f"{template_name}.tpl")

View File

@ -8,32 +8,38 @@ from groove.exceptions import ThemeConfigurationError, ConfigurationError
import groove.path
Theme = namedtuple('Theme', 'path,author,author_link,version,about')
Theme = namedtuple('Theme', 'name,path,author,author_link,version,about')
def load_theme(name=None):
name = os.environ.get('DEFAULT_THEME', name)
if not name:
name = name or os.environ.get('DEFAULT_THEME', None)
if not name: # pragma: no cover
raise ConfigurationError(
"It seems like DEFAULT_THEME is not set in your current environment.\n"
"Running 'groove setup' may help you fix this problem."
)
theme_path = groove.path.theme(name)
theme_info = _get_theme_info(theme_path)
try:
return Theme(
name=name,
path=theme_path,
**theme_info
)
except TypeError:
raise ThemeConfigurationError(f"The {name} them is misconfigured. Does the README.md contain a credits secton?")
def _get_theme_info(theme_path):
readme = theme_path / Path('README.md')
if not readme.exists:
if not readme.exists: # pragma: no cover
raise ThemeConfigurationError(
"The theme is missing a required file: README.md.\n"
"Refer to the Groove On Demand documentation for help creating themes."
)
config = {'about': ''}
config = {
'about': '',
}
with readme.open() as fh:
in_credits = False
in_block = False
@ -55,7 +61,7 @@ def _get_theme_info(theme_path):
key = key.strip()
value = value.strip()
except ValueError:
logging.warn(f"Could not parse credits line: {line}")
logging.warning(f"Could not parse credits line: {line}")
continue
logging.debug(f"Setting theme '{key}' to '{value}'.")
config[key] = value

View File

@ -5,6 +5,7 @@ import os
import bottle
from bottle import HTTPResponse, template, static_file
from bottle.ext import sqlalchemy
from sqlalchemy.exc import NoResultFound, MultipleResultsFound
import groove.db
from groove.auth import is_authenticated
@ -54,54 +55,32 @@ def index():
return "Groovy."
@server.route('/build')
@bottle.auth_basic(is_authenticated)
def build():
return "Authenticated. Groovy."
@server.route('/static/<filepath:path>')
def server_static(filepath):
def serve_static(filepath):
theme = themes.load_theme()
asset = theme.path / groove.path.theme_static(filepath)
if asset.exists():
root = asset.parent
else:
root = groove.path.static_root()
asset = groove.path.static(filepath)
if asset.is_dir():
logging.warning("Asset {asset} is a directory; returning 404.")
return HTTPResponse(status=404, body="Not found.")
logging.debug(f"Serving asset {asset.name} from {root}")
return static_file(asset.name, root=root)
@bottle.auth_basic(is_authenticated)
@server.route('/build/search/playlist')
def search_playlist(slug, db):
playlist = Playlist(slug=slug, session=db, create_ok=False)
response = json.dumps(playlist.as_dict)
logging.debug(response)
return HTTPResponse(status=200, content_type='application/json', body=response)
path = groove.path.static(filepath, theme=theme)
logging.debug(f"Serving asset {path.name} from {path.parent}")
return static_file(path.name, root=path.parent)
@server.route('/track/<request>/<track_id>')
def serve_track(request, track_id, db):
expected = requests.encode([track_id], '/track')
if not requests.verify(request, expected):
if not requests.verify(request, expected): # pragma: no cover
return HTTPResponse(status=404, body="Not found")
try:
track_id = int(track_id)
track = db.query(groove.db.track).filter(
groove.db.track.c.id == track_id
).one()
except (NoResultFound, MultipleResultsFound):
return HTTPResponse(status=404, body="Not found")
path = groove.path.media(track['relpath'])
if path.exists:
logging.debug(f"Service track {path.name} from {path.parent}")
return static_file(path.name, root=path.parent)
else:
return HTTPResponse(status=404, body="Not found")
@server.route('/playlist/<slug>')
@ -123,3 +102,21 @@ def serve_playlist(slug, db):
entry['url'] = f"/track/{sig}/{entry['track_id']}"
return serve('playlist', playlist=pl)
@server.route('/build')
@bottle.auth_basic(is_authenticated)
def build():
return "Authenticated. Groovy."
@bottle.auth_basic(is_authenticated)
@server.route('/build/search/playlist/<slug>')
def search_playlist(slug, db):
playlist = Playlist(slug=slug, session=db, create_ok=False).load()
if not playlist.record:
logging.debug(f"Playist {slug} doesn't exist.")
body = {}
else:
body = json.dumps(playlist.as_dict)
return HTTPResponse(status=200, content_type='application/json', body=body)

View File

@ -1,5 +1,5 @@
[pytest]
env_override_existing_values = 1
env_files = .test_env
env_files = test/fixtures/env
log_cli_level = DEBUG
addopts = --cov=groove/ --cov-report=term-missing

View File

@ -1,4 +1,8 @@
import pytest
import os
from pathlib import Path
from dotenv import load_dotenv
import groove.db
@ -6,6 +10,20 @@ from sqlalchemy import create_engine, insert
from sqlalchemy.orm import sessionmaker
@pytest.fixture(autouse=True, scope='function')
def env():
root = Path(__file__).parent / Path('fixtures')
load_dotenv(Path('test/fixtures/env'))
os.environ['GROOVE_ON_DEMAND_ROOT'] = str(root)
os.environ['MEDIA_ROOT'] = str(root / Path('media'))
return os.environ
@pytest.fixture(scope='function')
def auth():
return (os.environ.get('USERNAME'), os.environ.get('PASSWORD'))
@pytest.fixture(scope='function')
def in_memory_engine():
return create_engine('sqlite:///:memory:', future=True)
@ -32,9 +50,9 @@ def db(in_memory_db):
# create tracks
query = insert(groove.db.track)
in_memory_db.execute(query, [
{'id': 1, 'relpath': '/UNKLE/Psyence Fiction/01 Guns Blazing (Drums of Death, Part 1).flac'},
{'id': 2, 'relpath': '/UNKLE/Psyence Fiction/02 UNKLE (Main Title Theme).flac'},
{'id': 3, 'relpath': '/UNKLE/Psyence Fiction/03 Bloodstain.flac'}
{'id': 1, 'relpath': 'UNKLE/Psyence Fiction/01 Guns Blazing (Drums of Death, Part 1).flac'},
{'id': 2, 'relpath': 'UNKLE/Psyence Fiction/02 UNKLE (Main Title Theme).flac'},
{'id': 3, 'relpath': 'UNKLE/Psyence Fiction/03 Bloodstain.flac'}
])
# create playlists

24
test/fixtures/env vendored Normal file
View File

@ -0,0 +1,24 @@
# Will be overwritten by test setup
GROOVE_ON_DEMAND_ROOT=.
MEDIA_ROOT=.
# where to store the database
DATABASE_PATH=test.db
# Admin user credentials
USERNAME=test_username
PASSWORD=test_password
# Web interface configuration
HOST=127.0.0.1
PORT=2323
THEMES_PATH=themes
STATIC_PATH=static
DEFAULT_THEME=default_theme
SECRET_KEY=fnord
# Media scanner configuration
MEDIA_GLOB=*.mp3,*.flac,*.m4a
# Set this value to enable debugging
DEBUG=1

View File

@ -0,0 +1 @@
DRUMS OF DEATH YALL

0
test/fixtures/media/foo.mp3 vendored Normal file
View File

1
test/fixtures/static/favicon.ico vendored Normal file
View File

@ -0,0 +1 @@
favicon.ico

View File

View File

View File

View File

@ -0,0 +1,11 @@
# Default Theme Fixture
This directory is a test fixture used for verifying theme handling.
## Credits
```
author: Theme Author
author_link: link to my soundcloud
version: 1.3
```

View File

@ -0,0 +1 @@
/* test.css */

BIN
test/test.db Normal file

Binary file not shown.

13
test/test_path.py Normal file
View File

@ -0,0 +1,13 @@
import pytest
import os
from groove import path
from groove.exceptions import ConfigurationError
def test_missing_theme_root(monkeypatch):
broken_env = {k: v for (k, v) in os.environ.items()}
broken_env['GROOVE_ON_DEMAND_ROOT'] = '/dev/null/missing'
monkeypatch.setattr(os, 'environ', broken_env)
with pytest.raises(ConfigurationError):
path.themes_root()

19
test/test_requests.py Normal file
View File

@ -0,0 +1,19 @@
from groove.webserver import requests
def test_signing():
signed = requests.encode(['foo', 'bar'], uri='fnord')
assert requests.verify(signed, signed)
def test_signing_wrong_secret_key(env):
signed = requests.encode(['foo', 'bar'], uri='fnord')
env['SECRET_KEY'] = 'wrong key'
invalid = requests.encode(['foo', 'bar'], uri='fnord')
assert not requests.verify(invalid, signed)
def test_signing_wrong_uri(env):
signed = requests.encode(['foo', 'bar'], uri='fnord')
invalid = requests.encode(['foo', 'bar'], uri='a bad guess')
assert not requests.verify(invalid, signed)

View File

@ -4,8 +4,10 @@ from pathlib import Path
from unittest.mock import MagicMock
from sqlalchemy import func
import groove.exceptions
from groove.db import scanner, track
fixture_tracks = [
"/test/Spookey Ruben/Modes of Transportation, Volume 1/Spookey Ruben - Modes of Transportation, Volume 1 - 01 Terra Magnifica.flac",
"/test/Spookey Ruben/Modes of Transportation, Volume 1/Spookey Ruben - Modes of Transportation, Volume 1 - 02 These Days Are Old.flac",
@ -62,5 +64,5 @@ def test_scanner(monkeypatch, in_memory_db, media):
def test_scanner_no_media_root(in_memory_db):
del os.environ['MEDIA_ROOT']
with pytest.raises(SystemExit):
with pytest.raises(groove.exceptions.ConfigurationError):
assert scanner.media_scanner(root=None, db=in_memory_db)

16
test/test_themes.py Normal file
View File

@ -0,0 +1,16 @@
import pytest
from groove.webserver import themes
from groove.exceptions import ThemeConfigurationError
def test_load_theme():
theme = themes.load_theme('default_theme')
assert theme.name == 'default_theme'
assert theme.author == 'Theme Author'
assert theme.author_link == 'link to my soundcloud'
assert theme.version == '1.3'
def test_load_broken_theme():
with pytest.raises(ThemeConfigurationError):
themes.load_theme('alt_theme')

View File

@ -1,13 +1,12 @@
import pytest
import os
import sys
import atheris
import bottle
from boddle import boddle
from unittest.mock import MagicMock
from groove import webserver
from groove.webserver import webserver
def test_server():
@ -16,19 +15,58 @@ def test_server():
assert bottle.response.status_code == 200
def test_auth_with_valid_credentials():
with boddle(auth=(os.environ.get('USERNAME'), os.environ.get('PASSWORD'))):
@pytest.mark.parametrize('track_id, expected', [
('1', 200),
('99', 404)
])
def test_serve_track(monkeypatch, track_id, expected, db):
monkeypatch.setattr(webserver.requests, 'verify', MagicMock())
with boddle():
response = webserver.serve_track('ignored', track_id, db=db)
assert response.status_code == expected
def test_static_not_from_theme():
with boddle():
response = webserver.serve_static('favicon.ico')
assert response.status_code == 200
assert response.body.read() == b'favicon.ico\n'
def test_static_from_theme():
with boddle():
response = webserver.serve_static('test.css')
assert response.status_code == 200
assert response.body.read() == b'/* test.css */\n'
@pytest.mark.parametrize('slug, expected', [
('non-existent-slug', False),
('playlist-one', True),
])
def test_search_playlist(slug, expected, auth, db):
with boddle(auth=auth):
response = webserver.search_playlist(slug, db)
assert response.status_code == 200
if expected:
assert slug in response.body
else:
assert response.body == {}
def test_auth_with_valid_credentials(auth):
with boddle(auth=auth):
webserver.build()
assert bottle.response.status_code == 200
@pytest.mark.skip
def test_auth_random_input():
def auth(fuzzed_input):
with boddle(auth=(fuzzed_input, fuzzed_input)):
response = webserver.build()
assert response.status_code == 401
atheris.Setup([sys.argv[0], "-atheris_runs=100000"], auth)
try:
atheris.Fuzz()
@ -44,11 +82,11 @@ def test_auth_random_input():
])
def test_playlist(db, slug, expected):
with boddle():
response = webserver.get_playlist(slug, db)
response = webserver.serve_playlist(slug, db)
assert response.status_code == expected
def test_playlist_on_empty_db(in_memory_db):
with boddle():
response = webserver.get_playlist('some-slug', in_memory_db)
response = webserver.serve_playlist('some-slug', in_memory_db)
assert response.status_code == 404