add tests
This commit is contained in:
parent
d5934a99fb
commit
72dc85ac74
|
@ -1,6 +0,0 @@
|
|||
HOST=127.0.0.1
|
||||
DEBUG=1
|
||||
USERNAME=test_username
|
||||
PASSWORD=test_password
|
||||
MEDIA_ROOT=/test
|
||||
MEDIA_GLOB=*.flac,*.mp3
|
|
@ -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)
|
||||
|
||||
|
||||
|
@ -24,47 +26,64 @@ def media_root():
|
|||
path = os.environ.get('MEDIA_ROOT', None)
|
||||
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:
|
||||
path = Path(path).expanduser()
|
||||
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")
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
24
test/fixtures/env
vendored
Normal 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
|
1
test/fixtures/media/UNKLE/Psyence Fiction/01 Guns Blazing (Drums of Death, Part 1).flac
vendored
Normal file
1
test/fixtures/media/UNKLE/Psyence Fiction/01 Guns Blazing (Drums of Death, Part 1).flac
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
DRUMS OF DEATH YALL
|
0
test/fixtures/media/foo.mp3
vendored
Normal file
0
test/fixtures/media/foo.mp3
vendored
Normal file
1
test/fixtures/static/favicon.ico
vendored
Normal file
1
test/fixtures/static/favicon.ico
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
favicon.ico
|
0
test/fixtures/themes/alt_theme/README.md
vendored
Normal file
0
test/fixtures/themes/alt_theme/README.md
vendored
Normal file
0
test/fixtures/themes/alt_theme/static/test.css
vendored
Normal file
0
test/fixtures/themes/alt_theme/static/test.css
vendored
Normal file
0
test/fixtures/themes/alt_theme/templates/playlist.tpl
vendored
Normal file
0
test/fixtures/themes/alt_theme/templates/playlist.tpl
vendored
Normal file
11
test/fixtures/themes/default_theme/README.md
vendored
Normal file
11
test/fixtures/themes/default_theme/README.md
vendored
Normal 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
|
||||
```
|
1
test/fixtures/themes/default_theme/static/test.css
vendored
Normal file
1
test/fixtures/themes/default_theme/static/test.css
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
/* test.css */
|
0
test/fixtures/themes/default_theme/templates/playlist.tpl
vendored
Normal file
0
test/fixtures/themes/default_theme/templates/playlist.tpl
vendored
Normal file
BIN
test/test.db
Normal file
BIN
test/test.db
Normal file
Binary file not shown.
13
test/test_path.py
Normal file
13
test/test_path.py
Normal 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
19
test/test_requests.py
Normal 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)
|
|
@ -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
16
test/test_themes.py
Normal 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')
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue
Block a user