mirror of
https://github.com/spl0k/supysonic.git
synced 2024-12-23 01:16:18 +00:00
Merge remote-tracking branch 'ogarcia/config'
This commit is contained in:
commit
7f736c240b
70
.gitignore
vendored
70
.gitignore
vendored
@ -1,6 +1,68 @@
|
|||||||
*.pyc
|
# ---> Python
|
||||||
.*.sw[a-z]
|
# Byte-compiled / optimized / DLL files
|
||||||
*~
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
.Python
|
||||||
|
env/
|
||||||
build/
|
build/
|
||||||
|
develop-eggs/
|
||||||
dist/
|
dist/
|
||||||
MANIFEST
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
|
||||||
|
# PyInstaller
|
||||||
|
# Usually these files are written by a python script from a template
|
||||||
|
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||||
|
*.manifest
|
||||||
|
*.spec
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
pip-log.txt
|
||||||
|
pip-delete-this-directory.txt
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
.cache
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
*,cover
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
*.mo
|
||||||
|
*.pot
|
||||||
|
|
||||||
|
# Django stuff:
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Sphinx documentation
|
||||||
|
docs/_build/
|
||||||
|
|
||||||
|
# PyBuilder
|
||||||
|
target/
|
||||||
|
|
||||||
|
# ---> Vim
|
||||||
|
[._]*.s[a-w][a-z]
|
||||||
|
[._]s[a-w][a-z]
|
||||||
|
*.un~
|
||||||
|
Session.vim
|
||||||
|
.netrwhist
|
||||||
|
*~
|
||||||
|
|
||||||
|
5
.travis.yml
Normal file
5
.travis.yml
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
language: python
|
||||||
|
python:
|
||||||
|
- "2.7"
|
||||||
|
install: "python setup.py install"
|
||||||
|
script: "python setup.py test"
|
@ -1,3 +1,5 @@
|
|||||||
include cgi-bin/*
|
include cgi-bin/*
|
||||||
include config.sample
|
include config.sample
|
||||||
include README.md
|
include README.md
|
||||||
|
recursive-include supysonic/templates *
|
||||||
|
recursive-include supysonic/static *
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
#!/usr/bin/python
|
#!/usr/bin/env python
|
||||||
# coding: utf-8
|
# coding: utf-8
|
||||||
|
|
||||||
# This file is part of Supysonic.
|
# This file is part of Supysonic.
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
#!/usr/bin/python
|
#!/usr/bin/env python
|
||||||
# coding: utf-8
|
# coding: utf-8
|
||||||
|
|
||||||
# This file is part of Supysonic.
|
# This file is part of Supysonic.
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
#!/usr/bin/python
|
#!/usr/bin/env python
|
||||||
# coding: utf-8
|
# coding: utf-8
|
||||||
|
|
||||||
# This file is part of Supysonic.
|
# This file is part of Supysonic.
|
||||||
|
@ -7,6 +7,9 @@
|
|||||||
; Optional, restrict scanner to these extensions
|
; Optional, restrict scanner to these extensions
|
||||||
; scanner_extensions = mp3 ogg
|
; scanner_extensions = mp3 ogg
|
||||||
|
|
||||||
|
; Optional for develop, key for sign the session cookies
|
||||||
|
; secret_key = verydifficultkeyword
|
||||||
|
|
||||||
[webapp]
|
[webapp]
|
||||||
; Optional cache directory
|
; Optional cache directory
|
||||||
cache_dir = /var/supysonic/cache
|
cache_dir = /var/supysonic/cache
|
||||||
|
62
setup.py
62
setup.py
@ -1,36 +1,38 @@
|
|||||||
#!/usr/bin/python
|
#!/usr/bin/env python
|
||||||
# encoding: utf-8
|
# -*- coding: utf-8 -*-
|
||||||
|
# vim:fenc=utf-8
|
||||||
|
#
|
||||||
|
# This file is part of Supysonic.
|
||||||
|
# Supysonic is a Python implementation of the Subsonic server API.
|
||||||
|
#
|
||||||
|
# Copyright (C) 2013-2017 Alban 'spl0k' Féron
|
||||||
|
# 2017 Óscar García Amor
|
||||||
|
#
|
||||||
|
# Distributed under terms of the GNU AGPLv3 license.
|
||||||
|
|
||||||
from distutils.core import setup
|
import supysonic as project
|
||||||
|
|
||||||
setup(name='supysonic',
|
from setuptools import setup
|
||||||
description='Python implementation of the Subsonic server API.',
|
from setuptools import find_packages
|
||||||
keywords='subsonic music',
|
from pip.req import parse_requirements
|
||||||
version='0.1',
|
from pip.download import PipSession
|
||||||
url='https://github.com/spl0k/supysonic',
|
|
||||||
license='AGPLv3',
|
|
||||||
author='Alban Féron',
|
|
||||||
author_email='alban.feron@gmail.com',
|
|
||||||
long_description="""
|
|
||||||
Supysonic is a Python implementation of the Subsonic server API.
|
|
||||||
|
|
||||||
Current supported features are:
|
|
||||||
|
|
||||||
* browsing (by folders or tags)
|
setup(
|
||||||
* streaming of various audio file formats
|
name=project.NAME,
|
||||||
* transcoding
|
version=project.VERSION,
|
||||||
* user or random playlists
|
description=project.DESCRIPTION,
|
||||||
* cover arts (cover.jpg files in the same folder as music files)
|
keywords=project.KEYWORDS,
|
||||||
* starred tracks/albums and ratings
|
long_description=project.LONG_DESCRIPTION,
|
||||||
* Last.FM scrobbling
|
author=project.AUTHOR_NAME,
|
||||||
""",
|
author_email=project.AUTHOR_EMAIL,
|
||||||
packages=['supysonic', 'supysonic.api', 'supysonic.frontend',
|
url=project.URL,
|
||||||
'supysonic.managers'],
|
license=project.LICENSE,
|
||||||
|
packages=find_packages(),
|
||||||
|
install_requires=[str(x.req) for x in
|
||||||
|
parse_requirements('requirements.txt', session=PipSession())],
|
||||||
scripts=['bin/supysonic-cli', 'bin/supysonic-watcher'],
|
scripts=['bin/supysonic-cli', 'bin/supysonic-watcher'],
|
||||||
package_data={'supysonic': [
|
zip_safe=False,
|
||||||
'templates/*.html',
|
include_package_data=True,
|
||||||
'static/css/*',
|
test_suite="tests.suite"
|
||||||
'static/fonts/*',
|
|
||||||
'static/js/*'
|
|
||||||
]}
|
|
||||||
)
|
)
|
||||||
|
@ -1,25 +1,28 @@
|
|||||||
# coding: utf-8
|
# -*- coding: utf-8 -*-
|
||||||
|
# vim:fenc=utf-8
|
||||||
|
#
|
||||||
# This file is part of Supysonic.
|
# This file is part of Supysonic.
|
||||||
#
|
|
||||||
# Supysonic is a Python implementation of the Subsonic server API.
|
# Supysonic is a Python implementation of the Subsonic server API.
|
||||||
|
#
|
||||||
# Copyright (C) 2013-2017 Alban 'spl0k' Féron
|
# Copyright (C) 2013-2017 Alban 'spl0k' Féron
|
||||||
|
# 2017 Óscar García Amor
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify
|
# Distributed under terms of the GNU AGPLv3 license.
|
||||||
# it under the terms of the GNU Affero 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 Affero General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
import mimetypes
|
|
||||||
|
|
||||||
def get_mime(ext):
|
|
||||||
return mimetypes.guess_type('dummy.' + ext, False)[0] or config.get('mimetypes', ext) or 'application/octet-stream'
|
|
||||||
|
|
||||||
|
NAME = 'supysonic'
|
||||||
|
VERSION = '0.2'
|
||||||
|
DESCRIPTION = 'Python implementation of the Subsonic server API.'
|
||||||
|
KEYWORDS = 'subsonic music api'
|
||||||
|
AUTHOR_NAME = 'Alban Féron'
|
||||||
|
AUTHOR_EMAIL = 'alban.feron@gmail.com'
|
||||||
|
URL = 'https://github.com/spl0k/supysonic'
|
||||||
|
LICENSE = 'GNU AGPLv3'
|
||||||
|
LONG_DESCRIPTION = '''Supysonic is a Python implementation of the Subsonic server API.
|
||||||
|
Current supported features are:
|
||||||
|
* browsing (by folders or tags)
|
||||||
|
* streaming of various audio file formats
|
||||||
|
* transcoding
|
||||||
|
* user or random playlists
|
||||||
|
* cover arts (cover.jpg files in the same folder as music files)
|
||||||
|
* starred tracks/albums and ratings
|
||||||
|
* Last.FM scrobbling'''
|
||||||
|
@ -70,7 +70,7 @@ def stream_media():
|
|||||||
|
|
||||||
if format and format != 'raw' and format != src_suffix:
|
if format and format != 'raw' and format != src_suffix:
|
||||||
dst_suffix = format
|
dst_suffix = format
|
||||||
dst_mimetype = scanner.get_mime(dst_suffix)
|
dst_mimetype = config.get_mime(dst_suffix)
|
||||||
|
|
||||||
if format != 'raw' and (dst_suffix != src_suffix or dst_bitrate != res.bitrate):
|
if format != 'raw' and (dst_suffix != src_suffix or dst_bitrate != res.bitrate):
|
||||||
transcoder = config.get('transcoding', 'transcoder_{}_{}'.format(src_suffix, dst_suffix))
|
transcoder = config.get('transcoding', 'transcoder_{}_{}'.format(src_suffix, dst_suffix))
|
||||||
|
@ -1,49 +1,71 @@
|
|||||||
# coding: utf-8
|
# -*- coding: utf-8 -*-
|
||||||
|
# vim:fenc=utf-8
|
||||||
|
#
|
||||||
# This file is part of Supysonic.
|
# This file is part of Supysonic.
|
||||||
#
|
|
||||||
# Supysonic is a Python implementation of the Subsonic server API.
|
# Supysonic is a Python implementation of the Subsonic server API.
|
||||||
# Copyright (C) 2013 Alban 'spl0k' Féron
|
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify
|
# Copyright (C) 2013-2017 Alban 'spl0k' Féron
|
||||||
# it under the terms of the GNU Affero General Public License as published by
|
# 2017 Óscar García Amor
|
||||||
# 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,
|
# Distributed under terms of the GNU AGPLv3 license.
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU Affero General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
import os, sys, tempfile, ConfigParser
|
from ConfigParser import ConfigParser, NoOptionError, NoSectionError
|
||||||
|
|
||||||
|
import mimetypes
|
||||||
|
import os
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
# Seek for standard locations
|
||||||
|
config_file = [
|
||||||
|
'supysonic.conf',
|
||||||
|
os.path.expanduser('~/.config/supysonic/supysonic.conf'),
|
||||||
|
os.path.expanduser('~/.supysonic'),
|
||||||
|
'/etc/supysonic'
|
||||||
|
]
|
||||||
|
|
||||||
|
config = ConfigParser({ 'cache_dir': os.path.join(tempfile.gettempdir(), 'supysonic') })
|
||||||
|
|
||||||
config = ConfigParser.RawConfigParser({ 'cache_dir': os.path.join(tempfile.gettempdir(), 'supysonic') })
|
|
||||||
|
|
||||||
def check():
|
def check():
|
||||||
|
"""
|
||||||
|
Checks the config file and mandatory fields
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
ret = config.read([ '/etc/supysonic', os.path.expanduser('~/.supysonic') ])
|
config.read(config_file)
|
||||||
except (ConfigParser.MissingSectionHeaderError, ConfigParser.ParsingError), e:
|
except Exception as e:
|
||||||
print >>sys.stderr, "Error while parsing the configuration file(s):\n%s" % str(e)
|
err = 'Config file is corrupted.\n{0}'.format(e)
|
||||||
return False
|
raise SystemExit(err)
|
||||||
|
|
||||||
if not ret:
|
|
||||||
print >>sys.stderr, "No configuration file found"
|
|
||||||
return False
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
config.get('base', 'database_uri')
|
config.get('base', 'database_uri')
|
||||||
except:
|
except (NoSectionError, NoOptionError):
|
||||||
print >>sys.stderr, "No database URI set"
|
raise SystemExit('No database URI set')
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def get(section, name):
|
def get(section, option):
|
||||||
|
"""
|
||||||
|
Returns a config option value from config file
|
||||||
|
|
||||||
|
:param section: section where the option is stored
|
||||||
|
:param option: option name
|
||||||
|
:return: a config option value
|
||||||
|
:rtype: string
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
return config.get(section, name)
|
return config.get(section, option)
|
||||||
except:
|
except (NoSectionError, NoOptionError):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def get_mime(extension):
|
||||||
|
"""
|
||||||
|
Returns mimetype of an extension based on config file
|
||||||
|
|
||||||
|
:param extension: extension string
|
||||||
|
:return: mimetype
|
||||||
|
:rtype: string
|
||||||
|
"""
|
||||||
|
guessed_mime = mimetypes.guess_type('dummy.' + extension, False)[0]
|
||||||
|
config_mime = get('mimetypes', extension)
|
||||||
|
default_mime = 'application/octet-stream'
|
||||||
|
return guessed_mime or config_mime or default_mime
|
||||||
|
@ -27,7 +27,7 @@ from storm.variables import Variable
|
|||||||
import uuid, datetime, time
|
import uuid, datetime, time
|
||||||
import os.path
|
import os.path
|
||||||
|
|
||||||
from supysonic import get_mime
|
from supysonic import config
|
||||||
|
|
||||||
def now():
|
def now():
|
||||||
return datetime.datetime.now().replace(microsecond = 0)
|
return datetime.datetime.now().replace(microsecond = 0)
|
||||||
@ -213,7 +213,7 @@ class Track(object):
|
|||||||
|
|
||||||
if prefs and prefs.format and prefs.format != self.suffix():
|
if prefs and prefs.format and prefs.format != self.suffix():
|
||||||
info['transcodedSuffix'] = prefs.format
|
info['transcodedSuffix'] = prefs.format
|
||||||
info['transcodedContentType'] = get_mime(prefs.format)
|
info['transcodedContentType'] = config.get_mime(prefs.format)
|
||||||
|
|
||||||
return info
|
return info
|
||||||
|
|
||||||
|
@ -1,22 +1,13 @@
|
|||||||
# coding: utf-8
|
# -*- coding: utf-8 -*-
|
||||||
|
# vim:fenc=utf-8
|
||||||
|
#
|
||||||
# This file is part of Supysonic.
|
# This file is part of Supysonic.
|
||||||
#
|
|
||||||
# Supysonic is a Python implementation of the Subsonic server API.
|
# Supysonic is a Python implementation of the Subsonic server API.
|
||||||
# Copyright (C) 2014 Alban 'spl0k' Féron
|
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify
|
# Copyright (C) 2013-2017 Alban 'spl0k' Féron
|
||||||
# it under the terms of the GNU Affero General Public License as published by
|
# 2017 Óscar García Amor
|
||||||
# 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,
|
# Distributed under terms of the GNU AGPLv3 license.
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU Affero General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
from flask import session
|
from flask import session
|
||||||
from supysonic.web import app, store
|
from supysonic.web import app, store
|
||||||
@ -40,6 +31,9 @@ def login_check():
|
|||||||
elif UserManager.get(store, session.get('userid'))[0] != UserManager.SUCCESS:
|
elif UserManager.get(store, session.get('userid'))[0] != UserManager.SUCCESS:
|
||||||
session.clear()
|
session.clear()
|
||||||
should_login = True
|
should_login = True
|
||||||
|
elif UserManager.get(store, session.get('userid'))[1].name != session.get('username'):
|
||||||
|
session.clear()
|
||||||
|
should_login = True
|
||||||
|
|
||||||
if should_login:
|
if should_login:
|
||||||
flash('Please login')
|
flash('Please login')
|
||||||
@ -57,4 +51,3 @@ def index():
|
|||||||
from .user import *
|
from .user import *
|
||||||
from .folder import *
|
from .folder import *
|
||||||
from .playlist import *
|
from .playlist import *
|
||||||
|
|
||||||
|
@ -1,24 +1,18 @@
|
|||||||
# coding: utf-8
|
# -*- coding: utf-8 -*-
|
||||||
|
# vim:fenc=utf-8
|
||||||
|
#
|
||||||
# This file is part of Supysonic.
|
# This file is part of Supysonic.
|
||||||
#
|
|
||||||
# Supysonic is a Python implementation of the Subsonic server API.
|
# Supysonic is a Python implementation of the Subsonic server API.
|
||||||
# Copyright (C) 2013 Alban 'spl0k' Féron
|
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify
|
# Copyright (C) 2013-2017 Alban 'spl0k' Féron
|
||||||
# it under the terms of the GNU Affero General Public License as published by
|
# 2017 Óscar García Amor
|
||||||
# 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,
|
# Distributed under terms of the GNU AGPLv3 license.
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU Affero General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
import string, random, hashlib
|
import binascii
|
||||||
|
import string
|
||||||
|
import random
|
||||||
|
import hashlib
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
from supysonic.db import User, ChatMessage, Playlist
|
from supysonic.db import User, ChatMessage, Playlist
|
||||||
@ -149,17 +143,14 @@ class UserManager:
|
|||||||
def __encrypt_password(password, salt = None):
|
def __encrypt_password(password, salt = None):
|
||||||
if salt is None:
|
if salt is None:
|
||||||
salt = ''.join(random.choice(string.printable.strip()) for i in xrange(6))
|
salt = ''.join(random.choice(string.printable.strip()) for i in xrange(6))
|
||||||
return hashlib.sha1(salt + password).hexdigest(), salt
|
return hashlib.sha1(salt.encode('utf-8') + password.encode('utf-8')).hexdigest(), salt
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def __decode_password(password):
|
def __decode_password(password):
|
||||||
if not password.startswith('enc:'):
|
if not password.startswith('enc:'):
|
||||||
return password
|
return password
|
||||||
|
|
||||||
enc = password[4:]
|
try:
|
||||||
ret = ''
|
return binascii.unhexlify(password[4:]).decode('utf-8')
|
||||||
while enc:
|
except:
|
||||||
ret = ret + chr(int(enc[:2], 16))
|
return password
|
||||||
enc = enc[2:]
|
|
||||||
return ret
|
|
||||||
|
|
||||||
|
@ -25,7 +25,7 @@ import mutagen
|
|||||||
from storm.expr import ComparableExpr, compile, Like
|
from storm.expr import ComparableExpr, compile, Like
|
||||||
from storm.exceptions import NotSupportedError
|
from storm.exceptions import NotSupportedError
|
||||||
|
|
||||||
from supysonic import config, get_mime
|
from supysonic import config
|
||||||
from supysonic.db import Folder, Artist, Album, Track, User, PlaylistTrack
|
from supysonic.db import Folder, Artist, Album, Track, User, PlaylistTrack
|
||||||
from supysonic.db import StarredFolder, StarredArtist, StarredAlbum, StarredTrack
|
from supysonic.db import StarredFolder, StarredArtist, StarredAlbum, StarredTrack
|
||||||
from supysonic.db import RatingFolder, RatingTrack
|
from supysonic.db import RatingFolder, RatingTrack
|
||||||
@ -166,7 +166,7 @@ class Scanner:
|
|||||||
tr.duration = int(tag.info.length)
|
tr.duration = int(tag.info.length)
|
||||||
|
|
||||||
tr.bitrate = (tag.info.bitrate if hasattr(tag.info, 'bitrate') else int(os.path.getsize(path) * 8 / tag.info.length)) / 1000
|
tr.bitrate = (tag.info.bitrate if hasattr(tag.info, 'bitrate') else int(os.path.getsize(path) * 8 / tag.info.length)) / 1000
|
||||||
tr.content_type = get_mime(os.path.splitext(path)[1][1:])
|
tr.content_type = config.get_mime(os.path.splitext(path)[1][1:])
|
||||||
tr.last_modification = os.path.getmtime(path)
|
tr.last_modification = os.path.getmtime(path)
|
||||||
|
|
||||||
tralbum = self.__find_album(albumartist, album)
|
tralbum = self.__find_album(albumartist, album)
|
||||||
|
@ -1,58 +1,59 @@
|
|||||||
# coding: utf-8
|
# -*- coding: utf-8 -*-
|
||||||
|
# vim:fenc=utf-8
|
||||||
|
#
|
||||||
# This file is part of Supysonic.
|
# This file is part of Supysonic.
|
||||||
#
|
|
||||||
# Supysonic is a Python implementation of the Subsonic server API.
|
# Supysonic is a Python implementation of the Subsonic server API.
|
||||||
# Copyright (C) 2013 Alban 'spl0k' Féron
|
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify
|
# Copyright (C) 2013-2017 Alban 'spl0k' Féron
|
||||||
# it under the terms of the GNU Affero General Public License as published by
|
# 2017 Óscar García Amor
|
||||||
# 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,
|
# Distributed under terms of the GNU AGPLv3 license.
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU Affero General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
import os.path
|
|
||||||
from flask import Flask, g
|
from flask import Flask, g
|
||||||
|
from os import makedirs, path
|
||||||
from werkzeug.local import LocalProxy
|
from werkzeug.local import LocalProxy
|
||||||
|
|
||||||
from supysonic import config
|
from supysonic import config
|
||||||
from supysonic.db import get_store
|
from supysonic.db import get_store
|
||||||
|
|
||||||
def get_db_store():
|
# Supysonic database open
|
||||||
store = getattr(g, 'store', None)
|
def get_db():
|
||||||
if store:
|
if not hasattr(g, 'database'):
|
||||||
return store
|
g.database = get_store(config.get('base', 'database_uri'))
|
||||||
g.store = get_store(config.get('base', 'database_uri'))
|
return g.database
|
||||||
return g.store
|
|
||||||
|
|
||||||
store = LocalProxy(get_db_store)
|
# Supysonic database close
|
||||||
|
def close_db(error):
|
||||||
|
if hasattr(g, 'database'):
|
||||||
|
g.database.close()
|
||||||
|
|
||||||
def teardown_db(exception):
|
store = LocalProxy(get_db)
|
||||||
store = getattr(g, 'store', None)
|
|
||||||
if store:
|
|
||||||
store.close()
|
|
||||||
|
|
||||||
def create_application():
|
def create_application():
|
||||||
global app
|
global app
|
||||||
|
|
||||||
if not config.check():
|
# Check config for mandatory fields
|
||||||
return None
|
config.check()
|
||||||
|
|
||||||
if not os.path.exists(config.get('webapp', 'cache_dir')):
|
# Test for the cache directory
|
||||||
|
if not path.exists(config.get('webapp', 'cache_dir')):
|
||||||
os.makedirs(config.get('webapp', 'cache_dir'))
|
os.makedirs(config.get('webapp', 'cache_dir'))
|
||||||
|
|
||||||
|
# Flask!
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
app.secret_key = '?9huDM\\H'
|
|
||||||
|
|
||||||
app.teardown_appcontext(teardown_db)
|
# Set a secret key for sessions
|
||||||
|
secret_key = config.get('base', 'secret_key')
|
||||||
|
# If secret key is not defined in config, set develop key
|
||||||
|
if secret_key is None:
|
||||||
|
app.secret_key = 'd3v3l0p'
|
||||||
|
else:
|
||||||
|
app.secret_key = secret_key
|
||||||
|
|
||||||
|
# Close database connection on teardown
|
||||||
|
app.teardown_appcontext(close_db)
|
||||||
|
|
||||||
|
# Set loglevel
|
||||||
if config.get('webapp', 'log_file'):
|
if config.get('webapp', 'log_file'):
|
||||||
import logging
|
import logging
|
||||||
from logging.handlers import TimedRotatingFileHandler
|
from logging.handlers import TimedRotatingFileHandler
|
||||||
@ -68,8 +69,8 @@ def create_application():
|
|||||||
handler.setLevel(mapping.get(config.get('webapp', 'log_level').upper(), logging.NOTSET))
|
handler.setLevel(mapping.get(config.get('webapp', 'log_level').upper(), logging.NOTSET))
|
||||||
app.logger.addHandler(handler)
|
app.logger.addHandler(handler)
|
||||||
|
|
||||||
|
# Import app sections
|
||||||
from supysonic import frontend
|
from supysonic import frontend
|
||||||
from supysonic import api
|
from supysonic import api
|
||||||
|
|
||||||
return app
|
return app
|
||||||
|
|
||||||
|
25
tests/__init__.py
Normal file
25
tests/__init__.py
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# vim:fenc=utf-8
|
||||||
|
#
|
||||||
|
# This file is part of Supysonic.
|
||||||
|
# Supysonic is a Python implementation of the Subsonic server API.
|
||||||
|
#
|
||||||
|
# Copyright (C) 2013-2017 Alban 'spl0k' Féron
|
||||||
|
# 2017 Óscar García Amor
|
||||||
|
#
|
||||||
|
# Distributed under terms of the GNU AGPLv3 license.
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from .test_manager_folder import FolderManagerTestCase
|
||||||
|
from .test_manager_user import UserManagerTestCase
|
||||||
|
from .test_api import ApiTestCase
|
||||||
|
from .test_frontend import FrontendTestCase
|
||||||
|
|
||||||
|
def suite():
|
||||||
|
suite = unittest.TestSuite()
|
||||||
|
suite.addTest(unittest.makeSuite(FolderManagerTestCase))
|
||||||
|
suite.addTest(unittest.makeSuite(UserManagerTestCase))
|
||||||
|
suite.addTest(unittest.makeSuite(ApiTestCase))
|
||||||
|
suite.addTest(unittest.makeSuite(FrontendTestCase))
|
||||||
|
return suite
|
297
tests/test_api.py
Normal file
297
tests/test_api.py
Normal file
@ -0,0 +1,297 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# vim:fenc=utf-8
|
||||||
|
#
|
||||||
|
# This file is part of Supysonic.
|
||||||
|
# Supysonic is a Python implementation of the Subsonic server API.
|
||||||
|
#
|
||||||
|
# Copyright (C) 2013-2017 Alban 'spl0k' Féron
|
||||||
|
# 2017 Óscar García Amor
|
||||||
|
#
|
||||||
|
# Distributed under terms of the GNU AGPLv3 license.
|
||||||
|
|
||||||
|
from supysonic import db
|
||||||
|
from supysonic.managers.user import UserManager
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import unittest
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
# Create an empty sqlite database in memory
|
||||||
|
store = db.get_store("sqlite:")
|
||||||
|
# Read schema from file
|
||||||
|
with open('schema/sqlite.sql') as sql:
|
||||||
|
schema = sql.read()
|
||||||
|
# Create tables on memory database
|
||||||
|
for command in schema.split(';'):
|
||||||
|
store.execute(command)
|
||||||
|
# Create some users
|
||||||
|
UserManager.add(store, 'alice', 'alice', 'test@example.com', True)
|
||||||
|
UserManager.add(store, 'bob', 'bob', 'bob@example.com', False)
|
||||||
|
UserManager.add(store, 'charlie', 'charlie', 'charlie@example.com', False)
|
||||||
|
|
||||||
|
# Create a mockup of web
|
||||||
|
from flask import Flask
|
||||||
|
app = Flask(__name__)
|
||||||
|
class web():
|
||||||
|
app = app
|
||||||
|
store = store
|
||||||
|
sys.modules['supysonic.web'] = web()
|
||||||
|
|
||||||
|
# Import module and set app in test mode
|
||||||
|
import supysonic.api
|
||||||
|
|
||||||
|
class ApiTestCase(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.app = app.test_client()
|
||||||
|
|
||||||
|
def test_ping(self):
|
||||||
|
# GET non-existent user
|
||||||
|
rv = self.app.get('/rest/ping.view?u=null&p=null&c=test')
|
||||||
|
self.assertIn('status="failed"', rv.data)
|
||||||
|
self.assertIn('message="Unauthorized"', rv.data)
|
||||||
|
# POST non-existent user
|
||||||
|
rv = self.app.post('/rest/ping.view', data=dict(u='null', p='null', c='test'))
|
||||||
|
self.assertIn('status="failed"', rv.data)
|
||||||
|
self.assertIn('message="Unauthorized"', rv.data)
|
||||||
|
# GET user request
|
||||||
|
rv = self.app.get('/rest/ping.view?u=alice&p=alice&c=test')
|
||||||
|
self.assertIn('status="ok"', rv.data)
|
||||||
|
# POST user request
|
||||||
|
rv = self.app.post('/rest/ping.view', data=dict(u='alice', p='alice', c='test'))
|
||||||
|
self.assertIn('status="ok"', rv.data)
|
||||||
|
# GET user request with old enc:
|
||||||
|
rv = self.app.get('/rest/ping.view?u=alice&p=enc:616c696365&c=test')
|
||||||
|
self.assertIn('status="ok"', rv.data)
|
||||||
|
# POST user request with old enc:
|
||||||
|
rv = self.app.post('/rest/ping.view', data=dict(u='alice', p='enc:616c696365', c='test'))
|
||||||
|
self.assertIn('status="ok"', rv.data)
|
||||||
|
# GET user request with bad password
|
||||||
|
rv = self.app.get('/rest/ping.view?u=alice&p=bad&c=test')
|
||||||
|
self.assertIn('status="failed"', rv.data)
|
||||||
|
self.assertIn('message="Unauthorized"', rv.data)
|
||||||
|
# POST user request with bad password
|
||||||
|
rv = self.app.post('/rest/ping.view', data=dict(u='alice', p='bad', c='test'))
|
||||||
|
self.assertIn('status="failed"', rv.data)
|
||||||
|
self.assertIn('message="Unauthorized"', rv.data)
|
||||||
|
|
||||||
|
def test_ping_in_jsonp(self):
|
||||||
|
# If ping in jsonp works all other endpoints must work OK
|
||||||
|
# GET non-existent user
|
||||||
|
rv = self.app.get('/rest/ping.view?u=null&p=null&c=test&f=jsonp&callback=test')
|
||||||
|
self.assertIn('"status": "failed"', rv.data)
|
||||||
|
self.assertIn('"message": "Unauthorized"', rv.data)
|
||||||
|
# POST non-existent user
|
||||||
|
rv = self.app.post('/rest/ping.view', data=dict(u='null', p='null', c='test', f='jsonp', callback='test'))
|
||||||
|
self.assertIn('"status": "failed"', rv.data)
|
||||||
|
self.assertIn('"message": "Unauthorized"', rv.data)
|
||||||
|
# GET user request
|
||||||
|
rv = self.app.get('/rest/ping.view?u=alice&p=alice&c=test&f=jsonp&callback=test')
|
||||||
|
self.assertIn('"status": "ok"', rv.data)
|
||||||
|
# POST user request
|
||||||
|
rv = self.app.post('/rest/ping.view', data=dict(u='alice', p='alice', c='test', f='jsonp', callback='test'))
|
||||||
|
self.assertIn('"status": "ok"', rv.data)
|
||||||
|
# GET user request with bad password
|
||||||
|
rv = self.app.get('/rest/ping.view?u=alice&p=bad&c=test&f=jsonp&callback=test')
|
||||||
|
self.assertIn('"status": "failed"', rv.data)
|
||||||
|
self.assertIn('"message": "Unauthorized"', rv.data)
|
||||||
|
# POST user request with bad password
|
||||||
|
rv = self.app.post('/rest/ping.view', data=dict(u='alice', p='bad', c='test', f='jsonp', callback='test'))
|
||||||
|
self.assertIn('"status": "failed"', rv.data)
|
||||||
|
self.assertIn('"message": "Unauthorized"', rv.data)
|
||||||
|
|
||||||
|
def test_not_implemented(self):
|
||||||
|
# Access to not implemented endpoint
|
||||||
|
rv = self.app.get('/rest/not-implemented?u=alice&p=alice&c=test')
|
||||||
|
self.assertIn('message="Not implemented"', rv.data)
|
||||||
|
rv = self.app.post('/rest/not-implemented', data=dict(u='alice', p='alice', c='test'))
|
||||||
|
self.assertIn('message="Not implemented"', rv.data)
|
||||||
|
|
||||||
|
def test_get_license(self):
|
||||||
|
# GET user request
|
||||||
|
rv = self.app.get('/rest/getLicense.view?u=alice&p=alice&c=test')
|
||||||
|
self.assertIn('status="ok"', rv.data)
|
||||||
|
self.assertIn('license valid="true"', rv.data)
|
||||||
|
# POST user request
|
||||||
|
rv = self.app.post('/rest/getLicense.view', data=dict(u='alice', p='alice', c='test'))
|
||||||
|
self.assertIn('status="ok"', rv.data)
|
||||||
|
self.assertIn('license valid="true"', rv.data)
|
||||||
|
|
||||||
|
def test_get_user(self):
|
||||||
|
# GET missing username
|
||||||
|
rv = self.app.get('/rest/getUser.view?u=alice&p=alice&c=test')
|
||||||
|
self.assertIn('message="Missing username"', rv.data)
|
||||||
|
# POST missing username
|
||||||
|
rv = self.app.post('/rest/getUser.view', data=dict(u='alice', p='alice', c='test'))
|
||||||
|
self.assertIn('message="Missing username"', rv.data)
|
||||||
|
# GET non-admin request for other user
|
||||||
|
rv = self.app.get('/rest/getUser.view?u=bob&p=bob&c=test&username=alice')
|
||||||
|
self.assertIn('message="Admin restricted"', rv.data)
|
||||||
|
# POST non-admin request for other user
|
||||||
|
rv = self.app.post('/rest/getUser.view', data=dict(u='bob', p='bob', c='test', username='alice'))
|
||||||
|
self.assertIn('message="Admin restricted"', rv.data)
|
||||||
|
# GET non-existent user
|
||||||
|
rv = self.app.get('/rest/getUser.view?u=alice&p=alice&c=test&username=null')
|
||||||
|
self.assertIn('message="Unknown user"', rv.data)
|
||||||
|
# POST non-existent user
|
||||||
|
rv = self.app.post('/rest/getUser.view', data=dict(u='alice', p='alice', c='test', username='null'))
|
||||||
|
self.assertIn('message="Unknown user"', rv.data)
|
||||||
|
# GET admin request
|
||||||
|
rv = self.app.get('/rest/getUser.view?u=alice&p=alice&c=test&username=alice')
|
||||||
|
self.assertIn('adminRole="true"', rv.data)
|
||||||
|
self.assertIn('username="alice"', rv.data)
|
||||||
|
# POST admin request
|
||||||
|
rv = self.app.post('/rest/getUser.view', data=dict(u='alice', p='alice', c='test', username='alice'))
|
||||||
|
self.assertIn('adminRole="true"', rv.data)
|
||||||
|
self.assertIn('username="alice"', rv.data)
|
||||||
|
# GET admin request for other user
|
||||||
|
rv = self.app.get('/rest/getUser.view?u=alice&p=alice&c=test&username=bob')
|
||||||
|
self.assertIn('username="bob"', rv.data)
|
||||||
|
self.assertIn('adminRole="false"', rv.data)
|
||||||
|
# POST admin request for other user
|
||||||
|
rv = self.app.post('/rest/getUser.view', data=dict(u='alice', p='alice', c='test', username='bob'))
|
||||||
|
self.assertIn('username="bob"', rv.data)
|
||||||
|
self.assertIn('adminRole="false"', rv.data)
|
||||||
|
# GET non-admin request
|
||||||
|
rv = self.app.get('/rest/getUser.view?u=charlie&p=charlie&c=test&username=charlie')
|
||||||
|
self.assertIn('username="charlie"', rv.data)
|
||||||
|
self.assertIn('adminRole="false"', rv.data)
|
||||||
|
# POST non-admin request
|
||||||
|
rv = self.app.post('/rest/getUser.view', data=dict(u='charlie', p='charlie', c='test', username='charlie'))
|
||||||
|
self.assertIn('username="charlie"', rv.data)
|
||||||
|
self.assertIn('adminRole="false"', rv.data)
|
||||||
|
|
||||||
|
def test_get_users(self):
|
||||||
|
# GET admin request
|
||||||
|
rv = self.app.get('/rest/getUsers.view?u=alice&p=alice&c=test')
|
||||||
|
self.assertIn('alice', rv.data)
|
||||||
|
self.assertIn('bob', rv.data)
|
||||||
|
self.assertIn('charlie', rv.data)
|
||||||
|
# POST admin request
|
||||||
|
rv = self.app.post('/rest/getUsers.view', data=dict(u='alice', p='alice', c='test'))
|
||||||
|
self.assertIn('alice', rv.data)
|
||||||
|
self.assertIn('bob', rv.data)
|
||||||
|
self.assertIn('charlie', rv.data)
|
||||||
|
# GET non-admin request
|
||||||
|
rv = self.app.get('/rest/getUsers.view?u=bob&p=bob&c=test')
|
||||||
|
self.assertIn('message="Admin restricted"', rv.data)
|
||||||
|
# POST non-admin request
|
||||||
|
rv = self.app.post('/rest/getUsers.view', data=dict(u='bob', p='bob', c='test'))
|
||||||
|
self.assertIn('message="Admin restricted"', rv.data)
|
||||||
|
|
||||||
|
def test_create_user(self):
|
||||||
|
# GET non-admin request
|
||||||
|
rv = self.app.get('/rest/createUser.view?u=bob&p=bob&c=test')
|
||||||
|
self.assertIn('message="Admin restricted"', rv.data)
|
||||||
|
# POST non-admin request
|
||||||
|
rv = self.app.post('/rest/createUser.view', data=dict(u='bob', p='bob', c='test'))
|
||||||
|
self.assertIn('message="Admin restricted"', rv.data)
|
||||||
|
# GET incomplete request
|
||||||
|
rv = self.app.get('/rest/createUser.view?u=alice&p=alice&c=test')
|
||||||
|
self.assertIn('message="Missing parameter"', rv.data)
|
||||||
|
# POST incomplete request
|
||||||
|
rv = self.app.post('/rest/createUser.view', data=dict(u='alice', p='alice', c='test'))
|
||||||
|
self.assertIn('message="Missing parameter"', rv.data)
|
||||||
|
# GET create user and test that user is created
|
||||||
|
rv = self.app.get('/rest/createUser.view?u=alice&p=alice&c=test&username=david&password=david&email=david%40example.com&adminRole=True')
|
||||||
|
self.assertIn('status="ok"', rv.data)
|
||||||
|
rv = self.app.get('/rest/getUser.view?u=david&p=david&c=test&username=david')
|
||||||
|
self.assertIn('username="david"', rv.data)
|
||||||
|
self.assertIn('email="david@example.com"', rv.data)
|
||||||
|
self.assertIn('adminRole="true"', rv.data)
|
||||||
|
# POST create user and test that user is created
|
||||||
|
rv = self.app.post('/rest/createUser.view', data=dict(u='alice', p='alice', c='test', username='elanor', password='elanor', email='elanor@example.com', adminRole=True))
|
||||||
|
self.assertIn('status="ok"', rv.data)
|
||||||
|
rv = self.app.post('/rest/getUser.view', data=dict(u='elanor', p='elanor', c='test', username='elanor'))
|
||||||
|
self.assertIn('username="elanor"', rv.data)
|
||||||
|
self.assertIn('email="elanor@example.com"', rv.data)
|
||||||
|
self.assertIn('adminRole="true"', rv.data)
|
||||||
|
# GET create duplicate
|
||||||
|
rv = self.app.get('/rest/createUser.view?u=alice&p=alice&c=test&username=david&password=david&email=david%40example.com&adminRole=True')
|
||||||
|
self.assertIn('message="There is already a user with that username"', rv.data)
|
||||||
|
# POST create duplicate
|
||||||
|
rv = self.app.post('/rest/createUser.view', data=dict(u='alice', p='alice', c='test', username='elanor', password='elanor', email='elanor@example.com', adminRole=True))
|
||||||
|
self.assertIn('message="There is already a user with that username"', rv.data)
|
||||||
|
|
||||||
|
def test_delete_user(self):
|
||||||
|
# GET non-admin request
|
||||||
|
rv = self.app.get('/rest/deleteUser.view?u=bob&p=bob&c=test')
|
||||||
|
self.assertIn('message="Admin restricted"', rv.data)
|
||||||
|
# POST non-admin request
|
||||||
|
rv = self.app.post('/rest/deleteUser.view', data=dict(u='bob', p='bob', c='test'))
|
||||||
|
self.assertIn('message="Admin restricted"', rv.data)
|
||||||
|
# GET incomplete request
|
||||||
|
rv = self.app.get('/rest/deleteUser.view?u=alice&p=alice&c=test')
|
||||||
|
self.assertIn('message="Unknown user"', rv.data)
|
||||||
|
# POST incomplete request
|
||||||
|
rv = self.app.post('/rest/deleteUser.view', data=dict(u='alice', p='alice', c='test'))
|
||||||
|
self.assertIn('message="Unknown user"', rv.data)
|
||||||
|
# GET delete non-existent user
|
||||||
|
rv = self.app.get('/rest/deleteUser.view?u=alice&p=alice&c=test&username=nonexistent')
|
||||||
|
self.assertIn('message="Unknown user"', rv.data)
|
||||||
|
# POST delete non-existent user
|
||||||
|
rv = self.app.post('/rest/deleteUser.view', data=dict(u='alice', p='alice', c='test', username='nonexistent'))
|
||||||
|
self.assertIn('message="Unknown user"', rv.data)
|
||||||
|
# GET delete existent user
|
||||||
|
rv = self.app.get('/rest/deleteUser.view?u=alice&p=alice&c=test&username=elanor')
|
||||||
|
self.assertIn('status="ok"', rv.data)
|
||||||
|
rv = self.app.get('/rest/getUser.view?u=alice&p=alice&c=test&username=elanor')
|
||||||
|
self.assertIn('message="Unknown user"', rv.data)
|
||||||
|
# POST delete existent user
|
||||||
|
rv = self.app.post('/rest/deleteUser.view', data=dict(u='alice', p='alice', c='test', username='david'))
|
||||||
|
self.assertIn('status="ok"', rv.data)
|
||||||
|
rv = self.app.post('/rest/getUser.view', data=dict(u='alice', p='alice', c='test', username='david'))
|
||||||
|
self.assertIn('message="Unknown user"', rv.data)
|
||||||
|
|
||||||
|
def test_change_password(self):
|
||||||
|
# GET incomplete request
|
||||||
|
rv = self.app.get('/rest/changePassword.view?u=alice&p=alice&c=test')
|
||||||
|
self.assertIn('message="Missing parameter"', rv.data)
|
||||||
|
# POST incomplete request
|
||||||
|
rv = self.app.post('/rest/changePassword.view', data=dict(u='alice', p='alice', c='test'))
|
||||||
|
self.assertIn('message="Missing parameter"', rv.data)
|
||||||
|
# GET non-admin change own password
|
||||||
|
rv = self.app.get('/rest/changePassword.view?u=bob&p=bob&c=test&username=bob&password=newpassword')
|
||||||
|
self.assertIn('status="ok"', rv.data)
|
||||||
|
# POST non-admin change own password
|
||||||
|
rv = self.app.post('/rest/changePassword.view', data=dict(u='bob', p='newpassword', c='test', username='bob', password='bob'))
|
||||||
|
self.assertIn('status="ok"', rv.data)
|
||||||
|
# GET non-admin change other user password
|
||||||
|
rv = self.app.get('/rest/changePassword.view?u=bob&p=bob&c=test&username=alice&password=newpassword')
|
||||||
|
self.assertIn('message="Admin restricted"', rv.data)
|
||||||
|
# POST non-admin change other user password
|
||||||
|
rv = self.app.post('/rest/changePassword.view', data=dict(u='bob', p='bob', c='test', username='alice', password='newpassword'))
|
||||||
|
self.assertIn('message="Admin restricted"', rv.data)
|
||||||
|
# GET admin change other user password
|
||||||
|
rv = self.app.get('/rest/changePassword.view?u=bob&p=bob&c=test&username=bob&password=newpassword')
|
||||||
|
self.assertIn('status="ok"', rv.data)
|
||||||
|
# POST admin change other user password
|
||||||
|
rv = self.app.post('/rest/changePassword.view', data=dict(u='bob', p='newpassword', c='test', username='bob', password='bob'))
|
||||||
|
self.assertIn('status="ok"', rv.data)
|
||||||
|
# GET change non-existent user password
|
||||||
|
rv = self.app.get('/rest/changePassword.view?u=alice&p=alice&c=test&username=nonexistent&password=nonexistent')
|
||||||
|
self.assertIn('message="No such user"', rv.data)
|
||||||
|
# POST change non-existent user password
|
||||||
|
rv = self.app.post('/rest/changePassword.view', data=dict(u='alice', p='alice', c='test', username='nonexistent', password='nonexistent'))
|
||||||
|
self.assertIn('message="No such user"', rv.data)
|
||||||
|
# GET non-admin change own password using extended utf-8 characters
|
||||||
|
rv = self.app.get('/rest/changePassword.view?u=bob&p=bob&c=test&username=bob&password=новыйпароль')
|
||||||
|
self.assertIn('status="ok"', rv.data)
|
||||||
|
# POST non-admin change own password using extended utf-8 characters
|
||||||
|
rv = self.app.post('/rest/changePassword.view', data=dict(u='bob', p='новыйпароль', c='test', username='bob', password='bob'))
|
||||||
|
self.assertIn('status="ok"', rv.data)
|
||||||
|
# GET non-admin change own password using extended utf-8 characters with old enc:
|
||||||
|
rv = self.app.get('/rest/changePassword.view?u=bob&p=enc:626f62&c=test&username=bob&password=новыйпароль')
|
||||||
|
self.assertIn('status="ok"', rv.data)
|
||||||
|
# POST non-admin change own password using extended utf-8 characters with old enc:
|
||||||
|
rv = self.app.post('/rest/changePassword.view', data=dict(u='bob', p='enc:d0bdd0bed0b2d18bd0b9d0bfd0b0d180d0bed0bbd18c', c='test', username='bob', password='bob'))
|
||||||
|
self.assertIn('status="ok"', rv.data)
|
||||||
|
# GET non-admin change own password using enc: in password
|
||||||
|
rv = self.app.get('/rest/changePassword.view?u=bob&p=bob&c=test&username=bob&password=enc:test')
|
||||||
|
self.assertIn('status="ok"', rv.data)
|
||||||
|
# POST non-admin change own password using enc: in password
|
||||||
|
rv = self.app.post('/rest/changePassword.view', data=dict(u='bob', p='enc:test', c='test', username='bob', password='bob'))
|
||||||
|
self.assertIn('status="ok"', rv.data)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
105
tests/test_frontend.py
Normal file
105
tests/test_frontend.py
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# vim:fenc=utf-8
|
||||||
|
#
|
||||||
|
# This file is part of Supysonic.
|
||||||
|
# Supysonic is a Python implementation of the Subsonic server API.
|
||||||
|
#
|
||||||
|
# Copyright (C) 2013-2017 Alban 'spl0k' Féron
|
||||||
|
# 2017 Óscar García Amor
|
||||||
|
#
|
||||||
|
# Distributed under terms of the GNU AGPLv3 license.
|
||||||
|
|
||||||
|
from supysonic import db
|
||||||
|
from supysonic.managers.user import UserManager
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import unittest
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
# Create an empty sqlite database in memory
|
||||||
|
store = db.get_store("sqlite:")
|
||||||
|
# Read schema from file
|
||||||
|
with open('schema/sqlite.sql') as sql:
|
||||||
|
schema = sql.read()
|
||||||
|
# Create tables on memory database
|
||||||
|
for command in schema.split(';'):
|
||||||
|
store.execute(command)
|
||||||
|
# Create some users
|
||||||
|
UserManager.add(store, 'alice', 'alice', 'test@example.com', True)
|
||||||
|
UserManager.add(store, 'bob', 'bob', 'bob@example.com', False)
|
||||||
|
UserManager.add(store, 'charlie', 'charlie', 'charlie@example.com', False)
|
||||||
|
|
||||||
|
# Create a mockup of web
|
||||||
|
from flask import Flask
|
||||||
|
app = Flask(__name__, template_folder='../supysonic/templates')
|
||||||
|
class web():
|
||||||
|
app = app
|
||||||
|
store = store
|
||||||
|
sys.modules['supysonic.web'] = web()
|
||||||
|
|
||||||
|
# Import module and set app in test mode
|
||||||
|
import supysonic.frontend
|
||||||
|
app.secret_key = 'test-suite'
|
||||||
|
|
||||||
|
class FrontendTestCase(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.app = app.test_client()
|
||||||
|
|
||||||
|
def test_unauthorized_request(self):
|
||||||
|
# Unauthorized request
|
||||||
|
rv = self.app.get('/', follow_redirects=True)
|
||||||
|
self.assertIn('Please login', rv.data)
|
||||||
|
|
||||||
|
def test_login_with_bad_data(self):
|
||||||
|
# Login with not blank user or password
|
||||||
|
rv = self.app.post('/user/login', data=dict(name='', password=''), follow_redirects=True)
|
||||||
|
self.assertIn('Missing user name', rv.data)
|
||||||
|
self.assertIn('Missing password', rv.data)
|
||||||
|
# Login with not valid user or password
|
||||||
|
rv = self.app.post('/user/login', data=dict(user='nonexistent', password='nonexistent'), follow_redirects=True)
|
||||||
|
self.assertIn('No such user', rv.data)
|
||||||
|
rv = self.app.post('/user/login', data=dict(user='alice', password='badpassword'), follow_redirects=True)
|
||||||
|
self.assertIn('Wrong password', rv.data)
|
||||||
|
|
||||||
|
def test_login_admin(self):
|
||||||
|
# Login with a valid admin user
|
||||||
|
rv = self.app.post('/user/login', data=dict(user='alice', password='alice'), follow_redirects=True)
|
||||||
|
self.assertIn('Logged in', rv.data)
|
||||||
|
self.assertIn('Users', rv.data)
|
||||||
|
self.assertIn('Folders', rv.data)
|
||||||
|
|
||||||
|
def test_login_non_admin(self):
|
||||||
|
# Login with a valid non-admin user
|
||||||
|
rv = self.app.post('/user/login', data=dict(user='bob', password='bob'), follow_redirects=True)
|
||||||
|
self.assertIn('Logged in', rv.data)
|
||||||
|
# Non-admin user cannot acces to users and folders
|
||||||
|
self.assertNotIn('Users', rv.data)
|
||||||
|
self.assertNotIn('Folders', rv.data)
|
||||||
|
|
||||||
|
def test_root_with_valid_session(self):
|
||||||
|
# Root with valid session
|
||||||
|
with self.app.session_transaction() as sess:
|
||||||
|
sess['userid'] = store.find(db.User, db.User.name == 'alice').one().id
|
||||||
|
sess['username'] = 'alice'
|
||||||
|
rv = self.app.get('/', follow_redirects=True)
|
||||||
|
self.assertIn('alice', rv.data)
|
||||||
|
self.assertIn('Log out', rv.data)
|
||||||
|
self.assertIn('There\'s nothing much to see here.', rv.data)
|
||||||
|
|
||||||
|
def test_root_with_non_valid_session(self):
|
||||||
|
# Root with a no-valid session
|
||||||
|
with self.app.session_transaction() as sess:
|
||||||
|
sess['userid'] = uuid.uuid4()
|
||||||
|
sess['username'] = 'alice'
|
||||||
|
rv = self.app.get('/', follow_redirects=True)
|
||||||
|
self.assertIn('Please login', rv.data)
|
||||||
|
# Root with a no-valid user
|
||||||
|
with self.app.session_transaction() as sess:
|
||||||
|
sess['userid'] = store.find(db.User, db.User.name == 'alice').one().id
|
||||||
|
sess['username'] = 'nonexistent'
|
||||||
|
rv = self.app.get('/', follow_redirects=True)
|
||||||
|
self.assertIn('Please login', rv.data)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
91
tests/test_manager_folder.py
Normal file
91
tests/test_manager_folder.py
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# vim:fenc=utf-8
|
||||||
|
#
|
||||||
|
# This file is part of Supysonic.
|
||||||
|
# Supysonic is a Python implementation of the Subsonic server API.
|
||||||
|
#
|
||||||
|
# Copyright (C) 2013-2017 Alban 'spl0k' Féron
|
||||||
|
# 2017 Óscar García Amor
|
||||||
|
#
|
||||||
|
# Distributed under terms of the GNU AGPLv3 license.
|
||||||
|
|
||||||
|
from supysonic import db
|
||||||
|
from supysonic.managers.folder import FolderManager
|
||||||
|
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
class FolderManagerTestCase(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
# Create an empty sqlite database in memory
|
||||||
|
self.store = db.get_store("sqlite:")
|
||||||
|
# Read schema from file
|
||||||
|
with open('schema/sqlite.sql') as sql:
|
||||||
|
schema = sql.read()
|
||||||
|
# Create tables on memory database
|
||||||
|
for command in schema.split(';'):
|
||||||
|
self.store.execute(command)
|
||||||
|
# Create some temporary directories
|
||||||
|
self.media_dir = tempfile.mkdtemp()
|
||||||
|
self.music_dir = tempfile.mkdtemp()
|
||||||
|
# Add test folders
|
||||||
|
self.assertEqual(FolderManager.add(self.store, 'media', self.media_dir), FolderManager.SUCCESS)
|
||||||
|
self.assertEqual(FolderManager.add(self.store, 'music', self.music_dir), FolderManager.SUCCESS)
|
||||||
|
folder = db.Folder()
|
||||||
|
folder.root = False
|
||||||
|
folder.name = 'non-root'
|
||||||
|
folder.path = os.path.join(self.music_dir, 'subfolder')
|
||||||
|
self.store.add(folder)
|
||||||
|
self.store.commit()
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
shutil.rmtree(self.media_dir)
|
||||||
|
shutil.rmtree(self.music_dir)
|
||||||
|
|
||||||
|
def test_get_folder(self):
|
||||||
|
# Get existing folders
|
||||||
|
for name in ['media', 'music']:
|
||||||
|
folder = self.store.find(db.Folder, db.Folder.name == name, db.Folder.root == True).one()
|
||||||
|
self.assertEqual(FolderManager.get(self.store, folder.id), (FolderManager.SUCCESS, folder))
|
||||||
|
# Get with invalid UUID
|
||||||
|
self.assertEqual(FolderManager.get(self.store, 'invalid-uuid'), (FolderManager.INVALID_ID, None))
|
||||||
|
# Non-existent folder
|
||||||
|
self.assertEqual(FolderManager.get(self.store, uuid.uuid4()), (FolderManager.NO_SUCH_FOLDER, None))
|
||||||
|
|
||||||
|
def test_add_folder(self):
|
||||||
|
# Create duplicate
|
||||||
|
self.assertEqual(FolderManager.add(self.store,'media', self.media_dir), FolderManager.NAME_EXISTS)
|
||||||
|
# Duplicate path
|
||||||
|
self.assertEqual(FolderManager.add(self.store,'new-folder', self.media_dir), FolderManager.PATH_EXISTS)
|
||||||
|
# Invalid path
|
||||||
|
self.assertEqual(FolderManager.add(self.store,'invalid-path', os.path.abspath('/this/not/is/valid')), FolderManager.INVALID_PATH)
|
||||||
|
# Subfolder of already added path
|
||||||
|
os.mkdir(os.path.join(self.media_dir, 'subfolder'))
|
||||||
|
self.assertEqual(FolderManager.add(self.store,'subfolder', os.path.join(self.media_dir, 'subfolder')), FolderManager.PATH_EXISTS)
|
||||||
|
|
||||||
|
def test_delete_folder(self):
|
||||||
|
# Delete existing folders
|
||||||
|
for name in ['media', 'music']:
|
||||||
|
folder = self.store.find(db.Folder, db.Folder.name == name, db.Folder.root == True).one()
|
||||||
|
self.assertEqual(FolderManager.delete(self.store, folder.id), FolderManager.SUCCESS)
|
||||||
|
# Delete invalid UUID
|
||||||
|
self.assertEqual(FolderManager.delete(self.store, 'invalid-uuid'), FolderManager.INVALID_ID)
|
||||||
|
# Delete non-existent folder
|
||||||
|
self.assertEqual(FolderManager.delete(self.store, uuid.uuid4()), FolderManager.NO_SUCH_FOLDER)
|
||||||
|
# Delete non-root folder
|
||||||
|
folder = self.store.find(db.Folder, db.Folder.name == 'non-root').one()
|
||||||
|
self.assertEqual(FolderManager.delete(self.store, folder.id), FolderManager.NO_SUCH_FOLDER)
|
||||||
|
|
||||||
|
def test_delete_by_name(self):
|
||||||
|
# Delete existing folders
|
||||||
|
for name in ['media', 'music']:
|
||||||
|
self.assertEqual(FolderManager.delete_by_name(self.store, name), FolderManager.SUCCESS)
|
||||||
|
# Delete non-existent folder
|
||||||
|
self.assertEqual(FolderManager.delete_by_name(self.store, 'null'), FolderManager.NO_SUCH_FOLDER)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
96
tests/test_manager_user.py
Normal file
96
tests/test_manager_user.py
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# vim:fenc=utf-8
|
||||||
|
#
|
||||||
|
# This file is part of Supysonic.
|
||||||
|
# Supysonic is a Python implementation of the Subsonic server API.
|
||||||
|
#
|
||||||
|
# Copyright (C) 2013-2017 Alban 'spl0k' Féron
|
||||||
|
# 2017 Óscar García Amor
|
||||||
|
#
|
||||||
|
# Distributed under terms of the GNU AGPLv3 license.
|
||||||
|
|
||||||
|
from supysonic import db
|
||||||
|
from supysonic.managers.user import UserManager
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
class UserManagerTestCase(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
# Create an empty sqlite database in memory
|
||||||
|
self.store = db.get_store("sqlite:")
|
||||||
|
# Read schema from file
|
||||||
|
with open('schema/sqlite.sql') as sql:
|
||||||
|
schema = sql.read()
|
||||||
|
# Create tables on memory database
|
||||||
|
for command in schema.split(';'):
|
||||||
|
self.store.execute(command)
|
||||||
|
# Create some users
|
||||||
|
self.assertEqual(UserManager.add(self.store, 'alice', 'alice', 'test@example.com', True), UserManager.SUCCESS)
|
||||||
|
self.assertEqual(UserManager.add(self.store, 'bob', 'bob', 'bob@example.com', False), UserManager.SUCCESS)
|
||||||
|
self.assertEqual(UserManager.add(self.store, 'charlie', 'charlie', 'charlie@example.com', False), UserManager.SUCCESS)
|
||||||
|
|
||||||
|
def test_encrypt_password(self):
|
||||||
|
self.assertEqual(UserManager._UserManager__encrypt_password('password','salt'), ('59b3e8d637cf97edbe2384cf59cb7453dfe30789', 'salt'))
|
||||||
|
self.assertEqual(UserManager._UserManager__encrypt_password('pass-word','pepper'), ('d68c95a91ed7773aa57c7c044d2309a5bf1da2e7', 'pepper'))
|
||||||
|
|
||||||
|
def test_get_user(self):
|
||||||
|
# Get existing users
|
||||||
|
for name in ['alice', 'bob', 'charlie']:
|
||||||
|
user = self.store.find(db.User, db.User.name == name).one()
|
||||||
|
self.assertEqual(UserManager.get(self.store, user.id), (UserManager.SUCCESS, user))
|
||||||
|
# Get with invalid UUID
|
||||||
|
self.assertEqual(UserManager.get(self.store, 'invalid-uuid'), (UserManager.INVALID_ID, None))
|
||||||
|
# Non-existent user
|
||||||
|
self.assertEqual(UserManager.get(self.store, uuid.uuid4()), (UserManager.NO_SUCH_USER, None))
|
||||||
|
|
||||||
|
def test_add_user(self):
|
||||||
|
# Create duplicate
|
||||||
|
self.assertEqual(UserManager.add(self.store, 'alice', 'alice', 'test@example.com', True), UserManager.NAME_EXISTS)
|
||||||
|
|
||||||
|
def test_delete_user(self):
|
||||||
|
# Delete existing users
|
||||||
|
for name in ['alice', 'bob', 'charlie']:
|
||||||
|
user = self.store.find(db.User, db.User.name == name).one()
|
||||||
|
self.assertEqual(UserManager.delete(self.store, user.id), UserManager.SUCCESS)
|
||||||
|
# Delete invalid UUID
|
||||||
|
self.assertEqual(UserManager.delete(self.store, 'invalid-uuid'), UserManager.INVALID_ID)
|
||||||
|
# Delete non-existent user
|
||||||
|
self.assertEqual(UserManager.delete(self.store, uuid.uuid4()), UserManager.NO_SUCH_USER)
|
||||||
|
|
||||||
|
def test_try_auth(self):
|
||||||
|
# Test authentication
|
||||||
|
for name in ['alice', 'bob', 'charlie']:
|
||||||
|
user = self.store.find(db.User, db.User.name == name).one()
|
||||||
|
self.assertEqual(UserManager.try_auth(self.store, name, name), (UserManager.SUCCESS, user))
|
||||||
|
# Wrong password
|
||||||
|
self.assertEqual(UserManager.try_auth(self.store, name, 'bad'), (UserManager.WRONG_PASS, None))
|
||||||
|
# Non-existent user
|
||||||
|
self.assertEqual(UserManager.try_auth(self.store, 'null', 'null'), (UserManager.NO_SUCH_USER, None))
|
||||||
|
|
||||||
|
def test_change_password(self):
|
||||||
|
# With existing users
|
||||||
|
for name in ['alice', 'bob', 'charlie']:
|
||||||
|
user = self.store.find(db.User, db.User.name == name).one()
|
||||||
|
# God password
|
||||||
|
self.assertEqual(UserManager.change_password(self.store, user.id, name, 'newpass'), UserManager.SUCCESS)
|
||||||
|
self.assertEqual(UserManager.try_auth(self.store, name, 'newpass'), (UserManager.SUCCESS, user))
|
||||||
|
# Wrong password
|
||||||
|
self.assertEqual(UserManager.change_password(self.store, user.id, 'badpass', 'newpass'), UserManager.WRONG_PASS)
|
||||||
|
# With invalid UUID
|
||||||
|
self.assertEqual(UserManager.change_password(self.store, 'invalid-uuid', 'oldpass', 'newpass'), UserManager.INVALID_ID)
|
||||||
|
# Non-existent user
|
||||||
|
self.assertEqual(UserManager.change_password(self.store, uuid.uuid4(), 'oldpass', 'newpass'), UserManager.NO_SUCH_USER)
|
||||||
|
|
||||||
|
def test_change_password2(self):
|
||||||
|
# With existing users
|
||||||
|
for name in ['alice', 'bob', 'charlie']:
|
||||||
|
self.assertEqual(UserManager.change_password2(self.store, name, 'newpass'), UserManager.SUCCESS)
|
||||||
|
user = self.store.find(db.User, db.User.name == name).one()
|
||||||
|
self.assertEqual(UserManager.try_auth(self.store, name, 'newpass'), (UserManager.SUCCESS, user))
|
||||||
|
# Non-existent user
|
||||||
|
self.assertEqual(UserManager.change_password2(self.store, 'null', 'newpass'), UserManager.NO_SUCH_USER)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
Loading…
Reference in New Issue
Block a user